型属性の指定

 キーワード__attribute__により,struct型とunion型を定義する際,その型に特殊な属性を指定することができます.このキーワードの後に,2重の丸括弧(())に囲まれた属性指定が続きます.現在,3個の属性aligned,packed,transparent_unionが型に対して定義されています.

 属性はいずれも,キーワードの前後に__を付けて指定することもできます.これにより,同じ名前をもつマクロが定義済みであるかどうかを心配することなく,ヘッダファイルの中でこの属性を使うことができるようになります.たとえば,alignedの代わりに__aligned__を使うことができます.

 aligned属性とtransparent_union属性は,typedef宣言の中において,あるいは完結した列挙型,構造体型,共用体型の定義の終端の波括弧 }の直後において指定することができます.また,packed属性は,定義の終端の波括弧 }の後においてのみ指定することができます.

 また,終端の波括弧 }の後ではなく,列挙タグ,構造体タグ,共用体タグと型の名前の間において,属性を指定することもできます.

○aligned (alignment)

 この属性は,指定された型の変数の最小のアラインメントの値を(バイト単位で)指定します(リスト26).

〔リスト26〕alignedを指定したソースtest100.c)
#include <stdio.h>
//型属性の指定 aligned (alignment) 
struct data1 { short f[3]; } __attribute__ ((aligned (8)));
struct data2 { short f[3]; };
struct data1 dataa_;
struct data1 datab_;
struct data2 datac_;
struct data2 datad_;
int main(void)
{
    asm("nop");
    datab_  =   dataa_;
    asm("nop");
    datad_  =   datac_;
    asm("nop");
    return 0;
}

 この場合,ダブルワード単位に転送することができるので,実行時の効率が向上します.

 必要に応じてアラインメントの数値を指定できますが,リンカによっては制限がある場合があります.

 リスト27のようにeaxレジスタとaxレジスタで,それぞれ32ビット転送と16ビット転送を行っています.

〔リスト27〕alignedを指定したソースから生成したアセンブラtest100.s)
    .file       "test100.c"
    .version    "01.01"
gcc2_compiled.:
.text
    .align  4
.globl  main
    .type   main,@function
main:
    pushl   %ebp
    movl    %esp,%ebp
#APP
    nop
#NO_APP
    movl    dataa_,%eax
    movl    %eax,datab_
    movl    dataa_+4,%eax
    movl    %eax,datab_+4
#APP
    nop
#NO_APP
    movl    datac_,%eax
    movl    %eax,datad_
    movw    datac_+4,%ax
    movw    %ax,datad_+4
#APP
    nop
#NO_APP
    xorl    %eax,%eax
    jmp .L2
    .p2align    4,,7
.L2:
    movl    %ebp,%esp
    popl    %ebp
    ret
.Lfe1:
    .size   main,.Lfe1-main
    .comm   dataa_,8,8
    .comm   datab_,8,8
    .comm   datac_,6,2
    .comm   datad_,6,2
    .ident  "GCC: (GNU) 2.95.3 20010315 (release)"

○packed

 前項「変数の属性の指定」でpackedを説明しましたが,型の指定でこれを行うと変数ではなく,その型全体がpacked属性になります.つまりstruct型とunion型に対してこの属性を指定するのは,構造体または共用体の個々のメンバにpacked属性を指定するのと同等です.

○transparent_union

 名前のとおり,透過性共用体とでもいいましょうか.unionの型定義にこれが指定された場合,その共用体の型をもつ関数パラメータがあると,その関数の呼び出しが特殊な方法で扱われるようになります.

 これには,大きく分けて二つの機能があります.

 一つ目の機能:transparent_union属性が指定された共用体型に対応する引き数は,その共用体に定義された任意のメンバの型をもつことができます.キャストは必要ありません.また,その共用体のメンバにポインタ型のものがあれば,対応する引き数にはヌルポインタ定数またはvoidポインタ式を使うことができます.

 なお,その共用体のメンバにvoidポインタ型のものがあれば,対応する引き数には任意のポインタ式を使うことができます.

 二つ目の機能:関数に対してその引き数が渡される際には,transparent_union属性が指定された共用体自体の呼び出し規約ではなく,その共用体の1番目のメンバの呼び出し規約が使われます.

 この機能は次のような場合に有用です.たとえば,互換性のために複数のインターフェースをもつライブラリ関数に使用できます.くわしく説明すると,wait関数はPOSIXとの互換のためにint *型の値を受け付けなければなりません.その一方で,4.1BSDインターフェースとの互換のためにunion wait *型の値を受け付けなければならないのです.

 もしwaitのパラメータがvoid *型であったとすると,waitはどちらの引き数も受け付けるでしょうが,その他の任意のポインタ型も受け付けることになってしまい,引き数の型チェックがあまり役に立たなくなるでしょう.こうする代わりに<sys/wait.h>では,そのインターフェースを次のように定義することができます.

  typedef union
   {
   int *__ip;
   union wait *__up;
   } wait_status_ptr_t __attribute__ ((__transparent_union__));

  
  pid_t wait (wait_status_ptr_t);

 このインターフェースでは,int *型とunion wait *型の引き数どちらでも渡すことができます.

  int w1 () { int w; return wait (&w); }
  int w2 () { union wait w; return wait (&w); }

 このインターフェースでは,waitの実装例は次のようになります.

  pid_t wait (wait_status_ptr_t p)
  {
   return waitpid (-1, p.__ip, 0);
  }

