Cポインタとアライメント
個人学習記録です。
これまではjavaをメインに勤怠アプリなど(github に公開しているので興味あれば)を開発してきましたが、自分でOSを作成したいと思い立ち学習を始めました。
使用環境
- OS:Linux mint
- エディタ:vscode
- コンパイラ:gcc
- 教本:新・標準プログラマーズライブラリ C言語 ポインタ完全制覇 /前橋 和弥 (著)
作業開始
今回は教本2章7項「アライメント」の学習をします
まずはコードから(あ、あくまで自己学習なので間違いだらけです。なので、もしわかる方がいれば教えていただけると幸いです)
まずは前提知識の整理から
- cにおける構造体とは
- 複数の値をまとめて格納できるデータ型
- 定義する際は「struct」とつけて宣言する
#include <stdio.h> /*構造体定義 定義されている内容を「メンバ」と呼ぶ*/ typedef struct Hoge { char char1; //char型のデータ int int1; //int型のデータ char char2; double double1; //double型のデータ char char3; }Hoge; //タグ名
- 配列と異なり違う型であっても同じ構造体に入れられる
- 使う際はインスタンスの宣言を行い変数に代入する
結果
int main(void) { //インスタンスの宣言 Hoge hoge; //インスタンスの変数int1に10を代入 hoge.int1 = 10; //int1を出力 printf("%i\n",hoge.int1); return 0; }
10
本題
さて、ここからが本題。ここまでのコードの解説をしていきます。
#include <stdio.h>
/*構造体定義*/
typedef struct Hoge
{
char char1;
int int1;
char char2;
double double1;
char char3;
}Hoge;
/*構造体ごとのポインタを表示*/
int main(void) {
Hoge hoge;
//構造体(Hoge)の大きさ
printf("hoge size.. %d\n",(int)sizeof(Hoge));
//構造体とその中身のポインタ
printf("hoge.. %p\n",(void*)&hoge);
printf("char1.. %p\n",(void*)&hoge.char1);
printf("int1.. %p\n",(void*)&hoge.int1);
printf("char2.. %p\n",(void*)&hoge.char2);
printf("double1.. %p\n",(void*)&hoge.double1);
printf("char3.. %p\n",(void*)&hoge.char3);
return 0;
}
私の環境では以下の結果を得ました
hoge size.. 32
hoge.. 0x7fffffffd990
char1.. 0x7fffffffd990
int1.. 0x7fffffffd994
char2.. 0x7fffffffd998
double1.. 0x7fffffffd9a0
char3.. 0x7fffffffd9a8
ここまでの内容から、構造体のデータごとにアドレスが並び替えられているとわかります。
これは CPUの都合によって決まる ようで、環境ごとにどんな法則(CPUごとのアドレス番地とデータサイズの組み合わせの得意・不得意)で配置されるかは実験しないとわからないようです。
(更に詳しい内容はこちらの記事に詳しく載っています)
アライメントとは結局何だ?
アライメントとは「境界調整」とも呼ばる。
この調整はコンパイラが行い、データのアドレスの間に詰物(パディング)を入れることでCPUがデータにアクセスする際の高速化を担う、と調べた結果出てきた。
つまりアライメントとは
ハードウェアの環境に合わせてコンパイラがアドレスに対しての適切な調整をしてくれること
と考えています。
注意点
ハードウェアの環境に合わせてコンパイラがアドレスに対しての適切な調整をしてくれること
と言っても注意点もあるようで、例えば以下の構造体の場合
typedef struct Hoge2{
char char1; // 1バイト
int int1; // 4バイト
double double1; // 8バイト
int int2; // 4バイト
double double2; // 8バイト
}Hoge2;
ここで問題なのは、 同じ型同士が離れている ことが問題となる模様。
アライメントを行う際CPUは上から順に調整を行うため、上記の例ではこのようになります。
Hoge2 構造体のメモリレイアウト
オフセット | サイズ | メンバ | パディング | 備考 |
---|---|---|---|---|
0 | 1バイト | char1 |
なし | 1バイトアライメント |
1 | 3バイト | なし | パディング |
int1 のためのパディング |
4 | 4バイト | int1 |
なし | 4バイトアライメント |
8 | 8バイト | double1 |
なし | 8バイトアライメント |
16 | 4バイト | int2 |
なし | 4バイトアライメント |
20 | 4バイト | なし | パディング |
double2 のためのパディング |
24 | 8バイト | double2 |
なし | 8バイトアライメント |
問題となるのは、各メンバの合計サイズを25バイトを想定していたとしても
実際の結果は32となります。
これはアライメント規則に以下の制限があるためです。
「造体全体のサイズは、そのメンバの中で最も大きなデータ型のアライメントサイズの倍数になる」
なので今回の場合、最も大きなバイト数のdouble型に合わせて8の倍数でアライメントを行ったため、実際の結果が8の倍数である32となってしまう。
具体的な問題点
特に構造体をバイト配列として扱う際、パディング部分も含んでしまい、実行時に予期せぬエラーへと繋がることがある。