Linuxにおけるx86インライン・アセンブラー

構成要素を組立てる

Bharata B. Raoが、Linuxプラットフォームでのx86のインライン・アセンブラーについて、その使用法と仕組みの全般について説明しています。インライン・アセンブラーの基本とそのさまざまな使用例を示し、いくつかの基本的なインライン・アセンブラー・コーディング指針を提示し、Linuxカーネルのインライン・アセンブラー・コードの実例を説明します。

Bharata Rao (rbharata@in.ibm.com)IBM Linux Technology Center, IBM Software Labs, India

Bharata B. Raoは、Mysore University(インド) から電子通信の工学士号をもらっています。1999年からIBM Global Services (インド) で働いています。彼はIBM Linux Technology Centerのメンバーであり、そこでは主に、Linux RAS (信頼性、可用性、および保守容易性) を中心にした仕事をしています。他の関心領域は、オペレーティング・システムの内部とプロセッサー・アーキテクチャーです。彼の電子メール・アドレスは、rbharata@in.ibm.com です。



2001年 3月 01日

あなたがLinuxカーネルのデベロッパーであれば、おそらく、高度にアーキテクチャー依存した機能のコーディングや、コード・パスの最適化をかなり頻繁に行っておられるでしょう。また、こういった作業を行うために、アセンブラー言語命令をCステートメントに挿入しておられるでしょう (この方法は、インライン・アセンブラーとも呼ばれます)。では、Linuxでのインライン・アセンブラーの使用法を具体的に見ていきましょう。(この説明は、IA32アセンブラーに限定します。)

GNUアセンブラー構文の概要

まず、Linuxで使用される基本的なアセンブラー構文について説明します。GCC (LinuxのGNU Cコンパイラー) はAT&Tアセンブラー構文を使用します。この構文の基本的な規則をいくつか記載します。(ここに載せたものは、規則の全てではありません。インライン・アセンブラーに関係のある規則のみを含めています。)

レジスターの命名

レジスター名の接頭部には %が付きます。つまり、eaxを使用する場合は、%eaxとして使用しなければなりません。

sourceおよびdestinationの順序付け

どの命令でも、sourceが最初に来て、その後にdestinationが続きます。この点が、destinationの後にsourceが来るIntel構文と異なります。

mov %eax, %ebx -- eaxの内容をebxに転送します。

オペランドのサイズ

オペランドがバイト、ワード、またはロングのいずれであるかによって、命令の接尾部にb、w、またはlが付きます。これは必須ではありません。GCCは、オペランドを読み取って適切な接尾部を付けます。しかし、手操作で接尾部を指定すれば、コードがさらに読みやすくなり、コンパイラーが間違った推測をする可能性が無くなります。

movb %al, %bl -- バイト移動
	movw %ax, %bx -- ワード移動
	movl %eax, %ebx -- ロング・ワード移動

即値オペランド

即値オペランドは $ を使って指定します。

movl $0xffff, %eax -- 0xffffの値をeaxレジスターに移動します。

間接メモリー参照

メモリーの間接参照は、すべて ( ) を使用して行われます。

movb (%esi), %al -- esiでポイントされているメモリーのバイトを
alレジスターに転送します

インライン・アセンブラー

GCCは、インライン・アセンブラーのための特別な言語要素 "asm" を提供します。フォーマットは、以下のとおりです。

GCCの "asm" 構成
asm ( アセンブラー・テンプレート
	     : 出力オペランド              (オプション)
	    : 入力オペランド              (オプション)
	    : ワーク・レジスターのリスト    (オプション)
	    );

この例では、アセンブラー・テンプレートはアセンブラー命令からなっています。入力オペランドは、命令への入力オペランドとして機能するC表現です。出力オペランドは、アセンブラー命令の出力が実行されるC表現です。

インライン・アセンブラーが重要なのは、主として、自分の出力を操作し、それをC変数に挿入できるからです。この機能のために、"asm" は、アセンブラー命令とそれを含んでいるCプログラムとの間のインターフェースとして働きます。

1つの非常に基本的な、しかし重要な特質は、単純インライン・アセンブラー が命令だけで構成されていることです。これに対し、拡張インライン・アセンブラー はオペランドからなっています。以下、実例をあげて説明します。

