在2、函数调用栈的最后我们提到了<函数的参数和返回值>。我们讲到了:
-
ARM64
中,函数的参数
是存放在x0 ~ x7(w0 ~ w7)
这8个寄存器里面的。如果超过8个参数,就会入栈。 -
函数的返回值
是放在x0
寄存器里面的。
这里我们来详细的探讨一下:
i
:函数的参数超过8个,就会入栈
;那入的是哪个栈
?
ii
:函数的返回值
是放在x0
寄存器里面;那如果返回值超过8个字节呢,此时返回值放在哪里呢?
-
i
:函数的参数超过8个,就会入栈
;那入的是哪个栈
?
我们给函数设置9个返回值:
⊙0x102189e88 <+100>: mov x8, sp
: 将sp
的内容复制给x8
。我们知道sp
指向的是栈顶,则此代码执行完的结果就是x8
寄存器里面,保存了[ViewController viewDidLoad]
的栈顶地址。
⊙0x102189e8c <+104>: mov w10, #0x9
: 将常熟9
存入w10
寄存器。
⊙0x102189e90 <+108>: str w10, [x8]
: 将w10
存入栈。注意,此时x8
指向的是栈顶位置,而且栈的写入是向高地址写入。所以此时w10
里面的内存存入了[ViewController viewDidLoad]
的函数调用栈,并且位置在栈顶。
我们再进入test
函数看一下:
总结:函数的参数,超出8个的部分存放在栈里面。比如从
A
函数进入B
函数,B
函数拥有超过8个的参数,则超出的部分放入A
的函数调用栈中。
-
ii
:函数的返回值
是放在x0
寄存器里面;那如果返回值超过8个字节呢,此时返回值放在哪里呢?
我们来返回一个结构体,看一下返回的结构体存放在哪里:
进入getStr
函数内部:
总结:当函数的返回值大于8个字节,
x0
存不下的时候。返回值会保存在上层函数的栈空间中。比如从A
函数进入B
函数,B
函数的返回值超过8个字节,则返回值放入A
的函数调用栈中。
状态寄存器
-
CPU
内部的寄存器中,有一种特殊的寄存器(对于不同的处理器,个数和结构都很可能不同)。这种寄存器在ARM
中被称为状态寄存器
,就是CPSR(current program status register)
寄存器。 -
CPSR
和其他寄存器不一样,其他寄存器是用来存放数据的,都是整个寄存器具有一个含义。然而CPSR
寄存器是按位起作用的;也就是说它的每一位都有专门的含义,记录特定的信息。
⚠️ 注:
CPSR
寄存器是32位的。
-
CPSR
的低8位(包括I
、F
、T
和M[4:0]
)称为控制位,程序无法修改;除非CPU
运行于特权模式下,程序才能修改控制位。 -
N
、Z
、C
、V
均为条件码标志位,它们的内容可以被算术或逻辑运算的结果所改变,并且可以决定某条指令是否被执行。意义重大⚠️ ⚠️ ⚠️
N(Negative)标志
-
CPSR
的第31位是N
,符号标志位。它记录相关指令执行后,其结果是否为负数。记录方式如下:
指令执行后的结果 | N的值 |
---|---|
负数 | N = 1 |
非负数(注意:非负数,包括0) | N = 0 |
⚠️ 注意:在
ARM64
的指令集中,有的指令的执行是影响状态寄存器的,比如add/sub/or等(执行的时候要加s,如adds)。他们大都是运算指令(进行逻辑或算术运算)。
- 下面我们来验证一下(例子的执行结果
c
的值为-10
,我们看一下N
的值会不会变成1
):
通过上图,可以了解到,在执行相减指令之前,CPSR
寄存器里面的值是0x60000000
。下面我们通过计算器来看一下0x60000000
对应的二进制数据,看一下此时第31位N
是多少:
可以看到,此时N
的值是0
。
我们再来看一下,指令了subs
指令之后CPSR
寄存器里面的值:
执行完subs
指令之后,结果为负数(-10
),CPSR
寄存器里面的值是0x80000000
,此时N
的值为1
:
Z(Zero)标志
-
CPSR
的第30位是Z
,0标志位。它记录相关指令执行后,其结果是否为0。记录方式如下:
指令执行后的结果 | Z的值 | 助记理解 |
---|---|---|
结果为0 | Z = 1 |
在计算机中1 表示逻辑真,表示肯定;Z 是Zero(0) 的缩写;所以当结果为0 的时候,Z = 1 ,表示结果是0 。 |
结果不为0 | Z = 0 |
同上,Z = 0 ,表示结果不为0 。 |
- 根据上面验证
N
的过程,我们也可以看到CPSR
的第30位Z
是由1
变成0
的。下面我们通过汇编来看一下CPSR
的变化:
执行adds
之前:
执行adds
之后:
C(Carry)标志
-
CPSR
的第29位是C
,进位标志位。一般情况下进行无符号数
的运算。
i
加法运算:当运算结果产生了进位时(无符号数溢出)
运算方式 | 运算结果 | C的值 | 运算结果 | C的值 |
---|---|---|---|---|
加法运算 | 结果产生了进位(无符号数溢出) | C = 1 |
没有产生进位(无符号数没有溢出) | C = 0 |
减法运算(包括CMP) | 结果产生了借位(无符号数溢出) | C = 0 |
没有产生进位(无符号数没有溢出) | C = 1 |
对于位数为N的无符号数来说,其对应的二进制信息的最高位,即第N-1位,就是它的最高有效位,而假想存在第N位,就是相对于最高有效位的更高位。如下图所示:
-
进位
当两个数据相加的时候,有可能产生从最高有效位向更高位的进位。比如两个32位数据:0xaaaaaaaa
+0xaaaaaaaa
,将产生进位。由于这个进位值在32位中无法保存,我们就只是简单的说这个进位值丢失了。其实CPU
在运算的时候,并不丢弃这个进位值,而是记录在一个特殊的寄存器的某一位上。ARM
下就用C
位来记录这个进位值。比如下面的指令:
mov w0,#0xaaaaaaaa;0xa 的二进制是 1010
adds w0,w0,w0; 执行后 相当于 1010 << 1 进位1(无符号溢出) 所以C标记 为 1
adds w0,w0,w0; 执行后 相当于 0101 << 1 进位0(无符号没溢出) 所以C标记 为 0
adds w0,w0,w0; 重复上面操作
adds w0,w0,w0
-
借位
当两个数据做减法的时候,有可能向更高位借位。比如两个32位数据:0x00000000
-0x000000ff
将产生借位,借位后,相当于计算0x100000000
-ox000000ff
;得到0xffffff01
这个值。由于借了一位,所以C
位用来标记借位,C = 0
。比如下面的指令:
mov w0,#0x0
subs w0,w0,#0xff ;
subs w0,w0,#0xff
subs w0,w0,#0xff
V(Overflow)溢出标志
-
CPSR
的第28位是V
,溢出标志位。在进行有符号数运算的时候,如果超过了机器所能标识的范围,称为溢出。
计算 | 结果 |
---|---|
正数 + 正数 | 结果为负数,溢出 |
负数 + 负数 | 结果为正数,溢出 |
正数 + 负数 | 不可能溢出 |
tips
-
函数的局部变量
函数的局部变量是保存在当前栈里面的。
⚠️ 注意:函数的局部变量是保存在栈里面的,所以当函数执行完毕,栈被销毁之后,函数的内的局部变量也会被销毁。所以外部函数无法访问函数内部的局部变量。
-
现场保护
栈里面对一些数据进行入栈保存,保护起来,防止在其他其它地方被修改。比如我们之前讲到的,lr(x30)
寄存器,里面保存了函数回家的路,如果多层函数调用,则lr(x30)
寄存器里面的饿值就会被修改,这个时候就要进行现场保护,将lr(x30)
里面的值入栈,保护起来。
ldur
我们之前讲了ldr
,ldp
指令。再来认识一个ldur
指令。
都是将数据从内存中读出来,存到寄存器中。
指令名 | 区别 |
---|---|
ldr |
用于正数(偏移值是正数) |
ldp |
操作两个寄存器 |
ldur |
用于负数(偏移值是负数) |