静态链接

静态链接

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

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

本文的测试代码以及其他文件存在地址 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

空间地址的分配

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

按序叠加

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

按序叠加

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

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

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

相似段合并

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

相似段合并

何为地址和空间

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

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

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

真实的链接策略

使用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段分析,合并之后段的大小如下图所示

段合并结果

链接之后可以看到之前为空的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 <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
  18:   48 89 c7                mov    %rax,%rdi
  1b:   b8 00 00 00 00          mov    $0x0,%eax
  20:   e8 00 00 00 00          callq  25 <main+0x25>
  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 <main+0x25>这条指令表示函数swap的调用

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

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

[root@localhost linux]# 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 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 <swap>
  40010d:   b8 00 00 00 00          mov    $0x0,%eax
  400112:   c9                      leaveq 
  400113:   c3                      retq   

0000000000400114 <swap>:
# 省略swap函数的实现代码

发生的变化如下:

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

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

  • 0x6001b8 对应的是 shared 的地址,从前面的 objdump -h ab 看到 .data 段的地址为 00000000006001b8 ,因为 .data 段中只保存一个值就是 shared 变量,所以 0x6001b8 就是变量 shared 的地址
  • 400114 对应的是函数 swap 的地址,从 objdump -d ab 的结果 000000000400114 <swap>: 就可以直接看到 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;

字段说明如下:

重定位表字段说明

下面还是列出 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 <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
  18:   48 89 c7                mov    %rax,%rdi
  1b:   b8 00 00 00 00          mov    $0x0,%eax
  20:   e8 00 00 00 00          callq  25 <main+0x25>
  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 <main+0x25>指令的操作数部分的地址,也就是call函数地址变量

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

符号解析和指令的修正

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

重定位类型


其中:

  • 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+0x25>该指令进行分析指令的修正 ,假设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

总结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,233评论 6 495
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,357评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,831评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,313评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,417评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,470评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,482评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,265评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,708评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,997评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,176评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,503评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,150评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,391评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,034评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,063评论 2 352

推荐阅读更多精彩内容