(首先庆祝一下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上(即将退出历史舞台的架构)两种模式有代码生成差异。大多数架构没有差异。
模式
以下四种链接模式四选一,控制输出文件的类型(可执行档/shared object/relocatable object):
-no-pie
(default): 生成position-dependent executable (ET_EXEC
)。要求最宽松,源文件可用-fno-pic,-fpie,-fpic
编译-pie
: 生成position-independent executable (ET_DYN
)。源文件须要用-fpie,-fpic
编译-shared
: 生成position-independent shared object (ET_DYN
)。最严格,源文件须要用-fpic
编译-r
: relocatable link,保留relocations
-pie
可以和-shared
都是position-independent的链接模式。-pie
也可以和-no-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,(LLD行为)不生成dynamic relocation。GNU ld是否生成dynamic relocation有非常复杂的规则,且和架构相关
容易产生混淆的是,编译器driver提供了几个同名选项:-no-pie,-pie,-shared,-r
。 GCC 6引入了configure-time选项--enable-default-pie
:启用该选项的GCC预设-pie
和-fPIE
。现在,很多Linux发行版都启用了该选项作为基础的security hardening。
符号相关
--exclude-libs
If a matched archive defines a non-local symbol, don't export this symbol.
--export-dynamic
Shared objects预设导出所有non-local STV_DEFAULT/STV_PROTECTED
定义符号到dynamic symbol table。可执行档可用--export-dynamic
模拟shared objects行为。
下面描述可执行档/shared object里一个符号被导出的规则(logical AND):
- non-local
STV_DEFAULT/STV_PROTECTED
(this means it can be hid by--exclude-libs
) - logical OR of the following:
- undefined
- (
--export-dynamic
||-shared
) && ! (unnamed_addr linkonce_odr GlobalVariable || local_unnamed_addr linkonce_odr constant GlobalVariable) - matched by
--dynamic-list/--export-dynamic-symbol-list/--export-dynamic-symbol
- defined or referenced by a shared object as
STV_DEFAULT
STV_PROTECTED
definition in a shared object preempted by copy relocation/canonical PLT when--ignore-{data,function}-address-equality}
is specified-z ifunc-noplt
&& has at least one relocation
如果可执行档定义了在某个链接时shared object引用了一个符号,那么链接器需要导出该符号,使得运行时该shared object的undefined符号可以绑定到可执行档中的定义。
-Bsymbolic
and --dynamic-list
ELF中,non-local STV_DEFAULT
的定义的符号在一个shared object预设会被preempt(interpose),即运行时该定义可能被可执行档或另一个shared object中的定义替换。 可执行档中的定义是保证non-preemptible (non-interposable)的。 -fPIC
编译的程序被认为可能用于shared object,引用模块(一个可执行档或一个shared object被称为一个模块)内的定义预设会有不必要的开销:GOT或PLT的间接引用开销。
链接器提供了-Bsymbolic
、-Bsymbolic-functions
、version script和--dynamic-list
等几种机制使部分符号non-preemptible,获得和与-no-pie,-pie
相似的行为。
-Bsymbolic
: 所有定义的符号non-preemptible-Bsymbolic-functions
: 所有定义的STT_FUNC
(函数)符号non-preemptible--dynamic-list
: 蕴含-Bsymbolic
,但被列表匹配的符号仍为preemptible。--dynamic-list
也可用于-no-pie/-pie
,但含义不同,表示导出部分符号。我认为--dynamic-list
设计成双重含义容易产生困惑和误用
上述选项会使很多符号non-preemptible。GNU ld 2.35和LLD 11可以用--export-dynamic-symbol=glob
使部分符号保持原来的preemptible状态。GNU ld 2.35另外提供--export-dynamic-symbol-list
。
--discard-none
, --discard-locals
, and --discard-all
如果输出.symtab
,一个live section里定义的local符号被保留的条件是:
1 | if ((--emit-reloc or -r) && referenced) || --discard-none |
--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)。
Library相关
--as-needed
and --no-as-needed
防止一些没有用到的链接时shared objects留下DT_NEEDED
。
--as-needed
和--no-as-needed
是position-dependent选项(非正式叫法,但没找到更贴切的形容词),影响后面命令行出现的shared objects。一个shared object is needed,如果下面条件之一成立:
- 在命令行中至少一次出现在
--no-as-needed
模式下 - 定义了一个被.o引用的符号且non-weak。也就是说,weak定义仍可能被认为是unneeded
-Bdynamic
and -Bstatic
这两个选项是position-dependent选项,影响后面命令行出现的-lname
。
-Bdynamic
(default):在-L
指定的目录列表中查找libfoo.so
和libfoo.a
-Bstatic
:在-L
指定的目录列表中查找libfoo.a
注意,历史上-Bstatic
和-static
同义。编译器driver的-static
是个不同的选项,除了传递-static
给ld外,还会去除预设的--dynamic-linker
,影响libgcc libc等的链接。
--no-dependent-libraries
忽略object files里的.deplibs
section。
-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
GNU ld和gold在处理一个archive时,若该archive不能满足之前的某个undefined符号,则跳过该archive。详见--warn-backrefs
。如果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
,另一种则是main.o --start-group A.a B.a --end-group
。
--start-lib
and --end-lib
gold发明的很有用的功能。下文的--whole-archive
用于.a,而--start-lib
则用于.o
: 使regular object files有类似archive files的语义(按需加载)。
ld ... --start-lib b.o c.o --end-lib
作用类似ld ... a.a
,如果a.a
包含b.o c.o
。
我提交了一个GNU ld的feature request:https://sourceware.org/bugzilla/show_bug.cgi?id=24600
--sysroot
和GCC/Clang driver的--sysroot
不同。如果一个linker script在sysroot目录下,它打开绝对路径文件(INPUT
or GROUP
)时,在绝对路径前加上sysroot。
--whole-archive
and --no-whole-archive
链接器接受几类输入。对于符号,每个输入文件的符号表都会影响符号解析;对于sections,只有regular object files里的sections(称为input sections)会拼接得到输出文件的output sections。
- .o (regular object files)
- .so (shared objects): 只影响符号解析
- .a (archive files)
.a是特殊的,它们是一种惰性的输入文件,预设不会往输出贡献input sections。 如果链接器发现.a中的某个archive member定义了某个之前被引用但尚未定义的符号,则会从archive中pull out这个member。 该member会在概念上成为一个regular object file,之后的处理方式就和.o没有任何差异了。
在--whole-archive
之后的.a会当作.o一样处理,没有惰性语义。 如果a.a
包含b.o c.o
,那么ld --whole-archive a.o --no-whole-archive
和ld b.o c.o
作用相同。
--push-state
and --pop-state
-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
。
依赖关系相关
-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的不能解析的undefined符号,是否报错。可执行档预设为--no-allow-shlib-undefined
(不允许),而shared objects预设为--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和LLD使用一个简化的规则:如果一个shared object的所有DT_NEEDED
依赖都被直接链接了,则启用报错;如果部分依赖没有被链接,那么gold/LLD无法准确判断是否一个未被直接链接的shared object能提供定义,就保守地不报错。
值得一提的是,-z defs/-z undefs/--no-undefined
和--[no-]allow-shlib-undefined
可以被一个选项--unresolved-symbols
控制。
--warn-backrefs
LLD特有,参见http://lld.llvm.org/ELF/warn_backrefs.html。
Layout相关
--no-rosegment
LLD采用两个RW PT_LOAD
的设计:
- R
PT_LOAD
- RX
PT_LOAD
- RW
PT_LOAD
(和PT_GNU_RELRO
重叠) - RW
PT_LOAD
指定该选项可以合并R PT_LOAD
和RX PT_LOAD
。
-z separate-loadable-segments
LLD传统布局:所有PT_LOAD
segments都没有重叠(一个字节不会被同时加载到两个memory mappings)。
实现方式是每个新PT_LOAD
的地址对齐到max-page-size。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_LOAD
- 前缀部分为
PT_GNU_RELRO
- 非
PT_GNU_RELRO
的部分
- 前缀部分为
separate-code
的含义是文件中一个被映射到可执行段的字节(RX PT_LOAD
)不会被同时映射到一个R PT_LOAD
。 注意RX后的R是不忧的,理想情况是把这个R和第一个R合并,但似乎在GNU ld里实现会很困难。
我在LLD 10引入该选项,语义和GNU ld类似但布局不同(没有必要模仿两个R的非优布局):两个RW PT_LOAD
允许重叠,也就是说第二个PT_LOAD
的地址不用对齐,最多可浪费max-page-size*2字节。
-z noseparate-code
经典布局,允许可执行段和其他PT_LOAD
重叠。GNU ld通常用:
- RX
PT_LOAD
- RW
PT_LOAD
- 前缀部分为
PT_GNU_RELRO
。这部分在ld.so解析完dynamic relocations后mprotect成readonly - 非
PT_GNU_RELRO
的部分。这部分在运行时始终可写
- 前缀部分为
第一个PT_LOAD
常被笼统的称为text segment,实际上不准确:非执行部分的rodata也在里面。
LLD 10中预设使用这种布局,不需要对齐任何PT_LOAD
。
Relocation相关
--apply-dynamic-relocations
对于psABI采用RELA的architectures(AArch64,PowerPC,RISC-V,x86-64,etc),因为dynamic relocations包含addend字段,链接器在被relocate的地址填上0,而不是addend值。 如果可执行档/shared objects使用压缩,能稍稍利于压缩。
--emit-relocs
可用于-no-pie/-pie/-shared
获得类似-r
的效果:保留输入的relocations。可用于链接后的二进制分析,我知道的唯二用途是Linux kernel x86的CONFIG_RELOCATABLE
和BOLT。
--pack-dyn-relocs=value
relr
可以启用DT_RELR
,一种更加紧凑的relative relocation (R_*_RELATIVE
)编码方式。Relative relocations常见于-pie
链接的可执行档。
-z text
and -z notext
-z text
不允许text relocations。 -z notext
允许text relocations。
binutils 2.35起,Linux/x86上的GNU ld预设启用configure-time选项--enable-textrel-warning=warning
,若有text relocations会给出warning。
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。LLD会产生text relocation
Section相关
--gc-sections
非常常见的选项。编译时指定-ffunction-sections
或-fdata-sections
才有效果。链接器会做liveness analysis从输出中去除没有用的sections。
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
--icf=all
--icf=safe
启用Identical Code Folding。这个名称其实不准确:说是code,其实适用于一切readonly data;合并的单位是section,而不是函数。 作用是合并功能相同的text sections/rodata sections。
gold实现了基于relocation的--icf=safe
;LLD实现了基于LLVM address significance table的--icf=safe
。
--symbol-ordering-file=file
指定一个文本文件,每行一个定义的符号。如果符号A在符号B前面,那么在每一个input section description进行排序,A所在的section排在B所在的section前面。
如果一个符号未定义,或者所在的section被丢弃,链接器会输出一个warning,除非指定了--no-warn-symbol-ordering
。
如果一个函数频繁调用另一个,在linked image中如果让两个函数所在的input sections接近,可以增大它们落在同一个page的概率,减小page working set及减少TLB thrashing。参见Karl Pettis and Robert C. Hansen的 Profile Guided Code Positioning
这个选项是LLD特有的。gold有一个--section-ordering-file
,根据section name排序。实践中要求text/data sections具有不同的名字(不可使用clang -funique-section-names
)。 而基于符号名排序则可以使用-funique-section-names
。
分析相关
--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:)
其他
--build-id=value
生成.note.gnu.build-id
,标识一个链接结果。一般用SHA-1。链接器会给.note.gnu.build-id
的区域填零,散列每个字节后把结果填回.note.gnu.build-id
。 每个链接器用的计算方式各有不同。
--compress-debug-sections=zlib
用zlib压缩输出文件的.debug_*
sections,并标记SHF_COMPRESSED
。SHF_COMPRESSED
是合并入ELF specification的最后一个feature,之后ELF specification就处于不被维护的状态……
--hash-style
--hash-style=sysv
指定ELF specification定义的DT_HASH
,一个用于加速符号解析的hash table。 DT_GNU_HASH
在空间占用和效率都优于DT_HASH
。 指的一提的是Mips有个DT_MIPS_XHASH
(Mips ABI设计聪明反被聪明误的好例子),我个人觉得在解决一个错误的问题。实际上有办法用DT_GNU_HASH
,但可能Mips社区的人觉得东西塞进去了就不想多管了。
--no-ld-generated-unwind-info
参见PR12570 .plt has no associated .eh_frame/.debug_frame。
PC在PLT entry中时,如果链接器不合成.eh_frame
信息,unwinder可能会无法正确unwind。 在i386和x86-64上,lazy binding状态下,一个PLT entry的首次调用会执行push指令。在ESP/RSP改变后,如果PLT entry没有.eh_frame
提供的unwind信息,unwinder可能会无法正确unwind,影响profiler精度。
1 | jmp *got(%rip) |
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
不同。
在LLD中,-O0
禁用SHF_MERGE
的常量合并;-O2
启用SHF_MERGE|SHF_STRINGS
的string suffix merge,--compress-debug-sections=zlib
使用较高压缩比的zlib压缩。
-plugin file
GNU ld和gold支持这个选项加载GCC LTO插件(liblto_plugin.so
)或LLVM LTO插件(LLVMgold.so
)
插件的API接口由binutils-gdb/include/plugin-api.h
定义。
注意,LLVMgold.so
的名称含gold,但也能用于GNU ld、nm和ar。