前言

这里需要对 mach-o 有比较全面的理解,详情见 mach-O结构分析,不展开了。

大概说下:

  1. mach-O 分为三部分,第一部分是header,第三部分是数据区,就是一团一团的代码或者数据,不废话了;
  2. 第二部分是load command,存储着不同类型的 command。
  3. 不同的类型的 command 对应着不同的结构体,load_command 结构体类似于基类,其他类型的 command 结构体可以理解成继承自这个 command;
  4. 而 segment_command 只是其中一种,表示这个 command 指向数据区具体的 segment ,如 __TEXT、__DATA/__DATA_CONST 都是这种类型,而动态链接最关键的 __LINKEDIT 也是这种类型,只是在 MachOView 上没有体现出这个 segment;
  5. 这里使用到的还有类型为 LC_SYMTAB 和 LC_DYSYMTAB,对应的结构体为 symtab_command 和 dysymtab_command 。这两个 command 不指向具体的 segment,只是为动态链接器(dyld)提供一些信息;

一. 添加监听

主要代码如下:

if (!_rebindings_head->next) {_dyld_register_func_for_add_image(_rebind_symbols_for_image);} else {uint32_t c = _dyld_image_count();for (uint32_t i = 0; i < c; i++) {_rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));}}

调用 _dyld_register_func_for_add_image() 传入一个函数作为回调,会在两种情况下触发回调函数:

  1. 有新的 image 被 load;
  2. 已存在的 image 都会调用一遍函数;

回调函数格式如下:

extern void _dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide));

两个参数的意义:

  1. mach_header:第一个参数就是 image 在该进程的虚拟内存中的初始地址
  2. vmaddr_slide:对应 image 在虚拟内存中的偏移;

主工程 image 对应的 vmaddr_slide 就是 ALSR 生成的 slide + __PAGEZERO 的 size(一般为一页0x100000000);

因为 mach-O 文件的起始位置就是 mach_header ,所以这个 vmaddr_slide 一般和 mach_header 相等。但是主工程除外,因为主工程有 __PAGEZERO 这个段,如图:

image.png

非主工程的 image :

image.png

注意,这里是模拟器的情况,而模拟器的 dyld 版本为 433.x.x ,还是 dyld2 的版本。其实在 dyld3 中,共享库的加载位置和懒加载符号的替换方式会稍有不同,以后再说。

另外,vmaddr_slide 和使用 image list 打印出来的所有 image 的首地址对应:

image list

Mach-O 的结构就不展开讲了,详情请看:Mach-O文件结构分析;

二、计算 Load Command 地址

监听完成后会触发回调进而进入下一步,主要逻辑在 rebind_symbols_for_image() 中;

PS:可以在这个函数的开始部分打断点获取当前 image 相关的信息:

Dl_info

header 其实在 fishhook 中没怎么发挥作用,只是用来计算出 Load Command 的地址,代码如下:

// header指针指向__TEXT初始地址
// _TEXT头部是一个Header(mach_header_t结构体),紧接着是Loac Commanduintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);

因为 Load Command 紧跟在 Header 之后,所以代码很简单,就是首地址 + header 的 size;

三、 获取三个 command

这个阶段就是遍历 load command,获取三个 command :linkedit、symtab、dysymtab;

这一步主要代码如下:

// header->ncmds为loadcommand总共包含的段数for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {cur_seg_cmd = (segment_command_t *)cur;if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {//__LINKEDITlinkedit_segment = cur_seg_cmd;}} else if (cur_seg_cmd->cmd == LC_SYMTAB) {// symbol tablesymtab_cmd = (struct symtab_command*)cur_seg_cmd;} else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {// indrect symbol tabledysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;}}

源码分析:

ncmds 来自 header 结构体,表示 Load Command 总共有多少个 segment,遍历就是基于此;

LC_SEGMENT_ARCH_DEPENDENT 是经过 fishhook 二次封装的宏定义,表示 LC_SEGMENT / LC_SEGMENT_64 这种类型的结构体。

这里之所以添加这种判断,是因为 segment 对应的 command 的类型为 LC_SEGMENT/ LC_SEGMENT_64,对应的结构体为 segment_command。 __LINKEDIT、__TEXT、__DATA、__DATA_CONST 这些都是 segment。

