X86アセンブラ/GASでの文法

出典: フリー教科書『ウィキブックス(Wikibooks)』
ナビゲーションに移動 検索に移動

概要[編集]

この記事の例題は、GNU ASで使用されているAT&Tアセンブリ構文を使用して作成されています。この構文を使うことの主な利点は、GCCのインラインアセンブリ構文との互換性です。しかし、x86の操作を表現するのに使われる構文はこれだけではありません。例えば、NASMでは、いくつかの高水準アセンブラと同様に、アセンブリのニーモニック、オペランド、アドレッシングモードを表すのに異なる構文を使用しています。Unix系システムではAT&T構文が標準ですが、一部のアセンブラではインテル構文を使用したり、GAS自身は両方を受け入れることができます。

はじめに[編集]

この章は、GASの簡単な紹介として書かれています。GASはGNUプロジェクトの一部であり、次のような優れた特性を持っています。

  • 多くのオペレーティング・システムで利用可能
  • GNU Cコンパイラ(gcc)やGNUリンカ(ld)など、他のGNUプログラミングツールとのインタフェースが良好

Linuxオペレーティングシステムを搭載したコンピュータをお使いの方は、すでにGASがシステムにインストールされていると思います。Windows オペレーティングシステムのコンピュータを使用している場合は、CygwinまたはMingwをインストールすることで、GAS およびその他の有用なプログラミングユーティリティをインストールすることができます。 ここから先は、GASがインストールされていて、コマンドライン・インターフェースの開き方やファイルの編集方法を知っていることを前提に説明します。

C言語のコードからアセンブリコードを生成する[編集]

アセンブリ言語は、CPUが実行する演算に直接対応しているため、注意深く書かれたアセンブリ・ルーチンは、Cなどの高級言語で書かれた同じルーチンよりもはるかに速く実行できる可能性があります。 その一方で、アセンブリ・ルーチンは、Cで書かれた同等のルーチンよりも多くの労力を要するのが一般的です。 したがって、性能の良いプログラムを素早く書くための典型的な方法は、まず(記述やデバッグが容易な)高級言語でプログラムを書き、次に(性能の良い)アセンブリ言語で選択されたルーチンを書き直すことです。 C言語のルーチンをアセンブリ言語に書き換える最初のステップとしては、Cコンパイラを使ってアセンブリ言語を自動生成するのが良いでしょう。 これにより、正しくコンパイルされたアセンブリファイルが得られるだけでなく、アセンブリルーチンがあなたの意図した通りに動作することが保証されます[1]

ここでは、GAS アセンブリ言語の構文をしるために、GNU C コンパイラを使用してアセンブリ コードを生成します。

ここでは、C言語で書かれた古典的な「Hello, world」プログラムを紹介します。

hello.c
#include <stdio.h>

int main(void) {
  printf("Hello, world!\n");
}

これを「hello.c」というファイルに保存して、プロンプトで次のように入力します。

$ gcc -o hello_c hello.c

これで、Cファイルがコンパイルされ、"hello_c "という実行ファイルが作成されます。エラーが発生した場合は、"hello.c "の内容が正しいことを確認してください。

これで、プロンプトで次のように入力できるようになります。

$ ./hello_c
Hello, world!

「hello.c」が正しく入力され、目的の動作をすることがわかったので、それに相当する32ビットx86アセンブリ言語を生成してみましょう。プロンプトで次のように入力します。

$ gcc -S hello.c

これで「hello.s」というファイルが作成されるはずです(「.s」はGNUシステムがアセンブリファイルに与えるファイル拡張子です)。

$gcc -o hello_asm hello.s

(なお、gccはアセンブラ(as)とリンカ(ld)を呼び出してくれます) 次に、プロンプトで次のように入力します。

$ ./hello_asm

このプログラムは、コンソールに「Hello, world!」と同じ様に表示します。驚くことではありませんが、これはコンパイルされたCファイルと同じことをしています。

それでは、「hello.s」の中身を見てみましょう。

hello.s
        .file   "hello.c"
        .text
        .def    __main; .scl    2;      .type   32;     .endef
        .section .rdata,"dr"
.LC0:
        .ascii "Hello, world!\0"
        .text
        .globl  main
        .def    main;   .scl    2;      .type   32;     .endef
        .seh_proc       main
