深入理解iOS动态链接

By xia0

#深入理解iOS动态链接

本文将通过objc_msgsend来介绍iOS平台上面的动态链接

#开始

动态链接有很多好处这里就不再叙述,本文主要分析动态链接一些平时忽略的细节。在2年前写过macho动态链接文章去分析过动态链接相关,但是之前分析得平台是x64的mac平台,而且分析得并不是很详细。由于需要对这块知识有一个比较深刻的理解,分析了在iOS平台动态链接的一些细节。本文就是我在分析过程中的一些心得和理解,希望能够有所帮组。

#外部符号调用过程

这里以objc_msgsend这个常见的符号为例,首先我们在ida中找到该符号的桩代码

__stubs:000000010000A2E8 ; void *objc_msgSend(void *, const char *, ...)
__stubs:000000010000A2E8 _objc_msgSend  
__stubs:000000010000A2E8                                       
__stubs:000000010000A2E8                 NOP
__stubs:000000010000A2EC                 LDR             X16, =__imp__objc_msgSend
__stubs:000000010000A2F0                 BR              X16     ; __imp__objc_msgSend

可以看出仅仅是一个跳转,由于ida这里识别出来为一个外部符号,所以生成了一个imp段,实际上跳转的地址并不是__imp__objc_msgSend,所以我们在调试器中跟一下

Process 6478 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x000000010000a3e4 TestAPP
->  0x10000a3e4: ldr    w16, 0x10000a3ec
    0x10000a3e8: b      0x10000a384
    0x10000a3ec: .long  0x00000246                ; unknown opcode
    0x10000a3f0: ldr    w16, 0x10000a3f8
    0x10000a3f4: b      0x10000a384
    0x10000a3f8: .long  0x0000025b                ; unknown opcode
    0x10000a3fc: ldr    w16, 0x10000a404
    0x10000a400: b      0x10000a384

发现跳转到了0x10000a3e4地址,在ida中查看该地址发现在stub_helper这个区


__stub_helper:000000010000A3E4                 LDR             W16, =0x246
__stub_helper:000000010000A3E8                 B               loc_10000A384
__stub_helper:000000010000A3EC dword_10000A3EC DCD 0x246

下面就会跳转到loc_10000A384其实所有的外部符号第一次调用的时候都会跳到这里进行符号绑定,还需要说明的一点在于,这里会将一个数加载到w16这个寄存器之中。这个数事实上和后面的符号绑定相关

