深刻iOS系统底层之函数调用

古器合尺度,法物应矩规。--苏洵html

1、什么是函数

可执行程序是为了实现某个功能而由不一样机器指令按特定规则进行组合排列的集合。不管高级仍是低级程序语言,不管是面向对象仍是面向过程的语言最终的代码都会转化为一条条机器指令的形式被执行。为了管理上的方便和对代码的复用,每每须要将某一段实现特定功能的指令集合进行抽离和处理从而造成了函数的概念,函数也能够称之为子程序或者子例程。出现函数的概念后可执行程序的机器指令集合将再也不是单一的一块代码,而是由多个函数组成的分块代码,这样可执行程序就变成了由函数之间相互调用这种方式来构建和组织了。git

一个函数由函数签名、参数、返回、实现四部分组成。函数的前三者定义了明确的边界信息,也称之为函数接口描述。函数接口描述的意义在于调用者再也不须要了解被调用者函数的实现细节,而只须要按被调用者的定义的接口进行交互便可。如何去定义一个函数,如何去实现一个函数,如何去调用一个函数,如何将参数传递给被调用的函数,如何使用被调用者函数的返回这些都须要有统一的标准规范来进行界定,这个规则有两个层面的标准:在高级语言层面的规则称之为API规则;而在机器指令层面上则因为不一样的操做系统以及不一样的CPU体系结构下提供的指令集和构造程序的方式不一样而不一样,因此在系统层面的规则称之为ABI规则。本文的重点是详细介绍函数调用、函数参数传递、函数返回值这3个方面的ABI规则,经过对这些规则的详细介绍相信您对什么是函数就会有更加深刻的了解。须要注意的是这里的ABI规则是指基于OC语言实现的程序的ABI规则,这些规则并不适用于经过Swift实现的程序以及不适用于Linux等其余操做系统的ABI规则。github

因为内容过多所以我将分为两篇文章来作具体介绍,前一篇文章介绍函数接口相关的内容,后一篇文章介绍函数实现相关的内容。数组

2、函数调用

CPU中的程序计数器(IP/PC)中老是保存着下一条将要执行的指令的内存地址,这样每执行一条指令就会更新程序计数器中的值,从而能够继续执行下一条指令。系统就是这样经过不停的变化程序计数器中的值来实现程序指令的执行的。通常状况下程序计数器中的值老是按照程序指令顺序更新,只有在执行跳转指令和函数调用指令时才会打破执行的顺序。bash

函数调用的本质就是将函数在内存中的首地址赋值给程序计数器(IP/PC),这样下一条执行的指令就变为了函数首地址处的指令,从而实现函数的调用。除了要更新程序计数器的值外还须要保存调用现场,以便当函数调用返回后继续执行函数调用的下一条指令,因此这里所谓的保存调用现场就是将函数调用的下一条指令的地址保存起来。不一样的CPU体系都提供了特定的函数调用指令来实现函数调用的功能。好比x86系统提供一条称之为call的指令来实现函数调用,call指令除了会更新程序计数器的值外还会把函数调用的下一条指令压入到栈中进行保存;arm系统则提供b系列的指令来实现函数调用,b系列指令除了会更新程序计数器的值外还会把函数调用的下一条指令保存到LR寄存器中。app

函数返回的本质就是将前面说到的保存的调用现场地址赋值给程序计数器,这样下一条执行的指令就变为了调用者调用被调函数的下一条指令了。不一样的CPU体系也都提供了特定的函数返回指令来实现函数返回的功能(arm32位系统除外)。好比x86系统提供一条称之为ret的指令来实现函数返回,此指令会将栈顶保存的地址赋值给程序计数器而后执行出栈操做;arm64位系统也提供一条ret指令来实现函数的返回,此指令则会把当前的LR寄存器的值赋值给程序计数器。函数

对于x86系统来讲由于执行函数调用前会将调用者的下一条指令压入栈中,而被调用者函数内部由于有本地栈帧(stack frame)的定义又会将栈顶下移,因此在被调用者函数执行ret指令返回以前须要确保当前堆栈寄存器SP所指向的栈顶地址要和被调用函数执行前的栈顶地址保持一致,否则当ret指令执行时取出的调用者的下一条指令的值将是错误的,从而会产生崩溃异常。布局

对于arm系统来讲由于LR寄存器只有一个,所以若是被调用函数内部也调用其余函数时也会更新LR寄存器的值,一旦LR寄存器被更新后将没法恢复正确的调用现场,因此通常状况下被调用函数的前几条指令作的事情就是将LR寄存器的值保存到栈内存中,而被调用函数的最后几条指令所的事情就是将栈内存中保存的内容恢复到LR寄存器。post

有一种特殊的函数调用场景就是当函数调用发生在调用者函数的最后一条指令时,则不须要进行调用现场的保护处理,同时也会将函数调用指令改成跳转指令,缘由是由于调用者的最后一条指令再无下一条有效的指令,而仍然采用调用指令的话则保存的调用现场则是个无效的地址,这样当函数返回时将跳转到这个无效的地址从而产生执行异常!ui

