天天看點

Linux 程式 Linux編譯 Linux編譯過程的來龍去脈 Linux 程式 Linux編譯 Linux編譯過程的來龍去脈

Linux 程式 Linux編譯 Linux編譯過程的來龍去脈

大家肯定都知道計算機程式設計語言通常分為機器語言、彙編語言和進階語言三類。進階語言需要通過翻譯成機器語言才能執行,而翻譯的方式分為兩種,一種是編譯型,另一種是解釋型,是以我們基本上将進階語言分為兩大類,一種是編譯型語言,例如C,C++,Java,另一種是解釋型語言,例如Python、Ruby、MATLAB 、JavaScript。

本文将介紹如何将高層的C/C++語言編寫的程式轉換成為處理器能夠執行的二進制代碼的過程,包括四個步驟:

  • 預處理(Preprocessing)
  • 編譯(Compilation)
  • 彙編(Assembly)
  • 連結(Linking)

GCC 工具鍊介紹

通常所說的GCC是GUN Compiler Collection的簡稱,是Linux系統上常用的編譯工具。GCC工具鍊軟體包括GCC、Binutils、C運作庫等。

GCC

GCC(GNU C Compiler)是編譯工具。本文所要介紹的将C/C++語言編寫的程式轉換成為處理器能夠執行的二進制代碼的過程即由編譯器完成。

Binutils

一組二進制程式處理工具,包括:addr2line、ar、objcopy、objdump、as、ld、ldd、readelf、size等。這一組工具是開發和調試不可缺少的工具,分别簡介如下:

  • addr2line:用來将程式位址轉換成其所對應的程式源檔案及所對應的代碼行,也可以得到所對應的函數。該工具将幫助調試器在調試的過程中定位對應的源代碼位置。
  • as:主要用于彙編,有關彙編的詳細介紹請參見後文。
  • ld:主要用于連結,有關連結的詳細介紹請參見後文。
  • ar:主要用于建立靜态庫。為了便于初學者了解,在此介紹動态庫與靜态庫的概念:
    • 如果要将多個.o目标檔案生成一個庫檔案,則存在兩種類型的庫,一種是靜态庫,另一種是動态庫。
    • 在windows中靜态庫是以 .lib 為字尾的檔案,共享庫是以 .dll 為字尾的檔案。在linux中靜态庫是以.a為字尾的檔案,共享庫是以.so為字尾的檔案。
    • 靜态庫和動态庫的不同點在于代碼被載入的時刻不同。靜态庫的代碼在編譯過程中已經被載入可執行程式,是以體積較大。共享庫的代碼是在可執行程式運作時才載入記憶體的,在編譯過程中僅簡單的引用,是以代碼體積較小。在Linux系統中,可以用ldd指令檢視一個可執行程式依賴的共享庫。
    • 如果一個系統中存在多個需要同時運作的程式且這些程式之間存在共享庫,那麼采用動态庫的形式将更節省記憶體。
  • ldd:可以用于檢視一個可執行程式依賴的共享庫。
  • objcopy:将一種對象檔案翻譯成另一種格式,譬如将.bin轉換成.elf、或者将.elf轉換成.bin等。
  • objdump:主要的作用是反彙編。有關反彙編的詳細介紹,請參見後文。
  • readelf:顯示有關ELF檔案的資訊,請參見後文了解更多資訊。
  • size:列出可執行檔案每個部分的尺寸和總尺寸,代碼段、資料段、總大小等,請參見後文了解使用size的具體使用執行個體。

C運作庫

C語言标準主要由兩部分組成:一部分描述C的文法,另一部分描述C标準庫。C标準庫定義了一組标準頭檔案,每個頭檔案中包含一些相關的函數、變量、類型聲明和宏定義,譬如常見的printf函數便是一個C标準庫函數,其原型定義在stdio頭檔案中。

C語言标準僅僅定義了C标準庫函數原型,并沒有提供實作。是以,C語言編譯器通常需要一個C運作時庫(C Run Time Libray,CRT)的支援。C運作時庫又常簡稱為C運作庫。與C語言類似,C++也定義了自己的标準,同時提供相關支援庫,稱為C++運作時庫。

準備工作

由于GCC工具鍊主要是在Linux環境中進行使用,是以本文也将以Linux系統作為工作環境。為了能夠示範編譯的整個過程,本節先準備一個C語言編寫的簡單Hello程式作為示例,其源代碼如下所示:

#include <stdio.h> 

//此程式很簡單,僅僅列印一個Hello World的字元串。
int main(void)
{
  printf("Hello World! \n");
  return 0;
}
           

編譯過程

1.預處理

