アセンブラを勉強すると、プログラムがマイコンの中でどう動いているのかイメージできるようになってきます。
組み込みに関わらずプログラミングをしている人全てに、一度はアセンブラのプログラムを読んで勉強してみることをオススメします!
今回はArduinoを使ってアセンブラを勉強してみたいと思います。
アセンブラを読む前に…
まだプログラミングを始めて間もない方は、この記事を読むと記号だらけでイメージがわきづらい…と思うかもしれません。
そのような方には、まずCPUがどのように動作しているかのイメージを持つために、以下の本を読んでみることをオススメ致します!
とても分かりやすくプログラムが動作する原理について解説されているので、プログラマ必読の本です。
アセンブラとは
アセンブラは、より機械がわかる言語で書かれたプログラミング言語で「低水準言語」といいます。
それに比べて、CやC++のような人間が理解しやすいプログラミング言語を「高水準言語」といいます。
CPUが実行するプログラムは「1」か「0」の数字の羅列による命令で動きますが、この命令を機械語といいます。
CやC++で書かれたソースコードはコンパイルしてオブジェクトファイルを作り、リンクして実行ファイルにすることで、機械が理解できる「1」「0」の機械語(実行ファイル)になります。
アセンブラはその「1」「0」の命令1つに対して1つ対応する形で名前を付けたもので、より機械語に近い状態でありながら人間が理解しやすい形で記述されているのです。
以下がアセンブラの例です。
fc 01 movw r30, r24
左側の「fc 01」は16進数で表現された機械語で、機械が処理する1単位の命令です。
右側の「movw r30, r24」は機械語「fc 01」に対応したアセンブリ言語です。
アセンブラを理解すると、普段CPUがどのようにプログラムを実行しているかが分かるので、効率的にプログラムを組むことができるようになります!
Arduinoのスケッチをアセンブラに変換
ではアセンブラへの理解を深めるため、Arduinoを使って、スケッチをアセンブラに変換(逆アセンブル)してみましょう。
今回は一番簡単なLEDを光らせるスケッチを逆アセンブルしてみたいと思います。
オブジェクトファイルの作成
まずはArduino IDEで「Blink」のスケッチ例を開いて、任意のフォルダに保存します。
次に、「スケッチ」から「コンパイル済みのバイナリをエクスポート」を選択します。
すると、「Blink」のプロジェクトを保存したフォルダに「build」フォルダが新しく作られます。中をみると「Blink.ino.elf」というオブジェクトファイルが作られているはずです。
逆アセンブル
「Blink.ino.elf」はエルフ形式のオブジェクトファイルで、コンパイラがソースコードを処理した結果が書き込まれています。
このファイルを逆アセンブルすることにより、アセンブラの状態に戻します。
まずArduino IDEをインストールしたときに、同時にインストールされている「objdump.exe」というファイルを探します。
objdumpはUnixのコマンドで、オブジェクトファイルの中を見るときに使います。
私の環境では以下のパスに保存されていました。
C:\Users\ユーザー名\AppData\Local\Arduino15\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino7\avr\bin\objdump.exe
このファイルを「Blink.ino.elf」と同じ階層にコピーしておきます。
コマンドラインを開いてカレントディレクトリをこのフォルダに移動します。
cdの後ろは自分の環境のパスを入力してください。
cd C:\***\Blink\build\arduino.avr.uno
次に、以下のコマンドで逆アセンブルします。
objdump.exe -d Blink.ino.elf > assembler.txt
これで、先ほどのフォルダに「assembler.txt」というファイルが作られていれば逆アセンブル成功です。
余談 機械が理解するhexファイル
「Blink.ino.elf」が作られたフォルダに、同時に「Blink.ino.hex」というファイルが作られているはずです。
このファイルは実際にArduinoの中に書き込まれるファイルで、機械が理解するもの(16進数で書かれた機械語)になります。
フォーマットはWikipediaで調べると出てきますので、アセンブラのファイルと見比べてみてください。機械語まで勉強することができるのでオススメです!
アセンブリ言語を読んでみる
次に作成したアセンブラファイルの中身を見ながら、アセンブラの仕組みについてご紹介していきます。
アセンブラの基本
以下が、もともとのスケッチとアセンブラのloop関数の中身を比較したものです。
void loop() {
digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level)
delay(1000); // wait for a second
digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW
delay(1000); // wait for a second
}
Blink.ino:33
37a: 81 e0 ldi r24, 0x01 ; 1
37c: 0e 94 70 00 call 0xe0 ; 0xe0 <digitalWrite.constprop.0>
Blink.ino:34
380: 0e 94 dd 00 call 0x1ba ; 0x1ba <delay.constprop.1>
Blink.ino:35
384: 80 e0 ldi r24, 0x00 ; 0
386: 0e 94 70 00 call 0xe0 ; 0xe0 <digitalWrite.constprop.0>
Blink.ino:36
38a: 0e 94 dd 00 call 0x1ba ; 0x1ba <delay.constprop.1>
Arduinoに採用されているAVRマイコンのオペコードは以下の資料(AVR命令一式手引書)にまとめられているので、参考にしてください。
https://avr.jp/user/DS/PDF/AVRinst.pdf
解説
digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level)
37a: 81 e0 ldi r24, 0x01 ; 1
37c: 0e 94 70 00 call 0xe0 ; 0xe0 <digitalWrite.constprop.0>
まず、スケッチの先頭1行はアセンブラの2行で表現されています。
ldi
1行目のldiで、関数digitalWriteの引数HIGH(1)を汎用作業レジスタに保存しています。
これは、digitalWriteの処理に飛んだあとに(1)を使えるようにするための処理です。
AVR命令一式手引書によると、ldiは以下の仕様です。
即値定数を汎用作業レジスタに保存します。
機械語のd0~d3には汎用作業レジスタの「r24」が入ります。
ATmega328Pの仕様書によると「r24」のアドレスは下図の通り16進数で「0x18」なので2進数で「00011000」です。
d0~d3には下位ビットの「1000」が入ります。
(5ビット目は1固定のため、汎用作業レジスタはR16~R31までしか使えません。)
次にK0~K7には16進数で「0x01」がオペランドで指定されているので、2進数で「00000001」が入ります。
結果は「1110 0000 1000 0001」となり、16進数で「0xe0 81」、リトルエンディアンで「0x81 e0」なので、機械語の命令と一致します。
call
2行目のcallでdigitalWriteの処理が格納されたアドレスへ飛びます。
callは以下の仕様です。
まず、CALLの次の命令への復帰アドレスをスタック上に格納します。
これは、関数「digitalWrite」の次の行の関数「delay」の命令(delayの命令が格納されたアドレスをcallする)が格納されたアドレスをスタックへ保存することになります。
関数を処理しに行ったあと、元の場所に戻ってくるために必要です。
そして、プログラムカウンタに指定したアドレス「0xe0」を格納します。
アドレス「0xe0」には関数「digitalWrite」の先頭処理が格納されています。
機械語のk0~k21には「0xe0」が入ります…と言いたいところですが、ここで1つ注意点があります。
ATmega328Pのプログラムメモリは32KByteありますが、プログラムカウンタは14ビットしかありません。
14ビットで表せるアドレスは16KByteなので、プログラムメモリの半分しかありません。
よく使われるCPUではアドレス1wordに対して8ビットで構成されますが、ATmega328Pはアドレス1wordに対して16ビットで構成されます。
よって、アドレス「0xe0」にアクセスする場合、半分の「0x70」にアクセスする必要があります。
よってk0~k21には「0x70」が入るので、機械語を2進数で表すと「1001 0100 0000 1110 0000 0000 0111 0000」、16進数で表すと「940e 0070」となります。
ここで、エンディアンは以下の仕様のようです。
call命令のように32ビット(2word)の命令はビッグエンディアンなのでそのままの順番ですが、16ビット(1word)の中はリトルエンディアンとなるので配置が入れ替わります。
この規則で「940E 0070」を入れ替えると「0e94 7000」となり、機械語の命令と一致します。
同じ要領で、以降のloop関数の処理が進んでいきます。
まとめ
ArduinoのBlinkスケッチを逆アセンブルして読んでみることで、CPUがどのように動作しているのかイメージすることができました。
CやC++、もしくはPythonのような高水準言語で簡単にプログラミングができるようになったのはとても便利なことですが、その先でどのようなことが行われているのか、を知ることはとても大切です。
ぜひ、みなさんもアセンブラを読んでCPUの動作する原理を勉強してみてください。
コメント