構造体の値渡しとレジスタ

おそらくほとんどのプログラマでさえ気にしない話題です。

C言語でプログラムを書くとまれに構造体を値渡しで関数呼び出しする例が出てきます。

WinAPIでもSetFilePointerExのようにLARGE_INTEGER構造体を引数で呼び出すパターンがあり、これに該当します。

このとき、どのように対象の関数を呼び出すのでしょうか?

なお、関数の呼び出し規約に関する基礎的な情報は理解しているものとします。(cdecl、stdcall、fastcallでのスタック・レジスタの使い方(この記事を参照のこと)

一応記事の補足をしておくと、x86のfastcallは第一引数がecx、第二引数がedxになります。またx64の場合は、整数の第一引数がrcx、第二引数がrdx、第三引数がr8、第四引数がr9を使用します。

ちなみに、x64の場合に限り、浮動小数でも第一引数から第四引数まではレジスタ経由になります。(xmmレジスタに浮動小数が入っていることになる)

一応ただVisualStudioで調べた結果なので最適化とか値渡し時の速度とか気にする人でない限り意味がない情報だと思います。

速度を気にするならほとんどが参照渡しかポインタを使うことになると思います。

(C++上で参照渡しとポインタの値渡しは記述が異なるだけで命令上は同じ物(対象のオブジェクトのアドレス)を扱うので、引数はレジスタ幅の整数型一つになります)

構造体がレジスタサイズよりも大きい、もしくはレジスタ経由で引数を渡さない場合

たとえば、以下のようなパターンです。

struct numpack { int a,b,c,d; };
void f(numpack v){ }
void g(void)
{
	numpack n;
	n.a = n.b = n.c = n.d = 0;
	f(n);
}

この場合は単純に引数のメモリ位置に構造体のデータがそのままあるように転送されて呼び出されます。つまりただの値渡しです。

感じとしては、ちょっと書き方が微妙ですが、呼び出すときのメモリ状態は(x86の場合では)

void f(int v.a,int v.b,int v.c,int v.d){ }

のように関数を宣言したときと同じようなメモリ配置になります。

(単にメモリ領域がスタック上に確保されてそのまま値がコピーされるのだよ~という意味を言っているだけで、実際にこうなるわけではありません)

構造体がレジスタサイズ以下であり、引数をレジスタ経由で渡すように定義されているとき

以下のようなパターンが該当します。(x86としてみてください)

struct numpack { short a,b; };
void __fastcall f(numpack v){ }
void g(void)
{
	numpack n;
	n.a = n.b = 0;
	f(n);
}

このときは、fastcallの呼び出し規約を使って、構造体のデータがそのまま(構造体をメモリからみた一つの整数値と仮定して)ecxレジスタに転送された後呼び出されます。

なので、単体でaとかbの値を取り出すときには呼び出し先でビットシフトなどが使用されることになります。

構造体がレジスタサイズ以下であるが、複数引数が混じるとき

これに関してはx64とx86で異なった動きをしていました。

#ifdef __WIN64
struct numpack { int a,b; };
#else
struct numpack { short a,b; };
#endif
void __fastcall f(int x,numpack v){ }
void g(void)
{
	numpack n;
	n.a = n.b = 0;
	f(n);
}

このとき、呼び出しの状態がちょっと違って、x86の場合、edxを使わずにそのまま構造体のデータをメモリ上に転送していました。

x64の場合はrdxに転送しているのでこれはx64の呼び出し規約の通りになっていました。

構造体がレジスタサイズより大きいが、複数引数があるとき

x86では上記の例でほぼ説明がついていますのでx64の例だけです。

struct numpack { int a,b,c,d; };
void f(int u,numpack v,int w){ }
void g(void)
{
	numpack n;
	n.a = n.b = n.c = n.d = 0;
	f(2,n,3);
}

で、これ不思議だったのは、rcx <= 2,r8 <= 3となっているのに対して構造体の部分だけちゃんとメモリ経由で渡されているんですね・・・。

わかりやすく考えるなら、無効引数型みたいなものを考えて、それに擬似的にnoneを割り当てるなら、関数fの呼び出しは

void f(int u,none,int w,none,numpack v){ }

(メモリ上は)こんな関数宣言っぽくなります。

何ともおもしろい仕様ですね。

まあ、プログラム解析では知っておいた方がいいかもしれない情報ですが

関数呼び出し時に構造体の呼び出しをどう処理するか、を定めた情報ですので重要かもしれません。

それ以外のコンパイラだとどういう風に反応するのか、それはよく知りません。内部的な呼び出しだけならこれ以外の方法でも実は問題ないためです。

(呼び出し時に規約が整合されている状態なら何にも問題がないですので)

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

この記事のトラックバック用URL