キーワード

C言語, コンパイラ, 引数, 返却値, スタック, スクラッチレジスタ


これだけは覚えよう


関数呼出

C言語やPascalなどの高級言語の多くは、関数を作ることができます。ある程度の処理をひとつの関数(手続き)として分離することで、問題を簡単にしたり、使い回ししやすくしたり、コードの見栄えを良くしたりできます。呼び出す側は処理に必要な情報を与え、呼び出された側は渡された情報のみ(※)を用いて処理を行って答えを返します。

プログラムが計算を行うためには、レジスタを使用する必要があります。しかし、呼出元も呼出先もレジスタを使用するので、そのままではレジスタを取り合ってケンカがおきます。これを調停するのが呼出規約です。

呼出規約の中には、次のような決め事があります。

引数をスタックに積む場合、右から積むか左から積むかも呼出規約のひとつです。ただし、評価順序は仕様で決まっているので常に一定です (argument-expression-list := argument-expression-list, assignment-expression)

(※) 与えられた情報だけを使って処理を行なうプログラムを、再入可能なプログラムといいます (正確には違うけど)。大域変数などを触ると予想外の動きをすることがあり、周りを混乱の渦に陥れる原因となるので、できる限り与えられた情報だけで仕事をするのが良いとされています。

関数の名前

呼出規約のうち、一番最初に「シンボル名称」の話をしましょう。C言語でvoid func(void);という関数を作ったら、その関数にどういうシンボル名が付くか知ってますか? 多くの場合、次の3つのうちのどれかのパターンになります。

シンボル命名規約を簡単に知るには、ダミーの関数を作るのが一番手っ取り早いです。例えば、SuperH用のgccのシンボル命名規約は次の方法で調べることが出来ます。ARMの場合はarm-elf-gccを、V850の場合はv850-nec-elf-gccをsh-hitachi-elf-gccの代わりに起動します。

% echo "void foo(void) {}" > a.c
% sh-hitachi-elf-gcc -S a.c
% grep function a.s
.type _foo,@function

この結果から、SuperH用のgccは"foo"という関数を作ると"_foo"という名前をつけるのだということがわかります。アセンブリ言語から関数や変数を参照する場合は名称の前に"_"を付ける必要があり、C言語から呼出可能なアセンブリ言語関数を作るには"_"をつける必要があることがわかります (C言語から呼び出すときには"_"はいらない)

ちなみに、名称の後ろによく分からない文字が並ぶケースは、C++言語を使用するとよく起こります(関数オーバーロードのため)。C++言語からアセンブリ言語の関数を呼ぶ場合や、その逆をしたい場合はextern "C"を付けます。例えば、extern "C" void foo(int);といった感じです。関数オーバーロードは使用できなくなりますが、名称規約がC言語のものになり、単純化されます。


レジスタの種類

多くの場合、引数も返却値もレジスタに積まれます。呼出規約によって、レジスタは次の3種類に分類されます。

名称意味
呼出元退避レジスタ (Caller saved register)関数を呼び出す側 (呼出元, caller) が値を保存しなければいけないレジスタ
呼出先退避レジスタ (Callee saved register)関数として呼び出された側 (呼出先, callee) が値を保存しなければいけないレジスタ
特殊用途レジスタ (Special register)特殊な目的のために使ってはいけないレジスタ

これを違う言葉で言い換えると、こうなります。

呼出規約を調べる作業の殆どは、「レジスタを上の分類で分ける作業」になります。これが出来れば殆ど終わったも同然です。


上のレジスタをより細かく分類すると、次のようになります。


呼び出し方、戻り方


参考資料

参考資料として、gccのうち私が知っているプロセッサの呼出規約を載せておきます。

記述の説明
青い枠関数を呼び出す側(親,caller)が退避する
赤い枠呼び出された関数側(子,callee)が退避する
黄色の枠上記のどちらにも当てはまらない (自動退避,目的固定など)
緑色の枠補足説明
赤いレジスタ仮想レジスタ (実際にはない)
小さい字のレジスタ知らなくてもよいレジスタ
割付順序先にあるほうが優先的に利用される

GNU Compiler Collection - 2.95.3