GCCのインラインアセンブラの書き方 for x86

試行錯誤してインラインアセンブラチュートリアルが完成した。
やったぞ,なんだか分からないけど俺はやったんだ!

GAS構文の概要

まず,GAS のシンタックスについて見ていく。GAS は標準で AT&T 記法を使用しているが,.intel_syntax ディレクティブにより intel 記法を使うこともできる。忌々しい AT&T 記法とはおさらばだ! intel 記法を使うには,アセンブラファイルの先頭に次の行を置く。

.intel_syntax noprefix

また,C ファイルから作成される GAS を intel 記法で出力させる(又は,インラインアセンブラintel 記法を使う場合)には GCC にこんなオプションを加えてやる:

gcc -masm=intel ...

intel 記法が手に入りテンションが上がってきたところで,さっそく構文の説明を始めることにしよう。一応注意しとくけど,ここからは intel 記法を使っていることを前提として話を進める。

intel 記法でのオペランドの順番

intel 記法ではオペランドはディスティネーション,ソースの順番だ。次のコードは eax に ebx の値を代入している。あなたはこのコードのオペランドの名前を見て,レジスタ名がそのままの名前で使用できていることに気が付くだろう。そう,レジスタのアクセスにはそのままの名前を使うことが出来る。

mov eax, ebx
メモリアクセス

GAS のラベル名が変数名となる。

mov eax, foo
即値オペランド

即値はこのように自然に使える:

mov eax, 1000
mov ebx, 0xff
mov ecx, 0b11
mov edx, 'A'
間接アクセス

間接アクセスとは…まあ,C のポインタのようなものだと思って。メモリの間接アクセスには [] を使用する。次のコードは,eax に登録されているメモリアドレスへ ebx の値を書き込んでいる。

mov [eax], ebx
オペランドのサイズ

オペランドのサイズは GCC が自動で読み取ってくれることになっている。でもサイズが曖昧な場合は,次のようにしてサイズを明示的に指定し GCC を助けてやってほしい。面倒だと思うかもしれないが,GCC にはあなたの助けが必要だ。

mov [eax], byte ptr 100
mov [eax], word ptr 100
mov [eax], dword ptr 100
改行とセミコロン

一つ一つの命令はいままで見たきたように改行で区切る。また,セミコロンで区切ることもできる。

mov eax, ebx
mov ecx, ebx; mov edx, ecx
コメントとマクロ

GASでは # 以降がコメントになる。インラインアセンブラでは使えないが,アセンブラファイルを GCC で処理する場合はプリプロセッサを通るので,C で使っていた /* .. */,// ... もコメントとして使うことが出来るし,マクロだって使える。

# コメント(GAS標準)
// ここもコメント
/* ここも */
#define HOGE 1234

ここまでくれば,あなたはインラインアセンブラを使うのに十分なくらい GAS を理解できているはずだ。それではインラインアセンブラの使い方を見ていこう。

インラインアセンブラ

インラインアセンブラでは,C の式をアセンブラ命令のオペランドとして使うことが出来る。あなたが面倒なレジスタの退避処理をせずに! そして値がどのレジスタに登録されているかを知ること無しに!

インラインアセンブラの構文

基本的なインラインアセンブラの構文は次の通りだ:

asm( アセンブラテンプレート
    : 出力オペランド
    : 入力オペランド
    : ワークレジスタ);

asm は C の関数のように使え C ファイルにアセンブラコードを埋め込める。asm は __asm__ と書くことも出来る。__asm__ は asm が予約語や使用している関数とぶつかる場合などに使われ,一般的に __asm__ が使われる。例えば,プログラムをANSI Cに準拠させたい場合 asm は単に使えないし,C99 では asm キーワードが予約されていてぶつかる場合がある。

簡単な例

簡単な例から始めよう。次のコードは変数 x の値を変数 y へコピーしている。

__asm__("mov %0, %1" : "=r" (y) : "r" (x));

それぞれのステートメントを見ていこう:

