前言
iOS-Debug-Hacks involves the dynamic debugging, static analysis and decompile of third-party libraries.
《Advanced Apple Debugging & Reverse Engineering》值得一看
函数
谈谈对函数的理解
一个函数调用包括将数据(以参数和返回值的形式)和控制从代码的一部分转移到另一部分。在函数调用过程中,数据传递、局部变量的分配和释放是通过栈来实现的,而为单个函数调用分配的那部分栈称为栈帧(Stack Frame)。
栈帧
- 使用 bt 命令打印出当前线程的回溯信息
(lldb) bt
* thread #1, stop reason = trace
* frame #0: 0x000000010e533fcf dyld`_dyld_debugger_notification + 1
frame #1: 0x000000010e533795 dyld`gdb_image_notifier(dyld_image_mode, unsigned int, dyld_image_info const*) + 111
frame #2: 0x000000010a36b269 dyld_sim`notifyGDB(dyld_image_states, unsigned int, dyld_image_info const*) + 40
frame #3: 0x000000010a364142 dyld_sim`dyld::notifyBatchPartial(dyld_image_states, bool, char const* (*)(dyld_image_states, unsigned int, dyld_image_info const*), bool, bool) + 814
frame #4: 0x000000010a36d107 dyld_sim`ImageLoader::link(ImageLoader::LinkContext const&, bool, bool, bool, ImageLoader::RPathChain const&, char const*) + 101
frame #5: 0x000000010a364548 dyld_sim`dyld::link(ImageLoader*, bool, bool, ImageLoader::RPathChain const&, unsigned int) + 161
frame #6: 0x000000010a365a5c dyld_sim`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 3840
frame #7: 0x000000010a3613d4 dyld_sim`start_sim + 136
frame #8: 0x000000010e52bded dyld`dyld::useSimulatorDyld(int, macho_header const*, char const*, int, char const**, char const**, char const**, unsigned long*, unsigned long*) + 2200
frame #9: 0x000000010e5297a3 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 436
frame #10: 0x000000010e5253d4 dyld`dyldbootstrap::start(macho_header const*, int, char const**, long, macho_header const*, unsigned long*) + 453
frame #11: 0x000000010e5251d2 dyld`_dyld_start + 54
bt 命令是得益于栈帧才能实现的,栈帧可以看成是函数执行的上下文,其中保存了函数的返回地址和局部变量,我们知道堆是从低地址向高地址延伸的,而栈是从高地址向低地址延伸的。
每个函数的每次调用,都会分配给它一个独立的栈帧,rbp 寄存器指向当前栈帧的底部(高地址),被称作帧指针,rsp 寄存器指向栈帧的顶部(低地址),被称作栈指针
一次函数调用的过程
- 调用函数将参数压栈,如果没有参数,或者可以直接通过寄存器完成传参,则这步可以没有。
- 将执行完函数调用的下一条指令压栈,其实就是返回地址。
- 跳转到被调函数的起始地址开始执行。
- 被调函数将调用函数栈帧起始地址压栈,栈帧起始地址存放在 %rbp 寄存器中。
- 将 %rsp 寄存器赋值给 %rbp 寄存器,使得 %rbp 寄存器指向被调函数栈帧的起始地址。
- 将被调用者保存寄存器压栈,这步是可选的。
上述 2 和 3 步骤其实就是 call 指令的任务,而 4 和 5 通过汇编指令表示如下:
0x1054e09c0 <+0>: pushq %rbp //第四步
0x1054e09c1 <+1>: movq %rsp, %rbp //第五步
Call 指令
call function
参数中的 function 是 TEXT 段的程序,
call 指令其实可以拆解成两步,
- 第一步是将执行完 call 指令之后的地址压栈,这个地址其实是执行完调用函数体之后的返回地址;
- 第二步是将指令执行跳转到 function。
call 指令其实等价于下面的命令:
push next_instruction
jmp function
传参和返回值
在 OSX 中,最多可以有 6 个整型(整数和指针)通过寄存器传递,这 6 个寄存器分别是 rdi, rsi, rdx, rcx, r8 和 r9(顺序和参数的顺序保持一致),
- 如果一个函数超过 6 个参数该怎么办呢?
此时就需要借助栈了,可以将剩下的参数逆序压入栈中。OSX 允许将 8 个浮点数通过浮点数寄存器 xmm0-xmm7 进行传递。
- 函数的返回值,
使用 rax 寄存器作为整数返回值,浮点数返回值则使用 xmm0-xmm1 寄存器
输出寄存器的值
当寄存器的值是字符串的时候,LLDB 可以通过
po (char *) $rsi
命令输出寄存器对应的字符串值,
否则直接使用 po $rsi
,只会按照整数格式输出 rsi 寄存器的值。
OC 的消息转发
消息转发流程越往后,处理消息所付出的代价也就越大。所以若非必要,应当尽早结束消息转发流程。如果消息转发的流程中都没有处理未知消息,最终会调用 doesNotRecognizeSelector: 抛出异常,表示对象无法正确识别此 SEL。
补充
@dynamic
使用 @dynamic 告诉编译器不做处理,然后 Getter 和 Setter 方法是在运行时动态创建
打断点
- br s -n
br s 是 breakpoint set 的意思,-n 表示根据函数名称来下断点,作用和符号断点一样,
- 使用
br s -F
- 直接使用
b -[TCWebViewKit open]
只不过 b 命令是 _regexp-break 的缩写,是用正则匹配的方式下断点。
-给在指定的内存地址设置断点,
下断点的命令为 br s -a 0x000000010940b24e
,这种方式可以用在知道 block 内存地址的时候,给 block 设置断点。