GCCでインラインアセンブリを使用 する方法と留意点等 for x86  (1999〜2006年10回改訂、2006年1月22日と2011年3月22日に注意を追加、最終更新日2011年3月22日)

: A. SAITOH <s-akira at users.sourceforge.net>  home

※システム名、CPU名は一般に開発会社の登録商標です。
以下の情報はあまり過度に信用しないで下さい。より正確な情報は、asやgccのinfoから得て下さい。
個々のプロセッサ命令の解説はここでは述べません。そのような技術資料は、インテルやAMDのウェブ
サイトのdeveloper向けのページからpdf形式で入手できます。
以下の文及びプログラム例の運用結果に関して、筆者は一切責任を負いません。

参考文献
 [0] D. Stancevic, K. Scheibler, J. Leto, Linux Assembly, http://linuxassembly.org/
 [1] 蒲地輝尚 著 『はじめて読むMASM』, アスキー (1988)
 [2] 村瀬康治 監修 蒲地輝尚 著 『はじめて読む8086』, アスキー (1987)
 [3] Linux magazine 2001年11月号 藤沢敏喜 執筆 プログラミング工房 第23回 ステップアップC言語『asm ()文』, アスキー
 [4] トランジスタ技術の過去の多くの記事, CQ出版
 [5] インターフェースの過去の多くの記事, CQ出版
 [6] K6 Perfect Book, ソフトバンク, Oct. 1998
 [7] Intel Architecture Software Developer's Manual, Intel Corporation, http://developer.intel.com/design/mmx/manuals/
 [8] AMD-K6プロセッサ データシート, 日本AMD, Advanced Micro Devices, Inc.
 [9] AMD社製プロセッサ識別方法, 日本AMD, http://www.amd.com/japan/
 [10] Using the GNU Compiler Collection (GCC), Free Software Foundation, Inc. http://gcc.gnu.org/onlinedocs/gcc-3.2.1/gcc/index.html

目次
  x86_64ユーザー向けの注意

x86インラインアセンブリ
  インラインアセンブリ構文 レ ジスタ 一括りの書き方 数値 関数 メモ 型と命令末尾 グローバル変数 ローカル変数 関数の引数 関数の例(不定長ビットリバーサル)

x86拡張インラインアセンブリ
  拡張インラインアセンブリ構文 ConstraintとModifierの 表 引数や変数との対応付け コー ド内でのレジスタ表記 関数例(整数の積1) 関 数例(整数の積2) 関数例(実数の積) 関 数例(2進数で出力) ※Linuxのwriteシステムコール

SIMDに関する追記
  SIMDのチェック MMXを使った例(ベクト ル引き算) 3D Now!を使った例(除算推定値)


※x86_64ユーザ向け の注意
 x86_64向けに -m64 スイッチが標準で設定されたgccでここに載っているプログラム例(仮に
 hoge.cとする)をコンパイルする際には、-m32 オプションが必要である。

gcc -m32 hoge.c
 このようにしないと、64/32ビット命令が混在したアセンブリ コードが出力 され、予期しない
 結果になる。例えば、関数の引数の項の例では、-m32 がないとセグメンテーションフォルト
 が起きる。
 [akira@localhost asmtests]$ gcc -m486 asm_argument_test.c
 [akira@localhost asmtests]$ ./a.out
 セグメンテーション違反です
 [akira@localhost asmtests]$ gcc -m32 asm_argument_test.c
 [akira@localhost asmtests]$ ./a.out
 -2
 [akira@localhost asmtests]$

□□インラインアセンブリの構文□□
1:構文
 intel形式では、

   asm {命令 dest source};

 であったが、GASがAT&T形式なので、GCCでは

 asm ("命令 source,dest");
 としてdestとsourceを逆に書くこと。
 ','を忘れぬように。

 なお、 asm が他の語と競合する可能性があれば、__asm__(" ");と書いた方が良い。
 また、GCCの最適化によってもasm文の位置が変わらないようにするには、
   asm volatile (" ");   や   asm __volatile__ (" ");
 あるいは  __asm__ __volatile__ (" ");  というようにvolatileを併用する。
 これは主に、繰り返し文の中に入れ込みたいときに使う。最適化オプション(-O2 等)
 を指定してコンパイルするときはvolatileを付けると無難である。

 アセンブリコードの中では、コメント文は下のように'#'に続けて 記述する。

