解析GNU风味的linker options

(首先庆祝一下LLVM 2000 commits达成!)

编译器driver options

在描述链接器选项前先介绍一下driver options。通常使用gccclang,指定的都是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会传递--foovalue--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。

符号相关

--export-dynamic

Shared objects预设导出所有non-local STV_DEFAULT/STV_PROTECTED定义符号到dynamic symbol table。可执行档可用--export-dynamic模拟shared objects行为。

下面描述可执行档里一个符号被导出的规则(logical AND):

  • non-local STV_DEFAULT/STV_PROTECTED
  • 未定义,或指定了--export-dynamic,或被--dynamic-list/--export-dynamic-symbol-list/--export-dynamic-list匹配,或被某个链接时shared object的STV_DEFAULT undefined symbol引用

最后一个条件的意义是:如果可执行档定义了在某个链接时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

-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.solibfoo.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.soa.outDT_NEEDEDa.so.1。如果第一个命令不含-soname,则a.outDT_NEEDED./a.so

--start-group and --end-group

GNU ld和gold在处理一个archive时,若该archive不能满足之前的某个undefined符号,则跳过该archive。详见--warn-backrefs。如果A.aB.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

--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-archiveld 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++.alibc++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_64R_X86_64_PC64

下面的汇编程序里defined_in_so是定义在某个shared object的符号。注释里给出每种text relocation的场景。

1
2
3
4
5
6
7
.globl global
global:
local:
.quad local # (-pie or -shared) R_X86_64_RELATIVE
.quad global # (-pie) R_X86_64_RELATIVE or (-shared) R_X86_64_64
.quad defined_in_so # (-shared) R_X86_64_64
.quad defined_in_so - . # (-shared) R_X86_64_PC64

-no-pie-pie模式下,根据defined_in_so的符号类型,链接器会作出不同选择:

  • STT_FUNC: 产生canonical PLT
  • STT_OBJECT: 产生copy relocation
  • STT_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的差别除了是否包含warningerror字串外更重要的一点是,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_COMPRESSEDSHF_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
2
3
jmp *got(%rip)
pushq $0x0
jmpq .plt

GDB有heuristics可以识别这种情况。

这个问题不会影响C++ exception。PLT entry是tail call,__cxa_throw调用的_Unwind_RaiseException会穿透ld.so resolver和PLT entry的tail calls。
PC会还原为PLT entry的caller的下一条指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// b.cc - b.so
void ext() { throw 3; }

// a.cc - exe
#include <stdio.h>

void ext();
void foo() {
try {
ext(); // PLT entry
} catch (int x) {
printf("%d\n", x);
}
}

int main() {
foo();
}

-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。