一、介绍

在 MacOS 和 iOS 上,可执行程序的启动依赖于 xnu 内核进程运作和动态链接加载器 dyld。

dyld 全称 the dynamic link editor,即动态链接器,其本质是 Mach-O 文件,是专门用来加载动态库的库。

源码下载地址:https://opensource.apple.com/tarballs/dyld/

当点击 App 的时候,系统在内核态完成一些必要配置,从 App 的 MachO 文件解析出 dyld 的地址,这里会记录在 MachO 的 LC_LOAD_DYLINKER 命令中,内容参考如下:

  1. cmd LC_LOAD_DYLINKER
  2. cmdsize 28
  3. name /usr/lib/dyld (offset 12)
  4. Load command 8
  5. cmd LC_UUID
  6. cmdsize 24
  7. uuid DF0F9B2D-A4D7-37D0-BC6B-DB0297766CE8
  8. Load command 9
  9. cmd LC_VERSION_MIN_IPHONEOS

dyld 位于 /usr/lib/dyld,可以从越狱机或者 mac 电脑中找到。以 mac 为例,终端执行命令:

  1. $ cd /usr/lib
  2. $ file dyld

dyld 是 Mach-O 类型的通用二进制文件,支持 x86_64 和 i386 两种架构。iPhone 真机对应的 dyld 支持的为 arm 系列架构。

在 xnu 内核为程序启动做好准备后,执行由内核态切换到用户态,由 dyld 完成后面的加载工作:dyld 会将 App 依赖的动态库和 App 文件加载到内存以后执行,动态库不是可执行文件,无法独自执行。

二、otool

otool 是专门用来查看 Mach-O 类型文件的工具

Mac OS X 下二进制可执行文件的动态链接库是 dylib 文件。

dylib 也就是 bsd 风格的动态库。基本可以认为等价于 windows 的 dll 和 linux 的so。mac 基于 bsd,所以也使用的是 dylib。

Linux 下用 ldd 查看,苹果系统用 otool。

2.1 查看 otool 地址