○unused

 前項と同じように,この属性が型に対して指定されると,その型をもつ変数はおそらく使われないはずであるということを意味します.GNU Cは,その変数については警告メッセージを出力しません.

Cの式をオペランドとしてもつアセンブラ命令

 asmを使ったアセンブラ命令の中でオペランドをCの式を使って指定することができます.このことは,使いたいデータがどのレジスタまたはメモリ位置に保持されるのかを推測する必要がないということを意味しています.

 マシン記述(machine description)の中で使われるものとよく似たアセンブラ命令テンプレートに加えて,個々のオペランドのoperand constraint stringを指定しなければなりません.

 拡張されたアセンブラ命令の構文は次のとおりです.

  asm (アセンブリコード : 出力オペランド : 入力オペランド

: 保持されないレジスタ);

○operand constraint stringについて

 出力オペランドや入力オペランド中で,それに続く括弧でくくった式に,どのようなレジスタやメモリを割り当てるかを決めます.この概略は以下に記します.

○アセンブリコードについて

 内容が破損してしまうレジスタを3番目の : の後に書いておくと,レジスタの退避,復帰のためのコードを自動的に生成してくれます.プログラマはレジスタの退避,復帰を省いて書くことができます.

○operand constraint stringについての説明

 x86系でよく使われる制約は次のとおりです.

“r”:そのオペランドがレジスタでなくてはならないことを表します
“f”:浮動小数点レジスタでなくてはならないことを表します
“m”:メモリオペランドでなくてはならないことを表します
“g”:メモリでもレジスタでも構わないときにこれを指定します
“q”:EAX,EBX,ECX,EDXレジスタに割り当てることを意味します
“a”,“b”,“c”,“d”,“D”,“S”,“B”:

それぞれ,EAX,EBX,ECX,EDX,EDI,ESI,EBPの各レジスタに割り当てることを意味します

“0”,“1”,……:

これらの数字は入力オペランドのみに指定できます.出力オペランド%0や%1と同一であることをコンパイラに教えることができます

 例として単純な足し算を行うプログラムソースをリスト28リスト29に掲載します.

〔リスト28〕asm命令のoperand constraint string概略を説明するCソースtest105.c)
#include <stdio.h>
//Cの式をオペランドとして持つアセンブラ命令
int main(void)
{
    int x   =   100;
    int y   =   500;
    int result;
    asm("nop");
//#割り当て場所はメモリでもレジスタでも構わない設定
    asm("movl  %0,  %%eax" :: "g"(x): "ax");
    asm("movl  %0,  %%ebx" :: "g"(y): "bx");
    asm("addl  %%ebx, %%eax" ::: "ax","bx");
    asm("movl  %%eax, %0"  : "=g"(result) :: "ax");
    asm("nop");
//割り当て場所はレジスタである
    asm("movl  %0,  %%eax" :: "r"(x): "ax");
    asm("movl  %0,  %%ebx" :: "r"(y): "bx");
    asm("addl  %%ebx, %%eax" ::: "ax","bx");
    asm("movl  %%eax, %0"  : "=r"(result) :: "ax");
    asm("nop");
//割り当て場所はメモリである
    asm("movl  %0,  %%eax" :: "m"(x): "ax");
    asm("movl  %0,  %%ebx" :: "m"(y): "bx");
    asm("addl  %%ebx, %%eax" ::: "ax","bx");
    asm("movl  %%eax, %0"  : "=m"(result) :: "ax");
    asm("nop");
//割り当て場所はEAX, EBX, ECX, EDXレジスタのいずれか
    asm("movl  %0,  %%eax" :: "q"(x): "ax");
    asm("movl  %0,  %%ebx" :: "q"(y): "bx");
    asm("addl  %%ebx, %%eax" ::: "ax","bx");
    asm("movl  %%eax, %0"  : "=q"(result) :: "ax");
    asm("nop");
//
    asm("movl  %0,  %%eax" :: "D"(x): "ax");        //EDIレジスタに割り当て指定
    asm("movl  %0,  %%ebx" :: "S"(y): "bx");        //ESIレジスタに割り当て指定
    asm("addl  %%ebx, %%eax" ::: "ax","bx");
    asm("movl  %%eax, %0"  : "=c"(result) :: "ax"); //ECXレジスタに割り当て指定
    asm("nop");
    printf("%d\n",result);
    return 0;
  }

