FreeBSDで学ぶインラインアセンブラの読み方

この記事は,http://atnd.org/events/21910のために書かれました.
文章が全然まとまっていない..

目的

FreeBSDのcpufunc.hからインラインアセンブラの読み方を学ぶ

環境

FreeBSD 8.2 on i386

準備

○cpufunc.h
FreeBSDi386な人は,
/usr/include/machine/cpufunc.h
を探してみる.


FreeBSDi386ではない人は,
/usr/src/sys/i386/include/cpufunc.h
を探してみる.


FreeBSDでない人は,
http://www.freebsd.org/cgi/cvsweb.cgi/src/sys/i386/include/cpufunc.h
から探してみる.


○参考資料
IA-32 インテル アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル
英語大丈夫な人は,下のURLからファイルをダウンロード.
http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html
必要なのは,Volume:1,2,3の各ファイル.
全部入りなら下のファイルを選択.
・IntelR 64 and IA-32 Architectures Software Developerfs Manual Combined Volumes:1, 2A, 2B, 2C, 3A, 3B, and 3C


日本語大丈夫な人は,下のURLからファイルをダウンロード.
http://www.intel.com/jp/download/index.htm
必要なのは下の4つ.
IA-32 インテル® アーキテクチャー・ソフトウェア・デベロッパーズ・マニュアル、上巻: 基本アーキテクチャ
IA-32 インテル® アーキテクチャー・ソフトウェア・デベロッパーズ・マニュアル、中巻 A: 命令セット・リファレンス A-M
IA-32 インテル® アーキテクチャー・ソフトウェア・デベロッパーズ・マニュアル、中巻 B: 命令セット・リファレンス N-Z
IA-32 インテル® アーキテクチャー・ソフトウェア・デベロッパーズ・マニュアル、下巻: システム・プログラミング・ガイド


インラインアセンブラ
『6.4 Extensions to the C Language Family』
http://gcc.gnu.org/onlinedocs/gcc/C-Extensions.html#C-Extensions
特に,『6.41 Assembler Instructions with C Expression Operands』や『6.42 Constraints for asm Operands』
GCCインラインアセンブリを使用する方法と留意点等 for x86
http://sci10.org/on_gcc_asm.html
GCCインラインアセンブラの書き方 for x86
http://d.hatena.ne.jp/wocota/20090628/1246188338
Linuxにおけるx86インライン・アセンブラー』
http://www.ibm.com/developerworks/jp/linux/library/l-ia/
『Using Inline Assembly With gcc
http://www.cs.virginia.edu/~clc5q/gcc-inline-asm.pdf
GCC-Inline-Assembly-HOWTO』
http://ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html

読んでみる

・breakpoint関数

static __inline void
breakpoint(void)
{
	__asm __volatile("int $3");
}

__inlineがつくことで関数がインライン化される.
関数呼び出しなしで関数内が実行される.
ただ,そのままでは名前空間の関係で他のソースコードから呼ばれる可能性があるため,関数がコード上に出力されてしまう.
static __inlineとすることで関数がファイルローカルになり,関数が完全に出力されなくなる.


__inlineなし,__inlineあり,static __inlineありの3パターンのコードを用意して,gcc -O2 → objdump -d などして確かめてみると違いが分かる.


インライン化は関数呼び出しのオーバヘッドを無くすために利用している.
特にここでは,アセンブラを発行するだけの関数なので,アセンブラを発行する度に関数呼び出しの処理が入るのは無駄.
オーバヘッドの問題だけでなく,プログラマが望んだコードとならないことも問題.


関数内を読んでみる.__asm は インラインアセンブラを示すキーワード(環境や定義によって__asm__ だったり asm だったり).
__asm ("hoge"); と書くと hoge というアセンブラをコードに出力する.


この hoge が,本当にその位置で実行されるかは分からない.
その原因の1つが,コンパイラによる最適化.
コンパイラの判断によって,hoge が消されたり位置が変えられたりすることがある.


コンパイラの最適化を防ぐのが,__volatile(だったり,__volatile__ だったり volatile だったり.).
__asm __volatile ("hoge"); と書くとアセンブラコンパイラの最適化を防いで,hoge をそこで実行することを意味する.


さて,int $3命令の意味を調べてみたい.
説明は,"IntelR 64 and IA-32 Architectures Software Developerfs Manual"のVolume 2で探す.
すると,int $3 は,INT n/INTO/INT 3?Call to Interrupt Procedureの仲間でオペコードがccということが分かる.


Descriptionの3段落目には,debug exception handlerを呼ぶ命令ともある.
debug exceptionについては,Volume 3 の Chapter 6 の 6.15 EXCEPTION AND INTERRUPT REFERENCEや,Chapter 17 DEBUGGING, BRANCH PROFILING, AND TIME-STAMP COUNTER に詳しい.


