アセンブリは命令を1行ずつ書く
アセンブリとは、CPU が実行する命令を、人が読める短い言葉で書いたものです。1行が1つの命令で、読み方の型は数えるほどしかありません。
この読み方が分かれば、ソースのないプログラムでも、どこで何を比べているかが見えてきます。
このレッスンのゴール
メモ
x86-64 のアセンブリの数行を読んで、何をするか言える。
重要な言葉
| ことば | いみ | れい |
|---|---|---|
| レジスタ | CPU の中にある、小さな入れ物 | eax |
| 命令 | CPU への1つの指示 | add eax, 5 |
| オペランド | 命令が使う値や入れ物 | |
| フラグ | 直前の比較の結果を残す、CPU の印 | |
| 条件ジャンプ | フラグを見て、別の行へ飛ぶ命令 | jne |
| 戻り値 | 関数が呼んだ側に返す値。普通は rax に入る |
学生が入力するセルは、このプレビューでは空白です。
レジスタは CPU の中の入れ物
計算の途中の値は、メモリでなく、レジスタに置かれます。x86-64 のレジスタは64ビットで、名前は rax のように r で始まります。
下位の32ビットだけを使うときは、eax のように e で始めて呼びます。32ビットの整数は、ほとんど e の名前で現れます。
よく見るレジスタの役目
| レジスタ | 役目 |
|---|---|
| rdi、rsi | 関数の1番目と2番目の引数 |
| rax、eax | 計算と、関数の戻り値 |
| rsp | スタックの先頭の位置 |
| rbp | いまの関数の土台の位置 |
| rip | 次に実行する命令の位置 |
学生が入力するセルは、このプレビューでは空白です。
mov、add、cmp、jne が基本
命令は、名前と、オペランドからできています。Intel 形式では、左が行き先で、右が元です。mov eax, 5 は、eax に 5 を入れます。
cmp は2つを比べて、結果をフラグに残します。jne は、等しくなかったときだけ、別の行へ飛びます。
基本の命令と意味
| 命令 | 意味 |
|---|---|
| mov eax, 5 | eax に 5 を入れる |
| add eax, 3 | eax に 3 を足す |
| cmp eax, 12 | eax と 12 を比べ、結果をフラグに残す |
| jne 行き先 | 等しくなければ、行き先へ飛ぶ |
| call 関数 | 関数を呼ぶ |
| ret | 関数から戻る |
学生が入力するセルは、このプレビューでは空白です。
20行足らずの小さな CPU
# 命令を1行ずつ実行する、小さな CPU。
eax = 0
program = ["mov eax, 5", "add eax, 3", "add eax, 4"]
for line in program:
words = line.replace(",", "").split(" ")
op = words[0]
number = int(words[2])
if op == "mov":
eax = number
elif op == "add":
eax = eax + number
print(line, "のあと eax は", eax)
# 最後の行を "add eax, 10" に変えて、もう一度実行しましょう。
実物の step を読む
次の練習用プログラムには、step という関数があります。引数に 5 を足し、12 と比べて、等しければ 1、違えば 0 を返します。
ソースを作ってバイナリにし、ソースを消してから、step の命令だけを読みます。
ソースを書く
printf '%s\n' '#include <stdio.h>' '#include <stdlib.h>' 'static int step(int a) {' ' int b = a + 5;' ' if (b == 12) return 1;' ' return 0;' '}' 'int main(int c, char **v) {' ' if (c == 2) {' ' if (step(atoi(v[1]))) { puts("OK"); return 0; }' ' }' ' puts("NG");' ' return 1;' '}' > /tmp/rev2.cここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
プログラムを作る
gcc -O0 -no-pie -o /tmp/rev2 /tmp/rev2.cここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
ソースを消す
rm /tmp/rev2.cここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
step の命令を読む
objdump -d -M intel --no-show-raw-insn /tmp/rev2 --disassemble=step | grep "^ " | cut -f2ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
step を1行ずつ読む
| 命令 | 意味 |
|---|---|
| push rbp、mov rbp, rsp | 関数の始まりの決まり文句 |
| mov DWORD PTR [rbp-0x14], edi | 引数 a をメモリにしまう |
| mov eax, DWORD PTR [rbp-0x14] | a を eax に取り出す |
| add eax, 0x5 | eax に 5 を足す |
| mov DWORD PTR [rbp-0x4], eax | 結果を b にしまう |
| cmp DWORD PTR [rbp-0x4], 0xc | b を 12 と比べる |
| jne | 違えば mov eax, 0x0 へ飛ぶ |
| mov eax, 0x1、jmp | 等しければ 1 を返す準備をして終わりへ |
| mov eax, 0x0、pop rbp、ret | 違えば 0 を返して戻る |
学生が入力するセルは、このプレビューでは空白です。
書き方は2通りある
objdump は、何も指定しないと AT&T 形式で出します。そこでは、元が左で、行き先が右です。数には $ が、レジスタには % がつきます。
このモジュールは、-M intel の Intel 形式で読みます。下の Run は、同じ add と cmp を AT&T 形式で見せます。
AT&T 形式で読む
objdump -d --no-show-raw-insn /tmp/rev2 --disassemble=step | grep -E "add|cmp" | cut -f2ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
よくある間違い
注意
cmp は、左の値を書き換えると思いがちです。実際は、引き算の答えを捨てて、フラグだけを残します。eax は変わりません。その結果を読むのは、直後の jne のような条件ジャンプです。