__stub_helper:000000010000A384 ; Segment type: Pure code
__stub_helper:000000010000A384                 AREA __stub_helper, CODE, READWRITE
__stub_helper:000000010000A384                 ; ORG 0x10000A384
__stub_helper:000000010000A384                 CODE64
__stub_helper:000000010000A384                 ADR             X17, off_10000C030
__stub_helper:000000010000A388                 NOP
__stub_helper:000000010000A38C                 STP             X16, X17, [SP,#-0x10]!
__stub_helper:000000010000A390                 NOP
__stub_helper:000000010000A394                 LDR             X16, =dyld_stub_binder
__stub_helper:000000010000A398                 BR              X16     ; dyld_stub_binder

随后就会调用dyld_stub_binder函数进行符号绑定,这也是一个外部符号。当然你可能会说这样的话不就死循环了吗?其实不是,这个符号并不是lazy的即在初始化过程中就确定了地址。这里dyld_stub_binder的代码如下

libdyld.dylib`dyld_stub_binder:
->  0x181bccb64 <+0>:   stp    x29, x30, [sp, #-0x10]!
    0x181bccb68 <+4>:   mov    x29, sp
    0x181bccb6c <+8>:   sub    sp, sp, #0xf0             ; =0xf0
    0x181bccb70 <+12>:  stp    x0, x1, [x29, #-0x10]
    0x181bccb74 <+16>:  stp    x2, x3, [x29, #-0x20]
    0x181bccb78 <+20>:  stp    x4, x5, [x29, #-0x30]
    0x181bccb7c <+24>:  stp    x6, x7, [x29, #-0x40]
    0x181bccb80 <+28>:  stp    x8, x9, [x29, #-0x50]
    0x181bccb84 <+32>:  stp    q0, q1, [x29, #-0x80]
    0x181bccb88 <+36>:  stp    q2, q3, [x29, #-0xa0]
    0x181bccb8c <+40>:  stp    q4, q5, [x29, #-0xc0]
    0x181bccb90 <+44>:  stp    q6, q7, [x29, #-0xe0]
    0x181bccb94 <+48>:  ldr    x0, [x29, #0x18]
    0x181bccb98 <+52>:  ldr    x1, [x29, #0x10]
    0x181bccb9c <+56>:  bl     0x181bcd8b8        ; _dyld_fast_stub_entry(void*, long)
    0x181bccba0 <+60>:  mov    x16, x0
    0x181bccba4 <+64>:  ldp    x0, x1, [x29, #-0x10]
    0x181bccba8 <+68>:  ldp    x2, x3, [x29, #-0x20]
    0x181bccbac <+72>:  ldp    x4, x5, [x29, #-0x30]
    0x181bccbb0 <+76>:  ldp    x6, x7, [x29, #-0x40]
    0x181bccbb4 <+80>:  ldp    x8, x9, [x29, #-0x50]
    0x181bccbb8 <+84>:  ldp    q0, q1, [x29, #-0x80]
    0x181bccbbc <+88>:  ldp    q2, q3, [x29, #-0xa0]
    0x181bccbc0 <+92>:  ldp    q4, q5, [x29, #-0xc0]
    0x181bccbc4 <+96>:  ldp    q6, q7, [x29, #-0xe0]
    0x181bccbc8 <+100>: mov    sp, x29
    0x181bccbcc <+104>: ldp    x29, x30, [sp], #0x10
    0x181bccbd0 <+108>: add    sp, sp, #0x10             ; =0x10
    0x181bccbd4 <+112>: br     x16

然后调用了_dyld_fast_stub_entry函数,由于dyld本身开源的,这里下载源码去看下这个函数的实现

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;
}

然后这里调用了ImageLoaderMachOCompressed::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段。这里可以看出用用到这个段里面的一个数据去完成符号绑定。之前仅仅是知道这个段用作符号绑定,现在终于知道了原来就是这里用到了这个段的信息。这里先不去解释其中的opcode含义,关于这块的知识可以看这里DYLD Detailed。调试看下下一次调用时候,会不会直接返回绑定后的地址

TestAPP`objc_msgSend:
->  0x10000a2e8 <+0>: nop
    0x10000a2ec <+4>: ldr    x16, #0x1e1c       ; (void *)0x00000001817c9b80: objc_msgSend
    0x10000a2f0 <+8>: br     x16

从这里就能看出已经找到了objc_msgSend符号的地址

(lldb) p/x 0x10000a2ec+0x1e1c
(long) $9 = 0x000000010000c108
(lldb) x/g 0x000000010000c108
0x10000c108: 0x00000001817c9b80

那么这里的0x000000010000c108地址在macho文件在是什么地址呢?这里其实是一个指针,保存了外部符号的地址。这个地址在__DATA,__la_symbol_ptr这个区之中。初始值是__TEXT,__stub_helper中的某个地址。

#外部符号调用总结

上面的过程有一点绕,这里从宏哥来解释下整个过程

  • 程序调用一个外部符号(objc_msgsend)这里就是-[AppDelegate class]方法

    __text:0000000100008C34                 LDR             X30, [X30] ; _OBJC_CLASS_$_AppDelegate
    __text:0000000100008C38                 LDR             X1, [X1] ; "class"
    __text:0000000100008C3C                 STR             X0, [SP,#0x30+var_18]
    __text:0000000100008C40                 MOV             X0, X30 ; void *
    __text:0000000100008C44                 STR             W8, [SP,#0x30+var_1C]
    __text:0000000100008C48                 STR             X9, [SP,#0x30+var_28]
    __text:0000000100008C4C                 BL              _objc_msgSend
    
  • 然后跳转到__TEXT, __stubs这个区

    __stubs:000000010000A2E8 _objc_msgSend  
    __stubs:000000010000A2E8                                       
    __stubs:000000010000A2E8                 NOP
    __stubs:000000010000A2EC                 LDR          X16, =__imp__objc_msgSend
    __stubs:000000010000A2F0                 BR           X16
    
  • 这里x16寄存器的默认值就是objc_msgSend__TEXT,__stub_helper中对应的地址

    __stub_helper:000000010000A3E4                 LDR             W16, =0x246
    __stub_helper:000000010000A3E8                 B               loc_10000A384
    __stub_helper:000000010000A3EC dword_10000A3EC DCD 0x246
    
  • 接着就是去调用dyld_stub_binder函数进行符号绑定,第一次找到该符号地址以后直接返回给调用处,并且将__DATA,__la_symbol_ptrobjc_msgSend的指针值更改为找到的符号地址。这样下一次就不会跳转到__TEXT,__stub_helper里面,而是直接跳转到正确的函数地址。

    __stub_helper:000000010000A384 ; Segment type: Pure code
    __stub_helper:000000010000A384                 AREA __stub_helper, CODE, READWRITE
    __stub_helper:000000010000A384                 ; ORG 0x10000A384
    __stub_helper:000000010000A384                 CODE64
    __stub_helper:000000010000A384                 ADR             X17, off_10000C030
    __stub_helper:000000010000A388                 NOP
    __stub_helper:000000010000A38C                 STP             X16, X17, [SP,#-0x10]!
    __stub_helper:000000010000A390                 NOP
    __stub_helper:000000010000A394                 LDR             X16, =dyld_stub_binder
    __stub_helper:000000010000A398                 BR              X16     ; dyld_stub_binder
    
  • 最后一个函数符号的地址绑定就完成了

#参考