三、匯編
編譯過程就是生成匯編代碼的過程,在編譯過程中,也會調用匯編器 as,將源代碼生成匯編代碼。比如,執行 gcc -S hello.c -o hello.s
此時已經生成了匯編代碼。
匯編的過程就是將 hello.s 生成目標文件。
匯編器是將匯編代碼轉變成機器可以執行的指令,每一個匯編語句幾乎都對應一條機器指令。匯編器的匯編過程相對于編譯器來講比較簡單,只是根據匯編指令和機器指令的對照表一一翻譯就可以了。它沒有復雜的語法,也沒有語義,也不需要做指令優化。
匯編過程可以調用匯編器 as 來完成:
as hello.s -o hello.o 或者 gcc -c hello.s -o hello.o
也可以使用 gcc 命令從 C 源代碼文件開始編譯,經過預編譯,編譯和匯編直接輸出目標文件(Object File):
gcc -c hello.c -o hello.o
四、鏈接
鏈接的過程就是生成 a.out 的過程。此過程中分為靜態鏈接和動態鏈接兩種。
4.1 靜態鏈接
可以先看看一個靜態鏈接的過程:
ld -static /usr/lib/crtl.o /usr/lib/crti.o -L/usr/lib/gcc/xxxx/xxx -L/usr/lib -L/usr/lib -L/lib hello.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/xxxx/xxx /usr/lib/crt0.o
鏈接的過程就是一個模塊拼裝的過程,將各種模塊通過符號拼裝成一個整體,即 a.out 。鏈接的主要內容就是把各個模塊之間相互引用的部分都處理好,使得各個模塊之間能正確的銜接。
鏈接過程主要包括了地址和空間分配(Address and Storage Allocation)、符號決議(Symbol Resolution)和重定位(Relocation)等這些步驟。
靜態鏈接過程圖如下:
源碼文件(.c)文件經過編譯器編譯成目標文件(Object file,一般擴展名為 .o 或 .obj),目標文件和庫(Library)一起鏈接形成最終的可執行文件。
靜態鏈接過程:
我們在程序模塊 main.c 中使用另外一個模塊 fun.c 中的函數 foo()。在 main.c 模塊中每一處調用 foo() 的時候都必須確切知道foo() 函數的地址,但是每個模塊都是單獨編譯的,在編譯器編譯 main.c 的時候它并不知道 foo() 的地址,所以它暫時把這些調用 foo() 的指令的目標地址擱置,等待最后鏈接的時候由鏈接器去將這些指令的目標地址修正。如果沒有鏈接器,需要我們手工把每個調用 foo() 的指令修正,填入正確的 foo 函數地址。
當 func.c 模塊被重新編譯的時候,foo() 函數的地址有可能改變的時,那么在 main.c 中所有使用到 foo 的地址的指令將要全部重新調整。這個工作將是繁瑣的,但是使用鏈接器,我們可以直接引用其他模塊的函數和全局變量而無需知道它們的地址。因為鏈接器在鏈接的時候,會根據我們所引用的符號 foo ,自動去相應的 func.c 模塊查找 foo 的地址,然后將 main.c 模塊中所有引用到 foo 的指令重新修正,讓它們的目標地址為真正的 foo 函數的地址。
上面就是靜態鏈接最基本的過程和作用。
地址修正的過程也被叫做重定位(Relocation),每個要被修正的地方叫一個重定位入口(Relocation Entry)。重定位所作的就是給程序中每個符號的絕對地址引用的位置'打補丁',是他們指向正確的地址。
通過一個例子來具體分析靜態鏈接的過程:
建立如下兩個文件
編譯: gcc -c a.c b.c ,生成 a.o b.o 的目標文件。
b.c 中有兩個全局符號:shared 和 swap;a.c 中定義了全局符號 main。a.c 中引用了 b.c 中的兩個全局符號。接下來要將 a.o 和 b.o 兩個文件鏈接在一起形成一個可執行文件'ab'。
4.1.1 地址空間分配
對于鏈接器來說,整個鏈接過程中,就是將幾個輸入目標文件加工后合并成一個輸出文件。對于多個輸入目標文件,鏈接器通過下面的方法將它們的各個段合并的輸出文件(即輸出文件中的空間地址分配給輸入文件)。
1.按序疊加
按序疊加即是將輸入的目標文件按照次序疊加起來。如下圖:
這樣做會造成在有很多輸入文件的情況下,輸出文件將會有很多零散的段。一般不 采用這種方式
2.相似段合并
相似段合并就是將相同性質的段合并到一起,如下圖所示:
.bss 段在目標文件和可執行文件中并不占用文件的空間,但是它在裝載時占用地址空間。所以鏈接器在合并各個段的同時,也將'.bss'合并,并且分配虛擬空間。
'鏈接器為目標文件分配地址和空間'中地址和空間有兩個含義:
第一個是在輸出的可執行文件中的空間
第二個是在裝載后的虛擬地址中的虛擬地址空間
對于實際數據的段,如 .text 、 .data,它們在文件中和虛擬地址中都要分配空間,因為這兩者中都存在
對于 .bss 這樣的段,分配空間的意義只局限于虛擬地址空間,因為它在文件中沒有內容
我們只關注虛擬地址的分配。
現代鏈接器空間分配的策略都采用第二種方法進行分配,使用這種方法的鏈接器采用兩步鏈接(Two-pass Linking):
第一步,空間和地址分配
掃描所有的輸入目標文件,并且獲得它們各個段的長度、屬性和位置,并且將輸入目標文件中的符號表中所有的符號定義和符號引用收集起來,統一放到一個全局符號表。這一步中,鏈接器將能夠獲得所有輸入目標文件的段長度,并且將它們合并,計算出輸出文件中各個段合并后的長度和位置,并建立映射關系
第二步,符號解析與重定位
使用第一步收集到的所有信息,讀取輸入文件中段的數據、重定位信息,并且進行符號解析與重定位、調整代碼中的地址等。第二步是鏈接過程的核心,特別是重定位過程。
鏈接 a.o 和 b.o:gcc -o ab a.o b.o(不能直接用 ld 鏈接器,要使用的話也得指定當前使用得gcc 中得鏈接器工具)
鏈接完成后,使用 objdump 查看輸出后得 ab 文件:
ab
鏈接后得程序中所使用得地址已經是程序在進程中的虛擬地址,即各個段的 VMA(Virtual Memory Address)和 Size,而忽略文件偏移(File off)。
鏈接前,目標文件中的所有段的 VMA 都是 0 ,因為虛擬地址空間還未分配,等到鏈接之后,可執行文件 'ab' 中的各個段都被分配到了相應的虛擬地址。這里的輸出程序'ab'中,'.text'段被分配到了地址 0x00400450,大小為 212 個字節;'.data'段從地址 0x00601028 開始,大寫為 14 個字節,整個鏈接過程前后,目標文件中各段的分配、程序虛擬地址如下圖:
當前因為程序未指定程序入口為 main 因此合并后的 .text 和 .data 會大于a.o 和 b.o 之和。可以用反匯編指令查看 ab ,看看程序從哪里開始的。
上一篇:GCC編譯器原理(三)------編譯原理三:編譯過程(3)---編譯之匯編以及靜態鏈接【2】
下一篇:GCC編譯器原理(三)------編譯原理三:編譯過程(2-2)---編譯之語法分析
推薦閱讀最新更新時間:2025-06-07 23:41