GCC拡張インラインアセンブラ構文

Twitterでうっかり"GCCインラインアセンブラ拡張構文の解説でもするか"と発言したら、二人くらいに期待ageされたので、とりあえずCPUIDを例にインラインアセンブラの解説をします。 環境はi486(x86,IA-32)上のGCCが動く環境を想定しています。80386はCPUIDが無いのでごめんなさい。 x86_64(x64,AMD64)の人もそのまま遊べます。MIPSPPC動かしている人は回れ右。今回はOSは関係ありません。 コンパイラは当然GCC(gcc/g++)です。3.2以降または4.0以降で動くと思います。MSVCやdmdの人は回れ右。最適化は特記の無い限りしてもかまいません。

単純な例から。

とりあえずcpuid命令を発行してみます。

int main() {
	asm volatile (
		"xor %eax, %eax\n\t"
		"cpuid"
	);
	return 0;
}

これだけです。 asmは__asm__と書いてもいいです。インラインアセンブラ使うよ、という宣言です。 volatileは__volatile__と書いてもいいです。書かなくてもいいですが、外すと最適化の対象になります。 コンパイラインラインアセンブラの中身を理解せずに最適化しやがるので、通常はつけておいたほうが無難です。下手すりゃごっそり消されます。 3行目はeaxをクリアしています。頭にくっついている'%'はAT&T構文特有の記法です。x86のようにシンボリックなレジスタ名を使うときは、プレフィックスとして'%'を付加することになっています。

他のアーキテクチャによくあるr0のようなレジスタ名にはつける必要はありません(が、x86_64のr8-r15には'%'が必要です)。 末尾にも変なのがくっついています。これは一つのasm文の中に複数の命令を詰めたい時の書き方です。 もちろん、複数のasmに分けて書くことも出来ますが、分割すると最適化を阻害する上、最悪は間に別のコードを差し込まれて期待した動作をしなくなります。

当然これだけでは結果を取れないので、eaxを読みましょう。ここからが拡張構文の出番です。

拡張構文その1

#include <stdio.h>

int main() {
	unsigned eax_val;
	asm volatile (
		"xor %%eax, %%eax\n\t"
		"cpuid\n\t"
		"movl %%eax, %0"
		: "=g"(eax_val)
		);
	printf("eax: %u\n", eax_val);
	return 0;
}

なにやら一気に変なことになりました。ひとつずつ解説します。

		"xor %%eax, %%eax"

6行目はeaxのゼロクリアです。最初と違い、'%'が重なっています。

		"cpuid\n\t"

7行目、cpuidの発行です。

		"movl %%eax, %0"

8行目、VC++の人には見慣れないmovlという命令があります。これはAT&Tの記法で、命令の後にデータ長を加えるというルール?から来ています。 今回は拡張インラインアセンブラ構文を使っているので、'%'そのものをエスケープするために二つ重ねて'%%'としています。何をすると拡張構文になるのかの説明は後ほど。 また、オペランドの順番もVC++(Intelニモニック)とは逆になっています。AT&Tではsrcが先、dstが後です。 最初のうちは混乱すると思いますが、心配はいりません。ベテランでも混乱します・・・

どうしても慣れない人は、Intelニモニックを使うことも出来ます。

#include <stdio.h>

int main() {
	unsigned eax_val;
	asm volatile (
		"xor eax, eax\n\t"
		"cpuid\n\t"
		"mov %0, eax"
		: "=g"(eax_val)
		);
	printf("eax: %u\n", eax_val);
	return 0;
}

Intelニモニックを使うときはコンパイルオプションに-masm=intelを加えてください。 こうすればVC++とほぼ同じ書き方が出来ますが、筆者は逆アセンブル/コンパイルリストを読むとき以外はIntelニモニックは使わないので、この記事ではAT&Tに統一します。

話を戻して。

		"movl %%eax, %0"

8行目の続き。'%0'は、オペランド参照です。'%'に続けて参照番号を置くことで、外部(この場合はCプログラム)のシンボルを参照することが出来ます。 今回は参照するシンボルは一つしかないので、普通に'%0'とします。

		: "=g"(eax_val)

