コンパイラ作成に必要なx86のアセンブリ知識
x86アセンブリ言語入門:基本概念と主要命令の解説
低レイヤや機械語という言葉は難解な印象を与えがちです。しかし、本記事では、アセンブリ言語の世界への第一歩として、x86アーキテクチャのアセンブリ言語の基本を、図表を用いて分かりやすく解説します。
アセンブリ言語とは
アセンブリ言語は、コンピュータが直接理解できる機械語と1対1で対応する、人間が理解しやすい形式のプログラミング言語です。
私たちが普段使用するC++やPythonなどの高級言語は、コンピュータがそのまま実行することはできません。そこで、コンパイラが高級言語を機械語に翻訳します。機械語は0と1の羅列で構成されており人間には理解困難です。そこで、アセンブリ言語は、機械語の命令を人間が理解しやすい形式で表現した言語です。アセンブリ言語を機械語に変換するプログラムをアセンブラと呼びます。
プログラミング言語 -> コンパイラ -> アセンブリ言語 -> アセンブラ -> 機械語
アセンブリ言語の基本構文
アセンブリ言語のコードは、主に命令 (Instruction) と擬似命令 (Directive) で構成されます。
- 命令 (Instruction): CPUに対する具体的な指示を記述します。「データを転送する」、「2つの数値を加算する」などの動作を指定します。
- 擬似命令 (Directive): アセンブラに対する指示を記述します。「データを配置する場所を指定する」、「プログラムの開始アドレスを指定する」などの役割を持ちます。
基本的な命令の書式は以下の通りです。
ニーモニック オペランド1, オペランド2
-
ニーモニック (mnemonic): 命令の種類を表す短い英単語です。例えば、
mov(データ転送)、add(加算) などがあります。 - オペランド (operand): 命令の対象となるデータやその格納場所を指定します。「転送元と転送先」、「加算する2つの数値」などの情報を記述します。
以下に例を示します。
mov rax, 10 ; raxレジスタに10を代入する
add rbx, rcx ; rbxレジスタの値にrcxレジスタの値を加算する
セミコロン (;) 以降はコメントとして扱われ、コードの説明を記述できます。
x86アセンブリ言語の主要命令
x86アセンブリ言語には多数の命令が存在しますが、ここでは主要な命令を抜粋して紹介します。
データ転送命令
| ニーモニック | 説明 | 例 |
|---|---|---|
| mov | データをコピーします |
mov rax, rbx (rbxの値をraxにコピー) |
| push | データをスタックに積みます |
push rax (raxの値をスタックにプッシュ) |
| pop | データをスタックから取り出します |
pop rbx (スタックからrbxにポップ) |
算術演算命令
| ニーモニック | 説明 | 例 |
|---|---|---|
| add | 加算を行います |
add rax, rbx (rax = rax + rbx) |
| sub | 減算を行います |
sub rax, rbx (rax = rax - rbx) |
| mul | 符号なし乗算を行います |
mul rbx (rax:rdx = rax * rbx) |
| imul | 符号付き乗算を行います |
imul rax, rbx (rax = rax * rbx) |
| div | 符号なし除算を行います |
div rbx (rax = rax / rbx, rdx = 余り) |
| idiv | 符号付き除算を行います |
idiv rbx (rax = rax / rbx, rdx = 余り) |
論理演算命令
| ニーモニック | 説明 | 例 |
|---|---|---|
| and | 論理積 (AND) |
and rax, rbx (rax = rax & rbx) |
| or | 論理和 (OR) |
or rax, rbx (rax = rax | rbx) |
| xor | 排他的論理和 (XOR) |
xor rax, rbx (rax = rax ^ rbx) |
| not | 否定 (NOT) |
not rax (rax = ~rax) |
制御フロー命令
| ニーモニック | 説明 | 例 |
|---|---|---|
| jmp | 無条件ジャンプ | jmp label |
| je | 等しい時にジャンプ (ZF=1) | je equal_label |
| jne | 等しくない時にジャンプ (ZF=0) | jne not_equal_label |
| jl | より小さい時にジャンプ (SF!=OF) | jl less_than_label |
| jg | より大きい時にジャンプ (SF=OF かつ ZF=0) | jg greater_than_label |
| call | サブルーチン呼び出し | call my_function |
| ret | サブルーチンからの復帰 | ret |
レジスタ
レジスタは、CPU内部に存在する高速な記憶領域です。頻繁に使用するデータや計算の中間結果を一時的に格納するために利用されます。x86-64アーキテクチャには、以下のような汎用レジスタが存在します。
- rax: アキュムレータ。演算結果の格納などに使用されます。関数の戻り値も通常はraxに格納されます。
- rbx: ベースレジスタ。メモリアドレスの計算などに使用されます。
- rcx: カウンタレジスタ。ループ処理のカウンタなどに使用されます。
- rdx: データレジスタ。乗算や除算でraxと組み合わせて使用されることがあります。
- rsi, rdi: ソースインデックス、デスティネーションインデックス。文字列操作などでデータの転送元や転送先のアドレスを格納します。
- rbp: ベースポインタ。スタックフレームのベースアドレスを格納します。
- rsp: スタックポインタ。スタックの現在位置(トップ)を指します。誤った操作をするとプログラムがクラッシュする可能性があるため、操作には十分な注意が必要です。
- r8 ~ r15: 汎用レジスタ。
これらのレジスタは、64ビットのデータを格納できます。また、各レジスタの下位ビットにアクセスするための名前も定義されています。例えば、raxの下位32ビットはeax、下位16ビットはax、下位8ビットはalと呼ばれます。
メモリへのアクセス
プログラムが扱うデータは、通常メモリ上に配置されます。レジスタは高速ですが容量が限られているため、大量のデータを扱う場合はメモリを使用します。
アセンブリ言語でメモリにアクセスするには、アドレスを指定します。アドレスは、レジスタの値、即値、またはそれらの計算結果によって指定できます。
mov rax, [rbx] ; rbxレジスタに格納されているアドレスからデータを読み込み、raxに格納する
mov [rdi], rcx ; rcxレジスタの値を、rdiレジスタに格納されているアドレスのメモリに書き込む
mov rdx, [rsi+8] ; rsiレジスタの値に8を加算したアドレスからデータを読み込み、rdxに格納する
mov [rbp-16], 10 ; rbpレジスタの値から16を減算したアドレスに、10を書き込む
[]で囲まれた部分は、「そのアドレスに格納されているデータ」を意味します。
簡単なプログラム例
以下は、2つの数値を加算し、結果を標準出力に表示する簡単なアセンブリプログラムの例です。
section .data
num1 dq 10
num2 dq 20
msg db "The sum is: %ld", 10, 0
section .text
global main
extern printf
main:
push rbp
mov rbp, rsp
mov rax, [num1]
add rax, [num2]
mov rdi, msg
mov rsi, rax
xor rax, rax
call printf
mov rsp, rbp
pop rbp
ret
このプログラムを実行するには、以下の手順が必要です。
-
アセンブラ (nasm) のインストール: インストールされていない場合は、インストールします。
-
コードの保存: 上記のコードを
program.asmなどの名前で保存します。 -
アセンブル: 以下のコマンドを実行し、オブジェクトファイルを生成します。
nasm -f elf64 program.asm -o program.o -
リンク: 以下のコマンドを実行し、実行可能ファイルを生成します。
gcc program.o -o program -
実行: 以下のコマンドでプログラムを実行します。
./program
実行すると、"The sum is: 30"と表示されます。
まとめ
本記事では、x86アセンブリ言語の基本的な概念、構文、主要命令、レジスタ、メモリへのアクセス方法、および簡単なプログラム例について解説しました。アセンブリ言語は、コンピュータの動作原理を理解し、プログラムの最適化を行う上で重要な役割を果たします。この記事が、アセンブリ言語学習の足がかりとなれば幸いです。
より深く学びたい方は、各種ドキュメントやオンラインリソースを参照し、実際にコードを書いて実行してみることをお勧めします。
Discussion