【硬核干貨 | 程序的編譯、鏈接、裝載與運(yùn)行】

vx公眾號(hào):CurryCoder的程序人生

業(yè)精于勤,荒于嬉;行成于思,毀于隨

image

1.問題引入

學(xué)過C語(yǔ)言的小伙伴們,基本上都知道從一個(gè)xxx.c的源文件到最后生成的可執(zhí)行文件,需要經(jīng)過預(yù)處理、編譯、匯編、鏈接這幾個(gè)步驟。但是,這幾個(gè)步驟詳細(xì)的過程我一直沒搞清楚,本文將深度剖析這幾個(gè)步驟。例如,在Windows/Linux系統(tǒng)中,一個(gè)C源文件從編寫完成到最終被CPU執(zhí)行,中間要經(jīng)歷一系列復(fù)雜而又漫長(zhǎng)的過程,如下圖所示:

image

2.編譯

編譯就是將程序員先的高級(jí)語(yǔ)言源代碼如xxx.c/xxx.cpp源文件轉(zhuǎn)化成對(duì)應(yīng)的目標(biāo)文件過程。一般來說,高級(jí)語(yǔ)言的編譯詳細(xì)流程需要經(jīng)過預(yù)處理、編譯和匯編這幾步。

2.1 預(yù)處理預(yù)處理過程主要是對(duì)源代碼做了如下操作:(1).刪除所有的代碼注釋信息 (2).刪除所有的#define,并展開所有的宏定義 (3).插入所有的#include頭文件的內(nèi)容到xxx.c/xxx.cpp源文件中的對(duì)應(yīng)位置(4).其他信息......例如,gcc編譯器可以使用gcc -E test.c -o test.i命令對(duì)源文件test.c進(jìn)行預(yù)編譯,并且把預(yù)編譯的結(jié)果輸出到test.i文件中。

[njust@njust Make_Tutorials]$ ls
test.c
[njust@njust Make_Tutorials]$ cat test.c 
#include <stdio.h>

#define PI 3.14

int main() {
  printf("hello world!\n");
  return 0;
}
[njust@njust Make_Tutorials]$ gcc -E test.c -o test.i
[njust@njust Make_Tutorials]$ ls
test.c  test.i

2.2 編譯

編譯就是將預(yù)處理后的文件進(jìn)行詞法分析、語(yǔ)法分析、語(yǔ)義分析并優(yōu)化后生成相應(yīng)的匯編文件。例如,使用命令gcc -S test.i -o test.s來編譯預(yù)處理階段生成的文件,或者也可以使用命令gcc -S test.c -o hello.s將預(yù)處理與編譯兩個(gè)步驟合二為一。

[njust@njust Make_Tutorials]$ ls
test.c  test.i
[njust@njust Make_Tutorials]$ gcc -S test.i -o test.s
[njust@njust Make_Tutorials]$ ls
test.c  test.i  test.s
[njust@njust Make_Tutorials]$ gcc -S test.c -o test.s
[njust@njust Make_Tutorials]$ ls
test.c  test.i  test.s

匯編階段所生成的文件叫做目標(biāo)文件,目標(biāo)文件的結(jié)構(gòu)與可執(zhí)行文件的結(jié)構(gòu)是一致的,它們之間只存在一些細(xì)微的差異。目標(biāo)文件是無法被執(zhí)行的,它還需要經(jīng)過鏈接這一步操作后才能生成可執(zhí)行文件,最終被執(zhí)行。

3.目標(biāo)文件的格式

Linux系統(tǒng)中的目標(biāo)文件格式叫做ELF(Executable Linkable Format),ELF的格式如下圖所示:

image

ELF header是ELF文件中最重要的一個(gè)部分,header中保存了如下的內(nèi)容:

(1).ELF的magic number

(2).文件機(jī)器字節(jié)長(zhǎng)度

(3).操作系統(tǒng)平臺(tái)

(4).硬件平臺(tái)

(5).程序的入口地址

** (6).段表的位置和長(zhǎng)度**

(7).段的數(shù)量

(8).其他信息......