基本的なインライン・アセンブラー
{
	int a=10, b;
	asm ("movl %1, %%eax;
	      movl %%eax, %0;"
		:"=r"(b)	/* output */	
		:"r"(a)		/* input */
		:"%eax");	/* clobbered register */
}

この例では、アセンブラー命令を使用して "b" の値を "a" と等しくしています。以下の点に注意してください。

  • "b" は出力オペランドであり、%0によって参照されます。"a" は入力オペランドであり、%1によって参照されます。
  • "r" はオペランドの制約であり、変数 "a" および "b" をレジスターに保管するように指定します。ただし、出力オペランド制約は、それが出力オペランドであることを指定するために、制約修飾子 "=" を伴っていなければなりません。
  • "asm" の内部でレジスター %eaxを使用するには、%eaxの先頭にもう1つの %を付けなければなりません (つまり %%eax)。それは、"asm" が %0、%1などを使用して変数を識別するからです。単一の %が付いたものは、すべて入出力オペランドとして扱われ、レジスターとしては扱われません。
  • 3番目のコロンの後のワーク・レジスター %eaxは、%eaxの値を "asm" の内部で変更することをGCCに宣言します。したがって、GCCが他の値を保管するためにこのレジスターを使用することはありません。
  • movl %1, %%eax は "a" の値を %eaxに移動し、movl %%eax, %0 は %eaxの内容を "b" に移動します。
  • "asm" の実行が完了すると、出力オペランドとして指定されている "b" には更新された値が入ります。つまり、"asm" の内部で "b" に対して行われた変更は、"asm" の外部に入れられます。

では、これらの各項目をもう少し詳しく調べてみましょう。


アセンブラー・テンプレート

アセンブラー・テンプレートは、Cプログラムに挿入されるアセンブラー命令のセットです (単一の命令または命令のグループのいずれか)。各命令を二重引用符で囲むか、または命令のグループ全体を二重引用符内に入れるかしなければなりません。各命令は区切り文字で終わっていなければなりません。有効な区切り文字は、改行 (\n) とセミコロン (;) です。'\n' の後にタブ (\t) が続くことがあります。これはGCCがアセンブラー・ファイルに生成する命令を読み取りやすくするためのフォーマッターです。これらの命令は、%0、%1などの数値別にC表現 (オペランドとして指定される) を参照します。

コンパイラーに "asm" の内部で命令を最適化させたくない場合は、"asm" の後に "volatile" キーワードを使用することができます。プログラムをANSI C互換にしたい場合は、asmとvolatileではなく、__asm__と__volatile__を使用しなければなりません。


オペランド

C表現は、"asm" の内部でアセンブラー命令のオペランドとして使用できます。アセンブラー命令がCプログラムのC表現を操作して意味のある仕事をする場合は、オペランドは、インライン・アセンブラーの主要な機構になります。

各オペランドは、一連のオペランド制約の後に大括弧で囲んだC表現が続く形で指定されます。たとえば、「制約」(C表現)。オペランド制約の主な機能は、オペランドのアドレッシング・モードを決定することです。

複数オペランドを、入力セクションにも出力セクションにも使用することができます。各オペランドはコンマで区切ります。

オペランドは、アセンブラー・テンプレートの内部で番号により参照されます。合計n 個のオペランド (入力も出力も含めて) があれば、最初の出力オペランドには番号0が付き、順序が増えていき、最後の入力オペランドにn-1の番号が付きます。オペランドの合計数は、10個、またはマシン記述内の最大オペランド数 (任意の命令パターン) のいずれか大きいほうに制限されます。


ワーク・レジスターのリスト

"asm" 内の命令がハードウェア・レジスターを参照する場合は、ユーザー自身がそれらを使用し変更することをGCCに宣言するができます。GCCは、それ以降、自分がこれらのレジスターにロードする値が有効であるとは見なさなくなります。一般に、入力レジスターと出力レジスターをワーク・レジスターとしてリストする必要はありません。それは、"asm" がそれらのレジスターを使用することがGCCには分かっているからです(なぜならば、それらのレジスターは制約として明示的に指定されているからです)。ただし、命令が他のいずれかのレジスターを明示的または暗黙的に使用する場合 (および、それらのレジスターが入力制約リストにも出力制約リストにも含まれていない場合) は、それらのレジスターをワーク・レジスターのリストに指定しなければなりません。ワーク・レジスターは、レジスター名をストリングとして指定して、3番目のコロンの後にリストされます。

