摘要:鏈接器會(huì)掃描所有輸入的目標(biāo)文件,然后把所有符號(hào)表里的信息收集起來,構(gòu)成一個(gè)全局的符號(hào)表。這是一本難得的講解程序的鏈接裝載和運(yùn)行的好書。
既然程序最終都被變成了一條條機(jī)器碼去執(zhí)行,那為什么同一個(gè)程序,在同一臺(tái)計(jì)算機(jī)上,在Linux下可以運(yùn)行,而在Windows下卻不行呢?
反過來,Windows上的程序在Linux上也是一樣不能執(zhí)行的
可是我們的CPU并沒有換掉,它應(yīng)該可以識(shí)別同樣的指令呀!!!
如果你和我有同樣的疑問,那這一節(jié),我們就一起來解開。
1 編譯、鏈接和裝載:拆解程序執(zhí)行寫好的C語(yǔ)言代碼,可以通過編譯器編譯成匯編代碼,然后匯編代碼再通過匯編器變成CPU可以理解的機(jī)器碼,于是CPU就可以執(zhí)行這些機(jī)器碼了
你現(xiàn)在對(duì)這個(gè)過程應(yīng)該不陌生了,但是這個(gè)描述把過程大大簡(jiǎn)化了
下面,我們一起具體來看,C語(yǔ)言程序是如何變成一個(gè)可執(zhí)行程序的。
過去幾節(jié),我們通過gcc生成的文件和objdump獲取到的匯編指令都有些小小的問題
我們先把前面的add函數(shù)示例,拆分成兩個(gè)文件
add_lib.c
link_example.c
通過gcc來編譯這兩個(gè)文件,然后通過objdump命令看看它們的匯編代碼。
objdump -d -M intel -S link_example.o
既然代碼已經(jīng)被我們“編譯”成了指令
不妨嘗試運(yùn)行一下 ./link_example.o
不幸的是,文件沒有執(zhí)行權(quán)限,我們遇到一個(gè)Permission denied錯(cuò)誤
即使通過chmod命令賦予link_example.o文件可執(zhí)行的權(quán)限,運(yùn)行 ./link_example.o 仍然只會(huì)得到一條cannot execute binary file: Exec format error的錯(cuò)誤。
仔細(xì)看一下objdump出來的兩個(gè)文件的代碼,會(huì)發(fā)現(xiàn)兩個(gè)程序的地址都是從0開始
如果地址一樣,程序如果需要通過call指令調(diào)用函數(shù)的話,怎么知道應(yīng)該跳到哪一個(gè)文件呢?
無(wú)論是這里的運(yùn)行報(bào)錯(cuò),還是objdump出來的匯編代碼里面的重復(fù)地址
都是因?yàn)?add_lib.o 以及 link_example.o 并不是一個(gè)可執(zhí)行文件(Executable Program),而是目標(biāo)文件(Object File)
只有通過鏈接器(Linker) 把多個(gè)目標(biāo)文件以及調(diào)用的各種函數(shù)庫(kù)鏈接起來,我們才能得到一個(gè)可執(zhí)行文件
gcc的-o參數(shù),可以生成對(duì)應(yīng)的可執(zhí)行文件,對(duì)應(yīng)執(zhí)行之后,就可以得到這個(gè)簡(jiǎn)單的加法調(diào)用函數(shù)的結(jié)果。
C語(yǔ)言代碼-匯編代碼-機(jī)器碼 過程,在我們的計(jì)算機(jī)上進(jìn)行的時(shí)候是由兩部分組成:
第一個(gè)部分由編譯(Compile)、匯編(Assemble)以及鏈接(Link)三個(gè)階段組成
三階段后,就生成了一個(gè)可執(zhí)行文件link_example:
file format elf64-x86-64 Disassembly of section .init: ... Disassembly of section .plt: ... Disassembly of section .plt.got: ... Disassembly of section .text: ... 6b0: 55 push rbp 6b1: 48 89 e5 mov rbp,rsp 6b4: 89 7d fc mov DWORD PTR [rbp-0x4],edi 6b7: 89 75 f8 mov DWORD PTR [rbp-0x8],esi 6ba: 8b 55 fc mov edx,DWORD PTR [rbp-0x4] 6bd: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] 6c0: 01 d0 add eax,edx 6c2: 5d pop rbp 6c3: c3 ret 00000000000006c4: 6c4: 55 push rbp 6c5: 48 89 e5 mov rbp,rsp 6c8: 48 83 ec 10 sub rsp,0x10 6cc: c7 45 fc 0a 00 00 00 mov DWORD PTR [rbp-0x4],0xa 6d3: c7 45 f8 05 00 00 00 mov DWORD PTR [rbp-0x8],0x5 6da: 8b 55 f8 mov edx,DWORD PTR [rbp-0x8] 6dd: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 6e0: 89 d6 mov esi,edx 6e2: 89 c7 mov edi,eax 6e4: b8 00 00 00 00 mov eax,0x0 6e9: e8 c2 ff ff ff call 6b0 6ee: 89 45 f4 mov DWORD PTR [rbp-0xc],eax 6f1: 8b 45 f4 mov eax,DWORD PTR [rbp-0xc] 6f4: 89 c6 mov esi,eax 6f6: 48 8d 3d 97 00 00 00 lea rdi,[rip+0x97] # 794 <\_IO\_stdin\_used+0x4> 6fd: b8 00 00 00 00 mov eax,0x0 702: e8 59 fe ff ff call 560 707: b8 00 00 00 00 mov eax,0x0 70c: c9 leave 70d: c3 ret 70e: 66 90 xchg ax,ax ... Disassembly of section .fini:
...你會(huì)發(fā)現(xiàn),可執(zhí)行代碼dump出來內(nèi)容,和之前的目標(biāo)代碼長(zhǎng)得差不多,但是長(zhǎng)了很多
因?yàn)樵贚inux下,可執(zhí)行文件和目標(biāo)文件所使用的都是一種叫ELF(Execuatable and Linkable File Format)的文件格式,中文名字叫可執(zhí)行與可鏈接文件格式
這里面不僅存放了編譯成的匯編指令,還保留了很多別的數(shù)據(jù)。
第二部分,我們通過裝載器(Loader)把可執(zhí)行文件裝載(Load)到內(nèi)存中
CPU從內(nèi)存中讀取指令和數(shù)據(jù),來開始真正執(zhí)行程序
2 ELF格式和鏈接:理解鏈接過程程序最終是通過裝載器變成指令和數(shù)據(jù)的,所以其實(shí)生成的可執(zhí)行代碼也并不僅僅是一條條的指令
我們還是通過objdump指令,把可執(zhí)行文件的內(nèi)容拿出來看看。
比如我們過去所有objdump出來的代碼里,你都可以看到對(duì)應(yīng)的函數(shù)名稱,像add、main等等,乃至你自己定義的全局可以訪問的變量名稱,都存放在這個(gè)ELF格式文件里
這些名字和它們對(duì)應(yīng)的地址,在ELF文件里面,存儲(chǔ)在一個(gè)叫作符號(hào)表(Symbols Table)的位置里。符號(hào)表相當(dāng)于一個(gè)地址簿,把名字和地址關(guān)聯(lián)了起來。
我們先只關(guān)注和我們的add以及main函數(shù)相關(guān)的部分
你會(huì)發(fā)現(xiàn),這里面,main函數(shù)里調(diào)用add的跳轉(zhuǎn)地址,不再是下一條指令的地址了,而是add函數(shù)的入口地址了,這就是EFL格式和鏈接器的功勞
ELF文件格式把各種信息,分成一個(gè)一個(gè)的Section保存起來。ELF有一個(gè)基本的文件頭(File Header),用來表示這個(gè)文件的基本屬性,比如是否是可執(zhí)行文件,對(duì)應(yīng)的CPU、操作系統(tǒng)等等。除了這些基本屬性之外,大部分程序還有這么一些Section:
首先是.text Section,也叫作代碼段或者指令段(Code Section),用來保存程序的代碼和指令;
接著是.data Section,也叫作數(shù)據(jù)段(Data Section),用來保存程序里面設(shè)置好的初始化數(shù)據(jù)信息;
然后就是.rel.text Secion,叫作重定位表(Relocation Table)。重定位表里,保留的是當(dāng)前的文件里面,哪些跳轉(zhuǎn)地址其實(shí)是我們不知道的。比如上面的 link_example.o 里面,我們?cè)趍ain函數(shù)里面調(diào)用了 add 和 printf 這兩個(gè)函數(shù),但是在鏈接發(fā)生之前,我們并不知道該跳轉(zhuǎn)到哪里,這些信息就會(huì)存儲(chǔ)在重定位表里;
最后是.symtab Section,叫作符號(hào)表(Symbol Table)。符號(hào)表保留了我們所說的當(dāng)前文件里面定義的函數(shù)名稱和對(duì)應(yīng)地址的地址簿。
鏈接器會(huì)掃描所有輸入的目標(biāo)文件,然后把所有符號(hào)表里的信息收集起來,構(gòu)成一個(gè)全局的符號(hào)表。然后再根據(jù)重定位表,把所有不確定要跳轉(zhuǎn)地址的代碼,根據(jù)符號(hào)表里面存儲(chǔ)的地址,進(jìn)行一次修正。最后,把所有的目標(biāo)文件的對(duì)應(yīng)段進(jìn)行一次合并,變成了最終的可執(zhí)行代碼。這也是為什么,可執(zhí)行文件里面的函數(shù)調(diào)用的地址都是正確的
在鏈接器把程序變成可執(zhí)行文件之后,要裝載器去執(zhí)行程序就容易多了。裝載器不再需要考慮地址跳轉(zhuǎn)的問題,只需要解析 ELF 文件,把對(duì)應(yīng)的指令和數(shù)據(jù),加載到內(nèi)存里面供CPU執(zhí)行就可以了。
3 總結(jié)講到這里,相信你已經(jīng)猜到,為什么同樣一個(gè)程序,在Linux下可以執(zhí)行而在Windows下不能執(zhí)行了。其中一個(gè)非常重要的原因就是,兩個(gè)操作系統(tǒng)下可執(zhí)行文件的格式不一樣。
我們今天講的是Linux下的ELF文件格式,而Windows的可執(zhí)行文件格式是一種叫作PE(Portable Executable Format)的文件格式。Linux下的裝載器只能解析ELF格式而不能解析PE格式。
如果我們有一個(gè)可以能夠解析PE格式的裝載器,我們就有可能在Linux下運(yùn)行Windows程序了。這樣的程序真的存在嗎?
沒錯(cuò),Linux下著名的開源項(xiàng)目Wine,就是通過兼容PE格式的裝載器,使得我們能直接在Linux下運(yùn)行Windows程序的。
而現(xiàn)在微軟的Windows里面也提供了WSL,也就是Windows Subsystem for Linux,可以解析和加載ELF格式的文件。
我們?nèi)懣梢杂玫某绦?,也不僅僅是把所有代碼放在一個(gè)文件里來編譯執(zhí)行,而是可以拆分成不同的函數(shù)庫(kù),最后通過一個(gè)靜態(tài)鏈接的機(jī)制,使得不同的文件之間既有分工,又能通過靜態(tài)鏈接來“合作”,變成一個(gè)可執(zhí)行的程序。
對(duì)于ELF格式的文件,為了能夠?qū)崿F(xiàn)這樣一個(gè)靜態(tài)鏈接的機(jī)制,里面不只是簡(jiǎn)單羅列了程序所需要執(zhí)行的指令,還會(huì)包括鏈接所需要的重定位表和符號(hào)表。
4 推薦閱讀更深入了解程序的鏈接過程和ELF格式,推薦閱讀《程序員的自我修養(yǎng)——鏈接、裝載和庫(kù)》的1~4章。這是一本難得的講解程序的鏈接、裝載和運(yùn)行的好書。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/76194.html
摘要:這個(gè)辦法,在現(xiàn)在計(jì)算機(jī)的內(nèi)存管理里面,就叫作內(nèi)存分頁(yè)和分段這樣分配一整段連續(xù)的空間給到程序相比分頁(yè)則是把整個(gè)物理內(nèi)存空間切成一段段固定尺寸的大小而對(duì)應(yīng)的程序所需要占用的虛擬內(nèi)存空間,也會(huì)同樣切成一段段固定尺寸的大小。 showImg(https://image-static.segmentfault.com/290/765/2907653835-5d580caf245fd_articl...
摘要:不同的進(jìn)程,調(diào)用同樣的,各自里面指向最終加載的動(dòng)態(tài)鏈接庫(kù)里面的虛擬內(nèi)存地址是不同的。實(shí)際上,在進(jìn)行程序開發(fā),一直會(huì)用到各種各樣的動(dòng)態(tài)鏈接庫(kù)。通過動(dòng)態(tài)鏈接這個(gè)方式,可以說徹底解決了這個(gè)問題。參考深入淺出計(jì)算機(jī)組成原理 showImg(https://image-static.segmentfault.com/734/608/734608610-5d5846d292aa0_articlex...
摘要:馮諾依曼體系結(jié)構(gòu)示意圖總結(jié)馮諾依曼體系結(jié)構(gòu)確立了我們現(xiàn)在每天使用的計(jì)算機(jī)硬件的基礎(chǔ)架構(gòu)。因此,學(xué)習(xí)計(jì)算機(jī)組成原理,其實(shí)就是學(xué)習(xí)和拆解馮諾依曼體系結(jié)構(gòu)。 showImg(https://ask.qcloudimg.com/http-save/1752328/g6cdrb45jg.png); 1 計(jì)算機(jī)的基本硬件組成 早期,DIY一臺(tái)計(jì)算機(jī),要先有三大件 CPU 內(nèi)存 主板 1.1 C...
摘要:計(jì)算機(jī)組成中的大量原理和設(shè)計(jì),都對(duì)應(yīng)著性能這個(gè)詞。時(shí)間的倒數(shù)性能計(jì)算機(jī)的性能,其實(shí)和體力勞動(dòng)很像,好比是我們要搬東西。對(duì)于計(jì)算機(jī)的性能,我們需要有個(gè)標(biāo)準(zhǔn)來衡量?;ǖ臅r(shí)間越少,自然性能就越好。 0 學(xué)習(xí)路線的知識(shí)點(diǎn)概括 showImg(https://segmentfault.com/img/remote/1460000020031616?w=3832&h=2540); 學(xué)習(xí)計(jì)算機(jī)組成原...
摘要:匯編器是怎么把對(duì)應(yīng)的匯編代碼,翻譯成為機(jī)器碼的??偨Y(jié)打孔卡,其實(shí)就是一種存儲(chǔ)程序型計(jì)算機(jī)。推薦閱讀了解的指令集參看計(jì)算機(jī)組成與設(shè)計(jì)軟硬件接口第版的小節(jié)參考深入淺出計(jì)算機(jī)組成原理 你在學(xué)寫程序的時(shí)候,有沒有想過,古老年代的計(jì)算機(jī)程序是怎么寫出來的?showImg(https://ask.qcloudimg.com/http-save/1752328/fpfs9776q8.png); 當(dāng)...
閱讀 4325·2021-09-26 10:17
閱讀 921·2021-09-22 15:02
閱讀 3540·2021-09-06 15:00
閱讀 1107·2021-07-25 16:52
閱讀 2785·2019-08-29 16:16
閱讀 2570·2019-08-29 13:25
閱讀 1633·2019-08-26 13:51
閱讀 2228·2019-08-26 10:58