相关的数据结构与函数
-
MachO 文件中的链接信息段(LinkEdit Segment)
LC_SEGMENT_64(__LINKEDIT)
命令用于描述链接信息段,其在 MachOView 中对应的图形界面,如下所示:
LC_SEGMENT_64(__LINKEDIT)
命令对应的数据结构,如下所示:#import <mach-o/loader.h> // (64 位的)段 // 一个段(segment)可以包含 0 到多个节(section) // 一个段(segment)的所有节(section),按顺序紧跟在该段(segment)之后 struct segment_command_64 { uint32_t cmd; /* 加载指令类型 */ uint32_t cmdsize; /* 加载指令的大小,包括 segment 所包含的所有 section 的大小,单位:Byte */ char segname[16]; /* 16 Byte 的段名 */ uint64_t vmaddr; /* 段的虚拟内存起始地址(Byte) */ uint64_t vmsize; /* 段所占的虚拟内存的大小(Byte) */ uint64_t fileoff; /* 段数据在文件中的偏移(Byte) */ uint64_t filesize; /* 段数据在文件中的大小(Byte) */ vm_prot_t maxprot; /* 段页面(内存分页)所需要的最高内存保护(r=4, w=2, x=1) */ vm_prot_t initprot; /* 段页面(内存分页)初始的内存保护(r=4, w=2, x=1) */ uint32_t nsects; /* 段(segment)中所包含的节(section)的数量 */ uint32_t flags; /* 标志信息 */ };
关于空指针陷阱段
LC_SEGMENT_64(__PAGEZERO)
:
这是一个不可读、不可写、不可执行的空间,能够在空指针访问时抛出异常(用于捕捉对空指针的引用)
在 64 位的操作系统上,这个段的虚拟内存大小是4GB
4GB
并不是指该段物理文件的真实大小,也不是指该段所占物理内存的真实大小
4GB
是规定了进程地址空间的前4GB
被映射为:不可读、不可写、不可执行的空间
这就是为什么当读写一个NULL(0x0)
指针时会得到一个EXC_BAD_ACCESS
错误
注意:空指针陷阱段LC_SEGMENT_64(__PAGEZERO)
的物理文件大小为0
特别地,对于代码段
LC_SEGMENT_64(__TEXT)
、数据段LC_SEGMENT_64(__DATA)
、链接信息段LC_SEGMENT_64(__LINKEDIT)
都有:segment_command_64.vmaddr - segment_command_64.fileoff = 0x00000001 00000000 = 4GB = 空指针陷阱段在虚拟内存中的大小
例如,在上述链接信息段LC_SEGMENT_64(__LINKEDIT)
中:VM Address = 0x00000001 00010000
,File Offset = 0x00000000 00010000
则有:VM Address - File Offset = 0x00000001 00010000 - 0x00000000 00010000 = 0x00000001 00000000 = 4GB = 空指针陷阱段在虚拟内存中的大小
并且,
空指针陷阱段在虚拟内存中的大小 + 程序本次运行时 ASLR 的偏移量 = 程序本次运行时在虚拟内存中的基地址
-
MachO 文件中的符号表(Symbol Table)
LC_SYMTAB
命令用于描述符号表的位置和大小(即用于描述符号表的地址信息),其在 MachOView 中对应的图形界面,如下所示:
LC_SYMTAB
命令对应的数据结构,如下所示:#import <mach-o/loader.h> struct symtab_command { uint32_t cmd; /* LC_SYMTAB */ uint32_t cmdsize; /* sizeof(struct symtab_command) */ uint32_t symoff; /* 符号表相对于基地址的偏移量 */ uint32_t nsyms; /* 符号表中元素的数量 */ uint32_t stroff; /* 字符串表相对于基地址的偏移量(字符串表记录了所有符号的名字) */ uint32_t strsize; /* 字符串表的总大小(Byte) */ };
符号表是一个
struct nlist
类型的数组,包含了用于静态链接与动态链接的符号的信息。其在 MachOView 中对应的图形界面,如下所示:
符号表的元素对应的数据结构,如下所示:#import <mach-o/nlist.h> struct nlist_64 { union { uint32_t n_strx; /* 符号的名称在字符串表(String Table)中的索引 */ } n_un; uint8_t n_type; /* 符号的类型标识,可选的值有:N_STAB、N_PEXT、N_TYPE、N_EXT */ uint8_t n_sect; /* 符号所在的 section 的索引。如果没有对应的 section,则为 NO_SECT */ uint16_t n_desc; /* 符号的描述,see <mach-o/stab.h> */ uint64_t n_value; /* 符号所在的地址 或 stab 的偏移量 */ };
因为 MachOView 进行的是静态分析,所以 MachO 文件并未真正地在内存中运行
因此,ASLR 的偏移量 = 0
,基地址 = 空指针陷阱段在虚拟内存中的大小 + 程序本次运行时 ASLR 的偏移量 = 0x00000001 00000000 + 0x0 = 0x00000001 00000000
因此,符号表的首地址 = 基地址 + 符号表相对于基地址的偏移量 = 0x00000001 00000000 + 0x00000000 00010438 = 0x00000001 00010438
-
MachO 文件中的间接符号表(Dynamic Symbol Table)
LC_DYSYMTAB
命令用于描述间接符号表,间接符号表包含了:
① 一组指向符号表中符号的索引
② 一组定义了其他几个表位置的偏移量
其在 MachOView 中对应的图形界面,如下所示:
LC_DYSYMTAB
命令对应的数据结构,如下所示:#import <mach-o/loader.h> struct dysymtab_command { uint32_t cmd; /* LC_DYSYMTAB */ uint32_t cmdsize; /* sizeof(struct dysymtab_command) */ // 内部自行使用的符号在符号表(Symbol Table)中的索引与数量 uint32_t ilocalsym; /* index to local symbols */ uint32_t nlocalsym; /* number of local symbols */ // 导出给外部使用的符号在符号表(Symbol Table)中的索引与数量 uint32_t iextdefsym; /* index to externally defined symbols */ uint32_t nextdefsym; /* number of externally defined symbols */ // 用于懒绑定的符号在符号表(Symbol Table)中的索引与数量 uint32_t iundefsym; /* index to undefined symbols */ uint32_t nundefsym; /* number of undefined symbols */ // contents 表相对于基地址的偏移量与元素个数 uint32_t tocoff; /* file offset to table of contents */ uint32_t ntoc; /* number of entries in table of contents */ // module 表相对于基地址的偏移量与元素个数 uint32_t modtaboff; /* file offset to module table */ uint32_t nmodtab; /* number of module table entries */ // 引用符号表相对于基地址的偏移量与元素个数 uint32_t extrefsymoff; /* offset to referenced symbol table */ uint32_t nextrefsyms; /* number of referenced symbol table entries */ // 间接符号表相对于基地址的偏移量与元素个数 uint32_t indirectsymoff; /* file offset to the indirect symbol table */ uint32_t nindirectsyms; /* number of indirect symbol table entries */ // 外部重定位元素相对于基地址的偏移量与元素个数 uint32_t extreloff; /* offset to external relocation entries */ uint32_t nextrel; /* number of external relocation entries */ // 内部重定位元素相对于基地址的偏移量与元素个数 uint32_t locreloff; /* offset to local relocation entries */ uint32_t nlocrel; /* number of local relocation entries */ };
间接符号表是一个
uint32_t
类型的数组,包含动态符号在符号表中的索引。间接符号表的元素是一个个uint32_t
类型的索引值,用于标识动态符号在符号表中的位置。其在 MachOView 中对应的图形界面,如下所示:
因为 MachOView 进行的是静态分析,所以 MachO 文件并未真正地在内存中运行
因此,ASLR 的偏移量 = 0
,基地址 = 空指针陷阱段在虚拟内存中的大小 + 程序本次运行时 ASLR 的偏移量 = 0x00000001 00000000 + 0x0 = 0x00000001 00000000
因此,间接符号表的首地址 = 基地址 + 间接符号表相对于基地址的偏移量 = 0x00000001 00000000 + 0x00000000 00011128 = 0x00000001 00011128
间接符号表如何根据所存储的索引值,定位动态符号在符号表中的位置:
间接符号表的第 0 个元素所存储的索引值为0x000000B8(184)
,其用于标识动态符号_NSLog
为符号表的第184
个元素
符号表的基地址为0x00000001 00010438
,符号表的元素的大小为sizeof(nlist_64) = 16
因此,动态符号_NSLog
在符号表中的地址为0x00000001 00010438 + 16 * 184 = 0x00000001 00010FB8
-
MachO 文件中的字符串表(String Table)
LC_SYMTAB
命令也用于描述字符串表的位置和大小(即也用于描述字符串表的地址信息),其在 MachOView 中对应的图形界面,如下所示:
LC_SYMTAB
命令对应的数据结构,如下所示:#import <mach-o/loader.h> struct symtab_command { uint32_t cmd; /* LC_SYMTAB */ uint32_t cmdsize; /* sizeof(struct symtab_command) */ uint32_t symoff; /* 符号表相对于基地址的偏移量 */ uint32_t nsyms; /* 符号表中元素的数量 */ uint32_t stroff; /* 字符串表相对于基地址的偏移量(字符串表记录了所有符号的名字) */ uint32_t strsize; /* 字符串表的总大小(Byte) */ };
字符串表用于存储符号的名称。字符串表在存储符号名称时,会在每一个符号名称的末尾增加一个
'\0'
,而在 C 语言中一次字符串的获取会以'\0'
为结束。其在 MachOView 中对应的图形界面,如下所示:
因为 MachOView 进行的是静态分析,所以 MachO 文件并未真正地在内存中运行
因此,ASLR 的偏移量 = 0
,基地址 = 空指针陷阱段在虚拟内存中的大小 + 程序本次运行时 ASLR 的偏移量 = 0x00000001 00000000 + 0x0 = 0x00000001 00000000
因此,字符串表的首地址 = 基地址 + 字符串表相对于基地址的偏移量 = 0x00000001 00000000 + 0x00000000 000111A0 = 0x00000001 000111A0
符号表如何根据所存储的字符串表索引值,定位符号名称在字符串表中的位置:
符号表的第 0 个元素所存储的字符串表索引值为String Table Index = 0x00000220(544)
,其用于标识符号-[ViewController viewDidLoad]
的名称在字符串表中的起始索引为544
字符串表的基地址为0x00000001 000111A0
,每个字符的大小为sizeof(char) = 1
因此,符号-[ViewController viewDidLoad]
的名称在字符串表中的起始地址为0x00000001 000111A0 + 1 * 544 = 0x00000001 000113C0
-
向 dyld 注册镜像加载的回调(_dyld_register_func_for_add_image)
/* 以下函数允许你向 dyld 注册回调函数,并且每当 dyld 加载或者卸载镜像时,dyld 将会调用这些注册的回调函数 1.在调用 _dyld_register_func_for_add_image() 向 dyld 注册回调函数期间,会对每个已加载的镜像调用注册的回调函数 func 之后,每当加载和绑定新镜像时(此时该镜像还未执行初始化),也会调用注册的回调函数 func 2.每当镜像在调用终止器之后从内存中卸载之前,都会调用通过 _dyld_register_func_for_remove_image() 向 dyld 注册的回调函数 3.从以下两个函数的声明中可以看出:所注册的回调函数的参数列表一定是 (const struct mach_header* mh, intptr_t vmaddr_slide) */ extern void _dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide)); extern void _dyld_register_func_for_remove_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide));
-
intptr_t 和 uintptr_t
/* Types for void * pointers. */ #if __WORDSIZE == 64 # ifndef __intptr_t_defined typedef long int intptr_t; # define __intptr_t_defined # endif typedef unsigned long int uintptr_t; #else # ifndef __intptr_t_defined typedef int intptr_t; # define __intptr_t_defined # endif typedef unsigned int uintptr_t; #endif
intptr_t
和uintptr_t
这两种数据类型用于存储指针地址intptr_t
不是一个整型指针,而是一个整数。它在 64 位平台中被定义为long int
,在 32 位平台中被定义为int
我们都知道 C 语言的指针实际上就是变量的地址,在 64 位平台上指针的长度为 8 个字节,在 32 位平台上指针的长度为 4 个字节
而intptr_t
的容量刚好和这个字节数相等,使用intptr_t
可以安全地进行整数与指针的转换运算,也就是说当需要将指针作为整数运算时,将指针转换成intptr_t
进行运算才是安全的intptr_t
在不同的平台上编译时长度不同,但都是标准的平台字长的存储量
iOS 系统的懒绑定机制
-
iOS 的懒绑定流程 && MachO 相关的数据结构
在 XCode 中新建一个 iOS Project:LazyBindingDemo
并使用如下代码来探究 iOS 系统动态库Foundation.framwork
的NSLog
函数是如何被懒绑定的
使用 Release 配置构建项目 LazyBindingDemo,并使用 MachOView 打开主程序的 MachO 文件
① 在 iOS 系统中,当程序调用动态库的函数时,它实际上执行的是桩节区Section64(__TEXT, __stubs)
处的代码
下图红框标出的部分便是用来调用NSLog
函数的桩(Symbol Stub
)
它的地址是0x100006524
(先记住这个地址,后面通过 LLDB 调试验证的时候会用到)
② 外部函数的地址被存储在Section64(__DATA, __la_symbol_ptr)
中,而桩(Symbol Stub
)的作用便是获取相应的Lazy Symbol Pointer
并跳转到它所包含的地址
此处NSLog
函数的Lazy Symbol Pointer
所记录的地址为0x00000001 000065E4
③ 当我们第一次调用NSLog
函数时,Lazy Symbol Pointer
尚未记录NSLog
函数的真实地址,而是指向Section64(__TEXT, __stub_helper)
中相关的内容。在Section64(__TEXT, __stub_helper)
中,它将懒绑定函数dyld_stub_binder
所需的参数放到寄存器 w16
中,之后跳转到地址0x000065CC
处,也就是Section64(__TEXT, __stub_helper)
的头部,然后调用懒绑定函数dyld_stub_binder
进行符号绑定,懒绑定函数dyld_stub_binder
负责找到NSLog
函数的真实地址并将其回写到Section64(__DATA, __la_symbol_ptr)
中对应的Lazy Symbol Pointer
④ 仔细观察后发现,寄存器 w16
实际上是存放一个int
类型的值,那么这个int
类型的值究竟代表什么呢?为什么懒绑定函数dyld_stub_binder
可以利用它来绑定符号?实际上,它是相对于Lazy Binding Info
的偏移量(在LINKEDIT
段的Dynamic Loader Info
中)
懒绑定函数dyld_stub_binder
根据这个偏移量便可从Lazy Binding Info
中找到绑定过程所需的信息(比如:到系统的Foundation
动态库中寻找NSLog
函数)
-
通过 LLDB 的调试,验证懒绑定流程
上面介绍了 iOS 的懒绑定流程 && MachO 文件中用于支持懒绑定的数据结构
接下来通过 LLDB 调试项目:LazyBindingDemo 来验证以上的内容
在两个NSLog
输出语句中都下断点
并在调试的时候显示汇编代码(XCode - Debug - Debug Wrokflow - Always Show Disassembly
)
① 程序运行到NSLog(@"First");
处,我们可以看到程序实际上是跳转到地址0x104116524
处的桩(Symbol Stub
)代码。但是等等,我们之前在 MachOView 中观察到程序此时应该跳转到地址0x100006524
处才对,但是为什么这里的地址却是0x104116524
呢?
这是因为 iOS 系统在加载 MachO 文件的时候,使用了 ASLR 技术(地址空间布局随机化)。通过计算0x104116524 - 0x100006524
可以得到程序此次加载的偏移量为0x04110000
② 根据上一小节的介绍:
当程序调用动态库的函数时,它实际上是执行桩节区Section64(__TEXT, __stubs)
处的代码
那么地址0x104116524
处对应的汇编代码,应该就是NSLog
函数的桩(Symbol Stub
)
我们通过 LLDB 打印地址0x104116524
处对应的汇编代码
LLDB 的输出结果,与前面用 MachOView 显示的NSLog
函数的桩(Symbol Stub
)是一致的
③ 因为是首次调用NSLog
函数,所以地址0x00000001 041165e4
处记录的,应该是Section64(__TEXT, __stub_helper)
中NSLog
函数执行懒绑定前,用于准备懒绑定的参数的代码
我们通过 LLDB 打印地址0x00000001 041165e4
处对应的汇编代码
LLDB 的输出结果,与前面用 MachOView 显示的NSLog
函数执行懒绑定前,准备参数的代码是一致的
④ 那么可想而知,地址0x1041165cc
应该就是Section64(__TEXT, __stub_helper)
的首地址,其记录的应该就是对懒绑定函数dyld_stub_binder
的调用
我们通过 LLDB 打印地址0x1041165cc
处对应的汇编代码
果不其然,地址0x1041165cc
记录的就是对懒绑定函数dyld_stub_binder
的调用
出于好奇,我们通过地址0x00000001 a986e08c
进去看一下懒绑定函数长什么样子
⑤ iOS 系统首次调用NSLog
函数进行懒绑定的流程,我们已经验证完了
接下来清空 LLDB 的输出并过掉第一个断点,程序运行到NSLog(@"Second");
处
我们接着探索 iOS 系统第二次调用NSLog
函数的流程
可想而知,地址0x104116524
记录的应该还是NSLog
函数在桩节区Section64(__TEXT, __stubs)
的桩(Symbol Stub
)
我们通过 LLDB 打印地址0x104116524
处对应的汇编代码,LLDB 的输出结果与预期相符
⑥ 我们注意到, 此时Section64(__TEXT, __stubs)
中NSLog
函数的桩(Symbol Stub
)获取到的不再是指向Section64(__TEXT, __stub_helper)
的调用,而是NSLog
函数的真实地址
这也证实了,懒绑定只会在外部函数首次调用的时候执行一次
最后,出于严谨,我们还是要验证一下地址0x00000001 a9e6253c
处是不是存储着NSLog
函数对应的汇编代码
如何获取到 Lazy Symbol Pointers 对应的函数名
-
思考
先回忆起上节在讲 iOS 系统的懒绑定机制 时的这张图
这里是通过 MachOView 解析的,项目LazyBindingDemo
的 MachO 文件中的Lazy Symbol Pointers
在调用NSLog
函数时,会到这里获取NSLog
函数的地址,然后跳转执行根据上节对 iOS 系统懒绑定机制的介绍:
第一次从这里获取到的地址值会指向stub_helper
第二次从这里获取到的地址值会指向NSLog
函数的入口
留意到上图最右边的 Value 列,这里 MachOView 已经帮我们解析出:地址0x10000C000
存储的是指向NSLog
函数的调用
这里需要注意一点:MachOView 仅仅是帮我们解析出了地址0x10000C000
调用的函数名是NSLog
,而不是解析出了NSLog
函数的真实地址。因为 MachOView 打开的仅仅是存储在磁盘上的静态 MachO 文件,而不是装载到内存中的动态进程,所以 MachOView 是无法获取到位于动态库中的NSLog
函数的真实地址的言归正传:在
Lazy Symbol Pointers
中,MachOView 是如何解析出地址0x10000C000
对应的函数名就是NSLog
的呢? -
Section64 Header 中的 Indirect Sym Index
在开始介绍函数名获取的流程之前,这里先补充一个知识点
我们知道,MachO 文件的
Data
区域是分段(Segment
)管理的,每个段(Segment
)会有 0 到 多个节(Section
)
其中,用于描述节(Section
)的数据结构如下所示:// (64 位的)节 struct section_64 { char sectname[16]; /* 16 Byte 的节名 */ char segname[16]; /* 16 Byte 的段名,该节所属的段 */ uint64_t addr; /* 节的虚拟内存起始地址 */ uint64_t size; /* 节所占内存空间的大小(Byte) */ uint32_t offset; /* 节数据在文件中的偏移 */ uint32_t align; /* 节的内存对齐边界(2 的次方) */ uint32_t reloff; /* 重定位信息在文件中的偏移 */ uint32_t nreloc; /* 重定位信息的条数 */ uint32_t flags; /* 标志信息(节的类型与属性。一个节只能有一个类型,但是可以有多个属性,可以通过位运算分别获取节的类型和属性) */ uint32_t reserved1; /* 保留字段 1(可以用来表示偏移量或者索引,一般用来表示 Indirect Symbol Index,也就是当前节的首元素在间接索引表的位置) */ uint32_t reserved2; /* 保留字段 2(可以用来表示数量或者大小,比如,在 Section64(__TEXT, __sutbs) 中就用来表示 stub 的个数 */ uint32_t reserved3; /* 保留字段 3(无任何用处,真正的保留字段)*/ };
留意到
uint32_t reserved1
字段:保留字段 1(可以用来表示偏移量或者索引,一般用来表示Indirect Symbol Index
,也就是当前节的首元素在间接索引表的位置)。什么意思呢?uint32_t reserved1
字段其实是一个索引偏移量,指的是当前Section
的第 0 个元素对应Indirect Symbols
表中的第几个元素。以Section64 Header(__stubs)
为例进行说明:
-
由 Lazy Symbol Pointers 获取函数名的过程
① 由
LoadCommands
区域的LC_SEGMENT_64(__DATA)
.Section64 Header(__la_symbol_ptr)
.Indirect Sym Index
=0x0000000F(15)
可知,Lazy Symbol Pointers
的第0
个元素对应Indirect Symbols
的第15
个元素。因为NSLog
函数正好为Lazy Symbol Pointers
的第0
个元素,所以NSLog
函数对应Indirect Symbols
的第15
个元素,如下图所示:
② 留意上图NSLog
函数在Indirect Symbols
中对应的条目,其 Data 列的值为0x00000C0(192)
,说明NSLog
函数的符号在符号表Symbol Table
中的索引为 192,找到Symbol Table
的第 192 个元素,如下图所示:
③ 留意上图NSLog
函数在Symbol Table
中对应的条目,其String Table Index
为0x16
,说明NSLog
函数在字符串表String Table
中的起始位置为0x16
,找到String Table
的第0x16
个位置,正好为_NSLog
,如下图所示:
④ 整体的解析顺序为:
Section64 Header(__la_symbol_ptr)
->Lazy Symbol Pointers
->Indirect Symbols
->Symbols
->String Table
Section64 Header(__stubs)
->Symbol Stubs
->Indirect Symbols
->Symbols
->String Table
懒绑定函数 dyld_stub_binder 的执行流程
-
① 前面我们在讲解 iOS 系统的懒绑定机制时,知道了:MachO 在进行
Lazy Symbol
的绑定时,会调用位于Section64(__TEXT, __stub_helper)
中的懒绑定函数dyld_stub_binder
实际上对于 MachO 文件来说,dyld_stub_binder
也是一个外部符号,其实现位于 dyld 的源码中
-
②
dyld::fastBindLazySymbol(...)
函数用于获取需要进行懒绑定的镜像,并调用ImageLoader::doBindFastLazySymbol(...)
函数执行懒绑定
-
③
ImageLoader::doBindFastLazySymbol(...)
是一个虚函数,会根据不同的镜像类型调用不同的实现ImageLoaderMachOCompressed::doBindFastLazySymbol(...)
函数为压缩类型的镜像进行懒绑定并返回真实的符号地址
因为经典类型的镜像的LinkEdit
段没有LC_DYLD_INFO
、LC_DYLD_INFO_ONLY
,不能进行压缩类型的懒绑定
所以ImageLoaderMachOClassic::doBindFastLazySymbol(...)
函数的实现只有一句代码:抛出异常
-
④
ImageLoaderMachOCompressed::bindAt(...)
函数用于(进行符号表的解析)以及(进行最终的绑定操作)
-
⑤
ImageLoaderMachOCompressed::resolve(...)
函数用于解析符号表,返回符号地址
-
⑥
ImageLoaderMachO::bindLocation(...)
函数用于根据不同的绑定类型完成最终的符号地址绑定操作