キーワードに関する限り、命令が何らかの予期しない状況で、しかも明示的にではなくメモリーを変更する場合は、ワーク・レジスターのリストに "memory" キーワードが追加されることがあります。このため、GCCは、レジスターにキャッシュしたメモリー値を命令をまたがって保持しなくなります。


オペランド制約

上でも述べたように、"asm" 内の各オペランドは、オペランド制約のストリングの後に大括弧で囲んだC表現が続く形で指定されます。オペランド制約は、基本的に、命令のオペランドのアドレッシング・モードを決定します。制約は、以下の指定も行います。

  • オペランドをレジスターに入れることができるかどうか、およびそのレジスターをどの種類のレジスターに含めることができるか
  • オペランドをメモリー参照できるかどうか、およびその場合、どの種類のアドレス使用するか
  • オペランドを即値定数にできるかどうか

制約には、突き合わせるための2つのオペランドが必要になることもあります。


通常使用される制約

使用可能なオペランド制約のうち、一定以上の頻度で使用されるのはごく少数です。それらの制約を、簡単な説明とともに以下にリストしています。オペランド制約の全リストについては、GCCおよびGASのマニュアルを参照してください。

レジスター・オペランド制約 (r)

この制約を使ってオペランドを指定した場合は、それらのオペランドは汎用レジスター (GPR) に保管されます。次の例を見てください。

asm ("movl %%cr3, %0\n" :"=r"(cr3val));

ここでは、変数cr3valがレジスターに保管され、%cr3の値がそのレジスターにコピーされ、cr3valの値が更新されてこのレジスターからメモリーに入れられます。"r" 制約を指定すると、GCCは、変数cr3valを任意の使用可能なGPR (複数の場合もある) に保管することができます。レジスターを指定するには、特定のレジスター制約を使ってそのレジスターの名前を直接指定しなければなりません。

a	   a	%eax
  b 	%ebx
  c 	%ecx
  d 	%edx
  S	%esi
  D	%edi

メモリー・オペランド制約 (m)

オペランドがメモリーに入っていると、レジスター制約とは異なり、それらのオペランドに関するすべての操作はそのメモリー域で直接行われます。レジスター制約の場合は、まず、変更する値をレジスターに保管した後、それをメモリー域に戻します。しかしレジスター制約は、通常、それらが命令にとって絶対に必要な場合、またはプロセスの処理速度を大幅に高める場合にのみ使用されます。メモリー制約を最も効率的に使用できるのは、C変数を "asm" 内で更新しなければならない場合と、その値を保管するためにレジスターを使用したくない場合です。たとえば、以下のように、idtrの値をメモリー域locに保管します。

("sidt %0\n" : :"m"(loc));

マッチング (ディジット) 制約

場合によっては、単一の変数が入力オペランドと出力オペランドの両方の役目を果たすことがあります。このようなケースは、マッチング制約を使って "asm" に指定することができます。

asm ("incl %0" :"=a"(var):"0"(var));

このマッチング制約の例では、レジスター %eaxが入力変数と出力変数の両方の変数として使用されています。var入力が %eaxに読み込まれ、更新された %eaxが増分されて再度varに保管されます。この場合の "0" は、同じ制約を0番目の出力変数として指定します。つまり、varの出力インスタンスを %eaxだけに保管することを指定します。この制約は、次のように使用できます。

  • 入力データが変数から読み取られる場合、またはその変数が変更され、その変更が同じ変数に書き戻される場合
  • 入力オペランドと出力オペランドの別々のインスタンスが必要でない場合

マッチング制約を使用した場合の最も重要な効果は、使用可能なレジスターを効率的に使用できるようになることです。


共通なインライン・アセンブラーの使用例

以下の例は、異なるオペランド制約の使用法を示したものです。それぞれの使用法について例を示すには制約が多過ぎますが、ここに示した例は、最もよく使用される制約タイプです。

"asm" およびレジスター制約 "r"

まず、レジスター制約 'r' を持つ "asm" を見てみましょう。この例は、GCCがどのようにしてレジスターを割り振るか、どのようにして出力変数を更新するかを示しています。

