しまねソフト研究開発センター(略称 ITOC)にいます、東です。
C言語でプログラミングをしていて少し疑問に思った事があり、規格などを簡単に調べてみてもわからなかったので、実際のコンパイラの挙動を調査してみました。
疑問に思った事
ふだん gccを使っていて、グローバルに確保した char (int8_t, uint8_t) 配列では、アライメントが揃っていることが多いように感じていました。しかしこれは本当に信じて良いものだったのでしょうか。
つまり char 配列で確保したメモリ領域に double等のデータを格納した場合、アライメントにうるさいCPUでも大丈夫なのか? ということです。
コードにすると、こんなコードです。
int8_t buf[ sizeof(double) ];
double *p = (double *)buf;
*p = 1.0;
テスト環境
現在 32 bit 環境で仕事をすることが多いので、以下の2種類の環境で実験します。
- 1. GCC環境
- gcc version 12.2.0
- OS: Debian12 (i386)
- OS: Debian12 (i386)
- 2. clang環境
- clang version 18.1.6
- OS: FreeBSD14.2 (i386)
実験開始
まずは簡単なコードで、実際に確保されるアドレスを表示してみます。
int8_t buf1[1]; // .bssセクションに、buf1 1byte, buf2 2byteを確保
int8_t buf2[2];
int main()
{
printf("buf1: %p\n", buf1);
printf("buf2: %p\n", buf2);
return 0;
}
buf1: 0x404014
buf2: 0x404015
buf1: 0x4036b8
buf2: 0x4036b9
順当に、buf1とbuf2は1バイトのずれが観測され、buf2は奇数アドレスになっています。
この時点で既に、先の仮定はあやしくなってきました。
実験2
2つ目のバッファのサイズを、2 から 0x100 に拡大してみます。
int8_t buf1[1];
int8_t buf100[0x100]; // 2 to 0x100
int main()
{
printf("buf1: %p\n", buf1);
printf("buf100: %p\n", buf100);
return 0;
}
実行結果
buf1: 0x404040
buf100: 0x404060
buf1: 0x4036b8
buf100: 0x4036b9
先ほどと違い、gccの方は buf100
のアドレスがずれて、アライメントが揃っている様子が観測できます。ただし、clangの方は結果が変わりませんでした。
最初は、リンカの働きによってこのようなアライメント整列が行われているのかと思いましたが、nm コマンドで確認する限り、コンパイルの時点でそうなっているようですね。
% cc -c align2.c
% nm align2.o
U _GLOBAL_OFFSET_TABLE_
00000000 T __x86.get_pc_thunk.bx
00000000 B buf1
00000020 B buf100
00000000 T main
U printf
% cc -c align2.c
% nm align2.o
00000000 B buf1
00000001 B buf100
00000000 T main
U printf
ここまでの実験で、このアライメントが揃う現象は規格で定められたものではなく、おそらく gccが独自に行っているであろうということが予想されます。
ちなみに、リンカスクリプトの出力は以下のコマンドで確認できます。
ld -verbose
ただし、こちらの方によると、
https://progrunner.hatenablog.jp/entry/2019/01/19/233701
gcc -Wl,--verbose
このほうが、より正確なスクリプトが表示されるそうです。
実験3
.bssではなく、.data領域にした場合はどうでしょうか。
int8_t buf1[1] = {1}; // 初期データを与えると.dataになる
int8_t buf100[0x100] = {1};
int main()
{
printf("buf1: %p\n", buf1);
printf("buf100: %p\n", buf100);
return 0;
}
% cc align3.c && ./a.out
buf1: 0x404040
buf100: 0x404060
% cc -c align3.c
% nm align3.o
U _GLOBAL_OFFSET_TABLE_
00000000 T __x86.get_pc_thunk.bx
00000000 D buf1
00000020 D buf100
00000000 T main
U printf
% cc align3.c && ./a.out
buf1: 0x40369c
buf100: 0x40369d
% cc -c align3.c
% nm align3.o
00000000 D buf1
00000001 D buf100
00000000 T main
U printf
nmコマンドの出力を見ると BからDに代わり、確かに .data 領域に確保されたことが確認できますが、アライメントに関しては変化がありませんでした。
実験4
auto変数にしたらどうでしょうか。
int main()
{
int8_t buf1[1];
int8_t buf100[0x100];
printf("buf1: %p\n", buf1);
printf("buf100: %p\n", buf100);
return 0;
}
%cc align4.c && ./a.out
buf1: 0xbffffb9f
buf100: 0xbffffa9f
%cc align4.c && ./a.out
buf1: 0xffbfebbf
buf100: 0xffbfeabf
どちらも、アライメントは揃えている様子はありません。auto変数の場合は gccもアライメントを揃えるのをやめるようです。
実験5
構造体ではどうでしょうか。
struct STRUCT1 {
int8_t buf1[1];
int8_t buf100[0x100];
};
int main()
{
printf("buf1: %d\n", offsetof(struct STRUCT1, buf1));
printf("buf100: %d\n", offsetof(struct STRUCT1, buf100));
return 0;
}
% cc align5.c && ./a.out
buf1: 0
buf100: 1
% cc align5.c && ./a.out
buf1: 0
buf100: 1
構造体内部でも、アライメントを揃える事はしないようです。
積極的にアライメントを揃える
C11 からアライメントをそろえる機能、stdalign.h が用意されています。これを使うと積極的にアライメントをそろえる事ができます。
以下の例では、buf100 を 8バイトアラインにそろえるよう指示したコードです。
#include <stdalign.h>
int8_t buf1[1];
alignas(8) int8_t buf100[0x100];
int main()
{
printf("buf1: %p\n", buf1);
printf("buf100: %p\n", buf100);
return 0;
}
% cc align6.c && ./a.out
buf1: 0x404018
buf100: 0x404020
% cc align6.c && ./a.out
buf1: 0x4036b8
buf100: 0x4036c0
clang の方も、アドレス 0x4036c0 と、きちんと揃えることができました。
上記は、グローバル変数に関しての実験でしたが、auto 変数や構造体も同様に適用できます。
まとめ
今まであまり気にせず、大きなバッファをとって適当に使っていましたが、gcc に助けられていた面もあったのですね。もしかしたらこういった事は、gccのドキュメントなどに書いてあることかもしれませんが、すみませんが未確認です。
C11が使える場合は、コンパイラに頼らず stdalign.h を使った方がよさそうです。
本稿では主に静的なメモリ確保を話題にしましたが、動的なメモリ確保の場合、glibc malloc(3) では、8バイトや16バイト境界にアライメントされていることが明文化されています。
参考文献
坂井弘亮 著 リンカ・ローダ実践開発テクニック
https://shop.cqpub.co.jp/hanbai/books/38/38071.html
progrunner17様 gccで本当に使われているデフォルトリンカスクリプトを手に入れる。
https://progrunner.hatenablog.jp/entry/2019/01/19/233701