linker
鏈接器主要有兩個作用:
- 一是將若干輸入文件(.o文件)根據一定規則合併爲一個輸出文件(例如ELF格式的可執行文件);
- 一是將符號與地址綁定(當然加載器也要完成這一部分工作)。
關於鏈接器的工作機制可以參考 《Linker and Loader》
一書,本文只關心它的第一個功能,即如何根據一定規則將一個或多個輸入文件合併成輸出文件。這裏的 “一定規則” 是通過鏈接腳本描述的。
在進行鏈接時,linker 會根據鏈接腳本從輸入的 .o 文件中挑選出感興趣的 section,把它們合併生成新的 section,
這些新產生的 section 歸屬於目標文件的某個 segment(段),並出現在目標文件中。
這裏提到了segment的概念。Segment可以看作一組具有相同屬性(或部分相同屬性)的 section 的集合,屬性是指“讀、寫、執行”。例如 .text 通常存放的是代碼編譯後的二進制,它具有 r-x
權限;.rodata
存放是的只讀數據,如常量字符串,
它通常具有 r--
權限(實際上也可以具有x權限,例如用一個全局 const 數組存放可執行的機器碼);那麼在生成目標文件時,.text
和 .rodata
就可以通過一個具有 r-x
屬性的 text segment
來包含它們,這就是我們通常說的“文本段”。
segment 在 ELF 術語中稱爲 program header
,用來描述整個目標文件以什麼樣的方式加載到內存中,方式是指加載的地址、segment的長度和屬性等等。用 objdump –p
命令可以查看目標文件的 segment。
a.out: file format elf64-x86-64
Program Header:
PHDR off 0x0000000000000040 vaddr 0x0000000000400040 paddr 0x0000000000400040 align 2**3
filesz 0x00000000000001f8 memsz 0x00000000000001f8 flags r-x
INTERP off 0x0000000000000238 vaddr 0x0000000000400238 paddr 0x0000000000400238 align 2**0
filesz 0x000000000000001c memsz 0x000000000000001c flags r--
LOAD off 0x0000000000000000 vaddr 0x0000000000400000 paddr 0x0000000000400000 align 2**21
filesz 0x00000000000006fc memsz 0x00000000000006fc flags r-x
LOAD off 0x0000000000000e10 vaddr 0x0000000000600e10 paddr 0x0000000000600e10 align 2**21
filesz 0x0000000000000228 memsz 0x0000000000000230 flags rw-
DYNAMIC off 0x0000000000000e28 vaddr 0x0000000000600e28 paddr 0x0000000000600e28 align 2**3
filesz 0x00000000000001d0 memsz 0x00000000000001d0 flags rw-
NOTE off 0x0000000000000254 vaddr 0x0000000000400254 paddr 0x0000000000400254 align 2**2
filesz 0x0000000000000044 memsz 0x0000000000000044 flags r--
EH_FRAME off 0x00000000000005d0 vaddr 0x00000000004005d0 paddr 0x00000000004005d0 align 2**2
filesz 0x0000000000000034 memsz 0x0000000000000034 flags r--
STACK off 0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**4
filesz 0x0000000000000000 memsz 0x0000000000000000 flags rw-
RELRO off 0x0000000000000e10 vaddr 0x0000000000600e10 paddr 0x0000000000600e10 align 2**0
filesz 0x00000000000001f0 memsz 0x00000000000001f0 flags r--
...
elf 文件
object file
.text
,代碼段,就是CPU要運行的指令代碼;
.data
,數據段,程序中包含的一些數據,放在這個段裏;
.bss
,未初始化段,記錄了程序裏有哪些未初始化的變量,就相當於只記錄對應的名字,留着程序運行前去初始化爲0,所以,此處並不佔用具體空間。
symbol table 符號表
objdump -t
或者 nm
命令
defined symbol:通俗說就是全局變量、靜態變量、本文件的函數
undefined symbol:通俗說就是未初始化的全局變量、本文件引用的外部變量(extern)、以及引用的外部函數
section 分類
loadable,可加載,原先目標文件裏面包含對應的代碼或數據,裝載器要把這些內容,load到對應的地址,以便程序可以運行;
allocatable,可分配的,最簡單理解就是上面提到的.bss段,那裏記錄了變量信息,裝載器需要分配變量所要佔用的具體內存空間。
還有既不是loadbale的,也不是allocatable的,比如只存儲debug信息的段,此處不多解釋。
section 地址
VMA & LMA 分別對用程序運行時虛擬地址和程序的加載地址。
VMA(Virtual Memory Address):the address the section will have when the output file is run;
LMA(Load Memory Address): the address at which the section will be loaded.
通常情況下二者相等。可以通過 objdump -h
查看。
linker script
前面說到 linker 的作用,那麼鏈接器是依據什麼規則來生成最終的可執行文件的呢?沒錯,就是鏈接腳本。
如果沒有指定鏈接腳本的話,鏈接器有一個默認的,可以用 ld --verbose
查看。可以通過 ld -T
選項指定自己程序的鏈接腳本。
linker script 語法
// 什麼時候指定 LMA 和 VMA
SECTIONS
{
. = 0x10000;
.text :
{
*(.text)
}
. = 0x8000000;
.data :
{
*(.data)
}
.bss :
{
*(.bss)
}
}
.: address counter
上面的含義是說把輸入目標文件的代碼段全部放到輸出目標文件的代碼段,地址從 0x10000 開始;
數據段則放在 0x8000000 開始的位置。
INCLUDE filename
在看到這個命令的時候纔去載入filename這個linker script。可以被放在不同的命令如SETCTION, MEMORY等。
INPUT(file1 file2 ...)
指定加載的輸入object檔案,如abc.o這樣的檔案。
GROUP(file1 file2 ...)
指定加載的輸入archieve檔案,如libabc.a這樣的檔案。
AS_NEEDED(file1 file2 ...)
在INPUT和GROUP使用的命令,用來告訴linker說如果object裏面的數據有被reference到才link進來,
猜測應該可以減少儲存空間。範例(未測試請自行斟酌):INPUT(file1.o file2.o AS_NEEDED(file3.o file4.o))
OUTPUT(filename)
和gcc -o filename 一樣
SEARCH_DIR(path)
和-L path一樣
STARTUP(filename)
和INPUT相同,唯一差別是ld保證這個檔案一定是第一個被link
OUTPUT_FORMAT(bfdname)
指定輸出object檔案的binary 文件格式,可以使用objdump -i列出支持的binary 文件格式
OUTPUT_FORMAT(default, big, little)
指定輸出 object 檔案預設的 binary 文件格式
TARGET(bfdname)
指定使用哪種 binary 文件格式讀取輸入 object 檔案
可以使用 objdump -i
列出支持的 binary 文件格式。
HIDDEN(symbol name)
隱藏某個全局符號?
PROVIDE(symbol = expression)
如果程序沒有這個符號就使用這裏提供的符號和值
PROVIDE_HIDDEN(symbol = expression)
結合了上述兩者
有意思的地方
每次讀取一個目標文件,匹配裏面的 section,輸出到輸出目標文件相應的 section 中。
*(.sec1 .sec2):
File1.sec1
File1.sec2
File2.sec1
File2.sec2
*(.sec1) *(.sec2):
File1.sec1
File2.sec1
File1.sec2
File2.sec2