链接/装载/运行(4)-静态链接

静态链接是将多个目标文件合并为一个可执行文件的过程,涉及空间和地址分配、符号解析和重定位等步骤。链接器按序叠加输入文件段,进行相似段合并以节省空间。在符号解析阶段,链接器根据全局符号表进行重定位,修正指令地址。文章详细介绍了链接过程中的空间分配策略、重定位表、符号解析和指令修正方式,以及C++相关问题,如重复代码消除、全局构造与析构,还探讨了ABI兼容性和链接控制脚本的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

静态链接就是将多个目标文件合并为一个可执行文件。
实例文件:

/* a.c */
extern int shared;
extern void swap(int *a, int *b);
int main()
{
	int a = 100;
	swap(&a, &shared);
}

/* b.c */
int shared = 1;
void swap(int *a, int *b)
{
	*a ^= *b ^= *a ^= *b;
}

编译为目标文件:

/*
 *需要加上-fno-stack-protector,否则当链接时会提示:
 *a.o: In function `main':
 *a.c:(.text+0x44): undefined reference to `__stack_chk_fail'
*/
$ gcc -c a.c b.c -fno-stack-protector		

1 空间和地址分配

“链接器为目标文件分配地址和空间” 中的 “地址和空间” 有两个含义:

1、在输出的可执行文件中的空间
2、在装载后的虚拟地址中的虚拟地址空间
对于有实际数据的段,比如 .text 和 .data 来说,在可执行文件中和虚拟地址空间中都要分配空间;对于没有实际数据的段,比如 .bss 来说,分配空间的意义只局限于虚拟地址空间,因为它在文件中并没有内容。

注意:这里谈到的空间分配只关注于虚拟地址空间的分配。

可执行文件中的代码段和数据段都是由输入的目标文件合并而来的。
对于多个输入目标文件,链接器如何将它们的各个段合并到输出文件中呢?

1.1 按序叠加

这是最简单的方法:将输入文件按次序叠加起来,也就是将多个目标文件合并为一个大目标文件。
问题:在很多输入目标文件的情况下,输入文件会有很多零散的段。这样非常浪费空间,因为每个段都有一定的空间和地址对齐要求(生成可执行文件和装载运行时)。

1.2 相似段合并

这种方法就是讲输入文件的text段合并到输入文件的text段,其他段也是这样。
现在的链接器空间分配的策略基本上都采用这种方法,是用这种方法的链接器一般都采用两步链接的方法。

  • 空间与地址分配: 扫描说有的输入目标文件,并且获得它们的各个段的长度,属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到全局符号表中。这样就能将输入目标文件中的段进行合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系。
  • **符号解析与重定位:**使用第一步中收集到的信息,读取输入文件中段的数据,重定位信息,进行符号解析与重定位,调整代码中的地址。这一步才是链接过程的核心,特别是重定位过程。
/* 
 * 将目标文件链接成可执行文件
 * -e 指定 main 函数作为程序入口,ld 链接器的默认程序入口为 _start
 * -o 指定输出的可执行文件名称,默认为 a.out
 * /
$ ld a.o b.o -e main -o ab
/*
 * 链接前后各个段的属性
 * VMA(virtual memory address):虚拟地址;LMA(load memory address):加载地址。正常情况下这两个值应该是一样的,但是对于嵌入式系统,特别是将程序放入 ROM 的系统中 LMA 和 VMA 是不相同的。
 */
$ objdump -h a.o

a.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000027  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000000  0000000000000000  0000000000000000  00000067  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000067  2**0
                  ALLOC
  3 .comment      00000036  0000000000000000  0000000000000000  00000067  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  0000009d  2**0
                  CONTENTS, READONLY
  5 .eh_frame     00000038  0000000000000000  0000000000000000  000000a0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
$ objdump -h b.o

b.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000001b  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000004  0000000000000000  0000000000000000  0000005c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000060  2**0
                  ALLOC
  3 .comment      00000036  0000000000000000  0000000000000000  00000060  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  00000096  2**0
                  CONTENTS, READONLY
  5 .eh_frame     00000038  0000000000000000  0000000000000000  00000098  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
$ objdump -h ab