main:
        pushq   %rbp
        .seh_pushreg    %rbp
        movq    %rsp, %rbp
        .seh_setframe   %rbp, 0
        subq    $32, %rsp
        .seh_stackalloc 32
        .seh_endprologue
        call    __main
        leaq    .LC0(%rip), %rcx
        call    puts
        movl    $0, %rax
        addq    $32, %rsp
        popq    %rbp
        ret
        .seh_endproc
        .ident  "GCC: (GNU) 10.2.0"
        .def    puts;   .scl    2;      .type   32;     .endef

「hello.s」の内容は、インストールされているGNUツールチェインのバージョンによって異なる場合があります。

コード生成した環境
$ uname -a
MSYS_NT-10.0-19043 HOSTNAME 3.2.0-340.x86_64 2021-09-08 07:03 UTC x86_64 Msys

$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-pc-msys/10.2.0/lto-wrapper.exe
Target: x86_64-pc-msys
Configured with: /home/eguch/git.co/MSYS2-packages/gcc/src/gcc-10.2.0/configure --build=x86_64-pc-msys --prefix=/usr --libexecdir=/usr/lib --enable-bootstrap --enable-shared --enable-shared-libgcc --enable-static --enable-version-specific-runtime-libs --with-arch=x86-64 --with-tune=generic --disable-multilib --enable-__cxa_atexit --with-dwarf2 --enable-languages=c,c++,fortran,lto --enable-graphite --enable-threads=posix --enable-libatomic --enable-libgomp --disable-libitm --enable-libquadmath --enable-libquadmath-support --disable-libssp --disable-win32-registry --disable-symvers --with-gnu-ld --with-gnu-as --disable-isl-version-check --enable-checking=release --without-libiconv-prefix --without-libintl-prefix --with-system-zlib --enable-linker-build-id --with-default-libstdcxx-abi=gcc4-compatible --enable-libstdcxx-filesystem-ts
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 10.2.0 (GCC)

.file.def.asciiのようにピリオドで始まる行は、アセンブラの疑似命令(プロセッサではなくアセンブラに対する命令)です。_main:のように、テキストの後にコロンが続く行は、ラベル、つまりコードの中の名前のある場所です。それ以外の行は、アセンブラの實命令です。

CPUID命令を実行する[編集]

X86には、実行時にプログラムから「いまどんなCPUで走っているか?」を問い合わせる命令 CPUID があります。 CPUID命令は、それ自体が世代を経るごとに拡張されていますが、一番基本的なファンクション「ベンダーIDを返す」を実行してみます。 CPUID命令は、標準C言語では生成できないのでアセンブラーの出番です。

まず、雛形となるC言語のソースコードを用意します。

ccpuid.c
#include <stdint.h>
#include <stdio.h>

void cpuid_0(char vendor_id[12 + 1]) {
  vendor_id[12] = 0;
  int32_t *p = (int32_t *)vendor_id;
  p[0] = *(int32_t *)"TEST";
  p[1] = *(int32_t *)"test";
  p[2] = *(int32_t *)"TEXT";
}

int main(void) {
  char vendor_id[12 + 1];
  cpuid_0(vendor_id);
  printf("VendorID=\"%s\"\n", vendor_id);
}

main() はここまま使い、cpuid_0() の部分に細工することになります(別ファイルとするのが筋なのですが、分割コンパイルの説明が長くなりC/アセンブラーインターフェースの部分がぼやけるので、分割はしませんでした)。

ccpuid.c を コンパイルして ccpuid.s を得ます。

% clang -S -O ccpuid.c

-O は蛇足なのですが、変更する箇所を発見しやすくするために加えました。

ccpuid.s
       .text
        .file   "ccpuid.c"
        .globl  cpuid_0                         # -- Begin function cpuid_0
        .p2align        4, 0x90
        .type   cpuid_0,@function
cpuid_0:                                # @cpuid_0
        .cfi_startproc
# %bb.0:
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset %rbp, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register %rbp
        movb    $0, 12(%rdi)
        movabsq $8391162081026721108, %rax      # imm = 0x7473657454534554
        movq    %rax, (%rdi)
        movl    $1415071060, 8(%rdi)            # imm = 0x54584554
        popq    %rbp
        .cfi_def_cfa %rsp, 8
        retq
