Linux程序編譯過程的示例分析

小編給大家分享一下Linux程序編譯過程的示例分析,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

成都創(chuàng)新互聯(lián)主要從事成都網(wǎng)站設計、網(wǎng)站建設、網(wǎng)頁設計、企業(yè)做網(wǎng)站、公司建網(wǎng)站等業(yè)務。立足成都服務圖們,10年網(wǎng)站建設經(jīng)驗,價格優(yōu)惠、服務專業(yè),歡迎來電咨詢建站服務:18982081108

本文將介紹如何將高層的C/C++語言編寫的程序轉(zhuǎn)換成為處理器能夠執(zhí)行的二進制代碼的過程,包括四個步驟:

  •  預處理(Preprocessing)

  •  編譯(Compilation)

  •  匯編(Assembly)

  •  鏈接(Linking)

Linux程序編譯過程的示例分析

GCC 工具鏈介紹

通常所說的GCC是GUN Compiler Collection的簡稱,是Linux系統(tǒng)上常用的編譯工具。GCC工具鏈軟件包括GCC、Binutils、C運行庫等。

GCC

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

Binutils

一組二進制程序處理工具,包括:addr2line、ar、objcopy、objdump、as、ld、ldd、readelf、size等。這一組工具是開發(fā)和調(diào)試不可缺少的工具,分別簡介如下:

  • addr2line:用來將程序地址轉(zhuǎn)換成其所對應的程序源文件及所對應的代碼行,也可以得到所對應的函數(shù)。該工具將幫助調(diào)試器在調(diào)試的過程中定位對應的源代碼位置。

  •  as:主要用于匯編,有關匯編的詳細介紹請參見后文。

  •  ld:主要用于鏈接,有關鏈接的詳細介紹請參見后文。

  •  ar:主要用于創(chuàng)建靜態(tài)庫。為了便于初學者理解,在此介紹動態(tài)庫與靜態(tài)庫的概念:

    •  如果要將多個.o目標文件生成一個庫文件,則存在兩種類型的庫,一種是靜態(tài)庫,另一種是動態(tài)庫。

    •  在windows中靜態(tài)庫是以 .lib 為后綴的文件,共享庫是以 .dll 為后綴的文件。在linux中靜態(tài)庫是以.a為后綴的文件,共享庫是以.so為后綴的文件。

    •  靜態(tài)庫和動態(tài)庫的不同點在于代碼被載入的時刻不同。靜態(tài)庫的代碼在編譯過程中已經(jīng)被載入可執(zhí)行程序,因此體積較大。共享庫的代碼是在可執(zhí)行程序運行時才載入內(nèi)存的,在編譯過程中僅簡單的引用,因此代碼體積較小。在Linux系統(tǒng)中,可以用ldd命令查看一個可執(zhí)行程序依賴的共享庫。

    •  如果一個系統(tǒng)中存在多個需要同時運行的程序且這些程序之間存在共享庫,那么采用動態(tài)庫的形式將更節(jié)省內(nèi)存。

  •  ldd:可以用于查看一個可執(zhí)行程序依賴的共享庫。

  •  objcopy:將一種對象文件翻譯成另一種格式,譬如將.bin轉(zhuǎn)換成.elf、或者將.elf轉(zhuǎn)換成.bin等。

  •  objdump:主要的作用是反匯編。有關反匯編的詳細介紹,請參見后文。

  •  readelf:顯示有關ELF文件的信息,請參見后文了解更多信息。

  •  size:列出可執(zhí)行文件每個部分的尺寸和總尺寸,代碼段、數(shù)據(jù)段、總大小等,請參見后文了解使用size的具體使用實例。

C運行庫

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

C語言標準僅僅定義了C標準庫函數(shù)原型,并沒有提供實現(xiàn)。因此,C語言編譯器通常需要一個C運行時庫(C Run Time Libray,CRT)的支持。C運行時庫又常簡稱為C運行庫。與C語言類似,C++也定義了自己的標準,同時提供相關支持庫,稱為C++運行時庫。

