コンパイラ作成に必要な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

このプログラムを実行するには、以下の手順が必要です。

  1. アセンブラ (nasm) のインストール: インストールされていない場合は、インストールします。

  2. コードの保存: 上記のコードをprogram.asmなどの名前で保存します。

  3. アセンブル: 以下のコマンドを実行し、オブジェクトファイルを生成します。

    nasm -f elf64 program.asm -o program.o
    
  4. リンク: 以下のコマンドを実行し、実行可能ファイルを生成します。

    gcc program.o -o program
    
  5. 実行: 以下のコマンドでプログラムを実行します。

    ./program
    

実行すると、"The sum is: 30"と表示されます。

まとめ

本記事では、x86アセンブリ言語の基本的な概念、構文、主要命令、レジスタ、メモリへのアクセス方法、および簡単なプログラム例について解説しました。アセンブリ言語は、コンピュータの動作原理を理解し、プログラムの最適化を行う上で重要な役割を果たします。この記事が、アセンブリ言語学習の足がかりとなれば幸いです。

より深く学びたい方は、各種ドキュメントやオンラインリソースを参照し、実際にコードを書いて実行してみることをお勧めします。

Discussion