为了更好的描述函数的调用规则,假设A函数内部调用了B函数和C函数,下面定义了各函数的地址,以及函数调用处的地址,以及函数调用的伪代码块:

//这里的XX,YY,ZZ表明的是函数指令在内存中的地址。
A  XX1:   
    XX2:   调用B函数地址YY1
    XX3:
    XX4:
    XXn:  跳转到C函数ZZ1
    
B  YY1:
    YY2:
    YY3:
    YYn:  返回
    
C  ZZ1:
    ZZ2:
    ZZ3:
    ZZn:  返回
复制代码

1. x86_64体系下的函数调用规则

1.1 函数的调用

函数调用的指令是call 指令。在汇编语言中call 指令后面的操做数是调用的目标函数的绝对地址,而实际的机器指令中的操做数则是一个相对地址值,这个地址值是目标函数地址距离当前指令地址的相对偏移值。不管是x86系统仍是arm系统若是指令中的操做数部分的值是内存地址的话,通常都是相对当前指令的偏移地址而不是绝对地址。下面就是函数调用指令以及其内部实现的等价操做。

call YY1   <==>   RIP = YY1,   RSP = RSP-8,  *RSP = XX3
复制代码

也就是说执行一条函数调用指令等价于将指令中的地址赋值给IP寄存器,同时把函数的返回地址压入栈寄存器中去。

1.2 函数的跳转

函数跳转的指令是jmp指令。在汇编语言中jmp 指令后面的操做数是调用的目标函数的绝对地址,而实际的机器指令中的操做数则是一个相对地址值,这个地址值是目标函数地址距离当前指令地址的相对偏移值,下面就是函数跳转指令以及其内部实现的等价操做。

jmp ZZ1  <==>  RIP = ZZ1
复制代码

也就是说执行一条跳转指令等价于将指令中的地址赋值给IP寄存器。

1.3 函数的返回

函数返回的指令是ret指令。ret指令后面通常不跟操做数,下面就是函数返回指令以及其内部实现的等价操做。

ret   <==>   RIP = *RSP,   RSP = RSP + 8
复制代码

也就是说执行一条ret指令等价于将当前栈寄存器中的值赋值给IP寄存器,同时栈寄存器执行POP操做。

2. arm32位体系下的函数调用规则

2.1 函数的调用

函数的调用指令为bl/blx。 这两条指令的操做数能够是相对地址偏移也能够是寄存器。bl/blx的区别就是bl函数调用不会切换指令集,而blx调用则会从thumb指令集切换到arm指令集或者相反切换。arm32系统中存在着两套指令集即thumb指令集和arm指令集,其中的arm指令集中的全部的指令的长度都是32位而thumb指令集则存在着32位和16位两种长度的指令集。两种指令集是以函数为单位进行使用的,也就是说一个函数中的全部指令要么都是arm指令要么就都是thumb指令。正是由于如此若是调用者函数和被调用者函数之间用的是不一样的指令集则须要经过blx来执行函数调用,而若是两者所用的指令集相同则须要经过bl指令来执行调用。下面就是函数调用指令以及其内部实现的等价操做。

bl/blx  YY1  <==>  PC = YY1,  LR = XX3
复制代码

也就是说执行一条函数调用指令等价于将指令中的地址赋值给PC寄存器,同时把函数的返回地址赋值给LR寄存器中去。

2.2 函数的跳转

函数的跳转指令是b/bx, 这两条指令的操做数能够是相对地址偏移也能够是寄存器,b/bx的区别就是b函数调用不会切换指令集。下面就是函数跳转指令以及其内部实现的等价操做。

b/bx ZZ1   <==>  PC = ZZ1
复制代码

也就是说跳转指令等价于将指令中的地址赋值给PC寄存器。

2.3 函数的返回

arm32位系统没有专门的函数返回ret指令,由于arm32位系统能够直接修改PC寄存器的值,因此函数返回能够直接给PC指令赋值,也能够经过调用b/bx LR 来实现函数的返回处理。

b/bx LR
//或者
mov PC, XXX
复制代码

arm32位系统能够直接修改PC寄存器的值,所以函数返回时能够直接设置PC寄存器的值为函数的返回地址,也能够执行b/bx跳转指令并指定目标地址为LR寄存器中的值。

3.arm64位体系下的函数调用规则

3.1 函数的调用

函数调用的指令是bl/blr 其中bl指令的操做数是距离当前位置相对距离的偏移地址,blr指令的操做数则是寄存器,代表调用寄存器所指定的地址。由于bl指令中的操做数部分是函数的相对偏移地址,又由于arm64位系统的一条指令占用4个字节,根据指令的定义bl指令所能跳转的范围是距离当前位置±32MB的范围,因此若是要跳转到更远的地址则须要借助blr指令。 下面就是函数调用指令以及其内部实现的等价操做。

//若是YY1地址离调用指令的距离是在±32MB内则使用bl指令便可。
 bl YY1 <==>  PC = YY1, LR = XX3

//若是YY1地址离调用指令的距离超过±32MB则使用blr指令执行间接调用。
ldr  x16,  YY1
blr  x16
复制代码

