跟着 WWDC 一起探秘符号解析的魔法

栏目: IOS · 发布时间: 6年前

内容简介:在我们都知道,在事实上,由于大量系统库以及

Objective-C 的世界中,对对象的方法调用都会被转为消息发送,通过 self_cmd ,在 method_list 中寻找对应的函数指针,最终触发函数调用。对于我们自己代码中的类,这很好理解,Runtime 会维护一个巨大的列表,存储着我们的类的信息。但对于系统 framework 中的类型呢?难道 Runtime 会在启动时预先将所有系统类也加载进来吗?更广义一点说,诸如 printf 这样的方法又是如何找到函数指针的呢?

剧透在前

我们都知道,在 .m 中编写代码时,我们只需要引入 .h 头文件,就可以使用对应文件中定义的类与方法,而不需要关心对应实现。这是因为,在静态阶段,我们并不真正执行任何二进制代码,只是生成指令而已,因此,我们只需要知道我们希望调用的函数的 符号 ,而不需要知道地址。而这些符号,将在运行时被转化为实际的内存地址,以供调用。

事实上,由于大量系统库以及 ASLR 的存在,我们也不可能在静态阶段就得到所有实际代码的执行地址。

macOSiOS 系统中,可执行文件的格式为 Mach-OMach-O 内部一般由三个部分组成:

跟着 WWDC 一起探秘符号解析的魔法

其中 __TEXT 段为存放应用代码以及常量的地方,为 只读 属性。 __DATA 段存放全局变量、静态变量等数据,为 读写 属性。 __LINKEDIT 存放着加载该二进制时的一些元数据。这张截图自 2016 年的 Optimizing App Startup Time ,对以上三个段的解释简短清晰,但也留下了一些问题:在我们自己编写的代码中,夹杂了对系统库的调用,这些调用在编译后会变成符号,但我们的代码最终是放在只读的 __TEXT 段的,系统要怎么把符号转成地址呢?另外 __LINKEDIT 又到底存放了哪些信息呢?

从汇编说起

要弄清楚这些问题,就要先看看我们的代码经过编译,究竟会变成什么样,考虑如下代码:

@implementation ViewController
- (void)viewDidLoad {
    NSDictionary *obj = [NSDictionary new];
    NSLog(@"%@",obj);
}
@end

对应的汇编在精简后大概是这样(所有 .开头的均不是实际汇编代码):