デバッガでブレークポイントを設定するときにこの命令が埋めこまれると考えれば良い.
話は戻るけれど,ブレークポイントを設定するのでint $3は指定した位置で実行してもらわないと困る.
それで__inlineでコンパイラの最適化を防いでいる.


・bsfl

static __inline u_int
bsfl(u_int mask)
{
	u_int	result;

	__asm("bsfl %1,%0" : "=r" (result) : "rm" (mask) : "cc");
	return (result);
}

u_int は unsigned intのこと.


マニュアルからbsfl命令を探しても見付からない.
GNUアセンブラでは複数のデータ型を扱う命令の最後に付けた文字で,その命令が扱うデータ型を表現している.
bsflの l は,扱うデータ型がlong(32bit)であることを表現している.
それで,マニュアルからは,bsf命令を探そう.


bsflの後ろに%1と%0がある.
そして,"bsfl %1,%0"の後には,さっきは無かったコロンで区切られた指示が続いている.
これらはワンセットで見ないといけない.


ここで,__asm()を入力と出力を持ったモジュールと考えてみよう.
__asm()を実行する際は,メモリやレジスタを入力として渡し,実行後はメモリやレジスタに出力が返る.
__asm()は入力を元に何らかの処理を行なうので,場合によってはレジスタを書き換えることもある.
上で出てきたコロンの指示はこれを表わしている.


この形式は拡張アセンブリと呼ばれていて,次の構文になっている.
__asm(アセンブリコード : 出力オペランド : 入力オペランド : 上書きされるレジスタ);
より正確には,次のような構文だ.
__asm("アセンブリコード" : "制約"(出力オペランド) : "制約"(入力オペランド) : "上書きされるレジスタ");


まず,アセンブリコードを見てみよう.
マニュアルでは,
BSF r32, r/m32
となっている.
このとき,Intelの書式では,1番目のr32がデスティネーション,2番目のr/m32がソースになっている.
ところが,GNUが採用しているAT&Tの書式では,1番目がソース,2番目がデスティネーションになる.


したがって,

	__asm("bsfl %1,%0" : "=r" (result) : "rm" (mask) : "cc")

は,%1の最下位側に立っているビット位置を%0に格納することになる.
それでは,%0や%1は何だろうか.


ここで,次にアセンブリコードの次の出力オペランドと入力オペランドを見てみよう.
出力オペランドの制約は"=r",オペランドは(result),入力オペランドの制約は"rm",オペランドは(mask)だ.


%0や%1は,この出力オペランドと入力オペランドに対応している.
左から順に,%0,%1,%2に対応する.


したがって,"=r"(result)が%0,"rm"(mask)が%1に対応していて,ようするに変数maskの最下位側に立っているビットの位置を変数resultに格納することを示している.
変数maskは,bsfl関数の引数,変数resultは,bsfl関数のはじめで定義されていて戻り値に利用されている.


では,出力オペランドの制約はどういう意味だろう.
制約は"=r"となっている.
1文字目の"="は,出力オペランドには付けるきまりになっている.
2文字目の"r"は,オペランド(ここでは,変数result)を汎用レジスタに対応付けることを指示している.


入力オペランドの制約も同様に見てみよう.
制約は"rm"となっている.
1文字目の"r"は,先程と同様に汎用レジスタへの対応付けを,2文字目の"m"はメモリへの対応付けを指示している.
maskは,汎用レジスタあるいはメモリに対応付けられる.


最後に残るのが,上書きされるレジスタの項目.
ここでレジスタを指定すると,コンパイラが必要に応じて命令実行前の退避や実行後の復帰をしてくれる.
今回使われている"cc"は何かというと,Condition Code registerのこと.
詳細はこのページを見てほしい.http://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#Extended-Asm


condition code registerは別名flag registerとも呼ばれていて,こっちの方がなじみがある呼び方かもしれない.
結果が負数になったsign flag立って,,とかのあれ.
x86ではEFLAGSがcondition code registerの役割を担当している.


つまり,"cc"が付いているということは,EFLAGAレジスタの結果が上書きされるということを意味している.


マニュアルのVolume1,Appendix A EFLAGS Cross-Referenceを見てみると,各命令がEFLAGSのどのフラグに影響を与えるか書かれている.
フラグをresetのみする命令やsetのみする命令もあるが,どうやら,結果に応じてsetとresetのどちらかを行なう命令に"cc"を付ける必要があるようだ.
nBSF命令はZFがそうなっている.
(ソースのビットが立っていないときは,デスティネーションに0が入ってZFが立ち,ソースのビットが立っていればデスティネーションに1が入ってZFが落ちる.)