也就是说执行一条函数调用指令等价于将指令中的地址赋值给PC寄存器,同时把函数的返回地址赋值给LR寄存器中去。

3.2函数的跳转

函数跳转的指令是b/br, 其中b指令的操做数是距离当前位置相对距离的偏移地址,br指令的操做数则是寄存器,代表跳转到寄存器所指定的地址中去。下面就是函数跳转指令以及其内部实现的等价操做。

b ZZ1   <==>  PC = ZZ1
复制代码

也就是说跳转指令等价于将指令中的地址赋值给PC寄存器。

3.3 函数的返回

函数返回的指令是 ret, 下面就是函数返回指令以及其内部实现的等价操做。

ret  <==>   PC = LR
复制代码

也就是说执行一条ret指令等价于将LR寄存器中的值赋值给PC寄存器。

3、函数参数传递

某些函数定义中有参数须要传递,须要由调用者函数将参数传递给被调用者函数,所以在调用这类函数时,须要在执行函数调用指令以前,进行函数参数的传递。函数的参数个数能够为0个,也能够为某个固定的数量,也能够为任意数量(可变参数)。 函数的每一个参数类型能够是整型数据类型,也能够是浮点数据类型,也能够是指针,也能够是结构体。所以在函数传递的规则上须要明确指出调用者应该如何将参数进行保存处理,而被调用者又是从什么地方来获取这些外部传递进来的参数值。不一样体系下的系统会根据参数定义的个数和类型来制定不一样的规则。通常状况下各系统都会约定一些特定的寄存器来进行参数传递交换,或者使用栈内存来进行参数传递交换。

1. x86_64体系下的参数传递规则

1.1 常规类型参数

这里面的常规类型参数是指除浮点和结构体类型之外的参数类型,下面就是常规参数传递的规则:

  • R1: 若是函数没有参数则除了进行执行函数调用外不作任何处理,若是函数有参数则在执行函数调用指令以前须要按下面的规则设置参数值。

  • R2: 若是函数的参数个数<=6,则参数传递时将按照从左往右的定义的顺序依次保存到RDI, RSI, RDX, RCX, R8, R9这6个寄存器中。

  • R3: 若是参数的个数>6, 那么超过6个的参数,将会按从右往左的顺序依次压入到栈中。(由于栈是从高地址往低地址递减的,因此从栈顶往上来算的话后面的参数依然是从左到右的顺序)

  • R4: 若是每一个参数的类型的尺寸<8个字节的状况下,则前6个参数会分别保存在上述寄存器的对应的32位或者16位或者8位版本的寄存器中。

下面是几个函数的定义以及在执行这个函数调用和参数传递的实现规则(下面代码块中上面部分描述的函数接口,下面部分是函数调用ABI规则):

//函数的签名
void foo1(long, long);
void foo2(long, long, long, long, long, long);
void foo3(long, long, long, long, long, long, long, int, short);

//高级语言的函数调用以及对应的机器指令伪代码实现
foo1(a,b)  <==> RDI = a, RSI = b, call foo1
foo2(a,b,c,d,e,f) <==>  RDI = a, RSI = b, RDX = c, RCX = d, R8 = e, R9 = f, call foo2
foo3(a,b,c,d,e,f,g,h,i) <== > RDI = a, RSI = b, RDX = c, RCX = d, R8 = e, R9 = f,  RSP -= 2, *RSP = i,  RSP-=4, *RSP = h,  RSP-=8, *RSP = g,  call foo3 
复制代码

1.2 浮点类型参数

若是函数参数中有浮点数(不管是单精度仍是双精度)类型。则参数保存的地方则不是通用寄存器,而是特定的浮点数寄存器。下面就是传递的规则:

  • R5: 若是浮点数参数的个数<=8,那么参数传递将按从左往右的定义顺序依次保存到 XMM0 - XMM7这8个寄存器中。

  • R6: 若是浮点数参数个数>8,那么超过数量部分的参数,将会按从右往左的顺序依次压入到栈中。

  • R7: 若是函数参数中既有浮点也有常规参数那么保存到寄存器中的顺序和规则不会相互影响。

  • R8: 若是参数类型是扩展浮点类型(long double),扩展浮点类型的长度是16个字节, 那么全部的long double类型的参数都将直接压入到栈(注意这个栈不是浮点寄存器栈)中而不存放到浮点寄存器中。

下面是几个函数的例子:

//函数签名
void foo4(double, double);
void foo5(double, float, double, double, double, double, double, double, float, double);
void foo6(long, double, long, double, long, long, double);
void foo7(double, long double, long);

//高级语言的函数调用以及对应的机器指令伪代码实现
foo4(a,b) <==> XMM0 = a,  XMM1 = b,  call foo4
foo5(a,b,c,d,e,f,g,h,i,j) <==> XMM0 = a, XMM1 = b, XMM2 = c, XMM3 = d, XMM4 = e, XMM5 = f,  XMM6 = g, XMM7 = h,  RSP-=8,  *RSP = j,   RSP-=4  *RSP = i,  call foo5
foo6(a,b,c,d,e,f,g)  <==> RDI = a, XMM0 = b,  RSI = c,  XMM1 = d,  RDX = e, RCX = f,  XMM2 = g,  call foo6
foo7(a,b,c) <==> XMM0=a, RSP-=16, *RSP = b的低8字节, *(RSP+8) = b的高8字节, RDI = c,  call foo7

