99re热这里只有精品视频,7777色鬼xxxx欧美色妇,国产成人精品一区二三区在线观看,内射爽无广熟女亚洲,精品人妻av一区二区三区

Gcc 編譯的背后

2018-02-24 15:41 更新

Gcc 編譯的背后

前言

平時(shí)在 Linux 下寫代碼,直接用 gcc -o out in.c 就把代碼編譯好了,但是這背后到底做了什么呢?

如果學(xué)習(xí)過(guò)《編譯原理》則不難理解,一般高級(jí)語(yǔ)言程序編譯的過(guò)程莫過(guò)于:預(yù)處理、編譯、匯編、鏈接。

gcc 在后臺(tái)實(shí)際上也經(jīng)歷了這幾個(gè)過(guò)程,可以通過(guò) -v 參數(shù)查看它的編譯細(xì)節(jié),如果想看某個(gè)具體的編譯過(guò)程,則可以分別使用 -E,-S,-c-O,對(duì)應(yīng)的后臺(tái)工具則分別為 cpp,cc1as,ld

下面將逐步分析這幾個(gè)過(guò)程以及相關(guān)的內(nèi)容,諸如語(yǔ)法檢查、代碼調(diào)試、匯編語(yǔ)言等。

預(yù)處理

簡(jiǎn)述

預(yù)處理是 C 語(yǔ)言程序從源代碼變成可執(zhí)行程序的第一步,主要是 C 語(yǔ)言編譯器對(duì)各種預(yù)處理命令進(jìn)行處理,包括頭文件的包含、宏定義的擴(kuò)展、條件編譯的選擇等。

以前沒怎么“深入”預(yù)處理,腦子對(duì)這些東西總是很模糊,只記得在編譯的基本過(guò)程(詞法分析、語(yǔ)法分析)之前還需要對(duì)源代碼中的宏定義、文件包含、條件編譯等命令進(jìn)行處理。這三類的指令很常見,主要有 #define,#include#ifdef ... #endif,要特別地注意它們的用法。

#define 除了可以獨(dú)立使用以便靈活設(shè)置一些參數(shù)外,還常常和 #ifdef ... #endif 結(jié)合使用,以便靈活地控制代碼塊的編譯與否,也可以用來(lái)避免同一個(gè)頭文件的多次包含。關(guān)于 #include 貌似比較簡(jiǎn)單,通過(guò) man 找到某個(gè)函數(shù)的頭文件,復(fù)制進(jìn)去,加上 <> 就好。這里雖然只關(guān)心一些技巧,不過(guò)預(yù)處理還是隱藏著很多潛在的陷阱(可參考《C Traps & Pitfalls》)也是需要注意的。下面僅介紹和預(yù)處理相關(guān)的幾個(gè)簡(jiǎn)單內(nèi)容。

打印出預(yù)處理之后的結(jié)果

$ gcc -E hello.c

這樣就可以看到源代碼中的各種預(yù)處理命令是如何被解釋的,從而方便理解和查錯(cuò)。

實(shí)際上 gcc 在這里調(diào)用了 cpp(雖然通過(guò) gcc -v 僅看到 cc1),cpp 即 The C Preprocessor,主要用來(lái)預(yù)處理宏定義、文件包含、條件編譯等。下面介紹它的一個(gè)比較重要的選項(xiàng) -D

在命令行定義宏

$ gcc -Dmacro hello.c

這個(gè)等同于在文件的開頭定義宏,即 #define macro,但是在命令行定義更靈活。例如,在源代碼中有這些語(yǔ)句。

#ifdef DEBUG
printf("this code is for debugging\n");
#endif

如果編譯時(shí)加上 -DDEBUG 選項(xiàng),那么編譯器就會(huì)把 printf 所在的行編譯進(jìn)目標(biāo)代碼,從而方便地跟蹤該位置的某些程序狀態(tài)。這樣 -DDEBUG 就可以當(dāng)作一個(gè)調(diào)試開關(guān),編譯時(shí)加上它就可以用來(lái)打印調(diào)試信息,發(fā)布時(shí)則可以通過(guò)去掉該編譯選項(xiàng)把調(diào)試信息去掉。

編譯(翻譯)

簡(jiǎn)述

編譯之前,C 語(yǔ)言編譯器會(huì)進(jìn)行詞法分析、語(yǔ)法分析,接著會(huì)把源代碼翻譯成中間語(yǔ)言,即匯編語(yǔ)言。如果想看到這個(gè)中間結(jié)果,可以用 gcc -S。需要提到的是,諸如 Shell 等解釋語(yǔ)言也會(huì)經(jīng)歷一個(gè)詞法分析和語(yǔ)法分析的階段,不過(guò)之后并不會(huì)進(jìn)行“翻譯”,而是“解釋”,邊解釋邊執(zhí)行。

