i386機械語プログラミング
最小のプログラムの中身
演習1.1-4で作成した最小のプログラムの16進数表記とアセンブリ表記は以下であった。
b8 01 00 00 00
bb 7b 00 00 00
cd 80
mov eax, 1
mov ebx, 123
int 0x80
使っている命令は「mov
レジスタ, 即値」と「int 0x80
」の2種類だ。
これらの命令の機械語コードは以下の表のようになる。
「reg」はレジスタ番号(後述)を表し,「imm32」「imm8」はそれぞれ32ビット及び8ビットの即値を表す。(表中の機械語コード欄では16進数の接頭辞 0x を省略している。)
命令 | 機械語コード |
---|---|
mov reg, imm32 |
(b8 + reg) imm32 |
int imm8 |
cd imm8 |
「mov
レジスタ, 32ビット即値」の機械語コードは,1バイト目が2進法で
10111xxx
で,下位3ビットが第1オペランド(レジスタ番号)である。
続く4バイトが32ビットの即値(ただしリトルエンディアン)。
レジスタ番号は下表の通り。
レジスタ | 番号 | 2進 |
---|---|---|
EAX | 0 | 000 |
EBX | 3 | 011 |
ECX | 1 | 001 |
EDX | 2 | 010 |
ESI | 6 | 110 |
EDI | 7 | 111 |
ESP | 4 | 100 |
EBP | 5 | 101 |
以上から機械語コードが下記のように決まる(EBXに代入する数123は16進法では 0x7b。リトルエンディアンなので,最下位バイトが先頭になる)。
b8 01 00 00 00
bb 7b 00 00 00
cd 80
加減算命令
今度は 123 + 45 − 6 を計算して出力するプログラムを作ってみよう。
アセンブリ表記の下書きは以下のようになる。
mov ebx, 123
mov eax, 45
add ebx, eax
mov eax, 6
sub ebx, eax
mov eax, 1
int 0x80
最後にint 0x80
を実行する時点でEAXに1,EBXに出力したい値が格納されていればよい。
計算自体は第0部のときと同様だ(EAXとEBXだけで計算しているが,ECXやほかのレジスタを使っても構わない)。
add
とsub
はそれぞれ第0部と同じく加算・減算命令だが,引数にちがいがある。
第0部のミクロンのadd
・sub
は3引数命令だが,i386のadd
・sub
は2引数命令である。
// ミクロンの場合
add xx, yy, zz // xx = yy + zz
// i386の場合
add xx, yy // xx += yy
つまり,i386では「現在の xx
の値を yy
だけ増やす(または減らす)」という形の加算・減算のみ行える。
レジスタ同士の加減算(及びレジスタ間のmov
)の機械語コードは下表の通り。
「* 8」は「3ビット左にシフトする」という意味だ。つまり,「reg2 * 8 + reg1」は2つのレジスタ番号を左右に並べた6ビットの値を表す
(例えば add
reg1, reg2 の場合,第2バイトは2進法で 11yyyxxx
であり,yyyとxxxがそれぞれレジスタ番号である)。
命令 | 機械語コード |
---|---|
add reg1, reg2 |
01 (c0 + reg2 * 8 + reg1) |
sub reg1, reg2 |
29 (c0 + reg2 * 8 + reg1) |
cmp reg1, reg2 |
39 (c0 + reg2 * 8 + reg1) |
mov reg1, reg2 |
89 (c0 + reg2 * 8 + reg1) |
この表に従って上述のアセンブリ表記を16進数列に変換すると下記のようになる。(45 = 0x2d。add ebx, eax
は 01 c3
,sub ebx, eax
は 29 c3
。)
bb 7b 00 00 00
b8 2d 00 00 00
01 c3
b8 06 00 00 00
29 c3
b8 01 00 00 00
cd 80
演習1.1-5
123.hexの中身を上記のように書き換えて,実行可能ファイルを生成し,実行してみなさい。echo $?
を実行して終了コードを確認しなさい。
また,ndisasmを使ってアセンブリ表記を確認しなさい。
$ cat elf.hex 123.hex | xxd -r -p > a.out
$ chmod +x a.out -- すでに実行許可が付いていれば不要
$ ./a.out
$ echo $?
162
$ ndisasm -b32 -e96 a.out
00000000 BB7B000000 mov ebx,0x7b
00000005 B82D000000 mov eax,0x2d
0000000A 01C3 add ebx,eax
0000000C B806000000 mov eax,0x6
00000011 29C3 sub ebx,eax
00000013 B801000000 mov eax,0x1
00000018 CD80 int 0x80
補足:即値の加算・減算
なお,i386では,レジスタ同士の加算・減算だけでなく,即値を加算・減算することもできる。 それを使えば,上記のプログラムは以下のように少しだけ短くなる。
bb 7b 00 00 00
81 c3 2d 00 00 00
81 eb 06 00 00 00
b8 01 00 00 00
cd 80
アセンブリ表記:
mov ebx, 123
add ebx, 45
sub ebx, 6
mov eax, 1
int 0x80
第2引数が即値である
add
, sub
, cmp
の機械語コードは以下の表の通り。
先頭の13ビット(1バイトと5ビット)がオペコードということになる。
命令 | 機械語コード |
---|---|
add reg, imm32 |
81 (c0 + reg) imm32 |
sub reg, imm32 |
81 (e8 + reg) imm32 |
cmp reg, imm32 |
81 (f8 + reg) imm32 |
(ニーモニックは同じ add
や sub
でも,引数の種類が異なる命令は別のオペコードを持つ別の命令である。)
メモリからの読み出し
メモリから値を読み出すプログラムを考える。
第0部のミクロンでは,メモリから値を読み出す命令は ldr
(load register) だった。
i386も同様のことができるが,アセンブリ表記上は,ニーモニック mov
の引数に角括弧 []
を付けることでメモリへのアクセスであることを表す。
例えば mov ebx, [ecx]
は「ECXが指す番地に格納されているダブルワードをEBXに読み出す」命令であり,ミクロンで言えば ldr b, c
に当たる。
以下は,0x08048076番地のダブルワードと0x0804807a番地のダブルワードの和を計算するプログラムである。
アセンブリ表記:
mov ecx, 0x08048076
mov ebx, [ecx]
add ecx, 4
add ebx, [ecx]
mov eax, 1
int 0x80
dd 123, 57
16進数列:
b9 76 80 04 08
8b 19
81 c1 04 00 00 00
03 19
b8 01 00 00 00
cd 80
7b 00 00 00 39 00 00 00
i386では,加減算命令の引数にもメモリへのアクセスを指定できる。
add ebx, [ecx]
は,「ECXが指す番地に格納されているダブルワードをEBXに加算する」命令である。
主記憶領域を第2オペランドとする mov
, add
, sub
, cmp
の機械語コードは以下の表のようになる(第2バイト中の reg1, reg2 の順序に注意)。
命令 | 機械語コード |
---|---|
mov reg1, [reg2] |
8b (00 + reg1 * 8 + reg2) |
add reg1, [reg2] |
03 (00 + reg1 * 8 + reg2) |
sub reg1, [reg2] |
2b (00 + reg1 * 8 + reg2) |
cmp reg1, [reg2] |
3b (00 + reg1 * 8 + reg2) |
なお,アセンブリ表記の最後の dd
は Define Doubleword の略で,「引数に書いたダブルワード値をそのままその位置に置く」ことを表す。つまり,この行は16進数表記に変換すると
7b 00 00 00 39 00 00 00
となる。(123 = 0x7b,57 = 0x39。それぞれリトルエンディアンのダブルワード。)
dd
は機械語命令ではなく,アセンブリ表記を読む読者やソフトウェアに対して「ここにデータ値をそのまま置け」という指示を行う記述である。
このような指示を疑似命令 (pseudo instruction) と言う。
最初の命令でECXに代入している 0x08048076 は,この dd
で置かれたダブルワード列(7b 00 00 00 ...
)の先頭番地である。
elf.hex 中の96バイトと連結して実行可能ファイルを作った場合,機械語プログラムは 0x08048060 番地を先頭にして配置される設定になっている。
このプログラムは機械語命令部分が22バイト(= 0x16バイト)あるので,ダブルワード列の先頭番地は 0x08048076 となる。
演習1.1-6 上記の16進数列を mem.hex というファイルに保存して,実行可能ファイルを生成し,実行してみなさい。終了コードが 123 + 57 の値と等しいことを確認しなさい。 また,ndisasmを使ってアセンブリ表記を確認しなさい。
$ cat elf.hex mem.hex | xxd -r -p > a.out
$ chmod +x a.out -- すでに実行許可が付いていれば不要
$ ./a.out
$ echo $?
180
$ ndisasm -b32 -e96 a.out -- 逆アセンブル
-- 略 --
メモリ中のダブルワード
第0部では扱う値が基本的にバイト(8ビット)であった。 それに対し,i386は32ビットCPUであり,扱う値は基本的にダブルワード(32ビット = 4バイト)である。 一方,第0部と同様にメモリは1バイトごとのセルに分かれており,1バイトごとに番地が付いている。
mov ebx, [ecx]
を実行すると,メモリから4バイト(= 32ビット)読み出され,それがEBXに代入される。正確には,ECXが指す番地から始まる4バイトが読み出されてEBXに代入される。
「xx
番地のダブルワード」と言った場合,それは「xx
〜 xx
+ 3 番地に格納された4バイトを,低い番地の値を下位桁として連結したダブルワード」という意味になる。
また,xx
番地を先頭に複数のダブルワードが格納されている場合,2つ目のダブルワードの番地は xx
+ 4,3つ目のダブルワードの番地は xx
+ 8,…となる。
演習1.1-6のプログラムの3命令目でECXを 4 増やしているのは,ECXが2つ目のダブルワードの番地を持つようにするためである。
補足:特定番地へのデータの配置
演習1.1-6の mem.hex では,機械語命令列の直後にデータを配置し,機械語命令列のバイト数からデータの開始番地を求めていた。 一方,第0部では,例えば「0xf0 番地の値に100加えたものを 0xf1 番地に書き込む」等のように,データを配置する番地を予め決めていた。 そうすれば,機械語命令列の長さが変わっても,プログラム中のデータの番地を書いた箇所をいちいち変更しなくて済む。
演習1.1-6の mem.hex を改造し,データの開始番地が 0x08048200 番地になるようにしてみよう。
1命令目でECXに代入する値を 0x08048200 にし,データ値(7b 00 00 00 39 00 00 00
)が mem.hex の先頭から 416 バイト後(416 = 0x1a0)になるようにすればよい。
ただ,hexファイルに数百個も 00
を書くのは莫迦らしいので,xxd
コマンドの機能を使って同じ効果を得るようにする。
(1) まず,mem.hex には機械語命令列のみ記述し,それとは別にデータ部のみ記述したhexファイル data.hex を作る。
mem.hex:
b9 00 82 04 08
8b 19
81 c1 04 00 00 00
03 19
b8 01 00 00 00
cd 80
data.hex:
7b 00 00 00 39 00 00 00
(2) elf.hex と mem.hex のみ使って実行可能ファイル a.out を作った後,xxd
コマンドの -s
オプションを使って,a.out の先頭から 0x200 バイト目に data.hex の変換結果を書き込む。
$ cat elf.hex mem.hex | xxd -r -p > a.out
$ xxd -r -p -s0x200 data.hex a.out -- a.out中の0x200バイト目に書き込み
(xxd -g1 a.out
を実行して a.out の中身を確認してみなさい。elf.hex, mem.hex の内容の後,0 が続き,0x200 バイト目に data.hex の内容が格納されていることがわかる。)
演習1.1-7 上記の手順を実行して実行可能ファイルを作り,実行しなさい。 終了コードが演習1.1-6のプログラムと同じであることを確認しなさい。
練習問題
演習1.1-8
以下の各プログラムを作成し,プログラムを格納したhexファイル(elf.hex
に当たる部分は含まない)を提出しなさい。
提出ファイル名は各小問で指定する。異なる名前のファイルは提出できないので注意。
提出方法は後述。
期限は別途指示する。
小問4はオプション課題である。
各小問とも,0x08048200 番地以降のメモリの値を変更しても正しく動作するようにすること。 (提出ファイルには機械語命令列のみ含めればよい。)
提出前に後述の検査プログラムを使って検査すること。検査に合格しない提出物は提出できない。
- 123 + 45 − 67 + 8 − 9 を計算し,結果を終了コードとして出力するプログラムを作成しなさい。(提出ファイル名:
123.hex
) - 0x08048200 番地に格納されているダブルワード値に 100 (= 0x64) を加えた値を終了コードとして出力するプログラムを作成しなさい。(提出ファイル名:
add100.hex
) - 0x08048200 番地を先頭に連続して格納されている3つのダブルワードの合計を終了コードとして出力するプログラムを作成しなさい。(3つのダブルワードの和をEBXに格納してexitシステムコールを呼び出せばよい。終了コードとして観測できるのはその下位8ビットとなる。)
例えば,0x08048200 番地を先頭に 100, 101, 102 (バイト列
64 00 00 00 65 00 00 00 66 00 00 00
)が格納されている場合,終了コードとして 47 (= 303 − 256) が出力されればよい。(提出ファイル名:add3mem.hex
) - (オプション課題)0x08048200 番地を先頭に連続して格納されている2つのダブルワードをそれぞれ x, y とし,x 番地及び y 番地に格納されているダブルワードをそれぞれ u, w とする。u − w の値を終了コードとして出力するプログラムを作成しなさい。
例えば,0x08048200 番地を先頭に 0x08048210, 0x08048208, 100, 102, 201(バイト列
10 82 04 08 08 82 04 08 64 00 00 00 66 00 00 00 c9 00 00 00
)が格納されている場合,終了コードとして 101 が出力されればよい(この場合,u = 201,w = 100 である)。(提出ファイル名:sub2mem.hex
)
検査プログラム & 提出方法
以下のコマンドで検査と提出を行える.
$ ~y-takata/Public/pl2test118 123.hex
$ ~y-takata/Public/pl2test118 add100.hex
$ ~y-takata/Public/pl2test118 add3mem.hex
$ ~y-takata/Public/pl2test118 sub2mem.hex
$ ~y-takata/Public/pl2submit118 123.hex add100.hex
$ ~y-takata/Public/pl2submit118 add3mem.hex sub2mem.hex
付録:機械語コード一覧
この章で扱った機械語コードをまとめて再掲する。 以下の表の reg はすべて32ビットレジスタである。
命令 | 機械語コード |
---|---|
mov reg, imm32 |
(b8 + reg) imm32 |
mov reg1, reg2 |
89 (c0 + reg2 * 8 + reg1) |
mov reg1, [reg2] |
8b (00 + reg1 * 8 + reg2) |
add reg, imm32 |
81 (c0 + reg) imm32 |
add reg1, reg2 |
01 (c0 + reg2 * 8 + reg1) |
add reg1, [reg2] |
03 (00 + reg1 * 8 + reg2) |
sub reg, imm32 |
81 (e8 + reg) imm32 |
sub reg1, reg2 |
29 (c0 + reg2 * 8 + reg1) |
sub reg1, [reg2] |
2b (00 + reg1 * 8 + reg2) |
cmp reg, imm32 |
81 (f8 + reg) imm32 |
cmp reg1, reg2 |
39 (c0 + reg2 * 8 + reg1) |
cmp reg1, [reg2] |
3b (00 + reg1 * 8 + reg2) |
int imm8 |
cd imm8 |
参考:i386の命令フォーマット
i386のマニュアル(利用者向け説明書)によると,i386の機械語命令は下図のようなバイト列になっている。
- 参考:i386の命令フォーマット (『Intel 64 and IA-32 Architectures Software Developer Manual』Volume 2A, 2章「Instruction Format」Figure 2-1を参考に作成)
このうち,「Instruction Prefixes」「SIB」「Displacement」は今回取り上げた命令には現れない。 今回取り上げた命令はいずれも「Opcode」「ModR/M」「immediate」のみから成る(ModR/M や immediate がない命令もある)。また,今回取り上げた命令の「Opcode」はいずれも1バイトである。
「ModR/M」は「Reg」と「R/M」の2つのレジスタ番号を格納するが,「Mod」部の値によって「R/M」の意味が変わる。
- Mod = 11 なら,Reg も R/M もレジスタ
- Mod = 00 なら,R/M はレジスタ間接指定(
[reg]
) - Mod = 10 なら,R/M はディスプレースメント付きレジスタ間接指定(
[reg + disp32]
)
その他の部分:
- 「Instruction Prefix」は,直後の命令の動作を変更する特殊命令。本科目では使わない。
- 「SIB」は Scale-Index-Base の意味で,
[reg1 * 倍率 + reg2 (+ disp)]
の形の番地指定で使う。Scale = 倍率,Index = 倍率を掛けるレジスタ(何番目かを指すレジスタ),Base = 開始番地を指すレジスタである。 - 「Displacement」は名前の通り。