复制代码

1.3 结构体参数

针对结构体类型的参数,须要考虑结构体中的成员的数据类型以及结构体的尺寸两个因素。这里的结构体的尺寸分为:小于等于8字节、小于等于16字节、大于16字节三种。而结构体成员类型组成则分为:所有都是常规数据类型、所有都是浮点数据类型(不包括long double)、以及混合类型三种。这样一共分为9种组合状况,下面表格描述结构体参数的的传递规则:

  • R9:
类型/尺寸 <=8 <=16 >16
所有都是常规数据类型 6个通用寄存器中的某一个 6个通用寄存器中的某连续两个 压入栈内存中
所有都是浮点数据类型 8个浮点寄存器中的某一个 8个浮点寄存器中的某连续两个 压入栈内存中
混合类型 优先考虑通用寄存器,再考虑浮点寄存器,以及成员排列的顺序 参考左边 压入栈内存中
  • R10: 小于等于16个字节的结构体保存到寄存器中的规则并非按每一个数据成员来分别保存到寄存器,而是按结构体中的内存布局边界顺序以8字节为分割单位来保存到寄存器中的。

  • R11: 若是参数中混合有结构体、常规参数、浮点参数则按照前10个规则分别保存传递的参数

下面就是几个结构体在当作参数时的示例代码:

//长度<=8个字节的结构体
struct S1
{
    char a;
    char b;
    int c;
};

//长度<=16的混合结构体
struct S2
{
   float a;
   float b;
   double c;
};

//长度<=16的混合结构体
struct S3
{
  int a;
  int b;
  double c;
};

//长度>16个字节的结构体
struct S4
{
   long a;
   long b;
   double c;
}

 //函数签名
 void foo8(struct S1);
 void foo9(struct S2);
 void foo10(struct S3);
 void foo11(struct S4);


//高级语言的函数调用以及对应的机器指令伪代码实现
struct S1 s1;
struct S2 s2;
struct S3 s3;
struct S4 s4;
foo8(s1)  <==> RDI = s1.a | (s1.b <<8) | (s1.c << 32), call foo8
foo9(s2)  <==> XMM0 = s2.a | (s2.b << 32), XMM1 = s2.c, call foo9
foo10(s3) <==>  RDI = s3.a | (s3.b << 32), XMM0 = s3.c, call foo10
foo11(s4) <==>  RSP -= 24, *RSP = s4.a, *(RSP+8) = s4.b, *(RSP+16)=s4.c, call foo11

复制代码

针对结构体类型的参数建议是传指针而不是传结构体值自己。

1.4 可变参数

可变参数函数由于其参数的类型和参数的数量不固定,因此系统在编译时会根据函数调用时传递的参数的值类型而进行不一样的处理,所以规则以下:

  • R12: 函数调用时会根据传递的参数的数量和类型从左到右依次存放在对应的6个常规参数传递的寄存器或者XMM0-XMM7中,若是数量超过规定则剩余的参数依次压入栈内存中。

  • R13:对于可变参数函数的调用会使用AL寄存器,其规则为:若是传递的可变参数中没有浮点数类型则AL寄存器被设置为0,若是可变参数中出现了浮点数类型则AL寄存器会被设置为1。之因此用AL寄存器来标志的缘由是可变参数内部实现由于不知道外部会传递什么类型的参数以及参数的个数,因此内部实现中会将全部做为参数传递的常规寄存器和做为参数传递的浮点数寄存器都会保存到一个数组中去,以方便进行处理。所以这里借助这个AL寄存器来判断是否有浮点就能够在必定程度上减小将数组的长度。

    下面是可变参数的调用示例:

//函数签名
void foo12(int a, ...);

//高级语言的函数调用以及对应的机器指令伪代码实现
foo12(10,20,30.0, 40)  <==> RDI = 10,  RSI = 20, XMM0 = 30.0,  RDX = 40,AL=1,  call foo12
foo12(10,20,30,40)  <==> RDI = 10,  RSI = 20, RDX = 30,  RCX = 40,AL=0,  call foo7
复制代码

一个有意思的例子: 当调用printf函数传递的参数以下:

printf("%f,%d,%d", 10, 20.0, 30.0);       //输出的结果将是: 20.0,10, ???  
复制代码

缘由就是参数传递的规则和格式字符串不匹配致使的,经过上面对可变参数的传递规则,你能解释为何吗?

2. arm32位体系下的参数传递规则

整个arm32位体系下的参数传递和参数返回都不会用到浮点寄存器。对于大于4字节的基本类型则会拆分为两部分依次保存到连续的两个寄存器中。

