GCC編譯的背後( 預處理和編譯 彙編和連接 )

by falcon<zhangjinw@gmail.com>
2008-02-22

    平時在Linux下寫代碼,直接用"gcc -o out in.c"就把代碼編譯好了,可是這後面到底作了什麼事情呢?若是學習過編譯原理則不難理解,通常高級語言程序編譯的過程莫過於:預處理、編譯、彙編、連接。gcc在後臺實際上也經歷了這幾個過程,咱們能夠經過-v參數查看它的編譯細節,若是想看某個具體的編譯過程,則能夠分別使用-E,-S,-c和 -O,對應的後臺工具則分別爲cpp,cc1,as,ld。下面咱們將逐步分析這幾個過程以及相關的內容,諸如語法檢查、代碼調試、彙編語言等。

一、預處理

    開篇簡述:預處理是C語言程序從源代碼變成可執行程序的第一步,主要是C語言編譯器對各類預處理命令進行處理,包括頭文件的包含、宏定義的擴展、條件編譯的選擇等。

    之前沒怎麼「深刻」預處理,腦子對這些東西老是很模糊,只記得在編譯的基本過程(詞法分析、語法分析)以前還須要對源代碼中的宏定義、文件包含、條件編譯等命令進行處理。這三類的指令很常見,主要有#define, #include和#ifdef ... #endif,要特別地注意它們的用法。(更多預處理的指令請查閱相關資料)

    #define除了能夠獨立使用以便靈活設置一些參數外,還經常和#ifdef ... #endif結合使用,以便靈活地控制代碼塊的編譯與否,也能夠用來避免同一個頭文件的屢次包含。關於#include貌似比較簡單,經過man找到某個函數的頭文件,copy進去,加上<>就okay。這裏雖然只關心一些技巧,不過預處理仍是蘊含着不少潛在的陷阱(可參考<C Traps & Pitfalls>),咱們也須要注意的。下面僅介紹和預處理相關的幾個簡單內容。

php

  • 打印出預處理以後的結果:gcc -E hello.c

        這樣咱們就能夠看到源代碼中的各類預處理命令是如何被解釋的,從而方便理解和查錯。

        實際上gcc在這裏是調用了cpp的(雖然咱們經過gcc的-v僅看到cc1),cpp即The C Preprocessor,主要用來預處理宏定義、文件包含、條件編譯等。下面介紹它的一個比較重要的選項-D。

  • 在命令行定義宏:gcc -Dmacro hello.c

        等同於在文件的開頭定義宏,即#define maco,可是在命令行定義更靈活。例如,在源代碼中有這些語句。
    #ifdef DEBUG
    printf("this code is for debugging\n");
    #endif

        若是編譯時加上-DDEBUG選項,那麼編譯器就會把printf所在的行編譯進目標代碼,從而方便地跟蹤該位置的某些程序狀態。這樣-DDEBUG就能夠看成一個調試開關,編譯時加上它就能夠用來打印調試信息,發佈時則能夠經過去掉該編譯選項把調試信息去掉。

    本節參考資料:
    [1] C語言教程第九章:預處理
    http://www.bc-cn.net/Article/kfyy/cyy/jc/200409/9.html
    [2] 更多
    http://www.hemee.com/kfyy/c/6626.html
    http://www.91linux.com/html/article/program/cpp/20071203/8745.html
    http://www.janker.org/bbs/programmer/2006-10-13/327.html

    二、編譯(翻譯)

          開篇簡要:編譯以前,C語言編譯器會進行詞法分析、語法分析(-fsyntax-only),接着會把源代碼翻譯成中間語言,即彙編語言。若是想看到這個中間結果,能夠用-S選項。須要提到的是,諸如shell等解釋語言也會經歷一個詞法分析和語法分析的階段,不過以後並不會進行「翻譯」,而是「解釋」,邊解釋邊執行
    ************************

A、解釋程序

所謂解釋程序是高級語言翻譯程序的一種,它將源語言(如BASIC)書寫的源程序做爲輸入,解釋一句後就提交計算機執行一句,並不造成目標程序。就像外語翻譯中的「口譯」同樣,說一句翻一句,不產生全文的翻譯文本。這種工做方式很是適合於人經過終端設備與計算機會話,如在終端上打一條命令或語句,解釋程序就當即將此語句解釋成一條或幾條指令並提交硬件當即執行且將執行結果反映到終端,從終端把命令打入後,就能當即獲得計算結果。這的確是很方便的,很適合於一些小型機的計算問題。但解釋程序執行速度很慢,例如源程序中出現循環,則解釋程序也重複地解釋並提交執行這一組語句,這就形成很大浪費。