ab:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000042  00000000004000e8  00000000004000e8  000000e8  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .eh_frame     00000058  0000000000400130  0000000000400130  00000130  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .data         00000004  0000000000600188  0000000000600188  00000188  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  3 .comment      00000035  0000000000000000  0000000000000000  0000018c  2**0
                  CONTENTS, READONLY

链接前后文件中的 VMA 就是程序在进程中的虚拟地址,对于可执行文件,我们只关心 VMA 和 Size。链接前的目标文件中 VMA 都是0,因为虚拟空间还没有被分配。链接之后的可执行文件的各个段被分配了相应的虚拟地址。为什么代码段是从 0x00000000004000e8 开始,数据段是从 0x0000000000600188 开始呢?

1.3 符号地址的确定

当第一步空间和地址分配完成之后,链接器开始计算各个符号的虚拟地址。main, swap, shared,根据段的起始地址+偏移量就可以计算出这些符号的地址。

2 符号解析和重定位

2.1 重定位

链接器在完成地址和空间分配之后就已经可以确定所有符号的虚拟地址了,那么链接器就可以根据符号的地址对每个需要重定位的指令进行地址修正。

/* 程序代码中使用的都是虚拟地址。目标文件中 main 的起始地址是 0x0000000000000000,这是因为目标文件没有进行虚拟地址分配之前,目标文件代码段中的起始地址以 0x0000000000000000 开始。
 * 目标文件中并不知道外部符号的地址,所以用 0x00000000 填充
 * /
$ objdump -d a.o

a.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	48 83 ec 10          	sub    $0x10,%rsp
   8:	c7 45 fc 64 00 00 00 	movl   $0x64,-0x4(%rbp)
   f:	48 8d 45 fc          	lea    -0x4(%rbp),%rax
  13:	be 00 00 00 00       	mov    $0x0,%esi			// 引用 shared
  18:	48 89 c7             	mov    %rax,%rdi
  1b:	e8 00 00 00 00       	callq  20 <main+0x20>	// 引用 swap
  20:	b8 00 00 00 00       	mov    $0x0,%eax
  25:	c9                   	leaveq 
  26:	c3                   	retq   

/* 
 * 这里 main 的起始地址是 0x00000000004000e8 。
 * 调用 swap 采用的是 0xE8 指令,从 Intel 的 IA-32 体系软件开发手册查阅,这条指令是近址相对位移调用指令。后面 4 个字节是被调用函数的地址相对于这条指令的下一条指令的偏移量。
 * /
$ objdump -d ab

ab:     file format elf64-x86-64


Disassembly of section .text:

00000000004000e8 <main>:
  4000e8:	55                   	push   %rbp
  4000e9:	48 89 e5             	mov    %rsp,%rbp
  4000ec:	48 83 ec 10          	sub    $0x10,%rsp
  4000f0:	c7 45 fc 64 00 00 00 	movl   $0x64,-0x4(%rbp)
  4000f7:	48 8d 45 fc          	lea    -0x4(%rbp),%rax
  4000fb:	be 88 01 60 00       	mov    $0x600188,%esi
  400100:	48 89 c7             	mov    %rax,%rdi
  400103:	e8 07 00 00 00       	callq  40010f <swap>
  400108:	b8 00 00 00 00       	mov    $0x0,%eax
  40010d:	c9                   	leaveq 
  40010e:	c3                   	retq   

000000000040010f <swap>:
  40010f:	55                   	push   %rbp
  400110:	48 89 e5             	mov    %rsp,%rbp
  400113:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)
  400117:	48 89 75 f0          	mov    %rsi,-0x10(%rbp)
  40011b:	48 8b 45 f0          	mov    -0x10(%rbp),%rax
  40011f:	8b 10                	mov    (%rax),%edx
  400121:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  400125:	89 10                	mov    %edx,(%rax)
  400127:	90                   	nop
  400128:	5d                   	pop    %rbp
  400129:	c3                   	retq   

2.2 重定位表

链接器怎么知道哪些指令要被调整?这些指令的哪些部分需要被调整?
这个信息在重定位表中,对于可重定位文件来说,必须包含重定位表,用来描述如何修改相应的段里的内容;对于每个需要被重定位的段都有一个对应的重定位表,而一个重定位表就是一个段。比如.text有需要被重定位的地方,就有一个.rel.text的重定位段;.data有一个重定位的地方,就有一个.rel.data的重定位段。

/* 重定位表结构体
 * r_offset: 重定位入口的偏移量。对于可重定位文件表示该重定位入口要修正的位置的第一个字节相对于段其实的偏移;对于可执行文件或共享对象文件表示该重定位入口所要修正的第一个字节的虚拟地址。
 * r_info: 重定位入口的类型和符号地址。具体几位表示类型和符号地址,32位系统和64位系统不同,请参照 elf.h 文件中以 ELF32_R 开头和以 ELF64_R 开头的宏。
 * /
typedef struct
{
  Elf64_Addr	r_offset;		/* Address */
  Elf64_Xword	r_info;			/* Relocation type and symbol index */
} Elf64_Rel;