int main(void)
{
	int x = 10, y;
	asm ("movl %1, %%eax;
	     "movl %%eax, %0;"
		:"=r"(y)	/* y is output operand */
		:"r"(x)		/* x is input operand */
		:"%eax");	/* %eax is clobbered register */
}

この例では、xの値が "asm" の内部のyにコピーされます。xとyが "asm" に渡され、レジスターに保管されます。この例で生成されるアセンブラーは、次のようになります。

    main:
        pushl %ebp
        movl %esp,%ebp
        subl $8,%esp
        movl $10,-4(%ebp)	 movl -4(%ebp),%edx	/* x=10 is stored in %edx */
#APP	/* asm starts here */	
        movl %edx, %eax		/* x is moved to %eax */
        movl %eax, %edx		/* y is allocated in edx and updated */
#NO_APP	/* asm ends here */
        movl %edx,-8(%ebp)	/* value of y in stack is updated with 
				   the value in %edx */

ここでは、"r" 制約を使用した場合に、GCCは自由に任意のレジスターを割り振ることができます。この例では、%edxが選択されてxが保管されます。%edxのx値を読み取った後、同じレジスターをyに割り振ります。

yが出力オペランド・セクションに指定されているため、%edx内の更新値が、スタックのyのロケーション -8(%ebp) に保管されます。yが入力セクションに指定されていれば、スタックのyの値は更新されません。ただし、一時レジスター記憶域y(%edx) では更新されることがあります。

また、%eaxはワーク・レジスターのリストに指定されているため、GCCは、それをデータ保管のために別の場所では使用しません。

出力を作成する前に入力が参照されることを前提として、入力xも出力yも同じ %edxレジスターに割り振られています。命令が多いと、このケースは当てはまらないことがあります。入力と出力を別々のレジスターに割り振りたい場合は、& 制約修飾子を使用することができます。制約修飾子を使用した例を示します。

    int main(void)
{
	int x = 10, y;	
	asm ("movl %1, %%eax;
	     "movl %%eax, %0;"
		:"=&r"(y)	/* y is output operand, note the	
				   & constraint modifier. */
		:"r"(x)		/* x is input operand */
		:"%eax");	/* %eax is clobbered register */
}

ここでは、この例で使用するアセンブラー・コードが生成されています。このことから、xとyが "asm" をまたがって別々のレジスターに保管されたことが明らかです。

main:
        pushl %ebp
        movl %esp,%ebp
        subl $8,%esp
        movl $10,-4(%ebp)
        movl -4(%ebp),%ecx	/* x, the input is in %ecx */
#APP
	movl %ecx, %eax
	movl %eax, %edx		/* y, the output is in %edx */
#NO_APP
        movl %edx,-8(%ebp)

特定のレジスター制約の使用

それでは、個々のレジスターをオペランドの制約として指定する方法について検討しましょう。次の例では、cpuid命令が %eaxレジスターの入力を受け取り、出力を4つのレジスター %eax、%ebx、%ecx、および %edxに入れます。cpuid (変数 "op") への入力は、cpuidが期待するとおりに、eaxレジスターの "asm" に渡されます。出力のa、b、c、およびd制約を使用して、4つのレジスターのそれぞれの値が収集されます。