預處理的過程主要包括以下過程:

  • 将所有的#define删除,并且展開所有的宏定義,并且處理所有的條件預編譯指令,比如#if #ifdef #elif #else #endif等。
  • 處理#include預編譯指令,将被包含的檔案插入到該預編譯指令的位置。
  • 删除所有注釋“//”和“/* */”。
  • 添加行号和檔案辨別,以便編譯時産生調試用的行号及編譯錯誤警告行号。
  • 保留所有的#pragma編譯器指令,後續編譯過程需要使用它們。

    使用gcc進行預處理的指令如下:

$ gcc -E hello.c -o hello.i // 将源檔案hello.c檔案預處理生成hello.i
                        // GCC的選項-E使GCC在進行完預處理後即停止
           

hello.i檔案可以作為普通文本檔案打開進行檢視,其代碼片段如下所示:

// hello.i代碼片段

extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 942 "/usr/include/stdio.h" 3 4

# 2 "hello.c" 2


# 3 "hello.c"
int
main(void)
{
  printf("Hello World!" "\n");
  return 0;
}
           

2.編譯

編譯過程就是對預處理完的檔案進行一系列的詞法分析,文法分析,語義分析及優化後生成相應的彙編代碼。

使用gcc進行編譯的指令如下:

$ gcc -S hello.i -o hello.s // 将預處理生成的hello.i檔案編譯生成彙程式設計式hello.s
                        // GCC的選項-S使GCC在執行完編譯後停止,生成彙程式設計式
           

上述指令生成的彙程式設計式hello.s的代碼片段如下所示,其全部為彙編代碼。

// hello.s代碼片段


main:

.LFB0:

   .cfi_startproc

   pushq   %rbp

   .cfi_def_cfa_offset 16

   .cfi_offset 6, -16

   movq    %rsp, %rbp

   .cfi_def_cfa_register 6

   movl    $.LC0, %edi

   call    puts

   movl    $0, %eax

   popq    %rbp

   .cfi_def_cfa 7, 8

   ret

   .cfi_endproc           

3.彙編

彙編過程調用對彙編代碼進行處理,生成處理器能識别的指令,儲存在字尾為.o的目标檔案中。由于每一個彙編語句幾乎都對應一條處理器指令,是以,彙編相對于編譯過程比較簡單,通過調用Binutils中的彙編器as根據彙編指令和處理器指令的對照表一一翻譯即可。

當程式由多個源代碼檔案構成時,每個檔案都要先完成彙編工作,生成.o目标檔案後,才能進入下一步的連結工作。注意:目标檔案已經是最終程式的某一部分了,但是在連結之前還不能執行。

使用gcc進行彙編的指令如下:

$ gcc -c hello.s -o hello.o // 将編譯生成的hello.s檔案彙編生成目标檔案hello.o
                        // GCC的選項-c使GCC在執行完彙編後停止,生成目标檔案
//或者直接調用as進行彙編
$ as -c hello.s -o hello.o //使用Binutils中的as将hello.s檔案彙編生成目标檔案
           

注意:hello.o目标檔案為ELF(Executable and Linkable Format)格式的可重定向檔案。

4.連結

連結也分為靜态連結和動态連結,其要點如下:

  • 靜态連結是指在編譯階段直接把靜态庫加入到可執行檔案中去,這樣可執行檔案會比較大。連結器将函數的代碼從其所在地(不同的目标檔案或靜态連結庫中)拷貝到最終的可執行程式中。為建立可執行檔案,連結器必須要完成的主要任務是:符号解析(把目标檔案中符号的定義和引用聯系起來)和重定位(把符号定義和記憶體位址對應起來然後修改所有對符号的引用)。
  • 動态連結則是指連結階段僅僅隻加入一些描述資訊,而程式執行時再從系統中把相應動态庫加載到記憶體中去。
    • 在Linux系統中,gcc編譯連結時的動态庫搜尋路徑的順序通常為:首先從gcc指令的參數-L指定的路徑尋找;再從環境變量LIBRARY_PATH指定的路徑尋址;再從預設路徑/lib、/usr/lib、/usr/local/lib尋找。
    • 在Linux系統中,執行二進制檔案時的動态庫搜尋路徑的順序通常為:首先搜尋編譯目标代碼時指定的動态庫搜尋路徑;再從環境變量LD_LIBRARY_PATH指定的路徑尋址;再從配置檔案/etc/ld.so.conf中指定的動态庫搜尋路徑;再從預設路徑/lib、/usr/lib尋找。
    • 在Linux系統中,可以用ldd指令檢視一個可執行程式依賴的共享庫。

