添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
首发于 MaskRay

解析GNU风味的linker options

原文: 解析GNU风味的linker options

(首先庆祝一下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符号被保留的条件是:

if ((--emit-reloc or -r) && referenced) || --discard-none
  return true
if --discard-all
  return false
if --discard-locals
  return is not .L
# No --discard-* is specified.
return not (.L in a SHF_MERGE section)

--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 live section引用的符号且non-weak。也就是说,weak定义仍可能被认为是unneeded。--gc-sections丢弃的section的引用不算

-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: sourceware.org/bugzilla

--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 (允许)。

对于如下代码,链接可执行档时会报错:

// 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特有,参见 lld.llvm.org/ELF/warn_b

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的场景。

.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的差别除了是否包含 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精度。

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的下一条指令。

// 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);