從header中我們可以獲取很多有用的信息,其中一種重要的信息就是段表的位置和長(zhǎng)度。通過這個(gè)信息我們可以從ELF文件中獲取到段表(Section Header Table),在ELF中段表的重要性僅次于header。段表中保存了ELF文件中所有的段的基本屬性(包括每個(gè)段的段名、段在ELF文件中的偏移、段的長(zhǎng)度及段的讀寫權(quán)限等),段表決定了整個(gè)ELF文件的結(jié)構(gòu)。

既然段表決定了所有的段的基本屬性,那么ELF文件中的段究竟是個(gè)啥呢?其實(shí)段只是對(duì)ELF文件內(nèi)不同類型數(shù)據(jù)的一種分類。例如,我們把所有的代碼(指令)放在同一個(gè)段中,并且給這個(gè)段起名為.text;把所有已初始化的數(shù)據(jù)放在.data段;把所有未初始化的數(shù)據(jù)放在.bss段;把所有只讀的數(shù)據(jù)放在.rodata段,.......等等。

為什么又要將數(shù)據(jù)(指令在ELF文件中也算是一種數(shù)據(jù),它是ELF文件的數(shù)據(jù)之一)分成不同的類型,然后分別存放在不同的段中呢?除了便于進(jìn)行區(qū)分外,還有如下幾個(gè)原因:

    (1).便于給段設(shè)置讀寫權(quán)限,有的段只需要設(shè)置只讀權(quán)限; 

(2).方便CPU緩存的生效;

     (3).有利于節(jié)省內(nèi)存,例如程序有多個(gè)副本情況下,此時(shí)只需要一份代碼段即可;

用如下的hello.c程序?yàn)槔?,深入的分析一下ELF文件中的段信息,文件的內(nèi)容如下所示:

[njust@njust Make_Tutorials]$ cat hello.c
int printf(const char *format, ...);

int global_var_init_a = 84;
int global_var_uninit_b;

void bar(int i) {
    printf("%d\n", i);
}

int main() {
    static int static_var_a = 85;
    static int static_var_b;

    int a = 1;
    int b;

    bar(static_var_a + static_var_b + a + b);
    return a;
}

使用命令gcc -c hello.c -o hello.o將源文件hello.c編譯成目標(biāo)文件hello.o。然后,再使用objdump命令查看ELF文件的內(nèi)部結(jié)構(gòu),-h表示顯示ELF文件的頭部信息得到如下結(jié)果:

[njust@njust Make_Tutorials]$ gcc -c hello.c -o hello.o
[njust@njust Make_Tutorials]$ ls
hello.c  hello.o  test.c  test.i  test.s
[njust@njust Make_Tutorials]$ objdump -h hello.o

hello.o:     文件格式 elf64-x86-64

節(jié):
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000054  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000008  0000000000000000  0000000000000000  00000094  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000004  0000000000000000  0000000000000000  0000009c  2**2
                  ALLOC
  3 .rodata       00000004  0000000000000000  0000000000000000  0000009c  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .comment      0000002e  0000000000000000  0000000000000000  000000a0  2**0
                  CONTENTS, READONLY
  5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000ce  2**0
                  CONTENTS, READONLY
  6 .eh_frame     00000058  0000000000000000  0000000000000000  000000d0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

從上面的輸出結(jié)果可以看到顯示了7個(gè)段,每個(gè)段都有一些屬性,下面解釋一下一些重要屬性的含義:

(1).Size:段的大小; (2).VMA:段的虛擬地址,因?yàn)槟繕?biāo)文件還沒有執(zhí)行鏈接操作,因此虛擬地址為0;(3).LMA:段被加載的地址,值為0(原因同上); (4).File off:段在ELF文件中的偏移地址; (5).CONTENTS:段存在于ELF文件中;需要重點(diǎn)關(guān)注的是.text、.data、.bss和.rodata這幾個(gè)段,這幾個(gè)段的詳細(xì)信息如下所示: .text段:保存程序中的所有指令信息,objdump的-s參數(shù)表示將段的內(nèi)容以十六進(jìn)制的方式打印出來,而-d參數(shù)會(huì)對(duì)所有包含指令的段進(jìn)行反匯編。于是,使用命令objdump -s -d hello.o就可以獲取代碼段的詳細(xì)信息;