.Lfunc_end0:
        .size   cpuid_0, .Lfunc_end0-cpuid_0
        .cfi_endproc
                                        # -- End function
        .globl  main                            # -- Begin function main
        .p2align        4, 0x90
        .type   main,@function
main:                                   # @main
        .cfi_startproc
# %bb.0:
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset %rbp, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register %rbp
        pushq   %rbx
        subq    $24, %rsp
        .cfi_offset %rbx, -24
        leaq    -21(%rbp), %rbx
        movq    %rbx, %rdi
        callq   cpuid_0
        movl    $.L.str.3, %edi
        movq    %rbx, %rsi
        xorl    %eax, %eax
        callq   printf
        xorl    %eax, %eax
        addq    $24, %rsp
        popq    %rbx
        popq    %rbp
        .cfi_def_cfa %rsp, 8
        retq
.Lfunc_end1:
        .size   main, .Lfunc_end1-main
        .cfi_endproc
                                        # -- End function
        .type   .L.str.3,@object                # @.str.3
        .section        .rodata.str1.1,"aMS",@progbits,1
.L.str.3:
        .asciz  "VendorID=\"%s\"\n"
        .size   .L.str.3, 15

        .ident  "FreeBSD clang version 11.0.1 (git@github.com:llvm/llvm-project.git llvmorg-11.0.1-0-g43ff75f2c3fe)"
        .section        ".note.GNU-stack","",@progbits
        .addrsig

15-17行目が文字列を書き換えている部分です。この部分をCPUID命令の呼び出しに書き換えます。

ccpuid.mod.s
        .text
        .file   "ccpuid.c"
        .globl  cpuid_0                         # -- Begin function cpuid_0
        .p2align        4, 0x90
        .type   cpuid_0,@function
cpuid_0:                                # @cpuid_0
        .cfi_startproc
# %bb.0:
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset %rbp, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register %rbp
        movb    $0, 12(%rdi)
#;      movabsq $8391162081026721108, %rax      # imm = 0x7473657454534554
#;      movq    %rax, (%rdi)
#;      movl    $1415071060, 8(%rdi)            # imm = 0x54584554
        pushq   %rbx
        pushq   %rcx
        pushq   %rdx
        xorl    %eax, %eax
        cpuid
        movl    %ebx, (%rdi)
        movl    %edx, 4(%rdi)
        movl    %ecx, 8(%rdi)
        popq    %rdx
        popq    %rcx
        popq    %rbx
        popq    %rbp
        .cfi_def_cfa %rsp, 8
        retq
.Lfunc_end0:
        .size   cpuid_0, .Lfunc_end0-cpuid_0
        .cfi_endproc
                                        # -- End function
        .globl  main                            # -- Begin function main
        .p2align        4, 0x90
        .type   main,@function
main:                                   # @main
        .cfi_startproc
# %bb.0:
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset %rbp, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register %rbp
        pushq   %rbx
        subq    $24, %rsp
        .cfi_offset %rbx, -24
        leaq    -21(%rbp), %rbx
        movq    %rbx, %rdi
        callq   cpuid_0
        movl    $.L.str.3, %edi
        movq    %rbx, %rsi
        xorl    %eax, %eax
        callq   printf
        xorl    %eax, %eax
        addq    $24, %rsp
        popq    %rbx
        popq    %rbp
        .cfi_def_cfa %rsp, 8
        retq
.Lfunc_end1:
        .size   main, .Lfunc_end1-main
        .cfi_endproc
                                        # -- End function
        .type   .L.str.3,@object                # @.str.3
        .section        .rodata.str1.1,"aMS",@progbits,1
.L.str.3:
        .asciz  "VendorID=\"%s\"\n"
        .size   .L.str.3, 15

        .ident  "FreeBSD clang version 11.0.1 (git@github.com:llvm/llvm-project.git llvmorg-11.0.1-0-g43ff75f2c3fe)"
        .section        ".note.GNU-stack","",@progbits
        .addrsig

CPUID命令は、EBX,EDX,ECXの3つのレジスターの値を破壊するので予めスタックに退去(18-20)してCPUIDに関連するが終わったら復帰しています(26-28)。 EAXにファンクションコードの0をセットし(21)、CPUIDを実行し(22)、EBX,EDX,ECXにセットされた値をmainが用意した領域に書き込んでいます。