B、編譯程序

這是一類很重要的語言處理程序,它把高級語言(如FORTRAN、COBOL、Pascal、C等)源程序做爲輸入,進行翻譯轉換,產生出機器語言的目標程序,而後再讓計算機去執行這個目標程序,獲得計算結果。

編譯程序工做時,先分析,後綜合,從而獲得目標程序。所謂分析,是指詞法分析和語法分析;所謂綜合是指代碼優化,存儲分配和代碼生成。爲了完成這些分析綜合任務,編譯程序採用對源程序進行屢次掃描的辦法,每次掃描集中完成一項或幾項任務,也有一項任務分散到幾回掃描去完成的。下面舉一個四遍掃描的例子:第一遍掃描作詞法分析;第二遍掃描作語法分析;第三遍掃描作代碼優化和存儲分配;第四遍掃描作代碼生成。

值得一提的是,大多數的編譯程序直接產生機器語言的目標代碼,造成可執行的目標文件,但也有的編譯程序則先產生彙編語言一級的符號代碼文件,而後再調用匯編程序進行翻譯加工處理,最後產生可執行的機器語言目標文件

在實際應用中,對於須要常用的有大量計算的大型題目,採用招待速度較快的編譯型的高級語言較好,雖然編譯過程自己較爲複雜,但一旦造成目標文件,之後可屢次使用。相反,對於小型題目或計算簡單不太費機時的題目,則多選用解釋型的會話式高級語言,如BASIC,這樣能夠大大縮短編程及調試的時間
html

            ************************
        把源代碼翻譯成彙編語言,其實是編譯的整個過程當中的第一個階段,以後的階段和彙編語言的開發過程沒有什麼區別。這個階段涉及到對源代碼的詞法分析、語法檢查(經過-std指定遵循哪一個標準),並根據優化(-O)要求進行翻譯成彙編語言的動做

      若是僅僅但願進行語法檢查,能夠用-fsyntax-only選項;而爲了使代碼有比較好的移植性,避免使用gcc的一些特性,能夠結合-std和 -pedantic(或者-pedantic-erros)選項讓源代碼遵循某個C語言標準的語法。這裏演示一個簡單的例子。

java

$ cat hello.c
#include <stdio.h>
int main()
{
        printf("hello, world\n")
        return 0;
}
$ gcc -fsyntax-only hello.c
hello.c: In function ‘main’:
hello.c:5: error: expected ‘;’ before ‘return’
$ vim hello.c
$ cat hello.c
#include <stdio.h>
int main()
{
        printf("hello, world\n");
        int i;
        return 0;
}
$ gcc -std=c89 -pedantic-errors hello.c    #默認狀況下,gcc是容許在程序中間聲明變量的,可是turboc就不支持
hello.c: In function ‘main’:
hello.c:5: error: ISO C90 forbids mixed declarations and code
linux



    語法錯誤是程序開發過程當中難以免的錯誤(人的大腦在不少條件下都容易開小差),不過編譯器每每可以經過語法檢查快速發現這些錯誤,並準確地告訴你語法錯誤的大概位置。所以,做爲開發人員,要作的事情不是「恐慌」(不知所措),而是認真閱讀編譯器的提示,根據平時積累的經驗(最好在大腦中存一份常見語法錯誤索引,不少資料都提供了常見語法錯誤列表,如<C Traps&Pitfalls>和最後面的參考資料[12]也列出了不少常見問題)和編輯器提供的語法檢查功能(語法加亮、括號匹配提示等)快速定位語法出錯的位置並進行修改。

    語法檢查以後就是翻譯動做,gcc提供了一個優化選項-O,以便根據不一樣的運行平臺和用戶要求產生通過優化的彙編代碼。例如,

c++

$ gcc -o hello hello.c            #採用默認選項,不優化
$ gcc -O2 -o hello2 hello.c        #優化等次是2
$ gcc -Os -o hellos hello.c        #優化目標代碼的大小
$ ls -S hello hello2 hellos        #能夠看到,hellos比較小,hello2比較大
hello2  hello  hellos
$ time ./hello
hello, world

real    0m0.001s
user    0m0.000s
sys     0m0.000s
$ time ./hello2                #多是代碼比較少的緣故,執行效率看上去不是很明顯
hello, world

real    0m0.001s
user    0m0.000s
sys     0m0.000s

$ time ./hellos                #雖然目標代碼小了,可是執行效率慢了些
hello, world

