Contents

静态链接&动态链接

iOS AOP文章系列

前导知识:

AOP框架:

  • Method Swizzling
  • Fishhook
  • Apsects
  • NSProxy AOP

静态链接

还是通过 🌰 来分析:

➜  ios cat bar.c
int global = 1;
void fizz(int a) {
    global = a;
}
➜  ios cat main.c
extern int global;
void fizz(int a);
int main() {
    fizz(100 + global);
    return 0;
}

编译链接 bar.cmain.c 生成可执行文件 main

➜  ios xcrun -sdk iphoneos clang -c bar.c main.c -target arm64-apple-ios13.4
➜  ios xcrun -sdk iphoneos clang bar.o main.o -o main -target arm64-apple-ios13.4

我们看下链接前后符号表的变化:

➜  ios jtool -S bar.o
0000000000000000 T _fizz
0000000000000020 D _global
➜  ios jtool -S main.o
0000000000000000 T _main
                 U _fizz
                 U _global
➜  ios jtool -S main
0000000100000000 T __mh_execute_header
0000000100007f54 T _fizz
0000000100008000 D _global
0000000100007f74 T _main
                 U dyld_stub_binder

在链接前 bar.ofizzglobal 符号地址为临时地址,main.omain 符号为临时地址,fizzglobal 符号未定义,静态链接为可执行文件 main 后,发现各符号都有了对应地址,这中间发生了什么呢?

其中关键就是符号的解析重定位

用 MachOView 查看 main.o 文件:

./静态链接&动态链接.assets/1616051573607-0e9cbfb7-806e-41c7-bdbe-0560d945de0c.png

代码中符号的位置用 globalfizz 暂时用0x00x2c替代,链接器在完成地址空间分配后,就可以确定符号的虚拟地址了,链接器根据符号地址对每个需要重定位的指令进行地址修正,通过 _text 头中可以看到需要重定位的有三处以及重定位表偏移(488 = 0x1E8)。

./静态链接&动态链接.assets/1616052019933-d437e4f8-afe1-47a7-8b94-b4bd704a8a93.png

而具体重定位信息在重定位表 Relocations 中:

./静态链接&动态链接.assets/1616052905516-370e52eb-c467-4df9-927a-61c3e43efb6a.png

重定位表结构为:

struct relocation_info {
   int32_t  r_address;  /* offset in the section to what is being
                   relocated */
   uint32_t     r_symbolnum:24, /* symbol index if r_extern == 1 or section
                   ordinal if r_extern == 0 */
        r_pcrel:1,  /* was relocated pc relative already */
        r_length:2, /* 0=byte, 1=word, 2=long, 3=quad */
        r_extern:1, /* does not include value of sym referenced */
        r_type:4;   /* if not 0, machine specific relocation type */
};

r_symbolnum 具体为 Symbol 符号表中 index

./静态链接&动态链接.assets/1616052930555-0fa0b1ed-5579-4417-ac17-4db11b9b9163.png

符号表是一个数组,元素结构为 nlist_64

struct nlist_64 {
    union {
        uint32_t n_strx;   /* index into the string table */
    } n_un;
    uint8_t  n_type;       /* type flag, see below */
    uint8_t  n_sect;       /* section number or NO_SECT */
    uint16_t n_desc;       /* see <mach-o/stab.h> */
    uint64_t n_value;      /* value of this symbol (or stab offset) */
};

链接后(main)的符号表,其 n_value 已经替换为真实地址:

./静态链接&动态链接.assets/1616079174800-30bbebe4-11b5-4b1f-8a5c-da47e9462bdc.png

动态链接

静态链接是运行前就用 ld 链接器链接成一个完整的程序,而动态链接是程序主模块被加载时候,通过 dyld 加载命令,把 dylib 加载到内存,然后将程序中所有未决议的符号绑定到相应的 dylib 中,并进行重定位工作。

动态链接器加载命令 LC_LOAD_DYLINKER

./静态链接&动态链接.assets/1616080052259-10e499da-d350-40be-a591-4ab8960dc84c.png

