Mach-O 符号/静态库 、动态库

1,049 阅读7分钟

1. MachO

一个可执行文件;包括 代码层(汇编代码的逻辑层)、数据层、符号层;

image.png

image.png

1.1 符号

在开始符号,下面有几个问题,我们可以先看看

  1. 什么是符号?
  2. 符号存储在哪里?
  3. 符号的缺点?
  • 重定位符号
  • 全局符号 & 本地符号
  • 导出符号
  • 间接符号
  • 弱引用 & 弱定义

1.1.1 重定位


// clang -arch x86_64 -c test.c -o test.o : 编译成.o 文件 
// clang -arch x86_64 test.o -o test: 编译成 可执行文件

void test1() {
}
void test2() {
}
int main(int argc, char * argv[]) {
    test1();
    test2();
}

查看代码段:

重定位之前的: objdump --macho -d test.o

重定位之后的: objdump --macho -d test

image.png

查看重定位(reloc)符号 objdump --macho --reloc test.o

image.png

1.1.2 全局符号&本地符号

  • 全局(静态或者非静态) 初始化的符号是存在 __Data __data区的;
  • 未初始化的 全局变量(非静态) 存在 __Data __common
  • 未初始化的 静态变量(全局或者局部) 存在 __Data __bss (例如: __bss _g_s_u_1, __bss _main.l_s_u)
    • 静态全局 未初始化 && 未被函数使用g_s_u_2, 直接被忽略了;

    • 静态局部 未初始化 && 未被函数使用l_s_u 依然被存在__DATA,__bss _main.l_s_u

全局局部变量.png


// #include "export.h"
#include <stdio.h>

int g_1 = 0x0101;  // g     O __DATA,__data _g_1
int g_2 = 0x0202;
int g_3 = 0x0303;
int g_4 = 0x0404;

int g_u_1; // g     O __DATA,__common _g_u_1
int g_u_2;

static int g_s_1 = 0x0505; // l     O __DATA,__data _g_s_1
static int g_s_2 = 0x0606;
static int g_s_3 = 0x0707;
static int g_s_4 = 0x0808;
static int g_s_u_1;  // l     O __DATA,__bss _g_s_u_1
static int g_s_u_2; // 会被忽略


/**
 
 clang -arch x86_64 -c test.c -o test.o : 编译成.o 文件
 clang -arch x86_64 test.o -o test  : 编译成 可执行文件
 */
void test1() {
    g_3 = g_1 + g_2;
    g_s_u_1 = 0x2525;
}

void test2() {
    g_s_3 = g_s_1 + g_s_2;
    int a  = g_s_u_1 ;
}

int main(int argc, char * argv[]) {
    
    int l_1 = 0x1212; // 局部变量会被忽略
    int l_2 = 0x1313;
    int l_u;
    
    static int l_s_1 = 0x1414; // l     O __DATA,__data _main.l_s_1
    static int l_s_u ;   // l     O __DATA,__bss _main.l_s_u
    
//    g_s_6 = l_s_1;

    test1();
    test2();

    printf("g_1 = %x \n",g_1);
}

image.png

查看 符号表: objdump --macho -syms ./staticlib/static_test

image.png

1.1.3 导出符号

  • 全局符号默认导出的: objdump --macho --exports-trie static_test

image.png

1.1.4 间接符号

和动态库相关的 objdump --macho --indirect-symbols static_test

image.png

通过macho 查看 ,其中index 其实是执行符号表的序号,后面的 符号查找会详细介绍;

image.png

1.1.5 弱符号

弱定义

首先看一个经典的错误: 1 duplicate symbol,使用了多个 强引用的符号

image.png

解决: export.c 中 int g_1 attribute((weak)) = 0x0202; 或者 int g_1

弱导入

一个经典的错误 Undefined symbols ,使用了一个 只声明了头文件,没有实现的函数

export.h

void weak_export_func() __attribute__((weak_import));

在test.c 中 使用,并且编译链接的时候 需要 把 weak_export_func 指定为 未定义

clang test.c -L./staticlib -lexport -Xlinker -U -Xlinker _weak_export_func -o ./static_test

if (weak_export_func){
    weak_export_func();
    printf("weak_export_func = null \n");
}

1.2 符号查找

dynamic symbol table (存储符号表的 index) => symbol table (存储了 string 表的index) => 找到string table

image.png

下面根据 列表来查找

0xe8 = 232 image.png 很显然 我们在 symbol 表里面找到了 NSLog

image.png

然后我们可以看到 0x12e = 302 这个值就是 string 表的偏移量

使用命令查看string 表 strings Demo1

image.png

string 表 每行 16个字符; 那么 302 / 16 = 18 ... 14;

image.png

2 静态库 & 动态库

  • 库: 其实就是 .o 文件的集合

  • 静态库

    • 执行速度快;
    • 静态库在编译链接后 会作为 可执行的一部分;
  • 动态库

    • iOS系统的动态库,NSFoundation、UIKit 等
    • IOS自定义的动态库 会在 .app 的 内部,但不会合入 可执行文件中;
    • IOS会加快下载速度- 实现增量更新: 就是 如果用户要更新 app的时候,如果app 内部的 文件 (比如动态库)相同就不会下载: z.itpub.net/article/det…
    • 设计到 非懒加载 和 懒加载 ,分别会影响 启动速度 和 函数的执行速度(第一次会影响);

2.1 静态库