9行目が実際の参照です。まずコロンを置いて、その後オペランド制約文字列に続けて丸括弧でくくったCの式を置きます。 このオペランド参照があると、拡張構文として扱われます。 制約文字列については複雑なので、後で解説します。

	printf("eax: %u\n", eax_val);

これで無事eaxレジスタの値が、Cの変数eax_valにコピーされましたので、とりあえず出力してみます。 結果は環境によって違いますが、1以上が出てくるはずです。出てこない人は最適化やめてみたりgcc -Sでリスト出してみたりしてください。

制約子

GCC拡張インラインアセンブラ構文で一番判りにくいのが、この制約子です。日本語のまともな解説は片手で数えるほどしかありません。 制約子は、オペランドの入力/出力の選択と、アドレッシングモードの指定を兼ねています。 前述の"=g"を例に解説します。まず、'='は出力オペランドを示しています(参照オペランドに値を書くのが出力)。出力オペランドには必ずこれを入れます。 次の'g'はGeneralです。汎用レジスタ、メモリ、即値が使えます。 他にも、よく使うものでは'm'(メモリオペランド)、'r'(汎用レジスタ)、'i'(即値)などがあります。制約は複数指定することが出来ます("=rm"等) これらの制約、たとえば"=r"を指定すると、出力先の変数がグローバル変数等のメモリに確保されているものであっても、一時的にレジスタを経由して出力します。 これはコンパイラが勝手にやってくれるので、プログラマはどのアドレッシングモードが使えるかだけを気にすればよく、実際のオペランドがどこに確保されているかは気にする必要がありません。

制約子の書き方は、最適化にも影響します。最初のコードを最適化付でリスト出力(gcc -O2 -S)してみましょう。おそらく、eaxが直接pushされていると思います。 今度は、制約子をメモリのみに限定("=m")してみましょう。一度スタックにストアされるようになりました。 これは、効率的な movl reg, reg のアドレッシングモードが制約により許可されなくなったためです。 拡張インラインアセンブラ構文を使うときは、命令がとりうるアドレッシングモードはすべて制約子で許可するようにしましょう。

拡張構文その2

さて、上ではcpuidと値の取り出しを別の命令でやっていましたが、制約子をうまく使えば自分でmovlしなくても値を取り出せます。

#include <stdio.h>

int main() {
	unsigned eax_val;
	asm volatile (
		"xor %%eax, %%eax\n\t"
		"cpuid"
		: "=a"(eax_val)
		);
	printf("eax: %x\n", eax_val);
	return 0;
}

8行目、'a'という制約子が出てきました。これはi386(x86,x86_64)特有の制約子で、eaxを指定しています。 今回の場合は、「cpuid発行したらeaxに答えが入ってるからそれをeax_valに入れてね」と解釈されます。 最適化なしでリスト出力(gcc -O0 -S)すると、勝手にeaxから(もしかしたら他のレジスタ経由で)スタックにストアされていることがわかります。

x86ではレジスタを指定する制約子が多く使えます(命令に直交性がないため)。これを利用すると、他の結果も一気に取り出すことが出来ます。

#include <stdio.h>

union gp_reg_t {
	unsigned long long qword;
	unsigned dword[2];
	unsigned short word[4];
	unsigned char byte[8];
};
struct reg_t {
	union gp_reg_t rax;
	union gp_reg_t rbx;
	union gp_reg_t rcx;
	union gp_reg_t rdx;
};

int main() {
	struct reg_t reg;
	asm volatile (
		"xor %%eax, %%eax\n\t"
		"cpuid"
		: "=a"(reg.rax.dword[0]), "=b"(reg.rbx.dword[0]), "=c"(reg.rcx.dword[0]), "=d"(reg.rdx.dword[0])
		);
	printf("support-command: %x, vendor-string:%.4s%.4s%.4s\n", reg.rax.dword[0], reg.rbx.byte, reg.rdx.byte, reg.rcx.byte);
	return 0;
}

バイトアクセスを簡単にするため、構造体を定義しました(次から定義は省略します)。どうせ使わないのにraxとか作ってるのは趣味です。 21行目で、eax,ebx,ecx,edxを取り出しています。複数のオペランドがあるときはカンマで区切って並べます。制約子は順にa,b,c,dで、そのまんまです。 22行目で、取り出した値を出しています。"GenuineIntel"または"AuthenticAMD"が見えましたか?

