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

0x00 摘要

通过分析Mach-O的动态链接过程,加深对Mach-O文件结构的理解。对Mach-O文件格式的简单的分析看这里这里

0x01 Mach-O Lazy Bind

Mach-O文件的通过dyld加载的时候并没有确定每一个函数的具体地址在哪里,而是在真正调用该函数的时候通过过程连接表(procedure linkage table),后面简称PLT,来进行一次lazybind

结合Mach-O文件的分析与代码的调试简单的分析一下,只能算是管中窥豹了。

源码很简单。

1
2
3
4
5
6
7
8
#include <stdio.h>

int main(int argc, const char * argv[]) {
    // insert code here...
    printf("Hello, World!\n");
  	printf("2Hello, World!\n");
    return 0;
}

分别在两个printf函数处下断点,启动程序。

1.1 第一次调用prinf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
lazy_bind`main:
    0x100000f10 <+0>:  pushq  %rbp
    0x100000f11 <+1>:  movq   %rsp, %rbp
    0x100000f14 <+4>:  subq   $0x20, %rsp
    0x100000f18 <+8>:  leaq   0x57(%rip), %rax          ; "Hello, World!\n"
    0x100000f1f <+15>: movl   $0x0, -0x4(%rbp)
    0x100000f26 <+22>: movl   %edi, -0x8(%rbp)
    0x100000f29 <+25>: movq   %rsi, -0x10(%rbp)
    0x100000f2d <+29>: movq   %rax, %rdi
    0x100000f30 <+32>: movb   $0x0, %al
->  0x100000f32 <+34>: callq  0x100000f56               ; symbol stub for: printf
    0x100000f37 <+39>: leaq   0x47(%rip), %rdi          ; "2Hello, World!\n"
    0x100000f3e <+46>: movl   %eax, -0x14(%rbp)
    0x100000f41 <+49>: movb   $0x0, %al
    0x100000f43 <+51>: callq  0x100000f56               ; symbol stub for: printf
    0x100000f48 <+56>: xorl   %ecx, %ecx
    0x100000f4a <+58>: movl   %eax, -0x18(%rbp)
    0x100000f4d <+61>: movl   %ecx, %eax
    0x100000f4f <+63>: addq   $0x20, %rsp
    0x100000f53 <+67>: popq   %rbp
    0x100000f54 <+68>: retq

0x100000f52 <+34>行处通过callq 0x100000f64来调用printf。

执行callq指令之后代码跳转到这里:

1
2
lazy_bind`printf:
->  0x100000f56 <+0>: jmpq   *0xb4(%rip)               ; (void *)0x0000000100000f6c

1.2 __Data,__la_symbol_ptr 获取函数地址

这里的jmpq要跳转到0x0000000100000f6c这个地址是从__Data,__la_symbol_ptr中的Lazy Symbol Pointers中获取到的。(怎么来的不是很清楚?)希望通过stub来调用printf函数。

通过命令行查看0x100001010处的地址获得了同样的值。

1
2
3
(lldb) x 0x100001010
0x100001010: 6c 0f 00 00 01 00 00 00 00 00 00 00 00 00 00 00  l...............
0x100001020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

1.3 通过__stub__helper进行lazybind

在Mach-O中每一个Symbol Stub可能有以下两种行为其中之一:

  • 跳转到函数的指令,执行函数体
  • 通过动态链接器查找函数的Symbol(符号),然后执行函数。

通过工具查看__stubs的Section数据,发现只有一个函数就是_printf。

这里的Data其实就是上面看到的jmpq代码。执行之后代码跳转到了这样的代码片段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
->  0x100000f6c: pushq  $0x0
    0x100000f71: jmp    0x100000f5c
    0x100000f76: gs     				;这里往下都没有!!!!
    0x100000f78: insb   %dx, %es:(%rdi)
    0x100000f79: insb   %dx, %es:(%rdi)
    0x100000f7a: outsl  (%rsi), %dx
    0x100000f7b: subb   $0x20, %al
    0x100000f7d: pushq  %rdi
    0x100000f7e: outsl  (%rsi), %dx
    0x100000f7f: jb     0x100000fed
    0x100000f81: andl   %ecx, %fs:(%rdx)
    0x100000f84: addb   %dh, (%rdx)
    0x100000f86: gs     
    0x100000f88: insb   %dx, %es:(%rdi)
    0x100000f89: insb   %dx, %es:(%rdi)
    0x100000f8a: outsl  (%rsi), %dx

这里就是通过_stub_helper来调用dyld_stubbinder函数来计算printf函数的真实地址。通过下面的\_TEXT,__stub_helper具体信息可以看出,jmpq 0x100000f5c,就是在压入参数0x0(函数的link的时候给的编号)之后跳转到Section的起始处,调用了binder。