・cflush
bsrl関数は,bsfl関数と同じなので飛ばして次へ.

static __inline void
clflush(u_long addr)
{

	__asm __volatile("clflush %0" : : "m" (*(char *)addr));
}

コロンが2つしか無い.
そう,この構文では,入力オペランドだけが書かれている.
出力オペランドと上書きされるレジスタは書かれていない.
上書きされるレジスタがない場合は,3つめのコロンは省略できる.


clflushは,キャッシュラインを無効化するための命令.
リニアアドレスをソースで指定すると,そのリニアアドレスを含むキャッシュラインを無効化する.


・disable_intr

static __inline void
disable_intr(void)
{
#ifdef XEN
	xen_cli();
#else	
	__asm __volatile("cli" : : : "memory");
#endif
}

XENFreeBSDXENのゲストで利用する場合に有効にする.
ここでは,XENが定義されていないときの方を見てみよう.


cli命令は,割り込みフラグを落として,割り込みを無効化する命令.


上書きされるレジスタの箇所に"memory"がある.
"memory"は予期しないメモリ破壊があるときに書いておく,と説明されている.
どういうことかというと,cli命令の実行前後でメモリの値をレジスタにキャッシュしたままにしないことをコンパイラに要求している.
メモリの値をレジスタに入れていたら,cli命令を実行する前にレジスタからメモリに書き戻してくださいね,と.


"memory"指定は,インラインアセンブラを読む上では,あまり気にしなくて良いかもしれない.
書いてる本人も詳しくない.


・do_cpuid

static __inline void
do_cpuid(u_int ax, u_int *p)
{
	__asm __volatile("cpuid"
			 : "=a" (p[0]), "=b" (p[1]), "=c" (p[2]), "=d" (p[3])
			 :  "0" (ax));
}

cpuid命令は,eaxレジスタで指定した種類のCPU情報(CPUの種類や利用可能な機能など)を汎用レジスタeax,ebx,ecx,edxに返す命令.
ところで,cpuid命令の説明の See also: の直前には,serialize instruction execution と書かれている.
cpuid命令が入ると命令の実行がシリアル化されるので,out-of-orderな最近のプロセッサで命令の追い越しがおきなくなる効果あり.(Pentium以降)
リング0からリング3のどの特権でも使える命令の中で一番使いやすいこともあって,シリアル化命令の中ではよく利用されていると思う.
rdtsc命令と一緒に使うのがよくある使い方だろうか.
シリアル化命令の詳細は,マニュアルの 8.3 SERIALIZING INSTRUCTIONS に詳しい.


さて,本題.
出力オペランドがずらっと並んでいる.
"a"はeax(あるいはax,al),"b"はebx(あるいはbx,bl),"c"はecx(あるいはcx,cl),"d"はedx(あるいはdx,dl)


入力オペランドは1つだけ.
"0"は,アセンブリで指定する %0 と似た意味で,出力・入力オペランドで登場した順番でレジスタやメモリを指している.
"0"なので,"=a" (p[0]) を指している.


上書きされるレジスタは指定なし.


・cpuid_count関数

static __inline void
cpuid_count(u_int ax, u_int cx, u_int *p)
{
	__asm __volatile("cpuid"
			 : "=a" (p[0]), "=b" (p[1]), "=c" (p[2]), "=d" (p[3])
			 :  "0" (ax), "c" (cx));
}

一部,ecxレジスタの値も利用するものがあり,そのために入力オペランドに "c" (cx) が追加している.


・enable_intr関数

static __inline void
enable_intr(void)
{
#ifdef XEN
	xen_sti();
#else
	__asm __volatile("sti");
#endif
}

stiは,cliの逆で,EFLAGSのIFを立てる命令.
割り込みが有効になる.


・mfence関数

static __inline void
mfence(void)
{

	__asm __volatile("mfence" : : : "memory");
}

mfence命令は,store系の命令(レジスタ→メモリ)とload系の命令(メモリ→レジスタ)をシリアル化する命令だ.
mfence命令より前のstore系の命令やload系の命令は,mfence命令の実行前に完了し,後のstore系・load系の命令は,mfence命令の実行後に実行されるように,CPUに指示をしている.
メモリバリアとかメモリフェンスとか呼ばれる.

マニュアルの 8.2.5 Strengthening or Weakening the Memory-Ordering Model も詳しい.

まとめ

cpufunc.h を全部読むまでには至らなかった.
ただ,今回読んだ範囲の知識を活用すれば,残りのインラインアセンブラも読めると思う.
インラインアセンブラを書けるようになることをめざそう.