电脑已安装 Xcode。终端输入:

  1. $ otool
  2. Usage: /Applications/Xcode10.1.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/otool [-arch arch_type] [-fahlLDtdorSTMRIHGvVcXmqQjCP] [-mcpu=arg] [--version] <object file> ...
  3. -f print the fat headers
  4. -a print the archive header
  5. -h print the mach header
  6. -l print the load commands
  7. -L print shared libraries used
  8. -D print shared library id name
  9. -t print the text section (disassemble with -v)
  10. -p <routine name> start dissassemble from routine name
  11. -s <segname> <sectname> print contents of section
  12. -d print the data section
  13. -o print the Objective-C segment
  14. -r print the relocation entries
  15. -S print the table of contents of a library (obsolete)
  16. -T print the table of contents of a dynamic shared library (obsolete)
  17. -M print the module table of a dynamic shared library (obsolete)
  18. -R print the reference table of a dynamic shared library (obsolete)
  19. -I print the indirect symbol table
  20. -H print the two-level hints table (obsolete)
  21. -G print the data in code table
  22. -v print verbosely (symbolically) when possible
  23. -V print disassembled operands symbolically
  24. -c print argument strings of a core file
  25. -X print no leading addresses or headers
  26. -m don't use archive(member) syntax
  27. -B force Thumb disassembly (ARM objects only)
  28. -q use llvm's disassembler (the default)
  29. -Q use otool(1)'s disassembler
  30. -mcpu=arg use `arg' as the cpu for disassembly
  31. -j print opcode bytes
  32. -P print the info plist section as strings
  33. -C print linker optimization hints
  34. --version print the version of /Applications/Xcode10.1.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/otool

由上可知 otool 的地址:/Applications/Xcode10.1.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/otool

进入地址发现 otool 文件是一个替身(软连接)。

查看 otool 指向的软连接地址:

  1. $ cd /Applications/Xcode10.1.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/
  2. $
  3. $ ls -l
  4. total 223352
  5. -r-xr-xr-x 1 cykj staff 33920 10 20 2018 ar
  6. -r-xr-xr-x 1 cykj staff 28000 10 20 2018 as
  7. -rwxr-xr-x 1 cykj staff 18176 10 20 2018 asa
  8. -rwxr-xr-x 1 cykj staff 212208 10 20 2018 bison
  9. -r-xr-xr-x 1 cykj staff 150048 10 20 2018 bitcode_strip
  10. lrwxr-xr-x 1 cykj staff 5 11 22 2018 c++ -> clang
  11. -rwxr-xr-x 1 cykj staff 23152 10 20 2018 c89
  12. -rwxr-xr-x 1 cykj staff 23248 10 20 2018 c99
  13. lrwxr-xr-x 1 cykj staff 5 11 22 2018 cc -> clang
  14. -rwxr-xr-x 1 cykj staff 78705232 10 20 2018 clang
  15. lrwxr-xr-x 1 cykj staff 5 11 22 2018 clang++ -> clang
  16. -r-xr-xr-x 1 cykj staff 120064 10 20 2018 cmpdylib
  17. -r-xr-xr-x 1 cykj staff 145872 10 20 2018 codesign_allocate
  18. lrwxr-xr-x 1 cykj staff 17 11 22 2018 codesign_allocate-p -> codesign_allocate
  19. -rwxr-xr-x 1 cykj staff 4937600 10 20 2018 coremlcompiler
  20. -rwxr-xr-x 1 cykj staff 3344 9 26 2018 cpp
  21. -rwxr-xr-x 1 cykj staff 27712 10 20 2018 ctags
  22. -r-xr-xr-x 1 cykj staff 145824 10 20 2018 ctf_insert
  23. lrwxr-xr-x 1 cykj staff 13 11 22 2018 dsymutil -> llvm-dsymutil
  24. -rwxr-xr-x 1 cykj staff 1006032 10 20 2018 dwarfdump
  25. -rwxr-xr-x 1 cykj staff 219088 10 20 2018 dyldinfo
  26. -rwxr-xr-x 2 cykj staff 569056 10 20 2018 flex
  27. -rwxr-xr-x 2 cykj staff 569056 10 20 2018 flex++
  28. lrwxr-xr-x 1 cykj staff 8 11 22 2018 gcov -> llvm-cov
  29. -rwxr-xr-x 2 cykj staff 142336 10 20 2018 gm4
  30. -rwxr-xr-x 1 cykj staff 90960 10 20 2018 gperf
  31. -rwxr-xr-x 1 cykj staff 65520 10 20 2018 indent
  32. -r-xr-xr-x 1 cykj staff 136784 10 20 2018 install_name_tool
  33. -rwxr-xr-x 1 cykj staff 2480704 10 20 2018 ld
  34. -rwxr-xr-x 1 cykj staff 230 9 26 2018 lex
  35. -r-xr-xr-x 1 cykj staff 154592 10 20 2018 libtool
  36. -r-xr-xr-x 1 cykj staff 66000 10 20 2018 lipo
  37. -rwxr-xr-x 1 cykj staff 3320816 10 20 2018 llvm-cov
  38. -rwxr-xr-x 1 cykj staff 29723968 10 20 2018 llvm-dsymutil
  39. -rwxr-xr-x 1 cykj staff 10591472 10 20 2018 llvm-nm
  40. -rwxr-xr-x 1 cykj staff 11899296 10 20 2018 llvm-objdump
  41. -r-xr-xr-x 1 cykj staff 32672 10 20 2018 llvm-otool
  42. -rwxr-xr-x 1 cykj staff 1272096 10 20 2018 llvm-profdata
  43. -rwxr-xr-x 1 cykj staff 2873440 10 20 2018 llvm-size
  44. -rwxr-xr-x 1 cykj staff 3567 9 26 2018 lorder
  45. -rwxr-xr-x 2 cykj staff 142336 10 20 2018 m4
  46. -rwxr-xr-x 1 cykj staff 24800 10 20 2018 metal
  47. -rwxr-xr-x 1 cykj staff 24768 10 20 2018 metal-ar
  48. -rwxr-xr-x 1 cykj staff 24768 10 20 2018 metal-as
  49. -rwxr-xr-x 1 cykj staff 24768 10 20 2018 metal-link
  50. -rwxr-xr-x 1 cykj staff 24768 10 20 2018 metal-opt
  51. -rwxr-xr-x 1 cykj staff 24768 10 20 2018 metallib
  52. -rwxr-xr-x 1 cykj staff 7604 9 26 2018 mig
  53. lrwxr-xr-x 1 cykj staff 7 11 22 2018 nm -> llvm-nm
  54. -r-xr-xr-x 1 cykj staff 132896 10 20 2018 nm-classic
  55. -r-xr-xr-x 1 cykj staff 162720 10 20 2018 nmedit
  56. lrwxr-xr-x 1 cykj staff 12 11 22 2018 objdump -> llvm-objdump
  57. lrwxr-xr-x 1 cykj staff 10 11 22 2018 otool -> llvm-otool
  58. -r-xr-xr-x 1 cykj staff 648720 10 20 2018 otool-classic
  59. -r-xr-xr-x 1 cykj staff 132784 10 20 2018 pagestuff
  60. lrwxr-xr-x 1 cykj staff 7 11 22 2018 ranlib -> libtool
  61. -rwxr-xr-x 1 cykj staff 59344 10 20 2018 rebase
  62. -r-xr-xr-x 1 cykj staff 204960 10 20 2018 redo_prebinding
  63. -rwxr-xr-x 1 cykj staff 73664 10 20 2018 rpcgen
  64. -r-xr-xr-x 1 cykj staff 48864 10 20 2018 segedit
  65. lrwxr-xr-x 1 cykj staff 9 11 22 2018 size -> llvm-size
  66. -r-xr-xr-x 1 cykj staff 120080 10 20 2018 size-classic
  67. -r-xr-xr-x 1 cykj staff 120400 10 20 2018 strings
  68. -r-xr-xr-x 1 cykj staff 189568 10 20 2018 strip
  69. -rwxr-xr-x 1 cykj staff 87671328 10 20 2018 swift
  70. lrwxr-xr-x 1 cykj staff 5 11 22 2018 swift-autolink-extract -> swift
  71. -rwxr-xr-x 1 cykj staff 5031520 10 20 2018 swift-build
  72. -rwxr-xr-x 1 cykj staff 384480 10 20 2018 swift-build-tool
  73. -rwxr-xr-x 1 cykj staff 461136 10 20 2018 swift-demangle
  74. -rwxr-xr-x 1 cykj staff 5031552 10 20 2018 swift-package
  75. -rwxr-xr-x 1 cykj staff 5031472 10 20 2018 swift-run
  76. -rwxr-xr-x 1 cykj staff 53024 10 20 2018 swift-stdlib-tool
  77. -rwxr-xr-x 1 cykj staff 5031504 10 20 2018 swift-test
  78. lrwxr-xr-x 1 cykj staff 5 11 22 2018 swiftc -> swift
  79. -rwxr-xr-x 1 cykj staff 12042320 10 20 2018 tapi
  80. -rwxr-xr-x 1 cykj staff 32592 10 20 2018 unifdef
  81. -rwxr-xr-x 1 cykj staff 2946 9 26 2018 unifdefall
  82. -rwxr-xr-x 1 cykj staff 59776 10 20 2018 unwinddump
  83. -rwxr-xr-x 1 cykj staff 135 9 26 2018 yacc

可以看到 otool 指向 llvm-otool,而 llvm-otool 和 otool 在同一个目录中。

另外,还可以发现,这个文件夹下面还有很多有用的文件,如 lipo

2.2 otool -L

查看动态链接库

终端执行命令:

  1. $ cd /Users/cykj/Library/Developer/Xcode/DerivedData/Demo-fpfdxjbemnwnqcfjimbqpbzpnpem/Build/Products/Debug-iphonesimulator/Demo.app/
  2. $
  3. $ otool -L Demo
  4. Demo:
  5. /System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 1560.10.0)
  6. /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
  7. /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.200.5)
  8. /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation (compatibility version 150.0.0, current version 1560.10.0)
  9. /System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 61000.0.0)

查看动态库的依赖库:

  1. $ otool -L /usr/lib/system/libdispatch.dylib
  2. /usr/lib/system/libdispatch.dylib:
  3. /usr/lib/system/libdispatch.dylib (compatibility version 1.0.0, current version 913.60.3)
  4. /usr/lib/system/libdyld.dylib (compatibility version 1.0.0, current version 551.4.0)
  5. /usr/lib/system/libcompiler_rt.dylib (compatibility version 1.0.0, current version 62.0.0)
  6. /usr/lib/system/libsystem_kernel.dylib (compatibility version 1.0.0, current version 4570.71.8)
  7. /usr/lib/system/libsystem_platform.dylib (compatibility version 1.0.0, current version 161.50.1)
  8. /usr/lib/system/libsystem_pthread.dylib (compatibility version 1.0.0, current version 301.50.1)
  9. /usr/lib/system/libsystem_malloc.dylib (compatibility version 1.0.0, current version 140.50.6)
  10. /usr/lib/system/libsystem_c.dylib (compatibility version 1.0.0, current version 1244.50.9)
  11. /usr/lib/system/libsystem_blocks.dylib (compatibility version 1.0.0, current version 67.0.0)
  12. /usr/lib/system/libunwind.dylib (compatibility version 1.0.0, current version 35.3.0)
  13. /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)

2.3 otool -ov

显示 Objective-C 类结构及其定义的方法。

终端执行命令:

  1. $ otool -ov Demo
  2. Demo:
  3. Contents of (__DATA,__objc_classlist) section
  4. 00000001000041f0 0x100005080 _OBJC_CLASS_$_HookTool
  5. isa 0x1000050a8 _OBJC_METACLASS_$_HookTool
  6. superclass 0x0 _OBJC_CLASS_$_NSObject
  7. cache 0x0 __objc_empty_cache
  8. vtable 0x0
  9. data 0x100004328 (struct class_ro_t *)
  10. flags 0x80
  11. instanceStart 8
  12. instanceSize 8
  13. reserved 0x0
  14. ivarLayout 0x0
  15. name 0x100003555 HookTool
  16. baseMethods 0x1000042f0 (struct method_list_t *)
  17. entsize 24
  18. count 2
  19. name 0x1000028b3 swizzle_decodeObjectForKey:
  20. types 0x1000035c4 @24@0:8@16
  21. imp 0x1000015f0 -[HookTool swizzle_decodeObjectForKey:]
  22. name 0x100002914 swizzle_button_initWithCoder:
  23. types 0x1000035c4 @24@0:8@16
  24. imp 0x1000017c0 -[HookTool swizzle_button_initWithCoder:]
  25. baseProtocols 0x0
  26. ivars 0x0
  27. weakIvarLayout 0x0
  28. baseProperties 0x0
  29. Meta Class
  30. ...

2.4 otool -tV [Mach-O]

查看 ARM 汇编码

  1. $ otool -tV Demo
  2. Demo:
  3. (__TEXT,__text) section
  4. +[HookTool load]:
  5. 0000000100001400 pushq %rbp
  6. 0000000100001401 movq %rsp, %rbp
  7. 0000000100001404 subq $0x40, %rsp
  8. 0000000100001408 movl $0x2, %eax
  9. 000000010000140d movl %eax, %edx
  10. 000000010000140f movq %rdi, -0x8(%rbp)
  11. 0000000100001413 movq %rsi, -0x10(%rbp)
  12. 0000000100001417 movq 0x3c1a(%rip), %rsi ## Objc class ref: _OBJC_CLASS_$_NSMutableArray
  13. 000000010000141e movq 0x3b33(%rip), %rdi ## Objc selector ref: arrayWithCapacity:
  14. 0000000100001425 movq %rdi, -0x20(%rbp)
  15. 0000000100001429 movq %rsi, %rdi
  16. 000000010000142c movq -0x20(%rbp), %rsi
  17. 0000000100001430 callq *0x2bf2(%rip) ## Objc message: +[NSMutableArray arrayWithCapacity:]
  18. 0000000100001436 movq %rax, %rdi
  19. 0000000100001439 callq 0x10000265a ## symbol stub for: _objc_retainAutoreleasedReturnValue
  20. 000000010000143e movq __imageViewImageArray(%rip), %rdx
  21. 0000000100001445 movq %rax, __imageViewImageArray(%rip)
  22. 000000010000144c movq %rdx, %rdi
  23. 000000010000144f callq *0x2bdb(%rip) ## literal pool symbol address: _objc_release
  24. 0000000100001455 leaq 0x2cb4(%rip), %rax ## Objc cfstring ref: @"emaNecruoseRIU"
  25. 000000010000145c movq 0x3bdd(%rip), %rdx ## Objc class ref: HookTool
  26. 0000000100001463 movq 0x3af6(%rip), %rsi ## Objc selector ref: stringByReversed:
  27. 000000010000146a movq %rdx, %rdi
  28. ...

2.5 otool -h [Mach-O]

查看 Mach-O 头结构等

  1. $ otool -h Demo
  2. Mach header
  3. magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
  4. 0xfeedfacf 16777223 3 0x00 2 21 3272 0x00200085

一个 Mach-O 的文件头结构为:

各字段的含义,可参看 /usr/include/mach-o/loader.h

2.6 otool -l [Mach-O] | grep crypt1

查看 ipa 包是否加壳

  1. $ otool -l Demo | grep crypt1
  2. $

没有进行过加壳处理。

  1. cryptoff 16384
  2. cryptsize 6651904
  3. cryptid 0
  4. cryptoff 16384
  5. cryptsize 6553600
  6. cryptid 0123456

cryptid 代表是否加壳,1 - 加壳,0 - 已脱壳。

上面打印了两遍,其实代表着该可执行文件支持两种架构 armv7 和 arm64。

Mach-O 文件可以用 GUI 图形软件 MachOView 更加直观的查看相关信息。

三、dyld加载

动态库链接、load 方法执行都是在 main 函数执行之前的。

如图所示进行操作:



由上可知,load 的加载是从 __dyld_start 这个函数开始的。

3.1 __dyld_start

系统内核在加载动态库前,会加载 dyld,然后调用去执行 __dyld_start(汇编语言实现)。该函数会执行 dyldbootstrap::start(),后者会执行 _main()函数,dyld 的加载动态库的代码就是从_main()开始执行的。这里可以查看 dyldStartup.s的部分内容(以x86_x64架构做参考),其中标出了 _dyld_start() 与 dyldbootstrap 的 start 方法。

  1. #if __x86_64__
  2. #if !TARGET_IPHONE_SIMULATOR
  3. .data
  4. .align 3
  5. __dyld_start_static:
  6. .quad __dyld_start
  7. #endif
  8. #if !TARGET_IPHONE_SIMULATOR
  9. .text
  10. .align 2,0x90
  11. .globl __dyld_start
  12. __dyld_start:
  13. popq %rdi # param1 = mh of app
  14. pushq $0 # push a zero for debugger end of frames marker
  15. movq %rsp,%rbp # pointer to base of kernel frame
  16. andq $-16,%rsp # force SSE alignment
  17. subq $16,%rsp # room for local variables
  18. # call dyldbootstrap::start(app_mh, argc, argv, slide, dyld_mh, &startGlue)
  19. movl 8(%rbp),%esi # param2 = argc into %esi
  20. leaq 16(%rbp),%rdx # param3 = &argv[0] into %rdx
  21. movq __dyld_start_static(%rip), %r8
  22. leaq __dyld_start(%rip), %rcx
  23. subq %r8, %rcx # param4 = slide into %rcx
  24. leaq ___dso_handle(%rip),%r8 # param5 = dyldsMachHeader
  25. leaq -8(%rbp),%r9
  26. call __ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm
  27. movq -8(%rbp),%rdi
  28. cmpq $0,%rdi
  29. jne Lnew
  30. # clean up stack and jump to "start" in main executable
  31. movq %rbp,%rsp # restore the unaligned stack pointer
  32. addq $8,%rsp # remove the mh argument, and debugger end frame marker
  33. movq $0,%rbp # restore ebp back to zero
  34. jmp *%rax # jump to the entry point
  35. # LC_MAIN case, set up stack for call to main()

3.2 dyldInitialization.cpp

__dyld_start 内部调用 dyldbootstrap::start,位于 dyldInitialization.cpp。

  1. //
  2. // This is code to bootstrap dyld. This work in normally done for a program by dyld and crt.
  3. // In dyld we have to do this manually.
  4. //
  5. uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[],
  6. intptr_t slide, const struct macho_header* dyldsMachHeader,
  7. uintptr_t* startGlue)
  8. {
  9. // if kernel had to slide dyld, we need to fix up load sensitive locations
  10. // we have to do this before using any global variables
  11. // ①、获取 dyld 对应的 slide
  12. slide = slideOfMainExecutable(dyldsMachHeader);
  13. bool shouldRebase = slide != 0;
  14. #if __has_feature(ptrauth_calls)
  15. shouldRebase = true;
  16. #endif
  17. if ( shouldRebase ) {
  18. // ②、通过 slide 对 dyld 进行 rebase
  19. rebaseDyld(dyldsMachHeader, slide);
  20. }
  21. // allow dyld to use mach messaging
  22. // ③、mach 初始化
  23. mach_init();
  24. // kernel sets up env pointer to be just past end of agv array
  25. const char** envp = &argv[argc+1];
  26. // kernel sets up apple pointer to be just past end of envp array
  27. const char** apple = envp;
  28. // ④、栈溢出保护
  29. while(*apple != NULL) { ++apple; }
  30. ++apple;
  31. // set up random value for stack canary
  32. __guard_setup(apple);
  33. #if DYLD_INITIALIZER_SUPPORT
  34. // run all C++ initializers inside dyld
  35. runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);
  36. #endif
  37. // now that we are done bootstrapping dyld, call dyld's main
  38. // ⑤、获取应用的 slide(appsSlide)
  39. uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
  40. // ⑥、调用 dyld 的 main 函数
  41. return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
  42. }

3.3 slide、rebase

由于 apple 采用了 ASLR(Address space layout randomization)地址空间布局随机化技术。在 ASLR 技术出现之前,程序都是在固定的地址加载的,这样 hacker 可以知道程序里面某个函数的具体地址,植入某些恶意代码,修改函数的地址等,带来了很多的危险性。ASLR 就是为了解决这个的,程序每次启动后地址都会随机变化,这样程序里所有的代码地址都需要需要重新对进行计算修复才能正常访问。Mach-O 每次加载到内存中的首地址是变化的,此时想找到代码在内存中对应的地址需要重定位 rebase。rebase 要用到 slide 值:

  1. //
  2. // The kernel may have slid a Position Independent Executable
  3. //
  4. static uintptr_t slideOfMainExecutable(const struct macho_header* mh)
  5. {
  6. // Mach-O 文件中 load commands 数量
  7. const uint32_t cmd_count = mh->ncmds;
  8. // 偏移地址到 load commands 的首地址
  9. const struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(macho_header));
  10. const struct load_command* cmd = cmds;
  11. for (uint32_t i = 0; i < cmd_count; ++i) {
  12. // 选中 cmd = LC_SEGMENT_COMMAND
  13. if ( cmd->cmd == LC_SEGMENT_COMMAND ) {
  14. const struct macho_segment_command* segCmd = (struct macho_segment_command*)cmd;
  15. // 实际对应 LC_SEGMENT_COMMAND(_TEXT)
  16. if ( (segCmd->fileoff == 0) && (segCmd->filesize != 0)) {
  17. // Mach-O 文件首地址 - LC_SEGMENT_COMMAND(_TEXT).vmaddr
  18. return (uintptr_t)mh - segCmd->vmaddr;
  19. }
  20. }
  21. // 偏移 command 指针
  22. cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
  23. }
  24. return 0;
  25. }

应用本身的 Mach-O 及 dyld 采用的是 slideOfMainExecutable 的方式获取 slide。从上代码得知:side = Mach-O header 首地址 - Load Commands 中 __TEXT 段的 VM Address 的值。

  1. intptr_t _dyld_get_image_slide(const mach_header* mh)
  2. {
  3. log_apis("_dyld_get_image_slide(%p)\n", mh);
  4. // 获取 Mach-O 文件加载对象
  5. const MachOLoaded* mf = (MachOLoaded*)mh;
  6. // 如果 mach 文件头没有 magic 值
  7. if ( !mf->hasMachOMagic() )
  8. return 0;
  9. // 调用 MachOLoaded::getSlide() 方法
  10. return mf->getSlide();
  11. }
  12. intptr_t _dyld_get_image_vmaddr_slide(uint32_t imageIndex)
  13. {
  14. log_apis("_dyld_get_image_vmaddr_slide(%d)\n", imageIndex);
  15. // 获取到 Mach-O 文件
  16. const mach_header* mh = gAllImages.imageLoadAddressByIndex(imageIndex);
  17. if ( mh != nullptr )
  18. // 调用上面的方法
  19. return dyld3::_dyld_get_image_slide(mh);
  20. return 0;
  21. }
  1. intptr_t MachOLoaded::getSlide() const
  2. {
  3. // 诊断对象。
  4. Diagnostics diag;
  5. __block intptr_t slide = 0;
  6. // 循环 load command
  7. forEachLoadCommand(diag, ^(const load_command* cmd, bool& stop) {
  8. // 64 位
  9. if ( cmd->cmd == LC_SEGMENT_64 ) {
  10. const segment_command_64* seg = (segment_command_64*)cmd;
  11. // LC_SEGMENT_64(__TEXT)
  12. if ( strcmp(seg->segname, "__TEXT") == 0 ) {
  13. // mach-O 首地址 - LC_SEGMENT_64(__TEXT).vmaddr
  14. slide = (uintptr_t)(((uint64_t)this) - seg->vmaddr);
  15. stop = true;
  16. }
  17. }
  18. // 32 位
  19. else if ( cmd->cmd == LC_SEGMENT ) {
  20. const segment_command* seg = (segment_command*)cmd;
  21. // LC_SEGMENT(__TEXT)
  22. if ( strcmp(seg->segname, "__TEXT") == 0 ) {
  23. // mach-O 首地址 - LC_SEGMENT(__TEXT).vmaddr
  24. slide = (uintptr_t)(((uint64_t)this) - seg->vmaddr);
  25. stop = true;
  26. }
  27. }
  28. });
  29. diag.assertNoError(); // any malformations in the file should have been caught by earlier validate() call
  30. return slide;
  31. }

动态库加载采用的是 \_dyld\_get\_image\_vmaddr\_slide 的方式获取 slide。

简单验证一下,以应用 Mach-O 为例:

  1. Load Commands __TEXT 段 VM Address 值。

    VM Address 的地址为 4294967296(10进制)。

  2. 在 Demo 项目中 ViewController.m viewDidLoad 方法设置断点,触发后,在 lldb 执行 image list

    应用 Mach-O 的地址为 0x00000001004f8000(16进制)。

  3. 计算 viewDidLoad 在应用 Mach-O 文件中的地址,symbol address = stack address - slide

    ①、用 Mach-O 的 VM Address 减去对应虚拟地址,得到的 5210112(10进制)为 slide 值;
    ②、获取 viewDidLoad 函数在当前内存中的地址;
    ③、用 viewDidLoad 内存地址减去 slide 得到它在 Mach-O 中对应的虚拟地址;
    ④、将 10 进制转化为 16 进制。

    计算得到地址:0x00000001000022c0

  4. 在 Mach-O 文件中查看。

    可以看到,通过计算得出的值 0x100001750 与 Mach-O 中看到的值一致。

当然,也可以通过命令行直接获取 slide 的值。

3.4 dyld::_main

对 ASLR 有了基本认知后,接着看看位于 dyld.cpp 中的 _main 干了什么。

3.4.1 设置运行环境
  1. //
  2. // Entry point for dyld. The kernel loads dyld and jumps to __dyld_start which
  3. // sets up some registers and call this function.
  4. //
  5. // Returns address of main() in target program which __dyld_start jumps to
  6. //
  7. uintptr_t
  8. _main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
  9. int argc, const char* argv[], const char* envp[], const char* apple[],
  10. uintptr_t* startGlue)
  11. {
  12. if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
  13. launchTraceID = dyld3::kdebug_trace_dyld_duration_start(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, (uint64_t)mainExecutableMH, 0, 0);
  14. }
  15. // Grab the cdHash of the main executable from the environment
  16. uint8_t mainExecutableCDHashBuffer[20];
  17. const uint8_t* mainExecutableCDHash = nullptr;
  18. if ( hexToBytes(_simple_getenv(apple, "executable_cdhash"), 40, mainExecutableCDHashBuffer) )
  19. // 获取主程序 hash
  20. mainExecutableCDHash = mainExecutableCDHashBuffer;
  21. // Trace dyld's load
  22. // 告知 kernel,dyld 已加载
  23. notifyKernelAboutImage((macho_header*)&__dso_handle, _simple_getenv(apple, "dyld_file"));
  24. #if !TARGET_IPHONE_SIMULATOR
  25. // Trace the main executable's load
  26. // 告知 kernel,主程序 Mach-O 已加载
  27. notifyKernelAboutImage(mainExecutableMH, _simple_getenv(apple, "executable_file"));
  28. #endif
  29. uintptr_t result = 0;
  30. // 赋值参数。
  31. // mach_header 类型结构体,表示当前 App 的 Mach-O头部信息。有了头部信息,加载器就可以从头开始,遍历整个 Mach-O 文件的信息。
  32. sMainExecutableMachHeader = mainExecutableMH;
  33. // long 类型数据,表示 ASLR 位移长度
  34. sMainExecutableSlide = mainExecutableSlide;
  35. #if __MAC_OS_X_VERSION_MIN_REQUIRED
  36. // if this is host dyld, check to see if iOS simulator is being run
  37. // 获取 dyld 路径
  38. const char* rootPath = _simple_getenv(envp, "DYLD_ROOT_PATH");
  39. if ( (rootPath != NULL) ) {
  40. // look to see if simulator has its own dyld
  41. char simDyldPath[PATH_MAX];
  42. strlcpy(simDyldPath, rootPath, PATH_MAX);
  43. strlcat(simDyldPath, "/usr/lib/dyld_sim", PATH_MAX);
  44. // 打开 dyld_sim 路径
  45. int fd = my_open(simDyldPath, O_RDONLY, 0);
  46. // 成功
  47. if ( fd != -1 ) {
  48. // 如果是模拟器,并且正确加载`dyld_sim`,则直接返回主程序地址
  49. const char* errMessage = useSimulatorDyld(fd, mainExecutableMH, simDyldPath, argc, argv, envp, apple, startGlue, &result);
  50. if ( errMessage != NULL )
  51. halt(errMessage);
  52. return result;
  53. }
  54. }
  55. #endif
  56. CRSetCrashLogMessage("dyld: launch started");
  57. // 设置一个全局链接上下文,包括一些回调函数、参数与标志设置信息,其中的 context 结构体实例、回调函数都是 dyld 自己的实现
  58. setContext(mainExecutableMH, argc, argv, envp, apple);
  59. // Pickup the pointer to the exec path.
  60. // 获取主程序路径
  61. sExecPath = _simple_getenv(apple, "executable_path");
  62. // <rdar://problem/13868260> Remove interim apple[0] transition code from dyld
  63. if (!sExecPath) sExecPath = apple[0];
  64. // 获取应用 Mach-O 文件的绝对路径
  65. if ( sExecPath[0] != '/' ) {
  66. // have relative path, use cwd to make absolute
  67. char cwdbuff[MAXPATHLEN];
  68. if ( getcwd(cwdbuff, MAXPATHLEN) != NULL ) {
  69. // maybe use static buffer to avoid calling malloc so early...
  70. char* s = new char[strlen(cwdbuff) + strlen(sExecPath) + 2];
  71. strcpy(s, cwdbuff);
  72. strcat(s, "/");
  73. strcat(s, sExecPath);
  74. sExecPath = s;
  75. }
  76. }
  77. // Remember short name of process for later logging
  78. // 设置进程名称
  79. sExecShortName = ::strrchr(sExecPath, '/');
  80. if ( sExecShortName != NULL )
  81. ++sExecShortName;
  82. else
  83. sExecShortName = sExecPath;
  84. // 配置进程受限模式。根据当前进程是否受限,再次配置链接上下文以及其他环境参数
  85. configureProcessRestrictions(mainExecutableMH);
  86. // 再次检测/设置上下文环境
  87. #if __MAC_OS_X_VERSION_MIN_REQUIRED
  88. if ( !gLinkContext.allowEnvVarsPrint && !gLinkContext.allowEnvVarsPath && !gLinkContext.allowEnvVarsSharedCache ) {
  89. pruneEnvironmentVariables(envp, &apple);
  90. // set again because envp and apple may have changed or moved
  91. setContext(mainExecutableMH, argc, argv, envp, apple);
  92. }
  93. else
  94. #endif
  95. {
  96. checkEnvironmentVariables(envp);
  97. defaultUninitializedFallbackPaths(envp);
  98. }
  99. #if __MAC_OS_X_VERSION_MIN_REQUIRED
  100. if ( ((dyld3::MachOFile*)mainExecutableMH)->supportsPlatform(dyld3::Platform::iOSMac)
  101. && !((dyld3::MachOFile*)mainExecutableMH)->supportsPlatform(dyld3::Platform::macOS)) {
  102. gLinkContext.rootPaths = parseColonList("/System/iOSSupport", NULL);
  103. gLinkContext.marzipan = true;
  104. if ( sEnv.DYLD_FALLBACK_LIBRARY_PATH == sLibraryFallbackPaths )
  105. sEnv.DYLD_FALLBACK_LIBRARY_PATH = sRestrictedLibraryFallbackPaths;
  106. if ( sEnv.DYLD_FALLBACK_FRAMEWORK_PATH == sFrameworkFallbackPaths )
  107. sEnv.DYLD_FALLBACK_FRAMEWORK_PATH = sRestrictedFrameworkFallbackPaths;
  108. }
  109. #endif
  110. // 如果设置了DYLD_PRINT_OPTS,则打印参数
  111. if ( sEnv.DYLD_PRINT_OPTS )
  112. printOptions(argv);
  113. // 如果设置了DYLD_PRINT_ENV,则打印环境变量
  114. if ( sEnv.DYLD_PRINT_ENV )
  115. printEnvironmentVariables(envp);
  116. // 获取主程序架构信息
  117. getHostInfo(mainExecutableMH, mainExecutableSlide);
  118. ...

从源码可以看到,在模拟器运行程序时,通过 dyld_sim 来进行后续加载工作的,与正常真机加载流程略有不同。

模拟器:

真机:

具体实现在 useSimulatorDyld 这个函数中,本文不做进一步解析。

这里还有一个知识点,环境变量 DYLD_PRINT_OPTSDYLD_PRINT_ENV。在 processDyldEnvironmentVariable 方法中:

  1. else if ( strcmp(key, "DYLD_IMAGE_SUFFIX") == 0 ) {
  2. gLinkContext.imageSuffix = parseColonList(value, NULL);
  3. }
  4. else if ( strcmp(key, "DYLD_INSERT_LIBRARIES") == 0 ) {
  5. sEnv.DYLD_INSERT_LIBRARIES = parseColonList(value, NULL);
  6. #if SUPPORT_ACCELERATE_TABLES
  7. sDisableAcceleratorTables = true;
  8. #endif
  9. }

在 secheme 添加这两个环境变量,对应的字段会被设置为 true,并不需要设置 value。


但是并非每个环境变量都不需要配置 value,如:

  1. void processDyldEnvironmentVariable(const char* key, const char* value, const char* mainExecutableDir)
  2. {
  3. if ( strcmp(key, "DYLD_FRAMEWORK_PATH") == 0 ) {
  4. appendParsedColonList(value, mainExecutableDir, &sEnv.DYLD_FRAMEWORK_PATH);
  5. }
  6. else if ( strcmp(key, "DYLD_FALLBACK_FRAMEWORK_PATH") == 0 ) {
  7. appendParsedColonList(value, mainExecutableDir, &sEnv.DYLD_FALLBACK_FRAMEWORK_PATH);
  8. }
  9. else if ( strcmp(key, "DYLD_LIBRARY_PATH") == 0 ) {
  10. appendParsedColonList(value, mainExecutableDir, &sEnv.DYLD_LIBRARY_PATH);
  11. }
  12. else if ( strcmp(key, "DYLD_FALLBACK_LIBRARY_PATH") == 0 ) {
  13. appendParsedColonList(value, mainExecutableDir, &sEnv.DYLD_FALLBACK_LIBRARY_PATH);
  14. }
  15. ...
3.4.2 加载共享缓存

dyld3 与 dyld 不同点在 _main 方法中可以看出。在 dyld 的 _main 方法中,完成第一步以后会初始化主 App,然后加载共享缓存。到了 dyld3,调整了顺序:加载缓存的步骤可以划分为 mapSharedCache 和 checkVersionedPaths,先执行 mapSharedCache,然后加载主 App,最后checkVersionedPaths。(苹果在 2017 年发布的 dyld3,视频链接

对于共享缓存的理解:dyld 加载时,为了优化程序启动,启用了共享缓存(shared cache)技术。共享缓存会在进程启动时被 dyld 映射到内存中,之后,当任何 Mach-O 映像加载时,dyld 首先会检查该 Mach-O 映像及所需的动态库是否在共享缓存中,如果存在,则直接将它在共享内存中的内存地址映射到进程的内存地址空间。

  1. uintptr_t
  2. _main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
  3. int argc, const char* argv[], const char* envp[], const char* apple[],
  4. uintptr_t* startGlue)
  5. {
  6. ...
  7. // load shared cache
  8. // 检查共享缓存是否可用
  9. checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
  10. #if TARGET_IPHONE_SIMULATOR
  11. // <HACK> until <rdar://30773711> is fixed
  12. gLinkContext.sharedRegionMode = ImageLoader::kUsePrivateSharedRegion;
  13. // </HACK>
  14. #endif
  15. // 非 Dont Use
  16. if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
  17. // 映射共享缓存到共享区
  18. mapSharedCache();
  19. }
  20. // 缓存是否兼容(DyldSharedCache * loadAddress 为空 || 版本相同 -》YES)
  21. bool cacheCompatible = (sSharedCacheLoadInfo.loadAddress == nullptr) || (sSharedCacheLoadInfo.loadAddress->header.formatVersion == dyld3::closure::kFormatVersion);
  22. // 设置了 DYLD_USE_CLOSURES || 在白名单
  23. if ( cacheCompatible && (sEnableClosures || inWhiteList(sExecPath)) ) {
  24. }
  25. else {
  26. if ( gLinkContext.verboseWarnings )
  27. // 不使用closure,因为共享缓存格式版本与 dyld 不匹配
  28. dyld::log("dyld: not using closure because shared cache format version does not match dyld's\n");
  29. }
  30. // could not use closure info, launch old way
  31. // install gdb notifier
  32. stateToHandlers(dyld_image_state_dependents_mapped, sBatchHandlers)->push_back(notifyGDB);
  33. stateToHandlers(dyld_image_state_mapped, sSingleHandlers)->push_back(updateAllImages);
  34. // make initial allocations large enough that it is unlikely to need to be re-alloced
  35. sImageRoots.reserve(16);
  36. sAddImageCallbacks.reserve(4);
  37. sRemoveImageCallbacks.reserve(4);
  38. sAddLoadImageCallbacks.reserve(4);
  39. sImageFilesNeedingTermination.reserve(16);
  40. sImageFilesNeedingDOFUnregistration.reserve(8);
  41. #if !TARGET_IPHONE_SIMULATOR
  42. #ifdef WAIT_FOR_SYSTEM_ORDER_HANDSHAKE
  43. // <rdar://problem/6849505> Add gating mechanism to dyld support system order file generation process
  44. WAIT_FOR_SYSTEM_ORDER_HANDSHAKE(dyld::gProcessInfo->systemOrderFlag);
  45. #endif
  46. #endif
  47. try {
  48. // add dyld itself to UUID list
  49. // 添加 dyld 的 UUID 到共享缓存 UUID 列表中
  50. addDyldImageToUUIDList();
  51. ...
  52. }
  • 检测共享缓存是否可用;
  • 如果可用,映射共享缓存到共享区;
  • 添加 dyld 的 UUID 到缓存列表。

其中,检测共享缓存是否可用的函数 checkSharedRegionDisable 中有两句注释:

  1. static void checkSharedRegionDisable(const dyld3::MachOLoaded* mainExecutableMH, uintptr_t mainExecutableSlide)
  2. {
  3. #if __MAC_OS_X_VERSION_MIN_REQUIRED
  4. // if main executable has segments that overlap the shared region, then disable using the shared region
  5. // 如果主程序 Mach-O 有 segments 与共享区重叠,那么共享区不可用。并且,iOS 不开启共享区无法运行。
  6. // 检测两者是否重叠
  7. if ( mainExecutableMH->intersectsRange(SHARED_REGION_BASE, SHARED_REGION_SIZE) ) {
  8. gLinkContext.sharedRegionMode = ImageLoader::kDontUseSharedRegion;
  9. if ( gLinkContext.verboseMapping )
  10. dyld::warn("disabling shared region because main executable overlaps\n");
  11. }
  12. #if __i386__
  13. if ( !gLinkContext.allowEnvVarsPath ) {
  14. // <rdar://problem/15280847> use private or no shared region for suid processes
  15. gLinkContext.sharedRegionMode = ImageLoader::kUsePrivateSharedRegion;
  16. }
  17. #endif
  18. #endif
  19. // iOS cannot run without shared region
  20. }

具体检测代码:

  1. bool MachOLoaded::intersectsRange(uintptr_t start, uintptr_t length) const
  2. {
  3. __block bool result = false;
  4. uintptr_t slide = getSlide();
  5. forEachSegment(^(const SegmentInfo& info, bool& stop) {
  6. /*
  7. ①、主程序 segment 中的虚拟地址 + 虚拟地址大小 + 偏移量 >= 共享区起始地址
  8. ②、主程序 segment 中的虚拟地址 + 偏移量 < 共享区终止地址
  9. ① 和 ② 同时 YES,那么认为主程序 Mach-O 有 segments 与共享区重叠,此时共享区不可用,从而动态库缓存不可用
  10. 疑问:地址是从高到低分配?
  11. */
  12. if ( (info.vmAddr+info.vmSize+slide >= start) && (info.vmAddr+slide < start+length) )
  13. result = true;
  14. });
  15. return result;
  16. }

可以看到这段检测代码在满足重叠条件后,并没有设置 stop = true 停止 forEachLoadCommand 中的循环,这里值得深究和讨论。

加载共享缓存最核心的步骤在 mapSharedCache 中:

  1. static void mapSharedCache()
  2. {
  3. dyld3::SharedCacheOptions opts;
  4. opts.cacheDirOverride = sSharedCacheOverrideDir;
  5. opts.forcePrivate = (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion);
  6. #if __x86_64__ && !TARGET_IPHONE_SIMULATOR
  7. opts.useHaswell = sHaswell;
  8. #else
  9. opts.useHaswell = false;
  10. #endif
  11. opts.verbose = gLinkContext.verboseMapping;
  12. // 加载 dyld 缓存
  13. loadDyldCache(opts, &sSharedCacheLoadInfo);
  14. // update global state
  15. // 更新进程的全局状态信息
  16. if ( sSharedCacheLoadInfo.loadAddress != nullptr ) {
  17. gLinkContext.dyldCache = sSharedCacheLoadInfo.loadAddress;
  18. dyld::gProcessInfo->processDetachedFromSharedRegion = opts.forcePrivate;
  19. dyld::gProcessInfo->sharedCacheSlide = sSharedCacheLoadInfo.slide;
  20. dyld::gProcessInfo->sharedCacheBaseAddress = (unsigned long)sSharedCacheLoadInfo.loadAddress;
  21. sSharedCacheLoadInfo.loadAddress->getUUID(dyld::gProcessInfo->sharedCacheUUID);
  22. dyld3::kdebug_trace_dyld_image(DBG_DYLD_UUID_SHARED_CACHE_A, (const uuid_t *)&dyld::gProcessInfo->sharedCacheUUID[0], {0,0}, {{ 0, 0 }}, (const mach_header *)sSharedCacheLoadInfo.loadAddress);
  23. }
  24. }

SharedCacheRuntime.cpp 文件:

  1. bool loadDyldCache(const SharedCacheOptions& options, SharedCacheLoadInfo* results)
  2. {
  3. results->loadAddress = 0;
  4. results->slide = 0;
  5. results->errorMessage = nullptr;
  6. #if TARGET_IPHONE_SIMULATOR
  7. // simulator only supports mmap()ing cache privately into process
  8. // 模拟器只支持 mmap(内存映射) 缓存到当前进程
  9. return mapCachePrivate(options, results);
  10. #else
  11. if ( options.forcePrivate ) {
  12. // mmap cache into this process only
  13. // 只加载 mmap(内存映射) 缓存到当前进程
  14. return mapCachePrivate(options, results);
  15. }
  16. else {
  17. // fast path: when cache is already mapped into shared region
  18. bool hasError = false;
  19. // 已加载过的
  20. if ( reuseExistingCache(options, results) ) {
  21. hasError = (results->errorMessage != nullptr);
  22. }
  23. // 未加载过的
  24. else {
  25. // slow path: this is first process to load cache
  26. hasError = mapCacheSystemWide(options, results);
  27. }
  28. return hasError;
  29. }
  30. #endif
  31. }

加载缓存分三种情况:

①、仅加载到当前进程。通过 mapCachePrivate() 加载并返回错误信息;
②、已经加载过的。通过 reuseExistingCache() 加载并返回错误信息,同时返回是否加载过 BOOL 值;
③、未加载过的。通过 mapCacheSystemWide() 加载缓存并映射,返回错误信息。

options.forcePrivate 的定义:

  1. // dyld.cpp
  2. opts.forcePrivate = (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion)
  3. gLinkContext.sharedRegionMode = ImageLoader::kUseSharedRegion;
  4. // ImageLoader.h
  5. class ImageLoader {
  6. public:
  7. ...
  8. enum SharedRegionMode { kUseSharedRegion, kUsePrivateSharedRegion, kDontUseSharedRegion, kSharedRegionIsSharedCache };
  9. ...
  10. }

gLinkContext.sharedRegionMode 在 setContext() 方法中设置默认值,默认值为 kUseSharedRegion,也就是之前检测共享区是否可用的标识值。

3.4.3 实例化主程序

系统会对已经映射到进程空间的主程序(在 XNU 解析 MachO 阶段就完成了映射操作)创建一个ImageLoaderMachO,再将其加入到 master list 中(sAllImages)。如果加载的 MachO 的硬件架构与本设备相符,就执行 imageLoader 的创建和添加操作。其中主要实现是ImageLoaderMachO::instantiateMainExecutable方法,该方法将主 App 的 MachHeader、ASLR,文件路径和前面提到的链接上下文作为参数,做 imageLoader 的实例化操作。

  1. uintptr_t
  2. _main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
  3. int argc, const char* argv[], const char* envp[], const char* apple[],
  4. uintptr_t* startGlue)
  5. {
  6. ...
  7. CRSetCrashLogMessage(sLoadingCrashMessage);
  8. // instantiate ImageLoader for main executable
  9. // 实例化主程序
  10. sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
  11. gLinkContext.mainExecutable = sMainExecutable;
  12. // 代码签名
  13. gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
  14. #if TARGET_IPHONE_SIMULATOR
  15. // check main executable is not too new for this OS
  16. // 检测主程序是否支持当前设备版本
  17. {
  18. // 检查是否是模拟器二进制文件
  19. if ( ! isSimulatorBinary((uint8_t*)mainExecutableMH, sExecPath) ) {
  20. throwf("program was built for a platform that is not supported by this runtime");
  21. }
  22. uint32_t mainMinOS = sMainExecutable->minOSVersion();
  23. // dyld is always built for the current OS, so we can get the current OS version
  24. // from the load command in dyld itself.
  25. // 获取 dyld 中存储的当前 OS 版本
  26. uint32_t dyldMinOS = ImageLoaderMachO::minOSVersion((const mach_header*)&__dso_handle);
  27. // 应用 mach-O 文件的版本超过了当前模拟器设备的版本,抛出异常
  28. if ( mainMinOS > dyldMinOS ) {
  29. #if TARGET_OS_WATCH
  30. throwf("app was built for watchOS %d.%d which is newer than this simulator %d.%d",
  31. mainMinOS >> 16, ((mainMinOS >> 8) & 0xFF),
  32. dyldMinOS >> 16, ((dyldMinOS >> 8) & 0xFF));
  33. #elif TARGET_OS_TV
  34. throwf("app was built for tvOS %d.%d which is newer than this simulator %d.%d",
  35. mainMinOS >> 16, ((mainMinOS >> 8) & 0xFF),
  36. dyldMinOS >> 16, ((dyldMinOS >> 8) & 0xFF));
  37. #else
  38. throwf("app was built for iOS %d.%d which is newer than this simulator %d.%d",
  39. mainMinOS >> 16, ((mainMinOS >> 8) & 0xFF),
  40. dyldMinOS >> 16, ((dyldMinOS >> 8) & 0xFF));
  41. #endif
  42. }
  43. }
  44. #endif
  45. #if __MAC_OS_X_VERSION_MIN_REQUIRED
  46. // <rdar://problem/22805519> be less strict about old mach-o binaries
  47. uint32_t mainSDK = sMainExecutable->sdkVersion();
  48. gLinkContext.strictMachORequired = (mainSDK >= DYLD_MACOSX_VERSION_10_12) || gLinkContext.allowInsertFailures;
  49. #else
  50. // simulators, iOS, tvOS, and watchOS are always strict
  51. gLinkContext.strictMachORequired = true;
  52. #endif
  53. #if SUPPORT_ACCELERATE_TABLES
  54. sAllImages.reserve((sAllCacheImagesProxy != NULL) ? 16 : INITIAL_IMAGE_COUNT);
  55. #else
  56. sAllImages.reserve(INITIAL_IMAGE_COUNT);
  57. #endif
  58. // Now that shared cache is loaded, setup an versioned dylib overrides
  59. #if SUPPORT_VERSIONED_PATHS
  60. checkVersionedPaths(); // 设置加载的动态库版本。这里的动态库还没有包括经 DYLD_INSERT_LIBRARIES 插入的库。
  61. #endif
  62. // dyld_all_image_infos image list does not contain dyld
  63. // add it as dyldPath field in dyld_all_image_infos
  64. // for simulator, dyld_sim is in image list, need host dyld added
  65. // dyld 加载的 image_infos 并不包含 dyld 本身,它被放到 dyld_all_image_infos 的 dyldPath 字段中去了。而对于模拟器,dyld 加载的 image_infos 是包含 dyld_sim 的。
  66. #if TARGET_IPHONE_SIMULATOR
  67. // get path of host dyld from table of syscall vectors in host dyld
  68. void* addressInDyld = gSyscallHelpers;
  69. #else
  70. // get path of dyld itself
  71. void* addressInDyld = (void*)&__dso_handle;
  72. #endif
  73. // 获取 dyld 路径并与 gProcessInfo->dyldPath 对比
  74. char dyldPathBuffer[MAXPATHLEN+1];
  75. int len = proc_regionfilename(getpid(), (uint64_t)(long)addressInDyld, dyldPathBuffer, MAXPATHLEN);
  76. if ( len > 0 ) {
  77. dyldPathBuffer[len] = '\0'; // proc_regionfilename() does not zero terminate returned string
  78. // 如果不同将获取到的路径复制给 gProcessInfo->dyldPath
  79. if ( strcmp(dyldPathBuffer, gProcessInfo->dyldPath) != 0 )
  80. gProcessInfo->dyldPath = strdup(dyldPathBuffer);
  81. }
  82. ...
  83. }

dyld_all_image_infos 是个结构体,同样分为 32 位和 64 位两个版本,分别对应 dyld_all_image_infos_32 与 dyld_all_image_infos_64,由于获取 dyld_all_image_infos 需要用到一些未开源信息,这里为了方便,从侧面验证一下这条注释信息:

  1. #import <mach-o/dyld.h>
  2. - (void)viewDidLoad
  3. {
  4. [super viewDidLoad];
  5. for (uint32_t i = 0; i < _dyld_image_count(); ++i) {
  6. NSLog(@"%s", _dyld_get_image_name(i));
  7. }
  8. }

模拟器:

真机:

可以看到:模拟器打印的 image 没有 dyld,第 0 个 image 是 dyld_sim,第一个 image 才是主程序;真机打印出的加载 image 中也没有 dyld,第 0 个 image 是主程序。

回到最核心的 instantiateFromLoadedImage 实例化主程序函数:

  1. // The kernel maps in main executable before dyld gets control. We need to
  2. // make an ImageLoader* for the already mapped in main executable.
  3. // kernel 在 dyld 之前已经映射了主程序 Mach-O,dyld 判断 Mach-O 的兼容性后,实例化成 ImageLoader 加载到内存中交给 dyld 管理
  4. static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
  5. {
  6. // try mach-o loader
  7. // CPU 架构是否匹配
  8. if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
  9. // 实例化 ImageLoader 对象。参数:macho header、ASLR、执行路径、链接上下文
  10. ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
  11. // 分配主程序image的内存,更新。
  12. addImage(image);
  13. return (ImageLoaderMachO*)image;
  14. }
  15. throw "main executable not a known format";
  16. }

kernel 在 dyld 之前已经映射了主程序 Mach-O,dyld 判断 Mach-O 的兼容性后,实例化ImageLoader 对象,加载到内存,返回交给 dyld 管理。

  1. // create image for main executable
  2. ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
  3. {
  4. //dyld::log("ImageLoader=%ld, ImageLoaderMachO=%ld, ImageLoaderMachOClassic=%ld, ImageLoaderMachOCompressed=%ld\n",
  5. // sizeof(ImageLoader), sizeof(ImageLoaderMachO), sizeof(ImageLoaderMachOClassic), sizeof(ImageLoaderMachOCompressed));
  6. bool compressed;
  7. unsigned int segCount;
  8. unsigned int libCount;
  9. const linkedit_data_command* codeSigCmd;
  10. const encryption_info_command* encryptCmd;
  11. // sniffLoadCommands 函数会对主程序 Mach-O进 行一系列的校验:对代码签名,MachO加密,动态库数量,段的数量相关信息的 loadCommand 做解析,提取出 command 数据。
  12. /* case LC_DYLD_INFO:
  13. case LC_DYLD_INFO_ONLY:
  14. *compressed = true;
  15. */
  16. sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
  17. // instantiate concrete class based on content of load commands
  18. // 已解密
  19. if ( compressed )
  20. // Compressed
  21. return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
  22. else
  23. #if SUPPORT_CLASSIC_MACHO
  24. // Classic
  25. return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
  26. #else
  27. throw "missing LC_DYLD_INFO load command";
  28. #endif
  29. }

sniffLoadCommands 的校验并不包括对主程序 Mach-O 的解密操作,解密操作是由 xnu 完成的。

ImageLoaderMachOCompressed::instantiateMainExecutable、ImageLoaderMachOClassic::instantiateMainExecutable 两者内部的逻辑相同,只是返回类型一个是 ImageLoaderMachOCompressed 一个是 ImageLoaderMachOClassic。

以 ImageLoaderMachOCompressed 为例:

  1. // create image for main executable
  2. ImageLoaderMachOCompressed* ImageLoaderMachOCompressed::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path,
  3. unsigned int segCount, unsigned int libCount, const LinkContext& context)
  4. {
  5. // 初始化 image
  6. ImageLoaderMachOCompressed* image = ImageLoaderMachOCompressed::instantiateStart(mh, path, segCount, libCount);
  7. // set slide for PIE programs
  8. // 设置 image 偏移量
  9. image->setSlide(slide);
  10. // for PIE record end of program, to know where to start loading dylibs
  11. if ( slide != 0 )
  12. // 设置动态库起始地址
  13. fgNextPIEDylibAddress = (uintptr_t)image->getEnd();
  14. // 禁用段覆盖检测
  15. image->disableCoverageCheck();
  16. // 结束 image 上下文
  17. image->instantiateFinish(context);
  18. // 设置 image 加载状态为 dyld_image_state_mapped
  19. image->setMapped(context);
  20. if ( context.verboseMapping ) {
  21. dyld::log("dyld: Main executable mapped %s\n", path);
  22. for(unsigned int i=0, e=image->segmentCount(); i < e; ++i) {
  23. const char* name = image->segName(i);
  24. if ( (strcmp(name, "__PAGEZERO") == 0) || (strcmp(name, "__UNIXSTACK") == 0) )
  25. dyld::log("%18s at 0x%08lX->0x%08lX\n", name, image->segPreferredLoadAddress(i), image->segPreferredLoadAddress(i)+image->segSize(i));
  26. else
  27. dyld::log("%18s at 0x%08lX->0x%08lX\n", name, image->segActualLoadAddress(i), image->segActualEndAddress(i));
  28. }
  29. }
  30. return image;
  31. }
  1. void ImageLoader::setMapped(const LinkContext& context)
  2. {
  3. fState = dyld_image_state_mapped;
  4. context.notifySingle(dyld_image_state_mapped, this, NULL); // note: can throw exception
  5. }

instantiateFinish() 在内部解析 loadCmds、设置动态库连接信息、设置符号表相关信息等。setMapped() 内部调用 notifySingle 进行处理。

3.4.4 加载插入的动态库
  1. uintptr_t
  2. _main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
  3. int argc, const char* argv[], const char* envp[], const char* apple[],
  4. uintptr_t* startGlue)
  5. {
  6. ...
  7. // load any inserted libraries
  8. // 插入动态库
  9. if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
  10. for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
  11. loadInsertedDylib(*lib);
  12. }
  13. // record count of inserted libraries so that a flat search will look at
  14. // inserted libraries, then main, then others.
  15. // 记录插入的动态库个数
  16. sInsertedDylibCount = sAllImages.size()-1;
  17. ...
  18. }

如果配置了 DYLD_INSERT_LIBRARIES 环境变量,通过loadInsertedDylib() 方法插入配置的动态库。对于越狱插件而言,其实就是通过添加 DYLD_INSERT_LIBRARIES 这个环境变量达到加载插件的目的。

  1. static void loadInsertedDylib(const char* path)
  2. {
  3. ImageLoader* image = NULL;
  4. unsigned cacheIndex;
  5. try {
  6. LoadContext context;
  7. context.useSearchPaths = false;
  8. context.useFallbackPaths = false;
  9. context.useLdLibraryPath = false;
  10. context.implicitRPath = false;
  11. context.matchByInstallName = false;
  12. context.dontLoad = false;
  13. context.mustBeBundle = false;
  14. context.mustBeDylib = true;
  15. context.canBePIE = false;
  16. context.enforceIOSMac = true;
  17. context.origin = NULL; // can't use @loader_path with DYLD_INSERT_LIBRARIES
  18. context.rpath = NULL;
  19. image = load(path, context, cacheIndex);
  20. }
  21. catch (const char* msg) {
  22. if ( gLinkContext.allowInsertFailures )
  23. dyld::log("dyld: warning: could not load inserted library '%s' into hardened process because %s\n", path, msg);
  24. else
  25. halt(dyld::mkstringf("could not load inserted library '%s' because %s\n", path, msg));
  26. }
  27. catch (...) {
  28. halt(dyld::mkstringf("could not load inserted library '%s'\n", path));
  29. }
  30. }

内部构建 context 后调用 load() 函数生成 image。

  1. /**
  2. * @brief 做路径展开,搜索查找,排重,以及缓存查找工作。其中路径的展开和搜索分几个阶段(phase)
  3. */
  4. ImageLoader* load(const char* path, const LoadContext& context, unsigned& cacheIndex)
  5. {
  6. ...
  7. // 查找 image
  8. ImageLoader* image = loadPhase0(path, orgPath, context, cacheIndex, NULL);
  9. // 没有找到
  10. if ( image != NULL ) {
  11. // 继续查找
  12. CRSetCrashLogMessage2(NULL);
  13. return image;
  14. }
  15. // try all path permutations and try open() until first success
  16. std::vector<const char*> exceptions;
  17. image = loadPhase0(path, orgPath, context, cacheIndex, &exceptions);
  18. #if !TARGET_IPHONE_SIMULATOR
  19. // <rdar://problem/16704628> support symlinks on disk to a path in dyld shared cache
  20. // 在共享缓存中查找
  21. if ( image == NULL)
  22. image = loadPhase2cache(path, orgPath, context, cacheIndex, &exceptions);
  23. #endif
  24. CRSetCrashLogMessage2(NULL);
  25. if ( image != NULL ) {
  26. // <rdar://problem/6916014> leak in dyld during dlopen when using DYLD_ variables
  27. for (std::vector<const char*>::iterator it = exceptions.begin(); it != exceptions.end(); ++it) {
  28. free((void*)(*it));
  29. }
  30. // if loaded image is not from cache, but original path is in cache
  31. // set gSharedCacheOverridden flag to disable some ObjC optimizations
  32. if ( !gSharedCacheOverridden && !image->inSharedCache() && image->isDylib() && cacheablePath(path) && inSharedCache(path) ) {
  33. gSharedCacheOverridden = true;
  34. }
  35. return image;
  36. }
  37. ...
  38. }

load 方法不仅被 loadInsertedDylib 调用,也会被 dlopen 等运行时加载动态库的方法使用。

内部有一整套 loadPhase0~loadPhase6 的流程来查找及加载 image。如果在共享缓存中找到则直接调用 instantiateFromCache 实例化 image,否则通过 loadPhase5open 打开文件并调用loadPhase6,内部通过 instantiateFromFile 实例化 image,最后再调用 checkandAddImage 将image 加载进内存。

这些 phase 的搜索路径对应各个环境变量:DYLD_ROOT_PATH->LD_LIBRARY_PATH->DYLD_FRAMEWORK_PATH->原始路径->DYLD_FALLBACK_LIBRARY_PATH。

ImageLoaderMachO 的 instantiateFromFileinstantiateFromCache 是 loader 将 MachO 文件解析映射到内存的核心方法,两个都会进入 Compressed 和 Classic 的分叉步骤。以 Compressed 下的 instantiateFromFile 来分析。

  1. // create image by mapping in a mach-o file
  2. ImageLoaderMachOCompressed* ImageLoaderMachOCompressed::instantiateFromFile(const char* path, int fd, const uint8_t* fileData, size_t lenFileData,
  3. uint64_t offsetInFat, uint64_t lenInFat, const struct stat& info,
  4. unsigned int segCount, unsigned int libCount,
  5. const struct linkedit_data_command* codeSigCmd,
  6. const struct encryption_info_command* encryptCmd,
  7. const LinkContext& context)
  8. {
  9. ImageLoaderMachOCompressed* image = ImageLoaderMachOCompressed::instantiateStart((macho_header*)fileData, path, segCount, libCount);
  10. try {
  11. // record info about file
  12. image->setFileInfo(info.st_dev, info.st_ino, info.st_mtime);
  13. // if this image is code signed, let kernel validate signature before mapping any pages from image
  14. // ①、交给内核去验证动态库的代码签名
  15. image->loadCodeSignature(codeSigCmd, fd, offsetInFat, context);
  16. // Validate that first data we read with pread actually matches with code signature
  17. // ②、映射到内存的 first page, (4k大小)与代码签名是否match。在这里会执行沙盒,签名认证
  18. image->validateFirstPages(codeSigCmd, fd, fileData, lenFileData, offsetInFat, context);
  19. // mmap segments
  20. image->mapSegments(fd, offsetInFat, lenInFat, info.st_size, context);
  21. // if framework is FairPlay encrypted, register with kernel
  22. // 根据 DYLD_ENCRYPTION_INFO,让内核去注册加密信息。在该方法中,会调用内核方法 mremap_encrypted,传入加密数据的地址和长度等数据,查看了内核代码,应该是根据cryptid是否为1做了解密操作。
  23. image->registerEncryption(encryptCmd, context);
  24. // probe to see if code signed correctly
  25. image->crashIfInvalidCodeSignature();
  26. // finish construction
  27. image->instantiateFinish(context);
  28. ...
  29. }
  30. }

其中几个需要留意的步骤:

  • 交给内核去验证动态库的代码签名 loadCodeSignature。
  • 映射到内存的 first page(4k 大小)与代码签名是否 match。在这里会执行沙盒,签名认证,对于在线上运行时加载动态库的需求,可以重点研究这里
  • 根据 DYLD_ENCRYPTION_INFO,让内核去注册加密信息 registerEncryption。在该方法中,会调用内核方法 mremap_encrypted,传入加密数据的地址和长度等数据,查看了内核代码,应该是根据 cryptid 是否为 1 做了解密操作。
  • 如果走到 Phase6, 会调 xmap 函数将动态库从本地 mmap 到用户态内存空间。

根据上面的分析,主程序 imageLoader 在全局 image 表的首位,后面的是插入的动态库的 imageLoader,每个动态库对应一个 loader。

3.4.5 链接主程序

链接所有动态库,进行符号修正绑定工作。

  1. uintptr_t
  2. _main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
  3. int argc, const char* argv[], const char* envp[], const char* apple[],
  4. uintptr_t* startGlue)
  5. {
  6. ...
  7. // link main executable
  8. gLinkContext.linkingMainExecutable = true;
  9. #if SUPPORT_ACCELERATE_TABLES
  10. if ( mainExcutableAlreadyRebased ) {
  11. // previous link() on main executable has already adjusted its internal pointers for ASLR
  12. // work around that by rebasing by inverse amount
  13. sMainExecutable->rebase(gLinkContext, -mainExecutableSlide);
  14. }
  15. #endif
  16. // 链接主程序
  17. link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
  18. sMainExecutable->setNeverUnloadRecursive();
  19. if ( sMainExecutable->forceFlat() ) {
  20. gLinkContext.bindFlat = true;
  21. gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
  22. }
  23. ...
  24. }

可以看到,主程序的链接是通过 link 这个函数完成的:

  1. void link(ImageLoader* image, bool forceLazysBound, bool neverUnload, const ImageLoader::RPathChain& loaderRPaths, unsigned cacheIndex)
  2. {
  3. // add to list of known images. This did not happen at creation time for bundles
  4. // 添加到已知镜像列表中。这在创建 bundles 时没有处理。
  5. if ( image->isBundle() && !image->isLinked() )
  6. addImage(image);
  7. // we detect root images as those not linked in yet
  8. // 在根镜像中检测是否尚未链接
  9. if ( !image->isLinked() )
  10. addRootImage(image);
  11. // process images
  12. try {
  13. const char* path = image->getPath();
  14. #if SUPPORT_ACCELERATE_TABLES
  15. if ( image == sAllCacheImagesProxy )
  16. path = sAllCacheImagesProxy->getIndexedPath(cacheIndex);
  17. #endif
  18. // 调用 ImageLoader::link() 链接
  19. image->link(gLinkContext, forceLazysBound, false, neverUnload, loaderRPaths, path);
  20. }
  21. catch (const char* msg) {
  22. // 标记 image 为未使用,处理
  23. garbageCollectImages();
  24. throw;
  25. }
  26. }
  27. void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
  28. {
  29. //dyld::log("ImageLoader::link(%s) refCount=%d, neverUnload=%d\n", imagePath, fDlopenReferenceCount, fNeverUnload);
  30. // clear error strings
  31. (*context.setErrorStrings)(0, NULL, NULL, NULL);
  32. // 起始时间。用于记录时间间隔
  33. uint64_t t0 = mach_absolute_time();
  34. // ①、递归加载主程序依赖的库,完成之后发送一个状态为 dyld_image_state_dependents_mapped的通知。
  35. this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
  36. context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);
  37. // we only do the loading step for preflights 只做预检的装载步骤
  38. if ( preflightOnly )
  39. return;
  40. uint64_t t1 = mach_absolute_time();
  41. // 清空 image 层级关系
  42. context.clearAllDepths();
  43. // 递归更新 image 层级关系
  44. this->recursiveUpdateDepth(context.imageCount());
  45. __block uint64_t t2, t3, t4, t5;
  46. {
  47. dyld3::ScopedTimer(DBG_DYLD_TIMING_APPLY_FIXUPS, 0, 0, 0);
  48. t2 = mach_absolute_time();
  49. // ②、递归修正自己和依赖库的基地址,因为 ASLR 的原因,需要根据随机 slide 修正基地址。
  50. this->recursiveRebase(context);
  51. context.notifyBatch(dyld_image_state_rebased, false);
  52. t3 = mach_absolute_time();
  53. if ( !context.linkingMainExecutable )
  54. // ③、递归绑定 noLazy 的符号表,lazy的符号会在运行时动态绑定(首次被调用才去绑定)
  55. this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);
  56. t4 = mach_absolute_time();
  57. if ( !context.linkingMainExecutable )
  58. // ④、绑定弱符号表,比如未初始化的全局变量就是弱符号。
  59. this->weakBind(context);
  60. t5 = mach_absolute_time();
  61. }
  62. if ( !context.linkingMainExecutable )
  63. context.notifyBatch(dyld_image_state_bound, false);
  64. uint64_t t6 = mach_absolute_time();
  65. std::vector<DOFInfo> dofs;
  66. // ⑤、递归获取/注册程序的 DOF 节区,dtrace 会用其动态跟踪。
  67. this->recursiveGetDOFSections(context, dofs);
  68. // 注册
  69. context.registerDOFs(dofs);
  70. uint64_t t7 = mach_absolute_time();
  71. // interpose any dynamically loaded images
  72. if ( !context.linkingMainExecutable && (fgInterposingTuples.size() != 0) ) {
  73. dyld3::ScopedTimer timer(DBG_DYLD_TIMING_APPLY_INTERPOSING, 0, 0, 0);
  74. // 递归应用插入的动态库
  75. this->recursiveApplyInterposing(context);
  76. }
  77. // clear error strings
  78. (*context.setErrorStrings)(0, NULL, NULL, NULL);
  79. // 计算出各种时间间隔
  80. fgTotalLoadLibrariesTime += t1 - t0;
  81. fgTotalRebaseTime += t3 - t2;
  82. fgTotalBindTime += t4 - t3;
  83. fgTotalWeakBindTime += t5 - t4;
  84. fgTotalDOF += t7 - t6;
  85. // done with initial dylib loads
  86. fgNextPIEDylibAddress = 0;
  87. }

内部加载动态库、rebase、绑定符号表、注册dofs信息等,同时还计算各步骤的耗时。如果想获取这些耗时,只需要在环境变量中添加 DYLD_PRINT_STATISTICS 就可以了,这个环境变量不需要 value。

在步骤 ① 里,递归加载主 App 在打包阶段就确定好的动态库的操作,会使用前面提到的 setContext 里的链接上下文,调用它的 loadLibrary 方法;然后优先去加载依赖的动态库。loadLibary 的实现在设置链接上下文的时候就已经赋值确定,即 libraryLocator,在这个方法里会用到上面提到的 load 方法。

在步骤 ③ 里,会有符号绑定的操作。

  1. /**
  2. * @brief recursiveBind 完成递归绑定符号表的操作。此处的符号表针对的是非延迟加载的符号表,对于 DYLD_BIND_AT_LAUNCH 等特殊情况下的 non-lazy 符号才执行立即绑定。
  3. */
  4. void ImageLoader::recursiveBind(const LinkContext& context, bool forceLazysBound, bool neverUnload)
  5. {
  6. // Normally just non-lazy pointers are bound immediately.
  7. // The exceptions are:
  8. // 1) DYLD_BIND_AT_LAUNCH will cause lazy pointers to be bound immediately
  9. // 2) some API's (e.g. RTLD_NOW) can cause lazy pointers to be bound immediately
  10. if ( fState < dyld_image_state_bound ) {
  11. // break cycles
  12. fState = dyld_image_state_bound;
  13. try {
  14. // bind lower level libraries first
  15. for(unsigned int i=0; i < libraryCount(); ++i) {
  16. ImageLoader* dependentImage = libImage(i);
  17. if ( dependentImage != NULL )
  18. dependentImage->recursiveBind(context, forceLazysBound, neverUnload);
  19. }
  20. // bind this image
  21. // 绑定。this 表示递归调用时,recursiveBind 方法的调用者
  22. this->doBind(context, forceLazysBound);
  23. // mark if lazys are also bound
  24. if ( forceLazysBound || this->usablePrebinding(context) )
  25. fAllLazyPointersBound = true;
  26. // mark as never-unload if requested
  27. if ( neverUnload )
  28. this->setNeverUnload();
  29. // 通知
  30. context.notifySingle(dyld_image_state_bound, this, NULL);
  31. }
  32. catch (const char* msg) {
  33. // restore state
  34. fState = dyld_image_state_rebased;
  35. CRSetCrashLogMessage2(NULL);
  36. throw;
  37. }
  38. }
  39. }

方法的核心是 ImageLoaderMach 的 doBind,读取 image 的动态链接信息的 bind_off 与 bind_size 来确定需要绑定的数据偏移与大小,然后挨个对它们进行绑定,绑定操作具体使用 bindAt 函数;调用 resolve 解析完符号表后,调用 bindLocation 完成最终的绑定操作,需要绑定的符号信息有三种:

  • BIND_TYPE_POINTER:需要绑定的是一个指针。直接将计算好的新值屿值即可。
  • BIND_TYPE_TEXT_ABSOLUTE32:一个32位的值。取计算的值的低32位赋值过去。
  • BIND_TYPE_TEXT_PCREL32:重定位符号。需要使用新值减掉需要修正的地址值来计算出重定位值。

对延迟绑定的实现感兴趣的可以在Xcode中调试查看,或者参考这个

3.4.6 链接插入的动态库
  1. uintptr_t
  2. _main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
  3. int argc, const char* argv[], const char* envp[], const char* apple[],
  4. uintptr_t* startGlue)
  5. {
  6. ...
  7. // link any inserted libraries
  8. // do this after linking main executable so that any dylibs pulled in by inserted
  9. // dylibs (e.g. libSystem) will not be in front of dylibs the program uses
  10. // 链接其他被插入的动态库
  11. if ( sInsertedDylibCount > 0 ) {
  12. // 循环处理
  13. for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
  14. ImageLoader* image = sAllImages[i+1];
  15. // 链接
  16. link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
  17. // 递归修改 image 的 fNeverUnload 属性
  18. image->setNeverUnloadRecursive();
  19. }
  20. // only INSERTED libraries can interpose
  21. // register interposing info after all inserted libraries are bound so chaining works
  22. // 只有插入可插入的库。在绑定所有插入的库后注册插入信息,以便链接工作
  23. for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
  24. ImageLoader* image = sAllImages[i+1];
  25. image->registerInterposing(gLinkContext);
  26. }
  27. }
  28. // <rdar://problem/19315404> dyld should support interposition even without DYLD_INSERT_LIBRARIES
  29. // 即使没有 DYLD_INSERT_LIBRARIES,dyld 也应该支持插入
  30. for (long i=sInsertedDylibCount+1; i < sAllImages.size(); ++i) {
  31. ImageLoader* image = sAllImages[i];
  32. if ( image->inSharedCache() )
  33. continue;
  34. image->registerInterposing(gLinkContext);
  35. }
  36. #if SUPPORT_ACCELERATE_TABLES // !TARGET_IPHONE_SIMULATOR,非模拟器
  37. if ( (sAllCacheImagesProxy != NULL) && ImageLoader::haveInterposingTuples() ) {
  38. // Accelerator tables cannot be used with implicit interposing, so relaunch with accelerator tables disabled
  39. // 加速键表不能与隐式插入一起使用,因此在禁用加速键表的情况下重新启动
  40. ImageLoader::clearInterposingTuples();
  41. // unmap all loaded dylibs (but not main executable)
  42. // 取消映射所有加载的 dylib 文件,除了主程序
  43. for (long i=1; i < sAllImages.size(); ++i) {
  44. ImageLoader* image = sAllImages[i];
  45. if ( image == sMainExecutable )
  46. continue;
  47. if ( image == sAllCacheImagesProxy )
  48. continue;
  49. image->setCanUnload();
  50. ImageLoader::deleteImage(image);
  51. }
  52. // note: we don't need to worry about inserted images because if DYLD_INSERT_LIBRARIES was set we would not be using the accelerator table
  53. sAllImages.clear();
  54. sImageRoots.clear();
  55. sImageFilesNeedingTermination.clear();
  56. sImageFilesNeedingDOFUnregistration.clear();
  57. sAddImageCallbacks.clear();
  58. sRemoveImageCallbacks.clear();
  59. sAddLoadImageCallbacks.clear();
  60. sDisableAcceleratorTables = true;
  61. sAllCacheImagesProxy = NULL; // 下次不再进入
  62. sMappedRangesStart = NULL;
  63. mainExcutableAlreadyRebased = true;
  64. gLinkContext.linkingMainExecutable = false;
  65. resetAllImages();
  66. // 跳转回上面的步骤,重新执行,加载所有的镜像
  67. goto reloadAllImages;
  68. }
  69. #endif
  70. // apply interposing to initial set of images
  71. for(int i=0; i < sImageRoots.size(); ++i) {
  72. // 是调用 ImageLoader::applyInterposing(),不是 ClosureWriter.cpp。内部递归,最终是执行 doInterpose() 方法
  73. sImageRoots[i]->applyInterposing(gLinkContext);
  74. }
  75. // 插入信息存入 dyld 缓存
  76. ImageLoader::applyInterposingToDyldCache(gLinkContext);
  77. // 修改主程序插入标识
  78. gLinkContext.linkingMainExecutable = false;
  79. // Bind and notify for the main executable now that interposing has been registered
  80. // 从主程序开始调用,递归执行绑定、通知(插入信息已经注册)
  81. uint64_t bindMainExecutableStartTime = mach_absolute_time();
  82. // 内部执行 doBind()、notifySingle()
  83. sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
  84. uint64_t bindMainExecutableEndTime = mach_absolute_time();
  85. // 绑定和通知处理时间
  86. ImageLoaderMachO::fgTotalBindTime += bindMainExecutableEndTime - bindMainExecutableStartTime;
  87. gLinkContext.notifyBatch(dyld_image_state_bound, false);
  88. // Bind and notify for the inserted images now interposing has been registered
  89. if ( sInsertedDylibCount > 0 ) {
  90. for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
  91. ImageLoader* image = sAllImages[i+1];
  92. // 绑定插入的动态库
  93. image->recursiveBind(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
  94. }
  95. }
  96. ...
  97. }

这里参与链接的动态库根据第 4 步中加载的插入的动态库,从 sAllImages 的第二个 imageLoader 开始,取出 image,重复 link 操作进行连接。registerInterposing 内部会加载 loadCmds 并查找 __interpose 及 __DATA 段,读取段信息保存到 fgInterposingTuples 中,然后调用 applyInterposing,内部调用 recursiveApplyInterposing,通过这个函数调用到 doInterpose。

  1. void ImageLoaderMachOCompressed::doInterpose(const LinkContext& context)
  2. {
  3. if ( context.verboseInterposing )
  4. dyld::log("dyld: interposing %lu tuples onto image: %s\n", fgInterposingTuples.size(), this->getPath());
  5. // update prebound symbols。更新预绑定的符号
  6. eachBind(context, ^(const LinkContext& ctx, ImageLoaderMachOCompressed* image,
  7. uintptr_t addr, uint8_t type, const char* symbolName,
  8. uint8_t symbolFlags, intptr_t addend, long libraryOrdinal,
  9. ExtraBindData *extraBindData,
  10. const char* msg, LastLookup* last, bool runResolver) {
  11. // 直接调用 interposeAt()
  12. return ImageLoaderMachOCompressed::interposeAt(ctx, image, addr, type, symbolName, symbolFlags,
  13. addend, libraryOrdinal, extraBindData,
  14. msg, last, runResolver);
  15. });
  16. eachLazyBind(context, ^(const LinkContext& ctx, ImageLoaderMachOCompressed* image,
  17. uintptr_t addr, uint8_t type, const char* symbolName,
  18. uint8_t symbolFlags, intptr_t addend, long libraryOrdinal,
  19. ExtraBindData *extraBindData,
  20. const char* msg, LastLookup* last, bool runResolver) {
  21. // 直接调用 interposeAt()
  22. return ImageLoaderMachOCompressed::interposeAt(ctx, image, addr, type, symbolName, symbolFlags,
  23. addend, libraryOrdinal, extraBindData,
  24. msg, last, runResolver);
  25. });
  26. }

interposeAt 通过 interposedAddress 在上文提到的 fgInterposingTuples 中找到需要替换的符号地址进行替换。

3.4.7 弱符号绑定
  1. uintptr_t
  2. _main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
  3. int argc, const char* argv[], const char* envp[], const char* apple[],
  4. uintptr_t* startGlue)
  5. {
  6. ...
  7. // <rdar://problem/12186933> do weak binding only after all inserted images linked
  8. // 弱符号绑定
  9. sMainExecutable->weakBind(gLinkContext);
  10. ...
  11. }
  12. void ImageLoader::weakBind(const LinkContext& context)
  13. {
  14. if ( context.verboseWeakBind )
  15. dyld::log("dyld: weak bind start:\n");
  16. uint64_t t1 = mach_absolute_time();
  17. // get set of ImageLoaders that participate in coalecsing
  18. ImageLoader* imagesNeedingCoalescing[fgImagesRequiringCoalescing];
  19. unsigned imageIndexes[fgImagesRequiringCoalescing];
  20. // 合并所有动态库的弱符号到列表中
  21. int count = context.getCoalescedImages(imagesNeedingCoalescing, imageIndexes);
  22. // count how many have not already had weakbinding done
  23. int countNotYetWeakBound = 0;
  24. int countOfImagesWithWeakDefinitionsNotInSharedCache = 0;
  25. for(int i=0; i < count; ++i) {
  26. if ( ! imagesNeedingCoalescing[i]->weakSymbolsBound(imageIndexes[i]) )
  27. // 获取未进行绑定的弱符号的个数
  28. ++countNotYetWeakBound;
  29. if ( ! imagesNeedingCoalescing[i]->inSharedCache() )
  30. // 获取在共享缓存中已绑定的弱符号个数
  31. ++countOfImagesWithWeakDefinitionsNotInSharedCache;
  32. }
  33. // don't need to do any coalescing if only one image has overrides, or all have already been done
  34. if ( (countOfImagesWithWeakDefinitionsNotInSharedCache > 0) && (countNotYetWeakBound > 0) ) {
  35. // make symbol iterators for each
  36. ImageLoader::CoalIterator iterators[count];
  37. ImageLoader::CoalIterator* sortedIts[count];
  38. for(int i=0; i < count; ++i) {
  39. // 对需要绑定的弱符号排序
  40. imagesNeedingCoalescing[i]->initializeCoalIterator(iterators[i], i, imageIndexes[i]);
  41. sortedIts[i] = &iterators[i];
  42. if ( context.verboseWeakBind )
  43. dyld::log("dyld: weak bind load order %d/%d for %s\n", i, count, imagesNeedingCoalescing[i]->getIndexedPath(imageIndexes[i]));
  44. }
  45. // walk all symbols keeping iterators in sync by
  46. // only ever incrementing the iterator with the lowest symbol
  47. int doneCount = 0;
  48. while ( doneCount != count ) {
  49. //for(int i=0; i < count; ++i)
  50. // dyld::log("sym[%d]=%s ", sortedIts[i]->loadOrder, sortedIts[i]->symbolName);
  51. //dyld::log("\n");
  52. // increment iterator with lowest symbol
  53. // 计算弱符号偏移量及大小,绑定弱符号
  54. if ( sortedIts[0]->image->incrementCoalIterator(*sortedIts[0]) )
  55. ++doneCount;
  56. ...
  57. }
  58. }

主要流程:合并所有动态库的弱符号到列表中 -> 对需要绑定的弱符号排序 -> 计算弱符号偏移量及大小,绑定弱符号

3.4.8 初始化主程序
  1. uintptr_t
  2. _main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
  3. int argc, const char* argv[], const char* envp[], const char* apple[],
  4. uintptr_t* startGlue)
  5. {
  6. ...
  7. CRSetCrashLogMessage("dyld: launch, running initializers");
  8. #if SUPPORT_OLD_CRT_INITIALIZATION
  9. // Old way is to run initializers via a callback from crt1.o
  10. if ( ! gRunInitializersOldWay )
  11. // 初始化主程序
  12. initializeMainExecutable();
  13. #else
  14. // run all initializers
  15. // 初始化主程序
  16. initializeMainExecutable();
  17. #endif
  18. // notify any montoring proccesses that this process is about to enter main()
  19. // 通知任何监视进程,此进程将要进入main()。
  20. if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
  21. dyld3::kdebug_trace_dyld_duration_end(launchTraceID, DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, 0, 0, 2);
  22. }
  23. notifyMonitoringDyldMain();
  24. ...
  25. }
  26. void initializeMainExecutable()
  27. {
  28. // record that we've reached this step。开始初始化标识
  29. gLinkContext.startedInitializingMainExecutable = true;
  30. // run initialzers for any inserted dylibs
  31. ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
  32. initializerTimes[0].count = 0;
  33. const size_t rootCount = sImageRoots.size();
  34. if ( rootCount > 1 ) {
  35. for(size_t i=1; i < rootCount; ++i) {
  36. // 初始化动态库
  37. sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
  38. }
  39. }
  40. // run initializers for main executable and everything it brings up
  41. // 初始化主程序
  42. sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
  43. // register cxa_atexit() handler to run static terminators in all loaded images when this process exits
  44. if ( gLibSystemHelpers != NULL )
  45. (*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
  46. // dump info if requested
  47. if ( sEnv.DYLD_PRINT_STATISTICS )
  48. ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
  49. if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
  50. ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
  51. }

先初始化动态库,然后初始化主程序。上文提到的 DYLD_PRINT_STATISTICS 环境变量在这里也出现了,除此之外还有个 detail 版的环境变量 DYLD_PRINT_STATISTICS_DETAILS。

  1. void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
  2. {
  3. uint64_t t1 = mach_absolute_time();
  4. // 获取线程
  5. mach_port_t thisThread = mach_thread_self();
  6. ImageLoader::UninitedUpwards up;
  7. up.count = 1;
  8. up.images[0] = this;
  9. // 在进程中初始化
  10. processInitializers(context, thisThread, timingInfo, up);
  11. context.notifyBatch(dyld_image_state_initialized, false);
  12. mach_port_deallocate(mach_task_self(), thisThread);
  13. uint64_t t2 = mach_absolute_time();
  14. // 初始化耗时
  15. fgTotalInitTime += (t2 - t1);
  16. }
  17. // <rdar://problem/14412057> upward dylib initializers can be run too soon
  18. // To handle dangling dylibs which are upward linked but not downward, all upward linked dylibs
  19. // have their initialization postponed until after the recursion through downward dylibs
  20. // has completed.
  21. void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
  22. InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
  23. {
  24. uint32_t maxImageCount = context.imageCount()+2;
  25. ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
  26. ImageLoader::UninitedUpwards& ups = upsBuffer[0];
  27. ups.count = 0;
  28. // Calling recursive init on all images in images list, building a new list of
  29. // uninitialized upward dependencies.
  30. for (uintptr_t i=0; i < images.count; ++i) {
  31. images.images[i]->recursiveInitialization(context, thisThread, images.images[i]->getPath(), timingInfo, ups);
  32. }
  33. // If any upward dependencies remain, init them.
  34. if ( ups.count > 0 )
  35. // 递归调用
  36. processInitializers(context, thisThread, timingInfo, ups);
  37. }

动态库和主程序的初始化是调用 runInitializers,内部通过 processInitializers 调用 recursiveInitialization 递归初始化当前 image 所依赖的库。

  1. void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
  2. InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
  3. {
  4. // 递归锁
  5. recursive_lock lock_info(this_thread);
  6. recursiveSpinLock(lock_info);
  7. if ( fState < dyld_image_state_dependents_initialized-1 ) {
  8. uint8_t oldState = fState;
  9. // break cycles
  10. // 退出递归循环
  11. fState = dyld_image_state_dependents_initialized-1;
  12. try {
  13. // initialize lower level libraries first
  14. // 先初始化低级别的库
  15. for(unsigned int i=0; i < libraryCount(); ++i) {
  16. ImageLoader* dependentImage = libImage(i);
  17. if ( dependentImage != NULL ) {
  18. // don't try to initialize stuff "above" me yet
  19. // 不要试图初始化级别高于我的
  20. if ( libIsUpward(i) ) {
  21. uninitUps.images[uninitUps.count] = dependentImage;
  22. uninitUps.count++;
  23. }
  24. else if ( dependentImage->fDepth >= fDepth ) {
  25. dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
  26. }
  27. }
  28. }
  29. // record termination order. 记录终止命令
  30. if ( this->needsTermination() )
  31. context.terminationRecorder(this);
  32. // let objc know we are about to initialize this image
  33. uint64_t t1 = mach_absolute_time();
  34. fState = dyld_image_state_dependents_initialized;
  35. oldState = fState;
  36. //
  37. context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
  38. // initialize this image
  39. // 真正初始化镜像
  40. bool hasInitializers = this->doInitialization(context);
  41. // let anyone know we finished initializing this image
  42. fState = dyld_image_state_initialized;
  43. oldState = fState;
  44. context.notifySingle(dyld_image_state_initialized, this, NULL);
  45. if ( hasInitializers ) {
  46. uint64_t t2 = mach_absolute_time();
  47. timingInfo.addTime(this->getShortName(), t2-t1);
  48. }
  49. }
  50. catch (const char* msg) {
  51. // this image is not initialized
  52. fState = oldState;
  53. recursiveSpinUnLock();
  54. throw;
  55. }
  56. }
  57. recursiveSpinUnLock();
  58. }

注意内部有个调用 context.notifySingle(dyld_image_state_initialized, this, NULL),其实每次 image 状态改变都会调用 notifySingle 这个方法:

  1. static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
  2. {
  3. //dyld::log("notifySingle(state=%d, image=%s)\n", state, image->getPath());
  4. std::vector<dyld_image_state_change_handler>* handlers = stateToHandlers(state, sSingleHandlers);
  5. if ( handlers != NULL ) {
  6. dyld_image_info info;
  7. info.imageLoadAddress = image->machHeader();
  8. info.imageFilePath = image->getRealPath();
  9. info.imageFileModDate = image->lastModified();
  10. for (std::vector<dyld_image_state_change_handler>::iterator it = handlers->begin(); it != handlers->end(); ++it) {
  11. const char* result = (*it)(state, 1, &info);
  12. if ( (result != NULL) && (state == dyld_image_state_mapped) ) {
  13. //fprintf(stderr, " image rejected by handler=%p\n", *it);
  14. // make copy of thrown string so that later catch clauses can free it
  15. const char* str = strdup(result);
  16. throw str;
  17. }
  18. }
  19. }
  20. if ( state == dyld_image_state_mapped ) {
  21. // <rdar://problem/7008875> Save load addr + UUID for images from outside the shared cache
  22. if ( !image->inSharedCache() ) {
  23. dyld_uuid_info info;
  24. if ( image->getUUID(info.imageUUID) ) {
  25. info.imageLoadAddress = image->machHeader();
  26. addNonSharedCacheImageUUID(info);
  27. }
  28. }
  29. }
  30. if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
  31. uint64_t t0 = mach_absolute_time();
  32. dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
  33. (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
  34. uint64_t t1 = mach_absolute_time();
  35. uint64_t t2 = mach_absolute_time();
  36. uint64_t timeInObjC = t1-t0;
  37. uint64_t emptyTime = (t2-t1)*100;
  38. if ( (timeInObjC > emptyTime) && (timingInfo != NULL) ) {
  39. timingInfo->addTime(image->getShortName(), timeInObjC);
  40. }
  41. }
  42. // mach message csdlc about dynamically unloaded images
  43. if ( image->addFuncNotified() && (state == dyld_image_state_terminated) ) {
  44. notifyKernel(*image, false);
  45. const struct mach_header* loadAddress[] = { image->machHeader() };
  46. const char* loadPath[] = { image->getPath() };
  47. notifyMonitoringDyld(true, 1, loadAddress, loadPath);
  48. }
  49. }

当 state == dyld_image_state_mapped 时,将 image 对应的 UUID 存起来,当state == dyld_image_state_dependents_initialized 并且有 sNotifyObjCInit 回调时调用sNotifyObjCInit函数。

搜索回调函数赋值入口:

  1. void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
  2. {
  3. // record functions to call
  4. sNotifyObjCMapped = mapped;
  5. sNotifyObjCInit = init;
  6. sNotifyObjCUnmapped = unmapped;
  7. ...
  8. }
  9. void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
  10. _dyld_objc_notify_init init,
  11. _dyld_objc_notify_unmapped unmapped)
  12. {
  13. dyld::registerObjCNotifiers(mapped, init, unmapped);
  14. }

发现是通过 _dyld_objc_notify_register 这个函数注册回调的。

[NSObject load] 的堆栈:

  1. * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.2
  2. * frame #0: 0x000000010944f3b1 libobjc.A.dylib`+[NSObject load]
  3. frame #1: 0x000000010943d317 libobjc.A.dylib`call_load_methods + 691
  4. frame #2: 0x000000010943e814 libobjc.A.dylib`load_images + 77
  5. frame #3: 0x0000000108b73b97 dyld_sim`dyld::registerObjCNotifiers(void (*)(unsigned int, char const* const*, mach_header const* const*), void (*)(char const*, mach_header const*), void (*)(char const*, mach_header const*)) + 260
  6. frame #4: 0x000000010b779bf3 libdyld.dylib`_dyld_objc_notify_register + 113
  7. frame #5: 0x000000010944ca12 libobjc.A.dylib`_objc_init + 115
  8. frame #6: 0x000000010b7015c0 libdispatch.dylib`_os_object_init + 13
  9. frame #7: 0x000000010b70f4e5 libdispatch.dylib`libdispatch_init + 300
  10. frame #8: 0x0000000109e05a78 libSystem.B.dylib`libSystem_initializer + 164
  11. frame #9: 0x0000000108b82b96 dyld_sim`ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 506
  12. frame #10: 0x0000000108b82d9c dyld_sim`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 40
  13. frame #11: 0x0000000108b7e3fc dyld_sim`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 324
  14. frame #12: 0x0000000108b7e392 dyld_sim`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 218
  15. frame #13: 0x0000000108b7d5d3 dyld_sim`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 133
  16. frame #14: 0x0000000108b7d665 dyld_sim`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 73
  17. frame #15: 0x0000000108b71333 dyld_sim`dyld::initializeMainExecutable() + 129
  18. frame #16: 0x0000000108b75434 dyld_sim`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 4384
  19. frame #17: 0x0000000108b70630 dyld_sim`start_sim + 136
  20. frame #18: 0x00000001155c1234 dyld`dyld::useSimulatorDyld(int, macho_header const*, char const*, int, char const**, char const**, char const**, unsigned long*, unsigned long*) + 2238
  21. frame #19: 0x00000001155bf0ce dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 522
  22. frame #20: 0x00000001155ba503 dyld`dyldbootstrap::start(macho_header const*, int, char const**, long, macho_header const*, unsigned long*) + 1167
  23. frame #21: 0x00000001155ba036 dyld`_dyld_start + 54