[njust@njust Make_Tutorials]$ objdump -s -d hello.o

hello.o:     文件格式 elf64-x86-64

Contents of section .text:
 0000 554889e5 4883ec10 897dfc8b 45fc89c6  UH..H....}..E...
 0010 bf000000 00b80000 0000e800 000000c9  ................
 0020 c3554889 e54883ec 10c745fc 01000000  .UH..H....E.....
 0030 8b150000 00008b05 00000000 01c28b45  ...............E
 0040 fc01c28b 45f801d0 89c7e800 0000008b  ....E...........
 0050 45fcc9c3                             E...            
Contents of section .data:
 0000 54000000 55000000                    T...U...        
Contents of section .rodata:
 0000 25640a00                             %d..            
Contents of section .comment:
 0000 00474343 3a202847 4e552920 342e382e  .GCC: (GNU) 4.8.
 0010 35203230 31353036 32332028 52656420  5 20150623 (Red 
 0020 48617420 342e382e 352d3434 2900      Hat 4.8.5-44).  
Contents of section .eh_frame:
 0000 14000000 00000000 017a5200 01781001  .........zR..x..
 0010 1b0c0708 90010000 1c000000 1c000000  ................
 0020 00000000 21000000 00410e10 8602430d  ....!....A....C.
 0030 065c0c07 08000000 1c000000 3c000000  .\..........<...
 0040 00000000 33000000 00410e10 8602430d  ....3....A....C.
 0050 066e0c07 08000000                    .n......        

Disassembly of section .text:

0000000000000000 <bar>:
   0: 55                    push   %rbp
   1: 48 89 e5              mov    %rsp,%rbp
   4: 48 83 ec 10           sub    $0x10,%rsp
   8: 89 7d fc              mov    %edi,-0x4(%rbp)
   b: 8b 45 fc              mov    -0x4(%rbp),%eax
   e: 89 c6                 mov    %eax,%esi
  10: bf 00 00 00 00        mov    $0x0,%edi
  15: b8 00 00 00 00        mov    $0x0,%eax
  1a: e8 00 00 00 00        callq  1f <bar+0x1f>
  1f: c9                    leaveq 
  20: c3                    retq   

0000000000000021 <main>:
  21: 55                    push   %rbp
  22: 48 89 e5              mov    %rsp,%rbp
  25: 48 83 ec 10           sub    $0x10,%rsp
  29: c7 45 fc 01 00 00 00  movl   $0x1,-0x4(%rbp)
  30: 8b 15 00 00 00 00     mov    0x0(%rip),%edx        # 36 <main+0x15>
  36: 8b 05 00 00 00 00     mov    0x0(%rip),%eax        # 3c <main+0x1b>
  3c: 01 c2                 add    %eax,%edx
  3e: 8b 45 fc              mov    -0x4(%rbp),%eax
  41: 01 c2                 add    %eax,%edx
  43: 8b 45 f8              mov    -0x8(%rbp),%eax
  46: 01 d0                 add    %edx,%eax
  48: 89 c7                 mov    %eax,%edi
  4a: e8 00 00 00 00        callq  4f <main+0x2e>
  4f: 8b 45 fc              mov    -0x4(%rbp),%eax
  52: c9                    leaveq 
  53: c3                    retq

.data段:保存已初始化的全局變量和局部靜態(tài)變量; .bss段:保存未初始化的全局變量和局部靜態(tài)變量;.rodata段:保存只讀數(shù)據(jù),例如字符串常量,被const修飾的變量;

4.重定位表與符號(hào)表

在ELF文件中還有兩個(gè)很重要的段,它們分別是重定位表與符號(hào)表。它們對(duì)后續(xù)的鏈接階段很重要。

4.1 重定位表