$ objdump -r a.o

a.o:     file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE 
0000000000000014 R_X86_64_32       shared
000000000000001c R_X86_64_PC32     swap-0x0000000000000004


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET           TYPE              VALUE 
0000000000000020 R_X86_64_PC32     .text

目标文件中每个要被重定位的地方叫一个重定位入口(relocation entry)。重定位入口的偏移表示该入口在要被重定位的段中的位置。**RELOCATION RECORDS FOR [.text]**表示这个重定位为表是代码段的重定位表,所以偏移表示代码段中要被调整的位置。

2.3 符号解析

重定位的过程中,每个重定位的入口都是对一个符号的引用,那么当链接器需要对某个符号的引用进行重定位时,就要确定这个符号的目标地址。这时候链接器就会去查找所有输入目标文件的符号表组成的全局符号表,找到相应的符号后进行重定位。符号表中的UND项就是需要重定位的符号。

2.4 指令修正方式

寻址方式的区别:

  • 近址寻址或远址寻址
  • 绝对寻址或相对寻址
  • 寻址长度为8位,16位,32位或64位,表示被修正位置要修正的长度。

寻址方式是由r_info中的类型决定的。
相对寻址:S+A-P
绝对寻址:S+A
S:符号的地址,也就是r_info高位指定的符号的实际地址。
A:保存在被修正位置的值
P:被修正的位置(相对于段开始的偏移量或者虚拟地址),该值也就是r_offset的值。

绝对寻址修正后的地址为符号的实际地址;相对寻址修正的的值为符号距离被修正位置的地址差。

3 COMMON 块

如果一个弱符号定义在多个目标文件中,而它们的类型又不相同,怎么办?目前的链接器本身并不支持符号的类型,即变量的类型对于链接器来说是透明的。
编译器未初始化的全局变量定义作为弱符号处理,类型为SHN_COMMON类型。当其他目标文件中存在相同的弱符号定义,则最终链接后的输出文件中的符号的小以输入文件中最大的那个为准。
COMMON类型的链接规则时针对符号都是弱符号的情况,如果其中有一个符号为强符号,那么输出结果中的符号所占空间与强符号相同。
COMMON机制的原因:编译器和链接器允许不同类型的弱符号存在,本质原因是链接器不支持符号类型,无法判断各个符号的类型是否一致。
目标文件中为什么不直接把未初始化的全局变量当作未初始化的局部静态变量一样处理,放在.bss段呢?因为目标文件时其作为弱符号,所占的空间大小是未知的,有可能在其他目标文件中占的空间更大,所以无法为符号在.bss段分配空间。但是在最终的可执行文件中就确定了大小,可以分配空间了,所以放在了.bss段中了。
GCC可以用-fno-common选项来把所有未初始化的全局变量不以COMMON块处理,或者使用int global __attribute__(nocommon)。这样全局变量就相当于一个强符号了,其他目标文件中就不能定义相同的强符号了。

4 C++相关问题

4.1 C++重复代码消除

C++ 编译器产生重复代码:模块,外部内联函数,虚函数表都有可能在不同的编译单元里生成相同的代码。比如模板,模板从本质上来讲很像宏,当模板在一个编译单元里被实例化时,并不知道自己是否在别的编译单元也被实例化了。所以当一个模板被不同编译单元实例化成相同的类型的时候,就会生成重复的代码。
重复代码带来的问题:

  • 空间浪费
  • 地址容易出错。有可能两个指向同一个函数的指针会不相等。
  • 指令运行效率低