real    0m0.002s
user    0m0.000s
sys     0m0.000s
程序員



    根據上面的簡單演示,能夠看出gcc有不少不一樣的優化選項,主要看用戶的需求了,目標代碼的大小和效率之間貌似存在一個「糾纏」,須要開發人員本身權衡。

    下面咱們經過-S選項來看看編譯出來的中間結果,彙編語言,仍是以以前那個hello.c爲例。shell

$ gcc -S hello.c        #默認輸出是hello.s,可本身指定,輸出到屏幕-o -,輸出到其餘文件-o file
$ cat hello.s
cat hello.s
        .file   "hello.c"
        .section        .rodata
.LC0:
        .string "hello, world"
        .text
.globl main
        .type   main, @function
main:
        leal    4(%esp), %ecx
        andl    $-16, %esp
        pushl   -4(%ecx)
        pushl   %ebp
        movl    %esp, %ebp
        pushl   %ecx
        subl    $4, %esp
        movl    $.LC0, (%esp)
        call    puts
        movl    $0, %eax
        addl    $4, %esp
        popl    %ecx
        popl    %ebp
        leal    -4(%ecx), %esp
        ret
        .size   main, .-main
        .ident  "GCC: (GNU) 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)"
        .section        .note.GNU-stack,"",@progbits
編程



    不知道看出來沒?和咱們在課堂裏學的intel的彙編語法不太同樣,這裏用的是AT&T語法格式。若是以前沒接觸過AT&T的,能夠看看參考資料[2]。若是想學習Linux下的彙編語言開發,從下一節開始哦,下一節開始的全部章節基本上覆蓋了Linux下彙編語言開發的通常過程,不過這裏不介紹彙編語言語法。

    這裏須要補充的是,在寫C語言代碼時,若是可以對編譯器比較熟悉(工做原理和一些細節)的話,可能會頗有幫助。包括這裏的優化選項(有些優化選項可能在彙編時採用)和可能的優化措施,例如字節對齊(能夠看看這本書"Linux_Assembly_Language_Programming"的第六小節)、條件分支語句裁減(刪除一些明顯分支)等。

本節參考資料

[1] Guide to Assembly Language Programming in Linux(pdf教程,社區有下載)
http://oss.lzu.edu.cn/modules/wfdownloads/singlefile.php?cid=5&lid=94
[2] Linux彙編語言開發指南(在線):
http://www.ibm.com/developerworks/cn/linux/l-assembly/index.html
[3] PowerPC 彙編
http://www.ibm.com/developerworks/cn/linux/hardware/ppc/assembly/index.html
[4] 用於 Power 體系結構的彙編語言
http://www.ibm.com/developerworks/cn/linux/l-powasm1.html
[5] Linux Assembly HOWTO
http://mirror.lzu.edu.cn/tldp/HOWTO/Assembly-HOWTO/
[6] Linux 中 x86 的內聯彙編
http://www.ibm.com/developerworks/cn/linux/sdk/assemble/inline/index.html
[7] Linux Assembly Language Programming
http://mirror.lzu.edu.cn/doc/incoming/ebooks/linux-unix/Linux_EN_Original_Booksubuntu

 

 

 

三、彙編

       開篇:這裏實際上仍是翻譯過程,只不過把做爲中間結果的彙編代碼翻譯成了機器代碼,即目標代碼,不過它還不能夠運行。若是要產生這一中間結果,可用gcc的-c選項,固然,也可經過as命令_彙編_彙編語言源文件來產生。

       彙編是把彙編語言翻譯成目標代碼的過程,在學習彙編語言開發時,你們應該比較熟悉nasm彙編工具(支持Intel格式的彙編語言)了,不過這裏主要用 as彙編工具來彙編AT&T格式的彙編語言,由於gcc產生的中間代碼就是AT&T格式的。下面來演示分別經過gcc的-c選項和as來產生目標代碼。vim

Quote:

$ file hello.s
hello.s: ASCII assembler program text
$ gcc -c hello.s        #用gcc把彙編語言編譯成目標代碼
$ file hello.o            #file命令能夠用來查看文件的類型,這個目標代碼是可重定位的(relocatable),須要經過ld進行進一步的連接成可執行程序(executable)和共享庫(shared)
hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
$ as -o hello.o hello.s        #用as把彙編語言編譯成目標代碼
$ file hello.o
hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped


    gcc和as默認產生的目標代碼都是ELF格式[6]的,所以這裏主要討論ELF格式的目標代碼(若是有時間再回顧一下a.out和coff格式,固然你也能夠參考資料[15],本身先了解一下,並結合objcopy來轉換它們,比較異同)。

       目標代碼再也不是普通的文本格式,沒法直接經過文本編輯器瀏覽,須要一些專門的工具。若是想了解更多目標代碼的細節,區分relocatable(可重定位)、executable(可執行)、shared libarary(共享庫)的不一樣,咱們得設法瞭解目標代碼的組織方式和相關的閱讀和分析工具。下面咱們主要介紹這部份內容。
    "BFD is a package which allows applications to use the same routines to operate on object files whatever the object file format. A new object file format can be supported simply by creating a new BFD back end and adding it to the library."[24][25]。
    binutils(GNU Binary Utilities)的不少工具都採用這個庫來操做目標文件,這類工具備objdump,objcopy,nm,strip等(固然,你也能夠利用它。若是你深刻了解ELF格式,那麼經過它來分析和編寫Virus程序將會更加方便),不過另一款很是優秀的分析工具readelf並非基於這個庫,因此你也應該能夠直接用elf.h頭文件中定義的相關結構來操做ELF文件。

    下面將經過這些輔助工具(主要是readelf和objdump,可參考本節最後列出的資料[4]),結合ELF手冊[6](建議看第三篇中文版)來分析它們。

    下面大概介紹ELF文件的結構和三種不一樣類型ELF文件的區別。

ELF文件的結構:

ELF Header(ELF文件頭)
Porgram Headers Table(程序頭表,實際上叫段表好一些,用於描述可執行文件和可共享庫)
Section 1
Section 2   
Section 3
...
Section Headers Table(節區頭部表,用於連接可重定位文件成可執行文件或共享庫)

       對於可重定位文件,程序頭是可選的,而對於可執行文件和共享庫文件(動態鏈接庫),節區表則是可選的。這裏的可選是指沒有也能夠。能夠分別經過 readelf文件的-h,-l和-S參數查看ELF文件頭(ELF Header)、程序頭部表(Program Headers Table,段表)和節區表(Section Headers Table)。

      文件頭說明了文件的類型,大小,運行平臺,節區數目等。先來經過文件頭看看不一樣ELF的類型。爲了說明問題,先來幾段代碼吧。



Code:

[Ctrl+A Select All]





Code:

[Ctrl+A Select All]





Code:

[Ctrl+A Select All]



    下面經過這幾段代碼來演示經過readelf -h參數查看ELF的不一樣類型。期間將演示如何建立動態鏈接庫(便可共享文件)、靜態鏈接庫,並比較它們的異同。

Quote:

$ gcc -c myprintf.c test.c        #編譯產生兩個目標文件myprintf.o和test.o,它們都是可重定位文件(REL)
$ readelf -h test.o | grep Type   
  Type:                              REL (Relocatable file)
$ readelf -h myprintf.o | grep Type
  Type:                              REL (Relocatable file)
$ gcc -o test myprintf.o test.o    #根據目標代碼鏈接產生可執行文件,這裏的文件類型是可執行的(EXEC)
$ readelf -h test | grep Type
  Type:                              EXEC (Executable file)
$ ar rcsv libmyprintf.a myprintf.o    #用ar命令建立一個靜態鏈接庫,靜態鏈接庫也是可重定位文件(REL)
$ readelf -h libmyprintf.a | grep Type    #所以,使用靜態鏈接庫和可重定位文件同樣,它們之間惟一不
                                        #同是前者能夠是多個可重定位文件的「集合」。
  Type:                              REL (Relocatable file)
$ gcc -o test test.o -llib -L./        #能夠直接鏈接進去,也可使用-l參數,-L指定庫的搜索路徑
$ gcc -Wall myprintf.o -shared -Wl,-soname,libmyprintf.so.0 -o libmyprintf.so.0.0
                                    #編譯產生動態連接庫,並支持major和minor版本號,動態連接庫類型爲DYN
$ ln -sf libmyprintf.so.0.0 libmyprintf.so.0
$ ln -sf libmyprintf.so.0 libmyprintf.so
$ readelf -h libmyprintf.so | grep Type
  Type:                              DYN (Shared object file)
