静态链接分析

注:本篇内容基本参考自:Mach-O 与静态链接。然后自己做一些上手实践和分析,用来加深对静态链接过程的理解。

材料准备

首先手写一个简单的C程序:a.c,b.c

/* a.c */
#include <stdio.h>

extern int globalA;
extern int globalB;

void swap(int *a, int *b);
void printResult();
void printsomething();

int main() {
    globalA = 5;
    globalB = 6;
    swap(&globalA, &globalB);

    printsomething();
    printResult();

    return 0;
}

void printsomething() {
  printf("print something here:\n");
}
/* b.c */
#include <stdio.h>

int globalA = 3;
int globalB = 4;

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

void printResult() {
  printf("globalA = %d, globalB = %d\n", globalA, globalB);
}

然后分别编译a.c和b.c,生成a.o和b.o,然后把a.o和b.o进行静态链接生成ab.out。可以看到a.o和b.o是Mach-O 64-bit object类型的文件,即目标文件,虽然也是mach-O格式的,但还不能被加载执行。ab.out则是Mach-O 64-bit executable,是可以用来被执行的。

➜  静态链接 git:(master) ✗ file a.o
a.o: Mach-O 64-bit object x86_64
➜  静态链接 git:(master) ✗ file ab.out
ab.out: Mach-O 64-bit executable x86_64

查看Mach-O

我们使用工具来分析Mach-O文件:MachOView

因为三个文件都是Mach-O格式的文件,所以内容格式差不多,都有自己的TEXt段,DATA段等。

  1. a.o中TEXT段的内容就是main和printsomething两个函数的二进制指令
  2. b.o中TEXT段的内容则是swap和printResult两个函数的二进制指令,DATA段则存储了globalA和globalB两个全局变量
  3. ab.out就是把a.o和b.o做了一个合并和重新整理,包含了所有函数和变量的信息。

静态链接

静态链接是一个比较简单的概念,工作内容就是把若干个目标文件合并,并重新整理成一个新的目标文件(此时也是可执行文件了)的过程。本质上仍然应属于编译的过程。

为什么需要静态链接

静态链接应该是编程复杂化、工程化的产物,如果一个程序很简单,只有一个main函数,那么自然不需要链接的过程,直接编译就可以了。

随着程序复杂度的提高,源程序也被拆分成若干个源文件,但划分若干文件也并不代表一定需要静态链接。例如,如果我们修改了某个源文件,可以重新编译所有源文件,直接生成一个可执行文件也是可行的。不过仅为了1个文件的修改,要重新编译另外9999个没有改变的文件显然是没有必要的。

所以我们拆分了生成可执行文件的步骤:先编译单个文件,然后再把它们链接起来。这样当我们修改了某个文件后,只需重新编译该文件,然后再执行一遍链接过程即可。所以静态链接对软件开发的复杂化、工程化、工作效率等都有起到了很大的作用。

分析程序

我们再来分析下这个程序:

  1. b.c中声明了两个全局变量和两个函数,并没有引用外部变量和函数(不考虑printf函数)
  2. a.c中引用了b.c中声明的两个变量和函数

然后看下a.o中main函数的二进制指令:

globalA 和 globalB

我们看到两条指令:488B0500000000 和 488B0D00000000,意思是分别把globalA的地址放入rax,把globalB的地址放入rcx。

  1. 488B0500000000指令拆解是:48 8B05 00000000,48代表movq,8B05代表rax,因为在a.o中我们还不知道globalA的地址,所以此时地址暂时为0:00000000
  2. 同理:488B0D00000000 指令拆解是:48 8B0D 00000000,48代表movq,8B0D代表rcx,因为在a.o中我们还不知道globalB的地址,所以此时地址暂时为0:00000000
swap 和 printResult

看下swap和printResult的调用跳转指令:

  1. E800000000:指令拆解是:E8 00000000,因为还不知道swap的地址,所以此时地址暂时为0:00000000
  2. E800000000:指令拆解是:E8 00000000,因为还不知道printResult的地址,所以此时地址暂时为0:00000000
ab.out

a.o中引用外部符号的地方,因为暂时无法得知外部符号的地址,所以暂时都用0占位了。经过静态链接后,我们再看ab.out中的这些指令,占位0就已经被有意义的值替代了。

重定向表

我们可以看到在a.o中有三条指令都是E800000000,那Mach-O工具解析出来为什么能知道哪个是调用swap,哪个是调用printResult呢?这个通过后面分析重定向表(Relocations)得到的。**重定向表是一个list结构,其中每个item都指示了什么位置的,多长bit的内容需要被重定向到哪个符号。**在链接时,连接器就会依据重定向表的内容为需要重定向的地方进行重定向。

