アセンブリ言語プログラミング
これまで見てきたように,機械語プログラムとは命令を表す2進数を並べた列である。機械にとってのプログラムはその2進数列だが,それを人間が読み書きするのは非効率的なので,アセンブリ表記という読みやすい記法も併用される。
機械語プログラムの例:
b8 01 00 00 00 bb 7b 00 00 00 cd 80
アセンブリ表記:
mov eax, 1
mov ebx, 123
int 0x80
これまでの授業では,アセンブリ表記でプログラムの下書きを書いた後,手作業で機械語プログラムに翻訳していたが,この翻訳作業はかなり機械的で,自動化できそうだということを実感したと思う。 実際,「機械語命令列を2進数や16進数で直接書く」ことは現実にはほとんどなく,アセンブラ (assembler) を使って翻訳するのが普通だ。 本科目もこれ以降は,アセンブリ表記のプログラムを記述し,それをアセンブラで翻訳して実行可能ファイルを作る。 以降,記法としてのアセンブリ表記のことをアセンブリ言語 (assembly language) と呼ぶ。
人力で翻訳する場合は細かい構文規則は気にしなくてもよかったが,機械を使って翻訳するためには機械が受け付ける記法をある程度細かく学ぶ必要がある。 その代わり,機械語プログラムに翻訳する手間は大幅に減り,前回までの人力アセンブルに比べてずっと楽になることが実感できるだろう。
演習1.2-1 以下のアセンブリ言語プログラムを打鍵入力し,hello.s
というファイル名で保存しなさい(セミコロン (;
) より右はコメントなので,打鍵入力が億劫なら省略してもよい)。
その後,下記の手順でアセンブルし,実行しなさい。
section .text
global _start
_start:
mov eax, 4 ; writeのシステムコール番号
mov ebx, 1 ; 標準出力
mov ecx, msg ; 文字列の開始番地
mov edx, msglen ; 文字列の長さ
int 0x80
mov eax, 1 ; exitのシステムコール番号
mov ebx, 0 ; 終了コード
int 0x80
msg: db "I'm fine.", 0x0a
msglen: equ $ - msg
$ nasm -felf hello.s -- アセンブルし,結果をhello.oに出力
$ ld -m elf_i386 hello.o -- ELFファイルa.outを生成
$ ./a.out -- 実行
I'm fine.
上記のプログラムは演習1.1-2のプログラムと同一だ。
機械語プログラムを16進数列として打鍵入力する代わりに,アセンブリ言語プログラムを打鍵入力し,アセンブラ nasm
(及びリンカ ld
)を使って実行可能ファイルに変換している。
(ELFファイル中の各種付加情報を除けば)得られる機械語プログラム自体は演習1.1-2と同じだ。
コマンド入力を楽にするための設定
今後,nasm
コマンドと ld
コマンドを頻繁に使うが,上記のようにオプション引数が多いと面倒だしわかりにくい。シェルの設定に追記して,オプション引数無しで実行できるようにしよう。
~/.bashrc
の末尾に以下の3行を追加する(~/.bashrc
がなければ新規作成する)。
alias nasm='nasm -felf'
export LDEMULATION=elf_i386
export EDITOR="emacs -nw --eval '(global-unset-key \"\C-z\")'"
(最後の1行はGitのための設定で,nasm
やld
とは無関係だが,ついでなのでここで追記している。少し長くて複雑なので,コピー&ペーストで書き写した方がよいと思う。)
(EDITORを設定しないと nano
というエディタが使われる。nano
を使うのが嫌でなければそれでもよい。
vi
またはvim
に慣れている人は export EDITOR=vi
と設定してもよい。)
以降,新しく端末を起動すると,その端末に設定が反映される。
alias
コマンドを引数無しで実行してみて,alias nasm='nasm -felf'
という行が表示されたら,設定がうまくいっている。
この設定を行った後では,上記のアセンブル手順は次のように簡潔になる。
$ nasm hello.s -- アセンブルし,結果をhello.oに出力
$ ld hello.o -- a.outを生成
$ ./a.out -- 実行
I'm fine.
最小のアセンブリ言語プログラム
以下は「最小」のアセンブリ言語プログラムだ。 中身は演習1.1-4のプログラムと同一で,exitシステムコールを実行するだけのプログラムだ。
演習1.2-2
このプログラムを打鍵入力し,123.s
というファイル名で保存しなさい。
演習1.2-1と同様にアセンブル及びリンクを行って実行可能ファイルを作成し,実行しなさい。
また,終了コードを確認しなさい。
section .text
global _start
_start:
mov eax, 1 ; exitのシステムコール番号
mov ebx, 123 ; 終了コード
int 0x80 ; システムコール
$ nasm 123.s -- アセンブル
$ ld 123.o -- a.outを生成
$ ./a.out -- 実行
$ echo $? -- 直前のコマンドの終了コードを表示
123
上記プログラムの最初の3行は「決まり文句」だ。
section .text
は,以降が(データではなく)機械語命令であることを表す。global _start
は,ラベル_start
を外部から参照可能にするための指示。
3行目の _start:
のように行の左端に書かれた「名前:
」はラベルと言い,プログラム中の「位置」(機械語コードに翻訳したときの番地)を表す。上記プログラムの場合,_start
はプログラムの先頭番地を表している。
_start: mov eax, 1
のように,ラベルと命令を同じ行に書いてもよい。
特に指定がなければ _start
という名前のラベルがプログラム実行開始位置となる。(これはLinuxのld
コマンドの仕様。先頭が実行開始位置でなくてもよい。逆に,先頭が実行開始位置でも開始位置の明示が必要。)
空白の量は自由だが,「ラベルは字下げしない。それ以外は1レベル字下げする」というスタイルで記述するのが慣習だ。
;
から行末まではコメントとして扱われる。
上の例では説明を兼ねて細かくコメントを書いてある。「すべての行にコメントを書く」ほど細かく書く必要はないが,後で「この行はどういう意味だろう?」と思いそうな箇所にはコメントを付けておくべきだ。
機械語・アセンブリ言語は1命令の粒度が小さいので,命令だけでは意味がわかりにくくなりがちだ。
プログラムの先頭に「何をするプログラムか」を書くのはよい作法だ。
; 123を終了コードとして出力するあまり意味のないプログラム
section .text
global _start
_start:
mov eax, 1 ; exitのシステムコール番号
mov ebx, 123 ; 終了コード
int 0x80 ; システムコール
0x
は16進数を表す接頭辞だ(CやJavaと同じ)。1
や123
のような数字のみの列は10進数を表す。(0x80
の代わりに 128
と書いても,123
の代わりに 0x7b
と書いても同じ。機械語命令に変換するときにすべて2進数化される。「機械が扱う情報は必ずビット列である」ことを忘れずに。)
参考:ELFファイルに対する逆アセンブル
前章では ndisasm
を使って逆アセンブル(機械語プログラムをアセンブリ表記に変換)したが,ELFファイル(.oファイルやa.out)の逆アセンブルは objdump
コマンドでもできる。
objdump
はELFファイル中の付加情報を調べて機械語プログラムの部分だけを翻訳してくれるので使いやすい。
(前章のようにelf.hex
と連結してELFファイルを作った場合は必ず機械語プログラムはファイルの先頭から96バイト目以降に格納されるが,nasm
やld
を使ってELFファイルを作る場合,機械語プログラムがファイルのどの位置に格納されるか事前にわからない。)
$ nasm 123.s
$ ld 123.o
$ objdump -M intel -d a.out
-- 中略 --
セクション .text の逆アセンブル:
08049000 <_start>:
8049000: b8 01 00 00 00 mov eax,0x1
8049005: bb 7b 00 00 00 mov ebx,0x7b
804900a: cd 80 int 0x80
(-d
は逆アセンブル (disassemble) の意味。
-M intel
は「Intelの表記法を使う」という意味。指定しない場合はAT&T表記と呼ばれる記法で出力される。)
加算命令
次は 123 + 45 を計算するプログラムを作ろう。以下はその例だ。
演習1.2-3
123.s
を下記のように変更して,演習1.2-2と同様にアセンブル及びリンクを行って実行可能ファイルを作成し,実行しなさい。
また,終了コードが 123 + 45 に等しいことを確認しなさい。
section .text
global _start
_start:
mov ebx, 123 ; ebx = 123
add ebx, 45 ; ebx = ebx + 45
mov eax, 1 ; exitのシステムコール番号
int 0x80 ; システムコール
$ nasm 123.s
$ ld 123.o
$ ./a.out
$ echo $?
168
i386のadd
が2引数命令であることはすでに習った。(ミクロンのadd
は3引数命令だった。第II部以降で扱うARMのadd
もミクロンと同じ3引数命令である。)
ミクロンと違い,add
の第2引数はレジスタでも即値でもよい。
i386ではmov
もadd
も,「第2オペランドの値を使って第1オペランドの値を変化させる」命令だ。その意味で,第2オペランドをソースオペランド (source operand),第1オペランドをディスティネーションオペランド (destination operand)(目的オペランド,宛先オペランド)とも言う。
i386の主な演算命令
以下は,i386の主な演算命令の一覧だ(本科目では当分の間使用しない命令も含んでいる)。
減算をする命令が sub
(subtract) であることはすでに習った。
add
とsub
を使って 123 + 45 − 67 + 8 − 9 を計算するプログラムを作ってみよう。
演習1.2-4 「Gitを使った開発作業」を読んで,ローカルリポジトリの作成とgitの設定を行いなさい。
演習1.2-5 123 + 45 − 67 + 8 − 9 を計算して結果を出力するアセンブリ言語プログラムを作り,リモートリポジトリにpushしなさい。 ソースファイルは,サブディレクトリ chap2 の中に 123.s という名前で作成すること。