$ gcc -o test test.o -llib -L./        #編譯時和靜態鏈接庫相似,可是執行時須要指定動態鏈接庫的搜索路徑
$ LD_LIBRARY_PATH=./ ./test            #LD_LIBRARY_PATH爲動態連接庫的搜索路徑
$ gcc -static -o test test.o -llib -L./    #在不指定static時會優先使用動態連接庫,指定時則阻止使用動態鏈接庫
                                        #這個時候會把全部靜態鏈接庫文件加入到可執行文件中,使得執行文件很大
                                        #並且加載到內存之後會浪費內存空間,所以不建議這麼作


    通過上面的演示基本能夠看出它們之間的不一樣。可重定位文件自己不能夠運行,僅僅是做爲可執行文件、靜態鏈接庫(也是可重定位文件)、動態鏈接庫的 「組件」。靜態鏈接庫和動態鏈接庫自己也不能夠執行,做爲可執行文件的「組件」,它們二者也不一樣,前者也是可重定位文件(只不過多是多個可重定位文件的集合),而且在鏈接時加入到可執行文件中去;而動態鏈接庫在鏈接時,庫文件自己並無添加到可執行文件中,只是在可執行文件中加入了該庫的名字等信息,以便在可執行文件運行過程當中引用庫中的函數時由動態鏈接器去查找相關函數的地址,並調用它們。從這個意義上說,動態鏈接庫自己也具備可重定位的特徵,含有可重定位的信息。對於什麼是重定位?如何進行靜態符號和動態符號的重定位,咱們將在連接部分和《動態符號連接的細節》一節介紹。

    下面來看看ELF文件的主體內容,節區(Section)。ELF文件具備很大的靈活性,它經過文件頭組織整個文件的整體結構,經過節區表 (Section Headers Table)和程序頭(Program Headers Table或者叫段表)來分別描述可重定位文件和可執行文件。但無論是哪一種類型,它們都須要它們的主體,即各類節區。在可重定位文件中,節區表描述的就是各類節區自己;而在可執行文件中,程序頭描述的是由各個節區組成的段(Segment),以便程序運行時動態裝載器知道如何對它們進行內存映像,從而方便程序加載和運行。
    下面先來看看一些常見的節區,而關於這些節區(section)如何經過重定位構成成不一樣的段(Segments),以及有哪些常規的段,咱們將在連接部分進一步介紹。

    能夠經過readelf的-S參數查看ELF的節區。(建議一邊操做一邊看文檔,以便加深對ELF文件結構的理解)先來看看可重定位文件的節區信息,經過節區表來查看:

Quote:

$ gcc -c myprintf.c            #默認編譯好myprintf.c,將產生一個可重定位的文件myprintf.o
$ readelf -S myprintf.o        #經過查看myprintf.o的節區表查看節區信息
There are 11 section headers, starting at offset 0xc0:

Section Headers:
  [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 000018 00  AX  0   0  4
  [ 2] .rel.text         REL             00000000 000334 000010 08      9   1  4
  [ 3] .data             PROGBITS        00000000 00004c 000000 00  WA  0   0  4
  [ 4] .bss              NOBITS          00000000 00004c 000000 00  WA  0   0  4
  [ 5] .rodata           PROGBITS        00000000 00004c 00000e 00   A  0   0  1
  [ 6] .comment          PROGBITS        00000000 00005a 000012 00      0   0  1
  [ 7] .note.GNU-stack   PROGBITS        00000000 00006c 000000 00      0   0  1
  [ 8] .shstrtab         STRTAB          00000000 00006c 000051 00      0   0  1
  [ 9] .symtab           SYMTAB          00000000 000278 0000a0 10     10   8  4
  [10] .strtab           STRTAB          00000000 000318 00001a 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)
$ objdump -d -j .text   myprintf.o      #這裏是程序指令部分,用objdump的-d選項能夠看到反編譯的結果,
                                                                 #-j指定須要查看的節區
myprintf.o:     file format elf32-i386

Disassembly of section .text:

00000000 <myprintf>:
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   83 ec 08                sub    $0x8,%esp
   6:   83 ec 0c                sub    $0xc,%esp
   9:   68 00 00 00 00          push   $0x0
   e:   e8 fc ff ff ff          call   f <myprintf+0xf>
  13:   83 c4 10                add    $0x10,%esp
  16:   c9                      leave
  17:   c3                      ret
$ readelf -r myprintf.o                         #用-r選項能夠看到有關重定位的信息,這裏有兩部分須要重定位

Relocation section '.rel.text' at offset 0x334 contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0000000a  00000501 R_386_32          00000000   .rodata
0000000f  00000902 R_386_PC32        00000000   puts
$ readelf -x .rodata myprintf.o         #.rodata節區包含只讀數據,即咱們要打印的hello, world!.

Hex dump of section '.rodata':
  0x00000000 68656c6c 6f2c2077 6f726c64 2100     hello, world!.