把源代碼翻譯成匯編語(yǔ)言,實(shí)際上是編譯的整個(gè)過(guò)程中的第一個(gè)階段,之后的階段和匯編語(yǔ)言的開發(fā)過(guò)程沒有什么區(qū)別。這個(gè)階段涉及到對(duì)源代碼的詞法分析、語(yǔ)法檢查(通過(guò) -std 指定遵循哪個(gè)標(biāo)準(zhǔn)),并根據(jù)優(yōu)化(-O)要求進(jìn)行翻譯成匯編語(yǔ)言的動(dòng)作。

語(yǔ)法檢查

如果僅僅希望進(jìn)行語(yǔ)法檢查,可以用 gcc-fsyntax-only 選項(xiàng);如果為了使代碼有比較好的可移植性,避免使用 gcc 的一些擴(kuò)展特性,可以結(jié)合 -std-pedantic(或者 -pedantic-erros )選項(xiàng)讓源代碼遵循某個(gè) C 語(yǔ)言標(biāo)準(zhǔn)的語(yǔ)法。這里演示一個(gè)簡(jiǎn)單的例子:

$ 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    #默認(rèn)情況下,gcc是允許在程序中間聲明變量的,但是turboc就不支持
hello.c: In function ‘main’:
hello.c:5: error: ISO C90 forbids mixed declarations and code

語(yǔ)法錯(cuò)誤是程序開發(fā)過(guò)程中難以避免的錯(cuò)誤(人的大腦在很多情況下都容易開小差),不過(guò)編譯器往往能夠通過(guò)語(yǔ)法檢查快速發(fā)現(xiàn)這些錯(cuò)誤,并準(zhǔn)確地告知語(yǔ)法錯(cuò)誤的大概位置。因此,作為開發(fā)人員,要做的事情不是“恐慌”(不知所措),而是認(rèn)真閱讀編譯器的提示,根據(jù)平時(shí)積累的經(jīng)驗(yàn)(最好總結(jié)一份常見語(yǔ)法錯(cuò)誤索引,很多資料都提供了常見語(yǔ)法錯(cuò)誤列表,如《C Traps & Pitfalls》和編輯器提供的語(yǔ)法檢查功能(語(yǔ)法加亮、括號(hào)匹配提示等)快速定位語(yǔ)法出錯(cuò)的位置并進(jìn)行修改。

編譯器優(yōu)化

語(yǔ)法檢查之后就是翻譯動(dòng)作,gcc 提供了一個(gè)優(yōu)化選項(xiàng) -O,以便根據(jù)不同的運(yùn)行平臺(tái)和用戶要求產(chǎn)生經(jīng)過(guò)優(yōu)化的匯編代碼。例如,

$ gcc -o hello hello.c         # 采用默認(rèn)選項(xiàng),不優(yōu)化
$ gcc -O2 -o hello2 hello.c    # 優(yōu)化等次是2
$ gcc -Os -o hellos hello.c    # 優(yōu)化目標(biāo)代碼的大小
$ 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     # 可能是代碼比較少的緣故,執(zhí)行效率看上去不是很明顯
hello, world

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

$ time ./hellos     # 雖然目標(biāo)代碼小了,但是執(zhí)行效率慢了些
hello, world

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

根據(jù)上面的簡(jiǎn)單演示,可以看出 gcc 有很多不同的優(yōu)化選項(xiàng),主要看用戶的需求了,目標(biāo)代碼的大小和效率之間貌似存在一個(gè)“糾纏”,需要開發(fā)人員自己權(quán)衡。

生成匯編語(yǔ)言文件

下面通過(guò) -S 選項(xiàng)來(lái)看看編譯出來(lái)的中間結(jié)果:匯編語(yǔ)言,還是以之前那個(gè) hello.c 為例。

$ gcc -S hello.c  # 默認(rèn)輸出是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

不知道看出來(lái)沒?和課堂里學(xué)的 intel 的匯編語(yǔ)法不太一樣,這里用的是 AT&T 語(yǔ)法格式。如果想學(xué)習(xí) Linux 下的匯編語(yǔ)言開發(fā),下一節(jié)開始的所有章節(jié)基本上覆蓋了 Linux 下匯編語(yǔ)言開發(fā)的一般過(guò)程,不過(guò)這里不介紹匯編語(yǔ)言語(yǔ)法。

在學(xué)習(xí)后面的章節(jié)之前,建議自學(xué)舊金山大學(xué)的微機(jī)編程課程 CS 630,該課深入介紹了 Linux/X86 平臺(tái)下的 AT&T 匯編語(yǔ)言開發(fā)。如果想在 Qemu 上做這個(gè)課程里的實(shí)驗(yàn),可以閱讀本文作者寫的 CS630: Linux 下通過(guò) Qemu 學(xué)習(xí) X86 AT&T 匯編語(yǔ)言

需要補(bǔ)充的是,在寫 C 語(yǔ)言代碼時(shí),如果能夠?qū)幾g器比較熟悉(工作原理和一些細(xì)節(jié))的話,可能會(huì)很有幫助。包括這里的優(yōu)化選項(xiàng)(有些優(yōu)化選項(xiàng)可能在匯編時(shí)采用)和可能的優(yōu)化措施,例如字節(jié)對(duì)齊、條件分支語(yǔ)句裁減(刪除一些明顯分支)等。