準備工作

由于GCC工具鏈主要是在Linux環(huán)境中進行使用,因此本文也將以Linux系統(tǒng)作為工作環(huán)境。為了能夠演示編譯的整個過程,本節(jié)先準備一個C語言編寫的簡單Hello程序作為示例,其源代碼如下所示:

#include <stdio.h>   //此程序很簡單,僅僅打印一個Hello World的字符串。  int main(void)  {    printf("Hello World! \n");    return 0;  }

編譯過程

1.預處理

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

  • 將所有的#define刪除,并且展開所有的宏定義,并且處理所有的條件預編譯指令,比如#if #ifdef #elif #else #endif等。

  •  處理#include預編譯指令,將被包含的文件插入到該預編譯指令的位置。

  •  刪除所有注釋“//”和“/* */”。

  •  添加行號和文件標識,以便編譯時產(chǎn)生調(diào)試用的行號及編譯錯誤警告行號。

  •  保留所有的#pragma編譯器指令,后續(xù)編譯過程需要使用它們。

  •  使用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.編譯

編譯過程就是對預處理完的文件進行一系列的詞法分析,語法分析,語義分析及優(yōu)化后生成相應的匯編代碼。

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

$ gcc -S hello.i -o hello.s // 將預處理生成的hello.i文件編譯生成匯編程序hello.s                          // GCC的選項-S使GCC在執(zhí)行完編譯后停止,生成匯編程序

上述命令生成的匯編程序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.匯編

匯編過程調(diào)用對匯編代碼進行處理,生成處理器能識別的指令,保存在后綴為.o的目標文件中。由于每一個匯編語句幾乎都對應一條處理器指令,因此,匯編相對于編譯過程比較簡單,通過調(diào)用Binutils中的匯編器as根據(jù)匯編指令和處理器指令的對照表一一翻譯即可。

當程序由多個源代碼文件構(gòu)成時,每個文件都要先完成匯編工作,生成.o目標文件后,才能進入下一步的鏈接工作。注意:目標文件已經(jīng)是最終程序的某一部分了,但是在鏈接之前還不能執(zhí)行。

使用gcc進行匯編的命令如下:

$ gcc -c hello.s -o hello.o // 將編譯生成的hello.s文件匯編生成目標文件hello.o                          // GCC的選項-c使GCC在執(zhí)行完匯編后停止,生成目標文件  //或者直接調(diào)用as進行匯編  $ as -c hello.s -o hello.o //使用Binutils中的as將hello.s文件匯編生成目標文件

注意:hello.o目標文件為ELF(Executable and Linkable Format)格式的可重定向文件。

4.鏈接

鏈接也分為靜態(tài)鏈接和動態(tài)鏈接,其要點如下:

  •  靜態(tài)鏈接是指在編譯階段直接把靜態(tài)庫加入到可執(zhí)行文件中去,這樣可執(zhí)行文件會比較大。鏈接器將函數(shù)的代碼從其所在地(不同的目標文件或靜態(tài)鏈接庫中)拷貝到最終的可執(zhí)行程序中。為創(chuàng)建可執(zhí)行文件,鏈接器必須要完成的主要任務是:符號解析(把目標文件中符號的定義和引用聯(lián)系起來)和重定位(把符號定義和內(nèi)存地址對應起來然后修改所有對符號的引用)。

  •  動態(tài)鏈接則是指鏈接階段僅僅只加入一些描述信息,而程序執(zhí)行時再從系統(tǒng)中把相應動態(tài)庫加載到內(nèi)存中去。

    •  在Linux系統(tǒng)中,gcc編譯鏈接時的動態(tài)庫搜索路徑的順序通常為:首先從gcc命令的參數(shù)-L指定的路徑尋找;再從環(huán)境變量LIBRARY_PATH指定的路徑尋址;再從默認路徑/lib、/usr/lib、/usr/local/lib尋找。

    •   在Linux系統(tǒng)中,執(zhí)行二進制文件時的動態(tài)庫搜索路徑的順序通常為:首先搜索編譯目標代碼時指定的動態(tài)庫搜索路徑;再從環(huán)境變量LD_LIBRARY_PATH指定的路徑尋址;再從配置文件/etc/ld.so.conf中指定的動態(tài)庫搜索路徑;再從默認路徑/lib、/usr/lib尋找。

    •   在Linux系統(tǒng)中,可以用ldd命令查看一個可執(zhí)行程序依賴的共享庫。

