静态链接

静态链接

静态链接涉及的内容包含如下

  • 空间地址的分配
  • 符号解析和重定位
  • 静态库链接

本文的测试代码以及其他文件存在地址 CSFoundationLearning#les4

准备工作

首先需要准本两个源文件a.c和b.c,文件的内容如下:

[root@localhost linux]# cat a.c
extern int shared;

int main()
{
    int a = 100;
    swap(&a, &shared);
    return 0;
}
[root@localhost linux]# cat b.c
int shared = 1;

void swap(int * a, int * b)
{
    *a ^= *b ^= *a ^= *b;
}

使用 gcc -c 只编译不链接生成对应的目标文件

[root@localhost linux]# gcc -c a.c
[root@localhost linux]# gcc -c b.c
[root@localhost linux]# ls
a.c  a.o  b.c  b.o

空间地址的分配

对于有多个目标文件的链接情况,存在两种地址空间分配的策略按序叠加相似段合并,最后进行符号地址的确定,下面具体分析这两种情况

按序叠加

这是一种最简单的方案:直接把目标文件依次合并

静态链接_第1张图片
按序叠加

这种分配策略有两个缺点:

  • 段很多并且零散,每个文件有m个段,n个文件就会产生m*n个段
  • 浪费空间,段要求地址和空间对其(x86硬件平台是一个页,也就是4096字节),零散的段就会造成空间的浪费

因此,这个方案实际并不可行,所有分析另一种方案

相似段合并

相似段合并,顾名思义就是把相同类型的段合并在一起,比如.text段分为一组合并,.data段分为一组合并,这样可以解决按序叠加这种分配策略带来的问题

静态链接_第2张图片
相似段合并

何为地址和空间

地址和空间,会存在两种解释:

  • 链接输出可执行文件中的空间
  • 装载后的虚拟地址空间
    对于这两种情况
  • 有实际数据的段,文件和虚拟地址空间都存在
  • 没有实际数据的段,只有在虚拟地址空间客观存在

在链接阶段,链接器为目标文件分配地址和空间,这里谈到的地址空间只关注与虚拟地址空间的分配,因为这个关系到链接器后面的关于地址计算的步骤,与文件中的空间关系不大。

真实的链接策略

使用ld命令链接目标文件生成可执行文件,其中

  • -e 表示可执行文件入口函数
  • -o 表示可执行文件的名称
[root@localhost linux]# ld a.o b.o -e main -o ab
[root@localhost linux]# ls
ab  a.c  a.o  b.c  b.o

使用objdump查看目标文件和链接生成的可执行文件的段属性

[root@localhost linux]# objdump -h a.o

a.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000002c  0000000000000000  0000000000000000  00000040  2**2
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000000  0000000000000000  0000000000000000  0000006c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  0000006c  2**2
                  ALLOC
  3 .comment      0000002d  0000000000000000  0000000000000000  0000006c  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  00000099  2**0
                  CONTENTS, READONLY
  5 .eh_frame     00000038  0000000000000000  0000000000000000  000000a0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
[root@localhost linux]# objdump -h b.o

b.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000004c  0000000000000000  0000000000000000  00000040  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000004  0000000000000000  0000000000000000  0000008c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000090  2**2
                  ALLOC
  3 .comment      0000002d  0000000000000000  0000000000000000  00000090  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000bd  2**0
                  CONTENTS, READONLY
  5 .eh_frame     00000038  0000000000000000  0000000000000000  000000c0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
[root@localhost linux]# objdump -h ab

ab:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000078  00000000004000e8  00000000004000e8  000000e8  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .eh_frame     00000058  0000000000400160  0000000000400160  00000160  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .data         00000004  00000000006001b8  00000000006001b8  000001b8  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  3 .comment      0000002c  0000000000000000  0000000000000000  000001bc  2**0
                  CONTENTS, READONLY

根据以上的数据,发现合并的段数量没有变多,段的大小(Size值)变大了,针对.text段和.data段分析,合并之后段的大小如下图所示

静态链接_第3张图片
段合并结果

链接之后可以看到之前为空的VMA(Virtual Memory Address 虚拟地址)都分配的了对应的虚拟地址空间,.text段的VMA为00000000004000e8,偏移File off为000000e8,因为64位的Linux系统进程的虚拟地址空间分配规则是从0000000000400000开始的。

符号解析和重定位

使用objdump -d查看目标文件的反汇编结果

查看未链接的目标文件a.o的反汇编结果:

[root@localhost linux]# objdump -d a.o

a.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 
: 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 18: 48 89 c7 mov %rax,%rdi 1b: b8 00 00 00 00 mov $0x0,%eax 20: e8 00 00 00 00 callq 25 25: b8 00 00 00 00 mov $0x0,%eax 2a: c9 leaveq 2b: c3 retq