$ readelf -x .data myprintf.o           #沒有這個節區,.data應該包含一些初始化的數據

Section '.data' has no data to dump.
$ readelf -x .bss       mmyprintf.o             #也沒有這個節區,.bss應該包含一些未初始化的數據,程序默認初始爲0

Section '.bss' has no data to dump.
$ readelf -x .comment myprintf.o        #是一些註釋,能夠看到是是GCC的版本信息

Hex dump of section '.comment':
  0x00000000 00474343 3a202847 4e552920 342e312e .GCC: (GNU) 4.1.
  0x00000010 3200                                2.
$ readelf -x .note.GNU-stack myprintf.o #這個也沒有內容

Section '.note.GNU-stack' has no data to dump.
$ readelf -x .shstrtab myprintf.o       #包括全部節區的名字

Hex dump of section '.shstrtab':
  0x00000000 002e7379 6d746162 002e7374 72746162 ..symtab..strtab
  0x00000010 002e7368 73747274 6162002e 72656c2e ..shstrtab..rel.
  0x00000020 74657874 002e6461 7461002e 62737300 text..data..bss.
  0x00000030 2e726f64 61746100 2e636f6d 6d656e74 .rodata..comment
  0x00000040 002e6e6f 74652e47 4e552d73 7461636b ..note.GNU-stack
  0x00000050 00                                  .

$ readelf -symtab myprintf.o    #符號表,包括全部用到的相關符號信息,如函數名、變量名

Symbol table '.symtab' contains 10 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS myprintf.c
     2: 00000000     0 SECTION LOCAL  DEFAULT    1
     3: 00000000     0 SECTION LOCAL  DEFAULT    3
     4: 00000000     0 SECTION LOCAL  DEFAULT    4
     5: 00000000     0 SECTION LOCAL  DEFAULT    5
     6: 00000000     0 SECTION LOCAL  DEFAULT    7
     7: 00000000     0 SECTION LOCAL  DEFAULT    6
     8: 00000000    24 FUNC    GLOBAL DEFAULT    1 myprintf
     9: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND puts
$ readelf -x .strtab myprintf.o #字符串表,用到的字符串,包括文件名、函數名、變量名等。

Hex dump of section '.strtab':
  0x00000000 006d7970 72696e74 662e6300 6d797072 .myprintf.c.mypr
  0x00000010 696e7466 00707574 7300              intf.puts.


    從上表能夠看出,對於可重定位文件,會包含這些基本節區.text, .rel.text, .data, .bss, .rodata, .comment, .note.GNU-stack, .shstrtab, .symtab和.strtab。爲了進一步理解這些節區和源代碼的關係,這裏來看一看myprintf.c產生的彙編代碼。

Quote:

$ gcc -S myprintf.c
$ cat myprintf.s
        .file   "myprintf.c"
        .section        .rodata
.LC0:
        .string "hello, world!"
        .text
.globl myprintf
        .type   myprintf, @function
myprintf:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $8, %esp
        subl    $12, %esp
        pushl   $.LC0
        call    puts
        addl    $16, %esp
        leave
        ret
        .size   myprintf, .-myprintf
        .ident  "GCC: (GNU) 4.1.2"
        .section        .note.GNU-stack,"",@progbits


    是否是能夠從中看出可重定位文件中的那些節區和彙編語言代碼之間的關係?在上面的可重定位文件,能夠看到有一個可重定位的節區,即. rel.text,它標記了兩個須要重定位的項,.rodata和puts。這個節區將告訴編譯器這兩個信息在連接或者動態連接的過程當中須要重定位,具體如何重定位?將根據重定位項的類型,好比上面的R_386_32和R_386_PC32(關於這些類型的更多細節,請查看ELF手冊[6])。

    到這裏,對可重定位文件應該有了一個基本的瞭解,下面將介紹什麼是可重定位,可重定位文件究竟是如何被連接生成可執行文件和動態鏈接庫的,這個過程除了進行了一些符號的重定位外,還進行了哪些工做呢?

本節參考資料:

[1] 瞭解編譯程序的過程
http://9iyou.com/Program_Data/linuxunix-3125.html
http://www.host01.com/article/server/00070002/0621409075078127.htm
[2] C track: compiling C programs.
http://www.cs.caltech.edu/courses/cs11/material/c/mike/misc/compiling_c.html
[3] Dissecting shared libraries
http://www.ibm.com/developerworks/linux/library/l-shlibs.html