可以看到,_dyld_objc_notify_register 是在初始化 libobjc.A.dylib 这个动态库时调用的,然后 _objc_init 内部调用了 load_images,进而调用 call_load_methods,从而调用各个类中的load方法,Objc源码

notifySingle 调用完毕后,开始真正初始化工作 doInitialization

  1. bool ImageLoaderMachO::doInitialization(const LinkContext& context)
  2. {
  3. CRSetCrashLogMessage2(this->getPath());
  4. // mach-o has -init and static initializers
  5. doImageInit(context);
  6. doModInitFunctions(context);
  7. CRSetCrashLogMessage2(NULL);
  8. return (fHasDashInit || fHasInitializers);
  9. }

doImageInit 执行 LC_ROUTINES_COMMAND segment 中保存的函数,doModInitFunctions执行 __DATA,__mod_init_func section 中保存的函数。这个 section 中保存的是 C++ 的构造函数及带有 attribute((constructor)) 的 C 函数,简单验证一下:

  1. // ViewController.mm
  2. class Test {
  3. public:
  4. Test();
  5. };
  6. Test::Test(){
  7. NSLog(@"%s", __func__);
  8. }
  9. Test test;
  10. __attribute__((constructor)) void testConstructor() {
  11. NSLog(@"%s", __func__);
  12. }
  13. - (void)viewDidLoad
  14. {
  15. [super viewDidLoad];
  16. testConstructor();
  17. Test * t = new Test();
  18. }
  19. 2019-08-19 13:26:33.587051+0800 Demo[7105:314102] testConstructor
  20. 2019-08-19 13:26:33.587109+0800 Demo[7105:314102] Test