"-[ViewController viewDidLoad]":        ; @"\01-[ViewController viewDidLoad]"
Lfunc_begin0:
	sub	sp, sp, #48             ; =48
	stp	x29, x30, [sp, #32]     ; 8-byte Folded Spill
	add	x29, sp, #32            ; =32
    
	adrp	x8, l_OBJC_SELECTOR_REFERENCES_@PAGE
	add	x8, x8, l_OBJC_SELECTOR_REFERENCES_@PAGEOFF
	adrp	x9, l_OBJC_CLASSLIST_REFERENCES_$_@PAGE
	add	x9, x9, l_OBJC_CLASSLIST_REFERENCES_$_@PAGEOFF
	stur	x0, [x29, #-8]
	str	x1, [sp, #16]
Ltmp0:
	ldr	x9, [x9]
	ldr	x1, [x8]
	mov	x0, x9
	bl	_objc_msgSend
	str	x0, [sp, #8]
	ldr	x8, [sp, #8]
	mov	x9, sp
	str	x8, [x9]
	adrp	x0, l__unnamed_cfstring_@PAGE
	add	x0, x0, l__unnamed_cfstring_@PAGEOFF
	bl	_NSLog
	add	x9, sp, #8              ; =8
	mov	x0, x9
	mov	x1, x8
	bl	_objc_storeStrong
	ldp	x29, x30, [sp, #32]     ; 8-byte Folded Reload
	add	sp, sp, #48             ; =48
	ret

如果你不熟悉汇编,也没太大关系,其中大部分代码都是在往“正确”的寄存器里面塞参数,保证函数调用的正确性,我们关心的是这几句:

// selector 以及 class 的符号
adrp	x8, l_OBJC_SELECTOR_REFERENCES_@PAGE
add	x8, x8, l_OBJC_SELECTOR_REFERENCES_@PAGEOFF
adrp	x9, l_OBJC_CLASSLIST_REFERENCES_$_@PAGE
add	x9, x9, l_OBJC_CLASSLIST_REFERENCES_$_@PAGEOFF
// 常量 string 的符号
adrp	x0, l__unnamed_cfstring_@PAGE
add	x0, x0, l__unnamed_cfstring_@PAGEOFF

你可以在 ARM Information Center 搜到 adrp 的详细文档,简单地说它就是用来取某个“符号”的地址的

限于篇幅,我们先只关注其中 NSDictionary 相关的,在汇编代码中搜索 l_OBJC_CLASSLIST_REFERENCES_ ,最终可以发现他落在这样一个地方:

.section	__DATA,__objc_classrefs,regular,no_dead_strip
.p2align	3               ; @"OBJC_CLASSLIST_REFERENCES_$_"
l_OBJC_CLASSLIST_REFERENCES_$_:
.quad	_OBJC_CLASS_$_NSDictionary

可以看到,这个符号实际是在 __DATA 段中的,也就是说,我们在 __TEXT 中的符号,实际指向的是 __DATA 段里的东西

挖掘 Mach-O

到这一步,我们就需要借助一些工具,来继续看看 __DATA 段又发生了什么事情,我个人使用 MachOView 来查看文件内部大体布局,用 Hopper 来查看具体内存地址是什么。

根据上面汇编代码给出的信息,我们很容易就可以在 __objc_classrefs 中找到我们的指针。

跟着 WWDC 一起探秘符号解析的魔法

我们利用 offset,在 Hopper 中跳到对应地址,将会看到另一条非常简单的汇编语句:

跟着 WWDC 一起探秘符号解析的魔法

Hopper 提供非常强大的跳转功能,双击图中黄色高亮的符号,又可以跳到该符号定义的地方:

跟着 WWDC 一起探秘符号解析的魔法

如果你稍稍向上滑动一下,会发现这段代码对应在 __DATA 段的 External Symbols Segment 中,而除了我们的 _OBJC_CLASS_$_NSDictionary ,还有诸如 UIResponderNSObject 以及 NSLog 等等系统符号。就目前来看,符号最终指向了 0x00 ,也就是 null ,这也是符合预期的,正如前文提到的,这些系统库在 runtime 时的地址是我们编译阶段无法确定的,需要在装载可执行文件时动态确定,也就是说,系统一定会在某个时机,将 0x00 改为实际地址。

那么这一步发生在什么时候呢?

跟着 WWDC 一起探秘符号解析的魔法

截图同样来自 Optimizing App Startup Time ,在系统动态加载 Mach-O 文件的时候,会经过 Rebase 以及 Bind 两个阶段,其中 Rebase 是将内部指针进行固定数值的偏移(slide),而 Bind 则正是用于将外部符号转为实际指针的步骤。在 2018 年的 Behind the Scenes of the Xcode Build Process session 中也提到了这一个步骤:

跟着 WWDC 一起探秘符号解析的魔法

可以看到,整个流程 1-2-3-4 均和我们上面的分析相符,而上图中的最后一个步骤正是 Bind__LINKEDIT 中的信息就是建立应用内符号到系统函数符号的映射。这时候我们打开 MachOView 找到对应 Bind 阶段的信息,可以发现 _OBJC_CLASS_$_NSDictionary 的确是在这一步完成绑定的。

跟着 WWDC 一起探秘符号解析的魔法

至此,符号解析的流程就走通了, __TEXT 中的代码段指向 __DATA 中的符号,在装载二进制时,系统会根据 __LINKEDIT 中的信息,再将 __DATA 中的符号和实际系统函数地址建立映射。

Lazy Binding

细心的你一定还发现, __DATA 段中还有一个 __la_symbol_ptr ,而上面的截图中也存在 Lazy Binding Info 的字样。这是因为,并不是所有符号都是在启动时进行解析绑定的,出于性能考虑,一部分符号将会在首次调用时进行绑定。那么绑定的过程是如何的呢?

为了方便调试,我们稍微修改一下代码,让 app 启动时直接进入 lazybind 流程。

//main.m
int main(int argc, char * argv[]) {
    printf("hi, pritf");
}

重新编译后,用 Hopper 重新打开你的 Mach-O ,记得 不要 勾选 Resolve Lazy Bindings。

用寻找 NSDictionary 符号相似的方法,我们可以很快在 Hopper 中定位到如下位置:

跟着 WWDC 一起探秘符号解析的魔法

若继续跟踪代码,我们会来到一个似乎看不出什么端倪的地方:

跟着 WWDC 一起探秘符号解析的魔法

这段代码实际属于 __stub_helper ,不确定是否是我所使用的 Hopper 版本问题,同样的段落在 MachOView 中查看,可以看到正确的代码:

跟着 WWDC 一起探秘符号解析的魔法

这里,我们将 0x100006c38 地址处的内容,存入了 w16 ,回到 Hopper ,可以看到该处的值为 0x46 (具体值因 lazy pointer 数量而异)。随后我们将调用 0x100006bf4 处的函数:

跟着 WWDC 一起探秘符号解析的魔法

此处逻辑是将 0x100008008 指针 传递给 x17 ,随后调用 dyld_stub_binder 方法。

该方法是 dyld 内部的方法( 源码在这 ),作用就是将 __la_symbol_ptr 当前指向 __stub_helpers 段的指针绑定到真实的函数地址上。结合 dyld 源码以及 Xcode 的 Always Show Assembly 选项,我们得以了解这个绑定的大概流程:

跟着 WWDC 一起探秘符号解析的魔法

我们传入的首参,也就是之前放置于 x17 处的指针,实际上是用于让 dyld 创建 ImageLoader 的内存区域, ImageLoader 负责处理可执行文件及其依赖关系的抽象类,在本例中,其具体实例为 ImageLoaderMachOCompressed 类,在它的头文件中,也注明了它的作用:

ImageLoaderMachOCompressed is the concrete subclass of ImageLoader which loads mach-o files that use the compressed LINKEDIT format.

dyld 创建了负责处理 __LINKEDIT 信息的实例后,程序最终会进入:

ImageLoaderMachOCompressed::doBindFastLazySymbol(uint32_t lazyBindingInfoOffset, 
const LinkContext& context, 
void (*lock)(), 
void (*unlock)())

其中参数 context 为初始化时 dyld 为该实例设置的一系列上下文信息, lockunlock block 则为特定环境中的加解锁方法。简单阅读该方法,可以发现它主要干这么几件事:

首先,通过 fLinkEditBase + fDyldInfo->lazy_bind_off 确定 __LINKEDIT 段中存放 Lazy Binding Info 信息的基址,然后根据我们传入的偏移值(也就是最开始塞进 w16 中的 0x46 ),定位到具体位置。

回头在 MachOView 上看看这块的信息:

跟着 WWDC 一起探秘符号解析的魔法

0x46 正好就是基址 F0printf 信息段 36 的偏移量。到了这一步,我们就可以拿到:

  1. __la_symbol_ptr 段中,待改写的指针地址。
    跟着 WWDC 一起探秘符号解析的魔法
  2. 我们想要找到的目标符号以及其对应在哪个 dylib 之中。

接着,调用 bindAt 方法,通过上一步中获取的符号名称,在目标库中获取函数地址(也就是 printflibSystem.B.dylib 中的实际地址),然后调用 bindLocation 改写对应 __la_symbol_ptr 中的指向。

实际运行起来,也证实了我们的想法:

跟着 WWDC 一起探秘符号解析的魔法

resolve 之前,我们已经拿到了 _printf 符号和待改写指针,在 bindLocation 之前,我们已经拿到了 printf 的真实地址了,而在 bindLocation 之后,我们的 __la_symbol_ptr 就指向实际函数地址,变得不再 lazy 了。


以上所述就是小编给大家介绍的《跟着 WWDC 一起探秘符号解析的魔法》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Java夜未眠

Java夜未眠

蔡学镛 / 电子工业出版社 / 2003-4 / 20.00元

本书是一本散文集。作为一名资深程序设计师,作者走笔清新面独特,简练俏皮的文字下,是作者对工作,对人生的理性思考。书中收录的文章内容贴近程序员的生活,能令读者产生强烈共鸣。此外,书中的部分文章也以轻松的风格剖析了学习Java技术时的常见问题,并以专家眼光和经验推荐介绍了一批优秀的技术书籍,旨在帮助读者兴趣盎然地学习Java。一起来看看 《Java夜未眠》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

SHA 加密
SHA 加密

SHA 加密工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试