由于連結動态庫和靜态庫的路徑可能有重合,是以如果在路徑中有同名的靜态庫檔案和動态庫檔案,比如libtest.a和libtest.so,gcc連結時預設優先選擇動态庫,會連結libtest.so,如果要讓gcc選擇連結libtest.a則可以指定gcc選項-static,該選項會強制使用靜态庫進行連結。以Hello World為例:

  • 如果使用指令“gcc hello.c -o hello”則會使用動态庫進行連結,生成的ELF可執行檔案的大小(使用Binutils的size指令檢視)和連結的動态庫(使用Binutils的ldd指令檢視)如下所示:
    $ gcc hello.c -o hello
    $ size hello  //使用size檢視大小
       text    data     bss     dec     hex filename
       1183     552       8    1743     6cf     hello
    $ ldd hello //可以看出該可執行檔案連結了很多其他動态庫,主要是Linux的glibc動态庫
            linux-vdso.so.1 =>  (0x00007fffefd7c000)
            libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fadcdd82000)
            /lib64/ld-linux-x86-64.so.2 (0x00007fadce14c000)           
  • 如果使用指令“gcc -static hello.c -o hello”則會使用靜态庫進行連結,生成的ELF可執行檔案的大小(使用Binutils的size指令檢視)和連結的動态庫(使用Binutils的ldd指令檢視)如下所示:
    $ gcc -static hello.c -o hello
    $ size hello //使用size檢視大小
         text    data     bss     dec     hex filename
     823726    7284    6360  837370   cc6fa     hello //可以看出text的代碼尺寸變得極大
    $ ldd hello
           not a dynamic executable //說明沒有連結動态庫
               

連結器連結後生成的最終檔案為ELF格式可執行檔案,一個ELF可執行檔案通常被連結為不同的段,常見的段譬如.text、.data、.rodata、.bss等段。

分析ELF檔案

1.ELF檔案的段

ELF檔案格式如下圖所示,位于ELF Header和Section Header Table之間的都是段(Section)。一個典型的ELF檔案包含下面幾個段:

  • .text:已編譯程式的指令代碼段。
  • .rodata:ro代表read only,即隻讀資料(譬如常數const)。
  • .data:已初始化的C程式全局變量和靜态局部變量。
  • .bss:未初始化的C程式全局變量和靜态局部變量。
  • .debug:調試符号表,調試器用此段的資訊幫助調試。

可以使用readelf -S檢視其各個section的資訊如下:

$ readelf -S hello

There are 31 section headers, starting at offset 0x19d8:


Section Headers:

 [Nr] Name              Type             Address           Offset

      Size              EntSize          Flags  Link  Info  Align

 [ 0]                   NULL             0000000000000000  00000000

      0000000000000000  0000000000000000           0     0     0

……

 [11] .init             PROGBITS         00000000004003c8  000003c8

      000000000000001a  0000000000000000  AX       0     0     4

……

 [14] .text             PROGBITS         0000000000400430  00000430

      0000000000000182  0000000000000000  AX       0     0     16

 [15] .fini             PROGBITS         00000000004005b4  000005b4

……
           

2.反彙編ELF

由于ELF檔案無法被當做普通文本檔案打開,如果希望直接檢視一個ELF檔案包含的指令和資料,需要使用反彙編的方法。

使用objdump -D對其進行反彙編如下:

$ objdump -D hello

……
0000000000400526 <main>:  // main标簽的PC位址

//PC位址:指令編碼                  指令的彙編格式

 400526:    55                          push   %rbp 

 400527:    48 89 e5                mov    %rsp,%rbp

 40052a:    bf c4 05 40 00          mov    $0x4005c4,%edi

 40052f:    e8 cc fe ff ff          callq  400400 <puts@plt>

 400534:    b8 00 00 00 00          mov    $0x0,%eax

 400539:    5d                      pop    %rbp

 40053a:    c3                          retq   

 40053b:    0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

……
           

使用objdump -S将其反彙編并且将其C語言源代碼混合顯示出來:

$ gcc -o hello -g hello.c //要加上-g選項
$ objdump -S hello

……

0000000000400526 <main>:
#include <stdio.h>


int

main(void)

{

 400526:    55                          push   %rbp

 400527:    48 89 e5                mov    %rsp,%rbp

 printf("Hello World!" "\n");

 40052a:    bf c4 05 40 00          mov    $0x4005c4,%edi

 40052f:    e8 cc fe ff ff          callq  400400 <puts@plt>

 return 0;

 400534:    b8 00 00 00 00          mov    $0x0,%eax

}

 400539:    5d                          pop    %rbp

 40053a:    c3                          retq   

 40053b:    0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

……           

原文位址

https://blog.csdn.net/p23onzq/article/details/81977367