簡(jiǎn)單理解,編譯器將所有需要被重定位的數(shù)據(jù)存放在重定位表中,這樣鏈接器就能知道目標(biāo)文件中哪些數(shù)據(jù)是需要被重定位的。例如,我們有兩個(gè)源文件bar.c和foo.c,文件內(nèi)容如下所示:

[njust@njust Make_Tutorials]$ cat bar.c
extern int shared;

int main() {
    int a = 100;
    swap(&a,&shared);
}
[njust@njust Make_Tutorials]$ cat foo.c
int shared = 1;

void swap(int *a, int *b) {
    *a ^= *b ^= *a ^= *b;
}

可以使用命令objdump -r bar.o來獲取重定位表的信息。此外,還可以使用命令readelf -S bar.o來詳細(xì)了解一個(gè)ELF文件

[njust@njust Make_Tutorials]$ objdump -r bar.o

bar.o:     文件格式 elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE 
0000000000000014 R_X86_64_32       shared
0000000000000021 R_X86_64_PC32     swap-0x0000000000000004

RELOCATION RECORDS FOR [.debug_info]:
OFFSET           TYPE              VALUE 
0000000000000006 R_X86_64_32       .debug_abbrev
000000000000000c R_X86_64_32       .debug_str+0x000000000000002d
0000000000000011 R_X86_64_32       .debug_str+0x0000000000000022
0000000000000015 R_X86_64_32       .debug_str+0x0000000000000007
0000000000000019 R_X86_64_64       .text
0000000000000029 R_X86_64_32       .debug_line
000000000000002e R_X86_64_32       .debug_str+0x000000000000008a
0000000000000038 R_X86_64_64       .text
000000000000005b R_X86_64_32       .debug_str+0x0000000000000028
0000000000000070 R_X86_64_32       .debug_str

RELOCATION RECORDS FOR [.debug_aranges]:
OFFSET           TYPE              VALUE 
0000000000000006 R_X86_64_32       .debug_info
0000000000000010 R_X86_64_64       .text

RELOCATION RECORDS FOR [.debug_line]:
OFFSET           TYPE              VALUE 
0000000000000029 R_X86_64_64       .text

RELOCATION RECORDS FOR [.eh_frame]:
OFFSET           TYPE              VALUE 
0000000000000020 R_X86_64_PC32     .text

[njust@njust Make_Tutorials]$ readelf -S bar.o
共有 20 個(gè)節(jié)頭,從偏移量 0x678 開始:

節(jié)頭:
  [號(hào)] 名稱              類型             地址              偏移量
       大小              全體大小          旗標(biāo)   鏈接   信息   對(duì)齊
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       0000000000000027  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000450
       0000000000000030  0000000000000018   I      17     1     8
  [ 3] .data             PROGBITS         0000000000000000  00000067
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  00000067
       0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .debug_info       PROGBITS         0000000000000000  00000067
       000000000000007b  0000000000000000           0     0     1
  [ 6] .rela.debug_info  RELA             0000000000000000  00000480
       00000000000000f0  0000000000000018   I      17     5     8
  [ 7] .debug_abbrev     PROGBITS         0000000000000000  000000e2
       000000000000006f  0000000000000000           0     0     1
  [ 8] .debug_aranges    PROGBITS         0000000000000000  00000151
       0000000000000030  0000000000000000           0     0     1
  [ 9] .rela.debug_arang RELA             0000000000000000  00000570
       0000000000000030  0000000000000018   I      17     8     8
  [10] .debug_line       PROGBITS         0000000000000000  00000181
       000000000000003b  0000000000000000           0     0     1
  [11] .rela.debug_line  RELA             0000000000000000  000005a0
       0000000000000018  0000000000000018   I      17    10     8
  [12] .debug_str        PROGBITS         0000000000000000  000001bc
       000000000000008f  0000000000000001  MS       0     0     1
  [13] .comment          PROGBITS         0000000000000000  0000024b
       000000000000002e  0000000000000001  MS       0     0     1
  [14] .note.GNU-stack   PROGBITS         0000000000000000  00000279
       0000000000000000  0000000000000000           0     0     1
  [15] .eh_frame         PROGBITS         0000000000000000  00000280
       0000000000000038  0000000000000000   A       0     0     8
  [16] .rela.eh_frame    RELA             0000000000000000  000005b8
       0000000000000018  0000000000000018   I      17    15     8
  [17] .symtab           SYMTAB           0000000000000000  000002b8
       0000000000000180  0000000000000018          18    13     8
  [18] .strtab           STRTAB           0000000000000000  00000438
       0000000000000018  0000000000000000           0     0     1
  [19] .shstrtab         STRTAB           0000000000000000  000005d0
       00000000000000a8  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),
  l (large), p (processor specific)