通过 MachOView 可以看到:

显然,__mod_init_func 中的函数在类对应的 load 方法之后调用。

  1. 对于 dumpdcrypted 这一类注入方法实现功能的插件,他们添加的静态方法会在 doModInitFunctions方法中被解析出来,位置在 MachO 文件的 __DATA 段的 __mod_init_func section。C++ 的全局对象也会出现在这个section中。
  2. 在递归初始化 (recursiveInitialization)中,如果当前执行的是主程序 image,doInitialization 完毕后会执行 notifySingle 方法去通知观察者。在 doInitialization 方法前会发送 state 为 dyld_image_state_dependents_initialized 的通知,由这个通知,会调用 libobjc 的 load_images,最后去依次调用各个 OC 类的 load 方法以及分类的 load 方法。
  3. Objective-C 的入口方法是 _objc_init,dyld 唤起它的执行路径是从 runInitializers -> recursiveInitialization -> doInitialization -> doModInitFunctions ->.. _objc_init。

    1. void _objc_init(void)
    2. {
    3. // Register for unmap first, in case some +load unmaps something
    4. _dyld_register_func_for_remove_image(&unmap_image);
    5. dyld_register_image_state_change_handler(dyld_image_state_bound,
    6. 1/*batch*/, &map_2_images);
    7. dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
    8. }
  4. _objc_init 会在 dyld 中注册两个通知,对应的回调会分别执行将 OC 类加载到内存和调用 load 方法的操作。后面的就是 OC 类加载的经典方法 map_2_images 了。

  5. 从 recursiveInitialization 的以下代码片段可以看出 load 是在全局实例或者方法调用前被触发的。

    1. context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
    2. // initialize this image
    3. bool hasInitializers = this->doInitialization(context);
    4. // let anyone know we finished initializing this image
    5. fState = dyld_image_state_initialized;
    6. oldState = fState;
    7. context.notifySingle(dyld_image_state_initialized, this, NULL);
