アライメント(バイト境界)

構造体におけるアライメント

次のプログラムをコンパイル・実行した結果をその下に示す。

#include <stdio.h>

typedef struct _test {
    int n;
    char c;
    short s;
    int z;
} TEST;

void main(){
   TEST d;
   printf("%d\n", sizeof(d));
   printf("%d %d %d %d %d\n", &d, &d.n, &d.c, &d.s, &d.z);
}
c:\mh\www\mcc>mcc test/struct02.c
c:\mh\www\mcc>struct02
12
1245016 1245016 1245020 1245022 1245024

構造体の各要素の配置を図示すると次のようになる。 short型は 2 の倍数の番地から 2 バイトが割り当てられ、 int型は 4 の倍数の番地から 4 バイトが割り当てられる。 従って、char型、short型、int型が入り混じると、 所々に空きが生まれる。 構造体のサイズはこのような空きを含めた全体の領域のサイズとなる。

例えば、32ビットプロセッサならば、 データバスは4バイト(32ビット)幅であり、 int型の変数は 4 バイト境界に配置するのが最も効率がよい(実行速度が速い)。

アプリケーションプログラム独自の構造体では、 アライメントのルールは独自のものでも問題は起こらないが、 MCC に限らず、多くのコンパイラがシステムに標準装備の DLL を使用する。 従って、コンパイラの設計では、 アライメントは標準規約に従わないと、このような DLL が使えない。

アライメントはローカル変数やグローバル変数の宣言でも起こる。 高速化のために、構造体と同様の措置をとる。 また、次の節で述べるように、同様のことが関数呼び出しでも起こる。


関数呼び出しでのchar型,short型引数

これはアライメントではないが、 char型、short型では注意がいることから、ここで述べる。

次のプログラムをコンパイル・実行した結果をその下に示す。

// func01.c
#include <stdio.h>

void func(int a, char b, int c, short d, int e, char f) {
    printf("&a=%d, &b=%d, &c=%d, &d=%d, &e=%d, &e=%d\n", 
		&a, &b, &c, &d, &e, &f);
    printf("a=%d, b=%d, c=%d, d=%d, e=%d, f=%d\n", a, b, c, d, e, f);
    printf("a=%X, b=%X, c=%X, d=%X, e=%X, f=%X\n", a, b, c, d, e, f);
}

void main() {
    func(12, 'a', 34, -56, 78, -32);
}
c:\mh\www\mcc>func01
&a=1245004, &b=1245008, &c=1245012, &d=1245016, &e=1245020, &e=1245024
a=12, b=97, c=34, d=-56, e=78, f=-32
a=C, b=61, c=22, d=FFFFFFC8, e=4E, f=FFFFFFE0

前節で述べたアライメントでは例えばchar型変数が4つ連続した場合は、空きは生まれないが、 関数の呼び出しでは、個々のデータが4バイト境界に置かれる。 では、下図の左に示したように、空きが生じるのであろうか? 例えば、TCCのコンパイル結果を逆アセンブルしてみればわかるように、 空きを生じさせるのではなく、char型、short型はint型に符号拡張され、 それが引数の値としてスタックに積まれる。 ここで、符号拡張というのは、例えば、char型の -32 (16進表示で E016) を int型の -32 (16進表示で FFFFFFE016) に変換することである。 正の整数では上位に 0 を埋めればよいが、負の整数では F を埋めることになる。


スタックの内容は次のようなプログラムでも確認できる。

void func(int a, char b, int c, short d, int e, char f) {
    int n, *p = &a;
    for (n = 0; n < 6; n++) printf("%08X ", p[n]);
}

関数での実引数の読込みは int型に符号拡張されていることを配慮すれば、処理が単純化される。