入力オペランド

さて、これまではcpuidの発行に先立って手動でeaxをクリアしていました。実はこれも、オペランド制約のつけ方でコンパイラが勝手にやってくれます。

int main() {
	struct reg_t reg;
	asm volatile (
		"cpuid"
		: "=a"(reg.rax.dword[0]), "=b"(reg.rbx.dword[0]), "=c"(reg.rcx.dword[0]), "=d"(reg.rdx.dword[0])
		: "a"(0)
		);
	printf("support-command: %x, vendor-string:%.4s%.4s%.4s\n", reg.rax.dword[0], reg.rbx.byte, reg.rdx.byte, reg.rcx.byte);
	return 0;
}

6行目、'='のついていないオペランドがあります。これが入力オペランドです。全部の出力オペランドを並べた後に、コロンで区切って並べます。 この例では、入力としてeax=0を指定しています。アセンブリリストを見ると、勝手にeaxがゼロクリアされています。 -O2で最適化すると、多分xorでクリアになっていると思います。

次は、eax=1でやってみましょう。

int main() {
	struct reg_t reg;
	asm volatile (
		"cpuid"
		: "=a"(reg.rax.dword[0]), "=b"(reg.rbx.dword[0]), "=c"(reg.rcx.dword[0]), "=d"(reg.rdx.dword[0])
		: "a"(0)
		);
	printf("support-command: %x, vendor-string:%.4s%.4s%.4s\n", reg.rax.dword[0], reg.rbx.byte, reg.rdx.byte, reg.rcx.byte);
	asm volatile (
		"cpuid"
		: "=a"(reg.rax.dword[0]), "=b"(reg.rbx.dword[0]), "=c"(reg.rcx.dword[0]), "=d"(reg.rdx.dword[0])
		: "a"(1)
		);
	printf("Family %X Model %X Step %X, extra-info: %08x, feature-info: %08x:%08x\n",
		   ((reg.rax.byte[1]&0x0F)==0x0F ? ((reg.rax.word[1]>>4)&0xFF) : 0) + (reg.rax.byte[1]&0x0F),
		   (((reg.rax.byte[1]&0x0F)==0x06 || (reg.rax.byte[1]&0x0F)==0x0F) ? (reg.rax.byte[2]&0x0F)<<4 : 0) + (reg.rax.byte[0]>>4),
		   reg.rax.byte[0]&0x0F, reg.rbx.dword[0], reg.rcx.dword[0], reg.rdx.dword[0]);
	return 0;
}

cpuidの仕様が腐っているせいでfamilyとmodelの取出しがえらいことになっていますが、eax=0とeax=1の両方がきっちり取れていることが判ります。

破壊レジスタ指定

cpuidは、一部の出力結果がReservedになっています。これは使ってはいけないけれど、保存されてないかもしれないという厄介なものです。 拡張構文はこんなときのために、破壊されるレジスタを教えられるようになっています。

	asm volatile (
		"cpuid"
		: "=a"(reg.rax.dword[0])
		: "a"(0x80000000)
		: "ebx", "ecx", "edx"
		);
	printf("support-ext-command: %x\n", reg.rax.dword[0]);
	/*注:Pentium 4以降限定です。AMDは多分K8以降?*/

5行目、3つ目のフィールドにレジスタ名が並んでいます。これが破壊レジスタ指定です。 入力オペランドを並べ終わった後に、コロンを置いて、レジスタ名を列挙します。 メモリ上のどこかを勝手に書き換える場合は、"memory"を指定します。メモリバリアにも使えるらしいですが、そこらへんは詳しくないのでスルー。

まとめ

GCCの拡張構文を使うと、Cの式をオペランドとして参照できます。
制約子をうまく使えば、必要なレジスタの退避やバイパスは勝手にやってくれます。
拡張構文でキモになるのは制約子の指定なので、どんな制約が使えるのかはきっちり把握する必要があります。

次回予告

名前付オペランド参照、入出力オペランド、早期破壊オペランド、ターゲット固有の制約子
需要があるようなら、infoの和訳もします。