Kernelのソースを読んでいると、インラインアセンブラの記述が良く出てくる。あの記述は、まるで呪文のようだ!素のアセンブラならある程度は読めるが、インラインアセンブラが出てくると思考が止まってしまう。と言う状態なので調べてみた。
Kernelのソースを読んでいると、インラインアセンブラの記述が良く出てくる。あの記述は、まるで呪文のようだ!素のアセンブラならある程度は読めるが、インラインアセンブラが出てくると思考が止まってしまう。と言う状態なので調べてみた。
基本的なインラインアセンブリの構文は次のようになります。
また次のようにも書くこともできます。
"asm"と"__asm__"は、基本的には同じであるが、"__asm__"は使用している言語の予約語/関数等とぶつかる場合使用します。またはANSI C互換のコードをつくっている場合、"asm"キーワードが使用できなくなるので"__asm__"を使います。
/* keyword asm と同じ関数*/ function asm(void){ } /* asmが使用できないので__asm__を使用 */ function inline(){ __asm__("asm code"); }
インライン構文は複数に渡って書くことができます。複数の命令を書く場合は、一行毎にダブルクオーテーションで括り、命令の後に"\n\t"を書いておけば良いです。
__asm__("movb $0x02, %ah\n\t"
"testb $0x3, %al\n\t");
/*
こんな感じに展開される
#APP
movb $0x02, %ah
testb $0x3, %al
#NO_APP
*/
"\n\t"の代りに";"を使うことも出来ます。
__asm__("movb $0x02, %ah;"
"testb $0x3, %al;");
/*
こんな感じに展開される
#APP
movb $0x02, %ah; testb $0x3, %al;
#NO_APP
*/
また、一行にまとめて書いても問題ありません。
__asm__("movb $0x02, %ah\n\t testb $0x3, %al\n\t");
__asm__("movb $0x02, %ah; testb $0x3, %al;");
インライン構文の中ではレジスタを自由に書き換える事が出来ます。しかし、変更したレジスタの値をを復元しないで戻ってくると、まずい事が起る可能性があります。例えば、レジスタ変数である変数をレジスタに割り当てていた場合、そのレジスタをインライン側で破壊してしまうと、変数の値が変ってしまいます。
例えば次のコードを書いたとしましょう。
#include <stdio.h>
int main(void)
{
register int a=0;
register int b=1;
__asm__("movl $0x0202, %ax;");
a+=b;
printf("%d\n",a);
}
これは次のようなアセンブラコードになります(前後のコードは省略しています)。
movl $0, %eax /* register int a=0; */
movl $1, %edx /* register int b=1; */
#APP
/* 変数 aが破壊される!!!!!!! */
movl $0x0202, %eax; /* __asm__("movl $0x0202, %eal;"); */
#NO_APP
この場合、変数aが破壊されるので、a+=bの結果が515となってしまいます。こんな時、自動的に破壊を避けるようなコードをコンパイラが出力してくれれば便利です。そして、次に説明する拡張アセンブリ構文にその機能があるのです。
基本的なインライン構文では命令列を書くことしか出来なかったが、拡張アセンブリ構文ではオペランドの指定をする事が出来ます。具体的には、入力レジスタ、出力レジスタ、破壊されるレジスタのリストを指定することが出来ます。
__asm__ ( アセンブリテンプレート
: 出力オペランド /* オプション */
: 入力オペランド /* オプション */
: 破壊されるレジスタのリスト /* オプション */
);
アセンブリテンプレートは、アセンブラ命令が書かれますが、拡張アセンブリ構文の場合レジスタなどの書き方が、基本的なアセンブラ命令の書き方とは微妙に異なります。各オペランドは、オペランド制約文字と括弧で括られた、C言語の式を記述できます。
int in1=10,in2=5,out1;
__asm__ ("movl %1, %%eax; /* アセンブリテンプレート */
addl %2, %%eax;
movl %%eax, %0;"
:"=r"(out1) /* 出力 */
:"r"(in1),"r"(in2) /* 入力 */
:"%eax" /* 破壊されるレジスタ */
);
アセンブリテンプレートには、C 言語プログラムに挿入されるアセンブリ命令が書かれます。各命令は、ダブルクォートで囲まれているか、命令全体が一対のダブルクォートで囲まれている必要があります。各命令の最後は":"または"\n\t"で終わっている必要があります。
アセンブリテンプレートの中で、eaxやebxと言ったレジスタ名を使用したい場合は、%%eaxや%%ebxというように、%%を付けて使用します。
アセンブリテンプレートの中で、入力/出力オペランドで指定したC言語変数を使用したい場合は、左から指定した順に、%0, %1, %2,...と表記されます。
例えば、下記の例では出力オペランドで指定した出力先の変数out1が%0に、入力オペランドで指定した変数in1が%1 変数in2が%2 となります。
int in1=10,in2=5,out1;
__asm__ ("movl %1, %%eax; /* eaxにin1の値をストア */
addl %2, %%eax; /* eaxとin2を加算 */
movl %%eax, %0;" /* eaxをout1にストア */
:"=r"(out1) /* 出力 変数out1が%0 */
:"r"(in1),"r"(in2) /* 入力 変数in1が%1 変数in2が%2 */
:"%eax" /* 破壊されるレジスタ */
);
この番号は、左から指定した順に採番されるので、出力オペランドで2つ変数を指定すると、以降の番号は変る事になります。
int in1=10,in2=5,out1,out2;
__asm__ ("movl %2, %%eax;"
"addl %3, %%eax;"
"movl %%eax, %0;"
"movl %%eax, %1;"
:"=r"(out1),"=r"(out2) /* 出力 変数out1が%0 変数out1が%1 */
:"r"(in1),"r"(in2) /* 入力 変数in1が%2 変数in2が%3 */
:"%eax"
);
C言語の変数は、拡張アセンブリ構文中だとオペランドとして機能します。各オペランドの記述は、ダブルクォートで囲んだオペランド制約とオペランドそのものを表すC言語の式が書かれます。
出力オペランドの場合、修飾子がさらに付加されます。
インラインアセンブリ命令を実行したときに、レジスタの値を破壊してしまうものがある場合、破壊されるレジスタの一覧を、3つめの":"の後に列挙する必要があります。このリストはgccに、これらのレジスタを使用し変更を行うことを知らせます。そしてgccは、これらのレジスタの破壊を回避するようなコードを生成します。
破壊されるレジスタを"%eax"などの様に指定します。
命令によって条件レジスタ(x86の場合ステータスレジスタか?)が変更されるならば、変更されるレジスタのリストに"cc"を追加する必要があります。
__asm __volatile(DOINT(0x15) "\n\t"
"setc %b0\n\t"
: "=a" (r), "=b" (p)
: "0" (0xc000)
: "%ecx", "cc");
上記の場合、BIOSのInt 15H(AH=c0h)呼び出したとき、ステータスレジスタのCF bitが破壊されるので、"cc"を指定しています。
予測不可能なメモリ破壊を含むアセンブリコードを書く場合、破壊レジスタの所に "memory"を記述します。さらに、入出力オペランドとして使われていないメモリの予測不可能な破壊がある場合、インラインアセンブリのキーワードに"asm"ではなく"asm volatile"を追加する必要があります。
asmの後ろにキーワードvolatileを書くことによって、 asm命令が除去されたり、 大きく移動されたり、 1つにまとめられたりすることを防ぐことができます。
出力を持たないasm命令を書くと、 GNU CCは、 その命令が副作用を持つものと理解し、 その命令を除去したりループの外へ移動したりしません。 命令の持つ副作用が純粋に外部的なものではなく、 入力の読み込み、 指定されたレジスタまたはメモリの内容の破壊とは異なる方法で プログラムの中の変数に影響を及ぼすのであれば、 将来のバージョンのGNU CCが、 コア領域(core region)の内部で命令を移動するようになるのを予防するために、 volatileキーワードを指定するべきでしょう。
オペランドや破壊されるレジスタを一切持たないasm命令は、 volatileキーワードを指定した場合と同様に、 削除されることもありませんし、 到達不可能でない限り、 大きく移動されることもありません。
入力オペランドと出力オペランドに記述するオペランド制約。これが理解できないと、インラインアセンブルは謎の文字列としか見えないと思います。理解できれば、「なるほど」と思えるでしょう(たぶん...)。
詳しくはgccマニュアルにあるオペランド制約の項に書いてありますが、良く使われるものを簡単にまとめておきます。
最も単純な種類の制約は全部が英文字からなる文字列です。 その一つ一つの文字が、許されるオペランドの一つの種類を記述します。
レジスタの制約は下記の例のように、入力および出力にどのレジスタを使用するか指定します。
int in=1,out;
__asm__ ("movl %1, %%edx;" /* movl %eax,%edx */
"incl %%edx;"
"movl %%edx, %0;" /* movl %edx,%ebx */
:"=b"(out) /* %ebx -> 変数out (ebxの値がoutに代入) */
:"a"(in) /* %eax <- 変数in (inの値がeaxに代入) */
:"%edx"
);
レジスタの制約は、機種依存CPUが変ると制約文字も変ります。ここではx86系を対象に記述します。
| 制約文字 | 制約内容 |
|---|---|
| "a" | eaxレジスタ |
| "b" | ebxレジスタ |
| "c" | ecxレジスタ |
| "d" | edxレジスタ |
| "S" | esiレジスタ |
| "D" | ediレジスタ |
| "r" | eax,ebx,ecx,edx,esi,ediの中から、使用レジスタが自動割り当てされる。 |
| "q" | eax,ebx,ecx,edx,の中から、使用レジスタが自動割り当てされる。 |
| "A" | eaxとedxを合わせて64bitのlong long型で使う。 元々は 64 ビットの値を返すときに、`d’ レジスタを上位 32 ビットに、`a’ レジスタを下位 32 ビットを使うよう指定するのに使われた制約である。 |
| "g" | メモリ・即値オペランドを許容する、任意の汎用レジスタ。汎用レジスタ以外は許さない。 |
| "f" | 浮動小数点レジスタの中から、自動割り当てされる。 |
| "t" | 第一(スタックの一番上の)浮動小数点レジスタ |
| "u" | 第二浮動小数点レジスタ |
あるC言語の変数が、入力と出力として使われる事もある場合は、マッチング制約を指定する事が出来ます。
int in_out=100,in2=200;
__asm__ (
"addl %2,%0;"
:"=a"(in_out) /* eaxの値をin_out出力 */
:"0"(in_out),"b"(in2) /* in_outの値を出力オペランドで割り当てたレジスタ(eax)に入力値としてセット */
);
"0"は0番目の出力オペランド制約であることを示しています。例えば、出力オペランドが下記の場合、"=a"(in_out)が"0" で "=b"(in2)が"1"となります。
:"=a"(in_out),"=b"(in2)
この方法以外にも、制約修飾子(+)を使えば、変数を入出力に指定することが出来るようです。
int in_out=100,in2=200;
__asm__ (
"addl %1,%0;"
:"+a"(in_out) /* eaxの値をin_outの入出力に指定 */
:"b"(in2)
);
sgdt命令やsidt命令などのように、レジスタでなくメモリ上に値をストアする命令の場合、メモリ制約を使用します。レジスタ制約が、レジスタにまず値を入れてから、メモリ上にストアするのに対して、メモリ制約は、直接メモリ上にストアされます。
int64_t idtr;
__asm__ __volatile__("sidt %0" : "=m" (idtr)); /* スタック上のldtrに直接値が入る */
"m"以外にも、下記のメモリ制約も存在します。
| 制約文字 | 制約内容 |
|---|---|
| "o" | メモリオペランドのうち、アドレスがオフセット可能なもの、すなわち小さなオフセットを足しても有効なアドレスが得られるようなアドレス。 例えば、アドレスが定数であれば、それはオフセット指定可能である。 一個のレジスタと一個の定数の和もオフセット指定可能である。 |
| "V" | オフセット指定可能でないメモリオペランドを指定する。 言い換えると、制約 m には収まるが、o には収まらないものは 何でもここに入る。 |
| "<" | 自動デクリメントのアドレスのメモリオペランドを指定する。 プリデクリメントでもポストデクリメントのどちらでも良い。 |
| ">" | 自動インクリメントのアドレスのメモリオペランドを指定する。 プリインクリメントでもポストインクリメントのどちらでも良い。 |
間接/インデックス修飾アドレスがオフセット指定可能かどうかは、その機種がサポートする他のアドレッシングモードに依存します。
| 制約文字 | 制約内容 |
|---|---|
| "i" | 整数の即値オペランド(定数値のもの)が許される。これには、値がアセンブル時にならないとわからないシンボリックな定数も含まれる。 |
| "n" | 既知の数値を持つ整数即値のオペランドが許される。 多くのシステムでは、語長よりも小さいオペランドにはアセンブル時の定数は使えない。そのようなオペランドの制約には i ではなく n を 使うべきである。 |
| "I" (x86系) | 0 以上31以下の定数。(32 ビットオペランドのシフトに使用) |
| "J" (x86系) | 0 以上63以下の定数。(64 ビットオペランドのシフトに使用) |
| "K" (x86系) | 0xff |
| "L" (x86系) | 0xffff |
| "M" (x86系) | 0, 1, 2, あるいは 3。(lea 命令のシフト量に使用) |
| "N" (x86系) | 0 以上 255 以下の定数。(out命令向け) |
制約修飾子を、オペランド制約文字の前に書くことによって、より詳細に制約を与える事が出来ます。
| 制約文字 | 制約内容 |
|---|---|
| "=" | オペランドが書き込み専用であることを意味する。 |
| "+" | オペランドが読み込みと書き込みの両方に使われることを意味する。 |
| "&" | このオペランドが早期破壊 オペランドであることを意味する。早期破壊オペランドとは、 命令が入力オペランドを使い終わる前に変更されるオペランドである。 このため、このオペランドは、入力オペランドや任意のメモリアドレスの一部 として使われるレジスタには置かれない。 |