由于鏈接動態(tài)庫和靜態(tài)庫的路徑可能有重合,所以如果在路徑中有同名的靜態(tài)庫文件和動態(tài)庫文件,比如libtest.a和libtest.so,gcc鏈接時默認優(yōu)先選擇動態(tài)庫,會鏈接libtest.so,如果要讓gcc選擇鏈接libtest.a則可以指定gcc選項-static,該選項會強制使用靜態(tài)庫進行鏈接。以Hello World為例:

  •  如果使用命令“gcc hello.c -o hello”則會使用動態(tài)庫進行鏈接,生成的ELF可執(zhí)行文件的大?。ㄊ褂肂inutils的size命令查看)和鏈接的動態(tài)庫(使用Binutils的ldd命令查看)如下所示:   

$ gcc hello.c -o hello      $ size hello  //使用size查看大小         text    data     bss     dec     hex filename         1183     552       8    1743     6cf     hello      $ ldd hello //可以看出該可執(zhí)行文件鏈接了很多其他動態(tài)庫,主要是Linux的glibc動態(tài)庫              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”則會使用靜態(tài)庫進行鏈接,生成的ELF可執(zhí)行文件的大小(使用Binutils的size命令查看)和鏈接的動態(tài)庫(使用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 //說明沒有鏈接動態(tài)庫

鏈接器鏈接后生成的最終文件為ELF格式可執(zhí)行文件,一個ELF可執(zhí)行文件通常被鏈接為不同的段,常見的段譬如.text、.data、.rodata、.bss等段。

分析ELF文件

1.ELF文件的段

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

  •  .text:已編譯程序的指令代碼段。

  •  .rodata:ro代表read only,即只讀數(shù)據(jù)(譬如常數(shù)const)。

  •  .data:已初始化的C程序全局變量和靜態(tài)局部變量。

  •  .bss:未初始化的C程序全局變量和靜態(tài)局部變量。

  •  .debug:調(diào)試符號表,調(diào)試器用此段的信息幫助調(diào)試。

Linux程序編譯過程的示例分析

可以使用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  &hellip;&hellip;    [11] .init             PROGBITS         00000000004003c8  000003c8         000000000000001a  0000000000000000  AX       0     0     4  &hellip;&hellip;    [14] .text             PROGBITS         0000000000400430  00000430         0000000000000182  0000000000000000  AX       0     0     16    [15] .fini             PROGBITS         00000000004005b4  000005b4  &hellip;&hellip;

2.反匯編ELF

由于ELF文件無法被當做普通文本文件打開,如果希望直接查看一個ELF文件包含的指令和數(shù)據(jù),需要使用反匯編的方法。

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

$ objdump -D hello  &hellip;&hellip;  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)  &hellip;&hellip;

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

$ gcc -o hello -g hello.c //要加上-g選項  $ objdump -S hello  &hellip;&hellip;  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)  &hellip;&hellip;

以上是“Linux程序編譯過程的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對大家有所幫助,如果還想學習更多知識,歡迎關注創(chuàng)新互聯(lián)行業(yè)資訊頻道!

名稱欄目:Linux程序編譯過程的示例分析
鏈接地址:http://bm7419.com/article38/jdiesp.html

成都網(wǎng)站建設公司_創(chuàng)新互聯(lián),為您提供云服務器、網(wǎng)站排名搜索引擎優(yōu)化、網(wǎng)站建設、靜態(tài)網(wǎng)站、虛擬主機

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

成都seo排名網(wǎng)站優(yōu)化