X86アセンブラ/基本的なFAQ

出典: フリー教科書『ウィキブックス(Wikibooks)』

このページでは、アセンブリ言語でのプログラミング初心者の基本的なFAQについて答えよう。

プログラムと機械語[編集]

hello.c
#include <stdio.h>

int main() {
    printf("Hello world\n");
}
これは、高級言語の1つC言語で書かれた、短く有名なプログラムです。

これをコンパイルして、結果を llvm-objdump というツールでどのような機械語に変換されるか見てみましょう。

amd64をターゲットとしたコンパイル
% clang -c -g -o hello hello.c
% llvm-objdump  -S hello

hello:  file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <main>:
; int main() {
       0: 55                            pushq   %rbp
       1: 48 89 e5                      movq    %rsp, %rbp
;     printf("Hello world\n");
       4: 48 8d 3d 00 00 00 00          leaq    (%rip), %rdi            # 0xb <main+0xb>
       b: b0 00                         movb    $0, %al
       d: e8 00 00 00 00                callq   0x12 <main+0x12>
; }
      12: 31 c0                         xorl    %eax, %eax
      14: 5d                            popq    %rbp
      15: c3                            retq
Cで書かれたソースコードの間に
       0: 55                            pushq   %rbp
のようにソースコードになかった行があるのがコンパイル結果の機械語(55)とそのアセンブリ言語の表記(pushq %rbp)です。

この例は、amd64 を対象にコンパイルしているので、機械語およびアセンブリ言語の表記は amd64 のものですが、異なるプロセッサでは結果も異なります。

arm64をターゲットとしたコンパイル
% clang -c -g -o hello hello.c
% llvm-objdump  -S hello

hello:  file format elf64-littleaarch64

Disassembly of section .text:

