関数呼び出しの引数の数は合わせないといけない(C)
背景
また業務で遭遇した不具合で未定義動作を踏んでいました😢
将来のために記録に残します。
環境
VC++のプロジェクトで、 Cコードとしてコンパイルのオプション(/TC) が指定されていました。
何が起きたか?
問題が起きたのは以下のようなコードでした。funcA()
の処理は適当です。
int funcA(int a, int b)
{
return a + b;
}
#include <stdio.h>
extern int funcA(int a);
int main(void)
{
// このまえに様々な処理
printf("%d\n", funcA(3));
}
実際に問題が起きた際はfuncA()に相当する処理が想定通りに動かなかったです。それもそのはず、funcA()は実際には引数を2つ取る関数なのに、引数を一つ取る関数としてextern宣言されていたのです。
このような関数呼び出しで、渡されていないfuncAの第2引数が不定値となっていました。
なぜ不定となったのか?の予想
未定義動作なので、理由を推測するのはあまり意味のないことかもしれませんが…。
試しに、Compiler Explorerでアセンブラを出してみました。
funcA(int, int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
mov edx, DWORD PTR [rbp-4]
mov eax, DWORD PTR [rbp-8]
add eax, edx
pop rbp
ret
.LC0:
.string "%d\n"
main:
push rbp
mov rbp, rsp
mov edi, 3
call funcA(int)
mov esi, eax
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
mov eax, 0
pop rbp
ret
funcA()を呼び出す側のmain()では、call
命令をする前に、edi
レジスタに3をいれて引数の準備をしています。
一方、呼ばれる側のfuncA()の処理を見ると、edi
レジスタとesi
レジスタを足し合わせて、eax
レジスタに代入して、返り値としています。
当然ながら、呼び出し側のmain()関数では、esi
レジスタには何も入れていないので、どんな値が入っているかはわかりません。
引数はどのように渡すか?(スタック経由?レジスタ?)は、採用している関数の呼出規約や、CPUのアーキテクチャによるとは思いますが、少なくとも呼び出し元で用意していない以上、どんな値になるかは当然わからないわけです。
対策
問題となったソースでは、ファイル内で使用する外部関数を、使用する関数だけピックアップしてextern宣言を書いていました。
しかしこのような運用では、extern宣言を書くたびに実際の定義とは異なる宣言をしてしまう可能性があります。新たなextern宣言を追加するたびに、不具合を埋め込む可能性があるのです。
当然ながら、外部に今回する関数用のextern宣言をまとめたヘッダファイルを用意し、関数の使用箇所でincludeするべきですね(あたりまえですが…)。
余談?
Cコードとしてコンパイルのオプション(/TC)を有効化しなかった場合はリンクエラーとなるので問題が起きなかったです。
C++には名前マングリングの仕組みがあるので、仮引数の個数が違う場合はアセンブリレベルで別の関数名となるので、本件の事象が試せないのでしょうね。
装飾名 | Microsoft Learn
感想
ヘッダを使おう。あちこちでexternしたら、修正が必要になったときの作業量えらいことにならない?(なっている)
Discussion