有效的做法:将每个模板的实例代码都单独存放在一个段里,每个段只包含一个模板实例。链接时就可以区分相同的模板实例段,将它们合并入最后的代码段,对于外部内联函数和虚函数表的做法也是类似的。GCC 把这种类似的需要在最终链接时合并的段叫Link Once,做法是将这种类型的段命名为.gun.linkonce.name,其中name是该模板函数实例的修饰名称。VISUAL C++编译器是把这样的段叫做COMDAT,这种COMDAT段的属性字段都有IMAGE_SCN_LNK_COMDAT这个标记,在链接时有这个标记的段都会将重复的段丢弃。
上述做法存在的问题:比如相同名称的段可能拥有不同的内容,可能由于不同的编译单元使用的不同的编译器版本或者编译优化选项。这种情况下链接器可能会随意选择其中的一个段作为链接的输入,并提供一个警告信息。

一般链接时是目标文件级别的链接,也就是当用到目标文件中的任意一个函数或变量时,就需要把目标文件整个地链接进来,这也是为什么库文件都是一个函数一个文件的缘故。
VISUAL C++ 编译器提供了一个编译选项叫函数级别链接。这个选项的作用就是让所有的函数都像前面的模板函数一样,单独放在一个段里,链接器链接时用到了这个函数就合并到输出文件中,不用的话,就丢弃。这样会减慢编译和链接过程。
GCC 提供了类似的机制。-ffunction-sections将每个函数放到独立的段中。-fdata-sections将每个变量放到独立的段中。

4.2 全局构造与析构

main 函数之前:先初始化进程执行环境,比如堆分配初始化,线程子系统。Linux 系统下的程序就是_start函数调用main之前的代码,C++ 全局对象的构造函数就是在这里。
main 函数之后:就是是 Linux系统_start函数调用main之后的代码,C++ 的全局对象的析构函数就在这里。

ELF文件中的两个特殊段:

  • .init:初始化代码,当一个程序开始运行时,在 main 函数被调用之前,Glibc 会执行这里的代码。
  • .fini:进程终止代码指令。当 main 函数退出后,Glibc 执行这里的代码

4.3 C++与ABI

能不能将两个不同编译器的编译结果链接输出一个可执行文件呢?
条件:链接器必须支持两个编译器产生的目标文件格式;拥有相同的**ABI(Application Binary Interface),也就是拥有同样的符号修饰标准,变量的内存分布方式相同,函数的调用方式相同。
API 是源代码级别的接口;ABI 是二进制层面的接口。
影响 ABI 的因素很多:硬件,编程语言,编译器,链接器,操作系统等等。
例如对于 C 语言来说:

  • 内置类型的大小和在存储器中的放置方式
  • 组合类型的存储方式和内存分布
  • 外部符号与用户定义的符号之间的命名方式和解析方式
  • 函数调用方式。参数入栈顺序,返回值如何保持等
  • 堆栈的分布方式。参数和局部变量在堆栈中的位置,参数传递方法等
  • 寄存器使用约定。函数调用时哪些寄存器可以修改,哪些需要保存。
  • 等等。。。还有很多因素
    C++ 一直为人诟病一个原因就是二进制兼容不好,有时候连同一个编译器的不同版本之间的兼容性也不好。

5 静态库链接

$ objdump -t /usr/lib/x86_64-linux-gnu/libc.a
In archive /usr/lib/x86_64-linux-gnu/libc.a:
···············
printf.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    d  .text	0000000000000000 .text
0000000000000000 l    d  .data	0000000000000000 .data
0000000000000000 l    d  .bss	0000000000000000 .bss
0000000000000000 l    d  .text.unlikely	0000000000000000 .text.unlikely
0000000000000000 l    d  .note.GNU-stack	0000000000000000 .note.GNU-stack
0000000000000000 l    d  .eh_frame	0000000000000000 .eh_frame
0000000000000000 g     F .text	000000000000009e __printf
0000000000000000         *UND*	0000000000000000 stdout
0000000000000000         *UND*	0000000000000000 vfprintf
0000000000000000 g     F .text	000000000000009e _IO_printf
0000000000000000 g     F .text	000000000000009e printf

