生成可執(zhí)行文件的過程:
//用linux gcc編譯器飒炎,編譯main.c亏镰,sum.c
①預處理(preprocess):生成main.i
cpp main.c -o main.i
②編譯(compile):生成main.s
gcc main.i -o main.s
//CSAPP上說的是cc1横侦,但linux找不到這個见擦,只有cc抖甘,但是會報錯曙蒸。
③匯編(assembly):生成main.o
as mian.s -o main.o
④鏈接(linked):生成可執(zhí)行文件
ld -o prog main.o sum.o
目標文件
-
三種形式:
可重定位目標文件(file.o):包含二進制的代碼和數(shù)據(jù)憨降,可在鏈接時和其他目標文件合并起來父虑,生成可執(zhí)行目標文件。
共享目標文件:特殊類型的可重定向目標文件授药,可以再加載士嚎、運行時被動態(tài)加載進內(nèi)存并鏈接。
可執(zhí)行目標文件:包含二進制的代碼和數(shù)據(jù)悔叽,其可被直接復制到內(nèi)存并執(zhí)行莱衩。
可重定位目標文件
-
不同系統(tǒng)格式:
windows:可移植可執(zhí)行格式(Portable Executable),PE格式娇澎。
Max OS-X:Mach-O格式笨蚁。
Linux和Unix:可執(zhí)行可鏈接模式(Executable and Linkable Format),ELF格式趟庄。
-
linux的可重定位目標文件:
ELF格式為一個序列:{ELF括细,... , 節(jié)頭部表}
ELF和節(jié)頭部表中間,包含了很多個節(jié)戚啥, 如:
.text .rodate .data等等奋单,如下圖:
ELF格式
-
符號和符號表
//linux中,用readelf name -s查看符號表猫十。
每個目標文件(模塊)中都有一個符號表symtab览濒,其包含了本文件中定義、引用的符號的信息拖云。
三種不同符號:
1:模塊自己定義贷笛,并能被其他模塊引用的全局符號(函數(shù)或者變量,非靜態(tài))宙项。
2:由其他模塊定義乏苦,被本模塊引用的全局符號。
3:模塊自己定義的靜態(tài)全局符號尤筐,僅被自己使用汇荐。
單個符號的表,如下:
某個符號表
可以在linux中叔磷,用objdump -dx file.o 來得到符號表的反匯編代碼:
符號表反匯編
//ndx代表節(jié)索引
全局符號根據(jù)初始化與否拢驾,被分到不同的節(jié):
1:分配到 .data:全局變量初始化,且不為0改基。
2:分配到 .bss :全局變量初始化為0繁疤;未初始化的靜態(tài)變量符號 //(Block Storage Start)
3:分配到 .COMMON :沒有初始化的全局變量咖为。
4:分配到 .UND:沒有定義的全局函數(shù)。
//UND稠腊、COMMON是偽節(jié)躁染,僅在重定位文件中,執(zhí)行文件無架忌。
-
鏈接器解析多重定義的全局符號
解析:將全局符號的引用和定義關聯(lián)起來吞彤。
強符號:已初始化的全局符號。
弱符號:未初始化的全局符號叹放。
重定義 有三種情況:
1:兩個文件中饰恕,都有同樣的強符號出現(xiàn),會報錯井仰。
2:兩個文件有同名符號埋嵌,但只有一個強符號,則選擇強符號俱恶。
3:都是弱符號雹嗦,則隨機選擇一個。
如圖:
與靜態(tài)庫的鏈接
-
靜態(tài)庫定義:
所有的編譯系統(tǒng)都提供一種機制合是,將所有相關的目標模塊打包成一個單獨的文件了罪,稱之為靜態(tài)庫。格式為 lib.a
如圖:
靜態(tài)庫鏈接過程//(.a表示archive 存檔聪全,里面包含了很多個.o模塊泊藕,當某模塊引用了庫中的模塊名時,變會將其復制鏈接到可執(zhí)行文件)
//通常編譯器會自動隱式鏈接libc.a 標準函數(shù)庫荔烧,而自定義的一些庫吱七,需要顯式鏈接汽久。創(chuàng)建靜態(tài)庫用AR工具鹤竭。
不使用靜態(tài)庫的實現(xiàn)方法及自定義庫:CSAPP P475。
鏈接器使用靜態(tài)庫來解析引用
-
編譯驅(qū)動器按照命令行從左到右景醇,進行順序掃描臀稚。
鏈接器維護了三個集合:E(將被合并成可執(zhí)行文件)、U(還未被解析的引用符號)三痰、D(已經(jīng)解析的有定義的符號集合)
1:如果為file.o吧寺,則將其放入E,并修改U散劫,D稚机。
2:如果為libx.a,則將其與U中符號進行匹配获搏,如果匹配赖条,則將該模塊.o 放入E中。并修改U、D(U中清空已解析引用纬乍,并添加模塊.o中未解析引用)碱茁,存檔文件掃描完后,將無引用的模塊丟棄仿贬。
3:鏈接器完成對命令行的遍歷后纽竣,若U非空,則進行報錯并終止茧泪。否則蜓氨,會合并E中目標文件,生成可執(zhí)行文件队伟。
//所以切記语盈,不要將.o和.a順序弄錯,否則會無法解析引用缰泡,鏈接失敗刀荒。
重定位
-
解析引用之后,便是要重定位:
1:將不同模塊的相同節(jié)進行拼接:
如.text棘钞,形成一個新的.text節(jié):其包含了所有模塊的指令代碼缠借。數(shù)據(jù)代碼、符號表等待也一樣宜猜。2:對引用的符號進行重定位:
對全局變量泼返、全局函數(shù)等進行重定位,使其調(diào)用時姨拥,指向可執(zhí)行代碼中的定義位置绅喉。在可重定位目標程序中,如果有未定義的引用叫乌,則會將其放在.rel.text柴罐、.rel.data中(rel=relocation),使編譯器知道在生成可執(zhí)行代碼時憨奸,對這些符號進行重定位革屠。
-
每個重定位條目,類似結(jié)構(gòu)體排宰,包含:
offset :在代碼中的偏移信息似芝。如main函數(shù)內(nèi)部調(diào)用一個全局函數(shù)f()時,則offset = f()相對于main首地址的偏移量板甘。
type:重定位之后的類型党瓮。
symbol:模塊拼接之后,f()在符號表中的偏移量盐类。
addend:這個值是針對不同情況寞奸,如f()的實現(xiàn)在mian()之后時痕寓,因為到call時,PC指令已經(jīng)指到下一個代碼位置蝇闭,需要減去f()偏移量的大小呻率,64位=8,32位=4呻引;
如下圖:
重定位entry
可執(zhí)行目標文件
當鏈接器將所有模塊解析及重定位之后礼仗,會生成一個可執(zhí)行文件的ELF表,其與可重定位目標文件表類似逻悠,但是沒有了兩個.rel節(jié)元践,因為不需要重定位了。
增加的節(jié):
init:定義了一個_init函數(shù)童谒,程序的初始化代碼會調(diào)用它单旁。
段頭部表:描述了可執(zhí)行文件中,代碼段和數(shù)據(jù)段對內(nèi)存的映射關系饥伊。
加載可執(zhí)行目標文件
在linux中使用 ./prog 可以運行目標文件象浑。
-
加載流程://無動態(tài)鏈接
1:將代碼復制到內(nèi)存:
操作系統(tǒng)常駐在內(nèi)存的加載器(loader),會將可執(zhí)行目標文件中的代碼和數(shù)據(jù)從磁盤復制到內(nèi)存琅豆。2:將控制交給prog:
通過跳轉(zhuǎn)到程序的第一條指令或者入口點(entry point)來運行程序愉豺。
其在內(nèi)存中,圖示如下:
動態(tài)鏈接共享庫
靜態(tài)庫的缺點是太耗磁盤和內(nèi)存空間蚪拦,尤其是當同時運行了上百個程序的時候,幾乎每個程序中都有標準I/O函數(shù)及其他重復函數(shù)冻押,占用了很大的內(nèi)存的資源驰贷。
-
共享庫
(shared library)用.so后綴表示。是第三種目標文件洛巢,即共享目標文件括袒。在運行或加載時,可以加載到任意的內(nèi)存地址狼渊,并和一個在內(nèi)存中的程序鏈接起來箱熬,這個過程稱為動態(tài)鏈接(dynamic linking)类垦。動態(tài)連接器:dynamic linker狈邑。執(zhí)行動態(tài)鏈接過程。
"共享"說明:
1:磁盤中蚤认,對于一個動態(tài)庫只有一個.so文件米苹,所有引用該文件的可執(zhí)行目標文件共享一個庫,而非靜態(tài)庫砰琢,需要將其內(nèi)容復制鏈接到可執(zhí)行文件中蘸嘶。
2:在內(nèi)存中良瞧,共享庫的.text節(jié)的副本,可以被不同的正在運行的進程所共享训唱。大大節(jié)約了內(nèi)存空間褥蚯。
動態(tài)鏈接過程
動態(tài)鏈接時:
1:和靜態(tài)庫不同,生成prog21時况增,并沒有復制代碼和數(shù)據(jù)赞庶。反而是鏈接器,復制了一些重定位和符號表信息澳骤。
2:在內(nèi)存中時歧强,鏈接器根據(jù)這些信息,復制動態(tài)庫中的文本和數(shù)據(jù)到另一個內(nèi)存段为肮,并對prog21中摊册,有引用動態(tài)庫里定義的符號進行重定位。
3:動態(tài)鏈接器將控制傳遞給prog21颊艳,開始運行茅特。共享庫的位置固定不變了。
從應用程序中加載和鏈接共享庫棋枕、位置無關代碼PIC(Position Independent Code)温治、庫打樁機制沒有學,因為書里比較簡略戒悠。