摘要:鏈接器會掃描所有輸入的目標文件,然后把所有符號表里的信息收集起來,構成一個全局的符號表。這是一本難得的講解程序的鏈接裝載和運行的好書。
既然程序最終都被變成了一條條機器碼去執行,那為什么同一個程序,在同一臺計算機上,在Linux下可以運行,而在Windows下卻不行呢?
反過來,Windows上的程序在Linux上也是一樣不能執行的
可是我們的CPU并沒有換掉,它應該可以識別同樣的指令呀!!!
如果你和我有同樣的疑問,那這一節,我們就一起來解開。
1 編譯、鏈接和裝載:拆解程序執行寫好的C語言代碼,可以通過編譯器編譯成匯編代碼,然后匯編代碼再通過匯編器變成CPU可以理解的機器碼,于是CPU就可以執行這些機器碼了
你現在對這個過程應該不陌生了,但是這個描述把過程大大簡化了
下面,我們一起具體來看,C語言程序是如何變成一個可執行程序的。
過去幾節,我們通過gcc生成的文件和objdump獲取到的匯編指令都有些小小的問題
我們先把前面的add函數示例,拆分成兩個文件
add_lib.c
link_example.c
通過gcc來編譯這兩個文件,然后通過objdump命令看看它們的匯編代碼。
objdump -d -M intel -S link_example.o
既然代碼已經被我們“編譯”成了指令
不妨嘗試運行一下 ./link_example.o
不幸的是,文件沒有執行權限,我們遇到一個Permission denied錯誤
即使通過chmod命令賦予link_example.o文件可執行的權限,運行 ./link_example.o 仍然只會得到一條cannot execute binary file: Exec format error的錯誤。
仔細看一下objdump出來的兩個文件的代碼,會發現兩個程序的地址都是從0開始
如果地址一樣,程序如果需要通過call指令調用函數的話,怎么知道應該跳到哪一個文件呢?
無論是這里的運行報錯,還是objdump出來的匯編代碼里面的重復地址
都是因為 add_lib.o 以及 link_example.o 并不是一個可執行文件(Executable Program),而是目標文件(Object File)
只有通過鏈接器(Linker) 把多個目標文件以及調用的各種函數庫鏈接起來,我們才能得到一個可執行文件
gcc的-o參數,可以生成對應的可執行文件,對應執行之后,就可以得到這個簡單的加法調用函數的結果。
C語言代碼-匯編代碼-機器碼 過程,在我們的計算機上進行的時候是由兩部分組成:
第一個部分由編譯(Compile)、匯編(Assemble)以及鏈接(Link)三個階段組成
三階段后,就生成了一個可執行文件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:
...你會發現,可執行代碼dump出來內容,和之前的目標代碼長得差不多,但是長了很多
因為在Linux下,可執行文件和目標文件所使用的都是一種叫ELF(Execuatable and Linkable File Format)的文件格式,中文名字叫可執行與可鏈接文件格式
這里面不僅存放了編譯成的匯編指令,還保留了很多別的數據。
第二部分,我們通過裝載器(Loader)把可執行文件裝載(Load)到內存中
CPU從內存中讀取指令和數據,來開始真正執行程序
2 ELF格式和鏈接:理解鏈接過程程序最終是通過裝載器變成指令和數據的,所以其實生成的可執行代碼也并不僅僅是一條條的指令
我們還是通過objdump指令,把可執行文件的內容拿出來看看。
比如我們過去所有objdump出來的代碼里,你都可以看到對應的函數名稱,像add、main等等,乃至你自己定義的全局可以訪問的變量名稱,都存放在這個ELF格式文件里
這些名字和它們對應的地址,在ELF文件里面,存儲在一個叫作符號表(Symbols Table)的位置里。符號表相當于一個地址簿,把名字和地址關聯了起來。
我們先只關注和我們的add以及main函數相關的部分
你會發現,這里面,main函數里調用add的跳轉地址,不再是下一條指令的地址了,而是add函數的入口地址了,這就是EFL格式和鏈接器的功勞
ELF文件格式把各種信息,分成一個一個的Section保存起來。ELF有一個基本的文件頭(File Header),用來表示這個文件的基本屬性,比如是否是可執行文件,對應的CPU、操作系統等等。除了這些基本屬性之外,大部分程序還有這么一些Section:
首先是.text Section,也叫作代碼段或者指令段(Code Section),用來保存程序的代碼和指令;
接著是.data Section,也叫作數據段(Data Section),用來保存程序里面設置好的初始化數據信息;
然后就是.rel.text Secion,叫作重定位表(Relocation Table)。重定位表里,保留的是當前的文件里面,哪些跳轉地址其實是我們不知道的。比如上面的 link_example.o 里面,我們在main函數里面調用了 add 和 printf 這兩個函數,但是在鏈接發生之前,我們并不知道該跳轉到哪里,這些信息就會存儲在重定位表里;
最后是.symtab Section,叫作符號表(Symbol Table)。符號表保留了我們所說的當前文件里面定義的函數名稱和對應地址的地址簿。
鏈接器會掃描所有輸入的目標文件,然后把所有符號表里的信息收集起來,構成一個全局的符號表。然后再根據重定位表,把所有不確定要跳轉地址的代碼,根據符號表里面存儲的地址,進行一次修正。最后,把所有的目標文件的對應段進行一次合并,變成了最終的可執行代碼。這也是為什么,可執行文件里面的函數調用的地址都是正確的
在鏈接器把程序變成可執行文件之后,要裝載器去執行程序就容易多了。裝載器不再需要考慮地址跳轉的問題,只需要解析 ELF 文件,把對應的指令和數據,加載到內存里面供CPU執行就可以了。
3 總結講到這里,相信你已經猜到,為什么同樣一個程序,在Linux下可以執行而在Windows下不能執行了。其中一個非常重要的原因就是,兩個操作系統下可執行文件的格式不一樣。
我們今天講的是Linux下的ELF文件格式,而Windows的可執行文件格式是一種叫作PE(Portable Executable Format)的文件格式。Linux下的裝載器只能解析ELF格式而不能解析PE格式。
如果我們有一個可以能夠解析PE格式的裝載器,我們就有可能在Linux下運行Windows程序了。這樣的程序真的存在嗎?
沒錯,Linux下著名的開源項目Wine,就是通過兼容PE格式的裝載器,使得我們能直接在Linux下運行Windows程序的。
而現在微軟的Windows里面也提供了WSL,也就是Windows Subsystem for Linux,可以解析和加載ELF格式的文件。
我們去寫可以用的程序,也不僅僅是把所有代碼放在一個文件里來編譯執行,而是可以拆分成不同的函數庫,最后通過一個靜態鏈接的機制,使得不同的文件之間既有分工,又能通過靜態鏈接來“合作”,變成一個可執行的程序。
對于ELF格式的文件,為了能夠實現這樣一個靜態鏈接的機制,里面不只是簡單羅列了程序所需要執行的指令,還會包括鏈接所需要的重定位表和符號表。
4 推薦閱讀更深入了解程序的鏈接過程和ELF格式,推薦閱讀《程序員的自我修養——鏈接、裝載和庫》的1~4章。這是一本難得的講解程序的鏈接、裝載和運行的好書。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76194.html
摘要:這個辦法,在現在計算機的內存管理里面,就叫作內存分頁和分段這樣分配一整段連續的空間給到程序相比分頁則是把整個物理內存空間切成一段段固定尺寸的大小而對應的程序所需要占用的虛擬內存空間,也會同樣切成一段段固定尺寸的大小。 showImg(https://image-static.segmentfault.com/290/765/2907653835-5d580caf245fd_articl...
摘要:不同的進程,調用同樣的,各自里面指向最終加載的動態鏈接庫里面的虛擬內存地址是不同的。實際上,在進行程序開發,一直會用到各種各樣的動態鏈接庫。通過動態鏈接這個方式,可以說徹底解決了這個問題。參考深入淺出計算機組成原理 showImg(https://image-static.segmentfault.com/734/608/734608610-5d5846d292aa0_articlex...
摘要:馮諾依曼體系結構示意圖總結馮諾依曼體系結構確立了我們現在每天使用的計算機硬件的基礎架構。因此,學習計算機組成原理,其實就是學習和拆解馮諾依曼體系結構。 showImg(https://ask.qcloudimg.com/http-save/1752328/g6cdrb45jg.png); 1 計算機的基本硬件組成 早期,DIY一臺計算機,要先有三大件 CPU 內存 主板 1.1 C...
摘要:計算機組成中的大量原理和設計,都對應著性能這個詞。時間的倒數性能計算機的性能,其實和體力勞動很像,好比是我們要搬東西。對于計算機的性能,我們需要有個標準來衡量。花的時間越少,自然性能就越好。 0 學習路線的知識點概括 showImg(https://segmentfault.com/img/remote/1460000020031616?w=3832&h=2540); 學習計算機組成原...
摘要:匯編器是怎么把對應的匯編代碼,翻譯成為機器碼的。總結打孔卡,其實就是一種存儲程序型計算機。推薦閱讀了解的指令集參看計算機組成與設計軟硬件接口第版的小節參考深入淺出計算機組成原理 你在學寫程序的時候,有沒有想過,古老年代的計算機程序是怎么寫出來的?showImg(https://ask.qcloudimg.com/http-save/1752328/fpfs9776q8.png); 當...
閱讀 4220·2021-09-26 10:17
閱讀 871·2021-09-22 15:02
閱讀 3446·2021-09-06 15:00
閱讀 1055·2021-07-25 16:52
閱讀 2734·2019-08-29 16:16
閱讀 2515·2019-08-29 13:25
閱讀 1588·2019-08-26 13:51
閱讀 2182·2019-08-26 10:58