2025-02更新
(首先慶祝一下LLVM 2000 commits達成!)
編譯器driver options
在描述鏈接器選項前先介紹一下driver
options的概念。gcc和clang面向用戶的選項稱爲driver
options。一些driver options會影響傳遞給鏈接器的選項。 有些driver
options和鏈接器重名,它們往往在傳遞給鏈接器同名選項之外還有額外功效,比如:
-shared: 不設置-dynamic-linker,不鏈接crt1.o-static: 不設置-dynamic-linker,使用crtbegint.o而非crtbegin.o,使用--start-group鏈接-lgcc -lgcc_eh -lc(它們有(不好的)循環依賴)
-Wl,--foo,value,--bar=value會傳遞--foo、value、--bar=value三個選項給鏈接器。
如果有大量鏈接選項,可以每行一個放在文本文件response.txt裏,然後指定-Wl,@response.txt。
注意,-O2不會傳遞-O2給鏈接器,-Wl,-O2則會。
-fno-pic,-fno-PIC是同義的,生成position-dependent code-fpie,-fPIE分別叫做small PIE、large PIE,在PIC基礎上引入了一個優化:編譯的.o只能用於可執行檔。參見下文的-Bsymbolic。-fpic,-fPIC分別叫做small PIC、large PIC,position-independent code。在32-bit PowerPC和Sparc上(即將退出歷史舞臺的架構)兩種模式有代碼生成差異。大多數架構沒有差異。
輸入文件
鏈接器接受幾類輸入。對於符號,每個輸入文件的符號表都會影響符號解析;對於sections,只有regular object files裏的sections(稱爲input sections)會貢獻到輸出文件的sections(稱爲output sections)。
- .o (regular object files)
- .so (shared objects): 只影響符號解析
- .a (archive files)
符號解析細節參見Symbol processing。
模式
鏈接器處於以下四種模式之一。模式控制輸出類型(可執行檔/shared object/relocatable object):
-no-pie(預設): 生成position-dependent executable (ET_EXEC)。要求最寬鬆,源文件可用-fno-pic, -fpie/-fPIE, -fpic/-fPIC編譯-pie: 生成position-independent executable (ET_DYN)。源文件須要用-fpie/-fPIE, -fpic/-fPIC編譯-shared: 生成position-independent shared object (ET_DYN)。最嚴格,源文件須要用-fpic/-fPIC編譯-r: 生成relocatable file。這稱爲relocatable link,比較特殊。它會抑制各種linker synthesized sections並保留relocations。參見Relocatable linking。
容易產生混淆的是,編譯器driver提供了幾個同名選項:-no-pie,-pie,-shared,-r。
GCC
6引入了configure-time選項--enable-default-pie:啓用該選項的GCC預設-fPIE和-pie。現在,很多Linux發行版都啓用了該選項作爲基礎的security
hardening。
可執行檔鏈接(-no-pie和-pie)
定義的符號是non-preemptible的。 對於使用PLT-generating relocation的分支指令,分支可以直接綁定到定義,避免PLT。 對於涉及GOT-generating relocation的代碼序列,代碼序列可能被優化爲直接訪問。詳見All about Global Offset Table。
Non-local
STV_DEFAULT/STV_PROTECTED定義符號預設不導出到dynamic symbol
table。
-no-pie
-no-pie表示link-time地址等於run-time地址。
鏈接器利用這個特性:所有引用non-preemptible符號的relocations都可以解析,包括absolute
GOT-generating(如R_AARCH64_LD64_GOT_LO12_NC)、PC-relative
GOT-generating(如R_X86_64_REX_GOTPCRELX)等。
在沒有GOT優化的情況下,non-preemptible符號的GOT entry是常量,避免dynamic
relocation。 Image base預設爲架構特定的非零值。
- 某些架構有不同的PLT代碼序列(i386、ppc32 .glink)
R_X86_64_GOTPCRELX和R_X86_64_REX_GOTPCRELX可以進一步優化- ppc64
.branch_lt(long branch addresses)可以優化
-pie
-pie和-shared -Bsymbolic很相似,但它生成可執行檔。以下行爲和-no-pie貼近而與-shared不同:
- 允許copy relocation和canonical PLT
- 允許relax General Dynamic/Local Dynamic TLS models和TLS descriptors到Initial Exec/Local Exec
- 會鏈接時解析undefined weak符號爲零。ld.lld不生成dynamic relocation。GNU ld是否生成dynamic relocation有非常複雜的規則,且和架構相關
Shared
object鏈接(-shared)
Non-local
STV_DEFAULT定義預設是preemptible(可interpose)的,即定義可能在運行時被可執行檔或另一個shared
object中的定義替換。 編譯器和鏈接器通過使用GOT和PLT
entries來引用這類符號。
Non-local STV_DEFAULT/STV_PROTECTED符號會導出到dynamic
symbol table。
例如,在以下程序中,如果用-shared鏈接,foo會導出到dynamic
symbol
table,但如果用-no-pie或-pie鏈接則(預設)不會。
1
void foo() {}
PIC鏈接(-pie和-shared)
引用non-preemptible non-TLS符號的symbolic relocation(absolute relocation且寬度匹配word size)會轉換爲relative relocation。
詳見Relative relocations and RELR。
符號相關
-Bsymbolic
-Bsymbolic系列選項使shared object中的non-local
STV_DEFAULT定義變爲non-preemptible。對可執行檔輸出無效。關於"preemptible"的介紹參見上文"模式"。
-Bsymbolic使所有定義(除了被--dynamic-list/--export-dynamic-symbol-list/--export-dynamic-symbol匹配的)變爲non-preemptible-Bsymbolic-functions類似-Bsymbolic,但只適用於STT_FUNC定義-Bsymbolic-non-weak-functions類似-Bsymbolic,但只適用於non-STB_WEAKSTT_FUNC定義
詳見ELF interposition and -Bsymbolic。
--defsym
定義一個符號。類似ld64的-alias。
--exclude-libs
如果匹配的archive(regular或被--whole-archive/--no-whole-archive包圍的)定義了non-local符號,不導出該符號。
例如,clang++ -static-libstdc++ -Wl,--export-dynamic,--exclude-libs=libstdc++.a a.cc不會導出libstdc++定義的符號。
--export-dynamic
此選項將non-local
STV_DEFAULT/STV_PROTECTED定義符號放入可執行檔輸出的dynamic
symbol table。 在以下情況下此選項無效:
- 指定了
-shared,因爲shared object預設就會這樣做 - 指定了
-no-pie且沒有輸入shared object,因爲不存在dynamic symbol table
以下是符號被導出到dynamic symbol table的規則(logical AND):
- non-local
STV_DEFAULT/STV_PROTECTED(這意味着可以被--exclude-libs隱藏) - 以下條件的logical OR:
- undefined符號
- (
--export-dynamic||-shared) &&! (unnamed_addr linkonce_odr GlobalValue || local_unnamed_addr linkonce_odr (constant GlobalVariable || Function))(在LTO中,某些linkonce_odr符號可以被隱藏) - 被
--dynamic-list/--export-dynamic-symbol-list/--export-dynamic-symbol匹配 - 被shared object定義或引用爲
STV_DEFAULT - 當指定
--ignore-{data,function}-address-equality}時,shared object中的STV_PROTECTED定義被copy relocation/canonical PLT搶佔 -z ifunc-noplt&& 有至少一個relocation
如果可執行檔定義了一個被鏈接時shared object引用的符號,鏈接器會導出該符號,使得運行時該shared object的undefined符號可以綁定到可執行檔中的定義。 如果可執行檔定義了一個也被鏈接時shared object定義的符號,鏈接器會導出該符號以啓用運行時的symbol interposition。
在LLVM中,某些unnamed_addr
GlobalValue不受--export-dynamic對可執行檔鏈接的影響。
參見lld/test/ELF/lto/internalize-exportdyn.ll和lld/test/ELF/lto/unnamed-addr-comdat.ll。
--export-dynamic-symbol=glob,
--export-dynamic-symbol-list, and
--dynamic-list
這些選項對可執行檔和shared object有不同的語義:
- 可執行檔:將匹配的non-local定義符號放入dynamic symbol
table(
--export-dynamic適用於所有non-local定義符號) - shared object:對匹配的non-local
STV_DEFAULT符號的引用不應綁定到shared object內的定義,即使由於-Bsymbolic、-Bsymbolic-functions或--dynamic-list它們本來會綁定
--dynamic-list額外隱含-Bsymbolic。
對於shared object的情況,我通常稱這個操作爲"使符號preemptible"。
可以使用--export-dynamic-symbol=foo*來匹配所有non-local
STV_DEFAULT符號foo*。ld.lld
11之前使用精確匹配而不是glob。
--export-dynamic-symbol-list自GNU
ld 2.35和ld.lld
14起實現。
在以下示例中,當var是preemptible時,會看到GLOB_DAT
dynamic relocation。 1
2
3// a.c
int var;
int inc() { return ++var; }
1 | # shared object中預設是preemptible的。 |
如果version script中local:匹配的符號被dynamic
list指定,version script優先,符號會變爲local。
--discard-none,
--discard-locals, and --discard-all
如果生成.symtab,live
section中定義的local符號被保留的條件是:
1 | if ((--emit-relocs or -r) && referenced) || --discard-none |
這些.L符號(MC中的temporary
labels)可以在clang -Wa,-L構建中發出。然後如果未指定ld --discard-locals,鏈接器會將這些符號發出到可執行檔。
此外,RISC-V linker
relaxation可能會發出.L0(帶尾隨空格)符號(llvm-project#89693)。
對於RISC-V,較新的GCC和Clang會傳遞-X(--discard-locals)給鏈接器。
--no-undefined-version
如果version script指定了精確模式(非glob)但不匹配任何已定義的符號,則報錯。
假設有以下version
script,如果foo不是已定義的符號,鏈接器會報錯。
對於不匹配任何符號的glob模式(例如bar*),不會報錯。這是一種折衷。
1
2
3
4v1 {
foo;
bar*;
};
GNU ld自2002-08起支持--no-undefined-version,但--undefined-version是2022-10才添加的(milestone:
binutils 2.40)。
--strip-all
不要創建.strtab和.symtab。
-u symbol
若某個archive file定義了-u指定的符號則pull(由archive
file轉換爲object file,之後該文件就和一般的.o相同)。
比如:ld -u foo ... a.a。若a.a不定義被之前object
files引用的符號,a.a不會被pull。
如果指定了-u foo,那麼a.a中定義了foo的archive
member會被pull。
-u的另一個作用是指定一個GC root。
--version-script=script
Version script有三個用途:
- 定義versions
- 指定一些模式,使得匹配的、定義的、unversioned的符號具有指定的version
- Local
version:
local:可以改變匹配的、定義的、unversioned的符號的binding爲STB_LOCAL,不會導出到dynamic symbol table
Symbol versioning描述了具體的symbol versioning機制。
-y symbol
常用於調試。輸出指定符號在哪裏被引用、哪裏被定義。
-z muldefs
允許重複定義的符號。鏈接器預設不允許兩個同名的non-local regular definitions(非weak、非common)。
-z unique-symbol
重命名local符號使其沒有重複。
一些Intel的開發者在開發function granular kernel address space layout randomization功能,需要這個特性(https://sourceware.org/bugzilla/show_bug.cgi?id=26391)。 GNU ld自2.36起支持此選項。 我關閉了ld.lld的feature request。
我認爲這不是一個好的設計。
首先是穩定性問題。 假設舊kernel有foo.1 foo.2。
如果有一個新的local
foo符號,新kernel會有foo.1 foo.2 foo.3。
然而,新符號不一定對應舊kernel中同名的local符號。
這種擾動在LTO或PGO時可能更容易發生。 對於Clang LTO,kernel
Makefile目前指定-mllvm -import-instr-limit=5。
如果一個接近邊界的函數碰巧越過邊界,被inline到其他translation
units,穩定性問題可能影響很多translation units。
實現需要對所有local符號進行迭代,這會影響鏈接速度。
此外,.[0-9]+方案已被C++ mangling使用。 Itanium C++
ABI說"A
1 | % c++filt <<< $'_ZL3foov\n_ZL3foov.1' |
作爲替代方案,我建議FGASLR開發者使用STT_FILE符號:
1
2
3
4STT_FILE a.c
STT_NOTYPE foo
STT_FILE b.c
STT_NOTYPE foo
ELF規範說:
Conventionally, the symbol's name gives the name of the source file associated with the object file. A file symbol has STB_LOCAL binding, its section index is SHN_ABS, and it precedes the other STB_LOCAL symbols for the file, if it is present.
我在[PATCH v9 02/15] livepatch: use `-z unique-symbol` if available to nuke pos-based search的回覆中提到了我的擔憂。
Library相關
--as-needed and
--no-as-needed
通常每個鏈接時shared object都有一個DT_NEEDED標籤。
這樣的shared object會被dynamic loader加載。
--as-needed可以避免不需要的DT_NEEDED標籤。
--as-needed和--no-as-needed是position-dependent選項(非正式叫法,但沒找到更貼切的形容詞)。
在ld.lld中,一個shared object is needed,如果下面條件之一成立:
- 在命令行中至少一次出現在
--no-as-needed模式下(即--as-needed a.so --no-as-needed a.so=> needed) - 或者它有一個定義解析了來自live
section(未被
--gc-sections丟棄)的non-weak引用
在gold中,規則大概是:
- 在命令行中至少一次出現在
--no-as-needed模式下 - 或者它有一個定義解析了non-weak引用
在GNU ld中,規則相當複雜。基本如下:
- 在命令行中至少一次出現在
--no-as-needed模式下 - 或者它有一個定義解析了前一個輸入文件的non-weak引用(工作方式類似archive selection)
在ld.bfd ... a.so --as-needed b.so --no-as-needed中,如果a.so引用了b.so定義的符號但a.so不需要b.so,最終輸出會需要b.so。
這可能被用作underlinking問題的workaround。 當看到shared
object缺少的依賴(b.so)時,輸出會獲得DT_NEEDED條目來滿足b.so的需求,即使它本身不需要該依賴。
-Bdynamic and
-Bstatic
這兩個選項是position-dependent選項,影響後面命令行出現的-lname。
-Bdynamic(default):在-L指定的目錄列表中查找libfoo.so和libfoo.a-Bstatic:在-L指定的目錄列表中查找libfoo.a
注意,歷史上GNU
ld裏-Bstatic和-static同義。編譯器driver的-static是個不同的選項,除了傳遞-static給ld外,還會去除預設的--dynamic-linker,影響libgcc
libc等的鏈接。
--no-dependent-libraries
ld.lld特有。忽略object
files中類型爲SHT_LLVM_DEPENDENT_LIBRARIES(通常命名爲.deplibs)的sections。
該section包含一個文件名列表。這些文件名會被ld.lld作爲額外的輸入文件添加。
-soname=name
設置生成的shared object的dynamic
table中的DT_SONAME。
鏈接器會記錄鏈接時shared objects,在生成的可執行檔/shared
object的dynamic
table中用一條DT_NEEDED記錄描述每一個鏈接時shared
object。
- 若該shared
object含有
DT_SONAME,該字段提供`DT_NEEDED的值 - 否則,若通過
-l鏈接,值爲去除目錄後的文件名 - 否則值爲路徑名(絕對/相對路徑有差異)
比如:ld -shared -soname=a.so.1 a.o -o a.so; ld b.o ./a.so,a.out的DT_NEEDED爲a.so.1。如果第一個命令不含-soname,則a.out的DT_NEEDED爲./a.so。
--start-group and
--end-group
如果A.a和B.a有相互引用,且不能確定哪一個會被先pull
into the link,得使用這對選項。下面給出一個例子:
對於一個archive鏈接順序:main.o A.a B.a,假設main.o引用了B.a,而A.a沒有滿足之前的某個undefined符號,那麼該鏈接順序會導致錯誤。
鏈接順序換成main.o B.a A.a行不行呢?如果main.o變更後引用了A.a,而B.a沒有滿足之前的某個undefined符號,那麼該鏈接順序也會導致錯誤。
一種解決方案是main.o A.a B.a A.a。很多情況下重複一次就夠了,但是假如鏈接第一個A.a時僅加載了A.a(a.o),鏈接B.b時僅加載了B.a(b.o),鏈接第二個A.a時僅加載了A.a(c.o)且A.a(c.o)需要B.a中的另一個member,該鏈接順序仍會導致undefined
symbol錯誤。
我們可以再重複一次B.a,即main.o A.a B.a A.a B.a,但更好的解決方案是main.o --start-group A.a B.a --end-group,或main.o -( A.a B.a -)。
--start-lib and
--end-lib
參見Archives and
--start-lib。
如果a.a包含b.o c.o,ld ... --start-lib b.o c.o --end-lib作用類似ld ... a.a。
--sysroot
這與driver選項--sysroot不同。
在GCC/Clang中,driver選項--sysroot做兩件事:
- 決定include/library搜索路徑(例如
$sysroot/usr/include、$sysroot/lib64) - 傳遞
--sysroot給ld。
在ld中,
-l =foo和-l=foo在sysroot目錄下查找libfoo.so或libfoo.a。INPUT或GROUP中的foo在sysroot目錄下查找foo。- 如果一個linker
script在sysroot目錄下,當它打開絕對路徑文件(
INPUT或GROUP)時,在絕對路徑前加上sysroot。
-t --trace
輸出relocatable object files、shared objects和提取的archive members。
--whole-archive
and --no-whole-archive
--whole-archive選項後的.a會當作.o一樣處理,沒有惰性語義。
如果a.a包含b.o c.o,那麼ld --whole-archive a.a --no-whole-archive和ld b.o c.o作用相同。
--push-state and
--pop-state
GNU ld在binutils 2.25實現了這些選項。
-Bstatic, --whole-archive, --as-needed等都是表示boolean狀態的position-dependent選項。--push-state可以保存這些選項的boolean狀態,--pop-state則會還原。
在鏈接命令行插入新選項裏變更狀態時,通常希望能還原,這個時候就可以用--push-state和--pop-state。
比如確保鏈接libc++.a和libc++abi.a可以用-Wl,--push-state,-Bstatic -lc++ -lc++abi -Wl,--pop-state。
依賴關係相關
詳見Dependency related linker options。
-z defs and
-z undefs
遇到來自regular
objects的不能解析的undefined符號(不能在鏈接時綁定到可執行檔或一個鏈接時shared
object中的定義),是否報錯。可執行檔預設爲-z defs/--no-undefined(不允許),而shared
objects預設爲-z undefs(允許)。
很多構建系統會啓用-z defs,要求shared
objects在鏈接時指定所有依賴(link what you use)。
--allow-shlib-undefined
and --no-allow-shlib-undefined
遇到來自shared objects的不能解析的STB_GLOBAL
undefined符號,是否報錯。可執行檔預設爲--no-allow-shlib-undefined(報錯),而-shared鏈接預設爲--allow-shlib-undefined(不報錯)。
對於如下代碼,鏈接可執行檔時會報錯: 1
2
3
4
5
6
7// a.so
void f();
void g() { f(); }
// exe
void g()
int main() { g(); }
如果啓用--allow-shlib-undefined,鏈接會成功,但ld.so會在運行時報錯,在glibc中爲:symbol lookup error: ... undefined symbol:。
GNU ld有個複雜的算法查找transitive closure,只有transitive
closure的shared objects都無法解析一個undefined符號時纔會報錯。
gold和ld.lld使用一個簡化的規則:如果一個shared
object的所有DT_NEEDED依賴都被直接鏈接了,則啓用報錯;如果部分依賴沒有被鏈接,那麼gold/ld.lld無法準確判斷是否一個未被直接鏈接的shared
object能提供定義,就保守地不報錯。
值得一提的是,-z defs/-z undefs/--no-undefined和--[no-]allow-shlib-undefined可以被一個選項--unresolved-symbols控制。
--warn-backrefs
參見Dependency related linker options#--warn-backrefs。
Layout相關
--no-rosegment
預設情況下,ld.lld將只讀數據sections(如.rodata)和代碼sections(如.text)放入兩個PT_LOAD
segments。
- R
PT_LOAD - RX
PT_LOAD - RW
PT_LOAD(和PT_GNU_RELRO重疊) - RW
PT_LOAD
指定該選項可以合併R PT_LOAD和RX PT_LOAD。
RX PT_LOAD segment傳統上稱爲text
segment,是第一個segment。
ld.lld將rodata和data放在text兩側。這種佈局的優點是text和data之間的距離較短,降低了relocation overflow的壓力。
gold是第一個實現--rosegment的鏈接器。
--xosegment
此選項啟用對execute-only memory的支持。
- AArch32使用
SHF_ARM_PURECODEsection flag來指定純程序指令且無數據的sections。 - AArch64使用
SHF_AARCH64_PURECODEsection flag。
預設情況下,LLD將具有SHF_ALLOC|SHF_EXECINSTR|SHF_AARCH64_PURECODE標誌的sections視為與具有SHF_ALLOC|SHF_EXECINSTR標誌的sections兼容,將它們合併到單個PT_LOAD
segment中。
當指定--xosegment時,LLD將這些sections分離到不同的PT_LOAD
segments中。
-z separate-loadable-segments
ld.lld傳統佈局:所有PT_LOAD
segments都沒有重疊(一個字節不會被同時加載到兩個memory mappings)。
實現方式是每個新PT_LOAD的地址對齊到max-page-size。ld.lld預設有4個PT_LOAD(R,RX,RW(RELRO),RW(non-RELRO)),在輸出文件裏三次對齊都可能浪費一些字節。
在AArch64和PowerPC上因爲ABI指定的max-page-size較大(65536),最多可浪費65536*3字節。
-z separate-code
binutils 2.31引入該選項,在Linux/x86上爲預設。GNU ld採用這種佈局:
- R
PT_LOAD - RX
PT_LOAD - R
PT_LOAD - RW
PT_LOADPT_GNU_RELRO部分- 非
PT_GNU_RELRO部分
在這種佈局中,兩個相鄰的PT_LOAD program
headers不能在文件偏移上重疊。也就是說,文件中被映射到可執行section的字節(RX
PT_LOAD)不會同時被映射到R PT_LOAD。
這個想法是,既然只讀內存不能被執行,那裏的ROP gadgets就不能被使用。
然而,這基本上是安全劇場,因爲可執行內存本身就有大量的ROP gadgets。
由於實現複雜性,採用的佈局不太理想,因爲RX
PT_LOAD後面還有另一個只讀PT_LOAD。
更好的佈局是將這個R與第一個R合併(PR23704)。
另一個問題是,當沒有RW
PT_LOAD時,前幾個非SHF_ALLOC
sections的內容可能被映射到RX內存。
我在ld.lld 10引入該選項。語義和GNU ld類似但佈局不同:兩個RW
PT_LOAD允許重疊,這意味着第二個PT_LOAD的地址不需要對齊,最多可浪費max-page-size*2字節。
GNU
ld的-z separate-code在lld中本質上被分成兩個選項:-z separate-code和--rosegment。
-z noseparate-code
這是GNU
ld的經典佈局,允許某些文件內容被映射爲多個PT_LOAD
segments,其中一個是可執行的,另一個是不可執行的。
在這種佈局中,兩個相鄰的PT_LOAD program
headers在文件偏移上可能重疊。這個技巧避免了下一個program
header開頭的padding。
在沒有linker script
fragments的情況下,通常只有兩個PT_LOAD segments:
- RX
PT_LOAD:包含只讀sections(SHF_ALLOC)和可執行sections(SHF_ALLOC|SHF_EXECINSTR)。 - RW
PT_LOAD- 前綴部分爲
PT_GNU_RELRO。這部分在rtld處理完dynamic relocations後mprotect成readonly。 - 非
PT_GNU_RELRO的部分。這部分在運行時始終可寫。
- 前綴部分爲
第一個PT_LOAD常被稱爲text
segment。這個術語有些不準確,因爲該segment也包含只讀數據。
ld.lld 10中預設使用這種佈局,因爲其大小優勢。
注意:當一個SHT_NOBITS
section後面跟著另一個section時,SHT_NOBITS
section的行爲就像它佔用了文件偏移範圍。
這是因爲ld.lld沒有實現文件大小優化。
幾乎所有鏈接的image都不使用這個優化,因爲在SHT_NOBITS SHF_ALLOC
section後添加SHF_ALLOC sections是很少見的。
-z relro
將RELRO sections放在PT_GNU_RELRO program header中。
GNU ld使用一個RW PT_LOAD program
header,在開頭添加padding。PT_LOAD的前半部分與PT_GNU_RELRO重疊。
添加padding是爲了使PT_GNU_RELRO的結尾對齊到max-page-size。(參見ld.bfd --verbose輸出。)
GNU ld 2.39之前,結尾對齊到common-page-size。 GNU ld的單個RW
PT_LOAD佈局使對齊增加了文件大小。max-page-size可能很大,如65536,導致空間浪費。
ld.lld使用兩個RW PT_LOAD program headers:一個用於RELRO
sections,另一個用於non-RELRO sections。
雖然這最初看起來可能不尋常,但它消除了GNU ld佈局中的對齊padding需求。
關鍵變更:
- https://reviews.llvm.org/D58892
從
PT_LOAD(PT_GNU_RELRO(.data.rel.ro .bss.rel.ro) .data .bss)切換到PT_LOAD(PT_GNU_RELRO(.data.rel.ro .bss.rel.ro)) PT_LOAD(.data. .bss)。 PT_GNU_RELROsegment和關聯的RWPT_LOADsegment的結尾被padding到common-page-size邊界。padding section.relro_padding類似mold。 LLD 18之前有個問題:runtime_page_size < common-page-size不工作。
mold使用的佈局與ld.lld類似。
在mold的情況下,PT_GNU_RELRO的結尾通過附加一個SHT_NOBITS
.relro_padding section被padding到max-page-size。
這種方法確保無論系統page
size如何,PT_GNU_RELRO的最後一頁都被保護。 然而,當系統page
size小於max-page-size時,第一個RW
PT_LOAD的映射大於所需。
在我看來,當運行時page
size大於common-page-size時失去最後一頁的保護並不是真正的問題。
爲了保護而double mapping一頁達到max-common-page可能導致不必要的VM浪費。
保護.got.plt是-z now的主要目的。保護.data.rel.ro的一小部分並不能真正使程序更安全,因爲.data和.bss非常大且充滿了攻擊目標。
如果用戶真的很擔心,可以將common-page-size設置爲匹配系統page size。
GNU ld的內部linker scripts將RELRO
sections放在DATA_SEGMENT_ALIGN和DATA_SEGMENT_RELRO_END(內置函數)之間。
DATA_SEGMENT_ALIGN是添加padding的地方,使DATA_SEGMENT_RELRO_END對齊到max-page-size邊界。
1
2
3. = DATA_SEGMENT_ALIGN(CONSTANT(MAXPAGESIZE), CONSTANT(COMMONPAGESIZE));
. = DATA_SEGMENT_RELRO_END(0, .);
. = DATA_SEGMENT_END(.);
ld.lld模擬這些內置函數:
DATA_SEGMENT_ALIGN:將當前位置設置爲alignTo(script->getDot(), + align)DATA_RELRO_END:將當前位置設置爲alignTo(script->getDot(), MAXPAGESIZE)。.relro_padding被放置在DATA_RELRO_END之前。
-z lrodata-after-bss
參見Relocation overflow and code models#x86-64 linker requirement。
--execute-only
這是ld.lld特有的AArch64選項。該選項需要--rosegment,使RX
PT_LOAD segment為execute-only (PF_X)。
Relocation相關
--apply-dynamic-relocs
一些psABI使用RELA格式(AArch64、PowerPC、RISC-V、x86-64等):relocations包含addend字段。
在這些目標上,--apply-dynamic-relocs要求鏈接器將relocated位置的初始值設置爲addend而不是0。
如果可執行檔/shared
objects使用壓縮,--no-apply-dynamic-relocs可以改善壓縮。
ld.lld的所有ports都支持--apply-dynamic-relocs。
截至2023年8月,GNU ld僅aarch64
port支持--apply-dynamic-relocs。
--emit-relocs
此選項使-no-pie/-pie/-shared鏈接保留輸入的relocations,類似於-r。
可用於鏈接後的二進制分析。我知道的唯二用途是Linux kernel
x86的CONFIG_RELOCATABLE和BOLT。
使用--emit-relocs時,output section順序可能不同。
.rela.eh_frame sections被保留。參見https://reviews.llvm.org/D44679,第一個.rela.eh_frame
input section可能導致.eh_frame被放置在其他只讀output
sections之前。
GNU ld的powerpc使用transformed relocation types。
--pack-dyn-relocs=value
relr可以啓用DT_RELR,一種更加緊湊的relative
relocation (R_*_RELATIVE)編碼方式。Relative
relocations常見於-pie鏈接的可執行檔。
-z rel and
-z rela
每個架構有一個主要的relocation格式。
ld.lld實現了-z rel,即使在使用RELA作爲主要格式的架構上也使用REL作爲dynamic
relocations。 此選項可以節省一些空間。
- COPY、GLOB_DAT和J[U]MP_SLOT的addend始終爲0。rtld實現不需要讀取隱式addend。REL嚴格更好。
- RELATIVE有非零addend。它也可以使用隱式addend。或者,這些relocations可以用RELR relocation entry格式緊湊打包。
- 對於其他dynamic relocation類型(例如symbolic relocation R_X86_64_64),ld.so實現需要讀取隱式addend。REL可能有輕微的性能影響,因爲隱式addends強制隨機訪問讀取,而不是能夠在追蹤relocation數組時批量寫入。
-z report-relative-reloc
輸出關於R_*_RELATIVE和R_*_IRELATIVE
relocations的信息。
-z text and
-z notext
-z text不允許text relocations。
-z notext允許text relocations。
binutils 2.35起,Linux/x86上的GNU
ld預設啓用configure-time選項--enable-textrel-check={warning,error},若有text
relocations會給出warning/error。
Text relocations這個概念的用詞不準確,實際含義是作用在readonly
sections上的dynamic relocations的總稱。
.o中的relocations如果不能在鏈接時確定值,就需要轉換成dynamic
relocations在運行時由ld.so計算(type和.o中相同)。
如果作用的section沒有SHF_WRITE標誌,ld.so就得臨時執行mprotect變更memory
maps的權限、修改、再還原之前的只讀權限,這樣就妨礙了page sharing。
Shared objects形成text relocations的情況比可執行檔多。 可執行檔有canonical PLT和copy relocations可以避免某些text relocations。
不同鏈接器在不同架構上允許的text relocations的relocation
types不同。GNU ld會允許一些glibc ld.so支持的types。
在x86-64上,鏈接器都會允許R_X86_64_64和R_X86_64_PC64。
下面的彙編程序裏defined_in_so是定義在某個shared
object的符號。註釋裏給出每種text relocation的場景。
1 | .globl global |
在-no-pie或-pie模式下,根據defined_in_so的符號類型,鏈接器會作出不同選擇:
STT_FUNC: 產生canonical PLTSTT_OBJECT: 產生copy relocationSTT_NOTYPE:GNU ld會產生copy relocation。ld.lld會產生text relocation
Section相關
--gc-sections
在編譯時指定-ffunction-sections或-fdata-sections才能生效。
鏈接器會進行liveness分析,從輸出中移除未使用的sections。
-z start-stop-gc
and -z nostart-stop-gc
-z start-stop-gc意味着來自live
section的__start_foo或__stop_foo引用不會保留所有foo
input sections。
-z nostart-stop-gc意味着來自live
section的__start_foo或__stop_foo引用會保留所有foo
input sections。
詳見Metadata sections, COMDAT and SHF_LINK_ORDER。
--icf=all and
--icf=safe
啟用identical code folding。 名稱源於MSVC linker
/OPT:ICF,其中"ICF"代表"identical COMDAT
folding"。gold將其命名爲"identical code folding"。
這個名稱有些誤導性:
- 該功能操作的是sections而不是functions。
- 該功能也適用於只讀數據。
我們定義identical sections爲具有相同內容且其outgoing
relocation集不可區分的sections:
它們需要有相同數量的relocations,在相同的相對位置,且被引用的符號不可區分。
這是一個遞迴定義:如果.text.a和.text.b在相同位置引用不同符號,如果被引用的符號滿足identical
code/rodata要求,它們仍然可以不可區分。
在一組identical
sections中,鏈接器可能保守地抑制某些sections的folding。
--keep-unique=<symbol>使定義<symbol>的section唯一。
在ld.lld中,只讀section預設可folding(gold不fold只讀數據)。然而,定義.dynsym符號的只讀section不可以。
對於其餘sections,在一組identical sections中,鏈接器選擇一個代表並丟棄其餘,然後將引用重定向到代表。
gold基於relocation實現--icf=safe。
ld.lld --icf=safe使用由Clang
-faddrsig生成的特殊section .llvm_addrsig(LLVM
address significance table,類型SHT_LLVM_ADDRSIG)。
截至2023-01,-faddrsig在大多數Linux目標上是預設的,但在Android、Gentoo和-fintegrated-as上被禁用。
如果該section不存在,ld.lld會保守地假設表中定義符號的每個section都是地址顯著的。
SHT_LLVM_ADDRSIG將符號索引編碼爲ULEB128。objcopy、ld -r和其他二進制操作工具可能會更改符號表。
一個有趣的特性是objcopy和ld -r會將已知section類型SHT_LLVM_ADDRSIG的sh_link設置爲0。
ld.lld使用sh_link!=0來檢查有效性,並在sh_link==0時報告警告。
我有點遺憾設計權衡傾向於代碼大小而不是通用性,以及目前一些Linux發行版在Clang
Driver中預設-faddrsig而其他則預設-fno-addrsig的狀態。
如果我們使用R_*_NONE
relocations(並使用REL而不是RELA來減少大小膨脹)來編碼符號索引可能會更好。
那或許意味着我們應該預設-fno-addrsig並讓用戶選擇啓用該功能。
lld的Mach-O port選擇爲__DATA,__llvm_addrsig使用基於relocation的表示。
ld.lld --icf=all忽略.llvm_addrsig。
對於以下代碼的-shared鏈接 1
2int foo() { return 1; }
int bar() { return 1; }
foo和bar在.dynsym中。
ld.lld --icf=safe假設.dynsym中的符號是地址顯著的,兩個符號不能共享相同地址,所以ld.lld保守地抑制合併.text.foo和.text.bar。
gold --icf=safe會合併.text.foo和.text.bar。如果程序使用map並期望map[dlsym(h, "foo")]和map[dlsym(h, "bar")]解析爲不同對象,這種選擇將是不安全的。
在LLVMCodeGen中,具有{,local_}unnamed_addr屬性的global
value不會進入.llvm_addrsig。
--icf=all放棄了C++語言關於指針相等的保證。
有些人認爲這是公平的,因爲保證的某些部分無論如何都被破壞了(-fvisibility-inlines-hidden)。
參見ELF
interposition and -Bsymbolic。
在https://reviews.llvm.org/D141310中,提出了一個opt-in
Clang診斷-Wcompare-function-pointers來捕獲會導致--icf=all失敗的一些問題。
ICF會使調試更加困難,因爲調試器可能無法區分合併的實例。
- 與合併函數相關的調試信息本質上被重定向。
- 在一個函數上設置斷點也會影響合併的函數。
ICF可能會改變堆棧跟蹤中的函數名稱並使分析不準確。
如果啓用DW_AT_LLVM_stmt_sequence並使用調用者來消除合併函數中地址的歧義,可以緩解調試信息的退化。
https://github.com/llvm/llvm-project/pull/139493#issuecomment-2896493771
當有以下情況時:
- Section A1,在符號S1上有relocation,其中S1在section B1的偏移K處。
- Section A2,在符號S2上有relocation,其中S2在section B2的偏移K處。
當relocation類型是R_AARCH64_ADR_GOT_PAGE,且sections
B1和B2被合併而保持符號S1和S2分開時,sections
A1和A2目前可以被合併,這會導致正確性問題。
GC roots:
--entry/--init/--fini/-u指定的所有定義符號所在的sections- Linker script表達式被引用的定義符號所在的sections
.dynsym中的所有定義符號所在的sections- 類型爲
SHT_PREINIT_ARRAY/SHT_INIT_ARRAY/SHT_FINI_ARRAY - 名稱爲
.ctors/.dtors/.init/.fini/.jcr - 不在section group中的
SHT_NOTE(這個section group規則是爲了Fedora watermark) - 被
.eh_frame引用的personality routines和language-specific data area
-z start-stop-gc
and -z nostart-stop-gc
-z start-stop-gc表示來自live
section的__start_foo或__stop_foo引用不會保留所有foo
input sections。
-z nostart-stop-gc表示來自live
section的__start_foo或__stop_foo引用會保留所有foo
input sections。
詳見Metadata sections, COMDAT and SHF_LINK_ORDER。
--symbol-ordering-file=<file>
指定一個文本文件,每行一個定義的符號。 在input section
description(例如*(.text .text.*))內,對匹配的input
sections排序:如果符號A在ordering
file中位於符號B之前,則將定義A的section放在定義B的section之前。
如果一個符號未定義,或者所在的section被丟棄,鏈接器會輸出warning,除非指定了--no-warn-symbol-ordering。
然而,預設的--no-warn-symbol-ordering似乎經常會妨礙使用。
--symbol-ordering-file=主要用於兩個目標:性能或壓縮。
如果一個函數頻繁調用另一個,在linked image中如果讓兩個函數所在的input sections接近,可以增大它們落在同一個page的概率。 通過考慮函數之間的引用並將相關函數放在一起,可以減小page working set並減少TLB thrashing。參見Karl Pettis and Robert C. Hansen的 Profile Guided Code Positioning。
移動應用通常優先考慮壓縮後的代碼大小。 對於冷函數,它們的壓縮大小比性能重要得多。 爲了改善壓縮大小,可以將相似的函數分組在一起,以增強壓縮算法(如Lempel-Ziv系列)。
這個選項是ld.lld特有的。gold有一個--section-ordering-file,根據section
name排序。 實踐中text和data
sections大多有不同的名字。然而,clang -fno-unique-section-names(GCC feature
request)可以創建同名sections,從而使--section-ordering-file失效。
1 | cat > a.s <<e |
1 | % readelf -x .rodata a |
GNU ld 2.43引入了具有不同語義的--section-ordering-file。
section ordering script必須指定已在linker script中定義的output
sections。 指定的額外映射將被前置到output section。 1
2
3
4cat > b.txt <<e
.rodata : { *(.rodata.3) *(.rodata.[2]) *(.rodata.1) }
e
ld.bfd --section-ordering-file=b.txt a.o -o a
--call-graph-profile-sort
當--call-graph-profile-sort(預設)生效時,ld.lld會檢查輸入relocatable
object files中的SHT_LLVM_CALL_GRAPH_PROFILE sections(call
graph profile)。 SHT_LLVM_CALL_GRAPH_PROFILE
section由(from_symbol, to_symbol, weight)元組組成。
LLD利用這些信息計算一個以input sections爲節點、(from_section,
to_section, weight)爲邊的call graph,然後在input section
description內排序sections。 排序算法基於_Optimizing Function Placement
for Large-Scale Data-Center Applications_。
LLD按密度遞減排序input sections,其中密度計算爲weight除以size。 最初,每個input section單獨放在一個cluster中。 處理每個input section時,其cluster被附加到包含其在call graph中最可能前驅的cluster。 如果滿足以下任何條件,合併可能被阻止:
- 邊不太可能(考慮到輸入邊權重的總和,邊權重太小)。
- 兩個clusters的總大小大於閾值。
- 合併後的密度會使前驅cluster的密度大大降低。
最後,clusters按密度遞減排序。
如果指定了--symbol-ordering-file=,--symbol-ordering-file=指定的sections會先放置。
call graph profile仍用於其他sections(lld >= 20)。
當同時指定--call-graph-profile-sort和--print-symbol-order=時,ld.lld會將符號順序輸出到指定文件。
該文件可以與--symbol-ordering-file=一起使用。
--bp-compression-sort=
and --bp-startup-sort=
這兩個選項指示鏈接器優化section佈局,目標如下:
--bp-compression-sort=[data|function|both]:通過將相似sections分組在一起來改善Lempel-Ziv壓縮,從而減小壓縮後的應用大小。--bp-startup-sort=function --irpgo-profile=<file>:利用temporal profile文件來減少程序啓動期間的page faults。
鏈接器通過考慮三組來確定section順序:
- 根據temporal profile(
--irpgo-profile=)排序的function sections,優先考慮早期訪問和頻繁訪問的函數。 - Function sections。包含相似函數的sections被放在一起,最大化壓縮機會。
- Data sections。相似的data sections被放在一起。
在每組內,sections使用Balanced Partitioning算法排序。
鏈接器構建一個二分圖,有兩組頂點:sections和utility頂點。
- 對於profile引導的function sections:
- utility頂點的數量由profile文件中的符號順序決定。
- 如果指定了
--bp-compression-sort-startup-functions,會分配額外的utility頂點以優先考慮附近函數的相似性。
- 對於爲壓縮排序的sections:utility頂點通過分析section內容和relocations的k-mers來確定。
在此優化期間,call graph profile被禁用。
當指定--symbol-ordering-file=時,該文件中描述的sections會更早放置。
-z nosectionheader
GNU ld 2.41引入此選項,用於省略section header table。
--unique
預設情況下,鏈接器將所有具有相同名稱的input sections合併到單個output
section中。 例如,來自不同object files的所有.text
sections會合併成一個.text output section。
GNU ld的--unique選項爲每個orphan section創建單獨的output
sections。 注意,使用-r(relocatable output)時,內部linker
script仍會導致某些sections如.text和.debug_info被合併。
爲了更精細的控制,使用--unique=glob來匹配特定模式——例如,--unique=*匹配所有sections。
ld.lld只實現了--unique形式,適用於所有sections。在沒有linker
script的情況下,它將所有input sections視爲orphans。
分析相關
--cref
輸出cross reference table。對於每一個non-local符號,輸出定義的文件和被引用的文件列表。
-M and
-Map=<file>
輸出link map,可以查看output sections的地址、文件偏移、包含的input sections。
Warning相關
--fatal-warnings
把warnings轉成errors。Warning和error的差別除了是否包含warning或error字串外更重要的一點是,error會阻止輸出鏈接結果。
--noinhibit-exec
把部分errors轉成warnings。注意不要指定--fatal-warnings把降級的warnings再升級爲errors:)
--no-warnings
抑制warnings。因--fatal-warnings而轉為errors的warnings不受影響。
其他
--build-id=value
生成.note.gnu.build-id,給輸出一個標識符。標識符通過對整個輸出進行散列生成。
SHA-1是最常見的選擇。鏈接器會給.note.gnu.build-id的內容填零,散列每個字節後把結果填回.note.gnu.build-id。
一些鏈接器使用tree-style散列以實現並行化。
--compress-debug-sections=[zlib|zstd]
用zlib或zstd壓縮輸出文件的.debug_*
sections,並標記SHF_COMPRESSED。 參見Compressed debug
sections。
--format=binary,
-b binary
每個輸入文件被視爲一個ELF文件,其.data
section包含文件的原始二進制內容。
定義符號_binary_<filename>_{start,end,size}以提供對嵌入數據的訪問。
1 | echo hello > a.txt |
輸出: 1
2
3
4a.out: file format elf64-x86-64
Contents of section .data:
0000 68656c6c 6f0a hello.
輸入文件的轉換方式類似於以下objcopy操作:
1 | # 兼容llvm-objcopy |
--hash-style=style
ELF規範要求一個用於dynamic symbol lookup的hash table
DT_HASH。--hash-style=sysv生成該表。
DT_GNU_HASH在空間佔用和性能上都優於DT_HASH。
mips使用替代方案DT_MIPS_XHASH(mips
ABI設計聰明反被聰明誤的好例子)。
我個人認爲DT_MIPS_XHASH在解決一個錯誤的問題。實際上有辦法用DT_GNU_HASH,但可能mips社區的人不想再折騰了。
參見glibc and DT_GNU_HASH瞭解關於"Easy Anti-Cheat"的故事。
--no-ld-generated-unwind-info
參見PR12570 .plt has no associated .eh_frame/.debug_frame。
PC在PLT
entry中時,如果鏈接器不合成.eh_frame信息,從當前PC
unwind將無法獲得frames。 在i386和x86-64上,lazy binding狀態下,一個PLT
entry的首次調用會執行push指令。在ESP/RSP改變後,如果PLT
entry沒有.eh_frame提供的unwind信息,unwinder可能會無法正確unwind,影響profiler精度。
1 | jmp *got(%rip) |
然而,由於-Wl,-z,relro,-z,now(BIND_NOW)的普遍使用,這個功能如今已經基本過時。
PLT
entries的行爲像沒有prologue的函數。profiler可以使用預設規則輕鬆檢索返回地址:如果一個代碼區域沒有被metadata覆蓋,假設返回地址在*rsp處可用(x86-64)。
要識別PLT名稱,profiler需要:
- 解析
.pltsection以識別PLT entries的區域 - 解析
.rel[a].plt以獲取R_*_JUMP_SLOTdynamic relocations及其引用的符號名 - 如果當前PC在PLT區域內,解析附近的指令並找到GOT
load。關聯的
R_*_JUMP_SLOT標識符號名 - 連接符號名和
@plt形成foo@plt
注意:foo@plt是objdump等工具使用的慣例,但object
file中不包含這樣的符號。
gdb有heuristics可以識別這種情況。
這個問題不會影響C++ exception。PLT entry是tail
call,__cxa_throw調用的_Unwind_RaiseException會穿透ld.so
resolver和PLT entry的tail calls。 PC會還原爲PLT
entry的caller的下一條指令。
1 | // b.cc - b.so |
-O
啓用大小優化。 優化等級和編譯器driver選項-O不同。
-O不影響--lto-O:對LTO代碼生成沒有效果。
在ld.lld中,-O1是預設值。
-O0禁用SHF_MERGE的常量合併。
-O2啓用一些計算量大的大小優化:
- 啓用
SHF_MERGE|SHF_STRINGS的string suffix merge。這非常慢且不並行。 --compress-debug-sections=zlib使用較高壓縮比的zlib壓縮。- 自14.0.0起,在
.strtab中去重local符號名。一旦ld.lld支持並行.symtab寫入,我可能會完全移除這個功能。
在GNU
ld中,非零-O可以使.hash和.gnu.hash更小。
對於引用SHF_MERGE
section的符號賦值,它被認爲引用常量數據元素。
在消除重複後,符號值被調整爲引用output section中的數據元素。
-plugin file
GNU ld和gold支持這個選項加載GCC
LTO插件(liblto_plugin.so)或LLVM
LTO插件(LLVMgold.so)
clang -flto={full,thin}會傳遞-plugin path/to/LLVMgold.so,除非使用-fuse-ld=lld。
插件的API接口由binutils-gdb/include/plugin-api.h定義。
注意,LLVMgold.so的名稱含gold,但也能用於GNU binutils
(ld, gold, nm, ar)和mold。
--verbose
GNU ld會輸出linker script(內部或外部)。gold、ld.lld和mold不是linker script驅動的,沒有linker script輸出。
隨機性相關
--shuffle-sections=<seed>
隨機打亂input sections,用於發現依賴特定section順序的bug。
--randomize-section-padding=<seed>
使用給定的seed在input sections之間和每個segment的開頭隨機插入padding。
假設某個改動無意中降低了一個頻繁執行的函數的內存對齊。雖然原程序可能沒有保證這個函數的對齊,但這個改動可能會加劇問題。使用--randomize-section-padding可以通過引入內存佈局的變化來發現這類微妙的性能退化。
地址相關
-Ttext-segment
text
segment傳統上是第一個segment。指定-Ttext-segment的用戶可能實際上想指定image
base。
當與-z separate-code一起使用時,該選項有奇怪的語義(可能是bug):https://sourceware.org/bugzilla/show_bug.cgi?id=25207。
ld.lld提供--image-base來設置image base。
GNU ld的PE/COFF端口很早就支持--image-base,並在binutils
2.44版本中為ELF實現了該選項。
這個選項似乎主要用於mmap
MAP_FIXED的使用,以避免與ASLR衝突。
更好的替代方案是避免設置固定地址。qemu的linux-user/elfload.c:probe_guest_base可能會提供一些見解。