2.1 常规参数

  • R1: 对于32位的常规参数,若是数量<=4则分别保存到 R0 - R3中, 若是数量>4则剩余的参数从右往左分别压入栈内存中。

  • R2: 若是参数中有64位的参数好比long long 类型,则参数会占用2个寄存器,其中低32位部分保存在前一个寄存器,高32位部分保存在后一个寄存器。

  • R3: 若是前面3个参数是32位的参数,而第四个参数是64位的参数,那么前面三个参数分别放入R0,R1,R2中,而第四个参数的低32位部分则放入R3中,高32位部分则压入到栈内存中。

2.2 浮点参数

  • R4: 浮点参数和常规参数同样使用R0到R3寄存器,对于单精度浮点则使用一个寄存器,而双精度浮点则使用两个寄存器。超出部分则压入栈内存中。

2.3 结构体参数

  • R5: arm32位系统的结构体不区分红员数据类型,只区分结构体尺寸,系统根据结构体的内存布局以4个字节为分割单位保存到寄存器或者栈内存中。

  • R6: 结构体尺寸<=4则会将参数保存到一个寄存器中,若是尺寸<=8则保存到连续的两个寄存器中, 若是尺寸<=12则保存到3个连续的寄存器中, 若是尺寸<=16则保存到4个连续的寄存器中。若是尺寸>16则保存到栈内存中去。

  • R7: 若是前3个参数都是32位的参数,而第4个参数为尺寸>4的结构体,那么第4个参数的低4个字节的部分会保存到R3中,其余部分保存到栈内存中。

2.4 可变参数

  • R8: 可变参数传递根据参数的个数从左到右依次保存到R0-R3四个寄存器中,超过的部分从右往左依次保存到栈内存中。 下面的实例代码:
//函数签名
void foo1(int a, ...);

//高级语言的函数调用以及对应的机器指令伪代码实现。
foo1(10,20,30,40,50)  <==> R0 = 10,  R1 = 20, R2 = 30, R3 =40,  SP -=4,  *SP = 50,  bl foo1

复制代码

3.arm64位体系下的参数传递规则

3.1 常规参数

这里面的常规参数是指参数的类型是非浮点和非结构体类型的参数,下面就是常规参数传递的规则:

  • R1: 若是函数没有参数则除了进行执行函数调用外不作任何处理,若是函数有参数则在执行函数调用指令以前须要按下面的规则设置参数值。

  • R2: 若是函数的参数个数<=8个, 参数传递将按照从左往右的定义的顺序依次保存到X0 - X7 这8个寄存器中。

  • R3: 若是参数的个数>8个,那么超过数量部分的参数,将会按从右往左的顺序依次压入到栈中。

  • R4: 若是参数的类型是小于8个字节的状况下,则前8个参数会分别保存在对应的32位或者16位或者8位寄存器中。

下面是几个函数的例子:

//函数签名
void foo1(long, long);
void foo2(long, long, long, long, long, long, long, long);
void foo3(long, long, long, long, long, long, long, long, long, int, short);


//高级语言的函数调用以及对应的机器指令伪代码实现。
foo1(a,b) <==> X0 = a, X1 = b,  bl foo1
foo2(a,b,c,d,e,f,g,h) <==>X0 = a, X1 = b, X2 = c, X3 = d, X4 = e, X5 = f,  X6=g, X7 =h,  bl foo2
foo3(a,b,c,d,e,f,g,h,i,j,k) <==>X0 = a, X1 = b, X2 = c, X3 = d, X4 = e, X5 = f,  X6=g, X7=h,  *SP -=2,  *SP=k,  SP-=4, *SP = j,  SP-= 8,  *SP = i,  bl foo3 

复制代码

3.2 浮点参数

若是函数参数中有浮点数(不管是单精度仍是双精度)。则参数保存的地方则不是通用寄存器,而是特定的浮点数寄存器。系统提供32个128位的浮点寄存器Q0-Q31(V0-V31),其中的低64位则被称为D0-D31,其中的低32位则被称为S0-S31,其中的低16位则被称为H0-H31,其中的低8位则被称之为B0-B31。 也就是说单精度浮点保存到S开头的寄存器, 双精度浮点保存到D开头的寄存器。 arm系统中 long double 的长度都是8字节,所以可被当作双精度浮点。

下面就是传递的规则:

  • R5: 若是浮点数参数的个数<=8个,那么参数传递将按从左往右的顺序依次保存到 D0-D7或者S0-S7 这8个寄存器中。

  • R6: 若是浮点数参数个数>8个时,那么超过数量部分的参数,将会按从右往左的顺序依次压入到栈中。

  • R7: 若是函数参数中既有浮点也有常规参数那么保存到寄存器中的顺序和规则不会相互影响。

下面是几个函数的例子:

//函数签名
void foo4(double, double);
void foo5(double, float, float, double, double, double, double, double, double, double);
void foo6(long, double, long, double, long, long, double);