コンパイルと実行
% clang -o ccpuid ccpuid.mod.s
% ./ccpuid
VendorID="GenuineIntel"
(参考)GCCの__asm__拡張での実装例
#include <stdio.h>

void X86CPUID( int param,
                unsigned *eax,
                unsigned *ebx,
                unsigned *ecx,
                unsigned *edx )
{
    __asm__( "cpuid"
             : "=a" (*eax), "=b" (*ebx), "=c" (*ecx), "=d" (*edx)
             : "0" (param) );
}
int main(void){
    // Your code here!
    char str[12+1];
    unsigned int eax;
    str[12] = 0;
    X86CPUID(0, &eax, (unsigned *)str, (unsigned *)(str + 8), (unsigned *)(str + 4));
    printf("CPUID/VenderID = \"%s\"", str);
}

語順[編集]

GASの命令は一般的に、ニーモニック 転送元 転送先という形式をしています。例えば、次のようなmov命令があります。

 movb $0x05, %al

これは、16進数の数値5をレジスタalにコピーする。

アドレスオペランドの文法[編集]

アドレスを示すオペランドは、最大4個のパラメータを取ることができる。これはディスプレイスメント(ベースレジスタ, オフセットレジスタ, スケーラ)の形式をとります。 これは、インテル記法での[ベースレジスタ + ディスプレイスメント + オフセットレジスタ * スケーラ]という表記と同じ意味です。 パラメータ注の数値部分のいずれかあるいは両方は省略可能であり、同時に、レジスタ部分の一方は省略可能です。

 movl    -4(%rbp, %rdx, 4), %rax  # 完全な例: (rbp - 4 + (rdx * 4))のアドレスの内容をeaxに転送する
 movl    -4(%rbp), %rax           # よくある例: スタックの値をraxに転送する
 movl    (%rcx), %rdx             # オフセットのない場合: ポインタの指す内容をレジスタに転送する
 leal    8(,%rax,4), %rax         # 算術演算の例: raxに4を掛け8を足したアドレスをraxに格納する
 leal    (%rax,%rax,2), %rax      # 算術演算の例: raxの指す値を3倍したアドレスをraxに格納する

なお。「#」はWindows版NASMでのコメントアウトの記号です。その行でのコメントアウト以降の文字列は、アセンブルからは除去される。 Linuxなどウィンドウズ以外の場合では、コメントアウト記号がセミコロン「;」になっている場合もあるので、適宜に応用のこと。

オペレーションサフィックス[編集]

「pushl」とか「movl」とか、プッシュ命令やムーブ命令のうしろにエル「l」がサフィックスです。

データサイズなどを指定するためにサフィックスを指定する必要がある。

GASのアセンブリ命令では、一般にオペランドがどのサイズのデータを扱うか指定するために、b、s、w、l、q、tのいずれかの文字をオペランドの最後に付ける。これをサフィックスという。

b
バイト(8ビット)
s
ショート (16ビット整数)またはsingle(32ビット浮動小数点数)
w
ワード(16ビット)
l
ロング(32ビット整数または64ビット浮動小数点数)
q
クワッド(64ビット)
t
10バイト(80ビット浮動小数点数)

サフィックスが指定されていない場合、GASはメモリをオペランドにとる命令はサイズを特定できない。転送先あるいは転送元がレジスタの場合、レジスターオペランドのサイズからサイズを推定できる。

他の GAS についての読み物[編集]

GNU gas ドキュメントのページで、gas についてもっと知ることができる。

https://sourceware.org/binutils/docs-2.37/as/

アセンブリ言語に無い命令[編集]

GASに限った話題ではないが、アセンブリ言語の命令には、C言語でいう「関数」に相当する「命令」はありませんが、関数の出入り口機能はX86ではENTER/LEAVE命令やそれに相当するコード実装されます。 また、レジスタの幅を超える型の演算はライブラリーを呼び出したり複数のレジスターを数ステップ(あるいは数十ステップ)の命令を使って実現します。

脚注および参考文献[編集]

  1. ^ これは、コンパイラにバグがないことと、さらに重要なこととして、「あなたが書いたコードがあなたの意図を正しく実装していること」を前提としています。また、コンパイラはコードを最適化するために、低レベルの操作の順序を並べ替えることがあります。これにより、コードの全体的なセマンティクスは維持されますが、アセンブリの命令フローがアルゴリズムのステップと正確に一致しない可能性があります。