上面的輸出結(jié)果中,以.rela開頭的就是重定位段,上面的.rela.text就存放了需要被重定位的指令信息,如果是需要被重定位的數(shù)據(jù)則對(duì)應(yīng)的段名為.rela.data。

上面的操作都是針對(duì)目標(biāo)文件bar.o進(jìn)行的,對(duì)目標(biāo)文件foo.o執(zhí)行上述命令可以發(fā)現(xiàn)它既不存在數(shù)據(jù)段的重定位表,也不存在代碼段的重定位表。這是因?yàn)閒oo.c中的變量shared和函數(shù)swap()都已經(jīng)明確知道了自己的地址,所以不需要重定位。

但是,bar.c文件則不一樣,因?yàn)閎ar.c中變量shared和函數(shù)swap()都沒有定義在當(dāng)前的文件中,因此編譯后產(chǎn)生的目標(biāo)文件不存在它們的地址信息,所以編譯器需要把它們放在重定位表中,等到鏈接的時(shí)候再到其他目標(biāo)文件中找到對(duì)應(yīng)的符號(hào)信息后對(duì)其進(jìn)行重定位。

4.2 符號(hào)表(.symtab)

目標(biāo)文件中的某些部分是在鏈接階段需要使用到的"粘合劑",這些部分稱為"符號(hào)",符號(hào)就保存在符號(hào)表中。符號(hào)表中保存的符號(hào)很多,其中最重要的就是定義在本目標(biāo)文件中并且可以被其它目標(biāo)文件所引用的符號(hào)、在本目標(biāo)文件中引用的全局符號(hào),這兩個(gè)符號(hào)呈現(xiàn)互補(bǔ)的關(guān)系。使用命令readelf -s可以查看符號(hào)表的內(nèi)容。具體的信息如下所示:

Num:符號(hào)表數(shù)組中的坐標(biāo)

Value:符號(hào)值

Size:符號(hào)大小

Type:符號(hào)類型

Bind:綁定信息

Name:符號(hào)的名稱
[njust@njust Make_Tutorials]$ readelf -s bar.o

Symbol table '.symtab' contains 16 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS bar.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT   10 
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT   12 
    10: 0000000000000000     0 SECTION LOCAL  DEFAULT   14 
    11: 0000000000000000     0 SECTION LOCAL  DEFAULT   15 
    12: 0000000000000000     0 SECTION LOCAL  DEFAULT   13 
    13: 0000000000000000    39 FUNC    GLOBAL DEFAULT    1 main
    14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND shared
    15: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND swap
[njust@njust Make_Tutorials]$ readelf -s foo.o

Symbol table '.symtab' contains 15 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS foo.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    2 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    9 
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT   11 
    10: 0000000000000000     0 SECTION LOCAL  DEFAULT   13 
    11: 0000000000000000     0 SECTION LOCAL  DEFAULT   14 
    12: 0000000000000000     0 SECTION LOCAL  DEFAULT   12 
    13: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    2 shared
    14: 0000000000000000    74 FUNC    GLOBAL DEFAULT    1 swap

命令nm也可以對(duì)符號(hào)進(jìn)行查看,其中D表示該符號(hào)是已經(jīng)初始化的變量,T表示該符號(hào)是指令,U表示符號(hào)尚未定義;

[njust@njust Make_Tutorials]$ nm bar.o
0000000000000000 T main
                 U shared
                 U swap