"mov %0, %1"
  • コンマで区切られた出力オペランドリストを定義する。それぞれのエントリーはダブルクオートで囲まれたオペランド制約と括弧で囲まれた C の式を持つ。
"=r" (y)
"r" (x)
  • ワークレジスタリストを定義する。この例では省略されている。

この例でインラインアセンブラのイメージがつかめた? なんとなくでいいんだ。次の節では,いくつかの具体的な例を上げながらさらに詳しくインラインアセンブラについて見ていく。

よく使われるオペランド制約の紹介とコード例

アセンブラテンプレート,オペランド制約 "r" およびワークレジスタ

オペランド制約 "r"は先ほどの例でも使用したが,簡単化の為ワークレジスタリストが省略されていたので今回はワークレジスタも使ったコード例を示す:

__asm__("mov eax, %1\n\t"
        "mov %0, eax\n\t"
        : "=r" (y)
        : "r" (x)
        : "eax"
        );

アセンブラテンプレート ―― アセンブラテンプレートは文字列リテラルなので,このコードのようにアセンブラコードを複数行に分けて書くことができる。アセンブラ命令はそれぞれを改行かセミコロンで区切る必要があるから,ここでは文字列リテラルの中で改行(\n)を行っている。タブ(\t)は GAS を出力する場合のコードの可読性のためだけに使われる。GCC のエラーメッセージ中の行番号の有用性を下げてしまうため,一般に区切りとしてセミコロンは使われない。
オペランド制約 "r" ―― オペランド制約 "=r","r" を使用すれば GCC は自由に任意のレジスタアセンブラコードで使うために割り当て,そのレジスタはそれぞれ指定された C の変数の書き込み・読み込みに使用される。
ワークレジスタ ―― GCC は出力・入力オペランドリストで割り当てられたレジスタがどう使われるのかを制約を見て知っているので,自動的にそのレジスタの退避処理を行う。しかし,アセンブラコードでそれ以外のレジスタを使う場合 GCC にはこのレジスタがどう使われるかを知る手段が無い(GCC は埋め込むアセンブラコードになんの処理もしない)ためこのレジスタの退避処理は行われない。そのため,出力・入力レジスタリストで定義されていないレジスタを明示的・暗黙的に関わらずアセンブラコードで使用する場合は,ワークレジスタリストにこのレジスタ名を定義し退避処理が必要であることを GCC に教える必要がある。また,ワークレジスタリストで宣言されたレジスタはどの出力・入力オペランドにも割り当てられないことが保障され,アセンブラコードで自由に読み書きできる。
このコードではアセンブラコードで eax レジスタを使用するためワークレジスタリストにそのレジスタ名を定義している。

制約文字と制約修飾子

制約文字 ―― ここまでにいくつかのオペランド制約を見てきた。というか "r" と "=r" の二つだけだ。r はオペランドがどのレジスタを使用するかを指定するための制約文字だ。すでに解説したように r は GCC が自動で割り振ったレジスタを使うことを意味する。
制約修飾子 ―― さて,あなたは出力オペランドにだけ = が付いていることに疑問を持っている。そうにちがいない…たぶん。この = は制約修飾子と呼ばれ制約文字の前に置いて使う。制約修飾子が無ければオペランドは読み込み専用だ。制約修飾子 = はオペランドが書き込み専用であることを示す。
良く使われる制約文字・制約修飾子のリストはこのページの下のおまけに載せた。

マッチング制約

ここからは,さらに高度な例を見ていこう。あなたが出力レジスタと入力レジスタを同じレジスタに割り当てたいと思ったとき,簡単に次のようなコードを考えるかもしれない。

? __asm__("inc %0\n\t" :"=r" (x) :"r" (x));

しかし,このコードは間違っている。出力レジスタと入力レジスタで同じ C 変数を指定しているが,GCC は最適化や様々な理由からそれぞれのオペランドを同じレジスタに置かないかもしれない。
出力レジスタと入力レジスタを必ず同じレジスタに割り当てたい場合,マッチング制約を利用すればいい:

__asm__("inc %0\n\t" :"=r" (x) :"0" (x));