匯編

簡(jiǎn)述

匯編實(shí)際上還是翻譯過(guò)程,只不過(guò)把作為中間結(jié)果的匯編代碼翻譯成了機(jī)器代碼,即目標(biāo)代碼,不過(guò)它還不可以運(yùn)行。如果要產(chǎn)生這一中間結(jié)果,可用 gcc -c,當(dāng)然,也可通過(guò) as 命令處理匯編語(yǔ)言源文件來(lái)產(chǎn)生。

匯編是把匯編語(yǔ)言翻譯成目標(biāo)代碼的過(guò)程,如果有在 Windows 下學(xué)習(xí)過(guò)匯編語(yǔ)言開發(fā),大家應(yīng)該比較熟悉 nasm 匯編工具(支持 Intel 格式的匯編語(yǔ)言),不過(guò)這里主要用 as 匯編工具來(lái)匯編 AT&T 格式的匯編語(yǔ)言,因?yàn)?gcc 產(chǎn)生的中間代碼就是 AT&T 格式的。

生成目標(biāo)代碼

下面來(lái)演示分別通過(guò) gcc -c 選項(xiàng)和 as 來(lái)產(chǎn)生目標(biāo)代碼。

$ file hello.s
hello.s: ASCII assembler program text
$ gcc -c hello.s   #用gcc把匯編語(yǔ)言編譯成目標(biāo)代碼
$ file hello.o     #file命令用來(lái)查看文件類型,目標(biāo)代碼可重定位的(relocatable),
                   #需要通過(guò)ld進(jìn)行進(jìn)一步鏈接成可執(zhí)行程序(executable)和共享庫(kù)(shared)
hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
$ as -o hello.o hello.s        #用as把匯編語(yǔ)言編譯成目標(biāo)代碼
$ file hello.o
hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped

gccas 默認(rèn)產(chǎn)生的目標(biāo)代碼都是 ELF 格式的,因此這里主要討論ELF格式的目標(biāo)代碼(如果有時(shí)間再回顧一下 a.outcoff 格式,當(dāng)然也可以先了解一下,并結(jié)合 objcopy 來(lái)轉(zhuǎn)換它們,比較異同)。

ELF 文件初次接觸

目標(biāo)代碼不再是普通的文本格式,無(wú)法直接通過(guò)文本編輯器瀏覽,需要一些專門的工具。如果想了解更多目標(biāo)代碼的細(xì)節(jié),區(qū)分 relocatable(可重定位)、executable(可執(zhí)行)、shared libarary(共享庫(kù))的不同,我們得設(shè)法了解目標(biāo)代碼的組織方式和相關(guān)的閱讀和分析工具。下面主要介紹這部分內(nèi)容。

BFD is a package which allows applications to use the same routines tooperate on object files whatever the object file format. A new object fileformat can be supported simply by creating a new BFD back end and adding it tothe library.

binutils(GNU Binary Utilities)的很多工具都采用這個(gè)庫(kù)來(lái)操作目標(biāo)文件,這類工具有 objdump,objcopy,nm,strip 等(當(dāng)然,我們也可以利用它。如果深入了解ELF格式,那么通過(guò)它來(lái)分析和編寫 Virus 程序?qū)?huì)更加方便),不過(guò)另外一款非常優(yōu)秀的分析工具 readelf 并不是基于這個(gè)庫(kù),所以也應(yīng)該可以直接用 elf.h 頭文件中定義的相關(guān)結(jié)構(gòu)來(lái)操作 ELF 文件。

下面將通過(guò)這些輔助工具(主要是 readelfobjdump),結(jié)合 ELF 手冊(cè)來(lái)分析它們。將依次介紹 ELF 文件的結(jié)構(gòu)和三種不同類型 ELF 文件的區(qū)別。

ELF 文件的結(jié)構(gòu)

ELF Header(ELF文件頭)
Program Headers Table(程序頭表,實(shí)際上叫段表好一些,用于描述可執(zhí)行文件和可共享庫(kù))
Section 1
Section 2
Section 3
...
Section Headers Table(節(jié)區(qū)頭部表,用于鏈接可重定位文件成可執(zhí)行文件或共享庫(kù))