3.4.9 查找主程序入口函数指针并返回

调用getEntryFromLC_MAIN 获取主程序 main 函数的地址,如果未找到则调用getEntryFromLC_UNIXTHREAD 获取。

  1. void* ImageLoaderMachO::getEntryFromLC_MAIN() const
  2. {
  3. const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
  4. const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
  5. const struct load_command* cmd = cmds;
  6. for (uint32_t i = 0; i < cmd_count; ++i) {
  7. if ( cmd->cmd == LC_MAIN ) {
  8. entry_point_command* mainCmd = (entry_point_command*)cmd;
  9. void* entry = (void*)(mainCmd->entryoff + (char*)fMachOData);
  10. // <rdar://problem/8543820&9228031> verify entry point is in image
  11. if ( this->containsAddress(entry) )
  12. return entry;
  13. else
  14. throw "LC_MAIN entryoff is out of range";
  15. }
  16. cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
  17. }
  18. return NULL;
  19. }
  20. void* ImageLoaderMachO::getEntryFromLC_UNIXTHREAD() const
  21. {
  22. const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
  23. const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
  24. const struct load_command* cmd = cmds;
  25. for (uint32_t i = 0; i < cmd_count; ++i) {
  26. if ( cmd->cmd == LC_UNIXTHREAD ) {
  27. #if __i386__
  28. const i386_thread_state_t* registers = (i386_thread_state_t*)(((char*)cmd) + 16);
  29. void* entry = (void*)(registers->eip + fSlide);
  30. // <rdar://problem/8543820&9228031> verify entry point is in image
  31. if ( this->containsAddress(entry) )
  32. return entry;
  33. #elif __x86_64__
  34. const x86_thread_state64_t* registers = (x86_thread_state64_t*)(((char*)cmd) + 16);
  35. void* entry = (void*)(registers->rip + fSlide);
  36. // <rdar://problem/8543820&9228031> verify entry point is in image
  37. if ( this->containsAddress(entry) )
  38. return entry;
  39. #elif __arm64__ && !__arm64e__
  40. // temp support until <rdar://39514191> is fixed
  41. const uint64_t* regs64 = (uint64_t*)(((char*)cmd) + 16);
  42. void* entry = (void*)(regs64[32] + fSlide); // arm_thread_state64_t.__pc
  43. // <rdar://problem/8543820&9228031> verify entry point is in image
  44. if ( this->containsAddress(entry) )
  45. return entry;
  46. #endif
  47. }
  48. cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
  49. }
  50. throw "no valid entry point";
  51. }