0000000000000000 <main>:
; int main() {
       0: a9bf7bfd      stp     x29, x30, [sp, #-16]!
       4: 910003fd      mov     x29, sp
       8: 90000000      adrp    x0, 0x0 <main+0x8>
       c: 91000000      add     x0, x0, #0
;     printf("Hello world\n");
      10: 94000000      bl      0x10 <main+0x10>
      14: 2a1f03e0      mov     w0, wzr
; }
      18: a8c17bfd      ldp     x29, x30, [sp], #16
      1c: d65f03c0      ret
同じソースコードからのコンパイル結果ですが、プロセッサが違うと全く結果が違うことがわかります。

このように、Cのような高級言語を使うことで、プロセッサが違ってもプログラミング上は意識しなし、あるいは意識することが少なくなります。

コンピュータはアセンブリ言語をどのように読んで理解するのか?[編集]

コンピュータ自体は本質的には何も「読んだり」「理解したり」することはありません。コンピュータには意識や自覚がないからですが、それはあまり関係ありません。実際のところ、コンピュータはあなたが書いたアセンブリ言語を読むことはできません。あなたのアセンブラは、アセンブリ言語を「機械語」と呼ばれるバイナリ形式の情報に変換し、それをコンピュータが操作するために使用します。もしコードをアセンブルしなければ、コンピュータにとってはまったく理解できないものです。

ただし、アセンブリ言語は重要です。なぜなら、各アセンブリ命令は通常、単一の機械語に対応しており、何もない状態から「一般の人々」が空白の紙、鉛筆、アセンブリ命令のリファレンスブックだけでこのタスクを直接行うことが可能だからです。実際、コンピュータの初期にはこれが一般的な作業であり、一部の基本的なコンピュータプログラムにおいて、マシン命令を手動で組み立てることが必要でした。スティーブ・ウォズニアックが初期のApple Iコンピュータで使用するために、整数BASICインタプリタ全体を6502機械語に手動で組み立てたのがその典型的な例です。ただし、商業的に配布されるソフトウェアのために行われるこのような作業は非常にまれであり、その事実だけで特別な言及を受けるほどです。実際にこれを行ったプログラマは非常に少数であり、数命令以上の作業を行った人物はほとんどおらず、場合によっては教室の課題のために行ったとしてもほんの一部です。

Windows/DOS/Linux上のアセンブリ言語は同じですか?[編集]

この質問の答えは「はい」と「いいえ」の両方です。基本的なx86機械語はプロセッサにのみ依存しています。x86バージョンのWindowsとLinuxは明らかにx86機械語上に構築されています。x86アセンブリ言語でのLinuxとWindowsのプログラミングにはいくつかの違いがあります。

  1. Linuxコンピュータでは、最も一般的なアセンブラはAT&T構文を使うGASアセンブラと、MASMに似た構文を使うNASM(Netwide Assembler)があります。
  2. Windowsコンピュータでは、最も一般的なアセンブラはIntel構文を使用するMASMですが、多くのWindowsユーザーはNASMも使用しています。
  3. 利用可能なソフトウェア割り込みやシステムコールとその機能は、WindowsとLinuxで異なります。
  4. 利用可能なコードライブラリもWindowsとLinuxで異なります。

同じアセンブラを使用しても、各オペレーティングシステムで書かれた基本的なアセンブリコードは基本的に同じですが、WindowsとLinuxでは異なる方法で相互作用します。

どのアセンブラがベストか?[編集]

短い答えとしては、どのアセンブラも他より優れているわけではありません。それは個人の好みの問題です。

長い答えとしては、異なるアセンブラには異なる機能や欠点などがあります。もしGASの構文しか知らない場合は、おそらくGASを使いたいでしょう。Intelの構文を知っていてWindowsマシンで作業しているなら、MASMを使いたいと考えるかもしれません。MASMやGASのいくつかのクセや複雑さが気に入らない場合は、FASMやNASMを試してみたいと思うかもしれません。次のセクションで、それぞれのアセンブラの違いについて取り上げます。

アセンブリ言語を知る必要があるか?[編集]

アセンブリ言語は、ほとんどのコンピュータタスクでは必要ありませんが、確かに役立つことがあります。アセンブリ言語を学ぶことは新しいプログラミング言語を学ぶこととは異なります。新しいプログラムプロジェクトを始める予定であれば(それがブートローダー、デバイスドライバ、またはカーネルでない限り)、おそらくアセンブリを避ける方が賢明です。例外は、最も基本的なループ内でのパフォーマンスを最適化し、コンパイラが非効率なコードを生成している場合などです。ただし、早すぎる最適化はすべての悪の元であり、一部の計算集約型のリアルタイムタスクは、最適化手法を理解し、最初からそれを計画することで十分に最適化できる場合があります。

しかしながら、アセンブリを学ぶことで、コンピュータの内部動作を特定の視点で理解できます。CやAdaのような高レベル言語でプログラムを作成する場合、すべてのコードは最終的に機械語命令に変換されてコンピュータで実行される必要があります。プロセッサが最も基本的なレベルで何ができるかの限界を理解することは、高レベル言語でプログラムを作成する際にも役立ちます。

コードにはどのようにフォーマットを使うべきか?[編集]

ほとんどのはアセンブリコードの命令は、それぞれを各行に書き改行で分けている。また、ホワイトスペースが命令、オペランドなどを分けるのに使われる。正確に言えば、コードにどのようなフォーマットを採用すべきかは、あなたにまかされている。しかし、一般的な方法というものはいくつかある。


一つは全てを1行ずつ書く方法である。

Label1:
mov ax, bx
add ax, bx
jmp Label3
Label2:
mov ax, cx
...

もう一つは、ラベルを最初の列に書き、命令をそれ以降の列に書く方法である。

Label1: mov ax, bx
        add ax, bx
        jmp Label3
Label2: mov ax, cx
...

ほかにも、ラベルで1行を使い、命令を少しインデントして書く方法もある。

Label1:
   mov ax, bx
   add ax, bx
   jmp Label3
Label2:
   mov ax, cx
...

これらに加えて、ラベルを最初の列に書き、命令を以降の列に書くが、ラベルには別の行を使う方法もある。

Label1:
        mov ax, bx
        add ax, bx
        jmp Label3
Label2:
        mov ax, cx
...

本当にたくさんの書き方があるが、アセンブリプログラマが一般的に従っているルールがいくつかある。

  1. ラベルは、他のプログラマがどこにラベルがあるかはっきり分かるように書く。
  2. 構造(インデント)はコードが読みやすいようにする。
  3. コメントを使い、何をしているのか説明する。アセンブリ・コードの意味は、すぐにははっきりしないことが多いからである。

インラインアセンブラ[編集]

インラインアセンブラは、高水準言語(C、C++など)のコード内にアセンブリ言語のコードを埋め込む手法です。これにより、高水準言語と低水準(アセンブリ)の機能を組み合わせることができます。

インラインアセンブラ自体は、CやC++などのプログラミング言語の一部ではありません。それゆえ、規格標準には含まれていません。各コンパイラ(GCC、Clang、Visual C++など)が独自に提供している機能であり、それぞれのコンパイラによってサポートされる形式や機能が異なることがあります。

インラインアセンブラの使用方法や構文は、コンパイラベンダーのドキュメントやマニュアルで提供される情報に依存します。そのため、プログラムの移植性を確保するためには、インラインアセンブラを使用する際に特定のコンパイラに依存することを避けることが重要です。

具体的な例を見てみましょう。C言語で書かれたプログラム内に、インラインアセンブラを使用してアセンブリコードを挿入する方法を示します。

#include <stdio.h>

int main() {
    int a = 10, b = 20, result;
    
    // インラインアセンブラでのアセンブリコード挿入
    __asm {
        // アセンブリコード
        mov eax, a     // aをeaxレジスタにロード
        add eax, b     // bをeaxレジスタに加算
        mov result, eax // 結果をresultに保存
    }
    
    printf("Result: %d\n", result);
    return 0;
}

この例では、__asmキーワードを使ってインラインアセンブラを開始し、その中にアセンブリ言語のコードを記述しています。アセンブリコードは、mov命令でレジスタに値をロードし、add命令でレジスタ間で加算を行い、最終的に結果をresult変数に保存しています。

インラインアセンブラの利点は、高水準言語と低水準言語を柔軟に組み合わせられることです。特定の最適化や、ハードウェアレベルの操作を高水準言語内で実現することができます。ただし、アセンブリコードの挿入は、プラットフォームやコンパイラに依存するため、移植性に影響を与える可能性があります。また、アセンブリに精通していないと、正確な操作が難しい場合もあります。