[njust@njust Make_Tutorials]$ nm foo.o
0000000000000000 D shared
0000000000000000 T swap
[njust@njust Make_Tutorials]$ nm result
0000000000601004 D __bss_start
0000000000601004 D _edata
0000000000601008 D _end
00000000004000e8 T main
0000000000601000 D shared
000000000040010f T swap

通過上面的舉例,我們知道重定位表與符號(hào)表之間是一種相互合作的關(guān)系,鏈接器首先會(huì)根據(jù)重定位表找到該目標(biāo)文件中需要被重定位的符號(hào),然后再根據(jù)符號(hào)表去其他的目標(biāo)文件中找到匹配的上的符號(hào)。最后,對(duì)本目標(biāo)文件中的符號(hào)進(jìn)行重定位。

5.(靜態(tài))鏈接

現(xiàn)代計(jì)算機(jī)的內(nèi)存和磁盤空間已經(jīng)足夠大,同時(shí)動(dòng)態(tài)鏈接對(duì)內(nèi)存和磁盤的節(jié)省十分有限,所以我們已經(jīng)可以忽略動(dòng)態(tài)鏈接在節(jié)省使用空間上的優(yōu)勢(shì)。此外,由于沒有了對(duì)動(dòng)態(tài)鏈接庫(kù)的依賴,不需要考慮動(dòng)態(tài)鏈接庫(kù)的不同版本,靜態(tài)鏈接的文件可以做到鏈接即可執(zhí)行,減少了運(yùn)維和部署上的復(fù)雜度,是非常的方便的,在有些新發(fā)明的語(yǔ)言(例如 go語(yǔ)言)中鏈接過程默認(rèn)已經(jīng)開始使用靜態(tài)鏈接。

5.1 靜態(tài)鏈接過程可細(xì)分為兩步:

(1).掃描所有的目標(biāo)文件,獲取它們每個(gè)段的長(zhǎng)度、位置和屬性,并把每個(gè)目標(biāo)文件中的符號(hào)表的符號(hào)定義與符號(hào)引用集中存放在一個(gè)全局符號(hào)表中,建立起可執(zhí)行文件到目標(biāo)文件的段映射關(guān)系;

(2).讀取目標(biāo)文件中的段數(shù)據(jù),并解析符號(hào)表信息。根據(jù)符號(hào)表信息進(jìn)行重定位、調(diào)整代碼中的地址等操作;

使用命令gcc -c bar.c foo.c -zexecstack -fno-stack-protector -g編譯源代碼得到目標(biāo)文件bar.o和foo.o,然后使用命令ld bar.o foo.o -e main -o result鏈接bar.o和foo.o目標(biāo)文件得到可執(zhí)行文件result。

[njust@njust Make_Tutorials]$ ls
bar.c  foo.c  hello.c  hello.o  test.c  test.i  test.s
[njust@njust Make_Tutorials]$ gcc -c bar.c foo.c -zexecstack -fno-stack-protector -g
[njust@njust Make_Tutorials]$ ls
bar.c  bar.o  foo.c  foo.o  hello.c  hello.o  test.c  test.i  test.s
[njust@njust Make_Tutorials]$ ld bar.o foo.o -e main -o result
[njust@njust Make_Tutorials]$ ls
bar.c  bar.o  foo.c  foo.o  hello.c  hello.o  result  test.c  test.i  test.s
[njust@njust Make_Tutorials]$ cat bar.c

結(jié)合重定位表與符號(hào)表的知識(shí),我們可以知道鏈接器最終需要完成的工作有三個(gè):

(1).合并不同目標(biāo)文件中的同類型段;

(2).對(duì)目標(biāo)文件中的符號(hào)引用,在其它的目標(biāo)文件中找到引用的符號(hào);

(3).對(duì)目標(biāo)文件中的變量進(jìn)行重定位;