//高级语言的函数调用以及对应的机器指令伪代码实现。
foo4(double a, double b) <==> D0 = a,  D1 = b,  bl foo4
foo5(double a, float b, float c, double d, double e, double f, double g, double h, double i, double j) <==> D0 = a, S1 = b, S2 = c, D3 = d, D4 = e, D5 = f,  D6 = g, D7 = h,    *SP -=8,  *SP = j,   *SP -=8,  *SP = i,  bl foo5
foo6(long a, double b, long c, double d, long e, long f, double g) <==> X0 = a, D0 = b,  X1 = c,  D1 = d,  X2 = e, X3 = f,  D2 = g,  bl foo6

复制代码

3.3 结构体参数

针对结构体类型的参数,须要考虑结构体的尺寸以及数据类型和数量。这里的结构体的尺寸分别是考虑小于等于8字节,小于等于16字节,大于16字节。而结构体成员类型则分为:所有都是非浮点数据成员、所有都是浮点数成员(这里会区分单精度和双精度)、以及混合类型的成员(若是结构体中有单精度和双精度都算混合)。下面是针对结构体参数的规则:

  • R8: 若是数据成员所有都是非浮点数据成员则 若是尺寸<=8则会将值保存到X0-X8中的某一个寄存器中, 若是尺寸<=16则会将值保存到X0-X8中的某两个连续的寄存器中,若是尺寸>16则结构体将再也不按值传递而是以指针的形式进行传递并保存到X0-X8中的某一个寄存器中。

  • R9: 若是数据成员所有都是单精度浮点成员则若是成员数量<=4则会将数据成员保存到S0-S7中的某4个连续的浮点寄存器中,若是数量>4则结构体将再也不按值传递而是以指针的形式进行传递并保存到X0-X8中的某一个寄存器中。

  • R10: 若是数据成员所有都是双精度浮点成员则若是成员数量<=4则会将数据成员保存到D0-D7中的某4个连续的浮点寄存器中,若是数量>4则结构体将再也不按值传递而是以指针的形式进行传递并保存到X0-X8中的某一个寄存器中。

  • R11: 若是数据成员是混合类型的则若是尺寸<=8则保存到X0-X8中的某一个寄存器中,若是尺寸<=16则保存到X0-X8中的某两个连续的寄存器中, 若是尺寸>16则结构体将再也不按值传递而是以指针的形式进行传递并保存到X0-X8中的某一个寄存器中。

  • R12: 由于结构体参数的寄存器规则会影响到上述非结构体参数的传递规则,所以必定程度上能够将结构体当作多个参数传递来看待。

下面是演示的代码:

//长度<=8个字节的结构体
struct S1
{
    char a;
    char b;
    int c;
};

//长度<=16的单精度浮点结构体
struct S2
{
   float a;
   float b;
   float c;
};

//长度<=16的混合结构体
struct S3
{
  int a;
  int b;
  double c;
};

//长度>16个字节的结构体
struct S4
{
   long a;
   long b;
   double c;
}

 //函数签名
 void foo8(struct S1);
 void foo9(struct S2);
 void foo10(struct S3);
 void foo11(struct S4);


//高级语言的函数调用以及对应的机器指令伪代码实现
struct S1 s1;
struct S2 s2;
struct S3 s3;
struct S4 s4;
foo8(s1)  <==>  X0= s1.a | (s1.b <<8) | (s1.c << 32), bl foo8
foo9(s2)  <==> S0 = s2.a, S1 = s2.b, S3 = s2.c  bl foo9
foo10(s3) <==>  X0 = s3.a | (s3.b << 32), X1 = s3.c, bl foo10
foo11(s4) <==>  X0 = &s4, bl foo11

复制代码

3.4 可变参数

可变参数函数由于其参数的类型和参数的数量不固定,因此系统在编译时会根据函数调用时传递的参数的值类型而进行不一样的处理,所以规则以下:

  • R13: 函数调用时会根据传递的参数的数量和类型来决定,其中明确类型的部分按照上面介绍的规则进行传递,而可变部分则从右往左依次压入到堆栈中。

下面是示例代码:

//函数签名
void foo7(int a, ...);

//高级语言的函数调用以及对应的机器指令伪代码实现
foo7(10, 20, 30.0, 40)  <==> X0 = 10,   SP-=8,  *SP = 40,  SP-=8,  *SP = 30.0,  SP-=8,  *SP = 20, bl foo7

复制代码

一个有意思的例子: 当执行printf函数而传递参数以下:

printf("%f,%d,%d", 10, 20.0, 30.0);     //那么输出的结果将是: ?,?,?     
复制代码

由于arm系统对可变参数的传递和x86系统对可变参数的处理不一致,就会出现真机和模拟器的结果不一致的问题。甚至在参数传递规则上arm32位和arm64位系统都有差别。上面的参数传递和描述不匹配的状况下你能够说出为何输出的结果不肯定吗?

4、函数返回值

函数调用除了有参数传递外,还有参数返回。参数的传递是调用者向被调函数方向的传递,而函数的返回则是被调用函数向调用函数方向的传递,所以调用者和被调用者之间应该造成统一的规则。被调用函数内对返回值的处理应该在被调用函数返回指令执行前。而调用函数则应该在函数调用指令的下一条指令中尽量早的对返回的结果进行处理。函数的返回类型有无、非浮点数、浮点数、结构体四种类型,所以针对不一样的返回类型系统有不一样的处理规则。