LC_SYMTAB 则为符号表的 command 对应的类型,其结构体为 symtab_commandLC_DYSYMTAB 则代表动态重定向符号表,对应的结构体为 dysymtab_command,这也是为什么代码中会对 cur_seg_cmd 进行强制类型转换的原因;

经过上述代码,拿到了三个 command 的地址,接下来就要看看怎么使用这三个 command 了;

四、计算linkedit_base

先看这一句代码:

uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;

linkedit_segment 就是上一步中获取到的三个 command 中的一个。但是这么一句简单的代码其实包含很多问题:

  1. vmaddr 和 fileoff 是什么?
  2. linkedit_base 为什么这么算?
  3. linkedit_base 的意义是什么?

五、vmaddr和fileoff

首先解决第一个问题:

  1. vmaddr 和 fileoff 是什么?

先说结论:
vmaddr:该 segment 在虚拟内存中的起始位置(需要加上偏移);
fileoff:该 segment 在硬盘存储中相对于文件起始位置的偏移;

解释:

首先,参考 《Mach-O Runtime Architecture》可以知道 mach-O 文件是一种文件格式,用于存储 macos 相关架构上的可执行文件。

另外,在 iOS/MacOS 中采用的是进程级别的虚拟缓存。对于 iOS 而言,每个 App 在启动之前都会新生成一个进程,且为其分配和物理内存大小一样的虚拟内存,并和物理内存建立联系,当然这个映射关系是操作系统来控制。

再者,在程序运行时,mach-O 文件会被加载到虚拟内存中。但是 segment 会按照一定的方式进行内存对齐。文档上写的是按页对齐,但是实际上感觉不止如此,这里暂时不深究,需要知道的是:

  • segment 因为内存对齐的原因,在虚拟内存中的 size >= 磁盘存储中的 size;

最典型的例子就是 __PAGEZERO 段,在磁盘中不占据空间,被加载进入内存后占据 0x100000000 的空间,即一页。所以主工程的起始地址一般为:slide + pagezero;

这里的不占据空间指的是 command 指向的 segment 不占据空间。在 load command 中,__PAGEZERO 作为 command 还是会占据一个 command 结构体的空间。另外,mach_header 和 load command 都处于 __TEXT 段,也就是位于 __text 这个 section 之前;

官方文档的表述:

官方文档

来看看实例:

__LINKEDIT

如上图,Foundation 和 UIKit 的 __LINKEDIT 段的 VMSize 都比 FileSize 要大,而且就上图而言,看上去像是以 0x2000对齐(0x181490->0x183000);

再来看个实例:

__DATA

如上图 __DATA 的 vmsize 都是大于 filesize 的,但是一个感觉是按照 0x10000 对齐(0xC5000 -> 0xD0000),而另一个感觉像是按照 0x3000 对齐(0x363000 -> 0x369000)。这就是为啥感觉对齐规则不确定的原因,文档上也没找到说明,暂不深究吧~~~

再来看个相等的情况:

__TEXT

如上图,Foundation 和 UIKit 的 __TEXT 段在虚拟内存和磁盘存储中的大小都是一致的;

至此,总结一下吧,我们知道了:

  • segment 加载进入虚拟缓存后会按照一定规则对齐,导致虚拟缓存中的大小大于等于磁盘中的大小;

那么继续,vmaddr 和 fileoff 表示什么?

先看 vmaddr:

首先将 fishhook 的代码断点打在本章的那一行代码,然后计算:

断点

如上图,可知:

linkedit_segment->vmaddr + slide = __LINKEDIT 段在虚拟内存中的起始位置;

所以:

  • vmaddr 就是 segment 初始位置在虚拟内存中相对于 image 初始位置的偏移;

先不要关注 linkedit_base 是什么,后文会讲;

再来看看 fileoff:

先看看 Foundation 中 __LINKEDIT 的 command 信息:

fileoff

因为 Foundation 是 fat 模式,包含两个架构,所以 x86_64 的架构文件起始位置并不是 0:

起始位置

我们把上面的两个位置相加:

0x4BF000 + 0x3A5000 = 0x864000

接下来见证奇迹的时刻,来看看 mach-O 文件中 __LINKEDIT :

__LINKEDIT

