linker
链接器主要有两个作用:
- 一是将若干输入文件(.o文件)根据一定规则合并为一个输出文件(例如ELF格式的可执行文件);
- 一是将符号与地址绑定(当然加载器也要完成这一部分工作)。
关于链接器的工作机制可以参考 《Linker and Loader》
一书,本文只关心它的第一个功能,即如何根据一定规则将一个或多个输入文件合并成输出文件。这里的 “一定规则” 是通过链接脚本描述的。
在进行链接时,linker 会根据链接脚本从输入的 .o 文件中挑选出感兴趣的 section,把它们合并生成新的 section,这些新产生的 section 归属于目标文件的某个 segment(段),并出现在目标文件中。这里提到了 segment 的概念。Segment可以看作一组具有相同属性(或部分相同属性)的 section 的集合,属性是指“读、写、执行”。例如 .text
通常存放的是代码编译后的二进制指令,它具有 r-x
权限;.rodata
存放是的只读数据,如常量字符串,它通常具有 r--
权限(实际上也可以具有x权限,例如用一个全局 const 数组存放可执行的机器码);那么在生成目标文件时,.text
和 .rodata
就可以通过一个具有 r-x
属性的 text segment
来包含它们,这就是我们通常说的 “文本段”。
segment 在 ELF 术语中称为 program header
,用来描述整个目标文件以什么样的方式加载到内存中,方式是指加载的地址、segment 的长度和属性等等。用 objdump –p
命令可以查看目标文件的 segment。
a.out: file format elf64-x86-64
Program Header:
PHDR off 0x0000000000000040 vaddr 0x0000000000400040 paddr 0x0000000000400040 align 2**3
filesz 0x00000000000001f8 memsz 0x00000000000001f8 flags r-x
INTERP off 0x0000000000000238 vaddr 0x0000000000400238 paddr 0x0000000000400238 align 2**0
filesz 0x000000000000001c memsz 0x000000000000001c flags r--
LOAD off 0x0000000000000000 vaddr 0x0000000000400000 paddr 0x0000000000400000 align 2**21
filesz 0x00000000000006fc memsz 0x00000000000006fc flags r-x
LOAD off 0x0000000000000e10 vaddr 0x0000000000600e10 paddr 0x0000000000600e10 align 2**21
filesz 0x0000000000000228 memsz 0x0000000000000230 flags rw-
DYNAMIC off 0x0000000000000e28 vaddr 0x0000000000600e28 paddr 0x0000000000600e28 align 2**3
filesz 0x00000000000001d0 memsz 0x00000000000001d0 flags rw-
NOTE off 0x0000000000000254 vaddr 0x0000000000400254 paddr 0x0000000000400254 align 2**2
filesz 0x0000000000000044 memsz 0x0000000000000044 flags r--
EH_FRAME off 0x00000000000005d0 vaddr 0x00000000004005d0 paddr 0x00000000004005d0 align 2**2
filesz 0x0000000000000034 memsz 0x0000000000000034 flags r--
STACK off 0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**4
filesz 0x0000000000000000 memsz 0x0000000000000000 flags rw-
RELRO off 0x0000000000000e10 vaddr 0x0000000000600e10 paddr 0x0000000000600e10 align 2**0
filesz 0x00000000000001f0 memsz 0x00000000000001f0 flags r--
...
elf 文件
object file
.text
,代码段,就是CPU要运行的指令代码;
.data
,数据段,程序中包含的一些数据,放在这个段里;
.bss
,未初始化段,记录了程序里有哪些未初始化的变量,段表中只记录对应的大小,留着程序运行前去初始化为0,所以,此处并不占用elf 文件的空间。
symbol table 符号表
objdump -t
或者 nm
命令
defined symbol:通俗说就是全局变量、静态变量、本文件的函数
undefined symbol:通俗说就是未初始化的全局变量、本文件引用的外部变量(extern)、以及引用的外部函数
section 分类
loadable,可加载,原先目标文件里面包含对应的代码或数据,装载器要把这些内容,load到对应的地址,以便程序可以运行;
allocatable,可分配的,如上面提到的 .bss
段,段表里记录了变量信息,装载器需要分配变量所要占用的具体内存空间。
还有既不是 loadable 的,也不是 allocatable 的,比如只存储 debug 信息的段。
section 地址
VMA & LMA 分别对用程序运行时虚拟地址和程序的加载地址。
VMA(Virtual Memory Address):the address the section will have when the output file is run;
LMA(Load Memory Address): the address at which the section will be loaded.
通常情况下二者相等。可以通过 objdump -h
查看。
linker script
前面说到 linker 的作用,那么链接器是依据什么规则来生成最终的可执行文件的呢?没错,就是链接脚本。
如果没有指定链接脚本的话,链接器有一个默认的供使用,可以用 ld --verbose
查看。可以通过 ld -T
选项指定自己程序的链接脚本。
linker script 语法
// 什么时候指定 LMA 和 VMA
SECTIONS
{
. = 0x10000;
.text :
{
*(.text)
}
. = 0x8000000;
.data :
{
*(.data)
}
.bss :
{
*(.bss)
}
}
.: address counter
上面的含义是说把输入目标文件的代码段全部放到输出目标文件的代码段,地址从 0x10000 开始;
数据段则放在 0x8000000 开始的位置。
INCLUDE filename
在看到这个命令的时候才去载入filename这个linker script。可以被放在不同的命令如SETCTION, MEMORY等。
INPUT(file1 file2 ...)
指定加载的输入object档案,如abc.o这样的档案。
GROUP(file1 file2 ...)
指定加载的输入archieve档案,如libabc.a这样的档案。
AS_NEEDED(file1 file2 ...)
在INPUT和GROUP使用的命令,用来告诉linker说如果object里面的数据有被reference到才link进来,
猜测应该可以减少储存空间。范例(未测试请自行斟酌):INPUT(file1.o file2.o AS_NEEDED(file3.o file4.o))
OUTPUT(filename)
和gcc -o filename 一样
SEARCH_DIR(path)
和-L path一样
STARTUP(filename)
和INPUT相同,唯一差别是ld保证这个档案一定是第一个被link
OUTPUT_FORMAT(bfdname)
指定输出object档案的binary 文件格式,可以使用objdump -i列出支持的binary 文件格式
OUTPUT_FORMAT(default, big, little)
指定输出 object 档案预设的 binary 文件格式
TARGET(bfdname)
指定使用哪种 binary 文件格式读取输入 object 档案
可以使用 objdump -i
列出支持的 binary 文件格式。
HIDDEN(symbol name)
隐藏某个全局符号?
PROVIDE(symbol = expression)
如果程序没有这个符号就使用这里提供的符号和值
PROVIDE_HIDDEN(symbol = expression)
结合了上述两者
有意思的地方
每次读取一个目标文件,匹配里面的 section,输出到输出目标文件相应的 section 中。
*(.sec1 .sec2):
File1.sec1
File1.sec2
File2.sec1
File2.sec2
*(.sec1) *(.sec2):
File1.sec1
File2.sec1
File1.sec2
File2.sec2