〔リスト29〕asm命令のoperand constraint string概略を説明するCソースから生成したアセンブラtest105.s)
    .file       "test105.c"
    .version    "01.01"
gcc2_compiled.:
.section    .rodata
.LC0:
    .string "%d\n"
.text
    .align  4
.globl  main
    .type   main,@function
main:
    pushl   %ebp
    movl    %esp,%ebp
    subl    $28,%esp
    pushl   %edi
    pushl   %esi
    pushl   %ebx
    movl    $100,-4(%ebp)
    movl    $500,-8(%ebp)
#APP
#割り当て場所はメモリでもレジスタでも構わない設定
    nop
    movl    -4(%ebp),%eax
    movl    -8(%ebp),%ebx
    addl    %ebx,%eax
    movl    %eax,-12(%ebp)
    nop
#NO_APP
    movl    -4(%ebp),%edx
#APP
#割り当て場所はレジスタである
    movl    %edx,%eax
#NO_APP
    movl    -8(%ebp),%edx
#APP
    movl    %edx,%ebx
    addl    %ebx,%eax
    movl    %eax,%edx
#NO_APP
    movl    %edx,%eax
    movl    %eax,-12(%ebp)
#APP
    nop
#割り当て場所はメモリである
    movl    -4(%ebp),%eax
    movl    -8(%ebp),%ebx
    addl    %ebx,%eax
    movl    %eax,-12(%ebp)
    nop
#NO_APP
    movl    -4(%ebp),%edx
#APP
#割り当て場所はEAX, EBX, ECX, EDXレジスタのいずれか
    movl    %edx,%eax
#NO_APP
    movl    -8(%ebp),%edx
#APP
    movl    %edx,%ebx
    addl    %ebx,%eax
    movl    %eax,%edx
#NO_APP
    movl    %edx,%eax
    movl    %eax,-12(%ebp)
#APP
    nop
#NO_APP
    movl    -4(%ebp),%edi
#APP
    movl    %edi,%eax   #EDIレジスタに割り当て指定
#NO_APP
    movl    -8(%ebp),%esi
#APP
    movl    %esi,%ebx   #ESIレジスタに割り当て指定
    addl    %ebx,%eax
    movl    %eax,%ecx   #ECXレジスタに割り当て指定
#NO_APP
    movl    %ecx,%eax
    movl    %eax,-12(%ebp)
#APP
    nop
#NO_APP
    addl    $-8,%esp
    movl    -12(%ebp),%eax
    pushl   %eax
    pushl   $.LC0
    call    printf
    addl    $16,%esp
    xorl    %eax,%eax
    jmp .L2
    .p2align    4,,7
.L2:
    leal    -40(%ebp),%esp
    popl    %ebx
    popl    %esi
    popl    %edi
    movl    %ebp,%esp
    popl    %ebp
    ret
.Lfe1:
    .size   main,.Lfe1-main
    .ident  "GCC: (GNU) 2.95.3 20010315 (release)"

 これらのリストのとおり,制約を指定するとそのとおりに変数の割り当てを行います.

 なお,これはIntel 386系に関する情報です.ほかのプロセッサについても,いろいろなoperand constraint stringがあります.ARMファミリ,AMD 29000ファミリ,IBM RS6000,Intel 960,MIPS,Motorola 680x0,SPARC……以上のプロセッサ特有の設定があるので,詳細はGCCマニュアルを参照してください.

Copyright 2003 岸 哲夫

Copyright 1997-2017 CQ Publishing Co.,Ltd.

 

NEW記事内インデックス    連載インデックスはこちら   Interfaceのトップ
プロトタイプ宣言と古い方式の関数定義
コメントの形式/識別子の名前の中のドル記号/ESC文字について/型あるいは変数のアラインメントを問い合わせる/変数の属性の指定
◆型属性の指定/Cの式をオペランドとしてもつアセンブラ命令
アセンブラコードの中で使われる名前の制御/指定されたレジスタの中の変数/代替キーワード/不完全なenum型/関数名の文字列/関数の復帰アドレスとフレームアドレスの獲得