asm (" movl %eax,%edi #comment
       movl %edi,%edx #Move Data.
       xor  %edi,%edi #this is XOR.
     ");
 アセンブリコードの#を使ったコメント文では日本語は使用を避け るべきである。
 
アセンブリコード "..." の外でのコメント文 /*...*/ の中では日本語も可能。

 ラベルについては、C言語の関数やラベルと名前が衝突しないように名づける。

2:レジスタ
 レジスタは、%マークを付けて、
 %eax や %ebp と書くこと。

 また、レジスタの指しているアドレスのメモリは、
 intel形式では[eax]であったが、
 AT&T形式では、(%eax)である。

 さらに、オフセットが val であるとき、
 intel形式では[eax+val]であったが、
 AT&T形式では、val(%eax)である。

 AT&T形式で、val(a,b,c)と書くと、
 intel形式の[a + b * c + val]と等価である。

 例えば、int型の配列 array の%eax番目の数は、intが4bytesなので、
 array(,%eax,4)
 である。

 ●メモリの参照の書き方の違い
 oインテル形式  section:[base + index*scale + displacement]
 o AT&T形式     section:displacement(base, index, scale)

3:一括り
 引用符で括った複数の命令を一つの括弧内に書く場合は、
 最後の命令を除いて、文毎に"\n\t"が必要。
 例えば、

asm ("foo: ");   /*hogehoge*/
asm ("  movl $12,%eax ");
asm ("  imul $3,%eax ");
 は、一括りにして
asm ("foo: \n\t"   /*hogehoge*/
     "  movl $12,%eax \n\t"
     "  imul $3,%eax ");
 と書いても良い。
 また、一組の引用符の中にたくさん命令を書いても良 い。
 asm ("
        foo:     #hogehoge
          movl $12,%eax
          imul $3,%eax
      ");
 命令が多いときは最後の書き方が美しいであろう。
 改行する代わりに ; を使うこともできる。
 asm ("foo:; movl $12,%eax; imul $3,%eax");


■4:数値
 数値は先頭に$マークを付けること。$4や$0xa03fと書く。

 例えば、

asm ("foo: \n\t"
     " movl $0x1f2f,%eax \n\t"
     " ret ");
extern int foo (void);
 と書くとfooでは、0x1f2fを%eaxに代入し、手続きか ら復帰する。
 つまり、%eaxの内容0x1f2fをreturnする関数 foo が実現される。

5:関数
 上の例のように、非拡張asm文を使ってC言語の関数を作るときは、asm文中に
 関数名にしたいラベルを書き、リターンしたい値を%eax(浮動小数点数は%st)
 に置いた後、retをする。その後改めて関数をexternで宣言する。勿論、関数
 名はラベルと同じにする。

 なお、asm文中のretは手続きからの復帰命令であるが、C言語 のreturn
 命令であると思って差し支えない。
 ●整数もしくはポインタを返す場合は、その値を%eaxに入れてからretする。
 ●浮動小数点数を返すときは、FPUのある普通のx86プロセッサでは値を浮動小
 数点レジスタスタックの先頭(%st(0)。単に%stと記述しても良い。)に積んで
 からretする。(古いプロセッサでFPUがないときは%eaxを使う。)

 ここで浮動小数点数を返す例を示す。

/*非拡張asm文での関数例 asm_ffunc_test.c*/
#include <stdio.h>
double a = 1.234;
/*実数を%stにロードして返すだけ*/
asm ("foo:; fldl a; ret");
extern double foo(void);

int main ()
{
   printf("%g\n", foo());
   return 0;
}

実行結果
[akira@localhost mypage]$ gcc asm_ffunc_test.c
[akira@localhost mypage]$ ./a.out
1.234
[akira@localhost mypage]$

 拡張asm文で書くのが通常であり、非拡張asm文で関数を作るこ とはまずない。
 引数については後述する。

6:メモ
 コンパイルしたときに最適化などでasm文の位置が入れ替えられることが多いので、
 一組の引用符の中に命令を全て書いたり、上の例のように括弧の中に"\n\t"を用いて
 つらつら記述する方が良い。
 命令毎にasm文を書いた場合、意図したように動かないことが多い。

 しかし、いずれにしても、%ebxや%ecx、%ebpなどは、イ ンラインアセンブリの最初に
 退避させ、最後に元に戻しておくべきである。これを怠るとまともに動かない。
 ただし、スタックに積もうとしてもうまくいかないことがある。このときは、後述の
 グローバル変数に退避させてごまかす方法もあるが、最良の方法は拡張インラインア
 センブリの構文で書いてコンパイラ任せにすることであろう。

7:命令の 末尾
 movやpush、pop、lea など命令はいずれも、
 byte、word、longwordのデータ型でレジスタ等を扱う場合に、それぞれ
 movb, movw, movl や pushb, pushw, pushl と命令末尾を使い分けること。

8:グローバル変数
グローバル変数は、asm文の中からもそのまま簡単に使用できる。

 int foo_global;

 asm ("movl $12,foo_global");
 asm ("movl foo_global,%eax");

 などなど。

9:ローカル変数
 ローカル変数は、x86系では、%ebpを用いてアクセスする。
 例えばint型は4bytesなので、以下の例では、変数は書いた順に、
 -4(%ebp)、-8(%ebp)、-12(%ebp) ...となる。
 ただ通常、変数を使う際の労力を減らす為に拡張インラインアセンブリを使う。

/*ローカル変数テストプログラム asm_local_test.c*/
#include <stdio.h>

void foo (void)
{
   int a,b,c;
   /*aは-4(%ebp)、bは-8(%ebp)、cは-12(%ebp)*/

   asm ("movl $1,-4(%ebp)");
   asm ("movl $2,-8(%ebp)");
   asm ("movl $3,-12(%ebp)");

   printf ("%d %d %d\n", a,b,c);
   return;
}

int main ()
{
   foo();
   return 0;
}

実行結果
[akira@localhost mypage]$ gcc asm_local_test.c
[akira@localhost mypage]$ ./a.out
1 2 3
[akira@localhost mypage]$
(注意) この様に拡張でないasm文で%ebpを使う際、オプティマイズオプション -O を付けるとまともに
動かないことがある。

10:関数の引数
 x86系では関数の引数は、スタックポインタ(%esp)やフレームポインタ(%ebp)を用いて
 アクセスできる。しかし、プログラムの中でスタックの状態を意識しつつコーディングせ
 ねばならない。骨が折れるので、普通は拡張インラインアセンブリを使う。
 詳しくは、参考文献やインテルの配布しているpdfマニュアルを参照されたし。
 ここで敢えて非拡張asm文を用いた整数の掛け算の例を示す。

/*引数テストプログラム asm_argument_test.c*/
#include <stdio.h>

void foo (int a, int b)
{
   int c;
   /*ここでは、aは8(%ebp)、bは12(%ebp)、cは-4(%ebp)*/
   asm volatile("movl 8(%ebp),%edi  \n\t"
                "subl 12(%ebp),%edi \n\t"
                "movl %edi,-4(%ebp)     ");
   /* c=a-b を表示*/
   printf ("%d\n", c);
}

int main ()
{
   foo (3, 5);
   return 0;
}
実行結果
[akira@localhost mypage]$ gcc asm_argument_test.c
[akira@localhost mypage]$ ./a.out
-2
[akira@localhost mypage]$

★ %espのメモ ★
(a) なお、%espを使って、

 asm ("foo: \n\t"
      " movl 4(%esp),%eax \n\t"
      " ret ");
 extern int foo (int a);
 と書くと、4(%esp)つまり引数 a を%eaxに代入し、その%eaxの内容でreturnする関数 foo が
 実現される。
引数が foo (int a,int b,int c,... )と増えても同様に、 各 々、
 4(%esp)、8(%esp)、12(%esp)、 ... でアクセスできる。

 ただし、push命令後には退避したバイト数分%espの値が減るので、例えばpushlを3回実行す
 ると引数 a にアクセスするには 16(%esp) を使うことになる。(4 + バイト数4 × 3 = 16)

(b) このように%espの値を使う方法で引数を扱うと複雑で間違えやすいため、通常は、%ebpを
 pushして退避し、続いて
%ebpに%espの値を コピーして、それ以降は引数にアクセスするには
 %ebpの値[これは、pushする前の時点の-4(%esp)に等しい]を基準に使うようにする。
retする
 前にはleave命令を実行し、%ebpにコピーしていた値 を%espに戻し、退避していた%ebpをpop
 しておく。

 従って、上の例は以下のようにも書ける。

asm("foo:                \n\t"
    "pushl  %ebp         \n\t"
    "movl   %esp,%ebp    \n\t"

/* ここでは引数 a は8(%ebp) */

    "movl   8(%ebp),%eax \n\t"  
    "leavel              \n\t"

/* ここでleavelは、
 *   movl   %ebp,%esp
 *   popl   %ebp    
 * と同じ操作である
 */

    "ret                 \n\t");
extern int foo (int a);

 関数がfoo(int a, int b, int c)ならば、引数には左から順に
8(%ebp)、12(%ebp)、 16(%ebp)
 でアクセスする。

11:関数例(不定長ビットリバーサル)

 具体的に、前項の(a)の方法で意味のある関数を作成してみる。
 この関数は 10110001 を 10001101 と、反対から読んで返す、ビットリバースであ る。
 ただし、続きに示した通常のCで書いたコードの方が分かり易い。
 注意:これは不定長のビットリバーサルであり、FFTで使われる固定長のものとは異なる。

/*
   インラインアセンブリを用いたビットリバーサル。
   10011 --> 11001 とビット列を逆さに配置する。
   不定長であり、見つかった最初の1以降でのビットリバーサルである。
   pushl/popl 命令後には、バイトサイズ4だけ%espの値が減/増する点に注意。
   3回 pushl/poplしているので、バイトサイズ12だけ%espの値が変化している。
*/
__asm__ (
"btrv:                    \n\t"
"  pushl  %ebx            \n\t"
"  pushl  %ecx            \n\t"
"  pushl  %edx            \n\t"
/****srcを%ecxにロード。16(%esp)になっているのは pushl 3回の後だから*/
"  movl  16(%esp),%ecx    \n\t"
/****%ecxをbit scan reverseして1のビットを見つけ番号を%edxに入れる***/
"  bsr   %ecx,%edx        \n\t"
/****%eaxを0にする***************************************************/
"  xor   %eax,%eax        \n\t"
/****%ecxが0ならば0を返す********************************************/
"  jecxz retv             \n\t"
"btrb:                    \n\t"
/****%ecxをbit scan forwardして1のビットを見つけ番号を%ebxに入れる***/
"  bsf   %ecx,%ebx        \n\t"
/****%ecxの中の上の操作で見つかったビットを0にする*******************/
"  btr   %ebx,%ecx        \n\t"
/****最後尾の1であるビット番号から今最前列の1であるビット番号を引く**/
"  neg   %ebx             \n\t"
"  add   %edx,%ebx        \n\t"
/****その値の位置のビットを1にする***********************************/
"  bts   %ebx,%eax        \n\t"
/****もし%ecxが0であれば%eaxを返す***********************************/
"  jecxz retv             \n\t"
/****もし%ecxに1のビットが残っていれば繰り返し***********************/
"  jmp   btrb             \n\t"
"retv:                    \n\t"
"  popl   %edx            \n\t"
"  popl   %ecx            \n\t"
"  popl   %ebx            \n\t"
/****ret means return %eax*******************************************/
"  ret                    \n\t"
);

/*bit reverse 10011 --> 11001*/
extern int btrv (int src);


 C言語で書くと次のようになる。
 もう一度書くが、これは不定長のビットリバーサルであり、FFTに使う固定長のものではない。

int btrv (int src)
{
   int dst = 0;
   while (1)
   {
      dst += src & 1;
      src >>= 1;
      if (!src) break;
      dst <<= 1;
   }
   return dst;
}
ページの目次へ戻る

□□拡張インラインアセンブリに関して for x86□□
12:構 文
拡張インラインアセンブリの構文は以下のようである。
asm (アセンブリコード : 出力オペランド : 入力オペランド : 破壊レジスタ);
より細かくみると、次のようになっている。
asm ("アセンブリコード" : "指定文字列"  (出力先変数) : "指定文字列"  (入力用の式や変数) : "内容が破損するので退避したいレジスタ");
"指定文字列"は、続く括弧でくくった式に、どのようなレジスタやメモリを割 り当てるかを決める。これは
下の表の左側に示した文字列 (正確にはModifiersと Constraints) であり、表の右側に説明を付記した。

また、"アセンブリコード"によって内容が破損してしまうレジスタを3番目の : の後に書いておくと、その
レジスタの内容が事前に退避、事後に復帰されるようにコンパイラが自動でコード生成してくれる。これによっ
て、"アセンブリコード"はレジスタの退避、復帰を省いて書くことができる。

なお、出力と入力、コードによって破損するレジスタは何れも省略可能である。省 略する場合、その部分は空
白にしておく。曖昧でなければ : も省略可能である。

13:Constraint and Modifier
 出力先と入力元に使われるレジスタ指定には、1文字の指定文字もしくはその頭に修飾文字を加えた文字列が
使われる。
 ただし、出力先には、"=r"というように修飾文字 '=' を付ける決まりである。

386系のレジスタや定 数、メモリに対応した指定文字の抜粋
  (公式には Constraint、つまり制限子とい う。詳しくは、gccのinfo[10]のConstraintsの項目を参照。)
"a"
"b"
"c"
"d"
"s"
"D"
"I"
"J"
"K"
"L"
"M"
"N"
"G"
"q"
"r" 
"g"
"m"
"A" 
"f"
"t"
"u"
"0","1","2",...
 
eax
ebx
ecx
edx
esi
edi
0〜31までの定数 for 32 bit shifts
0〜63までの定数 for 64 bit shifts
定数0xff
定数0xffff
0,1,2,3のうちの定数 (shifts for `lea')
0〜255までの定数 (`out'命令向け)
80387標準浮動小数点定数
自動割り当て(eax,ebx,ecx,edxの中から)
自動割り当て(eax,ebx,ecx,edx,esi,ediの中から)
eax,ebx,ecx,edx もしくは許される凡ゆるアドレスのメモリ
許される凡ゆるアドレスのメモリ
eaxとedxを合わせて64bitのlong long型で使う。
数値浮動小数点レジスタの中から自動割り当て
浮動小数点スタックの先頭の数値浮動小数点レジスタ
浮動小数点スタックの2番目の数値浮動小数点レジスタ
入出力オペランド部分で割り当てられたレジスタやメモリを
割り当てられた順に指す。

修飾文字の抜粋 (Modifier Characters)
 =
 
 +
 &
  
  
 
 
書き込み専用を意味し、以前の値を消して、asm文の 
アセンブリ部分で書き込まれた値を持つようにする。
読み書き両用を意味する。 
そのオペランドが、入力オペランドの使用が終わる前に 
破損することに対して適切に対処する。
すなわち、そのオペランドをインプット用のレジスタや
メモリから切り離すことで、入力オペランドが壊れたレ
ジスタに影響されることを防ぐ。
(注意)
x86系で浮動小数点を扱う際は、入力用に"f"を1つでも使うならば、出力オペランドに "=f" ではなく、"=&t" など、"&"付のものを使う必要がある。
また、出力用のオペランドはスタックの先頭、つまり "=&t" から始めなければならない。そうしなければ、まともな動作は保証されない。

14:引数や変数と の対応付け
実際に関数の引数やブロック変数と指定するレジスタやメモリを対応づけるには、"指定文字列"の後ろの括弧内にそれを書くだけで良い。

asm文が実行される順を追って見てみる。

 1.  入力オペランド部分で括弧で囲んだ変数などの式が、その値をもって、指定したレジスタやメモリに割り当てられる。
 また、出力オペランド部分で括弧で囲んだ変数も指定したレジスタやメモリに割り当てられるが、出力専用なので変数の 値は保証されない。
 要するに、入力用の変数の値は指定したレジスタ,メモリの初期値になるが、出力用 の変数の値は指定したレジスタ,メモリの初期値になるとは限らない。

 2.  アセンブリコードが実行される。

 3.  出力オペランド部分で指定したレジスタやメモリのアセンブリコード実行後の値が、対応づけた出力用の変数に保持される。
 入力用の変数の値は、アセンブリコード実行後の対応するレジスタ,メモリの値を反映するとは限らない。(通常反映す るが、asm文以前の元の値となることも多い。)

例えば x,y を入力、 z を出力に対応させるには

int foo(int x, int y)
{
   int z;
   asm ("アセンブリコード" : "=r" (z) : "r" (x), "r" (y));
   return z;
}
或いは、#define文を使って x,y を入力用に使うには、
#define foo(x,y)  \
({ int z;\
asm volatile ("アセンブリコード" : "=r" (z) : "r" (x), "r" (y));
  z; })
というように記述する。こうするとインライン展開されて高速である。(具体例 は17)
普通、"r" や "g" などの自動割り当てを使う。

自動的に退避してほしいレジスタを指定するのであれば、

int foo(int x, int y)
{
   int z;
   asm ("%eaxと%edxの内容を壊すアセンブリコード" : "=r" (z) : "r" (x), "r" (y) : "%eax", "%edx");
   return z;
}
などとする。また、通常はないことだが、参考文献[10]には、− 予測不可能なメモリ破壊を含むアセンブリコードを
書く場合、破壊レジスタの所に  "memory"  を追加で書く。さらに、入出力オペランドとして使われていないメモリ
の予測不可能な破壊がある場合、  asm   ではなく   asm volatile   を使い、
破 壊レジスタに  "memory"  を追加
する。 − とも書かれている。

なお、関数例などで"q"や"r"、"g"や"m"を指定している意味は特にな い。アセンブリコードに書く命令のオペランド、
つまりdestやsourceとして正しければ良いのである。ただ、レジスタに"q"や"r"、メモリに"m"を使っていればあまり
考えずにコードが書ける。

15:指定したレジスタの コード内表記
入出力用に指定した上記のレジスタやメモリは、左から指定した順に、アセンブリコードの
中では %0, %1, %2, %3,... で表記される。
上の例では、"=r" (z) で割り当てられたものが %0 で、同様に"r" (x) が %1、"r" (y) が
%2 である。

また、 %0, %1, %2,... だけではなく、eaxやebxといったレジスタ名も使いたい場合は、
拡張asm中では、%%eax や %%ebxというように、%%を付けるようにする。
というのは、asm文の中で %0, %1,... のような入出力オペランドが使われているときは、
その文中のレジスタ名には % が二つ付く決まりだからである。

16:整数の積の例
 拡張インラインアセンブリを使った整数の掛け算の関数
(注意: アセンブリ文が短い場合、関数の呼び出しは非効率的なので#defineを使って
  17 のように書くことを好む人が多いようである。)

int mul (int x, int y)
{
   asm ("imul %2,%0"
        : "=r" (x) : "0" (x), "r" (y) );
   return x;
}
(解説)
 出力部分 "=r" が x 用のレジスタ指定であり、アセンブリコード内ではレジスタ %0 である。
 入力部分 一つ目の入力は出力で指定した同じレジスタを使うため "0" を指定する。アセンブリ
 コード内では %0 及び %1 兼用である。入出力を兼ねるものはこのように二重に指定する方が良い。
 入力部分 "r" が y 用のレジスタ指定であり、アセンブリコード内ではレジスタ %2 である。

演算としては単純にimulで、x *= yを実行している。
実際に上の関数を使って整数の掛け算をするプログラムを示そう。

/*
         asm_mul_test.c
*/
#include <stdio.h>
#include <stdlib.h>

int mul (int x, int y)
{
   asm ("imul %2,%0" : "=r" (x) : "0" (x), "r" (y));
   return x;
}

int main (int argc, char *argv[])
{
   int a,b;
   if (argc == 3)
   {
      a = atoi (argv[1]);
      b = atoi (argv[2]);
      printf ("%d\n", mul (a, b));
   }
   return 0;
}

実行結果
[akira@localhost mypage]$ gcc asm_mul_test.c
[akira@localhost mypage]$ ./a.out 12 13
156
[akira@localhost mypage]$

(注意点)コンパイルオプションとして -O 等を使っても良い。

17:整数の積の例 2
上記の整数の掛け算を行う関数を#defineを使ってスマートに作ると、以下のようになる。
同じレジスタを入出力に使うので、出力用に"=r"を指定したものを、入力用で"0"を指定することで利用している。
"=r" (_x) と "0" (_x) は %0 、"r" (_y) が %2 である。

#define mul(x,y)   \
({ int _x=(x), _y=(y);\
   asm volatile ( "imul %2,%0": "=r" (_x): "0" (_x), "r" (_y));\
   _x; })
まず _x, _y というブロック変数に x , y を代入し、_x *= _y を実行している。
これは引数の型チェックの為である。そして最後に小括弧 ( ) の中のブロック { } の値を _x としている。
このようにすると、_x = x * y が返ってくるので、このまま関数 int mul(int x, int y) として使える。
全体のプログラムは、次のようになる。
/*
         asm_mul_test2.c
*/
#include <stdio.h>
#include <stdlib.h>

#define mul(x,y)   \
({ int _x=(x), _y=(y);\
   asm volatile ( "imul %2,%0": "=r" (_x): "0" (_x), "r" (_y));\
   _x; })

int main (int argc, char *argv[])
{
   int a,b;
   if (argc == 3)
   {
      a = atoi (argv[1]);
      b = atoi (argv[2]);
      printf ("%d\n", mul (a, b));
   }
   return 0;
}

実行結果
[akira@localhost mypage]$ gcc -O asm_mul_test2.c
[akira@localhost mypage]$ ./a.out 12 13
156
[akira@localhost mypage]$

18:実数の積
浮動小数点の演算も拡張インラインアセンブリによって、出力に"=&t" 、入力に"f"を指定するだけで簡単に書ける。
例えば、倍精度の掛け算を行う関数は#define文を用いるならば

#define mul(x,y)   \
({ double _x=(x), _y=(y);\
   asm volatile ( "fmul %2,%0": "=&t" (_x): "0" (_x), "f" (_y));\
   _x; })
と書け、このブロックで_x = x * y という値が得られる。%0がxの値の入った数値浮動小数点レジスタ、
%2がyの値の入った数値浮動小数点レジスタで、両者にfmulを 実行してその結果をブロック { } の値としている。
なお、"0"は0番目のオペランド(つまり出力用のオペランド)の レジスタを入力用にも使うために指定している。
全体のプログラムは、次のようになる。
/*
  浮動小数点レジスタのスタック%stが使えるが、
  拡張インラインアセンブリで"f"や"t"を使う方が簡単に書ける。
*/
/*
         asm_fmul_test.c
*/
#include <stdio.h>
#include <stdlib.h>

#define mul(x,y)   \
({ double _x=(x), _y=(y);\
   asm volatile ( "fmul %2,%0": "=&t" (_x): "0" (_x), "f" (_y));\
   _x; })

int main (int argc, char *argv[])
{
   double a,b;
   if (argc == 3)
   {
      a = atof (argv[1]);
      b = atof (argv[2]);
      printf ("%f\n", mul (a,b));
   }
   return 0;
}

実行結果
[akira@localhost mypage]$ gcc -O asm_fmul_test.c
[akira@localhost mypage]$ ./a.out 2.2 -3.0
-6.600000
[akira@localhost mypage]$

倍精度の掛け算を行うCの関数として書くならば、次のようになる。

double mul(double x, double y)
{
   asm ("fmul %2,%0" : "=&t" (x) : "0" (x), "f" (y));
   return x;
}
(解説)
 浮動小数点スタックの中の先頭の数値浮動小数点レジス タに x を割り当て、他のある数値浮動
 小数点レジスタに y を割り当てる。前者は"=&t"と"0"で入出力用レジスタとして指定し、%0
 で表される。後者は"f"で指定し、%2 で表される。両者の積が %0 の値となり、それが x と
 して保持され、返値になる。

19:関数例(2進 数で出力)
最後にやや長めの例を示す。ここでも"q"や"r"などの Constraint は深く考えずに指定している。
以下は、整数値 n をコマンドラインから読み込んで2進数で出力するプログラムである。本当はC
のみで書く方が簡単なので、その例も続きに示す。

/*
         asm_print_as_bin.c
*/
#include <stdio.h>
#include <stdlib.h>

/* 整数 n のビット数をビットスキャンで得る関数 */
/*
   n は入出力を兼ねるため、出力"=r"を指定し、
  入力オペランドとして同じものを"0"を用いて指定する。
  bsr命令はdestがレジスタである必要があるので"=g"は避ける。
  また、%eaxを自動的に退避してもらう。
*/
int n_uppermost (int n)
{
   asm __volatile__ (
              "  movl %0,%%eax      \n\t"
              "  bsr  %%eax,%0          "/*Bit Scan Reverse*/
              : "=r" (n) : "0" (n) : "%eax");
   return n;
}

/* nのm番目のビット値を得る関数
  ●実際はマクロで以下のように書いた方が楽である。
  #define judge_m_th_bit(n,m) \
        ((n) & (1<<(m)) ? 1 : 0)
*/
/*
   n は入出力を兼ねるため、出力"=q"を指定し、
  入力オペランドとして同じものを"0"を用いて指定する。
  これも指定した回数として数えるため、 "q" (m) で割
  り当てたレジスタはアセンブリコードの中では %2 で表
  される。
*/
int judge_m_th_bit (int n, int m)
{
   asm __volatile__ ("
           bt   %2,%0          #bit test ==> CF
           jc   one_           #goto one_ when CF is 1
           movl $0,%0          #output 0 if the bit was 0
           jnc  out_
         one_:
           movl $1,%0          #output 1 if the bit was 1
         out_:
   " : "=q" (n) : "0" (n), "q" (m));
   return n;
}

/* nを2進数で表示する関数 */
void print_as_bin (int n)
{
   int m;
   m = n_uppermost(n);
   for ( ; m > -1; m--)
      printf ("%d", judge_m_th_bit(n, m));
}

int main (int argc, char *argv[])
{
   int a;
   if (argc == 2)
   {
      a = abs (atoi (argv[1]));
      print_as_bin (a);
      putchar ('\n');
   }
   return 0;
}

実行結果
[akira@localhost mypage]$ gcc asm_print_as_bin.c
[akira@localhost mypage]$ ./a.out 11
1011
[akira@localhost mypage]$

比較の為、標準のC言語で書くと以下のようになる。

/*
         print_as_bin.c
*/
#include <stdio.h>
#include <stdlib.h>

/* nを2進数で表示する関数 */                                 
void print_as_bin (int n)
{
   int m = 1, M, is_visible = 0;
   M = 1 << (sizeof(int) * 8 - 1);

   if (!n)
      putchar ('0');
   else
      while(m)
      {
         if ((!is_visible) && (n & M))
            is_visible = 1;
         if (is_visible)
            printf ("%d", (n & M ? 1 : 0));
         n <<= 1;
         m <<= 1;
      }
}

int main (int argc, char *argv[])
{
   int a;
   if (argc == 2)
   {
      a = abs (atoi (argv[1]));
      print_as_bin (a);
      putchar ('\n');
   }
   return 0;
}

実行結果
[akira@localhost mypage]$ gcc print_as_bin.c
[akira@localhost mypage]$ ./a.out 11
1011
[akira@localhost mypage]$

■20:※Linuxのwriteシステムコール■
これまでは出力にprintfやputsを利用してきたが、asm文中でシステムコールの書き出し命令を
使うこともできる。ただしOSに依存するので、各レジスタにどのような値を入れてソフトウェア
割り込みをしたら良いのかは、各OSの解説書やアセンブリに関する書籍を参照されたし。
DOSのシステムコールについては1980年代の書籍や雑誌に良く解説がされていた。
幸いにして、Linuxのシステムコールについては、ローカルファイル

     /usr/include/linux/syscall.h
     /usr/src/linux/arch/i386/kernel/entry.S
     /usr/include/linux/linkage.h

を読むと分かることが多い。また、詳しい情報はやはり参考文献[0]を参照されたし。
なおファイルデスクリプター fd は、0:stdin, 1:stdout, 2:stderrという対応である。
ソフトウェア割り込みはLinuxでは  int $0x80  である。
/*
   asm_lin_sys_write.c
*/
/*
   ※これはx86 Linux専用のコード
   ※プロセッサに依存する上にOSにも依存するのである。
   システムコール write で標準出力に書くテストプログラム
*/

/* size_t を得るためのインクルード */
#include <linux/types.h>

/*
   文字列 s を n 文字だけstdoutに書く関数。
   writeはシステムコール番号4、stdoutは fd が1。

   %eaxに4、%ebxに1、%ecxに文字列のアドレス、%edxに出力
   したい文字の個数を入れ、割り込みするだけ。

   "=a" (result)によりアセンブリコード実行後の%eaxレジス
   タの内容をresultとして出力する。
   "=a", "c", "d"を使っているので%eax,%ecx,%edxは塞がっ
   ており、これらをさらに退避しようとするとコンパイルで
   きない。
*/
int wr_n_sys_call (const char* s, size_t n)
{
   int result = 0;
   __asm__ __volatile__ ("
      movl $4,%%eax #system call number is 4 for write
      movl $1,%%ebx #output to stdout
      int  $0x80    #call software interrupt
   " : "=a" (result) : "c" (s), "d" (n) : "%ebx");

   return result;
}

/* 文字数を得るための関数(最大1万文字) */
#define MAX_CHARS 10000
size_t get_str_length (const char* s)
{
   size_t i;
   for (i = 0; *s != '\0' && i < MAX_CHARS; s++)
      i += sizeof(char);
   return i;
}
#undef MAX_CHARS

/* puts関数の代用に使える関数 */
int yaputs (const char *s)
{
   int result;
   if ((result = wr_n_sys_call (s, get_str_length (s))))
      wr_n_sys_call ("\n", sizeof(char));
   return result;
}

int main (int argc, char *argv[])
{
   if (argc == 2)
      yaputs (argv[1]);
   return 0;
}
実行結果
[akira@localhost mypage]$ gcc asm_lin_sys_write.c
[akira@localhost mypage]$ ./a.out irohanihoheto
irohanihoheto
[akira@localhost mypage]$

ページの目次へ戻る


□□SIMDに関する追記□□
アセンブリでは、SIMD(Single Instruction Multiple Data, 1命令で複数データを処理する命令)が使える。
昨今のgas、gccでは、MMXや3D Now!のレジスタ、命令はごく普通にasm()文で記述して使用できる。Pentium III
以降で搭載されたSSE(Streaming SIMD Extension)や、Athlonの拡張3D Now!も利用できるが、ここで説明する
のはMMXと3D Now! だけにしておく。

21:SIMDのサポートチェック
まず、手持ちのCPUでどのようなSIMD命令セットがサポートされているかチェックしてみよう。
以下のプログラムは、CPUID命令を使った標準的なチェック方法である。
asm文では、入出力兼用にブロック変数 i 、入力用に引数 id_t を用い、%eax,%ebx,%ecx,%edxを自動退避している。

/*
   asm_simd_test.c
*/
#include <stdio.h>
/*
   CPUID命令の後で %edx と比較すべき値
*/
#define ID_TEST_MMX 0x800000
#define ID_TEST_SSE 0x2000000
#define ID_TEST_3DNOW 0x80000000
/*
   CPUIDでMMXやSSEを検知する関数 (インテルやAMDのマニュアル通り) [7,8]
   注意: CPUIDでは、%eax, %ebx, %ecx, %edx を退避する必要がある。
*/
int cpuid_simd_detect (int id_t)
{
   int i = 1; /* CPUID命令の標準関数は %eax が 1 */
   if (id_t == ID_TEST_3DNOW)
      i = 0x80000001; /* CPUID命令の拡張関数の利用 */

   __asm__ __volatile__ ("
        movl  %1,%%eax
        cpuid
        testl %2,%%edx
        jnz   yes_
        movl  $0,%0
        jz    no_
      yes_:
        movl  $1,%0
      no_:
     " : "=m" (i) : "0" (i), "m" (id_t)
       : "%eax", "%ebx", "%ecx", "%edx");
   return i;
}
/*
   CPUID命令の有無を返す。
   以下のコードはAMDのマニュアル通り。[9]
   EFLAGSレジスタの21番ビットが書き込み可能か否か
   で判別する。
*/
int cpuid_supported (void)
{
   int v;
   __asm__ __volatile__ ("
          pushf
          popl  %%eax             #get EFLAGS
          movl  %%eax,%%ebx
          xor   $0x00200000,%%eax #toggle bit 21
          pushl %%eax             #write bit 21 of EFLAGS...
          popf                    #  .
          pushf                   #  .
          popl  %%eax             #  .
          cmpl  %%ebx,%%eax       #Is bit 21 changed?
          jz    no_cpuid_
          movl  $1,%0
          jmp   out_
    no_cpuid_:
          movl  $0,%0
    out_:
   " : "=r" (v) :: "%eax", "%ebx");

   return v;
}
/*
   CPUID拡張関数の最大入力値が0x80000000より大きいか
   否かを調べる。詳しくは参考文献[7,8,9]を参照。
*/
int cpuid_extended (void)
{
   unsigned int v;
   __asm__ __volatile__ ("
          movl $0x80000000,%%eax
          cpuid
          movl %%eax,%0
   " : "=r" (v) :: "%eax", "%ebx", "%ecx", "%edx");

   return (v > 0x80000000 ? 1 : 0);
}

int main ()
{
   puts ("SIMD命令セットのサポートチェック");
   if (!cpuid_supported ())
   {
      puts ("No CPUID instruction.");
      return 0;
   }
   printf ("  MMX : %d\n", cpuid_simd_detect (ID_TEST_MMX));
   printf ("  SSE : %d\n", cpuid_simd_detect (ID_TEST_SSE));
   printf ("3DNOW : %d\n", cpuid_extended () ?
                           cpuid_simd_detect (ID_TEST_3DNOW): 0);
   return 0;
}

AMDのDuronプロセッサで実行した結果

[akira@localhost mypage]$ gcc asm_simd_test.c
[akira@localhost mypage]$ ./a.out
SIMD命令セットのサポートチェック
  MMX : 1
  SSE : 0
3DNOW : 1
[akira@localhost mypage]$

妥当な結果である。

22:MMXを使った例(ベ クトル引き算)
MMXレジスタとして、%mm0 〜 %mm7 の8つの64-bitレジスタが利用できる。(拡張asmでは %%mm0 のように%%付きで書く。)
MMXレジスタは、64-bitを分割して以下のようなデータ型を扱える。
★ MMXデータ型の表 (詳しくはインテルの開発者用マニュアル[7]を参照)
データ型の名称 パックの仕方
Packed bytes 8-bitのデータ × 8   (char array[8])
Packed words 16-bitのデータ × 4  (short int array[4])
Packed doublewords 32-bitのデータ × 2  (int array[2])
Quadword 64-bitのデータ × 1  (long long int variable)

o 32-bitのデータの移動には movd 、64-bitのデータの移動には movq を使う。
o MMXレジスタは浮動小数点レジスタの流用なので、MMX命令利用後には、emms 命令によってプロセッサを浮動小数点状態に戻す必要がある。
o 表中の各々のデータ型向けに、例えば足し算であれば順に paddb、paddw、paddd といった命令が用意されている。

以下は、Packed words同士の引き算の例である。
入力部分でアドレスを使っているので、asm文中ではメモリを (%1) などとして使用している。

/*
   asm_mmx_psubsw_test.c
*/
#include <stdio.h>

int main ()
{
   short int a[4] = {1111,1112,1113,1114};
   short int b[4] = {1111,2112,3113,4114};
   short int c[4];

   /*Word 4要素のベクトル同士の符号付き引き算 a - b*/

   /*
      入力値には配列 a,b のアドレス &a,&b を使い、そのアドレスにある
      メモリ内容を movq で %mm0,%mm1に移動する。
      引き算の結果は出力用の配列 c に割り当てたレジスタ %0 に movq で
      移動する。
   */
   asm volatile ("
      movq   (%1),%%mm0
      movq   (%2),%%mm1
      psubsw %%mm1,%%mm0
      movq   %%mm0,%0
      emms
     " : "=g" (c) : "r" (&a), "r" (&b));

   printf ("%d,%d,%d,%d\n", c[0], c[1], c[2], c[3]);
   return 0;
}

実行結果
[akira@localhost mypage]$ gcc asm_mmx_psubsw_test.c
[akira@localhost mypage]$ ./a.out
0,-1000,-2000,-3000
[akira@localhost mypage]$

23:3D Now!を使った例(除算推定値)
3D Now!では、MMXレジスタを利用して単精度浮動小数点(float)のデータを二つ同時に演算可能である。
o emms よりも高速な femms 命令によってプロセッサを浮動小数点状態に戻せる。

以下は、3d Now!を使った高速で低精度の割り算の例である。

/*
   asm_3dnow_pfrcp_test.c
*/
#include <stdio.h>

int main ()
{
   float a[2] = {1.0, 2.0};
   float b = 4.0;
   float c[2];
   /* a/b の低精度推定値を計算する。(正確な結果はc={0.25,0.5})*/

   /*
      一つ目の入力値には配列 a のアドレス &a を使い、そのアドレスにある
      メモリ内容を movq で %mm0 に移動する。二つ目の入力値は変数 b を使
      い、movd で %mm1 に移動する。
      まず %mm1 の逆数をとり、それを %mm0 に掛ける。
      結果は出力用の配列 c に割り当てたレジスタ %0 に movq で移動する。
   */
   asm volatile ("
      movq  (%1),%%mm0
      movd  %2,%%mm1
      pfrcp %%mm1,%%mm1
      pfmul %%mm1,%%mm0
      movq  %%mm0,%0
      femms
     " : "=g" (c) : "r" (&a), "r" (b));

   printf ("c={%g,%g}\n", c[0], c[1]);
   return 0;
}

実行結果
[akira@localhost mypage]$ gcc asm_3dnow_pfrcp_test.c
[akira@localhost mypage]$ ./a.out
c={0.249992,0.499985}
[akira@localhost mypage]$

低精度であることは否めないが、ともかく高速である。精度も画像処理には十分で ある。

ページの目次へ戻る

homeへ戻る


    - gccでx86インラインアセンブリ(インラインアセンブラ)を使う際の留意点 - s-akira (1999-2006) -








[]