反汇编分析objc函数枢纽objc_msgSend

在分析objc_msgSend之前,先来搞清楚另一个问题。

函数是什么?可能会答 void foo(void) {} 像这样就是一个函数。或者函数包括函数原型和函数定义,是一段执行某样功能的机器代码。

调用函数时必须要准备两个要素,函数原型和函数入口地址。

函数原型的作用是什么?答声明了函数调用的方式。不够具体。函数原型是函数调用方和函数定义之间的关于参数传递和结果返回的协议约定。这个协议分别作用在函数入口两边的代码,一边是调用方在调用处协议的构建,另一边是函数定义对协议的访问解释。传统地就是调用栈。原型是一个协议,协议是可以传递的。所以在函数被调用处,到函数的入口地址之间,是可以做任何处理,只要协议不被破坏。

而objc在函数调用和函数入口之间加入了动态绑定的处理,这个处理就是msgSend。

大家都知道这个原型id(*IMP)(id, char*, …),而这个却只是用于传递的协议,并非函数的真正的原型。对于后面的省略号,传统地是va_list访问,但是实际上省略号即第三个参数开始可以是任何其实传参方式。至于从第三个参数开始之后的协议是怎么约定的,在objc函数调用处和函数定义是必须明确清楚的。然而在之两者之间的中继路由过程中,只需要知道前两个参数的约定,就是这个原型id()(id, char*,…),所以msgSend也是这个原型。

举例-(id)foo:(int)i;

id foo(id, char*, int) —> id msgSend(id, char*, …) —> (id()(id, char*, …))foo

作为objc中函数调用的枢纽,我们现在就来看一下它的反汇编样貌:

libobjc.A.dylib`objc_msgSend:
->  0x107b68800 <+0>:   testq  %rdi, %rdi
    0x107b68803 <+3>:   jle    0x107b68850               ; <+80>    // except the bad pointer to obj
    0x107b68805 <+5>:   movq   (%rdi), %r11
    0x107b68808 <+8>:   movq   %rsi, %r10
    0x107b6880b <+11>:  andl   0x18(%r11), %r10d         ;            // (int32)(%rsi) &= (int32)0x18(%rdi) , (%rsi) <<= 4, (int64)(%rsi) += (int64)0x10(%rdi).
    0x107b6880f <+15>:  shlq   $0x4, %r10
    0x107b68813 <+19>:  addq   0x10(%r11), %r10
    0x107b68817 <+23>:  cmpq   (%r10), %rsi
    0x107b6881a <+26>:  jne    0x107b68820               ; <+32>
    0x107b6881c <+28>:  jmpq   *0x8(%r10)                 ;            // jmp to imp
    0x107b68820 <+32>:  cmpq   $0x1, (%r10)
    0x107b68824 <+36>:  jbe    0x107b68833               ; <+51>
    0x107b68826 <+38>:  addq   $0x10, %r10
    0x107b6882a <+42>:  cmpq   (%r10), %rsi
    0x107b6882d <+45>:  jne    0x107b68820               ; <+32>
    0x107b6882f <+47>:  jmpq   *0x8(%r10)                            
    0x107b68833 <+51>:  jb     0x107b68871               ; <+113>
    0x107b68835 <+53>:  movq   0x8(%r10), %r10
    0x107b68839 <+57>:  jmp    0x107b68845               ; <+69>
    0x107b6883b <+59>:  cmpq   $0x1, (%r10)
    0x107b6883f <+63>:  jbe    0x107b6884e               ; <+78>
    0x107b68841 <+65>:  addq   $0x10, %r10
    0x107b68845 <+69>:  cmpq   (%r10), %rsi
    0x107b68848 <+72>:  jne    0x107b6883b               ; <+59>
    0x107b6884a <+74>:  jmpq   *0x8(%r10)                            
    0x107b6884e <+78>:  jmp    0x107b68871               ; <+113>
    0x107b68850 <+80>:  je     0x107b68866               ; <+102>    // a neg pointer is a objc debug tagged pointer classes.
    0x107b68852 <+82>:  leaq   0x348df7(%rip), %r11      ; objc_debug_taggedpointer_classes
    0x107b68859 <+89>:  movq   %rdi, %r10
    0x107b6885c <+92>:  shrq   $0x3c, %r10
    0x107b68860 <+96>:  movq   (%r11,%r10,8), %r11
    0x107b68864 <+100>: jmp    0x107b68808               ; <+8>        // jump back and deal with this debug obj.
    0x107b68866 <+102>: xorl   %eax, %eax                 ;            // deal with a nil obj.
    0x107b68868 <+104>: xorl   %edx, %edx
    0x107b6886a <+106>: xorps  %xmm0, %xmm0
    0x107b6886d <+109>: xorps  %xmm1, %xmm1
    0x107b68870 <+112>: retq                                ;            // bad return clause.
    0x107b68871 <+113>: pushq  %rbp
    0x107b68872 <+114>: movq   %rsp, %rbp
    0x107b68875 <+117>: subq   $0x88, %rsp                 ;            // and total push size 0x38, %rsp is 0xc0 bytes far away from %rbp when next call in soon
    0x107b6887c <+124>: movdqa %xmm0, -0x80(%rbp)
    0x107b68881 <+129>: pushq  %rax
    0x107b68882 <+130>: movdqa %xmm1, -0x70(%rbp)
    0x107b68887 <+135>: pushq  %rdi
    0x107b68888 <+136>: movdqa %xmm2, -0x60(%rbp)
    0x107b6888d <+141>: pushq  %rsi
    0x107b6888e <+142>: movdqa %xmm3, -0x50(%rbp)
    0x107b68893 <+147>: pushq  %rdx
    0x107b68894 <+148>: movdqa %xmm4, -0x40(%rbp)
    0x107b68899 <+153>: pushq  %rcx
    0x107b6889a <+154>: movdqa %xmm5, -0x30(%rbp)
    0x107b6889f <+159>: pushq  %r8
    0x107b688a1 <+161>: movdqa %xmm6, -0x20(%rbp)
    0x107b688a6 <+166>: pushq  %r9
    0x107b688a8 <+168>: movdqa %xmm7, -0x10(%rbp)
    0x107b688ad <+173>: movq   %rdi, %rdi
    0x107b688b0 <+176>: movq   %rsi, %rsi
    0x107b688b3 <+179>: movq   %r11, %rdx                 ;            // isa member of obj of %rdi
    0x107b688b6 <+182>: callq  0x107b59c57               ; _class_lookupMethodAndLoadCache3
    0x107b688bb <+187>: movq   %rax, %r11
    0x107b688be <+190>: movdqa -0x80(%rbp), %xmm0
    0x107b688c3 <+195>: popq   %r9
    0x107b688c5 <+197>: movdqa -0x70(%rbp), %xmm1
    0x107b688ca <+202>: popq   %r8
    0x107b688cc <+204>: movdqa -0x60(%rbp), %xmm2
    0x107b688d1 <+209>: popq   %rcx
    0x107b688d2 <+210>: movdqa -0x50(%rbp), %xmm3
    0x107b688d7 <+215>: popq   %rdx
    0x107b688d8 <+216>: movdqa -0x40(%rbp), %xmm4
    0x107b688dd <+221>: popq   %rsi
    0x107b688de <+222>: movdqa -0x30(%rbp), %xmm5
    0x107b688e3 <+227>: popq   %rdi
    0x107b688e4 <+228>: movdqa -0x20(%rbp), %xmm6
    0x107b688e9 <+233>: popq   %rax
    0x107b688ea <+234>: movdqa -0x10(%rbp), %xmm7
    0x107b688ef <+239>: leave  
    0x107b688f0 <+240>: cmpq   %r11, %r11
    0x107b688f3 <+243>: jmpq   *%r11                     ;            // the real imp address related to set
    0x107b688f6 <+246>: nopw   %cs:(%rax,%rax)

 

代码中分两部分,第一部分是取出正确的receiver,请看我的反c伪代码:

 

从代码中可以看到,0指针被过滤直接返回,负数指针被转换成正确的指针。负数指针?第一眼你可能会和我一样认为这是一个访问到了内核空间的指针,因为在32位体系系统中,一般地高2G地址是内核地址,最高位为1。但是在64位下并非就代表访问到了内核地址,现在的x64处理器有效寻址不是64位有效寻址,而是48位,而且高16位必须与第48位一致,其余的看作无效地址。这个负数地址,其实是一类被定义为tagged的指针,作用类似于erlang的原子量atom。

接下来是另一部分,找到正确的地址入口,然后跳过去,调用协议原封不动。请看我的反c伪代码:

从代码中可以看到SEL自始至终也只不过是一个调用名称,SEL和IMP以key-value方式存放在各种查找表中。不用多说,先从常用cache中查找,没有就从类描述中找出真实入口地址。在cache查找中有这么3点逻辑,

1.不命中,而且有效地址,下一个key-value

2.不命中,并且无效地址,中止在cache的查找

3.不命中,并且为1,必须还是首次遇到1,然后cache forward,继续在cache中查找。

 

最后是我手工对msgSend原代码中各处调用宏后的代码

/********************************************************************
 *
 * id objc_msgSend(id self, SEL    _cmd,...);
 *
 ********************************************************************/
    
    .data
    .align 3
    .globl _objc_debug_taggedpointer_classes
_objc_debug_taggedpointer_classes:
    .fill 16, 8, 0

    ENTRY    _objc_msgSend
    MESSENGER_START

    GetIsaCheckNil    NORMAL        // r11 = self->isa, or return zero
    CacheLookup NORMAL        // calls IMP on success

    GetIsaSupport    NORMAL

// cache miss: go search the method lists
LCacheMiss:
    // isa still in r11
    MethodTableLookup %a1, %a2    // r11 = IMP
    cmp    %r11, %r11        // set eq (nonstret) for forwarding
    jmp    *%r11            // goto *imp

    END_ENTRY    _objc_msgSend


/********************************************************************
 *
 * id objc_msgSend(id self, SEL    _cmd,...); Expand
 *
 ********************************************************************/

    .data
    .align 3
    .globl _objc_debug_taggedpointer_classes
_objc_debug_taggedpointer_classes:
    .fill 16, 8, 0

    // ENTRY    _objc_msgSend
.text
    .globl    _objc_msgSend
    .align    6, 0x90
_objc_msgSend:
    .cfi_startproc

    // MESSENGER_START
4:
    .section __DATA,__objc_msg_break
    .quad 4b
    .quad ENTER
    .text

    testq    %a1, %a1
    jle    LNilOrTagged_f    // MSB tagged pointer looks negative
    movq    (%a1), %r11    // r11 = isa

LGetIsaDone:    

    movq    %a2, %r10        // r10 = _cmd
    andl    24(%r11), %r10d        // r10 = _cmd & class->cache.mask
    shlq    $$4, %r10        // r10 = offset = (_cmd & mask)<<4
    addq    16(%r11), %r10        // r10 = class->cache.buckets + offset

    cmpq    (%r10), %a2        // if (bucket->sel != _cmd)
    jne     1f            //     scan more
    // CacheHit must always be preceded by a not-taken `jne` instruction
    CacheHit $0            // call or return imp

1:
    // loop
    cmpq    $$1, (%r10)
    jbe    3f            // if (bucket->sel <= 1) wrap or miss

    addq    $$16, %r10        // bucket++
2:    
    cmpq    (%r10), %a2        // if (bucket->sel != _cmd)
    jne     1b            //     scan more
    // CacheHit must always be preceded by a not-taken `jne` instruction
    CacheHit $0            // call or return imp

3:
    // wrap or miss
    jb    LCacheMiss_f        // if (bucket->sel < 1) cache miss
    // wrap
    movq    8(%r10), %r10        // bucket->imp is really first bucket
    jmp     2f

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

1:
    // loop
    cmpq    $$1, (%r10)
    jbe    3f            // if (bucket->sel <= 1) wrap or miss

    addq    $$16, %r10        // bucket++
2:    
    cmpq    (%r10), %a2        // if (bucket->sel != _cmd)
    jne     1b            //     scan more
    // CacheHit must always be preceded by a not-taken `jne` instruction
    CacheHit $0            // call or return imp

3:
    // double wrap or miss
    jmp    LCacheMiss_f

    .align 3
LNilOrTagged:
    jz    LNil_f        // flags set by NilOrTaggedTest

    // tagged
    
    leaq    _objc_debug_taggedpointer_classes(%rip), %r11
    movq    %a1, %r10
    shrq    $$60, %r10
    movq    (%r11, %r10, 8), %r11    // read isa from table
    jmp    LGetIsaDone_b

LNil:
    // nil
    xorl    %eax, %eax
    xorl    %edx, %edx
    xorps    %xmm0, %xmm0
    xorps    %xmm1, %xmm1

4:
    .section __DATA,__objc_msg_break
    .quad 4b
    .quad NIL_EXIT
    .text
    ret
    
// cache miss: go search the method lists
LCacheMiss:
    // isa still in r11

4:
    .section __DATA,__objc_msg_break
    .quad 4b
    .quad SLOW_EXIT
    .text
    
    SaveRegisters

    // _class_lookupMethodAndLoadCache3(receiver, selector, class)

    movq    %a1, %a1
    movq    %a2, %a2
    movq    %r11, %a3
    call    __class_lookupMethodAndLoadCache3

    // IMP is now in %rax
    movq    %rax, %r11

    RestoreRegisters


    cmp    %r11, %r11        // set eq (nonstret) for forwarding
    jmp    *%r11            // goto *imp    
    // END_ENTRY    _objc_msgSend
    .cfi_endproc
LExit_objc_msgSend:

 

最后多谢各位观看。后面的文章继续反汇编分析objc。

posted on 2016-01-07 16:00  bbqz007  阅读(2525)  评论(0编辑  收藏  举报