當前位置:
首頁 > 知識 > Linux 程序編譯過程的來龍去脈

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


來自:人人都是極客(微信號:rrgeek)




大家肯定都知道

計算機程序設計語言通常分為機器語言、彙編語言和高級語言三類。高級語言需要通過翻譯成機器語言才能執行,而翻譯的方式分為兩種,一種是編譯型,另一種是解釋型,因此我們基本上將高級語言分為兩大類,一種是編譯型語言,例如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!
"

);
 

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!"

"
"

);
 

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!" "
");
 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)
……



●編號579,輸入編號直達本文



●輸入m獲取文章

目錄

推薦↓↓↓

 



運維


更多推薦

18個技術類微信公眾號


涵蓋:程序人生、演算法與數據結構、黑客技術與網路安全、大數據技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Linux學習 的精彩文章:

C++遊戲開發入門項目精選:製作經典遊戲拳皇97
Docker 入門教程

TAG:Linux學習 |