目录
什么是dyld
dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作。
dyld加载所有的库和可执行文件。
dyld加载流程
一、程序执行从_dyld_star开始
在工程任一个类的load方法打断点,看下调用栈
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
* frame #0: 0x00000001024e16e4 LCClientDemo`+[AppDelegate load](self=AppDelegate, _cmd="load") at AppDelegate.m:23:2
frame #1: 0x00000001a0ef0ecc libobjc.A.dylib`load_images + 736
frame #2: 0x0000000102a4a0d4 dyld`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 448
frame #3: 0x0000000102a5958c dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 524
frame #4: 0x0000000102a58308 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 184
frame #5: 0x0000000102a583d0 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 92
frame #6: 0x0000000102a4a420 dyld`dyld::initializeMainExecutable() + 216
frame #7: 0x0000000102a4edb4 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 4616
frame #8: 0x0000000102a49208 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 396
frame #9: 0x0000000102a49038 dyld`_dyld_start + 56
(lldb)
可以看到函数程序开始是从_dyld_start开始的。
frame #9: 0x0000000102a49038 dyld_dyld_start + 56
在lldb调试模式下通过up指令,来到frame #9
dyld`_dyld_start:
bl 0x102a4907c ; dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*)
我们看到这样一句汇编代码,就是在frame#9函数内对frame#8函数的调用。
找到dyld的源码看下dyldbootstrap::start的方法实现
uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[],
intptr_t slide, const struct macho_header* dyldsMachHeader,
uintptr_t* startGlue)
{
// if kernel had to slide dyld, we need to fix up load sensitive locations
// we have to do this before using any global variables
slide = slideOfMainExecutable(dyldsMachHeader);
bool shouldRebase = slide != 0;
#if __has_feature(ptrauth_calls)
shouldRebase = true;
#endif
if ( shouldRebase ) {
rebaseDyld(dyldsMachHeader, slide);
}
// allow dyld to use mach messaging
mach_init();
// kernel sets up env pointer to be just past end of agv array
const char** envp = &argv[argc+1];
// kernel sets up apple pointer to be just past end of envp array
const char** apple = envp;
while(*apple != NULL) { ++apple; }
++apple;
// set up random value for stack canary
__guard_setup(apple);
#if DYLD_INITIALIZER_SUPPORT
// run all C++ initializers inside dyld
runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);
#endif
// now that we are done bootstrapping dyld, call dyld's main
uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
在该方法内完成了slide(ASLR)和rebase(MachO文件的重定向)。
二、进入dyld:main函数
这个main函数是dyld的main函数,不是main.m文件内的main函数。
程序启动最关键的函数。
1、配置一些环境变量
2、加载共享缓存库(一开始就判断是否禁用,iOS无法被禁用的)
3、实例化主程序
4、加载动态库(插入库、三方库)
5、链接主程序(符号绑定)
6、最关键的地方:初始化方法
6.1 经过一系列初始化调用到notifySignle函数
该函数会执行一个回调
通过断点调试:该函数是_objc_init初始化的时候赋值的一个函数load_images
load_images里面执行call_load_methods函数
call_load_methods函数循环调用各个类的load方法
6.2 doModIntFunction函数
内部会调用带__atrribute_((constructor))的c函数
6.3 返回主程序的入口函数。开始进入主程序的main函数。
符号绑定
一、静态语言与动态语言。
理解符号绑定之前先说明下静态语言与动态语言
静态语言:编译时确定并指定函数地址 如:C语言
动态语言:运行时确定并指定函数地址 如:OC语言。
比如:
C语言在编译时如果只有声明方法,但不实现方法就会报错。因为没有办法确定函数地址。
OC语言在编译时如果只有方法声明,但不实现方法是不会报错的,但在程序运行时会报找不到方法的错误。因为在运行时时没有办法确定函数地址。
二、符号表
我们以NSLog函数来说明符号表。
FOUNDATION_EXPORT void NSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2) NS_NO_TAIL_CALL;
1、NSLog函数的实现位于Foundation框架。
2、当我们开发时,在工程中调用NSLog函数,然后编译打包成MachO文件。这时我们的MachO文件内是没有NSLog的函数实现的,MachO也没有办法知道NSLog的函数地址,因为它在Foundation框架内。
那么工程是如何编译通过的?
通过后MachO又是如何调用到NSLog函数的呢?
这就要说到苹果的PIC(position Independent code 位置无关代码)技术。
因为C语言(静态语言)在编译时必须指定函数地址,但NSLog的地址又无法获得,这时就用一个符号来代替NSLog的函数地址,让编译通过。多个符号就构成了符号表
符号表位于MachO的数据段,如上图的1-0、1-1位置处。其中0000000100006614就是指定的符号的值。
3、符号值是如何找到的(NSLog与符号值是怎么对应的)?
上图中的1-2-3-4是一个逆推的过程,程序加载后的查找过程是4-3-2-1。
步骤4
步骤3
步骤2
三、符号绑定
符号绑定就是程序在执行时通过符号表中NSLog的符号找到Foundation框架中NSLog函数真实地址并将符号替换掉的过程。
需要说明的是程序编译时在动态库共享缓存中的函数的地址都会用一个符号替代(这些符号构成了符号表,符号表在MachO文件的数据段内),在程序执行时会根据符号找到具体的函数地址(绑定)。
我们通过lldb调试来看下绑定前后的不同。
创建一个空工程,在ViewController的viewDidLoad方法内添加 NSLog(@"test");
代码
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//断点一:此处还未完成绑定,符号(绑定也是懒加载)
NSLog(@"test");
//断点二:此处已经完成绑定
}
@end
断点一
(lldb) image list
[ 0] 49DA5C4C-026B-3395-B1AA-ACAF13C5D68E 0x00000001049c8000 /Users/xxx/Library/Developer/Xcode/DerivedData/Build/Products/Debug-iphoneos/bindingDemo.app/bindingDemo
MachO的起始内存地址是 0x00000001049c8000
Lazy Symbol Pointers 文件内 NSLog的偏移量是 0x8008
NSLog 的内存地址是 0x1049D0008 = 0x00000001049c8000 + 0x8008 读取该内存地址中的值进行反汇编
(lldb) memory read/4xg 0x1049D0008
0x1049d0008: 0x00000001049ce614 0x00000001a15829c8
0x1049d0018: 0x00000001a51ccf44 0x00000001a0f041a8
(lldb) dis -s 0x00000001049ce614
0x1049ce614: ldr w16, 0x1049ce61c
0x1049ce618: b 0x1049ce5fc
0x1049ce61c: udf #0x0
0x1049ce620: ldr w16, 0x1049ce628
0x1049ce624: b 0x1049ce5fc
0x1049ce628: udf #0xd
0x1049ce62c: ldr w16, 0x1049ce634
0x1049ce630: b 0x1049ce5fc
断点二 读取该内存地址中的值进行反汇编
(lldb) memory read/4xg 0x1049D0008
0x1049d0008: 0x00000001a158d940 0x00000001a15829c8
0x1049d0018: 0x00000001a51ccf44 0x00000001a0f041a8
(lldb) dis -s 0x00000001a158d940
Foundation`NSLog:
0x1a158d940 <+0>: sub sp, sp, #0x20 ; =0x20
0x1a158d944 <+4>: stp x29, x30, [sp, #0x10]
0x1a158d948 <+8>: add x29, sp, #0x10 ; =0x10
0x1a158d94c <+12>: adrp x8, 247579
0x1a158d950 <+16>: ldr x8, [x8, #0x958]
0x1a158d954 <+20>: ldr x8, [x8]
0x1a158d958 <+24>: str x8, [sp, #0x8]
0x1a158d95c <+28>: add x8, x29, #0x10 ; =0x10
在断点二处已经完成了绑定,符号值已经变成了NSLog的函数地址。
行者常至,为者常成!