對(duì)于可重定位文件,程序頭是可選的,而對(duì)于可執(zhí)行文件和共享庫(kù)文件(動(dòng)態(tài)鏈接庫(kù)),節(jié)區(qū)表則是可選的??梢苑謩e通過(guò) readelf 文件的 -h,-l-S 參數(shù)查看 ELF 文件頭(ELF Header)、程序頭部表(Program Headers Table,段表)和節(jié)區(qū)表(Section Headers Table)。

文件頭說(shuō)明了文件的類型,大小,運(yùn)行平臺(tái),節(jié)區(qū)數(shù)目等。

三種不同類型 ELF 文件比較

先來(lái)通過(guò)文件頭看看不同ELF的類型。為了說(shuō)明問(wèn)題,先來(lái)幾段代碼吧。

/* myprintf.c */
#include <stdio.h>

void myprintf(void)
{
    printf("hello, world!\n");
}
/* test.h -- myprintf function declaration */

#ifndef _TEST_H_
#define _TEST_H_

void myprintf(void);

#endif
/* test.c */
#include "test.h"

int main()
{
    myprintf();
    return 0;
}

下面通過(guò)這幾段代碼來(lái)演示通過(guò) readelf -h 參數(shù)查看 ELF 的不同類型。期間將演示如何創(chuàng)建動(dòng)態(tài)鏈接庫(kù)(即可共享文件)、靜態(tài)鏈接庫(kù),并比較它們的異同。

編譯產(chǎn)生兩個(gè)目標(biāo)文件 myprintf.otest.o,它們都是可重定位文件(REL):

$ gcc -c myprintf.c test.c
$ readelf -h test.o | grep Type
  Type:                              REL (Relocatable file)
$ readelf -h myprintf.o | grep Type
  Type:                              REL (Relocatable file)

根據(jù)目標(biāo)代碼鏈接產(chǎn)生可執(zhí)行文件,這里的文件類型是可執(zhí)行的(EXEC):

$ gcc -o test myprintf.o test.o
$ readelf -h test | grep Type
  Type:                              EXEC (Executable file)

ar 命令創(chuàng)建一個(gè)靜態(tài)鏈接庫(kù),靜態(tài)鏈接庫(kù)也是可重定位文件(REL):

$ ar rcsv libmyprintf.a myprintf.o
$ readelf -h libmyprintf.a | grep Type
  Type:                              REL (Relocatable file)

可見,靜態(tài)鏈接庫(kù)和可重定位文件類型一樣,它們之間唯一不同是前者可以是多個(gè)可重定位文件的“集合”。

靜態(tài)鏈接庫(kù)可直接鏈接(只需庫(kù)名,不要前面的 lib),也可用 -l 參數(shù),-L 指定庫(kù)搜索路徑。

$ gcc -o test test.o -lmyprintf -L./

編譯產(chǎn)生動(dòng)態(tài)鏈接庫(kù),并支持 majorminor 版本號(hào),動(dòng)態(tài)鏈接庫(kù)類型為 DYN

$ gcc -Wall myprintf.o -shared -Wl,-soname,libmyprintf.so.0 -o libmyprintf.so.0.0
$ 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)

動(dòng)態(tài)鏈接庫(kù)編譯時(shí)和靜態(tài)鏈接庫(kù)類似:

$ gcc -o test test.o -lmyprintf -L./

但是執(zhí)行時(shí)需要指定動(dòng)態(tài)鏈接庫(kù)的搜索路徑,把 LD_LIBRARY_PATH 設(shè)為當(dāng)前目錄,指定 test 運(yùn)行時(shí)的動(dòng)態(tài)鏈接庫(kù)搜索路徑:

$ LD_LIBRARY_PATH=./ ./test
$ gcc -static -o test test.o -lmyprintf -L./

在不指定 -static 時(shí)會(huì)優(yōu)先使用動(dòng)態(tài)鏈接庫(kù),指定時(shí)則阻止使用動(dòng)態(tài)鏈接庫(kù),這時(shí)會(huì)把所有靜態(tài)鏈接庫(kù)文件加入到可執(zhí)行文件中,使得執(zhí)行文件很大,而且加載到內(nèi)存以后會(huì)浪費(fèi)內(nèi)存空間,因此不建議這么做。

經(jīng)過(guò)上面的演示基本可以看出它們之間的不同:

  • 可重定位文件本身不可以運(yùn)行,僅僅是作為可執(zhí)行文件、靜態(tài)鏈接庫(kù)(也是可重定位文件)、動(dòng)態(tài)鏈接庫(kù)的 “組件”。
  • 靜態(tài)鏈接庫(kù)和動(dòng)態(tài)鏈接庫(kù)本身也不可以執(zhí)行,作為可執(zhí)行文件的“組件”,它們兩者也不同,前者也是可重定位文件(只不過(guò)可能是多個(gè)可重定位文件的集合),并且在鏈接時(shí)加入到可執(zhí)行文件中去。
  • 而動(dòng)態(tài)鏈接庫(kù)在鏈接時(shí),庫(kù)文件本身并沒有添加到可執(zhí)行文件中,只是在可執(zhí)行文件中加入了該庫(kù)的名字等信息,以便在可執(zhí)行文件運(yùn)行過(guò)程中引用庫(kù)中的函數(shù)時(shí)由動(dòng)態(tài)鏈接器去查找相關(guān)函數(shù)的地址,并調(diào)用它們。

