Calling Conventions
in Cocoa
by sunnyxx
Topics
- Calling Conventions - 函数调用的灵魂
- objc_msgSend 没想象中那么重要 ?
- NSInvocation & NSMethodSignature 动态调用好基友
- 大杀器 libffi
在 Hello World 背后
#include <stdio.h>
int main() {
printf("hello world");
return 0;
}
- 参数和返回值是如何传递的?
- 为什么没函数原型就调不了?
- 哪个编译器编译的 stdio 都不知道,那我的编译器怎么知道如何调用?
- 各个架构下都怎么处理?
- 可变参数咋搞的?万一有超多参数咋办?万一有个结构体咋办?
- 顺序执行指令
- 倒腾寄存器
- 倒腾内存
程序执行的时候都在干嘛?
本来挺简单的,直到有了 function
caller
callee
卧槽,我刚才的局部变量呢?
存在寄存器里的值也丢了!
刚才手头紧,临时用了下
Calling Conventions
规定在函数调用中:
- 各种情况下参数如何传递(用寄存器还是栈,还是混合?)
- 各种情况下返回值如何传递
- callee 如何使用寄存器和内存
- 进出函数如何保留现场和恢复现场
X86
- 所有参数都通过 stack 传递,所谓的“参数压栈”
- Callee 保证各个 register 进出函数不变
X86_64
- 按不同情况由 register 和 stack 混合传递
- Callee 只能使用一些 register,只需保证特定一些 register 进出函数不变
ARM32 & ARM64
prologue
epilogue
your code
保存现场
恢复现场
一个编译后的函数
int addUp(int first, int second) {
return first + second + 789;
}
int main() {
int ret = addUp(123, 456);
return 0
}
$clang -S main.m -arch x86_64
_addUp:
pushq %rbp
movq %rsp, %rbp
movl %edi, -4(%rbp) // arg0 = edi
movl %esi, -8(%rbp) // arg1 = esi
movl -4(%rbp), %esi // esi = arg0(123)
addl -8(%rbp), %esi // esi += arg1(456)
addl $789, %esi // esi += 789
movl %esi, %eax // eax = esi (eax储存返回值)
popq %rbp
retq
_main:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp // 申请 16 btyes 栈内存
movl $123, %edi // edi = 123
movl $456, %esi // esi = 456
movl $0, -4(%rbp)
callq _addUp // call addUp
xorl %esi, %esi
movl %eax, -8(%rbp) // 得到返回值,存到栈上
movl %esi, %eax
addq $16, %rsp
popq %rbp
retq
prologue
prologue
epilogue
epilogue
栈顶 高地址 0x7fffffff
- 低地址
bp
sp
本次函数调用使用的栈内存
申请局部变量时
rsp指向更低的地址
don't overflow stack
caller
管理栈内存
- Base Pointer 函数栈内存起始位置
- Stack Pointer 栈顶位置
- 在函数调用完成后回复到初始位置
函数原型 与 va_list 与 type promotion
还是讨论讨论吧
objc_msgSend
- 为什么它可以用一个函数原型处理所有调用?
- 为什么要用汇编实现?
- 不破坏参数和返回值寄存器
- tail jump
fake_msgSend
@interface Sark : NSObject
@end
@implementation Sark
- (int)fooWithBar:(int)bar baz:(int)baz {
return bar + baz;
}
@end
int fakeFoo(id self, SEL _cmd, int bar, int baz) {
return bar * baz;
}
extern void fake_msgSend(void);
__asm(
".global _fake_msgSend \n"
"_fake_msgSend:\n"
"jmp _fakeFoo\n"
);
int main(int argc, const char * argv[]) {
@autoreleasepool {
Sark *sark = [Sark new];
int ret = ((int (*)(id, SEL, int, int))fake_msgSend)
(sark, sel_registerName("fooWithBar:baz"), 123, 456);
NSLog(@"ret: %d", ret);
}
return 0;
}
imp_implementationWithBlock
__a1a2_tramphead:
popq %r10
andq $0xFFFFFFFFFFFFFFF8, %r10
subq $ PAGE_SIZE, %r10
movq %rdi, %rsi // arg1 -> arg2
movq (%r10), %rdi // block -> arg1
jmp *16(%rdi)
如何将 IMP 转发到 block->invoke ?
如何定义“动态语言”
- 拥有通过字符串反射出函数地址的能力
- 将函数的类型信息保留到 runtime
NSMethodSignature
i24@0:8i16i20
i@:ii
- 运行时的 type 解释器
- Calling Convention 的图纸
- objc runtime: method_getTypeEncoding
_TFC12TestFFISwift4Sark3foofT3barSi_Si
NSInvocation 实现了 Objective-C Calling Conventions
Message Forwarding!
- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
( JSPatch's Swizzling)
libffi
1. ffi_call 已知返回值和各参数type,动态调用一个 C 函数(objc,甚至 swift 方法)
2. ffi_closure 已知返回值和各参数type,动态生成一个函数指针,且这个函数被调用时,将调用的参数和返回值汇集到一个处理函数
可代替 NSInvocation
可代替 Forward 方式进行 Swizzle Patch
可动态生成 Block
讨论:Patch 非 @objc Swift 的可能性?
https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/LowLevelABI/130-IA-32_Function_Calling_Conventions/IA32.html
https://en.wikipedia.org/wiki/Calling_convention
https://msdn.microsoft.com/en-us/library/ms235286.aspx
https://www.mikeash.com/pyblog/friday-qa-2011-12-16-disassembling-the-assembly-part-1.html
https://www.raywenderlich.com/37181/ios-assembly-tutorial
https://www.mikeash.com/pyblog/friday-qa-2013-03-08-lets-build-nsinvocation-part-i.html
http://arigrant.com/blog/2014/2/12/why-objcmsgsend-must-be-written-in-assembly
http://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64/
Refs
Calling Conventions in Cocoa
By sunnyxx
Calling Conventions in Cocoa
介绍函数调用的灵魂 - Calling Conventions 以及它在 Cocoa 中的应用
- 8,912