首页 DB 正文
深入iOS系统底层之函数调用

 2020-09-16    38  

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

一、什么是函数

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

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

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

二、函数调用

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

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

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

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

对于arm系统来说因为LR寄存器只有一个,因此如果被调用函数内部也调用其他函数时也会更新LR寄存器的值,一旦LR寄存器被更新后将无法恢复正确的调用现场,所以一般情况下被调用函数的前几条指令做的事情就是将LR寄存器的值保存到栈内存中,而被调用函数的最后几条指令所的事情就是将栈内存中保存的内容恢复到LR寄存器。

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

为了更好的描述函数的调用规则,假设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寄存器。

三、函数参数传递

某些函数定义中有参数需要传递,需要由调用者函数将参数传递给被调用者函数,因此在调用这类函数时,需要在执行函数调用指令之前,进行函数参数的传递。函数的参数个数可以为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 foo1foo2(a,b,c,d,e,f) <==>  RDI = a, RSI = b, RDX = c, RCX = d, R8 = e, R9 = f, call foo2foo3(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类型的参数都将直接压入到栈(注意这个栈不是浮点寄存器栈)中而不存放到浮点寄存器中。


  •  标签:  

原文链接:http://51heixiazi.com/?id=10

=========================================

http://51heixiazi.com/ 为 “黑匣子经验分享” 唯一官方服务平台,请勿相信其他任何渠道。