Explain GNU style linker options

English version

2025-02更新

(首先庆祝一下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上(即将退出历史舞台的架构)两种模式有代码生成差异。大多数架构没有差异。

输入文件

链接器接受几类输入。对于符号,每个输入文件的符号表都会影响符号解析;对于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_GOTPCRELXR_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_WEAK STT_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.lllld/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-listGNU ld 2.35ld.lld 14起实现。

在以下示例中,当var是preemptible时,会看到GLOB_DAT dynamic relocation。

1
2
3
// a.c
int var;
int inc() { return ++var; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# shared object中预设是preemptible的。
% clang -O2 -fpic -shared a.cc && readelf -Wr a.out | grep var
0000000000003fc8 0000000500000006 R_X86_64_GLOB_DAT 000000000000400c var + 0

# -Bsymbolic使定义变为non-preemptible。
% clang -O2 -fpic -shared -Bsymbolic a.cc && readelf -Wr a.out | grep var

# 尽管有-Bsymbolic,--export-dynamic-symbol使定义变为preemptible
% clang -O2 -fpic -shared -Wl,-Bsymbolic,--export-dynamic-symbol=var a.cc && readelf -Wr a.out | grep var
0000000000003fc8 0000000500000006 R_X86_64_GLOB_DAT 000000000000400c var + 0

# 没有symbolic意图选项时,--export-dynamic-symbol对-shared无效。
% clang -O2 -fpic -shared -Wl,--export-dynamic-symbol=foo a.cc && readelf -Wr a.out | grep var
0000000000003fc8 0000000500000006 R_X86_64_GLOB_DAT 000000000000400c var + 0

# --dynamic-list隐含-Bsymbolic。
% clang -O2 -fpic -shared -Wl,--dynamic-list=<(printf '{a;};') a.cc && readelf -Wr a.out | grep var

# 匹配的符号仍然是preemptible的。
% clang -O2 -fpic -shared -Wl,--dynamic-list=<(printf '{var;};') a.cc && readelf -Wr a.out | grep var
0000000000003fc8 0000000500000006 R_X86_64_GLOB_DAT 000000000000400c var + 0

如果version script中local:匹配的符号被dynamic list指定,version script优先,符号会变为local。

--discard-none, --discard-locals, and --discard-all

如果生成.symtab,live section中定义的local符号被保留的条件是:

1
2
3
4
5
6
7
8
if ((--emit-relocs 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)

这些.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
4
v1 {
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 containing a period represents a vendor-specific version or portion of the entity named by the prior to the first period. There is no restriction on the characters that may be used in the suffix following the period." 在GNU中,这用于表示function cloning。

1
2
3
% c++filt <<< $'_ZL3foov\n_ZL3foov.1'
foo()
foo() [clone .1]

作为替代方案,我建议FGASLR开发者使用STT_FILE符号:

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

--start-group and --end-group

如果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。很多情况下重复一次就够了,但是假如链接第一个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.old ... --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.solibfoo.a
  • INPUTGROUP中的foo在sysroot目录下查找foo
  • 如果一个linker script在sysroot目录下,当它打开绝对路径文件(INPUTGROUP)时,在绝对路径前加上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-archiveld 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++.alibc++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_PURECODE section flag来指定纯程序指令且无数据的sections。
  • AArch64使用SHF_AARCH64_PURECODE section 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_LOAD
    • PT_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/D58892PT_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_RELRO segment和关联的RW PT_LOAD segment的结尾被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_ALIGNDATA_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_*_RELATIVER_*_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_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。ld.lld会产生text relocation

Section相关

--gc-sections

在编译时指定-ffunction-sections-fdata-sections才能生效。 链接器会进行liveness分析,从输出中移除未使用的sections。

详见Linker garbage collection

-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。objcopyld -r和其他二进制操作工具可能会更改符号表。 一个有趣的特性是objcopyld -r会将已知section类型SHT_LLVM_ADDRSIGsh_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
2
int foo() { return 1; }
int bar() { return 1; }

foobar.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

详见Linker garbage collection

-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-namesGCC feature request)可以创建同名sections,从而使--section-ordering-file失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cat > a.s <<e
.section .rodata.0,"a",@progbits; .byte 0x0
.section .rodata.1,"a",@progbits; .byte 0x1
.section .rodata.2,"a",@progbits; .byte 0x2
.section .rodata.3,"a",@progbits; .byte 0x3
.section .rodata.4,"a",@progbits; .byte 0x4
e
cat > a.txt <<e
.rodata.3
.rodata.[2]
.rodata.1
e
as a.s -o a.o
gold --section-ordering-file=a.txt a.o -o a
1
2
3
4
% readelf -x .rodata a

Hex dump of section '.rodata':
0x004000b0 00040302 01 .....

GNU ld 2.43引入了具有不同语义的--section-ordering-file。 section ordering script必须指定已在linker script中定义的output sections。 指定的额外映射将被前置到output section。

1
2
3
4
cat > 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的差别除了是否包含warningerror字串外更重要的一点是,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
2
3
echo hello > a.txt
ld.bfd -r -b binary -m elf_x86_64 a.txt
objdump -s a.out

输出:

1
2
3
4
a.out:     file format elf64-x86-64

Contents of section .data:
0000 68656c6c 6f0a hello.

输入文件的转换方式类似于以下objcopy操作:

1
2
3
4
5
# 兼容llvm-objcopy
objcopy -I binary -O elf64-x86-64 a.txt a.o

# 仅GNU objcopy
objcopy -I binary -O default a.txt a.o

--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
2
3
jmp *got(%rip)
pushq $0x0
jmpq .plt

然而,由于-Wl,-z,relro,-z,now(BIND_NOW)的普遍使用,这个功能如今已经基本过时。 PLT entries的行为像没有prologue的函数。profiler可以使用预设规则轻松检索返回地址:如果一个代码区域没有被metadata覆盖,假设返回地址在*rsp处可用(x86-64)。

要识别PLT名称,profiler需要:

  • 解析.plt section以识别PLT entries的区域
  • 解析.rel[a].plt以获取R_*_JUMP_SLOT dynamic 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
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不同。 -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可能会提供一些见解。

目标特定

--cmse-implib, --out-implib=out.lib

参见Linker notes on AArch32