C言語のコンパイルとリンクの手順を詳しく解説
ソースファイル
main.c
#include <stdio.h>
extern int data;
extern int add(int a,int b);
int a1;
int a2 = 0;
int a3 = 10;
static int b1;
static int b2 = 0;
static int b3 = 20;
int main()
{
int c1;
int c2 = 0;
int c3 = 30;
static int d1;
static int d2 = 0;
static int d3 = 40;
c1 = data;
c2 = add(a1,a2);
while(1);
return 0;
}
追加c
int data = 3;
int add(int a,int b)
{
return a+b;
}
2 つの主要なプロセス:コンパイルとリンク
1. コンパイルプロセス:
-
前処理(.i)
-
#で始まる前処理命令を処理します: #include #define #ifndef #if #else など。
-
コメントの削除、行番号の追加、ファイルインデックスの生成など。
コマンド: gcc -E main.c -o main.i、.i ファイルを生成
-
-
コンパイル (.s)
.i ファイルをコンパイルして .s アセンブリ ファイルを生成します。
コマンド: gcc -S main.i .s ファイルを生成します。
-
アセンブリ(.o)
アセンブリ ファイルを 2 プロセスの再配置可能ファイル、つまり.oファイルに変換します。
コマンド: gcc -c main.s .o ファイルを生成
PS: gcc コマンドは、いくつかのバックグラウンド プログラムの単なるラッパーであり、さまざまなパラメーターに従って他のプログラムを呼び出します。
-
プログラムcc1を使用して、プリコンパイルとコンパイルを 1 つのステップに結合するか、次のコマンドを使用して .s ファイルを生成できます。
cc1 こんにちは。c
gcc -S hello.c -o hello.s と同等
-
アセンブラとして
-
リンカールド
バイナリ再配置可能ファイルを分析する
main.c ファイル
#include <stdio.h>
int a1;
int a2 = 0;
int a3 = 10;
static int b1;
static int b2 = 0;
static int b3 = 20;
int main(void)
{
int c1;
int c2 = 0;
int c3 = 30;
static int d1;
static int d2 = 0;
static int d3 = 40;
return 0;
}
コンパイル コマンド: 64 ビット マシン上で 32 ビット .o ファイルをコンパイルします。
* gcc -m32 -fno-PIC -c .c
-m32 は 32 ビット ファイルを生成するためのコンパイルを指定します。 -fno-PIC は位置に依存しないセグメントを削除します (.text.data.bss.comment のみを残すなど)。
1.elfファイルのヘッダーを読み取ります。
$ readelf -h main.o
ELF 头:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
类别: ELF32
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: REL (可重定位文件)
系统架构: ARM
版本: 0x1
入口点地址: 0x0
程序头起点: 0 (bytes into file)
Start of section headers: 268 (bytes into file)
标志: 0x5000000, Version5 EABI
本头的大小: 52 (字节)
程序头大小: 0 (字节)
Number of program headers: 0
节头大小: 40 (字节)
节头数量: 10
字符串表索引节头: 7
(1) 魔数
マジック: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
(2) REL(リロケータブルファイル)
(3) エントリポイントアドレス:0x0
(4) セクションヘッダーの開始: 268 (ファイル内のバイト数)
(5) ヘッダサイズ:52(バイト)
2.elfファイルのセクションヘッダ情報を取得(リンク用)
$ readelf -S main.o
There are 12 section headers, starting at offset 0x2ec:
节头:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 000044 00 AX 0 0 1
[ 2] .rel.text REL 00000000 00026c 000020 08 I 9 1 4
[ 3] .data PROGBITS 00000000 000078 00000c 00 WA 0 0 4
[ 4] .bss NOBITS 00000000 000084 000014 00 WA 0 0 4
[ 5] .comment PROGBITS 00000000 000084 00002a 01 MS 0 0 1
[ 6] .note.GNU-stack PROGBITS 00000000 0000ae 000000 00 0 0 1
[ 7] .eh_frame PROGBITS 00000000 0000b0 00003c 00 A 0 0 4
[ 8] .rel.eh_frame REL 00000000 00028c 000008 08 I 9 7 4
[ 9] .symtab SYMTAB 00000000 0000ec 000140 10 10 14 4
[10] .strtab STRTAB 00000000 00022c 000040 00 0 0 1
[11] .shstrtab STRTAB 00000000 000294 000057 00 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),
p (processor specific)
セグメント ヘッダーは 12 個あり、開始セグメント ヘッダー オフセットは 0x2ec です。
各セグメントのオフセットとサイズを確認できます。
3. セグメントの内容を出力します。
~ $ objdump -s main.o
main.o: 文件格式 elf32-i386
Contents of section .text:
0000 8d4c2404 83e4f0ff 71fc5589 e55183ec .L$.....q.U..Q..
0010 14c745ec 00000000 c745f01e 000000a1 ..E......E......
0020 00000000 8945f48b 15000000 00a10000 .....E..........
0030 000083ec 085250e8 fcffffff 83c41089 .....RP.........
0040 45ecebfe E...
Contents of section .data:
0000 0a000000 14000000 28000000 ........(...
Contents of section .comment:
0000 00474343 3a202855 62756e74 7520372e .GCC: (Ubuntu 7.
0010 352e302d 33756275 6e747531 7e31382e 5.0-3ubuntu1~18.
0020 30342920 372e352e 3000 04) 7.5.0.
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 017c0801 .........zR..|..
0010 1b0c0404 88010000 20000000 1c000000 ........ .......
0020 00000000 44000000 00440c01 00471005 ....D....D...G..
0030 02750043 0f03757c 06000000 .u.C..u|....
4. .o ファイルのシンボル テーブルを読み取ります。
~ $ objdump -t main.o
main.o: 文件格式 elf32-little
SYMBOL TABLE:
00000000 l df *ABS* 00000000 main.c
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000004 l O .bss 00000004 b1
00000008 l O .bss 00000004 b2
00000004 l O .data 00000004 b3
00000008 l O .data 00000004 d3.1881
0000000c l O .bss 00000004 d2.1880
00000010 l O .bss 00000004 d1.1879
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .eh_frame 00000000 .eh_frame
00000000 l d .comment 00000000 .comment
00000004 O *COM* 00000004 a1
00000000 g O .bss 00000004 a2
00000000 g O .data 00000004 a3
00000000 g F .text 00000044 main
00000000 *UND* 00000000 data
00000000 *UND* 00000000 add
各シンボルがどのセグメントに存在し、どのくらいのメモリを占有しているかをマークします。A1 は、弱いシンボル (他のファイルで定義されているのと同じ名前を持つ可能性がある、初期化されていない静的でないグローバル変数) であることを示すために *COM* とマークされます。
2 つのシンボル data と add には、未定義のシンボルを示す *UND* のマークが付いています。この定義はこのファイルには見つからず、リンク時に他のファイルに見つかります。
5. セクションヘッダ情報に基づいて、バイナリリロケータブルファイル(.oファイル)の構成を描画します。
bss セグメントの開始衛星テレビとコメント セグメントが同じであることがわかりますが、実際の計算では、bss セグメントは .o ファイルに格納されず、bss セグメントはシンボル テーブルに記録されることがわかります。
結論: bss セクションは、初期化されていない/0 に初期化されたグローバル変数と、初期化されていない/0 に初期化された静的ローカル変数を保存するため、それらのデフォルト値はすべて 0 になります。.o ファイル、ストレージは必要ありませんが、シンボル テーブルに記録する必要があります。実行可能ファイルが最終的に実行された後、bss セグメントのシンボルは仮想アドレス空間に格納されます。
2. リンクプロセス:
64 ビット x86 マシンでのコンパイル - 32 ビットのオブジェクト ファイルと実行可能ファイルを生成するコマンドのリンク
编译:
gcc -m32 -fno-PIC -c *.c
手动链接:
ld -e main -melf_i386 *.o -o run
生成如下文件:
$ ls
add.c add.o main.c main.o run
追伸:
-m32 は、32 ビット ファイルを生成するコンパイルを指定します。
-fno-PIC は、位置に関係なくセグメントを削除します (.text.data.bss.comment のみを残すなど)。
-e はプログラム エントリを指定します。-e の後に記号を続けるか、プログラム エントリとして add 関数を使用できます (つまり、-e add)。
-melf_i386 は、32 ビット、x86 アーキテクチャの実行可能ファイルを生成するリンクを指定します。
リンク プロセスの本質は、複数のターゲット ファイルを「接着」することです。本質的に、結合されるのは、ターゲット ファイル間のアドレスへの参照、つまり関数名とグローバル変数です。
シンボル テーブルは .o ファイルsymtabのセクションであり、シンボル テーブル コマンドを表示します。
readelf -s main.o
objdump -t main.o
nm main.o
記号表に含まれるもの、主に1 と 2に焦点を当てたもの:
-
- このオブジェクトファイルに定義されている変数名や関数名などのグローバルシンボルです。
-
- 他のターゲット ファイルで参照されるシンボルは、このファイルでは定義されず、一般に外部シンボルと呼ばれます。
-
- 「.text」、「.data」などのセクション名。
-
- ローカル シンボルは、コンパイル ユニット内でのみ表示されます。デバッガは、これらのシンボルを使用して、プログラムまたはクラッシュ時にコア ダンプ ファイルを分析できます。リンカは、多くの場合、リンク プロセス中にこれらのシンボルを無視します。
$ objdump -t main.o
main.o: 文件格式 elf32-i386
SYMBOL TABLE:
00000000 l df *ABS* 00000000 main.c
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000004 l O .bss 00000004 b1
00000008 l O .bss 00000004 b2
00000004 l O .data 00000004 b3
00000008 l O .data 00000004 d3.1877
0000000c l O .bss 00000004 d2.1876
00000010 l O .bss 00000004 d1.1875
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .eh_frame 00000000 .eh_frame
00000000 l d .comment 00000000 .comment
00000004 O *COM* 00000004 a1
00000000 g O .bss 00000004 a2
00000000 g O .data 00000004 a3
00000000 g F .text 00000016 main
1. すべての .o ファイルのセグメントを結合します。
上の図に示すように、テキスト セグメント、データ セグメント、および bss セグメントがマージされる場合、弱いシンボルを強いシンボルに変換する (または弱いシンボルを強いシンボルに置き換える) 必要があり、bss のサイズセグメントが増加します。
そして、リンクを検出した後、生成された実行可能ファイルの各セグメントにメモリ アドレス (仮想メモリ) が割り当てられます。
2. シンボルテーブルのマージ、シンボル解析、および再配置
- シンボルテーブルを結合する
実行可能ファイルのシンボル テーブルは、複数の .o ファイルのシンボル テーブルを単純に組み合わせたものであることがわかります。
- シンボルの解析
弱いシンボル (*COM*) を強いシンボルに変換する
このファイル内の未定義シンボル (*UND*) が他のファイルで見つかりました
- リセット
仮想メモリ アドレスをシンボルに割り当てます。シンボルのアドレスは、セグメント アドレスに独自のオフセットを加えたものに基づいて計算されます。
実行ファイルの解析
1. ファイルヘッダーを表示する
$ readelf -h run
ELF 头:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
类别: ELF32
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: EXEC (可执行文件)
系统架构: Intel 80386
版本: 0x1
入口点地址: 0x80480a1
程序头起点: 52 (bytes into file)
Start of section headers: 4676 (bytes into file)
标志: 0x0
本头的大小: 52 (字节)
程序头大小: 32 (字节)
Number of program headers: 3
节头大小: 40 (字节)
节头数量: 9
字符串表索引节头: 8
エントリ ポイント アドレス: 0x80480a1。
2. セグメント情報を見る
$ readelf -S run
There are 9 section headers, starting at offset 0x1244:
节头:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 08048094 000094 000051 00 AX 0 0 1
[ 2] .eh_frame PROGBITS 080480e8 0000e8 00005c 00 A 0 0 4
[ 3] .data PROGBITS 0804a000 001000 000010 00 WA 0 0 4
[ 4] .bss NOBITS 0804a010 001010 000018 00 WA 0 0 4
[ 5] .comment PROGBITS 00000000 001010 000029 01 MS 0 0 1
[ 6] .symtab SYMTAB 00000000 00103c 000170 10 7 14 4
[ 7] .strtab STRTAB 00000000 0011ac 000059 00 0 0 1
[ 8] .shstrtab STRTAB 00000000 001205 00003f 00 0 0 1
各セグメントには仮想アドレスが割り当てられます。
3. プログラムヘッダーを表示する
$ readelf -l run
Elf 文件类型为 EXEC (可执行文件)
Entry point 0x80480a1
There are 3 program headers, starting at offset 52
程序头:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x08048000 0x08048000 0x00144 0x00144 R E 0x1000
LOAD 0x001000 0x0804a000 0x0804a000 0x00010 0x00028 RW 0x1000
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
Section to Segment mapping:
段节...
00 .text .eh_frame
01 .data .bss
02
バイナリのリロケータブル ファイルには「セクション ヘッダー」のみがあり、実行可能ファイルには「プログラム ヘッダー」があります。「プログラム ヘッダー」は各セクションの仮想アドレスとアライメント バイトを示します (1 ページは 4K)
セグメント属性、読み取り専用 (text+rodata)、読み取り可能および書き込み可能 (data+bss) などに従ってマージします。
ELF の「セグメント」を表示するには、 readelf -l mainを使用します。(積載用)
PS: C ライブラリをリンクせずに自分たちでリンクしたため、この段落の内容は比較的小さいです。
* gcc main.c -o mainを直接実行すると、デフォルトで C ライブラリがリンクされ、実行可能ファイルの各セクションを表示すると多くの内容が表示されます。
* 実行可能ファイルはexecveによってプロセスにロードされます
※実行ファイルが実行できるのは、エントリアドレス(メイン)とプログラムヘッダ(ロードする仮想アドレスを指定)を指定しているためです
* 「セグメント」を記述する構造は「プログラム ヘッダー」と呼ばれ、オペレーティング システムによって ELF ファイルがプロセスの仮想空間にどのようにマッピングされるべきかを記述します。