···············
/* 
 * -static:禁止使用动态库
 * --verbose:将整个编译过程的中间步骤打印出来
 * -fno-builtin:关闭内置函数优化选项
 */
$ gcc -static --verbose -fno-builtin hello.c
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 5.4.0-6ubuntu1~16.04.10' --with-bugurl=file:///usr/share/doc/gcc-5/README.Bugs --enable-languages=c,ada,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-5 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-5-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-5-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-5-amd64 --with-arch-directory=amd64 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.10) 
COLLECT_GCC_OPTIONS='-static' '-v' '-fno-builtin' '-mtune=generic' '-march=x86-64'
// 调用 cc1 (C 语言编译器)
 /usr/lib/gcc/x86_64-linux-gnu/5/cc1 -quiet -v -imultiarch x86_64-linux-gnu hello.c -quiet -dumpbase hello.c -mtune=generic -march=x86-64 -auxbase hello -version -fno-builtin -fstack-protector-strong -Wformat -Wformat-security -o /tmp/cctSMzFX.s
GNU C11 (Ubuntu 5.4.0-6ubuntu1~16.04.10) version 5.4.0 20160609 (x86_64-linux-gnu)
	compiled by GNU C version 5.4.0 20160609, GMP version 6.1.0, MPFR version 3.1.4, MPC version 1.0.3
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/5/../../../../x86_64-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/gcc/x86_64-linux-gnu/5/include
 /usr/local/include
 /usr/lib/gcc/x86_64-linux-gnu/5/include-fixed
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.
GNU C11 (Ubuntu 5.4.0-6ubuntu1~16.04.10) version 5.4.0 20160609 (x86_64-linux-gnu)
	compiled by GNU C version 5.4.0 20160609, GMP version 6.1.0, MPFR version 3.1.4, MPC version 1.0.3
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: bab7da148afbe213714f0f38814b36b0
COLLECT_GCC_OPTIONS='-static' '-v' '-fno-builtin' '-mtune=generic' '-march=x86-64'
// 调用汇编器
 as -v --64 -o /tmp/ccqtEJOX.o /tmp/cctSMzFX.s
GNU assembler version 2.26.1 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.26.1
COMPILER_PATH=/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/
LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-static' '-v' '-fno-builtin' '-mtune=generic' '-march=x86-64'
// 调用链接器
 /usr/lib/gcc/x86_64-linux-gnu/5/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/5/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper -plugin-opt=-fresolution=/tmp/cconL8ZX.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_eh -plugin-opt=-pass-through=-lc --sysroot=/ --build-id -m elf_x86_64 --hash-style=gnu --as-needed -static -z relro /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/5/crtbeginT.o -L/usr/lib/gcc/x86_64-linux-gnu/5 -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/5/../../.. /tmp/ccqtEJOX.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o
samsara@samsara-VirtualBox-ubuntu-16:/mnt/hgft/c/link-load-run/static-link$ 

6 链接过程控制

绝大部分情况下,我们使用链接器提供的默认链接规则对目标文件进行链接。

6.1 链接控制脚本

链接器提供的控制链接过程的方法一般有三种:

  • 使用命令行给链接器指定参数
  • 将链接指令放在目标文件里面,编译器通过这种方法向链接器传递指令。比如 VISUAL C++ 编译器把链接参数放在 PE 文件的.drectve段。
  • 使用链接控制脚本
    当我们使用 ld 链接器时,没有指定链接脚本,则使用默认链接脚本/usr/lib/ldscripts/目录下。可以用ld -verbose查看默认链接脚本。指定链接脚本ld -T link.script

6.2 最小程序

