成都蜀美網(wǎng)站建設(shè)徐州網(wǎng)站建設(shè)
關(guān)于Linux的編譯過(guò)程,其實(shí)只需要使用gcc這個(gè)功能,gcc并非一個(gè)編譯器,是一個(gè)驅(qū)動(dòng)程序。其編譯過(guò)程也很熟悉:預(yù)處理–編譯–匯編–鏈接。在接觸底層開發(fā)甚至操作系統(tǒng)開發(fā)時(shí),我們都需要了解這么一個(gè)知識(shí)點(diǎn),如何從我們的代碼到機(jī)器碼。這段過(guò)程經(jīng)歷了什么,我們的函數(shù)變量又是在哪里?一個(gè)個(gè)好奇心驅(qū)使著我寫下這篇文章。于博客中有提及Linux的安裝以及gcc基本環(huán)境搭建、gcc編譯流程、常用gcc指令集:https://blog.csdn.net/Alkaid2000/article/details/128036290?spm=1001.2014.3001.5501
文章目錄
- 0x01 編譯過(guò)程
- 0x02 預(yù)編譯
- 0x03 編譯
- 0x04 匯編
- 目標(biāo)文件ELF
- 翻譯機(jī)器指令
- 操作數(shù)地址通過(guò)ModR/M中的Mod+R/M指定
- 操作數(shù)通過(guò)ModR/M中的Reg/Opcode指定
- 操作數(shù)地址直接嵌入在機(jī)器指令中
- 操作數(shù)直接嵌入在指令中
- 操作數(shù)隱含在Opcode中
- 回到代碼
- 重定位表
- 符號(hào)表
- 0x05 鏈接
- 合并目標(biāo)文件
- 符號(hào)重定位
- 鏈接靜態(tài)庫(kù)
- 鏈接動(dòng)態(tài)庫(kù)
0x01 編譯過(guò)程
可以使用這么一句指令來(lái)觀察一個(gè).c文件所需要經(jīng)歷的編譯過(guò)程:gcc -v main.c
。
根據(jù)gcc的輸出可見,對(duì)于一個(gè)C程序來(lái)說(shuō),從源代碼構(gòu)建出可執(zhí)行文件經(jīng)歷了三個(gè)階段:
- 編譯
gcc使用編譯器ccl.exe
進(jìn)行編譯,產(chǎn)生的編譯代碼保存在目錄/temp
下的文件ccelFAGc.s
中。
- 匯編
gcc使用匯編器as.exe
進(jìn)行匯編,匯編過(guò)程產(chǎn)生匯編文件ccZfpupi.o
,將上面生成的ccelFAGc.s
進(jìn)行匯編。
- 鏈接
調(diào)用collect2.exe
進(jìn)行鏈接。實(shí)際上這個(gè)collect2
只是一個(gè)輔助程序,最終他將調(diào)用鏈接器ld
來(lái)完成真正的鏈接過(guò)程。包括框出來(lái)的crtend.o
、以及啟動(dòng)文件等等,本質(zhì)上都是ld
在進(jìn)行鏈接。
事實(shí)上,從gcc看到只有這三個(gè)過(guò)程,但是對(duì)于C程序來(lái)說(shuō),編譯過(guò)程也分為兩個(gè)階段:預(yù)編譯和編譯。所以軟件構(gòu)建過(guò)程通常分為四個(gè)階段:預(yù)編譯、編譯、匯編、鏈接。
可以通過(guò)gcc手動(dòng)控制以上的編譯流程,從而留下中間文件以方便研究:
gcc HelloWorld.c -E -o HelloWorld.i
預(yù)處理:加入頭文件,替換宏。gcc HelloWorld.c -S -c -o HelloWorld.s
編譯:包含預(yù)處理,將 C 程序轉(zhuǎn)換成匯編程序。gcc HelloWorld.c -c -o HelloWorld.o
匯編:包含預(yù)處理和編譯,將匯編程序轉(zhuǎn)換成可鏈接的二進(jìn)制程序。gcc HelloWorld.c -o HelloWorld
鏈接:包含以上所有操作,將可鏈接的二進(jìn)制程序和其它別的庫(kù)鏈接在一起,形成可執(zhí)行的程序文件。
那么接下來(lái)使用下面這段程序?qū)τ诰幾g過(guò)程來(lái)做個(gè)總結(jié):
hello.c:
#include <stdio.h>
#include "foo.h"extern int foo2;int main(int argc,char *argv[])
{int result;int r = 5;
#ifdef AREAresult = PI*r*r;
#elseresult = PI*r*2;
#endifreturn 0;
}
foo.h
#ifndef _FOO_H
#define _FOO_H#define PI 3.1415926
#define AREAstruct foo_struct{int a;
};#endif
fool2.c
int foo2 = 20;void foo2_func(int x)
{int ret = foo2;
}
fool1.c
int fool = 10;void fool_func()
{int ret = fool;
}
0x02 預(yù)編譯
C語(yǔ)言中的預(yù)編譯是以#
開頭,常用的預(yù)編譯指令包括#include
、#define
、#if
等等。在工具鏈中,一般都提供單獨(dú)的編譯器,比如GCC中提供的編譯器為cpp。但是預(yù)編譯也可以看作編譯過(guò)程的第一遍,是為編譯做的一些工作,所以通常編譯器中也包含了預(yù)編譯的功能。比如前面的gcc并沒有單獨(dú)調(diào)用cpp,而是直接調(diào)用ccl
進(jìn)行編譯,原因就是如上。
gcc -E hello.c -o hello.i
編譯之后可以查看文件hello.i
:
# 7 "foo.h"
struct foo_struct{int a;
};
# 3 "hello.c" 2extern int foo2;int main(int argc,char *argv[])
{int result;int r = 5;result = 3.1415926*r*r;return 0;
}
根據(jù)編譯后的結(jié)果可以總結(jié)出預(yù)編譯指令的處理步驟:
- 文件包含:指示預(yù)編譯器將一個(gè)源文件的內(nèi)容全部復(fù)制到當(dāng)前源文件中。
- 宏定義:預(yù)編譯器將宏名替換為具體的值。
- 條件編譯:保留用戶希望編譯的代碼。(使用#if #else這種形式)
0x03 編譯
編譯程序?qū)︻A(yù)處理過(guò)的結(jié)果進(jìn)行詞法分析、語(yǔ)法分析、語(yǔ)義分析,然后生成中間代碼,對(duì)中間代碼進(jìn)行優(yōu)化,目標(biāo)是使最終生成的可執(zhí)行代碼時(shí)間更短、占用的空間更小,最后生成相應(yīng)的匯編代碼。
gcc -S fool2.c
其內(nèi)容如下:
int foo2 = 20;void foo2_func(int x)
{int ret = foo2;
}
.file "fool2.c".text.globl foo2.data.align 4.type foo2, @object.size foo2, 4
foo2:.long 20.text.globl foo2_func.type foo2_func, @function
foo2_func:
.LFB0:.cfi_startprocendbr64pushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6movl %edi, -20(%rbp)movl foo2(%rip), %eaxmovl %eax, -4(%rbp)noppopq %rbp.cfi_def_cfa 7, 8ret.cfi_endproc
.LFE0:.size foo2_func, .-foo2_func.ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0".section .note.GNU-stack,"",@progbits.section .note.gnu.property,"a".align 8.long 1f - 0f.long 4f - 1f.long 5
0:.string "GNU"
1:.align 8.long 0xc0000002.long 3f - 2f
2:.long 0x3
3:.align 8
4:
在此源文件中,定義了一個(gè)全局變量以及函數(shù),區(qū)區(qū)一行代碼卻出現(xiàn)了這么多匯編語(yǔ)言。其實(shí)里面相當(dāng)多的代碼是偽指令。偽指令不參與CPU運(yùn)行,只指導(dǎo)編譯鏈接過(guò)程。
就像上面所生成的cfi
指令,這個(gè)指令主要的作用是輔助匯編器創(chuàng)建棧幀信息的。
這些偽指令也有其他的作用,比如說(shuō)中斷后會(huì)輸出回溯信息,比如在debug的時(shí)候,需要查找一些變量或者是查看函數(shù)調(diào)用信息。這個(gè)過(guò)程稱為棧的回卷。
在上面的程序中注意到了有個(gè)寄存器rbp
保存了frame pointer
、base pointer
均指向了棧的底部。對(duì)于main函數(shù)來(lái)說(shuō),他并非程序中第一個(gè)運(yùn)行的程序,所以main其實(shí)也是一個(gè)被調(diào)函數(shù),他也有自己的棧幀。在理論上可以使用這些指針來(lái)遍歷調(diào)用過(guò)程中各個(gè)函數(shù)的棧幀,但是由于gcc代碼的優(yōu)化,可能導(dǎo)致調(diào)試器或異常處理很難甚至不能正?;厮輻?#xff0c;所以這些偽指令的目的就是輔助編譯器創(chuàng)建棧幀信息,并且保存在目標(biāo)文件的段.eh_frame
中,這樣就不會(huì)被編譯器優(yōu)化所影響。
去除偽指令后,可以看到代碼如下:
foo2_func:pushq %rbpmovq %rsp, %rbpmovl %edi, -20(%rbp)movl foo2(%rip), %eaxmovl %eax, -4(%rbp)noppopq %rbpret
在匯編代碼中,在函數(shù)的開頭和結(jié)尾處分別會(huì)插入一小段代碼,分別稱為Prologue
和Epilogue
,比如上面1~3句是Prologue
,最后兩句是Epilogue
。
-
Prologue
:保存主調(diào)函數(shù)的frame pointer
,這是為了在子函數(shù)調(diào)用結(jié)束后,恢復(fù)主調(diào)函數(shù)的棧幀。同時(shí)為子函數(shù)準(zhǔn)備棧幀。pushq %rbpmovq %rsp, %rbpmovl %edi, -20(%rbp)
上面這三句話起了一種構(gòu)造函數(shù)的作用,首先需要保存主調(diào)函數(shù)的
frame pointer
,之后保存在寄存器中金壓棧,在退出主函數(shù)時(shí)可以從棧中恢復(fù)主調(diào)函數(shù)frame pointer
;將rsp
賦值給rbp
,即將子函數(shù)的frame pointer
指向主調(diào)函數(shù)的棧頂,這行代碼記錄了子函數(shù)棧幀的底部,從這里就開始了主函數(shù)的棧幀。下面那句是為本地變量分配??臻g。 -
Epilogue
功能是恰恰相反的,如果說(shuō)Prologue
是構(gòu)造函數(shù),那么這個(gè)部分則是析構(gòu)函數(shù)。popq %rbpret
當(dāng)前棧幀的棧底,是
Prologue
保存的主調(diào)函數(shù)的frame pointer
,將其pop
出回到了主調(diào)函數(shù)的main
棧幀,之后,CPU就返回主調(diào)函數(shù)繼續(xù)執(zhí)行。
中間程序的執(zhí)行部分,也就是int ret = foo2
這段,從第四行開始,CPU從數(shù)據(jù)段中讀取了全局變量foo2的值將其放在寄存器eax
中,之后在第五行代碼,將eax
的內(nèi)容,賦值到棧中局部變量ret
的位置。之后代碼根據(jù)局部變量相對(duì)于棧的frame pointer
的偏移來(lái)訪問局部變量,如變量ret
位于相對(duì)于棧底偏移為-4的內(nèi)存處。
0x04 匯編
匯編器將匯編代碼翻譯為機(jī)器指令,每一條匯編語(yǔ)句幾乎都對(duì)應(yīng)一條機(jī)器指令,所以匯編器的匯編過(guò)程相對(duì)于比較簡(jiǎn)單,只需要根據(jù)匯編指令和機(jī)器指令的對(duì)照表進(jìn)行翻譯即可。除了生成機(jī)器碼外,匯編器還要再目標(biāo)文件中創(chuàng)建輔助鏈接時(shí)需要的信息,包括符號(hào)表、重定位表等。
目標(biāo)文件ELF
目標(biāo)文件是匯編過(guò)程的產(chǎn)物。對(duì)于32位的ELF文件來(lái)說(shuō),其最前部是文件頭部信息,描述了整個(gè)文件的基本屬性,除了包括該文件運(yùn)行在什么操作系統(tǒng)中、運(yùn)行在什么硬件體系結(jié)構(gòu)上、程序入口地址是什么等基本信息外,最重要的是記錄了兩個(gè)表格的相關(guān)信息,如表格所在的位置、其中包括了條目數(shù)等。這兩個(gè)表格為:
Section Header Table
:主要是供編譯時(shí)鏈接使用的,表格中定義了各個(gè)段的位置、長(zhǎng)度、屬性等信息。Program Header Table
:主要是供內(nèi)核和動(dòng)態(tài)加載器從磁盤加載ELF文件到內(nèi)存時(shí)使用的。
對(duì)于目標(biāo)文件,由于其只是編譯過(guò)程中的一個(gè)中間產(chǎn)物,不涉及裝載運(yùn)行,因此在目標(biāo)文件中不會(huì)創(chuàng)建Program Header Table
。
如何列出目標(biāo)文件:
gcc -c hello.c fool.c fool2.c
readelf -h fool2.o
生成的目標(biāo)文件:
ELF Header:Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00Class: ELF64Data: 2's complement, little endianVersion: 1 (current)OS/ABI: UNIX - System VABI Version: 0Type: REL (Relocatable file)Machine: Advanced Micro Devices X86-64Version: 0x1Entry point address: 0x0Start of program headers: 0 (bytes into file)Start of section headers: 672 (bytes into file)Flags: 0x0Size of this header: 64 (bytes) Size of program headers: 0 (bytes)Number of program headers: 0Size of section headers: 64 (bytes)Number of section headers: 13Section header string table index: 12
- 可以看到ELF占了64字節(jié),通過(guò)ELF頭可見該文件是64位的ELF文件。
- 使用
little endian
字節(jié)序存儲(chǔ)字節(jié)。 - ABI遵循UNIX - System V標(biāo)準(zhǔn),運(yùn)行在類UNIX系統(tǒng)上。
- 該文件為REL類型文件:通??蓤?zhí)行文件的類型是
EXEC
;靜態(tài)庫(kù)和目標(biāo)文件的類型是REL
;動(dòng)態(tài)共享庫(kù)的類型是DYN
。 Entry point address
為程序入口,由于是目標(biāo)文件,則不存在執(zhí)行的概念。Start of section headers
:在偏移264字節(jié)處。Size of section headers
:每個(gè)Section Header
占用了40字節(jié),Section Header Table
一共包含了12個(gè)Section Header
。
看完頭信息后,就可以看到各個(gè)段的信息。ELF即各個(gè)段的組合。大體上,段可以分為如下幾種類型:一類是存儲(chǔ)指令的,通常稱為代碼段;第二類是存儲(chǔ)數(shù)據(jù)的,通常稱為數(shù)據(jù)段。數(shù)據(jù)段又細(xì)分為兩個(gè)段:
.bss
:未初始化的全局?jǐn)?shù)據(jù)。.data
:已初始化的全局?jǐn)?shù)據(jù)。
這兩個(gè)段本質(zhì)并沒有什么不同,但是因?yàn)槲闯跏蓟淖兞坎话菙?shù)據(jù),所以在ELF文件中并不需要占用空間,在程序裝載時(shí)進(jìn)行分配即可。
使用命令:
readelf -S fool2.o
可以看到fool2.o
中Section Header Table
中包含的12個(gè)Section Header
:
Section Headers:[Nr] Name Type Address OffsetSize EntSize Flags Link Info Align[ 0] NULL 0000000000000000 000000000000000000000000 0000000000000000 0 0 0[ 1] .text PROGBITS 0000000000000000 000000400000000000000017 0000000000000000 AX 0 0 1[ 2] .rela.text RELA 0000000000000000 000002000000000000000018 0000000000000018 I 10 1 8[ 3] .data PROGBITS 0000000000000000 000000580000000000000004 0000000000000000 WA 0 0 4[ 4] .bss NOBITS 0000000000000000 0000005c0000000000000000 0000000000000000 WA 0 0 1[ 5] .comment PROGBITS 0000000000000000 0000005c000000000000002c 0000000000000001 MS 0 0 1[ 6] .note.GNU-stack PROGBITS 0000000000000000 000000880000000000000000 0000000000000000 0 0 1[ 7] .note.gnu.propert NOTE 0000000000000000 000000880000000000000020 0000000000000000 A 0 0 8[ 8] .eh_frame PROGBITS 0000000000000000 000000a80000000000000038 0000000000000000 A 0 0 8[ 9] .rela.eh_frame RELA 0000000000000000 000002180000000000000018 0000000000000018 I 10 8 8[10] .symtab SYMTAB 0000000000000000 000000e00000000000000108 0000000000000018 11 9 8[11] .strtab STRTAB 0000000000000000 000001e80000000000000018 0000000000000000 0 0 1[12] .shstrtab STRTAB 0000000000000000 00000230000000000000006c 0000000000000000 0 0 1
-
.text
段存儲(chǔ)在文件中偏移0x40處,占據(jù)了0x17個(gè)字節(jié)。.text
段并不全是代碼段,在鏈接時(shí),.init
、.fini
等存儲(chǔ)的代碼都屬于代碼段,這些都被映射到了Program Header Table
中的一個(gè)段,在ELF加載時(shí),統(tǒng)一作為進(jìn)程的代碼段。 -
.data
段存儲(chǔ)在文件中偏移0x58字節(jié)處,占據(jù)了0x04個(gè)字節(jié)的空間。 -
.bss
段雖然包含著,但是他不必要記錄數(shù)據(jù),所以并沒有對(duì)應(yīng)的段。在加載程序時(shí),加載器將依據(jù).bss
段的Section Header
中的信息,在內(nèi)存中為其分配空間。所以占用著0x00字節(jié)空間。 -
.symtab
段記錄的是符號(hào)表。因?yàn)榉?hào)的名字字串長(zhǎng)度可變,所以目標(biāo)文件將符號(hào)的名字字符串剝離出來(lái),記錄在另一個(gè)段.strtab
中,符號(hào)表使用符號(hào)名字的索引在段.strtab
中的偏移來(lái)確定符號(hào)名字。 -
.strtab
段則是用于記錄段的名字 -
rel
開頭的文件,如rel.text
、rel.eh_frame
,記錄的是段中需要重定位的符號(hào)。 -
.eh_frame
段中記錄的是調(diào)試和異常處理時(shí)用到的信息。 -
.comment
、.note.GNU-stack
等都是在鏈接或者時(shí)裝載都不會(huì)用到的數(shù)據(jù),不需要關(guān)心。
那么綜上,ELF文件所有的內(nèi)容可以用如下的表所示:
翻譯機(jī)器指令
機(jī)器指令由操作碼和操作數(shù)組成,操作碼指明該指令要完成的操作,即指令的功能;操作數(shù)是參與操作的參數(shù),主要以寄存器或存儲(chǔ)器地址的形式指明數(shù)據(jù)的來(lái)源或者計(jì)算存放的位置等。
匯編過(guò)程就是將操作碼翻譯為對(duì)應(yīng)的0和1的機(jī)器指令,這也是操作碼和操作數(shù)的編碼過(guò)程。這個(gè)過(guò)程也比較簡(jiǎn)單,對(duì)應(yīng)關(guān)系可以查看對(duì)應(yīng)的CPU指令手冊(cè)。但是對(duì)于操作數(shù)翻譯為機(jī)器碼復(fù)雜一些,操作數(shù)并沒有直接嵌入在指令編碼中,而是根據(jù)匯編指令使用的具體尋址方式,設(shè)置ModR/M
、SIB
、Displacement
和Immediate
各項(xiàng)的值,這個(gè)過(guò)程稱為操作數(shù)的解碼,CPU根據(jù)ModR/M
、SIB
、Displacement
和Immediate
各項(xiàng)的值解碼出操作數(shù)。
IA32機(jī)器指令的格式:
下面是操作數(shù)的編碼方式:
操作數(shù)地址通過(guò)ModR/M中的Mod+R/M指定
ModR/M
占用1字節(jié),包含三個(gè)域:Mod
、Reg/Opcode
和R/M
,其中Mod
占2位,R/M
占3位,Reg/Opcode
占3位。操作數(shù)可以使用ModR/M
中的Mod
和R/M
字段聯(lián)合起來(lái)定義。
其中第二列表示尋址方式生成的有效地址;第三列和第四列表示對(duì)應(yīng)于某個(gè)尋址方式,Mod
和R/M
分別表示對(duì)應(yīng)的編碼。
在上面的表中,包含了直接尋址、寄存器尋址、寄存器間接尋址、基址尋址以及基址變址尋址等尋址方式下Mod
和R/M
對(duì)應(yīng)的編碼。如果匯編指令使用的是基址變址尋址,那么機(jī)器指令中也需要字段SIB
。
以第七行的指令為例,假設(shè)匯編指令使用的尋址方式是[EAX]+disp8
,那么Mod
應(yīng)該取值01,R/M
應(yīng)該取值位000。偏移disp8表示八位的Displacement
,根據(jù)機(jī)器指令的格式,Displacement
直接嵌入在指令中即可。Displacement
取值可以為8位、16位、32位,選擇取決于尺寸方面,Displacement
需要使用補(bǔ)碼的形式。當(dāng)CPU執(zhí)行指令時(shí),當(dāng)解析到ModR/M
這個(gè)字節(jié)時(shí),一旦發(fā)現(xiàn)Mod
的值是01,R/M
的值是000,那么CPU就到寄存器EAX
中取到其中的內(nèi)容,然后再取出嵌入在指令中的8位偏移Displacement
,將這兩個(gè)值相加作為操作數(shù)的內(nèi)存地址,從而完成操作數(shù)的解碼過(guò)程。
操作數(shù)通過(guò)ModR/M中的Reg/Opcode指定
ModR/M
中的字段Reg/Opcode
占據(jù)3位,如果在匯編指令中使用了寄存器作為操作數(shù),那么編碼時(shí)也可以使用Reg/Opcode
指定操作數(shù)使用的寄存器。如果操作數(shù)不需要使用字段Reg/Opcode
編碼,字段Reg/Opcode
也可以作為操作碼的編碼,下面是32位寄存器與字段Reg/Opcode
取值的對(duì)應(yīng)關(guān)系:
操作數(shù)地址直接嵌入在機(jī)器指令中
這就是所謂的直接尋址方式,那么在翻譯為機(jī)器指令時(shí),直接使用機(jī)器指令中的Displacement
字段表示操作數(shù)的地址。
操作數(shù)直接嵌入在指令中
如果在匯編指令中,操作數(shù)就是參與計(jì)算的數(shù)據(jù),即所謂的立即尋址,那么在翻譯為機(jī)器指令時(shí),直接使用機(jī)器指令中的Immediate
表示操作數(shù)。
操作數(shù)隱含在Opcode中
這就是所謂的隱含尋址。其實(shí)就是通過(guò)一些其他子指令來(lái)區(qū)分功能相同,但是操作數(shù)類型不同的作用:
mov r/m16,r16
mov r/m32,r32
Intel并沒有為上述兩個(gè)分類操作分別定義兩個(gè)操作碼,而是使用了同一個(gè)操作碼。但是使用了Instruction Prefixes
來(lái)區(qū)分指令中的操作數(shù)是16位的還是32為的,比如在32位環(huán)境下使用了16位的操作數(shù),那么需要在指令前使用0x66進(jìn)行標(biāo)識(shí)。
回到代碼
那么回到代碼,fool2中:
movl foo2(%rip), %eaxmovl %eax, -4(%ebp)
這兩條使用的都是mov指令,IA32架構(gòu)的mov指令可以簡(jiǎn)單了解如下:
需要關(guān)注Opcode
以及Op/En
(操作數(shù)的編碼方式)。
對(duì)于MOV指令,不僅僅只有一個(gè)操作碼,對(duì)于同一類操作,可能使用不同的操作數(shù),操作數(shù)可能是寄存器,也可能是內(nèi)存地址,同時(shí)操作數(shù)還會(huì)有長(zhǎng)度之分,比如8位、16位、32位。Intel采取的策略是為同一指令設(shè)計(jì)了多個(gè)操作碼來(lái)細(xì)分這些指令。
對(duì)于Op/En
操作數(shù)的編碼方式,具有六種:
需要注意的是,編譯器生成的匯編代碼使用的是AT&T
的格式,其操作數(shù)的順序與Intel的匯編指令正好相反,所以指令movl foo2(%rip), %eax
中,foo2是Intel語(yǔ)法中的第二個(gè)操作數(shù),%eax是第一個(gè)操作數(shù)。那么可以查表,第七行,mov eax,moffs32
,根據(jù)該指令說(shuō)明,操作碼0xa1隱含地指出了指令中第一個(gè)操作數(shù)是寄存器EAX,也就是尋址方式中所謂地操作數(shù)隱含尋址。
該指令地操作數(shù)編碼方式是C,C類編碼方式不需要ModR/M,也不需要SIB,而且也沒有使用立即數(shù)作為操作數(shù),也不需要指令前綴進(jìn)行修飾,所以第一個(gè)操作數(shù)寄存器EAX是通過(guò)操作碼隱含指明,所以該條匯編代碼最后轉(zhuǎn)換為如下形式地機(jī)器指令:Opcode+Displacement
。
第二個(gè)操作數(shù)是通過(guò)Displayment
來(lái)進(jìn)行表示地,由于還沒有進(jìn)行鏈接,所以foo2
的地址尚未確定,所以暫時(shí)填充0占位,在鏈接時(shí)根據(jù)實(shí)際地址修改。因?yàn)槭沁\(yùn)行在32位的環(huán)境下,所以地址是32位的,Displayment
占用了4字節(jié),綜上所述,該指令的機(jī)器碼可以翻譯為:
movl foo2(%rip), %eax||opcode + displayment||a1 00 00 00 00
下一條指令是movl %eax, -4(%ebp)
,這條指令也是有兩個(gè)操作數(shù),第一個(gè)操作數(shù)-4(%ebp)相當(dāng)于是[EBP]+dis8
,用8位是因?yàn)楸硎?4使用1個(gè)字節(jié)就夠了。根據(jù)A類編碼的要求,第一個(gè)操作數(shù)需要使用的寄存器需要由ModR/M中的Mod和R/M共同指明,根據(jù)尋址模式可匹配表的第十行,mod為01,r/m為101.且第一個(gè)操作數(shù)中的偏移-4由displayment來(lái)表示,在機(jī)器指令中需要使用數(shù)的補(bǔ)碼來(lái)表示,-4補(bǔ)碼為fc。
根據(jù)A類編碼的方式要求,第二個(gè)操作數(shù)由ModR/M
中的Reg/Opcode
指明。匯編指令第二個(gè)操作數(shù)使用的寄存器位EAX,對(duì)照表位000,那么第二條指令:
movl %eax, -4(%ebp)||
Opcode + ModR/M + displayment||
0x89 01 000 101 fc||89 45 fc
可以使用指令objdump -d fool2.o
來(lái)分析機(jī)器碼翻譯過(guò)程:
可以使用工具hexdump -e ' "%4_ax:" 16/1 " %02x" "\n"' fool2.o
原汁原味的進(jìn)行分析,%4_ax
表示使用4位十六進(jìn)制進(jìn)行偏移;16/1
表示每行顯示16字節(jié),逐字解析,%02x
表示以十六進(jìn)制顯示,每個(gè)字符占據(jù)兩位。
可以看到截取到的.text
段以及.data
段:
可以注意到起始于偏移0x40處,于我們ELF文件中看到的描述相同!!占據(jù)的字節(jié)數(shù)也是相同的,指令也是相同的!!
對(duì)于數(shù)據(jù),0x58開始的數(shù)據(jù)段,正好是0x14對(duì)應(yīng)的十進(jìn)制數(shù)20,信息也可以對(duì)的上。
重定位表
在進(jìn)行匯編時(shí),在一個(gè)模塊內(nèi),如果引用了其他模塊或者時(shí)庫(kù)中的變量或者函數(shù),匯編器并不會(huì)解析引用的外部符號(hào)。匯編器基本上是留空引用的外部符號(hào)的地址;然后在鏈接時(shí),在符號(hào)地址確定后,鏈接器再來(lái)修訂這些位置,這個(gè)修訂的過(guò)程,被稱之為重定位(編譯時(shí)、加載/運(yùn)行時(shí),這里說(shuō)的是前者)。
這些需要修訂的位置并不是全都置為0,有時(shí)候這里填充的是一個(gè)Addend
,這就是之所以使用引號(hào)將空引用起來(lái)的原因。
但是鏈接器并不能自動(dòng)找到目標(biāo)文件中引用外部符號(hào)的地方,所以在目標(biāo)文件中需要建立一個(gè)表格,這個(gè)表格中的每一條記錄對(duì)應(yīng)的就是一共需要重定位的符號(hào),這個(gè)表格通常稱為重定位表,匯編器將為可重定位文件中每個(gè)包含需要重定位符號(hào)的段都建立一個(gè)重定位表。
ELF標(biāo)準(zhǔn)規(guī)定,重定位表中的表項(xiàng)可以使用如下兩種格式:
唯一不同的成員即r_addend
,這個(gè)成員一般是個(gè)常量,用來(lái)輔助計(jì)算修訂值;若使用了第一種格式,那么r_addend
將被填充在引用外部符號(hào)的地址處,也就是留空處。
r_offset
為需要重定位的符號(hào)在目標(biāo)文件中的偏移;對(duì)于目標(biāo)文件,r_offset
是相對(duì)于段的,是段內(nèi)偏移;對(duì)于執(zhí)行文件或者動(dòng)態(tài)庫(kù),r_offset
是虛擬地址。r_info
中包含重定位類型和此處引用的外部符號(hào)在符號(hào)表中的索引。根據(jù)符號(hào)在符號(hào)表中的索引,鏈接器就可以從符號(hào)表中解析出符號(hào)的地址。
可以使用命令readelf -r hello.o
查看文件的重定位表。
可以看到段.text
以及.eh_frame
段中都有符號(hào)需要重定位,所以建立了兩重定位表。在.text
段的重定位表中,引用了兩個(gè)外部符號(hào),并且可以在第一列得到他們的偏移為0x15以及0x23。
根據(jù)objdump的輸出可見,在偏移0x15處,則是變量foo2的地址,匯編器填充的addend是0;在偏移0x23處,foo2_func填充addend的也是0。
符號(hào)表
在鏈接時(shí)需要重定位目標(biāo)文件中引用的外部符號(hào),顯然鏈接器也需要指定這些符號(hào)的定義是在哪里,所以匯編器在每個(gè)目標(biāo)文件中創(chuàng)建了一個(gè)符號(hào)表,符號(hào)表中記錄了這個(gè)模塊定義的可以提供給其他模塊引用的全局符號(hào)。
查看符號(hào)表readelf -s fool2.o
:
根據(jù)輸出可見,fool2.o
符號(hào)表包含了10個(gè)符號(hào)。
- value列表示的時(shí)符號(hào)的地址,由于鏈接時(shí)鏈接器才會(huì)分配地址,所以現(xiàn)在看到的符號(hào)地址全都是0。
- Size列代表的時(shí)申請(qǐng)內(nèi)存的大小,可以看到變量
foo2
占據(jù)了4個(gè)字節(jié),foo2_func
占據(jù)了23個(gè)字節(jié)。 - Type列表示符號(hào)的類型。如
foo2
類型為OBJECT
表示的是變量;FUNC
表示的是函數(shù)。 - Bind列表示符號(hào)綁定的相關(guān)信息,
LOCAL
表示模塊內(nèi)部符號(hào),對(duì)外不可見;GLOBAL
表示全局符號(hào),屬于全局變量。 - Ndx列表示該符號(hào)在哪個(gè)段,3為
.data
段,1為.text
段。
那么對(duì)于引用外部符號(hào)的符號(hào)表,可以看看hello.o:
由于符號(hào)foo2以及foo2_func都在模塊foo2中定義,對(duì)于模塊hello來(lái)說(shuō)是外部符號(hào),沒有在任何一個(gè)段中,所以在列Ndx中,他們的值都是UND
。UND
是Undefined
的縮寫,表示其是未定義的。
在鏈接時(shí),對(duì)于模塊中引用的外部符號(hào),鏈接器將根據(jù)符號(hào)表進(jìn)行符號(hào)的重定位。如果將符號(hào)表刪除,那么鏈接器在鏈接時(shí)將找不到符號(hào)的定義,從而不能進(jìn)行正確的符號(hào)解析??梢钥吹较旅娴牟僮?#xff1a;
0x05 鏈接
鏈接時(shí)編譯過(guò)程的最后一個(gè)階段,鏈接將一個(gè)或者多個(gè)目標(biāo)文件和庫(kù),包括動(dòng)態(tài)庫(kù)和靜態(tài)庫(kù),鏈接為一個(gè)單獨(dú)的文件(通常為可執(zhí)行文件、動(dòng)態(tài)庫(kù)或者靜態(tài)庫(kù))。
鏈接器的工作可以分為兩個(gè)階段:
- 第一階段是將多個(gè)文件合并為一個(gè)單獨(dú)的文件。對(duì)于可執(zhí)行文件,還需要為指令以及符號(hào)分配運(yùn)行時(shí)的地址。
- 第二階段進(jìn)行符號(hào)重定位。
合并目標(biāo)文件
合并多個(gè)目標(biāo)文件其實(shí)就是將多個(gè)目標(biāo)文件的相同類型的段合并到一個(gè)段中:
可以試著查看所有文件的目標(biāo)文件以及鏈接后可執(zhí)行文件的.text
段:
hello.o:
fool1.o:
foo2.o:
hello:
根據(jù)上面輸出的結(jié)果可見,對(duì)于目標(biāo)文件,并沒有為目標(biāo)文件的機(jī)器指令及符號(hào)分配運(yùn)行時(shí)的地址,而對(duì)于可執(zhí)行文件hello,鏈接器已經(jīng)為其機(jī)器指令及符號(hào)分配了運(yùn)行時(shí)地址,并且申請(qǐng)了對(duì)應(yīng)的內(nèi)存空間。
理論上,三個(gè)目標(biāo)文件的.text
段加起來(lái)應(yīng)該與可執(zhí)行文件的hello的.text
段的尺寸大小是相等的。三個(gè)可執(zhí)行文件加起來(lái)的大小是0x70,但是遠(yuǎn)小于可執(zhí)行文件0x1a5。
可以注意到在編譯時(shí)會(huì)向gcc傳遞了參數(shù)-v,細(xì)心可以發(fā)現(xiàn),實(shí)際上鏈接時(shí)鏈接器自作主張地鏈接了一些特別的文件,包括crtl.o\crti.0\crtn.o\crtbegin.o\ctrend.o
,其實(shí)就是我們前面提到的啟動(dòng)文件。所以會(huì)增加了.text
段的大小。
也可以手動(dòng)調(diào)用ld
,不鏈接這些啟動(dòng)文件,再來(lái)對(duì)比一下.text
段的尺寸。在默認(rèn)情況下,鏈接器將使用函數(shù)_start
作為可執(zhí)行文件的入口,但是這個(gè)函數(shù)的實(shí)現(xiàn)在啟動(dòng)文件ctrl.o
中,因此,在這里我們通過(guò)給鏈接器ld傳遞參數(shù)-e main
,明確告訴鏈接器不適用默認(rèn)的啟動(dòng)函數(shù)_start
了,否則鏈接器會(huì)找不到符號(hào)_start
,直接使用函數(shù)main
作為可執(zhí)行文件的入口。當(dāng)然main
函數(shù)中并沒有實(shí)現(xiàn)啟動(dòng)代碼的功能,在這里這是為了方便查看.text
段,尺寸是所有目標(biāo)文件size的總和。如果不是等于總和,差別有幾個(gè)字節(jié)的話,是由內(nèi)存對(duì)齊所引起的。
符號(hào)重定位
上面為鏈接的第一階段,目標(biāo)文件已經(jīng)合并完成了,并且已經(jīng)為符號(hào)分配了運(yùn)行時(shí)的地址,鏈接器將符號(hào)進(jìn)行重定位。
可以看到匯編器已經(jīng)將這兩處需要重定位的符號(hào)記錄在了重定位表中。
R_386_32
,ELF標(biāo)準(zhǔn)規(guī)定的計(jì)算修訂值得公式是:S+A
;其中,S表示符號(hào)的運(yùn)行地址,A就是匯編器填充在引用外部符號(hào)處的Addend
。R_386_PC32
,ELF標(biāo)準(zhǔn)規(guī)定的計(jì)算修訂值的公式是:S+A-P
;其中,S,A與前面的意義完全相同,P為修訂處的運(yùn)行地址或者偏移。對(duì)于可執(zhí)行文件和動(dòng)態(tài)庫(kù),P為修訂處的運(yùn)行時(shí)地址。
首先確定S,運(yùn)行時(shí)地址在鏈接時(shí)才分配:
可以看到foo2
、foo2_func
的運(yùn)行時(shí)的地址。
之后再捋捋匯編器為這兩個(gè)符號(hào)填充的Addend
是多少,可以使用objdump
反匯編hello.o
,也可以看到上面圖中的-8以及-4。
需要注意的是,對(duì)于函數(shù)占據(jù)的運(yùn)行時(shí)地址小于main函數(shù),那么這里的函數(shù)地址與PC相對(duì)地址將是負(fù)數(shù),其實(shí)就是將PC跳回去執(zhí)行。在機(jī)器指令中,使用的是數(shù)的補(bǔ)碼形式。
對(duì)于R_386_32這種重定位類型,是絕對(duì)地址重定位,鏈接器只要解析符號(hào)運(yùn)行時(shí)地址替換修訂處即可。而對(duì)于R_386_PC32,這是一個(gè)PC相對(duì)地址重定位,當(dāng) 執(zhí)行當(dāng)前指令時(shí),PC中已經(jīng)加載了下一條指令的地址,并不是當(dāng)前指令的地址。
在鏈接時(shí),鏈接器在需要重定位的符號(hào)所在的偏移處直接進(jìn)行了編輯修訂,所以鏈接器也被形象地稱為link editor
。
鏈接靜態(tài)庫(kù)
靜態(tài)庫(kù)其實(shí)就是多個(gè)目標(biāo)文件的打包,因此與合并多個(gè)目標(biāo)文件并沒有什么區(qū)別。但是在鏈接靜態(tài)庫(kù)時(shí),并不是將整個(gè)靜態(tài)庫(kù)中包含的目標(biāo)文件全部復(fù)制一份到最終的可執(zhí)行文件中,而是僅僅鏈接庫(kù)中使用的目標(biāo)文件。
可以將兩個(gè)源文件編譯為靜態(tài)庫(kù)libfoo.a
,然后將其鏈接到hello
:
可以看到靜態(tài)庫(kù)的符號(hào)表:
可以看到就是兩個(gè)目標(biāo)文件的合體,但是在hello中可不是什么都有:
鏈接動(dòng)態(tài)庫(kù)
與靜態(tài)庫(kù)不同,動(dòng)態(tài)庫(kù)不會(huì)在可執(zhí)行文件中有任何副本,那么為什么編譯鏈接依然需要指定動(dòng)態(tài)庫(kù)?
- 動(dòng)態(tài)加載器需要知道可執(zhí)行程序依賴的動(dòng)態(tài)庫(kù),這樣在加載可執(zhí)行程序時(shí)才能加載其依賴的動(dòng)態(tài)庫(kù)。
在鏈接時(shí)會(huì)根據(jù)可執(zhí)行程序引用的動(dòng)態(tài)庫(kù)中的符號(hào)的情況,在dynamic
段中記錄可執(zhí)行程序依賴的動(dòng)態(tài)庫(kù)。
gcc -c -fPIC fool1.c fool2.c #產(chǎn)生與地址無(wú)關(guān)的目標(biāo)文件
gcc -shared -o libfoo.so fool1.o fool2.o
gcc hello.c -o hello -L./ -lfoo
readelf -d hello | grep Shared
- 鏈接器需要在重定位表中創(chuàng)建重定位記錄,這樣當(dāng)動(dòng)態(tài)鏈接器加載hello時(shí),將依據(jù)重定位記錄重定位hello引用的這些外部符號(hào)。
重定位記錄存儲(chǔ)在ELF文件的重定位段中,ELF文件中可能有多個(gè)段包含需要重定位的符號(hào),所以可能會(huì)包含多個(gè)重定位段。
rel.dyn
段中記錄的是加載時(shí)需要重定位的變量。
rel.plt
段中記錄的是需要重定位的函數(shù)。
雖然編譯時(shí)不需要鏈接共享庫(kù),但是可執(zhí)行文件中需要記錄其依賴的共享庫(kù)以及加載/運(yùn)行時(shí)需要重定位的條目,在加載程序時(shí),動(dòng)態(tài)加載器需要這些信息來(lái)完成加載時(shí)的重定位。