上图中,可以看到a.o中的globalA,globalB,swap,printResult等在b.o中的外部符号是需要被重定向的。并且即使是自己内部的printsomething和字符串常量"print something here:\n"也是需要被重定向的,因为这些符号的位置随着最后合并成ab.out也是要变化的,所有索性就到后面一起处理。

重定向表的结构

Relocation table 可以看作是一个 relocation entry 的数组,每个 relocation entry 占 8 个字节,对应结构体是relocation_info:

struct relocation_info {
    int32_t   r_address;      /* offset in the section to what is being relocated */
    uint32_t  r_symbolnum:24, /* symbol index if r_extern == 1 or section ordinal if r_extern == 0 */
              r_pcrel:1,      /* was relocated pc relative already */
              r_length:2,     /* 0=byte, 1=word, 2=long, 3=quad */
              r_extern:1,     /* does not include value of sym referenced */
              r_type:4;       /* if not 0, machine specific relocation type */
};

我们以 globalB 为例,来分析一下这个结构体:

  1. 前4个字节是 r_address,这个字段代表需要被重定向的字段的起始地址(偏好地址),这里是0x12。
  2. length 代表需要被重定向的字段的长度,和 r_address 配合使用就是确定哪些bit位需要被重定向。这里就是从TEXT段偏移0x12的地址开始的4个字节需要被重定向。(检查一下TEXT段,确实是 globalB 的位置)

这样就已经确定了哪些bit需要被重定向了,那么下一步是确定应该被重定向到哪个符号。这个信息隐藏在后4个字节里,后四个字节是一个bit field。其中最重要的信息是 r_symbolnum

  1. r_symbolnum 是共用体的后24位(前24还是后24和机器架构有关) ,代表了需要重定向到的符号在符号表中的index,有了这个index,就能知道需要被定向到哪个符号了。这里的值是3。

然后我们再来看下符号表

符号表

符号表是程序编译过程中非常重要的数据,用于存储符号和符号相关的信息。符号表本质是一个键值对表,key是符号名,值是一些符号相关的信息

上面提到 globalB 的 r_symbolnum 的值是3,这里可以看到 index = 3 的符号正是 globalB。

符号表的结构

此处一个符号表的 entry 占 16个字节。它的核心字段是 n_strx 和 n_value

struct nlist_64 {
    union {
        uint32_t n_strx;   /* index into the string table */
    } n_un;
    uint8_t  n_type;       /* type flag, see below */
    uint8_t  n_sect;       /* section number or NO_SECT */
    uint16_t n_desc;       /* see <mach-o/stab.h> */
    uint64_t n_value;      /* value of this symbol (or stab offset) */
};
  1. 前4个字节是 n_strx ,用于表示该符号的名称,它指示了该符号名称在 string table中的起始位置。这里globalB的值是0x32,正好是string table中 globalB 的起始位置。
  2. 后8个字节是 n_value,用于表示该符号的value,如果该符号是全局变量,那么value就是该变量在DATA段的地址,如果该符号是函数,那么该value就是该函数在TEXT段的入口地址。这里 value 是0,因为 a.o 中还没有和 globalB 相关的任何信息(此时 globalB 是外部符号)。

我们再来验证一下 n_value。看下链接后ab.out符号表中的 globalB。

此时,value的值变成了 000000010000101C,该值的含义是 globalB 的地址是 000000010000101C。我们在 DATA 段找到该地址,发现当前该地址存储的值是4,正好就是 globalB(它前面的变量存储的值是3,是 globalA。符合 int globalA = 3; int globalB = 4; 这两条初始化语句)。

静态链接器

有了以上信息后,剩下了的就是静态链接器的工作的,链接器负责分析以上信息,然后重新调整各变量和方法的位置,补全重定向的地址,合并生成最终的可执行文件。

总结

我们可以看到静态链接本身并不算复杂,它的核心逻辑就是【重定向表】和【符号表】,搞懂了这两个表中字段的含义,基本就搞懂了静态链接的方式了。

  1. 在编译单个源文件时,此时涉及到的全局变量、调用函数的地址、字符串常量的地址都先用0占位(无论是不是外部符号),因为此时无法确定这些地址的最终值(后面还要和其他目标文件合并)
  2. 重定向符号表指示了:目标文件中哪些 bit 是需要被重定向的(起始地址+长度),并且要把它重定向到哪个符号(用符号在符号表中的index来指明)
  3. 符号表:收集了所有符号,和每个符号所对应的内存地址(如果是变量就对应到DATA段该变量的地址,如果是函数就对应到TEXT段该函数的起始地址)
  4. 字符串表:收集了所有字符串(字符串之间用 00 分割)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值