/*
 * gcc -c -fno-builtin TinyHelloWorld.c
 * ld -static -e nomain -o TinyHelloWorld TinyHelloWorld.o
 * 
char *str = "Hello world!\n";

void print(void)
{
	asm("movl $13, %%edx \n\t"		// 要写入的字节数
		"movl str, %%ecx \n\t"		// 要写入的缓冲区地址
		"movl $0, %%ebx \n\t"		// 要写入到默认终端,文件句柄为0
		"movl $4, %%eax \n\t"		// write 调用的调用号为4
		"int $0x80 \n\t"
		::"r"(str):"edx","ecx","ebx");
}

void exit(void)
{
	asm("movl $42,%ebx \n\t"	// 进程退出码
		"movl $1,%eax \n\t"		// EXIT 系统调用的调用号是1
		"int $0x80 \n\t");
}

void nomain(void)
{
	print();
	exit();
}

这里调用 EXIT 结束进程,是因为:如果是普通程序,main() 函数结束后控制权返回给系统库,系统库调用 EXIT,退出进程。这里 nomain() 结束后没有返回给系统库,可能执行到后面不正常的指令,导致进程异常退出。

6.3 使用 ld 链接脚本

脚本文件:

/*
 * 链接脚本由 命令语句 和 赋值语句构成。
 * 语句之间用 ; 作为分隔符
 * 使用与 C 语言类似的表达式和运算操作符
 * 注释用/**/
 * 命令语句:
 * STARTUP(filename):将文件 filename 作为链接过程中的第一个输入文件
 * SEARCH_DIR(path): 将路径 path 加入到 ld 链接器的库查找目录
 * INPUT(file): 将制定文件作为链接过程中的输入文件
 * INCLUDE filename: 将指定文件包含进本链接脚本
 * PROVIDE(symbol):在链接脚本中定义某个符号,该符号可以在程序中引用。
 */

 /* 命令语句:指定程序入口为 nomain() 函数 */
 /*
  * 指定入口地址方法:-e; ENTRY(symbol); 定义了 _start 符号,使用 _start 符号值;存在 .text 段, 使用 .text 段的第一字节的地址;使用0。
  * 上述五种方法,优先级依次降低
 */
 
ENTRY(nomain)

/* SECTION命令语句:后面大括号内包含了 SECTION 变换规则 */
SECTIONS
{
	/* 赋值语句:设置当前虚拟地址的值,也就是紧随其后输出段的起始虚拟地址。.表示当前虚拟地址;SIZEOF_HEADERS表示输出文件的文件头大小。 */
	. = 0x00000000004000e8 + SIZEOF_HEADERS;
	/* 转换规则:将所有输入文件中的名字为 .text, .data, .rodata的段依次合并到输出文件的 tinytext 段;这里是先合并相同的段呢,还是先合并一个文件的内容呢?
	tinytext表示输出段名。tinytext后面必须要有一空格,然后紧跟冒号和一对大括号,大括号内描述转换规则和条件,以空格隔开。
	如果输出 a.out 格式的文件,则只能使用 .text, .data, .bss 这三个段名*/
	tinytext : {*(.text) *(.data) *(.rodata)}
	/* 将所有输入文件的 .comment, .eh_frame 段丢弃*/
	/DISCARD/ :{*(.comment) *(.eh_frame)}
}

使用脚本:

ld -static -T TinyHelloWorld.lds -o TinyHelloWorld TinyHelloWorld.o

readelf工具查看,还有.shstrtab,.symtab,.strtab段。默认情况下,ld 链接器在产生可执行文件时会产生这3个段。对于可执行文件来说,.symtab,.strtab是可选的,.shstrtab是必不可少的。可以通过 ld 的-s参数来禁止链接器产生符号表和字符串表,或者使用strip命令来去除程序中的符号表和字符串表。

6.6 BFD 库

不同的硬件平台基础导致了每个平台有独特的目标文件格式。
即使同一格式的目标文件在不同硬件平台也有不同的变种。
BFD (Binary File Descriptor library)库的目标就是希望通过一种统一的接口来处理不同的目标文件格式。
BFD 把目标文件抽象成一个统一的模型,使得 BFD 库的程序只要通过操作这个抽象的目标文件模型就可以实现操作所有 BFD 支持的目标文件格式。
现在的GCC,链接器ld,调试器GDB,以及其他工具都通过 BFD 库来处理目标文件,而不是直接操作目标文件。好处是:将编译器和链接器本身同具体的目标文件格式隔离开来,一旦需要支持一种新的目标文件格式,只需在 BFD 库里添加就可以了,不用修改编译器和链接器。
安装 BFD 开发库以后,使用下列代码可以输出 BFD 库所支持的所有的目标文件格式。

#include<stdio.h>
#include "bfd.h"
 
int main(int argc, char **argv)
{
	const char **t = bfd_target_list();
	while(*t)
	{
		printf("%s\n", *t);
		t++
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值