《程序员的自我修养》番外笔记——符号解析与重定位

  • 程序以下:

重定位

  • 先来看这段代码的反汇编结果。

  • "main"的起始地址为0x00000000,这是由于在未进行空间分配以前,目标文件代码段中的起始地址以0x00000000开始,等到空间分配完成之后,各个函数才会肯定本身在虚拟地址空间中的位置。
  • 偏移为0x18的地址上是一条mov指令,总共8个字节,它的做用是将“shared”的地址赋值到esp寄存器+4的偏移地址中去,前面4个字节“c7442404”是mov的指令码,后面4个字节是“shared”的地址。
  • 偏移为0x26的地址上是一条调用指令,它表示对swap函数的调用。这条指令共5个字节,前面的0xe8是操做码,这是一条近址相对位移调用指令,后面4个字节就是被调用函数的相对于调用指令的下一条指令的偏移量。在没有重定位以前,相对偏移被置为0xFFFFFFFC(小端),它是常量“-4”的补码形式。

重定位表

  • 对于可重定位的ELF文件来讲,它必须包含有重定位表,用来描述如何修改相应的段里的内容。对于每一个要重定位的ELF段都有一个对应的重定位表,而一个重定位表每每就是ELF文件中的一个段,因此其实重定位表也能够叫重定位段。
  • 经过命令能够查看目标文件的重定位表。

  • OFFSET是重定位的入口偏移,表示该入口在要被重定位的段中的位置。“.text”表示这个重定位表示代码段的重定位表,因此偏移表示代码段中须要被调整的位置。这里的0x1c和0x27分别就是代码段中“mov”指令和“call”指令的地址部分

符号解析

  • 重定位过程也伴随着符号的解析过程,每一个目标文件均可能定义一些符号,也可能引用到定义在其余目标文件的符号。重定位的过程当中,每一个重定位的入口都是对一个符号的引用,那么当连接器须要对某个符号的引用进行重定位时,它就要肯定这个符号的目标地址。这时候连接器就会去查找由全部输入目标文件的符号表组成的全局符号表,找到相应的符号后进行重定位
  • 经过命令查看“a.o”的符号表。

  • 能够看到shared和swap的类型都是“UND”,即“undefined”未定义类型,在连接器扫描完全部的输入目标文件后,全部这些未定义的符号都应该可以在全局符号表中找到,不然连接器就报符号未定义错误。这种通常都是连接时缺乏了某些库,或者输入目标文件路径不正确或符号的声明与定义不同。

指令修改方式

  • 不一样的处理器指令对于地址的格式和方式都不同。
  • 对于32位x86平台下的ELF文件的重定位入口所修正的指令寻址方式只有两种:函数

    • 绝对近址32位寻址。
    • 相对近址32位寻址。
  • 这两种重定位方式指令修正方式每一个被修正的位置的长度都是32位。
  • 这两种方式的定义:

  • 经过前面的重定位表能够看到swap符号的类型为R_386_PC32,这是一条相对位移调用指令。而shared符号的类型为R_386_32,它修正的是一条传输指令的源,即shared的绝对地址。
  • 假设在将a.o和b.o连接成最终可执行文件后,main函数的虚拟地址为0x1000,swap函数的虚拟地址为0x2000,shared变量的虚拟地址为0x3000。
  • 首先看偏移为0x18的这条mov指令的修正,它是绝对寻址修正,它修正后的结果是S+A。布局

    • S是符号shared的实际地址,即0x3000。
    • A是被修正位置的值,即0x00000000。
  • 因此它的修正后的地址为:0x3000+0x00000000=0x3000。

  • 再来看偏移为0x26的这条call指令的修正,它是相对寻址修正,它修正后的结果是S+A-P。spa

    • S是符号swap的实际地址,即0x2000。
    • A是被修正位置的值,即0xFFFFFFFC(-4)。
    • P为被修正的位置,当连接成可执行文件时,这个值应该是被修正位置的虚拟地址,即0x1000+0x27。
  • 因此它的修正后的地址为0x2000+(-4)-(0x1000+0x27)=0xFD5。

  • 这条相对位移调用指令的调用地址是该指令下一条指令的起始地址加上偏移量,即:0x102b+0xfd5=0x2000,恰好是swap函数的地址。
  • 从这两个例子能够看出来,绝对寻址修正和相对寻址修正的区别就是绝对寻址修正后的地址为该符号的实际地址;相对寻址修正后的地址为符号距离被修正位置的地址差

one more thing!

C语言标准库中的变长参数

  • 变长参数是C语言的特殊参数形式,好比printf的声明:
int printf(const char* format, ...);
  • printf函数除了第一个参数类型为const char*以外,其后能够追加任意数量、任意类型的参数。
  • 变长参数的实现得益于C语言默认的cdecl调用惯例的自右向左压栈传递方式。
  • 首先,看这样一个函数。
// 第一个参数传递一个整数num,紧接着后面会传递num个整数,返回num个整数的和。
int sum(int num, ...);
  • 当咱们调用:”int n = sum(3, 16, 38, 53);“时,参数在栈上的布局会是这样的。

  • 在函数内部,函数可使用名称num来访问数字3,当没法使用任何名称访问其余的几个不定参数。当此时因为栈上其余的几个参数实际刚好依序排列在参数num的高地址方向,所以能够简单地经过num的地址计算出其余参数的地址。
// sum的实现
int sum(int num, ...) {
int *p = &num + 1;
int ret = 0;
while (num--)
  ret += *p++;
return ret;
}
  • printf的不定参数比sum要复杂不少,由于printf的参数不只数量不定,并且类型也不定。因此printf须要在格式字符串中注明参数类型。printf里的格式字符串若是将类型描述错误,由于不一样参数的大小不一样,不只可能致使这个参数的输出出错,还有可能致使其后的一系列参数错误。
相关文章
相关标签/搜索