わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

構造体返しの仕組みを探る

2007年になりました.今年もよろしくお願いいたします.
だいぶ前の授業のフォローなのですが,「関数の戻り値に構造体を書くことができるのはなぜか」をうまく説明できないか,ちょっと検討したり,手を動かしてみました.
私が学生のとき,コンパイラに関する授業で学んだのは,「戻り値もスタックに置く」という方法でした.
以下,機械語(アセンブリ言語)での挙動をかいつまんで説明します.メモリ上にスタック領域があって,値を乗せたり(push),最上段の値を取り出したり(pop)できるものとします.

  1. 関数呼び出しをするにあたり,最初に,戻り値を格納する領域を確保するためのpushをします.次に関数呼び出しの戻り先アドレス,そして仮引数の領域をpushします.
  2. それから,関数の最初のアドレスに制御を移します.
  3. 関数の中では,ローカル変数の領域をpushして,あとは何らかの処理をします.
  4. 関数が呼び出された時点で,戻り値を格納するアドレスが計算できますので,戻り値をそこに格納します.
  5. 関数の処理を終えるときは,ローカル変数の領域,仮引数の領域をpopします.そして関数呼び出しの戻り先アドレスもpopして,その値に制御を移します.それにより,関数の戻り値が,スタックの最上段に乗っていることになります.

当時,授業やコンパイラ作成演習でのソース言語はPascalのサブセット*1,ターゲット言語はCASLでした.
Cでも同じ振る舞いをするのか,少し調べてみました.コンパイラGCCです.コンパイル時のオプションに「-S」をつけて,例えば「gcc -S program.c」を実行すれば,「program.s」という名前のファイルが生成されます.これが,アセンブリ言語で書かれた(狭義の)コンパイルの結果です.
いろいろコードを書いて見ましたが,上の方法とは異なっていると言ってよさそうです.戻り値となる構造体の領域を確保しているのは,呼び出す側です.関数の中で,そのアドレスに値を格納しています.
単純な構造体であれば,レジスタのみで値を受け渡ししています.構造体が複雑になれば,ライブラリ関数memcpyを使って値をコピーしています.関数の定義が同じでも,元となる構造体が異なれば,受け渡しの方法が異なるため,アセンブリコードが違ってきます.
以下は,動作確認用のソースファイルです.

#include <stdio.h>

struct point1 {
  int x, y;
};

struct point2 {
  int x, y;
  int z[20];
};

struct point1 returnstruct1(void)
{
  struct point1 p = {1, 2};
  return p;
}

struct point2 returnstruct2(void)
{
  struct point2 p = {1, 2};
  return p;
}

int main(void)
{
  struct point1 p1;
  struct point2 p2;

  p1 = returnstruct1();
  printf("p1(%d,%d)\n", p1.x, p1.y);
  p2 = returnstruct2();
  printf("p2(%d,%d)\n", p2.x, p2.y);

  return 0;
}

*1:ここでの「サブセット」(subset,部分集合)は,仕様が,本来のものから一部欠けているという意味です.