アセンブラ入門-Arduinoでアセンブラを勉強する

Arduinoのスケッチを逆アセンブルして、アセンブラを勉強する Arduino
スポンサーリンク

アセンブラを勉強すると、プログラムがマイコンの中でどう動いているのかイメージできるようになってきます。

組み込みに関わらずプログラミングをしている人全てに、一度はアセンブラのプログラムを読んで勉強してみることをオススメします!

今回は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」のスケッチ例を開いて、任意のフォルダに保存します。

LEDを光らせるスケッチの例

次に、「スケッチ」から「コンパイル済みのバイナリをエクスポート」を選択します。

コンパイル済みバイナリをエクスポート

すると、「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」と同じ階層にコピーしておきます。

objdumpを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>
ファイルの読み方
  • 「Blink.ino:**」はもともとのスケッチの何行目に当たるか、を示しています。
  • 先頭の「37a」「37c」・・・は命令を書き込む先のアドレスを16進数で示しています。
    hexファイルの該当アドレスを参照すると、同じ命令が書き込まれているはずです。
  • 「81 e0」「0e 94 70 00」・・・は命令を表しています。
    機械が理解できる状態です。
  • 「ldi」「call」・・・はオペコードといい、機械の命令を人間が分かる単語に置き換えたものです。
  • 「r24, 0x01」「0xe0」・・・はオペコードに対する引数でオペランドといいます。
  • 「;」の後ろはコメントです。
  • オペコード、オペランドを繋げた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は以下の仕様です。
即値定数を汎用作業レジスタに保存します。

オペコード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の解説

まず、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」となります。

ここで、エンディアンは以下の仕様のようです。

ATmega328Pのエンディアンの仕様

call命令のように32ビット(2word)の命令はビッグエンディアンなのでそのままの順番ですが、16ビット(1word)の中はリトルエンディアンとなるので配置が入れ替わります。
この規則で「940E 0070」を入れ替えると「0e94 7000」となり、機械語の命令と一致します。

同じ要領で、以降のloop関数の処理が進んでいきます。

まとめ

ArduinoのBlinkスケッチを逆アセンブルして読んでみることで、CPUがどのように動作しているのかイメージすることができました。

CやC++、もしくはPythonのような高水準言語で簡単にプログラミングができるようになったのはとても便利なことですが、その先でどのようなことが行われているのか、を知ることはとても大切です。

ぜひ、みなさんもアセンブラを読んでCPUの動作する原理を勉強してみてください。

Arduino
スポンサーリンク
tsubablog

メーカーで組み込みプログラマーとして勤務しているサラリーマンです。
プログラミングの楽しさについて発信しています。

業務で扱っている言語はC、C++です。

tsubablogをフォローする
つばブログ

コメント

タイトルとURLをコピーしました