可以看到,入口是在 load_command 的 LC_MAIN 或者 LC_UNIXTHREAD 中 LC_MAIN。

四、dyld 闭包

在第 2 步和第 3 步之间有一个查找闭包并以其结果作为程序入口返回的代码,这里是 WWDC 2017 推出的 dyld3 中提出的一种优化 App 启动速度的技术。大致步骤如下:

  1. 如果满足条件:开启闭包(DYLD_USE_CLOSURES 环境变量为 1),App 的路径在白名单中(目前只有系统 Ap p享有使用闭包的特权),共享缓存加载地址不为空,则往下执行。
  2. 去内存中查找闭包数据,这里的方法是 findClosure。如果内存中不存在,再去 /private/var/staged_system_apps 路径下去查找硬盘数据,找到就返回结果。
  3. 如果没有闭包数据,就会调用 socket 通信走 RPC 去获取闭包数据,执行方法为 callClosureDaemon,感兴趣可以研究下。
  4. 如果闭包数据不为空,就会走核心方法:launchWithClosure,基于闭包去启动 App,并且返回该方法中获取的程序入口地址给外界。这个方法重复了上面的各个步骤。具体实现和内部的数据结构有待分析。

五、共享缓存机制

在 iOS 系统中,每个程序依赖的动态库都需要通过 dyld 一个一个加载到内存,然而,很多系统库几乎是每个程序都会用到的,如果在每个程序运行的时候都重复的去加载一次,势必造成运行缓慢,为了优化启动速度和提高程序性能,共享缓存机制就应运而生。所有默认的动态链接库被合并成一个大的缓存文件,放到 /System/Library/Caches/com.apple.dyld/ 目录下,按不同的架构保存分别保存着,原作者的 iPhone6 里面就有 dyld_shared_cache_armv7s 和 dyld_shared_cache_armv64 两个文件,如下图所示。

