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;
}
  1. 参数和返回值是如何传递的?
  2. 为什么没函数原型就调不了?
  3. 哪个编译器编译的 stdio 都不知道,那我的编译器怎么知道如何调用?
  4. 各个架构下都怎么处理?
  5. 可变参数咋搞的?万一有超多参数咋办?万一有个结构体咋办?
  • 顺序执行指令
  • 倒腾寄存器
  • 倒腾内存

程序执行的时候都在干嘛?

本来挺简单的,直到有了 function

caller

callee

卧槽,我刚才的局部变量呢?

 存在寄存器里的值也丢了!

刚才手头紧,临时用了下

Calling Conventions 

规定在函数调用中:

  1. 各种情况下参数如何传递(用寄存器还是栈,还是混合?)
  2. 各种情况下返回值如何传递
  3. callee 如何使用寄存器和内存
  4. 进出函数如何保留现场和恢复现场

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 

管理栈内存

  1. Base Pointer  函数栈内存起始位置
  2. Stack Pointer 栈顶位置
  3. 在函数调用完成后回复到初始位置

函数原型 与 va_list 与 type promotion

还是讨论讨论吧

objc_msgSend

  1. 为什么它可以用一个函数原型处理所有调用?
  2. 为什么要用汇编实现?
  1. 不破坏参数和返回值寄存器
  2. 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

Made with Slides.com