こちらの記事を見て、C++の呼び出し規約が気になったので少し調べてみました、という話です。
(最初コメントに書こうかと思ったのですが、長くなりそうなのでこちらに書きます)
何が気になったか
ぼくの記憶だと、確かSystem VのAMD64でのCの呼び出し規約は次のようになっていたと思います。
- 十分大きい構造体が引数であるとき、スタックに載せて呼び出し先に渡す
- そうでないとき、レジスタで渡す
これを踏まえたとき、
- もし
std::string
が「十分大きい構造体」でないなら、レジスタで渡されるはず。すると、printf("%s", std::stringのインスタンス)
は、printf("%s", std::stringの「第一メンバ」, 「第二メンバ」...)
と等価なので、変な文字はでなさそう(SEGVするか、正しい文字が出る?)。 - もし
std::string
が「十分大きい構造体」なら、スタック経由で渡されるはず。この時、呼ばれた側の関数は可変長引数なんだけど、どう処理されるのだろうか。
という二点を疑問に思いました。
環境
- Windows 10 Fall Creators Update上のWSL + Ubuntu 16.04.3 LTS(!)
- g++ (Ubuntu 5.4.0-6ubuntu1~16.04.5)
- libstdc++6 5.4.0-6ubuntu1~16.04.5
- Linux 4.13.0-1-amd64 + Debian buster/sid amd64
- g++ (Debian 7.2.0-12)
- libstdc++6 7.2.0-12
のそれぞれ2つの環境でチェックしています。WSLがこういうローレベルな用途に使えるのは楽しいですね。
なおgdbはgdb-pedaを入れています。
std::string
の中身
std::string
はstd::basic_string<T>
のインスタンスになっていて、大体こんな感じの構造体です。
(具体的な中身は、ubuntu/WSLなら/usr/include/c++/5/bits/basic_string.hにあります)
class string {
char* pointer;
std::size_t size;
char local_buffer[16];
}
local_buffer
が何なのかまではまあ関係なさそうなので追いかけていないのですが、本稿では使わないので大丈夫です。
むりやり正しくprintf
を呼んでみる
ということで、上のところで書いた、『もしstd::string
が「十分大きい構造体」でない』ケースでは、正しい文字が表示されてしまいそうな気がします。これを確かめるため、次のようなコードを書いてみます。
#include <string>
#include <cstdio>
int main() {
std::string a = "hoge";
// (C++的なキャストはわざと使っていません)
void* b = &a;
char* c = *(char **)b;
std::printf("cast to char* %s\n", c);
return 0;
}
これを実行すると、たしかにhoge、と表示されます。
構造体と呼び出しかた
さて、ではstd::string
は「十分大きい」のでしょうか?これを確かめるため、次のようなコードを書いてみます。
#include <string>
#include <iostream>
#include <cstdio>
struct fuga {
const char* a;
std::size_t size;
char local_buffer[16];
};
int main() {
std::cout << "sizeof string: " << sizeof(std::string) << std::endl;
std::cout << "sizeof fuga: " << sizeof(fuga) << std::endl;
fuga d;
d.a = "fuga";
std::printf("different class %s\n", d);
return 0;
}
すると…、このコードはstd::printf
のところでSEGVします。実際、gdbで見ていると、構造体fuga
はstack経由で渡されていることがわかります。
ここはまだ確かめていないところなのですが、おそらく可変長引数の関数では、引数をva_args
とかで取り出すときに、サイズに応じてstack経由かregister経由かを決めているのだと思います。特にprintf
では、フォーマット文字列に応じて型を推測できます。%s
ならポインタ型が来ることが想定されますが、ポインタはレジスタ経由で渡されます。ですから、レジスタの値が指す先を見ようとしてSEGVを起こしてしまったのだと思います。
ではなぜstd::string
を呼んだときは、SEGVを起こさなかったのでしょうか。これを見るため、次のコードをgdbで実行してみます。
#include <string>
#include <cstdio>
int main() {
std::string a = "hoge";
std::printf("string %s\n", a);
return 0;
}
すると、(使ったコードは微妙に違いますが、、)
[-------------------------------------code-------------------------------------]
0x400cbc <main+182>: mov rsi,rax
0x400cbf <main+185>: mov edi,0x400e6c
0x400cc4 <main+190>: mov eax,0x0
=> 0x400cc9 <main+195>: call 0x400a00 <printf@plt>
0x400cce <main+200>: mov rax,QWORD PTR [rbp-0x68]
0x400cd2 <main+204>: mov rsi,rax
0x400cd5 <main+207>: mov edi,0x400e77
0x400cda <main+212>: mov eax,0x0
Guessed arguments:
arg[0]: 0x400e6c ("string %s\n")
arg[1]: 0x7ffffffdde50 --> 0x7ffffffdde60 --> 0x7f0065676f68
[------------------------------------stack-------------------------------------]
0000| 0x7ffffffdde30 --> 0x1
0008| 0x7ffffffdde38 --> 0x7ffffffdde60 --> 0x7f0065676f68
0016| 0x7ffffffdde40 --> 0x7ffffffdde50 --> 0x7ffffffdde60 --> 0x7f0065676f68
0024| 0x7ffffffdde48 --> 0x7ffffffdde60 --> 0x7f0065676f68
0032| 0x7ffffffdde50 --> 0x7ffffffdde60 --> 0x7f0065676f68
0040| 0x7ffffffdde58 --> 0x4
0048| 0x7ffffffdde60 --> 0x7f0065676f68
0056| 0x7ffffffdde68 --> 0x400db7 --> 0x801f0fc35d
[------------------------------------------------------------------------------]
というような結果になりました。つまり、『std::string
はスタックでもレジスタでもなく、ポインタで渡されている』ということがわかりました!
さて、これまでの実験で、
- C++の場合、引数が構造体のときの呼び方は構造体のサイズだけで決まっているわけではない
- C++の場合、構造体の引数はポインタ経由で渡されることがある
ということがわかりました。これ以上実験をやってもそろそろ何もわからなくなってくるので、ここからは実際に呼び出し規約を眺めてみましょう。
呼び出し規約を見る
System V ABIの21ページ…かと思ったら、どうやらここによるとg++はItanium ABIを使っているようです。(ここのところ、情報が若干錯綜していてよくわかっていません。WikipediaによるとSystem V ABIを使っていると書かれています。しかし、System V ABIだと64 bit以下の構造体はレジスタ経由で渡される可能性があるはずなのですが、実際試してみてもそうなっているような感じがしません。実験してみると、16 bitより大きい構造体はスタック経由で渡されているような感じがあります。もしかしたらalignmentをよく理解していないかもしれない?)
関数呼び出し規約は、次のようになっています。
In general, C++ value parameters are handled just like C parameters. This includes class type parameters passed wholly or partially in registers. There are, however, some special cases.
If the parameter type is non-trivial for the purposes of calls, the caller must allocate space for a temporary and pass that temporary by reference.
A type is considered non-trivial for the purposes of calls if:
- it has a non-trivial copy constructor, move constructor, or destructor, or
- all of its copy and move constructors are deleted.
とにかく重要なのは、
- it has a non-trivial copy constructor, move constructor, or destructor, or
のところで、結局std::string
はこれらを持っているため、リファレンス(=ポインタ)として渡されており、SEGVを起こさず変な文字が表示された、ということのようです。
まとめ
ということで中途半端ですがまとめです。
- 非自明なコピーコンストラクタなどを持つ構造体は、リファレンスとして渡される。
- g++の採用しているABIでは16 bitより大きい構造体はstackに載せて渡されている?要調査です。
多分まだわかっていないところがたくさんあるので、有識者の方は何か文献等をサジェストしていただけるとありがたいです。