想要分析某个系统库,就需要从 dyld_shared_cache 里先将的原始二进制文件提取出来,这里从易到难提供 3 种方法:

5.1 dyld_cache_extract 提取

dyld_cache_extract 是一个可视化的工具,使用极其简单,把 dyld_shared_cache 载入即可解析出来,如下图所示。

5.2 jtool 提取

以提取 CFNetwork 为例,使用如下命令即可:

  1. $ jtool -extract CFNetwork ./dyld_shared_cache_arm64

5.3 dsc_extractor 提取

在 dyld 源代码的 launch-cache 文件夹里面找到 dsc_extractor.cpp,将 653 行的“#if 0”修改为“#if 1”,然后用如下命令编译生成 dsc_extractor,并使用它提取所有缓存文件:

  1. $ clang++ dsc_extractor.cpp dsc_iterator.cpp -o dsc_extractor
  2. $ ./dsc_extractor ./dyld_shared_cache_arm64 ./

六、总结

每个 MachO 都会由一个 imageLoader 来处理加载和依赖管理的操作,这里是由 dyld 来安排。主程序 app 的 image 的加载是由内核来完成的。其他的动态库的加载细节可以参考上面提到的 link 方法实现,当一个 image 加载完毕,dyld 会发送 dyld_image_state_bound 通知;著名的 hook 工具 fishhook 的实现原理也是借助监听这个通知,在回调里完成 hook 操作的。

