ld链接器
TODO:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加
前言
ld是GNU操作系统上的连接器,把二进制文件连接成可执行文件。了解ld之前需要对可执行文件的节(section)和段(segment)有一定了解。
一、ELF文件解析
Executable and Linking Format,即可执行和可链接的格式,是Unix/Linux系统ABI (Application Binary Interface)规范的一部分。文件格式可参考官方文档:ELF FORMAT
1. 常见的ELF文件解析工具
objdump /readelf
通常使用以上工具完成,对ELF文件格式的查询,段/节查看等。比如 readelf -S/objdump -h
显示节(section);readelf -l
可以显示文件的program header信息,其中包含段(segment)以及节与段的映射信息;readelf -h 可以显示ELF文件头信息。
nm/objdump -t
查看符号表
对于每一个符号来说,其类型如果是小写的,则表明该符号是local的;大写则表明该符号是global(external)的。
类型 D 该符号位于初始化数据段中。一般来说,分配到data section中
类型 T 该符号位于代码区text section。
把一个C/C++程序编译为一个目标文件时,一个定义的函数和全局或静态变量,会得到一个定义的符号;在输入文件(.o,.a)中只是一个引用而未定义的函数或全局变量会变成一个未定义的符号.
2. 段和节的概念
section是从连接视角描述一个二进制文件布局的,而segment是从运行视角描述。常见节section有 .text .bss .data
,但从程序运行开始section将被重新组织成segment,每个segment都是由若干section组成的,ld时可以指定segment由哪些section构成。通常所说运行程序的代码段数据段指的就是segment,可以从ELF的program header里查看segment及其映射关系。
这里对section与segment的关系再多谈一点:ld脚本可以创建segment,支持设置类型访问权限等;可以指定某些section从属于一个或多个segment。
readelf -l xx.elf
...
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align
...
Type {PHDR,LOAD,INTERP,DYNAMIC...}
Flags {RWX}
二、ld常见的一些概念
参考 ld中文手册完全版
1. ld命令
一般用于将目标文件与库链接为可执行文件或库文件,完成从输入文件目标文件(.o,.a)–>lds脚本(’-T’命令行选项指定)–>输出文件(可执行文件elf)的过程。
几个常用的参数:
[链接时的分组选项] -start-group archives --end-group
: archives
可以是一组文件,这里允许重复搜索未定义符号,而一般情况archive只会搜索一次。链接的时候库文件只会按它们出现在命令行的顺序搜索一遍,如果包里有未定义的引用标号,而且该包还被放在命令行的后面,这样链接器就无法解决该标号的引用问题。通过给包分组,这些包可以被循环搜索直到所有的引用都可以解决为止。使用该选项将降低性能。只有在无法避免多个包之间互相引用的情况下才使用。
[指定某section的绝对地址] --section-start SECTIONNAME=ORG
,通过指定ORG
(十六进制整数),指定节在输出文件中的绝对地址。-Tbss ORG'
-Tdata ORG'
-Ttext ORG
跟-section-start 同义,不过把SECTIONNAME 替换为.bss
, .data
或.text
。
/*
* -Wl为选项前缀,链接器通过编译器间接引用的例子
*/
gcc -Wl,--start-group foo.o bar.o -Wl,--end-group
/* Makefile 直接调用ld生成可执行文件 */
foo.elf: foo.o $(FOO_LDS)
$(CROSS_COMPILE)ld -T $(FOO_LDS) -o $@ $(LDFLAGS) $< \
--start-group \
$(LIB)/xxxlibs.a $(LIB)/libgcc.a ... \
--end-group \
-Ttext=$(TEXT_START)
2.链接脚本
实际上使用ld命令进行编译后的链接操作时,连接都被一个’连接脚本’所控制,如果不指定链接脚本,ld会使用默认的链接脚本(ld --verbose
可查看)。链接脚本是用连接命令语言书写,主要目的是,描述输入文件中的“section”节如何被映射到输出文件中, 并控制输出文件的内存排布等。另外,ld命令行的选项基本都可以写进链接脚本里。
连接脚本是文本文件但可include其他头文件,语法包含 命令语句(带有参数的关键字),赋值语句。
几个常用的语法:
ENTRY(symbol)
【命令】设置入口点,符号symbol的值作为用户进程第一条指令的地址。
SECTIONS
【命令】来描述如何把输入节映射到输出节, 并如何把输出节放入到内存中(内存布局):
SECTIONS
{
SECTIONS-COMMAND
SECTIONS-COMMAND
...
}
/* 如果你在连接脚本中不使用'SECTIONS'命令, 连接器会按在输入文件中遇到的节的顺序把每一个输入节放到同名的输出节中 */
SECTIONS里用SECTION(不带S)来描述一个输出节:
SECTION [ADDRESS] [(TYPE)] : [AT(LMA)]
{
OUTPUT-SECTION-COMMAND
OUTPUT-SECTION-COMMAND
...
} [>REGION] [AT>LMA_REGION] [:PHDR :PHDR ...] [=FILLEXP]
/* ld手册中的例子:代码应当被载入到地址'0x10000'处, 而数据应当从0x8000000 处开始. 命令的开始处, 定位计数器拥有值'0'.*/
SECTIONS
{
/*【赋值】第一行是对一个特殊的符号'.'赋值, 这是一个定位计数器.
*如果你没有以其它的方式指定输出节的地址(其他方式在后面会描述),
*那地址值就会被设为定位计数器的现有值
*/
. = 0x10000;
/*【赋值】节名后面的花括号中,你列出所有应当被放入到这个输出节中的输入节的名字.
* '*'是一个通配符,匹配任何文件名。表达式'*(.text)'意思是所有的输入文件中的'.text'输入节.
* 因为当输出节'.text'定义的时候, 定位计数器的值是'0x10000',
* 连接器会把输出文件中的'.text'节的地址设为'0x10000'
*/
.text : { *(.text) }
. = 0x8000000;
/*【命令】这里的*是所有连接文件的意思,.data : {} 是定义输出节及其具体包含内容;
* .data后没接地址,就按照“.” 定位计数器的地址对齐边界计算。
* .text ALIGN(0x10) : { *(.text) } 'ALIGN'返回当前的定位计数器,并向上对齐到指定的值.
*/
.data : { *(.data) }
/*【命令】连接器放好'.data'输出节之后, 定位计数器的值是'0x8000000'加上'.data'输出节的长度.
* 得到的结果是连接器会把'.bss'输出节放到紧接'.data'节后面的位置
*/
.bss : { *(.bss) }
}
OUTPUT_ARCH(BFDARCH)
【命令】指定机器架构, objdump -f
可以查看elf文件的架构信息。
INPUT(FILE, FILE, ...)
【命令】指示连接器在连接时包含文件,事实上, 可以把所有的输入文件列在连接脚本中, 然后在连接的时候什么也不需要,只要一个’-T’选项就够了.
INPUT (-lFILE)
【命令】ld 会把文件名转换为’libFILE.a’, 就象命令行参数’-l’一样
OUTPUT(FILENAME)
【命令】命名输出文件 与 ld的’-oFILENAME’命令参数是完全等效的
KEEP
【命令】背景:使用gcc参数-Wl,–gc-sections
,不链接未用函数,可以减小可执行文件大小. 另外在连接命令行内使用了选项–gc-sections,连接器可将某些它认为没用的section过滤掉,此时如果有必要强制连接器保留一些特定的 section,可用KEEP()关键字达此目的。
如KEEP(*(.text))或KEEP(SORT(*)(.text))
// 这里sort文件连接器会把匹配通配符的文件和节按在连接中被看到的顺序放置.
PHDRS
【命令】用来控制标准文件格式使用的程序头,它也就是人们熟知的“段”–segment。使用`PHDR’把一个节赋给前面已定义的一个程序段,连接器在缺省状态下会自己创建一个可用的程序头。
PHDRS
{
NAME TYPE [ FILEHDR ] [ PHDRS ] [ AT ( ADDRESS ) ] // TYPE 比如 `PT_LOAD' (1)表示这个程序头描述了一个被从文件中载入的段, objdump -p/ readelf -l 可以查看elf文件的程序头
[ FLAGS ( FLAGS ) ] ; // FLAGS 设置段属性的p_flags域, RWX
}
例如,PHDRS { // 一次设置多个段
textxx PT_LOAD ;
testbb PT_LOAD ;
}
SECTIONS { .text : { *(.text) } :textxx:testbbb }
// 这里引用了textxx,指定了.text节的segment为textxx和testbbb,把节放入指定段内。
// 使用:NONE告诉连接器不要把节放到任何一个段中。
PHDRS设置的段属性将在程序头(描述了segment与section的关系,描述了segment的属性)体现出来,objdump -p
和 readelf -l
都可以查看。
总结
以上内容多参考 ld中文手册