5.2 靜態(tài)庫(kù)的鏈接
操作系統(tǒng)一般都自帶有一些庫(kù)文件,linux中最有名的就是libc靜態(tài)庫(kù),它一般位于/usr/bin/libc.a中,libc.a是一個(gè)壓縮文件,它當(dāng)中包含了printf.o、scanf.o、malloc.o、read.o等庫(kù)文件。當(dāng)使用標(biāo)準(zhǔn)庫(kù)中的文件時(shí),鏈接器會(huì)對(duì)用戶目標(biāo)文件和標(biāo)準(zhǔn)庫(kù)文件進(jìn)行鏈接,得到最終的可執(zhí)行文件。

** 6.裝載**

完成鏈接步驟后,得到一個(gè)可執(zhí)行文件,在可執(zhí)行文件中包含了很多段,但是一旦這些段加載到內(nèi)存中后,我們就不需要再關(guān)心它們到底是什么類型的數(shù)據(jù)了,只需要關(guān)心這些數(shù)據(jù)在內(nèi)存中的讀寫權(quán)限??蓤?zhí)行文件被加載到內(nèi)存中的數(shù)據(jù)可分為:可讀不可寫和可讀可寫。 現(xiàn)代操作系統(tǒng)均采用分頁(yè)的方式來管理內(nèi)存,所以操作系統(tǒng)只需要讀取可執(zhí)行文件的文件頭,之后建立起可執(zhí)行文件到虛擬內(nèi)存的映射關(guān)系,不需要真正的將程序載入內(nèi)存。在程序的運(yùn)行過程中,CPU發(fā)現(xiàn)有些內(nèi)存頁(yè)在物理內(nèi)存中并不存在時(shí),會(huì)觸發(fā)缺頁(yè)異常,此時(shí)CPU將控制權(quán)限轉(zhuǎn)交給操作系統(tǒng)的異常處理函數(shù),操作系統(tǒng)負(fù)責(zé)將此內(nèi)存頁(yè)的數(shù)據(jù)從外存(磁盤)上讀取到物理內(nèi)存中。數(shù)據(jù)讀取完畢之后,操作系統(tǒng)讓CPU jmp到觸發(fā)了缺頁(yè)異常的那條指令處繼續(xù)執(zhí)行,此時(shí)指令執(zhí)行就不會(huì)再有缺頁(yè)異常了。忽略物理內(nèi)存地址以及缺頁(yè)異常的影響,一旦操作系統(tǒng)創(chuàng)建進(jìn)程(fork) 并載入了可執(zhí)行文件(exec),那么虛擬內(nèi)存的分布應(yīng)該如下圖所示??梢钥吹紼LF文件中的多個(gè)段在內(nèi)存中被合并為三個(gè)段。

image

上圖中,除了三個(gè)保存了ELF文件中的數(shù)據(jù)的段之外,還有其他幾部分。如下表所示:

| 名稱 | 描述 |
| Kernel Space | 內(nèi)核空間,用戶進(jìn)程無權(quán)訪問 |
| Stack | 實(shí)現(xiàn)函數(shù)調(diào)用 |
| Heap | 保存程序運(yùn)行時(shí)產(chǎn)生的全局變量 |
| Memory Map | 磁盤空間到內(nèi)存的映射 |

7.運(yùn)行

操作系統(tǒng)jmp到進(jìn)程的第一條指令并不是main方法,而是別的代碼。那些代碼負(fù)責(zé)初始化main方法執(zhí)行所需要的環(huán)境并調(diào)用main方法執(zhí)行,運(yùn)行這些代碼的函數(shù)被稱為入口函數(shù)或者入口點(diǎn)(Entry Point)。一個(gè)程序的執(zhí)行過程如下

(1).操作系統(tǒng)在創(chuàng)建進(jìn)程之后,jmp到這個(gè)進(jìn)程的入口函數(shù)

(2).入口函數(shù)對(duì)程序運(yùn)行環(huán)境進(jìn)行初始化,包括堆、I/O、線程、全局變量的構(gòu)造等

(3).入口函數(shù)在完成初始化之后,調(diào)用main函數(shù),開始執(zhí)行程序的主體

(4).main函數(shù)執(zhí)行完畢之后返回到入口函數(shù),入口函數(shù)進(jìn)行清理工作,最后通過系統(tǒng)調(diào)用結(jié)束進(jìn)程

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容