從這個(gè)意義上說(shuō),動(dòng)態(tài)鏈接庫(kù)本身也具有可重定位的特征,含有可重定位的信息。對(duì)于什么是重定位?如何進(jìn)行靜態(tài)符號(hào)和動(dòng)態(tài)符號(hào)的重定位,我們將在鏈接部分和《動(dòng)態(tài)符號(hào)鏈接的細(xì)節(jié)》一節(jié)介紹。

ELF 主體:節(jié)區(qū)

下面來(lái)看看 ELF 文件的主體內(nèi)容:節(jié)區(qū)(Section)。

ELF 文件具有很大的靈活性,它通過(guò)文件頭組織整個(gè)文件的總體結(jié)構(gòu),通過(guò)節(jié)區(qū)表 (Section Headers Table)和程序頭(Program Headers Table 或者叫段表)來(lái)分別描述可重定位文件和可執(zhí)行文件。但不管是哪種類型,它們都需要它們的主體,即各種節(jié)區(qū)。

在可重定位文件中,節(jié)區(qū)表描述的就是各種節(jié)區(qū)本身;而在可執(zhí)行文件中,程序頭描述的是由各個(gè)節(jié)區(qū)組成的段(Segment),以便程序運(yùn)行時(shí)動(dòng)態(tài)裝載器知道如何對(duì)它們進(jìn)行內(nèi)存映像,從而方便程序加載和運(yùn)行。

下面先來(lái)看看一些常見的節(jié)區(qū),而關(guān)于這些節(jié)區(qū)(Section)如何通過(guò)重定位構(gòu)成不同的段(Segments),以及有哪些常規(guī)的段,我們將在鏈接部分進(jìn)一步介紹。

可以通過(guò) readelf -S 查看 ELF 的節(jié)區(qū)。(建議一邊操作一邊看文檔,以便加深對(duì) ELF 文件結(jié)構(gòu)的理解)先來(lái)看看可重定位文件的節(jié)區(qū)信息,通過(guò)節(jié)區(qū)表來(lái)查看:

默認(rèn)編譯好 myprintf.c,將產(chǎn)生一個(gè)可重定位的文件 myprintf.o,這里通過(guò) myprintf.o 的節(jié)區(qū)表查看節(jié)區(qū)信息。

$ gcc -c myprintf.c
$ readelf -S 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 可看反編譯結(jié)果,用 -j 選項(xiàng)可指定需要查看的節(jié)區(qū):

$ objdump -d -j .text   myprintf.o
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

-r 選項(xiàng)可以看到有關(guān)重定位的信息,這里有兩部分需要重定位:

$ readelf -r myprintf.o

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

.rodata 節(jié)區(qū)包含只讀數(shù)據(jù),即我們要打印的 hello, world!

$ readelf -x .rodata myprintf.o

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

沒有找到 .data 節(jié)區(qū), 它應(yīng)該包含一些初始化的數(shù)據(jù):

$ readelf -x .data myprintf.o

Section '.data' has no data to dump.

也沒有 .bss 節(jié)區(qū),它應(yīng)該包含一些未初始化的數(shù)據(jù),程序默認(rèn)初始為 0:

$ readelf -x .bss       myprintf.o

Section '.bss' has no data to dump.

.comment 是一些注釋,可以看到是是 Gcc 的版本信息

$ readelf -x .comment myprintf.o

Hex dump of section '.comment':
  0x00000000 00474343 3a202847 4e552920 342e312e .GCC: (GNU) 4.1.
  0x00000010 3200                                2.

.note.GNU-stack 這個(gè)節(jié)區(qū)也沒有內(nèi)容:

$ readelf -x .note.GNU-stack myprintf.o

Section '.note.GNU-stack' has no data to dump.

.shstrtab 包括所有節(jié)區(qū)的名字:

$ 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                                  .

符號(hào)表 .symtab 包括所有用到的相關(guān)符號(hào)信息,如函數(shù)名、變量名,可用 readelf 查看:

$ 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

字符串表 .strtab 包含用到的字符串,包括文件名、函數(shù)名、變量名等:

$ readelf -x .strtab myprintf.o

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

從上表可以看出,對(duì)于可重定位文件,會(huì)包含這些基本節(jié)區(qū) .text, .rel.text, .data, .bss, .rodata, .comment, .note.GNU-stack, .shstrtab, .symtab.strtab。