1. x86_64体系下的函数返回值规则

1.1 常规类型返回

  • R1: 若是函数有返回值则老是将返回值保存到RAX寄存器中。

1.2 浮点类型返回

  • R2: 返回的浮点数类型保存到XMM0寄存器中。

  • R3: 返回的(扩展双精度)long double 类型则保存到浮点寄存器栈顶中。FPU计算单元中提供了8个独立的128位的寄存器STMM0-STMM7,这8个寄存器以堆栈形式组织在一块儿,统称为浮点寄存器栈。系统同时也提供了专门的指令来对浮点寄存器栈进行入栈和出栈处理, 编写浮点指令时这些寄存器也写做st(x),这里的x是浮点寄存器的索引。须要明确的是XMM系列的寄存器和STMM系列的寄存器是彻底不一样的两套寄存器。

1.3 结构体类型返回

针对结构体类型的返回,须要考虑结构体的尺寸以及成员的数据类型。这里的结构体的尺寸分为:小于等于8字节,小于等于16字节,大于16字节。而结构体成员类型则分为:所有都是非浮点数据成员、所有都是浮点数据成员(不包括 long double)、以及混合类型的成员。这样一共分为9种状况,下面表格描述针对结构体返回的规则:

  • R4
类型/尺寸 <=8 <=16 >16
所有非浮点数据成员 RAX RAX,RDX 返回的结构体将保存到RDI寄存器所指向的内存地址中。也就是RDI寄存器是一个结构体地址指针,这样函数参数中的第一个参数将由保存到RDI,变为保存到RSI寄存器了。
所有为浮点数据成员 XMM0 XMM0,XMM1 同上
混合类型 优先存放到RAX,或者XMM0,而后再存放到RDX或者XMM1中。一个特殊状况就是若是成员中有long double类型,则老是按>16字节的规则来处理返回值 同左 同上

下面是一个展现的代码:

//长度<=8个字节的结构体
struct S1
{
    char a;
    char b;
    int c;
};

//长度<=16的混合结构体
struct S2
{
  int a;
  int b;
  double c;
};

//长度>16个字节的结构体
struct S3
{
   long a;
   long b;
   double c;
}

 //函数签名
 struct S1 foo1();
 struct S2 foo2();
 struct S3 foo3(int );

//高级语言的函数调用以及对应的机器指令伪代码实现
struct S1 s1 = foo1()  <==>  函数调用时:call foo1,  函数返回时 s1 = RAX
struct S2 s2 = foo2() <==> 函数调用时:call foo2, 函数返回时s2.a&s2.b = RAX, s2.c = XMM0
struct S3 s3 = foo3(a)  <==> 函数调用时: RDI = &s3, RSI = a, call foo3


复制代码

2. arm32位体系下的函数返回值规则

2.1 常规类型返回

  • R1: 函数的返回值的尺寸<=4字节则保存到R0寄存器,若是返回值的尺寸<=8字节(好比 long long类型)则保存到R0,R1寄存器其中低32位保存到R0,高32位保存到R1

2.2 浮点类型返回

  • R2: 单精度浮点数保存到R0寄存器,双精度浮点数保存在R0,R1中其中R0保存低32位,R1保存高32位。 long double 类型的返回同双精度浮点返回一致。

2.3 结构体类型返回

  • R3: 无论任何类型的结构体,老是将结构体返回到R0寄存器所指向的内存中, 所以R0寄存器中保存的是一个指针,这样函数的第一个参数将保存到R1寄存器并依次日后推,也就是说若是函数返回的是一个结构体那么系统就会将返回的值当作第一个参数,而将真实的第一个参数当作第二个参数。

下面的代码说明了这种状况:

struct XXX
{
  //任意内容
};

//函数返回结构体
struct XXX foo(int a)
{
   //...
}

实际在编译时会转化为函数
void foo(struct XXX *pret, int a)
{
}
复制代码

也就是在arm32位的系统中凡有结构体做为返回的函数,其实都会将结构体指针做为函数调用的第一个参数保存到R0中,而将源代码中的第一个参数保存到R1中。

3.arm64位体系下的函数返回值规则

2.1 常规类型返回

  • R1: 函数的返回参数保存到X0寄存器上

2.2 浮点类型返回

  • R2: 单精度浮点返回保存到S0,双精度浮点返回保存到D0

2.3 结构体类型返回