オペランド制約に定数 n を宣言すれば,そのオペランドは n 番目のオペランドと同じレジスタを使用する。制約文字に数字を使うのは入力オペランドのみ許される。マッチング制約を利用した場合の最も重要な効果はレジスタを効率よく使用できるようになることだ。
このコードは制約修飾子 + (読み込みと書き込みで使うことを示す)を使って次のようにも書ける:

__asm__("inc %0\n\t" :"+r" (x));
制約修飾子 &

先ほどのマッチング制約は出力・入力オペランドに同じレジスタを使用するために利用するが,制約修飾子 & は同じレジスタを使用されたくない場合に利用する。
少々作為的というか明らかにあからさまだが次のようなコードを考える:

? __asm__("mov %0, 99\n\t"
?         "add %0, %1\n\t"
?         : "=r" (y)
?         : "r" (x)
?         );

これは x の値に 99 を足して y へ出力するように意図されたコードだ。一見何の問題も無い様に見えるが,このコードが正しい動作をすることは殆ど無い。なぜなら,制約修飾子 & が宣言されていない出力オペランドがあれば,GCC はその出力オペランドが変更される前に全ての入力オペランドが消費されることを想定してレジスタ割り当てを行う。つまり,その出力オペランドは或る入力オペランドと同じレジスタを使用する場合がある(実はマッチング制約を使わなくても,殆どの場合は同じレジスタが割り当てられる)。それは最適化のためで,単にそう想定すれば消費されるレジスタの数を減らせるからだ。
もし,その想定が間違えているなら制約修飾子 & を使って出力オペランドを宣言する。その出力オペランドは早期破壊オペランドと呼ばれ,どんな入力オペランドとも同じレジスタを使用しないことが保障される。つまり,その出力オペランドアセンブラコードで自由に読み書きできる。
つまり,先ほどの間違ったコードは次のように書くのが正しい:

__asm__("mov %0, 99\n\t"
        "add %0, %1\n\t"
        : "=&r" (y)
        : "r" (x)
        );

オペランド制約 "m" と ワークレジスタ "memory"

オペランド制約 "m" ―― オペランド制約 "m" は直接メモリへアクセスしたい場合に利用する。次のコードは x の値をレジスタを経由せず直接 y のメモリ領域にコピーしている:

__asm__("mov %0, %1\n\t"
        : "=m" (y)
        : "r" (x)
        );

このコードは次のコードとほぼ等価だ(もちろん,上のコードの方が良いコードだ):

__asm__("mov [%0], %1\n\t"
        : 
        : "r" (y), "r" (x)
        : "memory"
        );

出力オペラントは一切無いが,構文が曖昧になるためコロンは省略できない。
ワークレジスタ "memory" ―― ワークレジスタ "memory" はメモリ領域が変更されることを GCC に教えている。なぜなら,アセンブラコードでメモリが変更されることを GCC はこのコードのオペランド制約からは把握出来ないからだ。"memory" が定義されれば,変更されたメモリがレジスタ上にキャッシュされている可能性があると想定して,GCCオペランドに割り当てられていないレジスタを全て破棄する。"memory" を利用するのは一般に変更されるメモリ領域が実行してみないと分からない場合だ。このコードのように予め変数 y のメモリ領域が変更されることが分かっていれば "m" を使う。

特定のレジスタオペランドに割り当てとダミー変数

GCC が自動で割り当てたレジスタを使うべきではなくて,或る特定のレジスタオペランドを割り当てたい場合,特定のレジスタを割り当てるための制約文字を利用する。この制約文字は或る特定のレジスタが意味を持つようなアセンブラ命令を使う場合に有効だ。
例えばアセンブラ命令 div は edx:eax の値をオペランドで割り,商を eax へ余りを edx へ登録する。div を使って x の値を 2 で割った余りを y へ代入するコードを考えることにしよう。ただし,次のようなコードは間違いだ:

? __asm__("div ecx\n\t"
?         : "=d" (y)
?         : "a" (x), "d" (0), "c" (2)
?         );