其中:

  • 13: be 00 00 00 00 mov $0x0,%esi 这条指令表示的是对shared变量的引用
  • 8: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp) f: 48 8d 45 fc lea -0x4(%rbp),%rax 18: 48 89 c7 mov %rax,%rdi 这几条指令表示的是对变量a的赋值,最终保存在rax寄存器中
  • 20: e8 00 00 00 00 callq 25 这条指令表示函数swap的调用

从上面的结果可知,编译阶段,shared变量的引用和函数swap的调用地址都是为0,到了链接节点,才会把地址指向虚拟地址空间的地址,下面通过查看链接之后的汇编代码,找到这两个符号发生了那些变化。

查看链接之后的ab的反汇编结果:

[root@localhost linux]# objdump -d ab

ab:     file format elf64-x86-64


Disassembly of section .text:

00000000004000e8 
: 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 b8 01 60 00 mov $0x6001b8,%esi 400100: 48 89 c7 mov %rax,%rdi 400103: b8 00 00 00 00 mov $0x0,%eax 400108: e8 07 00 00 00 callq 400114 40010d: b8 00 00 00 00 mov $0x0,%eax 400112: c9 leaveq 400113: c3 retq 0000000000400114 : # 省略swap函数的实现代码

发生的变化如下:

  • 4000fb: be b8 01 60 00 mov $0x6001b8,%esi 这条指令表示的是对shared变量的引用
  • 400108: e8 07 00 00 00 callq 400114 这条指令表示函数swap的调用

可以看到对应的地址重定位到了了对应的变量和函数的虚拟地址空间的地址,为什么是这些地址,分析如下

  • 0x6001b8 对应的是 shared 的地址,从前面的 objdump -h ab 看到 .data 段的地址为 00000000006001b8 ,因为 .data 段中只保存一个值就是 shared 变量,所以 0x6001b8 就是变量 shared 的地址
  • 400114 对应的是函数 swap 的地址,从 objdump -d ab 的结果 000000000400114 : 就可以直接看到 swap 的地址了,call 是一条近地址相对位移调用指令,他的下一条指令mov地址是0x40010d,最终的地址为0x40010d+0x7=0x400114,对应的是swap的地址

以上介绍了链接的策略以及链接符号的地址重定位的变化过程,在链接的步骤中有哪些符号是需要重定位的呢?接下来就是要介绍的内容。

重定位表

ELF文件中定义了一个重定位表段,文件定义了需要在链接阶段进行重定位的符号,使用 objdump -r 命令查看a.o目标文件的重定位表信息如下

[root@localhost linux]# objdump -r a.o

a.o:     file format elf64-x86-64

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


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

对应的定义在 /usr/lib/elf.h 头文件中的重定位表信息的结构体如下

/* Relocation table entry without addend (in section of type SHT_REL).  */

typedef struct
{
  Elf32_Addr    r_offset;       /* Address */
  Elf32_Word    r_info;         /* Relocation type and symbol index */
} Elf32_Rel;

字段说明如下:

静态链接_第4张图片
重定位表字段说明

下面还是列出 objdump -d a.o 反汇编的结果,和 objdump -r a.o 重定位表信息进行对照分析

[root@localhost linux]# objdump -d a.o 

a.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 
: 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 18: 48 89 c7 mov %rax,%rdi 1b: b8 00 00 00 00 mov $0x0,%eax 20: e8 00 00 00 00 callq 25 25: b8 00 00 00 00 mov $0x0,%eax 2a: c9 leaveq 2b: c3 retq
  • 0000000000000014 R_X86_64_32 shared 该重定位信息的的值为14,即13: be 00 00 00 00 mov $0x0,%esi指令的操作数部分的地址,也就是shared变量地址
  • 0000000000000021 R_X86_64_PC32 swap-0x0000000000000004 该重定位信息的值为21,即20: e8 00 00 00 00 callq 25 指令的操作数部分的地址,也就是call函数地址变量

上面的分析我们看到了符号解析以及指令修正的结果,接下来回具体的分析符号的解析和指令的修正过程

符号解析和指令的修正

从上面 objdump -r a.o 的结果看到了重定位的两种类型 R_X86_64_32R_X86_64_PC32,解释如下,下表中的386表示的是32位的,X86_64表示的是64位的,一一对应就行了,重定位修正方法是一致的。

静态链接_第5张图片
重定位类型


其中:

  • A=保存在被修改位置的值,重定位表可以查看该值,0000000000000021 R_X86_64_PC32 swap-0x0000000000000004 表示swap的值为-0x4
  • P=被修改的位置(相对于段开始的偏移量或者虚拟地址),该值通过r_offset计算得到
  • S=符号的实际地址,f_info的高24位指定的符号实际地址