动态链接库加载命令 LC_LOAD_DYLIB

./静态链接&动态链接.assets/1616080117478-dfaa7aef-8cf5-4f5c-9cdd-daaec7d10df4.png

还是用之前的 🌰 ,但是我们通过动态链接库生成可执行文件 main_dyld

➜  ios xcrun -sdk iphoneos clang -c main.c -o main_dyld.o -target arm64-apple-ios13.4
➜  ios xcrun -sdk iphoneos clang -fPIC -shared bar.c -o libbar.dylib -target arm64-apple-ios13.4
➜  ios xcrun -sdk iphoneos clang main_dyld.o -o main_dyld -L . -l bar -target arm64-apple-ios13.4

我们查看 main_dyld 的符号表,发现并没有真实地址,但是标记了该符号来自动态库 libbar.dylib

./静态链接&动态链接.assets/1616080726776-71e3446c-e580-4aa0-9aa5-e3ce46031ef7.png

我们跟踪 _TEXT,__text 看下 global 变量 和 fizz 函数是如何重定位的

./静态链接&动态链接.assets/1616082634070-cc9b2245-754b-4a5a-bdea-57c353c82c3f.png

global 是通过 adrpldr 指令间接寻址,取0x100008000里面的值,而这个地址指向的 got(也称 Non-Lazy Symbol Pointers ) 表中:

./静态链接&动态链接.assets/1616083101771-f7f8a583-f570-4db4-b845-4a75daa9c463.png

在程序装载过程中,会重定位 got 表中地址,但是如何知道这是什么符号?以及这个符号来自哪里?

这就需要用到 Section64 Header(__got) 中的 reserved1 字段,它代表在间接符号表(Indirect Symbols)中的 index

./静态链接&动态链接.assets/1616083318418-eb2b455c-adcd-49b3-8284-3799e8d77c51.png

而间接符号表本身存的是符号表中的 index,可以看到 index=1 的项值为4,代表符号表中 index=4 的项

./静态链接&动态链接.assets/1616083546713-b82bb228-384c-4890-bbc1-a733fa7a3c0c.png

./静态链接&动态链接.assets/1616083716102-84a1698d-224e-4411-9c68-98e12b6a7657.png

22代表在 String Table 中偏移22的字符串,即为 _global,并且描述 _global 来自 libbar.dylib 动态库,解决了上述两个问题,现在可以重定位 got 表中第一个符号地址。

在看下 fizz 函数的重定位过程

bl #0x100007f88

指向 _stubs 符号桩中

./静态链接&动态链接.assets/1616084090534-23415a04-a235-498d-b535-fc057efb2a51.png

➜  ios otool -v main_dyld -s __TEXT __stubs
main_dyld:
Contents of (__TEXT,__stubs) section
0000000100007f88    nop
0000000100007f8c    ldr x16, #0x4074            ; Latency: 4
0000000100007f90    br  x16

0x7f8c + 0x4074 = 0xc000

指向 __la_symbol_str 数据段

./静态链接&动态链接.assets/1616084517026-e59964a7-5bc6-4150-8bd5-faaa7f49e199.png

同样我们根据 Section64 Header(la_symbol_str) 中的 reserverd1 能够知道其符号名为 _fizz,而该函数的具体实现指向了 0x7facstub_helper 桩代码段)

./静态链接&动态链接.assets/1616084754983-40f42096-9d26-4c3f-9984-b26af85cfd65.png

通过 b #0x100007f94 跳到节头地址,然后走到 br x16x16 为存储 0x1000080008 在的 dyld_stub_binder 函数

./静态链接&动态链接.assets/1617260104224-49b92146-2ca7-49ed-94a1-d8c3afd28757.png

这个我们在Mach-O文件结构分析中已经讲过,是一个延迟绑定的过程,第一次调用外部函数,会调用 dyld_stub_binder 函数去寻址函数地址,然后写入 __la_symbol_str 段,之后再调同样的函数就会直接访问函数实现地址。我们看下 dyld_stub_binder 具体源码实现:

#if __arm__
 /*    
 * sp+4 lazy binding info offset
 * sp+0 address of ImageLoader cache
 */
  
    .text
    .align 2
    .globl  dyld_stub_binder
dyld_stub_binder:
    stmfd   sp!, {r0,r1,r2,r3,r7,lr}    // save registers
    add r7, sp, #16         // point FP to previous FP

    ldr r0, [sp, #24]           // move address ImageLoader cache to 1st parameter
    ldr r1, [sp, #28]           // move lazy info offset 2nd parameter

    // call dyld::fastBindLazySymbol(loadercache, lazyinfo)
    bl  __Z21_dyld_fast_stub_entryPvl
    mov ip, r0              // move the symbol`s address into ip

    ldmfd   sp!, {r0,r1,r2,r3,r7,lr}    // restore registers
    add sp, sp, #8          // remove meta-parameters

    bx  ip              // jump to the symbol`s address that was bound

#endif /* __arm__ */

调用 fastBindLazySymbol 函数,该函数也可以在 git 上找到:

uintptr_t fastBindLazySymbol(ImageLoader** imageLoaderCache, uintptr_t lazyBindingInfoOffset)
{
    uintptr_t result = 0;
    // get image 
    if ( *imageLoaderCache == NULL ) {
        // save in cache
        *imageLoaderCache = dyld::findMappedRange((uintptr_t)imageLoaderCache);
        if ( *imageLoaderCache == NULL ) {
            const char* message = "fast lazy binding from unknown image";
            dyld::log("dyld: %s\n", message);
            halt(message);
        }
    }
    
    // bind lazy pointer and return it
    try {
        result = (*imageLoaderCache)->doBindFastLazySymbol((uint32_t)lazyBindingInfoOffset, gLinkContext, 
                                (dyld::gLibSystemHelpers != NULL) ? dyld::gLibSystemHelpers->acquireGlobalDyldLock : NULL,
                                (dyld::gLibSystemHelpers != NULL) ? dyld::gLibSystemHelpers->releaseGlobalDyldLock : NULL);
    }
    catch (const char* message) {
        dyld::log("dyld: lazy symbol binding failed: %s\n", message);
        halt(message);
    }

    // return target address to glue which jumps to it with real parameters restored
    return result;
}

真正进行符号绑定的操作在 doBindFastLazySymbol 中:

uintptr_t ImageLoaderMachOCompressed::doBindFastLazySymbol(uint32_t lazyBindingInfoOffset, const LinkContext& context,
                                                            void (*lock)(), void (*unlock)())
{
    // <rdar://problem/8663923> race condition with flat-namespace lazy binding
    if ( this->usesTwoLevelNameSpace() ) {
        // two-level namespace lookup does not require lock because dependents can't be unloaded before this image
    }
    else {
        // acquire dyld global lock
        if ( lock != NULL )
            lock();
    }
    
    const uint8_t* const start = fLinkEditBase + fDyldInfo->lazy_bind_off;
    const uint8_t* const end = &start[fDyldInfo->lazy_bind_size];
    if ( lazyBindingInfoOffset > fDyldInfo->lazy_bind_size ) {
        dyld::throwf("fast lazy bind offset out of range (%u, max=%u) in image %s", 
            lazyBindingInfoOffset, fDyldInfo->lazy_bind_size, this->getPath());
    }

    uint8_t type = BIND_TYPE_POINTER;
    uintptr_t address = 0;
    const char* symbolName = NULL;
    uint8_t symboFlags = 0;
    long libraryOrdinal = 0;
    bool done = false;
    uintptr_t result = 0;
    const uint8_t* p = &start[lazyBindingInfoOffset];
    while ( !done && (p < end) ) {
        uint8_t immediate = *p & BIND_IMMEDIATE_MASK;
        uint8_t opcode = *p & BIND_OPCODE_MASK;
        ++p;
        switch (opcode) {
            case BIND_OPCODE_DONE:
                done = true;
                break;
            case BIND_OPCODE_SET_DYLIB_ORDINAL_IMM:
                libraryOrdinal = immediate;
                break;
            case BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB:
                libraryOrdinal = read_uleb128(p, end);
                break;
            case BIND_OPCODE_SET_DYLIB_SPECIAL_IMM:
                // the special ordinals are negative numbers
                if ( immediate == 0 )
                    libraryOrdinal = 0;
                else {
                    int8_t signExtended = BIND_OPCODE_MASK | immediate;
                    libraryOrdinal = signExtended;
                }
                break;
            case BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM:
                symbolName = (char*)p;
                symboFlags = immediate;
                while (*p != '\0')
                    ++p;
                ++p;
                break;
            case BIND_OPCODE_SET_TYPE_IMM:
                type = immediate;
                break;
            case BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB:
                if ( immediate >= fSegmentsCount )
                    dyld::throwf("BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB has segment %d which is too large (0..%d)", 
                            immediate, fSegmentsCount-1);
                address = segActualLoadAddress(immediate) + read_uleb128(p, end);
                break;
            case BIND_OPCODE_DO_BIND:
                
            
                result = this->bindAt(context, address, type, symbolName, 0, 0, libraryOrdinal, "lazy ", NULL, true);
                break;
            case BIND_OPCODE_SET_ADDEND_SLEB:
            case BIND_OPCODE_ADD_ADDR_ULEB:
            case BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB:
            case BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED:
            case BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB:
            default:
                dyld::throwf("bad lazy bind opcode %d", *p);
        }
    }   
    
    if ( !this->usesTwoLevelNameSpace() ) {
        // release dyld global lock
        if ( unlock != NULL )
            unlock();
    }
    return result;
}

这里通过 __LINKEDIT 段的基地址 + Lazy Binding Info 偏移(LC_DYLD_INFO_ONLY 加载命令中)计算 Lazy Binding Info 地址

并通过函数在表偏移定位具体函数信息,看到 _fizz 是在 libbar.dylib 动态库中,待改写的区域为 __DATA.__la_symbol_str

最后调用 bindAt 方法通过符号名在目标库中获取函数真实地址,并调用 bindLocation 方法改写 __la_symbol_str 中指向。

uintptr_t ImageLoaderMachOCompressed::bindAt(const LinkContext& context, uintptr_t addr, uint8_t type, const char* symbolName, 
                                uint8_t symboFlags, intptr_t addend, long libraryOrdinal, const char* msg, 
                                LastLookup* last, bool runResolver)
{
    const ImageLoader*  targetImage;
    uintptr_t           symbolAddress;
    
    // resolve symbol
    symbolAddress = this->resolve(context, symbolName, symboFlags, libraryOrdinal, &targetImage, last, runResolver);

    // do actual update
    return this->bindLocation(context, addr, symbolAddress, targetImage, type, symbolName, addend, msg);
}

动态链接的过程就结束了,这其中涉及到几个关键原理和概念:

  • 相对寻址和间接寻址
  • PIC原理
  • 延迟绑定
  • gotla_symbol_ptr
  • dyld 动态链接

PIC原理

上述 globalfizz 两个符号是在动态库中,而动态库是可以被多个进程共享的,而动态库编译时不知道自己在进程中的虚拟内存地址,因此动态库中不能包含绝对地址,那动态库如何将代码段做到与位置无关共享出去,就需要将对地址的引用分离,放到主程序数据部分(可修改),在加载和运行过程做寻址绑定操作。

got 和 la_symbol_ptr

通过上述分析,我们可以看到动态库数据 (global) 地址放到 got (Non-Lazy Symbol Pointers) 数据段,非惰性代表动态链接阶段就寻找好所有数据符号的地址;而函数 (fizz) 用了延迟绑定技术,将外部函数地址放在 la_symbol_ptr (Lasy Symbol Pointers) 数据段,惰性的,程序第一次调用到这个函数才寻址真实地址并写入这个数据段。