解析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: 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
(允许)。
对于如下代码,链接可执行档时会报错:
// 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_b ackrefs.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的场景。
.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);