测试静态库:

exprot.c

int g_export_1 = 0xbb;
int g_export_2_hidden  __attribute__((visibility("hidden"))) = 0xaa ; //改变全局属性
static int g_export_3 = 0xcc;
int g_export_3_u ;
int g_export_4_u_hidden __attribute__((visibility("hidden"))) ;

int g_1 __attribute__((weak)) = 0x1012;
// int g_1 = 0x1012;
static int s_export_func() {
    return 20;
}
int export_func() __attribute__((visibility("hidden")));
int export_func() {
    return s_export_func();
}

void weak_export_func() __attribute__((weak_import));

makeStatic.sh

# 编译成.o 文件:  
clang -c  export.c   -o ./staticlib/export.o
# 制作静态库:     
ar rcs ./staticlib/libexport.a  ./staticlib/export.o
#  链接 静态库:    
# clang test.c -weak_library ./staticlib/libexport.a  -L./staticlib  -lexport   -o ./static_test 
clang test.c   -L./staticlib  -lexport -Xlinker -U -Xlinker _weak_export_func  -o ./static_test 

image.png

可以看到 动态库里面的 全局变量符号 g_export_1 等 和 局部符号 g_export_4_u_hidden 等 已经在 可执行文件 static_test 中;

2.2 动态库

makeDy.sh

# 创建动态库, @rpath/dylib/libdyExport.dylib 会先存着  libdyExport.dylib 中 => 
# 后面宿主文件 链接 它的时候,会把这个路径注入到宿主文件中
clang  -Xlinker -install_name -Xlinker @rpath/dylib/libdyExport.dylib  -dynamiclib -o ./dylib/libdyExport.dylib export.c

# image not found
clang  test.c  -L./dylib  -ldyExport  -o dy_test_crash
# 修复 (宿主中提供 @rpath的路径)
clang  test.c  -Xlinker -rpath -Xlinker @executable_path  -L./dylib  -ldyExport   -o  dy_test
# Undefined symbols : 如果宿主中使用了 动态库的 局部变量会报 这个错误

为什么需要使用@rpath/dylib/libdyExport.dylib,而静态库不需要?

  • 动态库是一个独立的文件,所以动态库 需要告诉 可执行文件,我在哪里?
  • rpath: 其实是一个变量,run path,需要 可执行文件 指定,才能合成一个完整的路径;
    • 如果path 错误,编译没问题,运行的时候,会出现一个比较经典的错误 image not found
    • image.png

可以看到,我们的动态库的符号 全局变量符号 g_export_1 等 和 局部符号 g_export_4_u_hidden 等 是没有加载到可执行文件中; image.png

是在动态库自己的符号表里面:

image.png

2.3 绑定动态库符号

接下来,我们使用一个简单的例子 NSLog 来说明:

image.png


NSLog(@"hello1");

NSLog(@"hello2");

Mach-O

  1. Xcode => debug => debug workflow => Always show disamble 在 nslog 之前打个断点

  2. 打印出 运行基址:

  • 运行基址 = 虚拟地址 + ASLR偏移
  • 二进制地址(MachView 查看) = 【运行地址】 - 【运行基址】
image list | grep "Demo1"

运行基址: 0x000000010c796000 (已经包括了ASLR)

ASLR: 0x000000010c796000 - 0x100000000(Macho的运行虚拟地址) = 0xc796000

虚拟基地址: image.png

  1. 找到 NSLog 的 位置
0x10c797b00 <+80>:  callq  0x10c798318 ; symbol stub for: NSLog

image.png

找到 nslog地址: 0x10c798318;

获得二进制的NSLog地址: p/x 【nslog地址】 - [基地址] = 0x10c798318 - 0x000000010c796000 = 0x2318

=> 去MachoView中查看

image.png

  1. 我们通过 nslog 0x10c798318 (入口地址) 反汇编一下,看看内部逻辑

解读指令 FF25 E25c0000

  • FF25: jmpq 指令
  • e25c 0000(大端) => 小端 0x00005ce2 => 0x5ce2 大端、小端模式: 例如: 0x1234, 指针P 先存0x12(高位) 为大端模式,反之 先存 0x34(低位) 为小端模式; 小端模式的好处就是 比如 高字节(long: 0x0000000055667788 ) 转 低字节(int 0x55667788), 会直接抛弃高位的; 0x5ce2 + rip寄存器 => 0x000000010c798370

image.png

4.1 查看 0x000000010c798370 指针在 Mach-o 中的地址

p/x 0x000000010c798370 - 0x000000010c796000(运行基址) = 0x2370

会进入 __stub_helper image.png

下一步会执行 jmp 0x100002360(Macho的虚拟地址)

运行地址 = 0x100002360 (虚拟地址) + ASLR (0xc796000) = 0x000000010c798360, 然后给 这个点 加个断点:

br set -a 0x000000010c798360 点击下一步断点(也可以通过 si,在汇编调试模式下进入)

我们就很显然的看到 会先进入 _dyld_private 再进入 dyld_stub_binder 去执行 NSLog 的加载: image.png

到这里基本上就是 nglog的 第一次加载过程;

  1. 第一个NSLog 执行后,我们看看 第二个 NSLog
0x10c797b00 <+80>:  callq  0x10c798318 ; symbol stub for: NSLog

很明显看到 已经没有了 第一次的 _dyld_private, 直接进入的 Foundtion NSLog的函数地址;

image.png