四、連接

    開篇:重定位是將符號引用與符號定義進行連接的過程。所以連接是處理可重定位文件,把它們的各類符號引用和符號定義轉換爲可執行文件中的合適信息(通常是虛擬內存地址)的過程。連接又分爲靜態連接和動態連接,前者是程序開發階段程序員用ld(gcc實際上在後臺調用了ld)靜態連接器手動連接的過程,而動態連接則是程序運行期間系統調用動態連接器(ld-linux.so)自動連接的過程。好比,若是連接到可執行文件中的是靜態鏈接庫libmyprintf.a,那麼. rodata節區在連接後須要被重定位到一個絕對的虛擬內存地址,以便程序運行時可以正確訪問該節區中的字符串信息。而對於puts,由於它是動態鏈接庫libc.so中定義的函數,因此會在程序運行時經過動態符號連接找出puts函數在內存中的地址,以便程序調用該函數。在這裏主要討論靜態連接過程,動態連接過程見《動態符號連接的細節》。

          靜態連接過程主要是把可重定位文件依次讀入,分析各個文件的文件頭,進而依次讀入各個文件的節區,並計算各個節區的虛擬內存位置對一些須要重定位的符號進行處理,設定它們的虛擬內存地址等,並最終產生一個可執行文件或者是動態連接庫。這個連接過程是經過ld來完成的,ld在連接時使用了一個連接腳本(linker script),該連接腳本處理連接的具體細節。因爲靜態符號連接過程很是複雜,特別是計算符號地址的過程,考慮到時間關係,相關細節請參考ELF手冊[6]。這裏主要介紹可重定位文件中的節區(節區表描述的)和可執行文件中段(程序頭描述的)的對應關係以及gcc編譯時採用的一些默認連接選項。

    下面先來看看可執行文件的節區信息,經過程序頭(段表)來查看:

Quote:

$ readelf -S test.o                        #爲了比較,先把test.o的節區表也列出
There are 10 section headers, starting at offset 0xb4:

Section Headers:
  [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 000024 00  AX  0   0  4
  [ 2] .rel.text         REL             00000000 0002ec 000008 08      8   1  4
  [ 3] .data             PROGBITS        00000000 000058 000000 00  WA  0   0  4
  [ 4] .bss              NOBITS          00000000 000058 000000 00  WA  0   0  4
  [ 5] .comment          PROGBITS        00000000 000058 000012 00      0   0  1
  [ 6] .note.GNU-stack   PROGBITS        00000000 00006a 000000 00      0   0  1
  [ 7] .shstrtab         STRTAB          00000000 00006a 000049 00      0   0  1
  [ 8] .symtab           SYMTAB          00000000 000244 000090 10      9   7  4
  [ 9] .strtab           STRTAB          00000000 0002d4 000016 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)
$ gcc -o test test.o libmyprintf.o
$ readelf -l test        #咱們發現,test和test.o,libmyprintf.o相比,多了不少節區,如.interp和.init等

Elf file type is EXEC (Executable file)
Entry point 0x80482b0
There are 7 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
  INTERP         0x000114 0x08048114 0x08048114 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x0047c 0x0047c R E 0x1000
  LOAD           0x00047c 0x0804947c 0x0804947c 0x00104 0x00108 RW  0x1000
  DYNAMIC        0x000490 0x08049490 0x08049490 0x000c8 0x000c8 RW  0x4
  NOTE           0x000128 0x08048128 0x08048128 0x00020 0x00020 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

 Section to Segment mapping:
  Segment Sections...
   00    
   01     .interp
   02     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame
   03     .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
   04     .dynamic
   05     .note.ABI-tag
   06    


    上表給出了可執行文件的以下幾個段(segment),

PHDR: 給出了程序表自身的大小和位置,不能出現一次以上。
INTERP: 由於程序中調用了puts(在動態連接庫中定義),使用了動態鏈接庫,所以須要動態裝載器/連接器(ld-linux.so)
LOAD: 包括程序的指令,.text等節區都映射在該段,只讀(R)
LOAD: 包括程序的數據,.data, .bss等節區都映射在該段,可讀寫(RW)
DYNAMIC: 動態連接相關的信息,好比包含有引用的動態鏈接庫名字等信息
NOTE: 給出一些附加信息的位置和大小
GNU_STACK: 這裏爲空,應該是和GNU相關的一些信息

    這裏的段可能包括以前的一個或者多個節區,也就是說通過連接以後原來的節區被重排了,並映射到了不一樣的段,這些段將告訴系統應該如何把它加載到內存中。

    從上表中,經過比較可執行文件(test)中擁有的節區和可重定位文件(test.o和myprintf.o)中擁有的節區後發現,連接以後多了一些以前沒有的節區,這些新的節區來自哪裏?它們的做用是什麼呢?先來經過gcc的-v參數看看它的後臺連接過程。

