Mach-o动态链接

By xia0

0x00 序

可执行文件很少是独立的,除了极少数的一些静态链接的可执行文件以外,大多的都是动态链接,这就需要依赖一些预先存在的库,这些库可以是操作系统提供的动态共享库,也可以是第三方的库。
所以在可执行文件中充满了大量对外部库的符号的引用,这些空洞就需要动态链接器来完成所谓的符号绑定。
macOS中是内核执行LC_DYLINKER加载命令时启动的,通常为/usr/lib/dyld接管刚创建进程的控制权。
本文就来分析其符号的动态链接过程。

0x01 符号的加载过程

以下面这个简单的C语言程序为例。

void main(int argc, char *argv[]) {

    printf("first printf");
    printf("second printf");
}

对应的汇编代码

->  0x100000f40 <+0>:  pushq  %rbp
    0x100000f41 <+1>:  movq   %rsp, %rbp
    0x100000f44 <+4>:  subq   $0x20, %rsp
    0x100000f48 <+8>:  leaq   0x4b(%rip), %rax          ; "first printf"
    0x100000f4f <+15>: movl   %edi, -0x4(%rbp)
    0x100000f52 <+18>: movq   %rsi, -0x10(%rbp)
    0x100000f56 <+22>: movq   %rax, %rdi
    0x100000f59 <+25>: movb   $0x0, %al
    0x100000f5b <+27>: callq  0x100000f7a               ; symbol stub for: printf
    0x100000f60 <+32>: leaq   0x40(%rip), %rdi          ; "second printf"
    0x100000f67 <+39>: movl   %eax, -0x14(%rbp)
    0x100000f6a <+42>: movb   $0x0, %al
    0x100000f6c <+44>: callq  0x100000f7a               ; symbol stub for: printf
    0x100000f71 <+49>: movl   %eax, -0x18(%rbp)
    0x100000f74 <+52>: addq   $0x20, %rsp
    0x100000f78 <+56>: popq   %rbp
    0x100000f79 <+57>: retq

和大多数的Linux系统一样,mach-o符号的动态绑定也采用了打桩机制,简单的说就是在遇到外部符号的时候就会先跳转到stub区

stub区

  • 第一次printf会先进入dyld_stub_binder区去找到printf函数的地址,我们用lldb调试观察DATAla_symbol_ptr区地址值

stub跳转过程
会发现正好地址在dyld_stub_binder

验证:

验证1

验证1

  • 第二次printf我们在观察DATAla_symbol_ptr区地址值

第二次

我们会发现当第二次再次调用printf函数时,还是会先跳转到stub区,但此时la_symbol_ptr中的值却变为了printf的真实地址,而不是dyld_stub_binder。这样就完成了一次延时绑定,后面就直接调用。

0x02 stub桩机制总结

综上分析,我们可以发现所有的外部函数引用都会在DATAla_symbol_ptr区中产生一个占位符,其初始值为dyld_stub_binder区中对应的编号地址。当第一个调用时,就会进入符号的动态链接过程,一旦找到其地址后,就会将DATAla_symbol_ptr区中的占位符改为找到后的地址。这样就完成了只需要一个符号绑定。

stub桩机制的巧妙之处也在此,首先当产生一个外部符号调用时,直接跳到对应的stub桩位置,然后由里面保存的地址来判断是第一次调用还是已经找到符号的地址。就像桩这个名字含义一样,一个占位符的思想。

0x03 参考

Mach-O的动态链接相关知识

Dynamic Linking: ELF vs. Mach-O

Dynamic symbol table duel: ELF vs Mach-O, round 2