C言語のコンパイルとリンクの手順を詳しく解説

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. コンパイルプロセス:


  1. 前処理(.i)

    • #で始まる前処理命令を処理します: #include #define #ifndef #if #else など。

    • コメントの削除、行番号の追加、ファイルインデックスの生成など。

    コマンド: gcc -E main.c -o main.i、.i ファイルを生成

  2. コンパイル (.s)

    .i ファイルをコンパイルして .s アセンブリ ファイルを生成します。

    コマンド: gcc -S main.i .s ファイルを生成します。

  3. アセンブリ(.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に焦点を当てたもの:

    1. このオブジェクトファイルに定義されている変数名や関数名などのグローバルシンボルです。
    1. 他のターゲット ファイルで参照されるシンボルは、このファイルでは定義されず、一般に外部シンボルと呼ばれます。
    1. 「.text」、「.data」などのセクション名。
    1. ローカル シンボルは、コンパイル ユニット内でのみ表示されます。デバッガは、これらのシンボルを使用して、プログラムまたはクラッシュ時にコア ダンプ ファイルを分析できます。リンカは、多くの場合、リンク プロセス中にこれらのシンボルを無視します。
$ 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 ファイルがプロセスの仮想空間にどのようにマッピングされるべきかを記述します。

おすすめ

転載: blog.csdn.net/HuangChen666/article/details/133493602