int 型変数 a、 b を宣言し、 初期化するだけの単純なプログラム。
ex1.c:
int a = 1234;
int b = 6666;
int
main (void)
{
return 0;
}
gcc でコンパイルし、 オブジェクトファイルを生成する。 コンパイラの最適化を抑止するため -O0 オプションをつける。
> gcc -O0 -c ex1.c
生成されたファイル ex1.o を確認する。 1152 バイトの EFL 形式のオブジェクトファイルであることがわかる。
> ls -l ex1.o
-rw-r--r-- 1 ohsaki lsnl 1152 Oct 14 09:12 ex1.o
> file ex1.o
ex1.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
バイナリダンプしてみる。 バイナリファイルであるため、 ASCII でダンプしてもよくわからない。
> xxd ex1.o | head -20
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
00000010: 0100 3e00 0100 0000 0000 0000 0000 0000 ..>.............
00000020: 0000 0000 0000 0000 c001 0000 0000 0000 ................
00000030: 0000 0000 4000 0000 0000 4000 0b00 0a00 ....@.....@.....
00000040: 5548 89e5 b800 0000 005d c300 d204 0000 UH.......]......
00000050: 0a1a 0000 0047 4343 3a20 2844 6562 6961 .....GCC: (Debia
00000060: 6e20 3134 2e32 2e30 2d31 3929 2031 342e n 14.2.0-19) 14.
00000070: 322e 3000 0000 0000 1400 0000 0000 0000 2.0.............
00000080: 017a 5200 0178 1001 1b0c 0708 9001 0000 .zR..x..........
00000090: 1c00 0000 1c00 0000 0000 0000 0b00 0000 ................
000000a0: 0041 0e10 8602 430d 0646 0c07 0800 0000 .A....C..F......
000000b0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000c0: 0000 0000 0000 0000 0100 0000 0400 f1ff ................
000000d0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000e0: 0000 0000 0300 0100 0000 0000 0000 0000 ................
000000f0: 0000 0000 0000 0000 0700 0000 1100 0200 ................
00000100: 0000 0000 0000 0000 0400 0000 0000 0000 ................
00000110: 0900 0000 1100 0200 0400 0000 0000 0000 ................
00000120: 0400 0000 0000 0000 0b00 0000 1200 0100 ................
00000130: 0000 0000 0000 0000 0b00 0000 0000 0000 ................
ELF 形式のオブジェクトファイルなので、 readelf コマンドでどんなセクションがあるか見てみる。 .text がテキストセグメント (機械語の命令が書かれたセグメント)、 .data がデータセグメント (データが格納されたセグメント)。
> readelf -S ex1.o
There are 11 section headers, starting at offset 0x1c0:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
000000000000000b 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 0000004c
0000000000000008 0000000000000000 WA 0 0 4
[ 3] .bss NOBITS 0000000000000000 00000054
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .comment PROGBITS 0000000000000000 00000054
0000000000000020 0000000000000001 MS 0 0 1
[ 5] .note.GNU-stack PROGBITS 0000000000000000 00000074
0000000000000000 0000000000000000 0 0 1
[ 6] .eh_frame PROGBITS 0000000000000000 00000078
0000000000000038 0000000000000000 A 0 0 8
[ 7] .rela.eh_frame RELA 0000000000000000 00000150
0000000000000018 0000000000000018 I 8 6 8
[ 8] .symtab SYMTAB 0000000000000000 000000b0
0000000000000090 0000000000000018 9 3 8
[ 9] .strtab STRTAB 0000000000000000 00000140
0000000000000010 0000000000000000 0 0 1
[10] .shstrtab STRTAB 0000000000000000 00000168
0000000000000054 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)
.data セグメントを逆アセンブラでダンプしてみる。
> objdump -d -j .data ex1.o
ex1.o: file format elf64-x86-64
Disassembly of section .data:
0000000000000000 <a>:
0: d2 04 00 00 ....
0000000000000004 <b>:
4: 0a 1a 00 00 ....
変数 a が d2 04 00 00、 変数 b が 0a 1a 00 00 の 4 バイトにコンパイルされていることがわかる。
64 ビット OS 上の C コンパイラの int は通常 32 ビットなので、それぞれ 4 バイト。 使用している計算機の CPU は Intel Core Ultra 5 で、 1234 は 16 進数で 0x00001a0a である。 インテルの CPU はリトルエンディアンなので、下位バイトが前に来て、 d2 04 00 00 のように格納されている。
変数 a と変数 b がつめて配置されていることにも注意。
構造体のサンプル。
ex2.c:
typedef struct
{
char name[4];
int price;
double weight;
} item_t;
item_t a = { "APP", 120, 205.8 };
int
main (void)
{
return 0;
}
コンパイルや確認の手順はさきほど (int 型のもの) と同じ。
> gcc -O0 -c ex2.c
> objdump -d -j .data ex2.o
ex2.o: file format elf64-x86-64
Disassembly of section .data:
0000000000000000 <a>:
0: 41 50 50 00 78 00 00 00 9a 99 99 99 99 b9 69 40 APP.x.........i@
char が 4 バイト、int が 4 バイト、ダブル 8 バイトが連続したメモリ領域に確保されている。
"APP" は最後 (4 バイト目) に NULL が入っていることに注意。
120 は 16 進数で 0x00000078 であり、 205.8 は 16 進数で 9a 99 99 99 99 b9 69 40 の 8 バイトである。 120 は 32 ビットの符号なし整数が、 リトルエンディアンで格納されている。 205.8 は倍精度 (double precision) の IEEE 745 浮動小数点フォーマットで符号化されたバイト列が格納されている。
IEEE 754
https://en.wikipedia.org/wiki/IEEE_754
配列のサンプル。 配列の要素は int 型。
ex3.c:
int x[] = { 0, 10, 20, 30, 40 };
int
main (void)
{
return 0;
}
コンパイルや確認の手順はさきほど (int 型のもの) と同じ。
> gcc -O0 -c ex2.c
> objdump -d -j .data ex3.o
ex3.o: file format elf64-x86-64
Disassembly of section .data:
0000000000000000 <x>:
0: 00 00 00 00 0a 00 00 00 14 00 00 00 1e 00 00 00 ................
10: 28 00 00 00 (...
これも、 配列の要素である int 型の 0, 10, 20, 30, 40 が詰めて配置されていることがわかる。 したがって、n 番目の要素は、配列の先頭から 4 * (n - 1) バイト目にある。
C コンパイラは、 配列のデータをこのようにメモリ上に配置するので、 配列の n 番目の要素へのアクセスが瞬時に行える。 つまり、 n 番目の要素にアクセスしたければ、 配列の先頭から 4 * (n - 1) バイト目からの連続する 4 バイトにアクセスすればよい。