匯編語(yǔ)言文件中的節(jié)區(qū)表述

為了進(jìn)一步理解這些節(jié)區(qū)和源代碼的關(guān)系,這里來(lái)看一看 myprintf.c 產(chǎn)生的匯編代碼。

$ 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

是不是可以從中看出可重定位文件中的那些節(jié)區(qū)和匯編語(yǔ)言代碼之間的關(guān)系?在上面的可重定位文件,可以看到有一個(gè)可重定位的節(jié)區(qū),即 .rel.text,它標(biāo)記了兩個(gè)需要重定位的項(xiàng),.rodataputs。這個(gè)節(jié)區(qū)將告訴編譯器這兩個(gè)信息在鏈接或者動(dòng)態(tài)鏈接的過(guò)程中需要重定位, 具體如何重定位?將根據(jù)重定位項(xiàng)的類型,比如上面的 R_386_32R_386_PC32。

到這里,對(duì)可重定位文件應(yīng)該有了一個(gè)基本的了解,下面將介紹什么是可重定位,可重定位文件到底是如何被鏈接生成可執(zhí)行文件和動(dòng)態(tài)鏈接庫(kù)的,這個(gè)過(guò)程除了進(jìn)行一些符號(hào)的重定位外,還進(jìn)行了哪些工作呢?

鏈接

簡(jiǎn)述

重定位是將符號(hào)引用與符號(hào)定義進(jìn)行鏈接的過(guò)程。因此鏈接是處理可重定位文件,把它們的各種符號(hào)引用和符號(hào)定義轉(zhuǎn)換為可執(zhí)行文件中的合適信息(一般是虛擬內(nèi)存地址)的過(guò)程。

鏈接又分為靜態(tài)鏈接和動(dòng)態(tài)鏈接,前者是程序開發(fā)階段程序員用 ldgcc 實(shí)際上在后臺(tái)調(diào)用了 ld)靜態(tài)鏈接器手動(dòng)鏈接的過(guò)程,而動(dòng)態(tài)鏈接則是程序運(yùn)行期間系統(tǒng)調(diào)用動(dòng)態(tài)鏈接器(ld-linux.so)自動(dòng)鏈接的過(guò)程。

比如,如果鏈接到可執(zhí)行文件中的是靜態(tài)鏈接庫(kù) libmyprintf.a,那么 .rodata 節(jié)區(qū)在鏈接后需要被重定位到一個(gè)絕對(duì)的虛擬內(nèi)存地址,以便程序運(yùn)行時(shí)能夠正確訪問(wèn)該節(jié)區(qū)中的字符串信息。而對(duì)于 puts 函數(shù),因?yàn)樗莿?dòng)態(tài)鏈接庫(kù) libc.so 中定義的函數(shù),所以會(huì)在程序運(yùn)行時(shí)通過(guò)動(dòng)態(tài)符號(hào)鏈接找出 puts 函數(shù)在內(nèi)存中的地址,以便程序調(diào)用該函數(shù)。在這里主要討論靜態(tài)鏈接過(guò)程,動(dòng)態(tài)鏈接過(guò)程見《動(dòng)態(tài)符號(hào)鏈接的細(xì)節(jié)》。

靜態(tài)鏈接過(guò)程主要是把可重定位文件依次讀入,分析各個(gè)文件的文件頭,進(jìn)而依次讀入各個(gè)文件的節(jié)區(qū),并計(jì)算各個(gè)節(jié)區(qū)的虛擬內(nèi)存位置,對(duì)一些需要重定位的符號(hào)進(jìn)行處理,設(shè)定它們的虛擬內(nèi)存地址等,并最終產(chǎn)生一個(gè)可執(zhí)行文件或者是動(dòng)態(tài)鏈接庫(kù)。這個(gè)鏈接過(guò)程是通過(guò) ld 來(lái)完成的,ld 在鏈接時(shí)使用了一個(gè)鏈接腳本(linker script),該鏈接腳本處理鏈接的具體細(xì)節(jié)。

由于靜態(tài)符號(hào)鏈接過(guò)程非常復(fù)雜,特別是計(jì)算符號(hào)地址的過(guò)程,考慮到時(shí)間關(guān)系,相關(guān)細(xì)節(jié)請(qǐng)參考 ELF 手冊(cè)。這里主要介紹可重定位文件中的節(jié)區(qū)(節(jié)區(qū)表描述的)和可執(zhí)行文件中段(程序頭描述的)的對(duì)應(yīng)關(guān)系以及 gcc 編譯時(shí)采用的一些默認(rèn)鏈接選項(xiàng)。

可執(zhí)行文件的段:節(jié)區(qū)重排

下面先來(lái)看看可執(zhí)行文件的節(jié)區(qū)信息,通過(guò)程序頭(段表)來(lái)查看,為了比較,先把 test.o 的節(jié)區(qū)表也列出:

$ readelf -S 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 myprintf.o
$ 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 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

可發(fā)現(xiàn),testtest.omyprintf.o 相比,多了很多節(jié)區(qū),如 .interp.init 等。另外,上表也給出了可執(zhí)行文件的如下幾個(gè)段(Segment):

  • PHDR: 給出了程序表自身的大小和位置,不能出現(xiàn)一次以上。
  • INTERP: 因?yàn)槌绦蛑姓{(diào)用了 puts(在動(dòng)態(tài)鏈接庫(kù)中定義),使用了動(dòng)態(tài)鏈接庫(kù),因此需要?jiǎng)討B(tài)裝載器/鏈接器(ld-linux.so
  • LOAD: 包括程序的指令,.text 等節(jié)區(qū)都映射在該段,只讀(R)
  • LOAD: 包括程序的數(shù)據(jù),.data,.bss 等節(jié)區(qū)都映射在該段,可讀寫(RW)
  • DYNAMIC: 動(dòng)態(tài)鏈接相關(guān)的信息,比如包含有引用的動(dòng)態(tài)鏈接庫(kù)名字等信息
  • NOTE: 給出一些附加信息的位置和大小
  • GNU_STACK: 這里為空,應(yīng)該是和GNU相關(guān)的一些信息

這里的段可能包括之前的一個(gè)或者多個(gè)節(jié)區(qū),也就是說(shuō)經(jīng)過(guò)鏈接之后原來(lái)的節(jié)區(qū)被重排了,并映射到了不同的段,這些段將告訴系統(tǒng)應(yīng)該如何把它加載到內(nèi)存中。

鏈接背后的故事

從上表中,通過(guò)比較可執(zhí)行文件 test 中擁有的節(jié)區(qū)和可重定位文件(test.omyprintf.o)中擁有的節(jié)區(qū)后發(fā)現(xiàn),鏈接之后多了一些之前沒有的節(jié)區(qū),這些新的節(jié)區(qū)來(lái)自哪里?它們的作用是什么呢?先來(lái)通過(guò) gcc -v 看看它的后臺(tái)鏈接過(guò)程。

把可重定位文件鏈接成可執(zhí)行文件:

$ 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 在鏈接了我們自己的目標(biāo)文件 test.omyprintf.o 之外,還鏈接了 crt1.o,crtbegin.o 等額外的目標(biāo)文件,難道那些新的節(jié)區(qū)就來(lái)自這些文件?

用 ld 完成鏈接過(guò)程

另外 gcc 在進(jìn)行了相關(guān)配置(./configure)后,調(diào)用了 collect2,卻并沒有調(diào)用 ld,通過(guò)查找 gcc 文檔中和 collect2 相關(guān)的部分發(fā)現(xiàn) collect2 在后臺(tái)實(shí)際上還是去尋找 ld 命令的。為了理解 gcc 默認(rèn)鏈接的后臺(tái)細(xì)節(jié),這里直接把 collect2 替換成 ld,并把一些路徑換成絕對(duì)路徑或者簡(jiǎn)化,得到如下的 ld 命令以及執(zhí)行的效果。

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

不出所料,它完美地運(yùn)行了。下面通過(guò) ld 的手冊(cè)(man ld)來(lái)分析一下這幾個(gè)參數(shù):

  • --eh-frame-hdr

    要求創(chuàng)建一個(gè) .eh_frame_hdr 節(jié)區(qū)(貌似目標(biāo)文件test中并沒有這個(gè)節(jié)區(qū),所以不關(guān)心它)。

  • -m elf_i386

    這里指定不同平臺(tái)上的鏈接腳本,可以通過(guò) --verbose 命令查看腳本的具體內(nèi)容,如 ld -m elf_i386 --verbose,它實(shí)際上被存放在一個(gè)文件中(/usr/lib/ldscripts 目錄下),我們可以去修改這個(gè)腳本,具體如何做?請(qǐng)參考 ld 的手冊(cè)。在后面我們將簡(jiǎn)要提到鏈接腳本中是如何預(yù)定義變量的,以及這些預(yù)定義變量如何在我們的程序中使用。需要提到的是,如果不是交叉編譯,那么無(wú)須指定該選項(xiàng)。

  • -dynamic-linker /lib/ld-linux.so.2

    指定動(dòng)態(tài)裝載器/鏈接器,即程序中的 INTERP 段中的內(nèi)容。動(dòng)態(tài)裝載器/鏈接器負(fù)責(zé)鏈接有可共享庫(kù)的可執(zhí)行文件的裝載和動(dòng)態(tài)符號(hào)鏈接。

  • -o test

    指定輸出文件,即可執(zhí)行文件名的名字

  • /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o

    鏈接到 test 文件開頭的一些內(nèi)容,這里實(shí)際上就包含了 .init 等節(jié)區(qū)。.init 節(jié)區(qū)包含一些可執(zhí)行代碼,在 main 函數(shù)之前被調(diào)用,以便進(jìn)行一些初始化操作,在 C++ 中完成構(gòu)造函數(shù)功能。

  • 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

    鏈接 libgcc 庫(kù)和 libc 庫(kù),后者定義有我們需要的 puts 函數(shù)

  • /usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o /usr/lib/crtn.o

    鏈接到 test 文件末尾的一些內(nèi)容,這里實(shí)際上包含了 .fini 等節(jié)區(qū)。.fini 節(jié)區(qū)包含了一些可執(zhí)行代碼,在程序退出時(shí)被執(zhí)行,作一些清理工作,在 C++ 中完成析構(gòu)造函數(shù)功能。我們往往可以通過(guò) atexit 來(lái)注冊(cè)那些需要在程序退出時(shí)才執(zhí)行的函數(shù)。

C++構(gòu)造與析構(gòu):crtbegin.o和crtend.o

對(duì)于 crtbegin.ocrtend.o 這兩個(gè)文件,貌似完全是用來(lái)支持 C++ 的構(gòu)造和析構(gòu)工作的,所以可以不鏈接到我們的可執(zhí)行文件中,鏈接時(shí)把它們?nèi)サ艨纯矗?/p>