binder是一块汇编代码。这里就不做解释了。作用就是计算具体的函数地址,并调用printf。

1.4 第二次调用printf函数

这个释放断点,程序将在调用第二个printf函数的地方停下来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
lazy_bind`main:
    0x100000f10 <+0>:  pushq  %rbp
    0x100000f11 <+1>:  movq   %rsp, %rbp
    0x100000f14 <+4>:  subq   $0x20, %rsp
    0x100000f18 <+8>:  leaq   0x57(%rip), %rax          ; "Hello, World!\n"
    0x100000f1f <+15>: movl   $0x0, -0x4(%rbp)
    0x100000f26 <+22>: movl   %edi, -0x8(%rbp)
    0x100000f29 <+25>: movq   %rsi, -0x10(%rbp)
    0x100000f2d <+29>: movq   %rax, %rdi
    0x100000f30 <+32>: movb   $0x0, %al
    0x100000f32 <+34>: callq  0x100000f56               ; symbol stub for: printf
    0x100000f37 <+39>: leaq   0x47(%rip), %rdi          ; "2Hello, World!\n"
    0x100000f3e <+46>: movl   %eax, -0x14(%rbp)
    0x100000f41 <+49>: movb   $0x0, %al
->  0x100000f43 <+51>: callq  0x100000f56               ; symbol stub for: printf
    0x100000f48 <+56>: xorl   %ecx, %ecx
    0x100000f4a <+58>: movl   %eax, -0x18(%rbp)
    0x100000f4d <+61>: movl   %ecx, %eax
    0x100000f4f <+63>: addq   $0x20, %rsp
    0x100000f53 <+67>: popq   %rbp
    0x100000f54 <+68>: retq

执行指令之后发现和第一次调用printf已经不一样了。

1
2
lazy_bind`printf:
->  0x100000f56 <+0>: jmpq   *0xb4(%rip)               ; (void *)0x00007fff96b1815c: printf

通过指令再一次查看0x100001010处的内存值。

1
2
3
(lldb) x 0x100001010
0x100001010: 5c 81 b1 96 ff 7f 00 00 00 00 00 00 00 00 00 00  \...............
0x100001020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

也就是说 __Data,__la_symbol_ptr中指向printf地址的值已经发生了变化,指向了printf的指令。

1.5 小结

证明了,延迟绑定只会在第一次调用的时候发生。整个流程与linux中的PLT与GOT在实现逻辑上基本是相同的,只是具体的代码实现不一样。

0x02 相关LoadCommand

上面只是通过调试简单的了解了动态链接的表现,要理解动态链接还需对几个数据结构有所了解。

2.1 LC_SYMTAB

LC_SYMTAB这个LoadCommand主要提供了两个信息

  • Symbol Table的偏移量与Symbol Table中元素的个数
  • String Table的偏移量与String Table的长度

2.1.1 Symbol Table

在计算机科学中,符号表是一种用于语言翻译器(例如编译器和解释器)中的数据结构。在符号表中,程序源代码中的每个标识符都和它的声明或使用信息绑定在一起,比如其数据类型、作用域以及内存地址。

–wiki

简单的理解就是Symbol Table里面包含了所有会被调用的函数的信息,无论是已经bind的还是没有bind的函数。

2.2.2 String Table

这个很好理解,在符号处理时所有会用到的字符串放在了这里。和__TEXT,__cstring不同。

2.2 LC_DYSYMTAB

LC_DYSYMTAB的数据结构,如图所示。这一个LoadCommand与动态链接相关的就是红框标出的两个字段,标示了需要动态符号表的偏移量与符号个数。

动态符号表的数据结构非常的简单,是一个32bit的索引的数组。通过索引可以在Symbol Table中寻找到对应的函数信息。

0x03 小结

通过分析两次printf的调用流程,加深对Mach-O结构以及动态链接的流程理解,为进一步理解dyld的工作原理,源码阅读提供了知识的储备。

通过和Linux的PTL与GOT比较可以更容易理解逻辑。

整个流程是如何通过代码实现的还需要进一步的分析与研究。

0x04 参考

1.Dynamic Linking: ELF vs. Mach-O

http://timetobleed.com/dynamic-linking-elf-vs-mach-o/

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

http://timetobleed.com/dynamic-symbol-table-duel-elf-vs-mach-o-round-2/


原文地址:http://turingh.github.io/2016/03/10/Mach-O%E7%9A%84%E5%8A%A8%E6%80%81%E9%93%BE%E6%8E%A5/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值