以上都是确定参数,那么如果是不确定参数,又是怎么传递的呢?
在AAPCS 64
文档里有明确的说明,但是这里我们从汇编的角度来看这个问题。
int mutableAragsFunc(int arg, ...) {
va_list list;
va_start(list, arg);
int ret = arg;
while(int a = va_arg(list, int)) {
ret += a;
}
va_end(list);
return ret;
}
mutableAragsFunc(1, 2, 3, 0);
在函数入口打断点,打印参数寄存器:
x0 = 0x0000000000000001
x1 = 0x000000016fce7930
x2 = 0x000000016fce7a18
x3 = 0x000000016fce7a90
x4 = 0x0000000000000000
x5 = 0x0000000000000000
x6 = 0x0000000000000001
x7 = 0x00000000000004b0
可以发现除了x0是正确的第一个参数,其他都是随机的,那么说明参数肯定被放到了栈上。
cfunction`main:
0x100121be4 <+0>: sub sp, sp, #0xa0 ; =0xa0
0x100121be8 <+4>: stp x29, x30, [sp, #0x90]
0x100121bec <+8>: add x29, sp, #0x90 ; =0x90
0x100121bf0 <+12>: mov w8, #0x0
0x100121bf4 <+16>: stur w8, [x29, #-0x4]
0x100121bf8 <+20>: stur w0, [x29, #-0x8]
0x100121bfc <+24>: stur x1, [x29, #-0x10]
0x100121c00 <+28>: mov x1, sp
0x100121c04 <+32>: mov x9, #0x0
0x100121c08 <+36>: str x9, [x1, #0x10] ; 压栈 0
0x100121c0c <+40>: orr w8, wzr, #0x3
0x100121c10 <+44>: mov x9, x8
0x100121c14 <+48>: str x9, [x1, #0x8] ; 压栈 3
0x100121c18 <+52>: orr w8, wzr, #0x2
0x100121c1c <+56>: mov x9, x8
0x100121c20 <+60>: str x9, [x1] ; 压栈 2
0x100121c24 <+64>: orr w0, wzr, #0x1 ; arg = 1
0x100121c28 <+68>: bl 0x1001218d8 ; mutableAragsFunc at main.mm:67
也就是表明被明确定义的参数,是按照上面所说的规则传递,而...
参数全部按照栈方式传递。这从实现原理上也比较容易理解,在取va_arg的时候,只需要将栈指针+sizeof(type)就可以了。
那么现在,我们回过头来看看第一个问题。C语言为什么会有函数签名?
函数签名决定了参数以及返回值的传递方式,同时还决定了函数栈帧的分布与大小,所以如果不确定函数签名,我们也就无法知道如何去传递参数了。
那么错误的函数签名会导致什么样的后果呢?运行时是否会崩溃?我们来看:
int arg1_func(int a) {
return a;
}
int arg2_func(int a, int b) {
return a+b;
}
void arg_test_func() {
int ret1 = ((int (*)(int, int))arg1_func)(1, 2);
int ret2 = ((int (*)(int))arg2_func)(1);
int ret3 = ((int (*)())arg1_func)();
int ret4 = ((int (*)())arg2_func)();
printf("%d, %d, %d, %d\n", ret1, ret2, ret3, ret4);
}
首先说结果,结果是一切运行正常,只是结果值有部分是错误的。那么我们来看看汇编代码:
cfunction`arg_test_func:
0x1003462cc <+0>: sub sp, sp, #0x50 ; =0x50
0x1003462d0 <+4>: stp x29, x30, [sp, #0x40]
0x1003462d4 <+8>: add x29, sp, #0x40 ; =0x40
; 以上都是处理栈帧
0x1003462d8 <+12>: orr w0, wzr, #0x1 ; w0 = 1
0x1003462dc <+16>: orr w1, wzr, #0x2 ; w1 = 2
0x1003462e0 <+20>: bl 0x100346298 ; arg1_func at main.mm:87
0x1003462e4 <+24>: orr w1, wzr, #0x1 ; w1 = 1
0x1003462e8 <+28>: stur w0, [x29, #-0x4] ; 将结果存入临时变量 ret1
; 按照寄存器的状态,这里相当于调用了 arg1_func(1)
; 其结果是正确的,只是可能没有符合预期
0x1003462ec <+32>: mov x0, x1 ; x0 = 1
0x1003462f0 <+36>: bl 0x1003462ac ; arg2_func at main.mm:90
0x1003462f4 <+40>: stur w0, [x29, #-0x8] ; 将结果存入临时变量 ret2
; 相当于 arg2_func(1, 1) = 2
; 第二个参数取决于上一次x1的状态
; 所以结果应该是随机的
0x1003462f8 <+44>: bl 0x100346298 ; arg1_func at main.mm:87
0x1003462fc <+48>: stur w0, [x29, #-0xc] ; 相当于 ret3 = arg1_func(2) = 2
0x100346300 <+52>: bl 0x1003462ac ; arg2_func at main.mm:90
0x100346304 <+56>: stur w0, [x29, #-0x10] ; 相当于 ret4 = arg2_func(2, 1) = 3
所以结果应该是1, 2, 2, 3
。
这里的结果不能代表任何在其他环境下的结果,可以说其结果是难以预测的。这里没有奔溃也只是随机参数并不会带来奔溃的风险。
所以我们是不能用其他函数签名来传递参数的。
接下来,我们来说说iOS中最著名的函数obj_msgSend
,可以说,这个函数是objc的核心和基础,没有这个方法,就不存在objc。
根据我们上面的分析,理论上我们不能改变obj_msgSend
的函数签名,来传递不同类型和个数的参数。那么苹果又是怎么实现的呢?
以前我们一直说obj_msgSend
用汇编来写是为了速度,但这并不是主要原因,因为retain,release也是非常频繁使用的方法,为什么不把这几个也改为汇编呢。其实更重要的原因是如果用C来写obj_msgSend
根本实现不了!
我们翻开苹果objc的源码,查看其中arm64.s汇编代码:
ENTRY _objc_msgSend
MESSENGER_START
cmp x0, #0 // nil check and tagged pointer check
b.le LNilOrTagged // (MSB tagged pointer looks negative)
ldr x13, [x0] // x13 = isa
and x9, x13, #ISA_MASK // x9 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
LNilOrTagged:
b.eq LReturnZero // nil check
// tagged
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x9, [x10, x11, LSL #3]
b LGetIsaDone
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
MESSENGER_END_NIL
ret
END_ENTRY _objc_msgSend
看出于上面其他C方法编译出来的汇编的区别了吗?
那就是obj_msgSend
居然不存在栈帧!同时也没有任何地方修改过X0-X7
,X8
,LR
,SP
,FP
!
而且当找到真正对象上的方法的时候,并不像其他方法一样使用BL
,而是使用了
.macro CacheHit
br x17 // call imp
也就是说并没有修改LR
。这样做的效果就相当于在函数调用的时候插入了一段代码!更像是c语言的宏。
由于obj_msgSend
并没有改变任何方法调用的上下文,所以真正的objc方法就好像是被直接调用的一样。
可以说,这种想法实在是太精彩了。
大家都知道,向空对象发送消息,返回的内容肯定都是0。那么这是为什么呢?
还是来看obj_msgSend
的源代码部分,第一行就判断了nil:
cmp x0, #0 // nil check and tagged pointer check
b.le LNilOrTagged // (MSB tagged pointer looks negative)
其中tagged pointer技术并不是我们本期的话题,所以我们直接跳到空对象的处理方法上:
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
MESSENGER_END_NIL
ret
他将可能的保存返回值的寄存器全部写入0!(为什么会有多个寄存器,是因为ARM其实是支持向量运算的,所以在某些条件下会用多个寄存器保存返回值,具体可以去参考ARM官方文档)。
这样我们的返回值就只能是0了!
等等,还缺少一个类型,struct!如果是栈上的返回,上文已经分析过是保存在X8
中的,可是我们并没有看到任何有关X8
的操作。那么我们来写一个demo尝试一下:
void struct_objc_nil(Test *t) {
struct BigStruct retB;
printf("stack: %d,%d,%d,%d,%d,%d,\n", retB.arg1, retB.arg2, retB.arg3, retB.arg4, retB.arg5, retB.arg6);
retB = ((struct BigStruct(*)(Test *, SEL))objc_msgSend)(t, @selector(retStruct));
printf("msgSend: %d,%d,%d,%d,%d,%d,\n", retB.arg1, retB.arg2, retB.arg3, retB.arg4, retB.arg5, retB.arg6);
retB = [t retStruct];
printf("objc: %d,%d,%d,%d,%d,%d,\n", retB.arg1, retB.arg2, retB.arg3, retB.arg4, retB.arg5, retB.arg6);
}
首先我们打开编译优化-os
(非优化状态,栈空间会被清0)。其结果居然是:
stack: 50462976,185207048,0,0,0,0,
msgSend: 1,0,992,0,0,0,
objc: 0,0,0,0,0,0,
struct类型两者的返回并不一致!按照我们阅读源码来推论,随机数值才是正确的结果,这是为什么呢?
我们还是来看汇编,我将关键部分特意标注了出来:
cfunction`struct_objc_nil:
0x10097e754 <+0>: sub sp, sp, #0x90 ; =0x90
0x10097e758 <+4>: stp x20, x19, [sp, #0x70]
0x10097e75c <+8>: stp x29, x30, [sp, #0x80]
0x10097e760 <+12>: add x29, sp, #0x80 ; =0x80
0x10097e764 <+16>: bl 0x10097e9d4 ; symbol stub for: objc_retain
0x10097e768 <+20>: mov x19, x0
0x10097e76c <+24>: adr x0, #0x1730 ; "stack: %d,%d,%d,%d,%d,%d,\n"
0x10097e770 <+28>: nop
0x10097e774 <+32>: bl 0x10097e9f8 ; symbol stub for: printf
0x10097e778 <+36>: nop
0x10097e77c <+40>: ldr x20, #0x262c ; "retStruct"
0x10097e780 <+44>: add x8, sp, #0x30 ; =0x30
0x10097e784 <+48>: mov x0, x19
0x10097e788 <+52>: mov x1, x20
0x10097e78c <+56>: bl 0x10097e9b0 ; symbol stub for: objc_msgSend
0x10097e790 <+60>: ldp w8, w9, [sp, #0x30]
0x10097e794 <+64>: ldp w10, w11, [sp, #0x38]
0x10097e798 <+68>: ldp w12, w13, [sp, #0x40]
0x10097e79c <+72>: stp x12, x13, [sp, #0x20]
0x10097e7a0 <+76>: stp x10, x11, [sp, #0x10]
0x10097e7a4 <+80>: stp x8, x9, [sp]
0x10097e7a8 <+84>: adr x0, #0x170f ; "msgSend: %d,%d,%d,%d,%d,%d,\n"
0x10097e7ac <+88>: nop
0x10097e7b0 <+92>: bl 0x10097e9f8 ; symbol stub for: printf
//////////////////////////////////////////////////////////
-> 0x10097e7b4 <+96>: cbz x19, 0x10097e7d8 ; <+132> at main.mm:134
; 这里的意思是:
; IF X19 == NULL THEN
; GOTO 0x10097e7d8
; 而 0x10097e7d8 就是内存清0的地方!
; X19 在 0x10097e768 被赋值为 objc 对象 'nil'
; 而在第一次调用 'obj_msgSend' 就没有这一段!
; (由于优化,有些逻辑和代码中有变化)
//////////////////////////////////////////////////////////
0x10097e7b8 <+100>: add x8, sp, #0x30 ; =0x30
0x10097e7bc <+104>: mov x0, x19
0x10097e7c0 <+108>: mov x1, x20
0x10097e7c4 <+112>: bl 0x10097e9b0 ; symbol stub for: objc_msgSend
0x10097e7c8 <+116>: ldp w8, w9, [sp, #0x30]
0x10097e7cc <+120>: ldp w10, w11, [sp, #0x38]
0x10097e7d0 <+124>: ldp w12, w13, [sp, #0x40]
0x10097e7d4 <+128>: b 0x10097e800 ; <+172> at main.mm:135
; 这里有一段清0的代码!正好就是返回值的局部变量地址
0x10097e7d8 <+132>: mov w13, #0x0
0x10097e7dc <+136>: mov w12, #0x0
0x10097e7e0 <+140>: mov w11, #0x0
0x10097e7e4 <+144>: mov w10, #0x0
0x10097e7e8 <+148>: mov w9, #0x0
0x10097e7ec <+152>: mov w8, #0x0
0x10097e7f0 <+156>: stp xzr, xzr, [sp, #0x60]
0x10097e7f4 <+160>: stp xzr, xzr, [sp, #0x50]
0x10097e7f8 <+164>: stp xzr, xzr, [sp, #0x40]
0x10097e7fc <+168>: stp xzr, xzr, [sp, #0x30]
0x10097e800 <+172>: stp x12, x13, [sp, #0x20]
0x10097e804 <+176>: stp x10, x11, [sp, #0x10]
0x10097e808 <+180>: stp x8, x9, [sp]
0x10097e80c <+184>: adr x0, #0x16c8 ; "objc: %d,%d,%d,%d,%d,%d,\n"
0x10097e810 <+188>: nop
0x10097e814 <+192>: bl 0x10097e9f8 ; symbol stub for: printf
0x10097e818 <+196>: mov x0, x19
0x10097e81c <+200>: bl 0x10097e9c8 ; symbol stub for: objc_release
0x10097e820 <+204>: ldp x29, x30, [sp, #0x80]
0x10097e824 <+208>: ldp x20, x19, [sp, #0x70]
0x10097e828 <+212>: add sp, sp, #0x90 ; =0x90
0x10097e82c <+216>: ret
0x10097e830 <+220>: b 0x10097e834 ; <+224> at main.mm
0x10097e834 <+224>: mov x20, x0
0x10097e838 <+228>: mov x0, x19
0x10097e83c <+232>: bl 0x10097e9c8 ; symbol stub for: objc_release
0x10097e840 <+236>: mov x0, x20
0x10097e844 <+240>: bl 0x10097e98c ; symbol stub for: _Unwind_Resume
到这里我们就能够明白了,为什么struct返回值也会变成0。是编译器给我们加入了一段判定的代码!
那么'objc空对象的返回值一定是0'这个判定就需要在一定条件下了。
对这一部分的探索一直持续了很久,一直是迷糊状态,不过经过长时间的多次探索,慢慢思考,总算有一个比较基础的认识了。
本文来自网易实践者社区,经作者段家顺授权发布。