Quote:

$ gcc -v -o test test.o myprintf.o    #把可重定位文件連接成可執行文件
Reading specs from /usr/lib/gcc/i486-slackware-linux/4.1.2/specs
Target: i486-slackware-linux
Configured with: ../gcc-4.1.2/configure --prefix=/usr --enable-shared --enable-languages=ada,c,c++,fortran,java,objc --enable-threads=posix --enable-__cxa_atexit --disable-checking --with-gnu-ld --verbose --with-arch=i486 --target=i486-slackware-linux --host=i486-slackware-linux
Thread model: posix
gcc version 4.1.2
 /usr/libexec/gcc/i486-slackware-linux/4.1.2/collect2 --eh-frame-hdr -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/gcc/i486-slackware-linux/4.1.2/../../../crt1.o /usr/lib/gcc/i486-slackware-linux/4.1.2/../../../crti.o /usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/lib/gcc/i486-slackware-linux/4.1.2/../../../../i486-slackware-linux/lib -L/usr/lib/gcc/i486-slackware-linux/4.1.2/../../.. test.o myprintf.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o /usr/lib/gcc/i486-slackware-linux/4.1.2/../../../crtn.o


    從上邊的演示看出,gcc在鏈接了咱們本身的目標文件test.o和myprintf.o以外,還鏈接了crt1.o,crtbegin.o等額外的目標文件,難道那些新的節區就來自這些文件?
    另外gcc在進行了相關配置(./configure)後,調用了collect2,卻並無調用ld,經過查找gcc文檔中和collect2相關的部分發現collect2在後臺實際上仍是去尋找ld命令的。爲了理解gcc默認鏈接的後臺細節,這裏直接把collect2替換成ld,並把一些路徑換成絕對路徑或者簡化,獲得以下的ld命令以及執行的效果。

Quote:

$ ld --eh-frame-hdr \
-m elf_i386 \
-dynamic-linker /lib/ld-linux.so.2 \
-o test \
/usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o \
test.o myprintf.o \
-L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/i486-slackware-linux/lib -L/usr/lib/ -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed \
/usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o /usr/lib/crtn.o
$ ./test
hello, world!


不出咱們所料,它完美的運行了。下面經過ld的手冊(man ld)來分析一下這幾個參數。

--eh-frame-hdr

要求建立一個.eh_frame_hdr節區(貌似目標文件test中並無這個節區,因此不關心它)。

 

 

Quote:

$ ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/crt1.o /usr/lib/crti.o test.o myprintf.o -L/usr/lib -lc /usr/lib/crtn.o    #後面發現不用連接libgcc,也不用--eh-frame-hdr參數
$ readelf -l test

Elf file type is EXEC (Executable file)
Entry point 0x80482b0
There are 7 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
  INTERP         0x000114 0x08048114 0x08048114 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x003ea 0x003ea R E 0x1000
  LOAD           0x0003ec 0x080493ec 0x080493ec 0x000e8 0x000e8 RW  0x1000
  DYNAMIC        0x0003ec 0x080493ec 0x080493ec 0x000c8 0x000c8 RW  0x4
  NOTE           0x000128 0x08048128 0x08048128 0x00020 0x00020 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

 Section to Segment mapping:
  Segment Sections...
   00    
   01     .interp
   02     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata
   03     .dynamic .got .got.plt .data
   04     .dynamic
   05     .note.ABI-tag
   06    
$ ./test
hello, world!

 

 

Quote:

$ ld  -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/crt1.o test.o myprintf.o -L/usr/lib/ -lc
/usr/lib/libc_nonshared.a(elf-init.oS): In function `__libc_csu_init':
(.text+0x25): undefined reference to `_init'

 

 

Quote:

$ readelf -s /usr/lib/crt1.o | grep __libc_csu_init
    18: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND __libc_csu_init
$ readelf -s /usr/lib/crti.o | grep _init
    17: 00000000     0 FUNC    GLOBAL DEFAULT    5 _init

 

 

Quote:

$ ld  -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test test.o myprintf.o -L/usr/lib/ -lc
ld: warning: cannot find entry symbol _start; defaulting to 00000000080481a4

 

 

Quote:

$ ./test
hello, world!
Segmentation fault

 

 

Quote:
相關文章
相關標籤/搜索