这不是巧合,也就是说:

  • fileoff 就是对应的 segment 的起始位置相对于文件起始位置的偏移;

至此,第一个问题解决,总结一下:

  1. segment 加载进入虚拟缓存后会按照一定规则对齐,导致虚拟缓存中的大小大于等于磁盘中的大小;
  2. vmaddr 表示 segment 在虚拟缓存中的相对于 image 的初始地址的偏移;
  3. fileoff 指对应数据在磁盘文件中,相对于初始位置的偏移;

六、三个表的初始地址计算原理

上文中值分析了 linkedit_base 的那一句代码,接下来要和后面的代码结合来看了:

// Find base symbol/string table addresses
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;// symbol table
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
// string table
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
// dynamic symbol table
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);

后面的三个计算都是基于 linkedit_base 计算符号表、字符串表、重定向表的位置;

先看一张图:

计算原理

如上图:

① = LC_SYMTAB.symoff;
② = __LINKEDIT.vmaddr;
⑤ = __LINKEDIT.fileoff;
④ = ②-⑤(vmaddr-fileoff)
③ = ④

暂且不深究 segment 的对齐原则,现在可以确定的是对于 __LINKEDIT 段而言, vmaddr - fileoff 得到的值就是 __TEXT 段和 __DATA 段因为对齐而相对于磁盘存储中多出来的空间,加上 slide 就成了 linkedit_base。

上句话是计算原理的关键所在,值得多理解理解!!!

因为对齐也只是在 Page 最后补 0,不影响当前 segment 中的位置。所以, symtab 位于 __LINKEDIT 的位置在 file 和 vm 中都是不变的,所以 ③ 和 ④ 的长度是相等的。

而我们又知道在 LC_SYMTAB 的 command 中有 symoff,这个就是 file 中符号表相对于磁盘中的mach'-O文件的起始位置的偏移,即图中的 ①,最终如图,符号表的位置计算为:

// 注意此处的linkedit_base未添加偏移哦~~~
linkedit_base = __LINKEDIT.vmaddr - __LINKEDIT.fileoff (②-⑤);
vm中符号表的位置 = linkedit_base + slide + symoff;
string表的位置 = linkedit_base + slide +stroff;

这样就验证了代码的计算原理;

在 MachOView 中可以直观的看到:

  1. 符号表 command:
LC_SYMTAB

LC_SYMTAB 指的是 symbol table,也就是符号表,不是桩函数表(__stubs)。从上图可以看出,LC_SYMTAB 记录了 symbol table 和 string table 的 offset 以及 size,其中两个 offset 很重要;

  1. 重定向表的 command:
LC_DYSYMTAB

重定向表中记录的 offset 就是用来基于 linkedit_base 进行寻址的;

至此,可以知道后面两个问题的答案了:

  1. linkedit_base 为什么这么算?
  2. linkedit_base 的意义是什么?

答:linkedit_base 去掉 slide 后的本质是处于 __LINKEDIT 之前的 segment 因为内存对齐规则而多出来的 size;又因为 section 不会因为内存对齐而改变在 segment 中的位置,所以可以依据 linkedit_base 计算出 symbol table、string table、indirect table 在虚拟内存中的初始位置;

七、几点补充

第一点要补充的是:

fishhook 中三个表的计算方式和 dyld 源码略有不同,以 symtab 举例:

//dyld源码中的写法
//uint8_t为char,&ptr[symtab_cmd->symoff] 等价于 linkedit_base + symtab_cmd->symoff,其意义是(*prt + sizeof(uint8_t) * symtab_cmd->symoff)
uint8_t *ptr = (uint8_t *)linkedit_base;
uint8_t *p2 = (uint8_t *)&ptr[symtab_cmd->symoff];// symbol table(fishhook)nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);

dyld 中写法的核心在于 &pointer[adress],有点类似于数组指针的 +1 ,其含义是:

向后取 &{*pointer + adress * sizeof(pointer)};

此处不需要过于纠结,只是看 dyld 源码时看到不同,稍微研究了一下;

第二点要补充的是:

linkedit_base 的本质是因为虚拟内存对齐多出来的 size,所以如果虚拟内存和磁盘缓存一样大,那么 linkedit_base = 0 + slide = slide ,也就等于 image 的起始位置。这也是为什么使用 image lookup 查看内存时,有时候会看到该地址为 __TEXT 段的初始位置,有时候啥也看不到。