七、文章

01_Jack & dyld源码解读
伊织__ & Mac - otool
RemisKrlet & App启动过程 - dyld加载动态库
dyld详解

dyld的更多相关文章

  1. dyld 加载 Mach-O

    ➠更多技术干货请戳:听云博客 前言 最近看 ObjC的runtime 是怎么实现 +load 钩子函数的实现.进而引申分析了 dyld 处理 Mach-O 的这部分机制. 1.简单分析 Mach-O ...

  2. (TODO:)下载图片,报错:warning: could not load any Objective-C class information from the dyld shared cache. This will significantly reduce the quality of type information available.

    想使用NSInvocationOperation下载图片,然而并没有下载下来, NSData为nil, 还有报错:(打断点就报错) warning: could not load any Object ...

  3. CURL命令报错:dyld: lazy symbol binding failed: Symbol not found: _SSL_load_error_strings解决办法

    Mac OS X 10.11.6, curl 命令报错,错误如下: dyld: lazy symbol binding failed: Symbol not found: _SSL_load_erro ...

  4. php memcache扩展 出现错误dyld: Symbol not found: _mmc_queue_free

    mac 10.10 系统安装php memcache扩展 在使用memcache的时候出现错误dyld: Symbol not found: _mmc_queue_free需要重新编译memcache ...

  5. [issue] dyld`dyld_fatal_error: -> 0x120015088 <+0>: brk #0x3

    iOS "dyld`dyld_fatal_error: -> 0x12000d088 <+0>: brk #0x3"错误 根据上面的博客里的方法二 尝试解决方法二 ...

  6. dyld: Symbol not found: _OBJC_CLASS_$_NSURLSessionDataTask

    dyld: Symbol not found: _OBJC_CLASS_$_NSURLSessionDataTask   Referenced from: /var/mobile/Applicatio ...

  7. iOS中dyld缓存的实现原理是怎样的?

    在iOS开发中,为了提升系统的安全性,很多系统库文件都被打包到一个缓存的文件当中即dyld缓存,那大家对dyld缓存了解多少呢?今天小编将和大家分享的就是一位iOS大神对dyld缓存的使用分析,一起来 ...

  8. xcode解决问题dyld: Library not loaded

    一.问题 编译通过,联机调试时,应用启动闪退,XCODE的Output出现提示: dyld: Library not loaded: /System/Library/Frameworks/AdSupp ...

  9. 解决 dyld: Library not loaded:Reason: image not found

    在使用第三方framework时,直接把framework拖到项目中,运行时报错: dyld: Library not loaded: @rpath/ZipZap.framework/ZipZap R ...

  10. dyld binding test

    ========================================================================= a.c ---------------------- ...

随机推荐

  1. Dart 运行速度测评与比较

    引言 Dart 是一门优秀的跨平台语言,尽管生态方面略有欠缺,但无疑作为一门编程语言来说,Dart 是很优美,很健壮的,同时也引入了一些先进的编程范式,值得去学习. 测试内容 现在,我们就来测评一下D ...

  2. Java 在PDF中添加表格

    本文将介绍通过Java编程在PDF文档中添加表格的方法.添加表格时,可设置表格边框.单元格对齐方式.单元格背景色.单元格合并.插入图片.设置行高.列宽.字体.字号等. 使用工具:Free Spire. ...

  3. linux同步当前网络时间

    [root@root ~]# yum install -y ntpdate 执行:ntpdate[root@root ~]# ntpdate 120.24.81.91或[root@root ~]# n ...

  4. 学习Java技术哪家强

    https://github.com/CyC2018/CS-Notes https://github.com/Snailclimb/JavaGuide SpringBoot 之 配置文件优先级 htt ...

  5. linux yum安装MySQL5.6

    1.新开的云服务器,需要检测系统是否自带安装mysql # yum list installed | grep mysql 2.如果发现有系统自带mysql,果断这么干 # yum -y remove ...

  6. VUE二 生命周期详解

    vue官网对vue生命周期的介绍 Vue实例有一个完整的生命周期,也就是从开始创建.初始化数据.编译模板.挂载Dom.渲染→更新→渲染.销毁等一系列过程,我们称这是Vue的生命周期.通俗说就是Vue实 ...

  7. 18 JpaRepository和JpaSpecificationExecutor

    继承JpaRepository后的方法列表 JpaRepository findAll() List<T> findAll(Sort) List<T> findAll(Iter ...

  8. JSON Serialization/Deserialization in C#

    因为对C#不是特别熟悉,但是最近写个c#的demo,需要对获取的的json字符串进行解析,开始使用Newtonsoft.Json.Linq尝试了以下,但是感觉操作起来比较麻烦,尤其对与JSON结构比较 ...

  9. Java反射之Class类

    接下来的几章,我们谈一谈java的反射机制. 反射就是从一个java类中映射出一个java类或是一个实例.通常在很多框架中都用到反射,比如常用的ssm框架,在配置文件中总是会写到类的全名,框架通过读取 ...

  10. PDA程序开发的运行配置

    前言:因为这个项目是公司一直在做的项目,所以只是简单说一下我从下载项目到成功运行的配置 开发工具:APICloud.雷神模拟器.Visusl studio vs配置: 1.svn下载后台代码后,修改w ...