アセンブラ命令 div は eax に割り算の答えを書き込むことを思い出そう。このコードでは eax を読み込み専用として宣言しているのに,アセンブラコードで eax の値が変えられてしまっている。それは大抵の場合 x の値が変わってしまうというバクを発生する。このバグを回避するには C コードで必要の無い変数を定義して,この変数を eax の出力オペランドとして宣言する。この変数はしばしばダミー変数と呼ばれる。

int dummy;
__asm__("div ecx\n\t"
        : "=a"(dummy), "=d" (y)
        : "a" (x), "b" (0), "c" (2)
        );

ここで,ダミー変数を使わず eax をワークレジスタに宣言してバグを回避することは出来ない。実際に eax をワークレジスタとして宣言すると GCC はエラーを吐く。すでにオペランドとして宣言し制約が与えられているのにも関わらず,そのレジスタをワークレジスタとして宣言することは変な話だというエラーだ。

高度なインラインアセンブラ

ここまでは,インラインアセンブラを理解するために実用的ではないが簡潔なコード例をみてきた。最後に,実用的なコードの例を見てこのチュートリアルは終了する。

static __inline__ void *memcpy(void *dest, const void *src, unsigned int n) {
  int d0, d1, d2;
  __asm__ __volatile__(
	   "cld		\n\t"
	   "rep movsd	\n\t"
	   "test %4, 2	\n\t"
	   "je 1f	\n\t"
	   "movsw	\n"
	   "1:		\t"
	   "test %4, 1	\n\t"
	   "je 2f	\n\t"
	   "movsb	\n"
	   "2:		\t"
	   : "=&c" (d0), "=&D" (d1), "=&S" (d2)
	   : "0" (n/4), "r" (n), "1" ((long) dest), "2" ((long) src)
	   : "memory");
  return dest;
}

これは或るメモリ領域を別のメモリ領域にコピーする関数だ。このアセンブラコードはメモリをコピーする一般的なコードなのでアセンブラコードの解説は割愛する。僕たちの興味の対象はインラインアセンブラだからね。
さて,このコードのように不特定のメモリ領域を変更するコードはワークレジスタ "memory" を定義するに相応しい。出力・入力オペランドは先ほど解説したように巧く制約修飾子 & と マッチング制約を使っている。

参考サイト

ここまで読んだなら,もう私があなたに教えることは殆ど無い,あなたはもう一人でこの世界を生きていける。ここで見てきたインラインアセンブラの解説は GCC が提供する機能の全てを網羅するものではない(例えばオペランドにアルファベットで名前を付ける方法は紹介しなかった),私はより使えそうな機能を厳選して説明した。もし,インラインアセンブリをさらに詳しく知りたいなら GCCチュートリアルなどを読むべきだろう。そこで, GCCチュートリアルとこの解説を書くために参考にしたサイトへのポインタを次に示す(本当に最高のサイトたちだ):

おまけ

制約文字
制約文字 制約内容
a eaxレジスタを割り当てる
b ebxレジスタを割り当てる
c ecxレジスタを割り当てる
d edxレジスタを割り当てる
S esiレジスタを割り当てる
D sdiレジスタを割り当てる
r eax, eax, ecx, edx, esi, edi の中から自動で割り当てる
制約修飾子
制約文字 制約内容 詳細
(無し) 読み込み専用オペラント アセンブラコードで変更されないと想定する ― 値を変更することは許されない
= 書き込み専用オペランド 入力オペランドがすべて使い切ってから変更されると想定する ― 入力オペランドが使い終わった後であれば自由に値を変更できる
+ 読み込み・書き込みオペラン どこでも値を変更可能
& 早期破壊オペラント どこでも値を変更可能 ― 早期破壊オペランドであることを示すだけなので,一般に = とセットで =& として使われる。

GCCアセンブラコードのチェックはしないので,この制約従わないようなアセンブラコードを書くことは可能(GCC は何のエラーも吐かない)。もちろんそのプログラムは正しい動作はしないけど…。