实例如下图:

image lookup

有时候却啥也查不到或者结果比较懵逼:

乱内存

所以,不要去直接查看 linkedit_segment->vmaddr - linkedit_segment->fileoff;,这个值不代表 mach-O 的某部分在内存中的位置,而是单纯的表示 vm 和 file 中 size 的差值;

八、寻找懒加载和非懒加载的setion

接下来,看下这句代码:

cur = (uintptr_t)header + sizeof(mach_header_t);

这句代码让位置回到了 Load Command 的初始位置,后面又开始遍历了:

for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {cur_seg_cmd = (segment_command_t *)cur;if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {// 不是SEG_DATA/SEG_DATA_CONST则退出,即只在这两个段中查找continue;}// 遍历 segment 中的 sectionfor (uint j = 0; j < cur_seg_cmd->nsects; j++) {section_t *sect =(section_t *)(cur + sizeof(segment_command_t)) + j;if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {// 懒加载符号表perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);}if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {// 非懒加载符号表perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);}}
}

代码分为几步:

  1. 遍历 Load Command;
  2. 只查找 segment 类型的 command;
  3. 只在 __DATA 或者 __DATA_CONST 的 segment_command 中查找;
  4. 遍历 segment 的 section,找出 S_NON_LAZY_SYMBOL_POINTERS 和 S_LAZY_SYMBOL_POINTERS 的 section;
  5. 两个 section 都调用 perform_rebinding_with_section 方法;

总结:这一步中,找到了 __DATA/__DATA_CONST 段中的懒加载和非懒加载的 section;

这里的 section 之和不一定是 2,要看 TYPE 决定,只要是这两种类型,都会调用 perform_rebinding_with_section 方法,即该方法的调用次数为懒加载和非懒加载表之和。比如 __got 和 __nl_symbol_ptr 的 TYPE 都是非懒加载类型,如下图:

__got

估计和编译器设置有关,暂不深究;这一步的代码代码不复杂,这里就略过了,好好看看 perform_rebinding_with_section 方法;

九、reserved1字段的意义

perform_rebinding_with_section 这个方法的重点比较多,一个一个看:

首先是:

uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;

这里的 section 只有两种:懒加载(__la_symbol_ptr) 或者 非懒加载(__nl_symbol_ptr/__got),对于 section 这个结构体中,reserved1 的解释如下:

reserved1

即:

  • 当前表第一个符号在重定向表中的起始 index;

来看个实例:

reserved1

来算一下:

(lldb) p/x 0x24b00 + 0x64 * 4
(int) $8 = 0x00024c90

再去重定向表中找确认:

重定向表

如上图,验证成功;

所以,上述代码的意义是:

  • 找到当前入参 section 中第一个符号在重定向表中的位置;

十、重绑定函数

继续看代码,perform_rebinding_with_section 代码太多,先看外部的 for 循环:

// 指向指针的指针,表中存储的都是指针void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);for (uint i = 0; i < section->size / sizeof(void *); i++) {// 取出重定向表中的indexuint32_t symtab_index = indirect_symbol_indices[i];//使用symtab_index在symbol table中取出符号对象(结构体)// 再取出该符号在string table 中的偏移uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;// 取出该符号的namechar *symbol_name = strtab + strtab_offset;bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1];struct rebindings_entry *cur = rebindings;// while......// ......symbol_loop:;}

这个 for 循环就是遍历 section 中的所有符号,并取出了两个重要信息:

  1. 取出重定向表中的index;
  2. 取出符号的name;

具体流程就是:indirect.index -> symbol table -> string table;

再来看第二个 循环:

//遍历rebindings中的符号,即需要被替换的符号  while (cur) {for (uint j = 0; j < cur->rebindings_nel; j++) {if (symbol_name_longer_than_1 &&strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {// name命中if (cur->rebindings[j].replaced != NULL &&indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {// 将原函数的地址保存*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];}// 替换重定向表中的指针为新函数地址indirect_symbol_bindings[i] = cur->rebindings[j].replacement;goto symbol_loop;}}cur = cur->next;}

提取这个 while 循环,其实就两句关键代码:

// 保留原函数到replaced
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
// 修改表中的指针为自定义的函数
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;

这个两句代码对应着两个关键步骤:

  1. 将重定向表中的指针赋值到 replaced 中,replaced 是我们自定义的一个函数指针,用于保存原函数;
  2. 将重定向表中的指针修改成了我要要替换的函数地址,replacement 就是我们用于替换原函数的函数地址;

十一、懒加载和非懒加载表的补充

上一章节中其实有一句代码也比较关键:

uint32_t symtab_index = indirect_symbol_indices[i];

这里直接按照 i++ 顺序取出了重定向表中的数据,这里有几个知识点:

  1. 重定向表只存储该符号位于符号表中的 index;
  2. 重定向表中的数据按类型分组,顺序存储;
  3. 重定向表、懒加载表、非懒加载表,各个类型的符号在这几个表中排列顺序都是一样的;

这里有点拗口,按照个人的理解,这里跟编译链接的过程有关;大概的过程应该是静态编译时期生成重定向表。这一步是将符号表中的外部符号按照类型取出存放到重定向表中,且只存储 index,看下实例:

image.png

上图中可以理解成重定向表中进行了分组排序,例如 __stub 中的符号不会和 __got 中的符号位置互串。

紧接着,静态编译器根据重定向表在对应的 section 中生成符号指针,这里就要区分两种情况了:

懒加载:生成符号对应的桩函数,桩函数会去懒加载符号表中取出指针跳转到对应函数位置,懒加载表中的初始指针指向 stub_helper 函数,进而指向 binder 函数;
非懒加载:不生成也不需要生成桩函数,但是因为依赖的动态库在动态链接时才 load,所以非懒加载符号表中的函数指针为 0;

如下图:

非懒加载

懒加载

总结:

  1. 符号表中记录了所有的符号,静态依赖库的符号会被直接拷贝进入到主工程,生成最终的 mach-O 文件;
  2. 而依赖的动态库源码不会被拷贝到主工程中,之所以叫做动态库,是因为程序被加载时才进行链接,准确来说在 dyld2 中,是在链接主程序时才加载依赖的动态库;
  3. 符号表中存储全量符号,而动态库的符号额外存储一份在重定向表中,为了节约内存,表中只存储该符号在符号表中的 index;
  4. 重定向表分组排序,依次印射到 __stub、懒加载表(__la_symbol_ptr)、非懒加载表(__nl_symbol_ptr、__got),这一步在静态编译时期就完成了;
  5. 懒加载符号的调用在静态编译时期就被替换成了桩函数,桩函数只管取出懒加载符号表中的函数指针进行跳转;
  6. 懒加载符号表中的函数指针初始化(静态编译时期)时指向 __stub_helper 进而指向 binder 函数;
  7. binder 函数在符号于运行时第一次被调用时进行寻址,然后替换懒加载符号表中的函数指针为真实的函数地址;
  8. 非懒加载符号表中的指针初始化时值为 0,动态链接之后立马进行寻址,寻址完成后进行替换;

这里还有一点不确定,非懒加载的符号是和懒加载符号一样?真实调用代码被替换成桩函数?还是在动态链接时期直接替换成了函数地址?还是说基于 PIC (-fpic)技术,在静态链接时期已经将调用代码替换成了去非懒加载符号表中取出指针进行跳转的代码?感觉更像第三种~~后面再深入~~

十二、fishhook中的replaced最终保存的函数

replaced 是保存原函数,如果是懒加载,懒加载表中一开始存储的是 stub_helper 函数,如果在调用该函数之前调用了 rebind 方法,replaced 中会被替换成 stub_helper 而不是原函数?如果是这样,那么每次调用 replaced 函数,都会去进行一次重复绑定?

验证:

  1. iphone7(10.3)

模拟器中 dyld 实际使用的是 dyld_sim,其 dyld 的版本是:

dyld

源码如下:

// 指向函数的指针
static void (*sys_NSLog)(NSString *format, ...);void xk_NSLog(NSString *format, ...) {//    format = [format stringByAppendingString:@"(我被hook了)"];printf("hook succ\n");sys_NSLog(format);}void rebind(void) {// 定义结构体struct rebinding xkNSLogBind;// 需要hook的函数的名称xkNSLogBind.name = "NSLog";// 新函数的地址xkNSLogBind.replacement = xk_NSLog;// 保存被替换掉函数的指针xkNSLogBind.replaced = (void *)&sys_NSLog;// 创建需要hook的结构体数组struct rebinding rebind[1] = {xkNSLogBind};// hookrebind_symbols(rebind, 1);
}int main(int argc, char * argv[]) {rebind();sys_NSLog(@"---");sys_NSLog(@"---");
}

断点之后的汇编:

image.png

image.png

image.png

其实上面的问题就是 fishhook 源码必定会导致的现象。 fishhook 按照依赖库的加载顺序对每个库中的懒加载和非懒加载符号表进行了替换,一次达到全局替换的目的;

正因为这样的逻辑,第一句代码就会找到最后一个包含该函数的库,然后将该库中的 stub_helper 函数保存到 replaced 中;

即:

  • placed 中保存的是最后一个包含被替换函数的依赖库中,函数的 stub_helper 函数;

其实这个问题在 dyld2 和 dyld3 中不一样,在 iOS14 中运行。然后使用一个奇技淫巧:

watchpoint set v sys_NSLog

或者直接在源码中添加:

image.png

这样就可以看到打印了:

image.png

直接查看这个内存:

image.png

如上图,在 rebind 操作完成之后,就已经指向 Foundation 中真实的 NSLog 函数了。

再来看看 dyld 版本:

image.png

很明显,iOS14 的模拟器中的 dyld 已经是 dyld3 的版本了;

关于这个现象查阅到以下资料:

dyld3

即:dyld3 中使用了 lauch closure 机制,会导致流程不一样;

至于 dyld3 中的具体流程,以后再分析,不是本文重点;

十三、留个疑问

非懒加载符号主动调用

有个有意思的现象:

非懒加载符号主动调用之后就变成懒加载了,比如 objc_msgSend :

image.png

这里留个疑问:

  1. 非懒加载符号在运行时是直接被替换成了函数指针,还是和懒加载符号一样使用 stub 函数来调用?
  2. 如果是被替换成了真实的函数地址,那么 fishhook 中替换 __nl_symbol_ptr 就没有意义?

感觉这里肯定有个知识点自己还不知道,暂时存疑吧~~


http://www.taodudu.cc/news/show-1912764.html

相关文章:

  • mach-O文件结构分析
  • 设备唯一标志的解决方案
  • iOS:主流启动优化方案浅析
  • iOS:segment对齐原则
  • HTTP缓存机制及其在iOS中的应用
  • iOS:SideTable
  • iOS:isa指针
  • iOS底层:PAGEZERO的作用
  • iOS图形学(三):屏幕成像原理
  • iOS图形学(四):iOS中的绘图框架
  • Java基础(一):简介和基础数据类型
  • Java基础(二):面向对象
  • Java:常量池
  • Java基础(三):常用对象
  • Java基础(四):异常处理
  • Java基础(五):多线程
  • Android:权限处理
  • AsyncTask的基本使用
  • 在Nginx中配置SSL证书
  • Base64编码流程
  • Nginx配置基础认证
  • Cookie、Session、Token、RefreshToken
  • JSCore浅析及其在iOS上的使用
  • 编程语言的动态性(Dart和OC对比)
  • iOS:Universal Link
  • AFN中的鉴权
  • openGL ES 教程(二):渲染管线
  • MySQL(2)----DDL语句之增、删、改、查操作
  • MySQL(3)-----DML数据库操作(上)
  • 线性表的基本运算

iOS:fishhook原理分析相关推荐

  1. IOS 之FishHook原理及例子

    一.HOOK概述 HOOK,中文为"钩子"或"挂钩",在ios逆向中是指改变程序的运行流程的一种技术,通过hook可以让别人的程序执行自己的代码逻辑,在逆向中经 ...

  2. Android免Root环境下Hook框架Legend原理分析

    0x1 应用场景 现如今,免Root环境下的逆向分析已经成为一种潮流! 在2015年之前的iOS软件逆向工程领域,要想对iOS平台上的软件进行逆向工程分析,越狱iOS设备与安装Cydia是必须的!几乎 ...

  3. 数据结构 练习21-trie的原理分析和应用

    前言 今天具体分析一下trie树,包括:原理分析,应用场合,复杂度分析,与hash的比较,源码展现.大部分内容来自互联网,文中会注明出处. 原理分析 主要是hash树的变种,先看下图: 每一个点存储一 ...

  4. java8 lambda map排序_Android兼容Java 8语法特性的原理分析

    本文主要阐述了Lambda表达式及其底层实现(invokedynamic指令)的原理.Android第三方插件RetroLambda对其的支持过程.Android官方最新的dex编译器D8对其的编译支 ...

  5. KVC原理分析及应用

    前言: KVC又称键值编码(Key-Value-Coding) ,在iOS开发中是一个比较常见的技术点,相信一般开发人员都会使用KVC,其主要的两个方法无非就是设置值和取值,相信也有不少人写UI喜欢使 ...

  6. 一篇读懂:Android手机如何通过USB接口与外设通信(附原理分析及方案选型)

    更多技术干货,欢迎扫码关注博主微信公众号:HowieXue,共同探讨软件知识经验,关注就有海量学习资料免费领哦: 目录 0背景 1.手机USB接口通信特点 1.1 使用方便 1.2 通用性强 1.3 ...

  7. html中点击图标变色,css可变色图标及原理分析

    第一步,把图标转成svg格式的 第二步,使用iconfont生成代码 点击图标管理->我的图标,如下图 进入到我的图标之后,点击上传icon,如下图 点此上传 选中svg文件, 上传之后,点击去 ...

  8. iOS底层原理探究 第一探. 事件传递和响应者链

    一. 声明:  本文意在探讨, 也参考了几位大神的文章, 在最后我会把链接发出来, 如果有理解错误的地方, 请大神们指正哈! 二. 前言:  最近自己做项目的时候, 用到了UITabbarContro ...

  9. 一篇读懂无线充电技术(附方案选型及原理分析)

    更多技术干货,欢迎扫码关注博主微信公众号:HowieXue,一起学习探讨软硬件技术知识经验,关注就有海量学习资料免费领哦: 目录 一篇读懂无线充电技术(附方案选型及原理分析) 0.背景 1.无线供电特 ...

  10. Java 数据交换格式反射机制SpringIOC原理分析

    数据交换格式&反射机制&SpringIOC原理分析 什么是数据交换格式? 数据交换格式使用场景 JSON简单使用 什么是JSON? JSON格式的分类 常用JSON解析框架 使用fas ...

最新文章

  1. 开源(Open Source)那些事儿 (一)
  2. 点击调用ajax,jQuery ajax在点击时调用,仅工作一次
  3. SLAM-ch2-使用kdevelop创建helloWorld程序
  4. Write operations are not allowed in read-only mode
  5. UVa810 A Dicey Problem 筛子难题
  6. 关于编程学习的一些思考
  7. AngularJs 1.5 $location获取url参数
  8. java中后退键_java - 单击后退按钮两次以退出活动
  9. 控制理论与控制工程_控制理论与控制工程专业介绍_研究方向_就业前景分析
  10. python系统路径_python中os模块简单了解(系统命令和路径的获取)
  11. 洛谷P3853 路标设置
  12. 数据库课程设计—超市零售信息管理系统(Python实现)
  13. VS2019 + QT5.12调试时无法显示Qt相关变量如QString具体值
  14. 交换机基本原理与配置
  15. RobotStudio 创建第一个工作站
  16. 加班申请 ----中间表--系统自动算出---可调休天数
  17. 电网计算机面试专业题,国家电网计算机管理员面试经验|面试题 - 职朋职业圈...
  18. 深圳大学计算机专硕就业工资,深大毕业研究生初次就业薪酬平均月薪广东第一,十年后是全国两倍...
  19. 晕菜:新域名在60天内不能转移。
  20. 如果报了前端培训班,还是学不会怎么办?

热门文章

  1. Java 注解Annotation总结二
  2. 为何区块链能成为金融行业的香饽饽?只因这5个关键因素!
  3. 简述Java内存模型的happen before原则
  4. 【Amaple教程】4. 组件
  5. Android 项目规范
  6. 第二课:电场与偶极子
  7. [C#]巧妙获取正在使用的IPv4地址
  8. java楼宇报警器_智能楼宇包含哪些安防子系统
  9. 动态启用和禁用mainfest中组件
  10. 100个高质量Java开发者博客 【转】