CS_APP 读书笔记:链接(3)重定位、可执行目标文件及其加载,动态链接库(1)

1,078 阅读5分钟

重定位

符号解析成功,就意味着每个符号引用都能找到一个符号的定义,并已经与其关联起来。

接下来要进行的工作就是重定位 —— 合并输入的模块,并为每个符号分配运行时地址

重定位步骤

重定位节( Section )、符号定义

将多个模块合并为同一个文件,并为合并后文件中的每个指令和变量确定运行时地址。

  • 将所有模块的同类型 Section 合并为同类型的聚合 Section

  • 将运行时内存地址:

    • 赋给新的聚合节
    • 赋给模块定义的每个节
    • 赋给模块定义的每个符号

    完成重定位后,程序中的每个指令、全局变量就确定了唯一的运行时内存地址

对 Section 中的符号引用进行重定位

ld 修改 .code 、.data 中对每个符号的引用,指向刚刚确定的运行时地址。引用重定位要使用到目标文件中的重定位条目(Relocation entry)进行。

.rela.text 中存着代码的重定位条目(可使用 readelf 查看),重定位条目能够告诉链接器,要如何在将 .o 合并为 .out 时修改引用

若使用 objdump -dx 查看,会发现这个条目被插在相应的引用处。比如函数调用的,就会在 callq 的下一行标注出来,一个条目大概像下面这样:

e:  R_X86_64_PLT32  a-0x4

上面这行就是重定位条目了,一个条目可以被分为四个部分。每个重定位条目要表示一个必须被重定位的引用,并指明如何计算被修改的引用。

long offset;		// 要重定位的引用的节偏移
long type:32,		// 重定位类型
	 symbol:32;		// 符号表索引
long addend;		// 常数,一些重定位要使用它来偏移调整引用值

引用指针会根据 type 决定如何赋值——ld 会将经过计算后的地址值写到被汇编器留空的位置,这样就确定了运行时指向的地址

对于这条 e: R_X86_64_PLT32 a-0x4 条目, e 是 offset, R_X86_64_PLT32 是 type,a 是 symbol, -0x4 是 addend

关于 type

CS:APP 这本书中写的 R_X86_64_32 这个 type 已经被淘汰了,现在的基本类型是下面这两种

  • R_X86_64_PC32:重定位用 32 位 PC 相对地址的引用
  • R_X86_64_PLT32:过程连接表延迟绑定

可执行目标文件及其加载

可执行文件由编译器和链接器生成,可以被操作系统用 Loader 加载进内存并执行

可执行目标文件的结构

可执行目标文件的加载

操作系统中有一个叫做 Loader (加载器)的程序。操作系统中的任何程序都可以通过调用 execve 来调用 Loader

Loader 会将可执行文件的代码、数据复制进 RAM(这个复制的过程,就称为【加载】),再跳转到第一条指令/入口点来运行程序。

“入口点(Entry point)” 是 _start 函数的地址,该函数定义在系统目标文件 ctrl.o 中,其会调用系统启动函数 __libc_start_main()(该函数定义在 libc.so 中)

程序在内存中的分布

动态链接库(共享库)

动态链接库(Shared Object)后缀名一般为 .so ,其可被加载到任意的内存地址,并和一个已经在内存中的程序链接起来——这个过程叫做“动态链接”,由动态链接器完成。

所有引用同一个动态库的 Executable 都共享该库的 .text 段代码(.text 段可以被多个正在运行的进程共享,但数据会被各个可执行文件拷贝到自己的内存区并维护一个其本地的版本)

用 gcc 生成 .so

命令大概长这样: $ gcc -shared -fpic -o libmyso.so myso.c myso2.c

这句话的意思是,用我们提供的 myso.cmyso2.c 两个源文件生成动态库 libmyso.so 。 shared 参数说明要生成共享库,pic 参数说明要生成位置无关代码 【Position Independent Code】

使用 .so

命令大概长这样:$ gcc -o myProgram main.c ./libmyso.so

so 在程序加载时动态链接

对于含动态链接的程序,在编译器创建可执行文件时,链接器会先执行一部分链接:将重定位、符号表的信息复制到可执行文件中,并添加一个存有动态链接器路径的 .interp 节( section )。

当进行程序加载时,Loader 发现程序中的 .interp 节,便会加载运行动态链接器来完成动态链接,这一步动态链接包含三个工作:

  1. 重定位 libc.so 到一个内存段
  2. 重定位 libmyso.so 到另一个内存段
  3. 重定位该可执行文件中所有对 libc.so 和 libmyso.so 定义的符号引用。

除了这种方式之外,应用程序还可以在程序运行时调用链接器,动态链接到某个共享库。这个操作可以以函数调用的方式手动控制,有下面相关四个函数可以用:

#include <dlfcn.h>

/* filename 是要链接的链接库 */
/* flag 标明应在何时进行解析。RTLD_NOW 立即解析,RTLD_LAZY 待执行链接库代码时解析 */
void* dlopen(const char* filename, int flag);

/* handle 是指向已经打开的链接库的指针 */
/* symbol 是想要找到的符号,存在则返回地址,否则返回 NULL */
void* dlsym(void* handle, char* symbol);

/* 卸载 so */
int dlclose(void* handle);

/* 返回调用上述函数时发生的最近的错误,没错误则返回 NULL */
const char* dlerror(void);

实际上,JNI 就是通过调用这些接口来实现动态链接加载 native 代码的