dyld是一个精细而又复杂的过程,在上一篇文章之后,有必要再详细剖析这个过程。这里讲到第一篇:dyld_start之前都经历了什么.
既然各种二进制都是走dyld加载的,那么dyld自身是如何加载进来的呢?_dyld_start之前系统都做了什么?
0x1 dyld源码分析 当我们打个断点到_objc_init里面的时候,发现一切都是从一个叫 _dyld_start 的方法开始的。 拉到 dyld 的源码,发现是个汇编方法。而且没有搜到代码调用。 既然dyld是加载二进制的库,那么_dyld_start又是谁调用的呢?
于是我先打个断点到_dyld_start, 发现并不能断。既然上层代码无法进一步分析,那就从内核里面找找答案吧!于是从苹果开源代码平台上down下来内核代码:xnu . 开始分析。
先直接搜一下 _dyld_start。 发现没有直接调用这个方法的地方。于是找到dyld的二进制,用hopper或machoview分析一下。 可以在deviceSupport下面找到dyld. 如图:
在hopper中找到_dyld_start如图:
从这个图中可以看到,_dyld_start的地址是 0x1000, 在0x88 和 0x6d8 这两个地址有引用。我们再分别看下这2处是什么:
其中 0x88 是记录了 section0的起始地址,这里就补贴图了。直接贴 0x6d8的内容:
可以看到这个是记录在执行LC_UNIXTHREAD 推测是在执行这个LoadCommand的时候可能会调用到 _dyld_start。
dyld源码分析到这一步,后续在dyld上就不能更进一步挖掘了。带着上面的结果,我们可以到内核再挖掘一番。
0x2 内核挖掘 _dyld_start是怎么被调用到的 那就再往下层挖掘,往内核挖掘吧。 你可以很简单的在 opensource.apple.com 下载内核代码: xnu 当然这个应该不是最新的代码,而且里面看起来只开放了i386相关的内核。但是对于我们分析dyld来说,应该是够用了。
先上结论,不喜推理同学跳过:
_dyld_start的方法地址的确是在 LC_UNIXTHREAD 段中解析出来的。后续通过thread_setentrypoint 直接将用户态的pc设置到这个地址来执行的。
探索过程开始:
第一步,搜索一下 LC_UNIXTHREAD. 找到如下代码:
1 2 3 4 5 6 7 8 9 case LC_UNIXTHREAD: if (pass != 1 ) break ; ret = load_unixthread( (struct thread_command *) lcp, thread, slide, result); break ;
再看 load_unixthread 方法, 并且在这个方法里面找到了解析entry_point的地方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 struct thread_command { uint32_t cmd; uint32_t cmdsize; }; ret = load_threadentry(thread, (uint32_t *)(((vm_offset_t )tcp) + sizeof (struct thread_command)), tcp->cmdsize - sizeof (struct thread_command), &addr); if (ret != LOAD_SUCCESS) return (ret); if (result->using_lcmain || result->entry_point != MACH_VM_MIN_ADDRESS) { return (LOAD_FAILURE); } result->entry_point = addr; result->entry_point += slide;
这个 entry_point 就是_dyld_start的地址,为什么? 让我们进一步往load_threadentry里看,这个方法不长,我就全贴了, 直接在注释里面看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 static load_return_t load_threadentry ( thread_t thread, uint32_t *ts, uint32_t total_size, mach_vm_offset_t *entry_point ) { kern_return_t ret; uint32_t size; int flavor; uint32_t entry_size; *entry_point = MACH_VM_MIN_ADDRESS; while (total_size > 0 ) { flavor = *ts++; size = *ts++; if (UINT32_MAX-2 < size || UINT32_MAX/sizeof (uint32_t ) < size+2 ) return (LOAD_BADMACHO); entry_size = (size+2 )*sizeof (uint32_t ); if (entry_size > total_size) return (LOAD_BADMACHO); total_size -= entry_size; ret = thread_entrypoint(thread, flavor, (thread_state_t )ts, size, entry_point); if (ret != KERN_SUCCESS) { return (LOAD_FAILURE); } ts += size; } return (LOAD_SUCCESS); } kern_return_t thread_entrypoint ( __unused thread_t thread, int flavor, thread_state_t tstate, __unused unsigned int count, mach_vm_offset_t *entry_point ) { if (*entry_point == 0 ) *entry_point = VM_MIN_ADDRESS; switch (flavor) { case x86_THREAD_STATE32: { x86_thread_state32_t *state25; state25 = (i386_thread_state_t *) tstate; *entry_point = state25->eip ? state25->eip: VM_MIN_ADDRESS; break ; } case x86_THREAD_STATE64: { x86_thread_state64_t *state25; state25 = (x86_thread_state64_t *) tstate; *entry_point = state25->rip ? state25->rip: VM_MIN_ADDRESS64; break ; } } return (KERN_SUCCESS); }
经过上面的源码分析,我们再看下上一节中看到的图,就可以这么解析了:
之所以这样推测,参考了libcxxabi中的这个结构体:
1 2 3 4 5 6 7 8 struct GPRs { uint64_t __x[29 ]; uint64_t __fp; uint64_t __lr; uint64_t __sp; uint64_t __pc; uint64_t padding; };
到了这一步,已经取到了entry_point. 那什么时候被调用呢?直接搜索一下entry_point. 发现在activate_exec_state 方法里面执行thread_setentrypoint 来设置entry_point. 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 thread_setentrypoint(thread, result->entry_point); void thread_setentrypoint (thread_t thread, mach_vm_address_t entry) { pal_register_cache_state(thread, DIRTY); if (thread_is_64bit(thread)) { x86_saved_state64_t *iss64; iss64 = USER_REGS64(thread); iss64->isf.rip = (uint64_t )entry; } else { x86_saved_state32_t *iss32; iss32 = USER_REGS32(thread); iss32->eip = CAST_DOWN_EXPLICIT(unsigned int , entry); } }
0x3 串起来: _dyld_start 是如何一步步执行到的? 上面一节的代码里面找到了_dyld_start是如何被调用的,但是整个代码的执行顺序是怎么样的呢?主二进制是什么时候解析的,dyld是什么时候加载的呢?
这里就不再贴大段代码了,分析方法也很简单,就是在内核源码里面直接看。下面直接通过一个调用栈图来说明, 这里面每个方法都做了很多事情,我这里只注释了在走到_dyld_start路上的关键事情,很简略。
1 2 3 4 5 6 7 8 9 10 ▼ execve ▼ __mac_execve ▼ exec_activate_image ▼ exec_mach_imgact ▼ load_machfile ▶︎ parse_machfile ▼ load_dylinker ▼ parse_machfile ▼ activate_exec_state ▶︎ thread_setentrypoint
0x4 小结 本文简单探究了一下内核如何启动一个app的。也就是在 _dyld_start之前系统怎么加载的。如有错误,欢迎指正。