この章では、基本的な演算命令の機械語における構文を紹介します。以降の章で使うことになる構文の紹介が主で、この章では章を通して作り上げるサンプルはありません。
CPUは、基本的にレジスタ上の値で演算を行うため、まず、任意のレジスタへ値を格納する方法を紹介し、その後、四則演算・論理演算等の基本的な演算の方法を紹介します。
なお、機械語コードは、オペランドのパターン(オペランドの数や種類)が変わると機械語のバイト列も変化します。この章では、以降の章でも使用するような代表的な構文のみ紹介します。アセンブラの構文も併せて紹介しますので、演算対象が異なる場合など、ここで紹介しない構文については、as_dump等で確認してみてください。
まず、一般的にCPU命令で使用するレジスタを紹介します。
CPUが汎用的にデータ格納先として利用できるレジスタが「汎用レジスタ」です。QEMUモニターのinfo registers
コマンドで表示されるレジスタの内、リスト2.1のレジスタが汎用レジスタです。
RAXからRDI、R8からR15までの計16個のレジスタが並んでいます。これらはそれぞれ64ビットの大きさがあります。なお、一部のレジスタは、64ビット幅の中の一部分にのみアクセスすることが可能です。レジスタの下位32ビットへのアクセスや、さらにその中の下位16ビットへのアクセス、その16ビットの内の上位/下位それぞれ8ビットずつへのアクセスといったことができます。アクセスできるビット幅には名前が付いており、一覧にすると表2.1の通りです。
64bit全体 | 下位32bit | 下位16bit | 下位16bitの上位8bit | 下位16bitの下位8bit |
---|---|---|---|---|
RAX | EAX | AX | AH | AL |
RCX | ECX | CX | CH | CL |
RDX | EDX | DX | DH | DL |
RBX | EBX | BX | BH | BL |
RSP | ESP | SP | - | - |
RBP | EBP | BP | - | - |
RSI | ESI | SI | - | - |
RDI | EDI | DI | - | - |
R8 | R8D | - | - | - |
R9 | R9D | - | - | - |
R10 | R10D | - | - | - |
R11 | R11D | - | - | - |
R12 | R12D | - | - | - |
R13 | R13D | - | - | - |
R14 | R14D | - | - | - |
R15 | R15D | - | - | - |
8ビットの場合のみ、16ビットアクセス時の上位側(H)と下位側(L)とで別々にアクセスできます。16ビット/32ビットのアクセスの際は、「32ビット幅の上位16ビット」や「64ビット幅の上位32ビット」というアクセスはできません。
なお、「R8」から「R15」のレジスタ(32ビットにおける「R8D」から「R15D」)は本書では使用しないため特に紹介しません。使い方はその他のレジスタ同様なので、使ってみたい場合は各自でas_dump等から機械語を確認してみてください。
レジスタへの値の格納は「mov
」命令で行えます。ここでは8ビットの即値*1を8ビットのレジスタへ格納する命令を見てみます。mov命令はバリエーションが多いので、その他の構文は、必要になったらその都度確認することにします。
[*1] 値そのものを示すオペランドです。アセンブラでは先頭に「$」を付けると即値を表すオペランドになります。
例えば「0x12」という8ビット即値を「AL」レジスタへ格納する場合、アセンブラとしては以下のコードになります。
as_dumpを使って機械語コードを確認してみます。
$ as_dump mov \$0x12,%al /tmp/tmp.zHlktVD4V2/a.out: ファイル形式 elf64-x86-64 セクション .text の逆アセンブル: 0000000000000000 <.text>: 0: b0 12 mov $0x12,%al
「mov $0x12,%al
」は機械語では「b0 12
」であることが分かりました。2バイト目の「12」がオペランドで指定した即値「0x12」です。この値を変えれば、ALへ格納する値を変更できます。
また、機械語の1バイト目の「b0」が「ALへ格納する」事を示しており、ここを変更することで別のレジスタへ値を格納できます。
例えば、CLレジスタ・DLレジスタへ格納する命令のアセンブラと機械語は表2.2の通りです。
CLレジスタ | |
---|---|
アセンブラ | mov $0x12, %cl |
機械語 | b1 12 |
DLレジスタ | |
アセンブラ | mov $0x12, %dl |
機械語 | b2 12 |
機械語の1バイト目に着目すると、ALの時は「b0
」、CLの時は「b1
」、DLの時は「b2
」と、1ずつ増えています。
このように、レジスタをオペランドに取る機械語は、レジスタを示すバイトを1ずつ変化させることで別のレジスタを指定することができ、レジスタには並び順があります。
8ビットレジスタの並び順は他の命令でも共通で以下の通りです。
reg_ofs | レジスタ |
---|---|
0 | AL |
1 | CL |
2 | DL |
3 | BL |
4 | AH |
5 | CH |
6 | DH |
7 | BH |
「reg_ofs
」は、レジスタを示すオペランド部分のベース値に対するオフセットを示します。今回の場合ベースは「b0」で、そこにオフセット値を足すことで任意の8ビットレジスタを選択できます。今後、レジスタをオペランドにとる構文の表現に使用します。
以上をまとめると、構文としては表2.4の通りです。
アセンブラ | mov 即値(8bit), レジスタ(8bit) |
---|---|
機械語 | b0+reg_ofs 即値(8bit) |
続いて、16ビット即値を16ビットレジスタへ格納する場合も見てみます。
$ as_dump mov \$0x1234,%ax /tmp/tmp.eTaoyVyqMf/a.out: ファイル形式 elf64-x86-64 セクション .text の逆アセンブル: 0000000000000000 <.text>: 0: 66 b8 34 12 mov $0x1234,%ax
機械語「66 b8 34 12
」の最後の2バイト「34 12
」が即値0x1234を示しています。「0x12」と「0x34」がひっくり返っているのは、Intel命令のバイトオーダーがリトルエンディアン*2であるためです。そして、対象とするレジスタは2バイト目の「b8」が示しています。
[*2] バイトの並び順(バイトオーダー)を、下の位から順に1バイトずつ並べること。
なお、16ビットレジスタの並び順は以下の通りです。
reg_ofs | レジスタ |
---|---|
0 | AX |
1 | CX |
2 | DX |
3 | BX |
4 | SP |
5 | BP |
6 | SI |
7 | DI |
以上をまとめると構文としては以下の通りです。
アセンブラ | mov 即値(16bit), レジスタ(16bit) |
---|---|
機械語 | 66 b9+reg_ofs 即値(16bit) |
32ビットと64ビットについては、即値をレジスタへ格納する機会が本書では無く、確認方法等もこれまで同様なので得に紹介しません。
ただ、それぞれのレジスタは他の命令で使うことがあるので、並び順だけ他のビットも合わせて紹介しておきます。
reg_ofs | 64bit | 32bit | 16bit | 8bit |
---|---|---|---|---|
0 | RAX | EAX | AX | AL |
1 | RCX | ECX | CX | CL |
2 | RDX | EDX | DX | DL |
3 | RBX | EBX | BX | BL |
4 | RSP | ESP | SP | AH |
5 | RBP | EBP | BP | CH |
6 | RSI | ESI | SI | DH |
7 | RDI | EDI | DI | BH |
それでは、続いて四則演算を機械語で記述する方法を紹介します。
四則演算の命令は、一部を除いて基本的に2つのオペランドを取り、第1オペランドと第2オペランドで演算した結果を第2オペランドのレジスタ(あるいはメモリアドレス先)へ格納するという挙動になります。
分かりやすい代表的な例として、8ビット即値と8ビットレジスタで計算する場合を中心に紹介します。その他のパターンも同様の方法で構文を確認できますので、興味があれば適宜確認してみてください。
和を計算するのがadd命令です。2つのオペランドを取り、第1オペランドと第2オペランドの和を第2オペランドへ格納します。
例えば、8ビットレジスタALへ即値0x12を加えた結果をALへ格納する場合、アセンブラでは「add $0x12,%al」となり、その機械語コードは以下の通りです。
$ as_dump add \$0x12,%al /tmp/tmp.Ebxte7x4HM/a.out: ファイル形式 elf64-x86-64 セクション .text の逆アセンブル: 0000000000000000 <.text>: 0: 04 12 add $0x12,%al
「12」は即値で、「04」が対象レジスタを決めるベース値に見えます。
CLレジスタの場合はどうなるのか見てみましょう。
$ as_dump add \$0x12,%cl /tmp/tmp.t62F0yZWGi/a.out: ファイル形式 elf64-x86-64 セクション .text の逆アセンブル: 0000000000000000 <.text>: 0: 80 c1 12 add $0x12,%cl
機械語コードの構文ががらっと変わりました。
実は、「04 XX
」という表現は、8ビット即値をALに加算する場合にだけ使えるもので、8ビット即値と8ビットレジスタでの加算命令の「シンタックスシュガー」のようなものです。
アセンブラ | add 即値(8bit), %al |
---|---|
機械語 | 04 即値(8bit) |
AL以外の8ビットレジスタへ8ビット即値を加算する場合の構文は以下の通りです。
アセンブラ | add 即値(8bit), レジスタ(8bit) |
---|---|
機械語 | 80 c0+reg_ofs 即値(8bit) |
das_dump逆アセンブルしてみると分かりますが、構文7でALレジスタを使うこともできるようです。
$ das_dump 80 c0 12 /tmp/tmp.NLv4saxnSr/a.bin: ファイル形式 binary セクション .data の逆アセンブル: 0000000000000000 <.data>: 0: 80 c0 12 add $0x12,%al
ただ、せっかく1バイト減らせる表現があるので、ALレジスタを使う際は構文6の表現を使うほうが良いです。このように、命令によっては特定のパターンのみ命令のバイト数を減らす構文があります。
差は「sub」命令です。
和(add)と同様なので、さくっと構文をまとめます。
subもaddと同様にALレジスタでのみ使える構文があります。以下の通りです。
アセンブラ | sub 即値(8bit), %al |
---|---|
機械語 | 2c 即値(8bit) |
例えば、8ビット即値0x12を8ビットレジスタALから減算する場合、以下のようになります。
アセンブラ | sub $0x12, %al |
---|---|
機械語 | 2c 12 |
次に、AL以外でも使える構文です。
アセンブラ | sub 即値(8bit), レジスタ(8bit) |
---|---|
機械語 | 80 e8+reg_ofs 即値(8bit) |
例えば、8ビット即値0x12を8ビットレジスタCLから減算する場合、以下のようになります。
アセンブラ | sub $0x12, %cl |
---|---|
機械語 | 80 e9 12 |
オペランドが1つの指定ができます。オペランドが1つのパターンで指定できるのはレジスタ名で、例えば8ビットレジスタを指定した場合、「AL * 指定されたレジスタ → AX」という命令になります。暗黙の内にALレジスタとの掛け算を行い、結果を一回り大きい16ビットのAXレジスタへ格納します。単一オペランド時のレジスタ幅に対する挙動を表2.14にまとめます。
レジスタ幅 | 挙動 |
---|---|
8ビット | AL * オペランド → AX |
16ビット | AX * オペランド → DX:AX |
32ビット | EAX * オペランド → EDX:EAX |
64ビット | RAX * オペランド → RDX:RAX |
オペランドが16ビット以上のレジスタの場合、積は上位ビットと下位ビットがDのレジスタとAのレジスタに分けて格納されます。例えば、16ビットの演算で積が「0x1234 5678」であった場合、「0x1234」がDXへ、「0x5678」がAXへ格納されます。
オペランドに8ビットレジスタを指定した場合、構文としては以下の通りです。
アセンブラ | imul レジスタ(8bit) |
---|---|
機械語 | f6 e8+reg_ofs |
例えば、ALの二乗を計算するような場合、以下のようになります。
アセンブラ | imul %al |
---|---|
機械語 | f6 e8 |
オペランドが2つの場合の挙動は、和や差と同じで「第1オペランドと第2オペランドの演算結果を第2オペランドへ格納」です。
ただし、第1オペランドに即値を使う構文はありません。
例えば、第1オペランドに「AX」を、第2オペランドに「CX」を指定した場合は、「AX * CX」の結果が「CX」へ格納されます。(単一オペランドの時のように「一回り大きなレジスタへ格納」とはなりません。)
構文としては以下の通りです。
アセンブラ | imul レジスタ(16bit,src), レジスタ(16bit,dst) |
---|---|
機械語 | 66 0f af c0+reg_dst_src |
「reg_dst_src
」は「第1オペランド(src)」と「第2オペランド(dst)」のレジスタの組み合わせを示すオフセット値で、以下の通りです。
dst/src | AX | CX | DX | BX | SP | BP | SI | DI |
---|---|---|---|---|---|---|---|---|
AX | 0x00 | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 |
CX | 0x08 | 0x09 | 0x0a | 0x0b | 0x0c | 0x0d | 0x0e | 0x0f |
DX | 0x10 | 0x11 | 0x12 | 0x13 | 0x14 | 0x15 | 0x16 | 0x17 |
BX | 0x18 | 0x19 | 0x1a | 0x1b | 0x1c | 0x1d | 0x1e | 0x1f |
SP | 0x20 | 0x21 | 0x22 | 0x23 | 0x24 | 0x25 | 0x26 | 0x27 |
BP | 0x28 | 0x29 | 0x2a | 0x2b | 0x2c | 0x2d | 0x2e | 0x2f |
SI | 0x30 | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 |
DI | 0x38 | 0x39 | 0x3a | 0x3b | 0x3c | 0x3d | 0x3e | 0x3f |
各列が「第1オペランド(src)」で、各行が「第2オペランド(dst)」を示す表です。
例に挙げた「AX * CXをCXへ格納する」場合、以下のようになります。
アセンブラ | imul %ax, %cx |
---|---|
機械語 | 66 0f af c8 |
なお、imul命令のオペランド2つの場合では、第1・第2いずれにも8ビットレジスタを使用する構文はありません。
imulはオペランドが3つの指定もできます。その場合、第1オペランドは「即値」、第2・第3オペランドが「レジスタ」での指定となり、「第1オペランドと第2オペランドの演算結果を第3オペランドへ格納」という挙動になります。
なお、第2・第3オペランドのレジスタ幅に応じて第1オペランドで指定できる即値のビット数が異なります。まとめると表2.20の通りです。
第1オペランド(即値) | 第2・第3オペランド(レジスタ) |
---|---|
8ビット | 16ビット |
8ビット | 32ビット |
8ビット | 64ビット |
16ビット | 16ビット |
32ビット | 32ビット |
32ビット | 64ビット |
例えば、第1オペランドに8ビット即値「0x12」を、第2オペランドに16ビットレジスタ「AX」を、第3オペランドに16ビットレジスタ「CX」を指定した場合、「0x12 * AX」の結果が「CX」へ格納されます。
構文としては以下の通りです。
アセンブラ | imul 即値(8bit), レジスタ(16bit,src), レジスタ(16bit,dst) |
---|---|
機械語 | 66 6b c0+reg_dst_src 即値(8bit) |
例に挙げた「0x12 * AXをCXへ格納する」場合、以下のようになります。
アセンブラ | imul $0x12, %ax, %cx |
---|---|
機械語 | 66 6b c8 12 |
商と剰余を求める「idiv
」命令の場合、「オペランドが1つ」のパターンの命令しかありません。単一オペランドでレジスタを指定することができ、指定したレジスタのビット幅に応じて表2.23の挙動となります。
オペランドレジスタ幅 | 挙動 |
---|---|
8ビット | 「AX/オペランド」の商をALへ剰余をAHへ格納 |
16ビット | 「DX:AX/オペランド」の商をAXへ剰余をDXへ格納 |
32ビット | 「EDX:EAX/オペランド」の商をEAXへ剰余をEDXへ格納 |
64ビット | 「RDX:RAX/オペランド」の商をRAXへ剰余をRDXへ格納 |
そして、オペランドが8ビットレジスタの場合の構文は以下の通りです。
アセンブラ | idiv レジスタ |
---|---|
機械語 | f6 f8+reg_ofs |
例えば、AX/ALの商をALへ剰余をAHへ格納する場合、以下のようになります。
アセンブラ | idiv %al |
---|---|
機械語 | f6 f8 |
なお、0で割る演算を行った場合、CPUで「0除算」の例外が発生します。本書では例外時のジャンプ先(ハンドラ)の設定を行っていないので、例外発生時の挙動は未定義となりますので、ご注意ください*3。
[*3] QEMU上なので試してみても何も被害は無いとは思います。何が書かれているか分からないところへジャンプしてCPUがそこに書かれたバイト列を命令と解釈して突き進んで行きます。
続いて、論理演算の方法をAND・OR・XOR・NOTの4つについて紹介します。
オペランドの数は2つのパターンのみで、第1オペランドと第2オペランドの演算結果を第2オペランドへ格納します。
8ビット即値と8ビットレジスタのANDの結果を8ビットレジスタへ格納する場合の構文を紹介します。
第2オペランドに指定するレジスタがALの場合、1バイト少ない機械語表現があり、構文は以下の通りです。
アセンブラ | and 即値(8bit), %al |
---|---|
機械語 | 24 即値(8bit) |
例えば、8ビット即値0x12とALのANDをALへ格納する場合、以下のようになります。
アセンブラ | and $0x12, %al |
---|---|
機械語 | 24 12 |
そして、第2オペランドのレジスタがAL以外でも使える構文は表2.28の通りです。
アセンブラ | and 即値(8bit), レジスタ(8bit) |
---|---|
機械語 | 80 e0+reg_ofs 即値(8bit) |
例えば、即値0x12とレジスタCLのANDをレジスタCLへ格納する場合の機械語コードは表2.29の通りです。
アセンブラ | and $0x12, %cl |
---|---|
機械語 | 80 e1 12 |
or命令もand命令同様、オペランドの数は2つのパターンのみです。
「8ビット即値(第1オペランド) OR 8ビットレジスタ(第2オペランド)」の結果を第2オペランドへ格納する構文を紹介します。
and命令同様に、第2オペランドのレジスタがALの場合には1バイト短い構文があります。
アセンブラ | or 即値(8bit), %al |
---|---|
機械語 | 0c 即値(8bit) |
例えば、8ビット即値0x12とレジスタALのORをレジスタALへ格納する場合は以下のようになります。
アセンブラ | or $0x12, %al |
---|---|
機械語 | 0c 12 |
そして、第2オペランドのレジスタがAL以外でも使える構文は表2.32の通りです。
アセンブラ | or 即値(8bit), レジスタ(8bit) |
---|---|
機械語 | 80 c8+reg_ofs 即値(8bit) |
例えば、即値0x12とレジスタCLのORをレジスタCLへ格納する場合の機械語コードは表2.33の通りです。
アセンブラ | or $0x12, %cl |
---|---|
機械語 | 80 c9 12 |
XORもオペランドの数は2つのパターンのみです。
「8ビット即値(第1オペランド) XOR 8ビットレジスタ(第2オペランド)」の結果を第2オペランドへ格納する構文を紹介します。
and命令・or命令同様に、第2オペランドのレジスタがALの場合には1バイト短い構文があります。
アセンブラ | xor 即値(8bit), %al |
---|---|
機械語 | 34 即値(8bit) |
例えば、8ビット即値0x12とレジスタALのXORをレジスタALへ格納する場合は以下のようになります。
アセンブラ | xor $0x12, %al |
---|---|
機械語 | 34 12 |
そして、第2オペランドのレジスタがAL以外でも使える構文は表2.36の通りです。
アセンブラ | xor 即値(8bit), レジスタ(8bit) |
---|---|
機械語 | 80 f0+reg_ofs 即値(8bit) |
例えば、即値0x12とレジスタCLのXORをレジスタCLへ格納する場合の機械語コードは表2.37の通りです。
アセンブラ | xor $0x12, %cl |
---|---|
機械語 | 80 f1 12 |
NOTは、オペランドの数は1つのみです。
「オペランドで指定された8ビットレジスタのNOTをオペランドのレジスタへ格納する」場合、構文は以下の通りです。
アセンブラ | not レジスタ(8bit) |
---|---|
機械語 | f6 d0+reg_ofs |
例えば、レジスタALのNOTをレジスタALへ格納する場合は以下のようになります。
アセンブラ | not %al |
---|---|
機械語 | f6 d0 |
シフト演算の命令は以下の4つです。
全てオペランドの数は2つです。
各命令について、オペランドを使って実際に行われる演算は以下の通りです。
sal | 第1オペランドの回数分、第2オペランドへ2を掛ける |
---|---|
sar | 第1オペランドの回数分、第2オペランドを2で割る(符号有) |
shl | 第1オペランドの回数分、第2オペランドへ2を掛ける |
shr | 第1オペランドの回数分、第2オペランドを2で割る(符号無) |
sal命令とshl命令は挙動が同じで、どちらでアセンブラを書いても同じ機械語が生成されます。そのため、機械語として実質存在するシフト命令は3つです。
なお、シフト命令は、第1オペランドに即値あるいはCLレジスタしか指定できません。
「第1オペランドを即値(8bit)、第2オペランドをレジスタ(8bit)」の場合、構文は以下の通りです。(shlはsalと同一なので省略)
アセンブラ | sal 即値(8bit), レジスタ(8bit) |
---|---|
機械語 | c0 e0+reg_ofs 即値(8bit) |
アセンブラ | sar 即値(8bit), レジスタ(8bit) |
機械語 | c0 f8+reg_ofs 即値(8bit) |
アセンブラ | shr 即値(8bit), レジスタ(8bit) |
機械語 | c0 e8+reg_ofs 即値(8bit) |
例えば、ALレジスタに対して即値3でSAL/SAR/SHRを実施する場合の機械語コードは表2.42の通りです。
アセンブラ | sal $3,%al |
---|---|
機械語 | c0 e0 03 |
アセンブラ | sar $3,%al |
機械語 | c0 f8 03 |
アセンブラ | shl $3,%al |
機械語 | c0 e0 03 |
アセンブラ | shr $3,%al |
機械語 | c0 e8 03 |
前述の通り、算術左シフトと論理左シフトは挙動は同じなので、機械語としては同じバイナリ列になっています。
インクリメント(inc)とデクリメント(dec)は共に取れるオペランドの数は一つのみで、構文も似たような感じです。
8ビットレジスタをインクリメント/デクリメントする場合の構文は以下の通りです。
アセンブラ | inc レジスタ(8bit) |
---|---|
機械語 | fe c0+reg_ofs |
アセンブラ | dec レジスタ(8bit) |
機械語 | fe c8+reg_ofs |
例えば、ALレジスタをインクリメント/デクリメントする場合は以下のようになります。
アセンブラ | inc %al |
---|---|
機械語 | fe c0 |
アセンブラ | dec %al |
機械語 | fe c8 |