asm ("cpuid"
      sm ("cpuid"
                : "=a" (_eax),
                  "=b" (_ebx),
                  "=c" (_ecx),
                  "=d" (_edx)
                : "a" (op));

この場合の生成済みアセンブラー・コードを以下に示します (_eax、_ebx、.... などの変数がスタックに保管されるものと想定しています)。

movl -20(%ebp),%eax	/* store 'op' in %eax -- input */
#APP
        cpuid
#NO_APP
        movl %eax,-4(%ebp)	/* store %eax in _eax -- output */
        movl %ebx,-8(%ebp)	/* store other registers in
        movl %ecx,-12(%ebp)	   respective output variables */  movl %edx,-16(%ebp)

strcpy関数は、以下のようにして、"S" および "D" 制約を使ってインプリメントすることができます。

asm ("cld\n
	      rep\n
	      movsb"
	      : /* no input */
	      :"S"(src), "D"(dst), "c"(count));

ソース・ポインターsrcが "S" 制約を使って %esiに入れられ、destinationポインターdstが "D" 制約を使って %ediに入れられます。カウント値は、repプレフィックスに必要なため、%ecxに入れられます。

ここで、2つのレジスター %eaxおよび %edxを使って2つの32ビット値を結合し、64ビット値を生成するもう1つの制約を以下に示します。

リスティングを見るにはここをクリック

#define rdtscll(val) \
     __asm__ __volatile__ ("rdtsc" : "=A" (val))
生成されたアセンブラーは、次のようになります
(valが64ビット・メモリー・スペースを持っている場合)。
#APP
        rdtsc
#NO_APP
        movl %eax,-8(%ebp)	/* As a result of A constraint movl %edx,-4(%ebp)	   %eax and %edx serve as outputs */
この場合、%edx:%eaxの値が64ビット出力として機能している点に注意してください。

マッチング制約の使用

ここで、システム・コールのためのコードと4つのパラメーターを示します。

#define _syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4) \
type name (type1 arg1, type2 arg2, type3 arg3, type4 arg4) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
        : "=a" (__res) \
        : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
          "d" ((long)(arg3)),"S" ((long)(arg4))); \
__syscall_return(type,__res); \
}

上の例では、システム・コールのための4つの引数が、制約b、c、d、およびSを使って %ebx、%ecx、%edx、および %esiに入れられます。"=a" 制約が出力で使用されるため、%eaxに入っているシステム・コールの戻り値が変数__resに入れられている点に注意してください。マッチング制約 "0" を入力セクションの第1オペランド制約として使用することにより、システム・コール番号__NR_##nameが %eaxに入れられ、システム・コールのための入力として機能します。このため、ここでの %eaxは、入力レジスターとしても機能するし、出力レジスターとしても機能します。別個のレジスターがこのために使用されることはありません。また、出力 (システム・コールの戻り値) を作成する前に、入力 (システム・コール番号) が参照される点にも注意してください。


メモリー・オペランド制約の使用

次のようなアトミック減分オペレーションを考えてみます。

__asm__ __volatile__(
                "lock; decl %0"
                :"=m" (counter)
                :"m" (counter));

ここで生成されるアセンブラーは、次のようになります。

#APP
lock
decl -24(%ebp) /* counter is modified on its memory location */
#NO_APP.

ここでは、レジスター制約をカウンターとして使用することができます。その場合は、カウンターの値をまずレジスターにコピーし、減分してから、そのメモリーに合わせて更新しなければなりません。しかしそうすれば、ロックと整合性の目的を総て失ってしまうことになりますので、メモリー制約を使用する必要があることを明確に示しています。


ワーク・レジスターの使用

メモリー・コピーの基本的なインプリメンテーションについて考えてみます。

asm ("movl $count, %%ecx;
	      up: lodsl;	
	      stosl;
	      loop up;"
		: 			/* no output */
		:"S"(src), "D"(dst)	/* input */
		:"%ecx", "%eax" );	/* clobbered list */

lodslは %eaxを変更しますが、lodslとstosl命令はそれを暗黙的に使用します。また、%ecxレジスターは明示的にカウントをロードします。しかしGCCは、通知を受けなければ、このことを知りません。この通知は、%eaxと %ecxをワーク・レジスター・セットに含めることで行います。これを行わない限り、GCCは、%eaxと %ecxが使用されていないと見なし、それらを他のデータの保管に使用することがあります。ここで、%esiと %ediが "asm" によって使用され、ワーク・レジスターのリストには入れられないという点に注意してください。これは、"asm" がそれらを入力オペランド・リストで使用することが宣言されているからです。結局、レジスターを "asm" 内で使用する (暗黙的または明示的に) 場合に、それが入力オペランド・リストにも出力オペランド・リストにも入っていなければ、それをワーク・レジスターとしてリストしなければなりません。


結論

一般に、インライン・アセンブラーは大規模なものであり、ここで触れていない多くの機能を備えています。しかし、この記事で取り上げた資料の基本を理解すれば、あなた自身のインライン・アセンブラーのコーディングを開始できるようになるはずです。

参考文献

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=228553
ArticleTitle=Linuxにおけるx86インライン・アセンブラー
publish-date=03012001