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可能会提供一些见解。