下面针对20: e8 00 00 00 00 callq 25 该指令进行分析指令的修正 ,假设main函数地址为0x1000,swap函数地址为0x2000,重定位表信息0000000000000021 R_X86_64_PC32 swap-0x0000000000000004看到修正的swap位置的值为 0x0000000000000004,并且是类型为R_X86_64_PC32属于相对寻址修正,所有对应的S/A/P值如下:

  • S=0x2000
  • A=-0x04
  • P=0x1000+0x21=0x1021

地址修正:S+A-P=0x2000+(-0x04)-(0x1021) = 0xFDB

...
20: e8 db 0f 00 00          callq  0xfdb
25: b8 00 00 00 00          mov    $0x0,%eax
...

实际调用的地址是下一条指令的起始地址加上偏移量,即 0xFDB+0x1025=0x2000,也就是swap函数的虚拟地址

静态库链接

以C的静态库 libc.a 分析

使用命令objdump -t libc.a | grep printf查找libc.a文件中的printf符号,可以看到printf符号位于printf.o目标文件中

aron@ubuntu:~/gitrepo/CSFoundationLearning/les4/linux/ubuntu_libc$ objdump -t libc.a | grep printf
...
fprintf.o:     file format elf64-x86-64
0000000000000000 g     F .text  000000000000008f __fprintf
0000000000000000         *UND*  0000000000000000 vfprintf
0000000000000000 g     F .text  000000000000008f fprintf
0000000000000000  w    F .text  000000000000008f _IO_fprintf
printf.o:     file format elf64-x86-64
0000000000000000 g     F .text  000000000000009e __printf
0000000000000000         *UND*  0000000000000000 vfprintf
0000000000000000 g     F .text  000000000000009e printf
0000000000000000 g     F .text  000000000000009e _IO_printf
...

libc.a静态库文件其实是目标文件的一个组合,使用 ar -t 命令查看静态库中的所有目标文件,可以看到里面包含了许多的目标文件

aron@ubuntu:~/gitrepo/CSFoundationLearning/les4/linux/ubuntu_libc$ ar -t libc.a
init-first.o
libc-start.o
sysdep.o
version.o
check_fds.o
libc-tls.o
elf-init.o
dso_handle.o
errno.o
init-arch.o
errno-loc.o
hp-timing.o
iconv_open.o
iconv.o
iconv_close.o
gconv_open.o
...

使用 ar -x 命令把静态库中的所有目标文件解压到当前文件夹

aron@ubuntu:~/gitrepo/CSFoundationLearning/les4/linux/ubuntu_libc$ ar -t libc.a
aron@ubuntu:~/gitrepo/CSFoundationLearning/les4/linux/ubuntu_libc$ ls
C-address.o               getaliasname_r.o         mkdtemp.o              spawn.o
C-collate.o               getauxval.o              mkfifo.o               spawn_faction_addclose.o
C-ctype.o                 getc.o                   mkfifoat.o             spawn_faction_adddup2.o
C-identification.o        getc_u.o                 mknod.o                spawn_faction_addopen.o
C-measurement.o           getchar.o                mknodat.o              spawn_faction_destroy.o
C-messages.o              getchar_u.o              mkostemp.o             spawn_faction_init.o
C-monetary.o              getclktck.o              mkostemp64.o           spawnattr_destroy.o
...

下面以简单的 hello.c 文件为例,做个简单的测试,因为 hello.c 文件中只包含引用符号 printf ,而 printf 符号位于 printf.o 文件中,所以链接的时候单独链接 printf.o 文件,看下结果如何

aron@ubuntu:~/gitrepo/CSFoundationLearning/les4/linux/hello$ ld hello.o ../ubuntu_libc/printf.o
ld: warning: cannot find entry symbol _start; defaulting to 00000000004000b0
hello.o: In function `main':
hello.c:(.text+0xa): undefined reference to `puts'
../ubuntu_libc/printf.o: In function `__printf':
(.text+0x6e): undefined reference to `stdout'
../ubuntu_libc/printf.o: In function `__printf':
(.text+0x92): undefined reference to `vfprintf'

链接发生了错误,因为 printf.o 文件本身有对其他目标对象符号的引用,可以看到对 stdoutvfprintf 这两个符号有引用,类型是UND的,所以还需要链接对应的目标文件才行,这是一个递归的过程,使用 gcc 自动编译链接的时候会自动处理,所以不在深入研究了。

aron@ubuntu:~/gitrepo/CSFoundationLearning/les4/linux/ubuntu_libc$ objdump -t printf.o

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  .comment   0000000000000000 .comment
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 printf
0000000000000000 g     F .text  000000000000009e _IO_printf

总结

以上就是对静态链接过程的一个学习型的总结,如有不妥之处还请不吝赐教

你可能感兴趣的:(静态链接)