$ 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    #后面發(fā)現(xiàn)不用鏈接libgcc,也不用--eh-frame-hdr參數(shù)
$ 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!

完全可以工作,而且發(fā)現(xiàn) .ctors(保存著程序中全局構(gòu)造函數(shù)的指針數(shù)組), .dtors(保存著程序中全局析構(gòu)函數(shù)的指針數(shù)組),.jcr(未知),.eh_frame 節(jié)區(qū)都沒有了,所以 crtbegin.ocrtend.o 應(yīng)該包含了這些節(jié)區(qū)。

初始化與退出清理:crti.o 和 crtn.o

而對(duì)于另外兩個(gè)文件 crti.ocrtn.o,通過(guò) readelf -S 查看后發(fā)現(xiàn)它們都有 .init.fini 節(jié)區(qū),如果我們不需要讓程序進(jìn)行一些初始化和清理工作呢?是不是就可以不鏈接這個(gè)兩個(gè)文件?試試看。

$ 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'

貌似不行,竟然有人調(diào)用了 __libc_csu_init 函數(shù),而這個(gè)函數(shù)引用了 _init。這兩個(gè)符號(hào)都在哪里呢?

$ 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

竟然是 crt1.o 調(diào)用了 __libc_csu_init 函數(shù),而該函數(shù)卻引用了我們沒有鏈接的 crti.o 文件中定義的 _init 符號(hào)。這樣的話不鏈接 crti.ocrtn.o 文件就不成了羅?不對(duì)吧,要不干脆不用 crt1.o 算了,看看 gcc 額外鏈接進(jìn)去的最后一個(gè)文件 crt1.o 到底干了個(gè)啥子?

$ 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

這樣卻說(shuō)沒有找到入口符號(hào) _start,難道 crt1.o 中定義了這個(gè)符號(hào)?不過(guò)它給默認(rèn)設(shè)置了一個(gè)地址,只是個(gè)警告,說(shuō)明 test 已經(jīng)生成,不管怎樣先運(yùn)行看看再說(shuō)。

$ ./test
hello, world!
Segmentation fault

貌似程序運(yùn)行完了,不過(guò)結(jié)束時(shí)冒出個(gè)段錯(cuò)誤?可能是程序結(jié)束時(shí)有問(wèn)題,用 gdb 調(diào)試看看:

$ gcc -g -c test.c myprintf.c #產(chǎn)生目標(biāo)代碼, 非交叉編譯,不指定-m也可鏈接,所以下面可去掉-m
$ ld -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 00000000080481d8
$ ./test
hello, world!
Segmentation fault
$ gdb -q ./test
(gdb) l
1       #include "test.h"
2
3       int main()
4       {
5               myprintf();
6               return 0;
7       }
(gdb) break 7      #在程序的末尾設(shè)置一個(gè)斷點(diǎn)
Breakpoint 1 at 0x80481bf: file test.c, line 7.
(gdb) r            #程序都快結(jié)束了都沒問(wèn)題,怎么會(huì)到最后出個(gè)問(wèn)題呢?
Starting program: /mnt/hda8/Temp/c/program/test
hello, world!

Breakpoint 1, main () at test.c:7
7       }
(gdb) n        #單步執(zhí)行看看,怎么下面一條指令是0x00000001,肯定是程序退出以后出了問(wèn)題
0x00000001 in ?? ()
(gdb) n        #誒,當(dāng)然找不到邊了,都跑到0x00000001了
Cannot find bound
以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)