针对结构体类型的参数,须要考虑结构体中的成员的数据类型以及总体结构体的尺寸。这里的结构体的尺寸分别是考虑小于等于8字节,小于等于16字节,大于16字节。而结构体成员类型则分为:所有都是非浮点数据成员、所有都是浮点数成员(这里会区分单精度和双精度)、以及混合类型的成员(若是结构体中有单精度和双精度都算混合)。这样一共分为9种情,下面就是针对结构体类型返回的规则:

  • R3:针对非浮点数据成员的结构体来讲若是结构体的尺寸<=8,那么结构体的值会保存到X0, 若是尺寸<=16,那么保存到X0,X1中,若是尺寸>16则结构体返回会保存到X8寄存器所指向的内存中,也就是X8寄存器比较特殊,专门用来保存返回的结构体的指针。

  • R4: 若是结构体的成员都是单精度而且数量<=4 则返回结构体的每一个成员分别保存到S0,S1,S2, S3四个寄存中,若是结构体成员数量超过4个则结构体返回会保存到X8寄存器所指向的内存中。

  • R5: 若是结构体的成员都是双精度而且数量<=4 则返回结构体的每一个成员分别保存到D0,D1,D2,D3四个寄存器中,若是结构体成员数量超过4个则结构体返回会保存到X8寄存器所指向的内存中。

  • R6: 若是结构体是混合型数据成员,而且结构体的尺寸<=8字节,那么结构体的值保存到X0, 若是尺寸<=16字节则保存到X0,X1中,若是尺寸>16则结构体返回会保存到X8寄存器所指向的内存中。

下面演示几个结构体定义以及返回结构体的函数:

//长度为16字节的结构体
struct S1
{
   char a;
   char b;
   double c;
};

 //长度超过16字节的混合成员结构体
struct S2
{
   int a;
   int b;
   int c;
   double d;
};

//长度小于等于8字节的结构体
struct S3
{
   int a;
   int b;
};


CGRect  foo1()
{
      //高级语言实现的返回
      return CGRectMake(10,20,30,40);
     //机器指令的函数返回的伪代码以下:
    /* 
      D0 = 10
      D1 = 20
      D2 = 30
      D3 = 40
     ret
     */
}

struct S1 foo2()
{
    //高级语言实现的返回
    return (struct S1){10, 20, 30};
   //机器指令的函数返回的伪代码以下:
    /* 
      X0 = 10 |  20 << 8
      X1 = 30
     ret
     */

}

struct S2 foo3()
{
   //高级语言实现的返回
   return (struct S2){10, 20, 30, 40};
  //机器指令的函数返回的伪代码以下:
  /*
     struct S2 *p = X8     //X8中保存返回的结构体内存地址
     p->a = 10
     p->b = 20
     p->c = 30
     p->d = 40
     ret
  */
  
}

struct S3 foo4()
{
   //高级语言实现的返回
   return (struct S3){20, 30};
  //机器指令的函数返回的伪代码以下:
  /*
        X0 = 20 | 30 << 32
        ret
  */
}

复制代码

从上面的代码能够看出来在x86_64/arm32两种体系结构下若是返回的类型是结构体而且知足特定要求时,系统会将结构体指针当作函数的第一个参数,而将源代码中的第一个参数传递的寄存器日后移动,而在arm64位系统中则x8寄存器专门负责处理返回值为特殊结构体的状况。

6、谈谈objc_msgSend系列函数

全部的OC方法最终都会经过objc_msgSend系列函数进行调用。这个函数系列有以下函数:

objc_msgSend(void /* id self, SEL op, ... */ )
objc_msgSend_stret(void /* id self, SEL op, ... */ )
objc_msgSend_fpret(void /* id self, SEL op, ... */ )
objc_msgSend_fp2ret(void /* id self, SEL op, ... */ )
复制代码

这一系列的函数的差异主要是针对返回类型的不一样而使用不一样的消息发送函数。

从上述的函数返回值规则能够看对于long double 类型的函数返回在x86_64位系统的处理方式比较特殊,其返回的值将保存在特定的浮点堆栈寄存器中,因此objc_msgSend_fpret函数只用在x86_64位系统中返回类型为long double的OC方法的消息分发中,其余体系结构都不会用到这个函数。一样由于C99中引入了复数类型 _Complex 关键字,因此针对这种类型的 long double 返回会使用objc_msgSend_fp2ret函数。

从上述的函数的返回值规则还能够看出对于结构体返回,若是结构体尺寸大于必定的阈值后,x86_64位系统和arm32位系统都会将返回的结构体转化为第一个参数来进行传递,这样就会使得真实的参数传递的寄存器日后顺延,而arm64则直接只用x8寄存器来保存大于阈值的结构体指针且并不会影响到参数的传递顺序。所以除了arm64位系统外其余体系结构系统中针对那些返回结构体大于必定阈值的OC方法将使用objc_msgSend_stret函数进行消息分发。

上述的函数返回规则对<objc/message.h> 中的其余函数也是一样适用的。


针对函数的调用、参数传递、函数的返回值的介绍规则就是这些了,固然这些规则除了对普通函数适用外对OC类方法也是一样适用的。至于一个函数内部应该怎样实现,其实也是有必定的规则的。经过这些规则你能够了解到函数是如何跟栈内存结合在一块儿的,以及函数调用栈是如何被构造出来的,你还能够了解为何一些函数调用不会出如今调用栈中等等相关的知识,以及可变参数函数内部是如何实现的等等这部分的详细介绍将会在: 深刻iOS系统底层之函数(二):实现 进行深刻的探讨。

7、参考

👉【返回目录


欢迎你们访问个人github地址

相关文章
相关标签/搜索