文档更新说明
- 最后更新 2020年09月13日
- 首次更新 2020年11月19日
前言
本文主要讲述在iOS,Mac OS X的程序中调用一个外部符号(动态库函数)时发生的有趣事情。读懂本文有一定门槛, 这里假设读者对Mach-O格式有一定了解, 对地址偏移概念有了解, 对虚拟内存概念有了解 . 可以参考这Mach-O格式解析
于此同时, 我附上源码, 编译好的二进制文件, 以及MachOView这个工具, 这样这样读者就可以一步一步跟着动手调试, 这样才能真正理解, 相信我, 纸上谈兵是无法深刻理解的, 而网上大部分文章, 就是纸上谈兵, 这也是这篇文章存在的意义.
#include <stdio.h>
#include <stdlib.h>
const char* str1 = "Hello, World\n";
const char* str2 = "Hello, Boy\n";
static void static_say() {
printf("static hello\n");
}
void say() {
printf("hello\n");
}
int main(int argc, const char * argv[], char **envp, char **apple) {
// insert code here...
printf("%s", str1);
char *tiny = malloc(sizeof(int));
free(tiny);
say();
static_say();
return 0;
}
代码比较简单, 主要是测试printf函数. 在iOS或者MacOS上, printf函数是由动态库提供的, 后面可以看到dyld在加载进程的时候做的符号绑定流程. 编译后的可执行文件和工具, 都放到本文末尾.
概念一: stubs 桩
要理解rebase和bind, 必须先理解stubs(stub的复数). 在TEXT区域(代码区), 可以看到编译器为动态库的符号设置了对应的stub, 每个stub占用6个字节(仅指x64编译出来), 存放的是一条汇编指令jumq.(编译后的程序存放的都是机器码, 反编译后就可以看到汇编指令), 代码中调用函数, 被编译之后, 汇编指令就是调用这个函数对应的stub
在源码printf("%s", str1);
这行添加断点, 然后勾选xcode菜单Debug->Workflow->Always Show Disassembly
, 这样就可以逐步调试汇编代码. 在lldb上输入si就可以按照指令逐步调试.
从上图可以看出, 调用printf这个符号时, 会跳转到printf对应的stub上(stub相当于中介), 指令逐步执行(si), 进入f50这个地址.
其中jmpq *10ba(%rip), 就是stub存放的数据 FF25(jmpq 指针) BA10(这个是大端写法, 转成本地序就是0x10ba), 执行这条指令之后, rip(指令寄存器)的值是f50+6=f56, f56+10ba = 0x2010, 整条指令的意思就是跳转到0x2010这个地址存放的值上(这个概念和指针一样, 0x2010这个地址存放的是一个内存地址), 结合MachOView和调试的汇编代码, 可以看到首次调用_printf这个符号, 会跳转到f7c这个地址上
概念二: rebase
这里开始体现出rebase的威力了, 实际上由于进程地址空间随机化, 提醒一下, 这里真实的内存虚拟地址不是f7c, 仅为了调试方便, xcode调试模式运行的程序, 进程起始地址固定在0x100000000, 也就是0+4GB(其中4GB是__PAGEZERO陷阱区). 正常情况下, 由于空间随机化的存在, 在dyld加载可执行文件到内存之后, 会对所有指向进程内的符号地址进行调整, 比如f7c这个地址, 调整为进程头部随机化后的地址xxxx+f7c, 可以看出来, 因为符号_printf的地址需要进程被加载后才能确定, 所以被放到__DATA区, 方便修改, 修改的过程称为rebase.
概念三: bind
接着上文, 从MachOView工具上看, 0x2010这个地址位于__DATA,__la_symbol_prt节上. (经过调试可以看出, __la_symbol_prt就是存放赖加载符号的地址, 符号完成bind之前, 存放的是__stub_helper区对应符号的地址. bind之后, 真正的地址就会被写进__la_symbol_prt对应符号上)
上图可以看到, bind之前, 0x2010的值确实是地址f7c, 所以前文在调试stubs的时候, 程序的确跳到了f7c这个地址上.
继续跟踪这个地址, 可以看到f7c这个地址是位于[TEXT, __stub_helper]这个section中的, 也就是代码区. 通过MachOView工具, 可以直接看到汇编代码
先push一个常数0x1a到栈上(这个常数是Dynamic Loader Info -> Lazy Binding info -> Actions的偏移, 根据这个参数可以找到具体符号), 然后跳到0xf58这个地址上, 也就是上图第一行. 直接看, 看不出含义, 直接调试汇编可以看到xcode的注释.
可以看到xcode帮我们注释出来的两个符号, 其中dyld_stub_binder是动态库里的函数, 而且已经被bind好地址了. 注意, 这个符号非延迟绑定符号, 会在dyld载入进程的时候就查找并绑定, 可以在Section(__DATA_CONST, __got)这个节里找到这个符号
调试程序, 直接读取0x100001000地址的内存数据, 可以看到被内容已经不是0了, 而是真实地址 0x7fff6ef89578
可以看出来, 这几行指令执行下去, 就会进入dyld_stub_binder这个函数中, 从相关动态库里找到_printf符号, 进行绑定. 这里需要注意的是, _printf这个符号是一个延迟绑定符号, 所以执行完dyld_stub_binder函数之后, 真实的地址会被写入到0x2010这个指针上(位于__la_symbol_prt), 下次再调用_printf符号时, 逻辑和第一次一样, 先调用__stub中, 然后获取0x2010这个指针的值, 内容不再是f7c了, 而是真实的符号地址.
第二次调用_printf时, 就可以正确跳到该符号在动态库中的真实地址了. 通过下面lldb调试指令memory read 0x100002010
, 可以读取到里面保存的符号真实地址
到这里, 延迟绑定的符号, 也顺利进行了bind了. 以上就是MachO文件中, rebase和bind的执行原理, 可以看出来, 为了优化程序的启动速度, 兼顾动态库的灵活性, 系统设计师又发明了延迟绑定这种简单又巧妙的技术, 佩服佩服…
其他问题
这篇文章中, 还有以下未解决的问题.
- 如何根据动态库的符号表, 找到符号在动态库中的地址?
dyld_stub_binder函数的工作原理
未完待续
相关工具下载
作者:Cocos
链接:https://juejin.cn/post/6896728016953540621