汇编(三)
一. 函数的参数
1.1 多个参数
创建空工程001--Demo,编写代码如下
// ViewController.m文件内容
@implementation ViewController
int test(int a,int b,int c,int d,int e,int f,int g,int h,int i){
return a + b + c + d + e + f + g + h + i;
}
- (void)viewDidLoad {
[super viewDidLoad];
test(1, 2, 3, 4, 5, 6, 7, 8, 9);
}
@end
test方法调用处打断点,运行工程断点执行到汇编代码中,如下图
image.png接下来我们分析viewdidload栈空间存放过程,viewdidload汇编代码如下
001--Demo`-[ViewController viewDidLoad]:
0x10012a578 <+0>: sub sp, sp, #0x40 ; =0x40
0x10012a57c <+4>: stp x29, x30, [sp, #0x30]
0x10012a580 <+8>: add x29, sp, #0x30 ; =0x30
0x10012a584 <+12>: stur x0, [x29, #-0x8]
0x10012a588 <+16>: stur x1, [x29, #-0x10]
0x10012a58c <+20>: ldur x8, [x29, #-0x8]
0x10012a590 <+24>: add x9, sp, #0x10 ; =0x10
0x10012a594 <+28>: str x8, [sp, #0x10]
0x10012a598 <+32>: adrp x8, 2
0x10012a59c <+36>: add x8, x8, #0xd18 ; =0xd18
0x10012a5a0 <+40>: ldr x8, [x8]
0x10012a5a4 <+44>: str x8, [x9, #0x8]
0x10012a5a8 <+48>: adrp x8, 2
0x10012a5ac <+52>: add x8, x8, #0xd00 ; =0xd00
0x10012a5b0 <+56>: ldr x1, [x8]
0x10012a5b4 <+60>: mov x0, x9
0x10012a5b8 <+64>: bl 0x10012aa00 ; symbol stub for: objc_msgSendSuper2
// adrp 表示 address page,内存是分页的
// adrp x8, 2 add x8, x8, #0xd18 ; =0xd18 表示从内存中取数据
// symbol stub for: objc_msgSendSuper2 表示调用[super viewDidLoad];
首先执行sub sp, sp, #0x40 拉伸栈空间
sp寄存器拉伸前指向地址 sp寄存器拉伸后指向地址接下来执行汇编stp x29, x30, [sp, #0x30],把x29 x30寄存器放入栈中
image.png执行add x29, sp, #0x30,让FP寄存器指向栈底
image.png从SP到FP就是拉伸的栈空间
字符串常量
NSLog(@"%@",self); 其中 @"%@" 就是字符串常量
注意⚠️ 开辟空间是以16字节对齐,操作数据可以是8字节也可以是16字节
继续执行上面代码,跳入test汇编代码中
001--Demo`test:
0x104e364d0 <+0>: sub sp, sp, #0x30 ; =0x30
0x104e364d4 <+4>: ldr w8, [sp, #0x30] // 从上一个函数栈中取数据,接近函数栈顶的区域,有可能作为函数参数的传递
0x104e364d8 <+8>: str w0, [sp, #0x2c]
0x104e364dc <+12>: str w1, [sp, #0x28]
0x104e364e0 <+16>: str w2, [sp, #0x24]
0x104e364e4 <+20>: str w3, [sp, #0x20]
0x104e364e8 <+24>: str w4, [sp, #0x1c]
0x104e364ec <+28>: str w5, [sp, #0x18]
0x104e364f0 <+32>: str w6, [sp, #0x14]
0x104e364f4 <+36>: str w7, [sp, #0x10]
0x104e364f8 <+40>: str w8, [sp, #0xc]
-> 0x104e364fc <+44>: ldr w8, [sp, #0x2c]
0x104e36500 <+48>: ldr w9, [sp, #0x28]
0x104e36504 <+52>: add w8, w8, w9
0x104e36508 <+56>: ldr w9, [sp, #0x24]
0x104e3650c <+60>: add w8, w8, w9
0x104e36510 <+64>: ldr w9, [sp, #0x20]
0x104e36514 <+68>: add w8, w8, w9
0x104e36518 <+72>: ldr w9, [sp, #0x1c]
0x104e3651c <+76>: add w8, w8, w9
0x104e36520 <+80>: ldr w9, [sp, #0x18]
0x104e36524 <+84>: add w8, w8, w9
0x104e36528 <+88>: ldr w9, [sp, #0x14]
0x104e3652c <+92>: add w8, w8, w9
0x104e36530 <+96>: ldr w9, [sp, #0x10]
0x104e36534 <+100>: add w8, w8, w9
0x104e36538 <+104>: ldr w9, [sp, #0xc]
0x104e3653c <+108>: add w0, w8, w9 // 最终相加的结果给了wo寄存器
0x104e36540 <+112>: add sp, sp, #0x30 ; =0x30
0x104e36544 <+116>: ret
// 参数超过8个,效率就会降低,因为存在函数栈传递参数
现在把工程001--Demo切换到release模式下,运行工程发现并没有调用test汇编,原因是test方法的运行对整个程序并没有作用,编译器会把无用的代码屏蔽掉
image.png现在修改viewcontroller.m文件内容如下
// ViewController.m文件内容
@implementation ViewController
int test(int a,int b,int c,int d,int e,int f,int g,int h,int i){
return a + b + c + d + e + f + g + h + i;
}
- (void)viewDidLoad {
[super viewDidLoad];
printf(@"%d",test(1, 2, 3, 4, 5, 6, 7, 8, 9));
}
@end
运行工程发现并没有调用test汇编,编译器直接算出来结果,release模式下编译器会进行优化
image.png1.2 汇编实现参数调用
// ViewController.m文件内容
@implementation ViewController
int funcA(int a, int b);
- (void)viewDidLoad {
[super viewDidLoad];
int a = funcA(10, 20);
printf(@"%d", a);
}
@end
// asm.s文件内容
.text
.global _funcA,_sum
_funcA:
sub sp,sp,#0x10
stp x29,x30,[sp]
bl _sum
stp x29,x30,[sp]
add sp,sp,#0x10
ret
// _funcA也可以这样简写
_funcA:
stp x29,x30,[sp,#-0x10]!
bl _sum
stp x29,x30,[sp],#0x10
ret
_sum:
add x0,x0,x1
ret
// 成功运行打印30
二. 函数的返回值
// ViewController.m文件内容
@implementation ViewController
struct str {
int a;
int b;
int c;
int d;
int f;
int g;
};
struct str getStr(int a,int b,int c,int d,int f,int g){
struct str str1;
str1.a = a;
str1.b = b;
str1.c = d;
str1.d = d;
str1.f = f;
str1.g = g;
return str1;
}
- (void)viewDidLoad {
[super viewDidLoad];
struct str str2 = getStr(1, 2, 3, 4, 5, 6);
}
@end
下图add x8, sp, #0x8 让x8寄存器指向开辟的结构体空间,这里为什么不用sp寄存器呢?
- 因为调用getStr之后会拉伸栈空间,sp就会发生变化,如果以sp寄存器作为参照,就会找不到创建的结构体。
// getStr 方法汇编代码如下
001--Demo`getStr:
-> 0x104f19d94 <+0>: sub sp, sp, #0x20 ; =0x20
// 这里把1 ~ 6 分别存入栈中
0x104f19d98 <+4>: str w0, [sp, #0x1c]
0x104f19d9c <+8>: str w1, [sp, #0x18]
0x104f19da0 <+12>: str w2, [sp, #0x14]
0x104f19da4 <+16>: str w3, [sp, #0x10]
0x104f19da8 <+20>: str w4, [sp, #0xc]
0x104f19dac <+24>: str w5, [sp, #0x8]
// 上面是把6个参数放入栈中,是从w0 ~ w6,下面为什么直接跳转到w9寄存器,不用w6寄存器呢?
因为w0 ~ w7 都是用作参数,参数不够的话,w6 w7 两个寄存器不会用到。w8用作返回值,所以此处用w9寄存器作为临时变量
// 再从栈中取出放入w9寄存器
0x104f19db0 <+28>: ldr w9, [sp, #0x1c]
// 这里以x8寄存器作为参照,把数据写入上一个栈空间中(viewDidLoad栈空间)
0x104f19db4 <+32>: str w9, [x8]
// 以下都是以w9寄存器作为参照取出数据,再把数据写入上一个栈空间
0x104f19db8 <+36>: ldr w9, [sp, #0x18]
0x104f19dbc <+40>: str w9, [x8, #0x4]
0x104f19dc0 <+44>: ldr w9, [sp, #0x14]
0x104f19dc4 <+48>: str w9, [x8, #0x8]
0x104f19dc8 <+52>: ldr w9, [sp, #0x10]
0x104f19dcc <+56>: str w9, [x8, #0xc]
0x104f19dd0 <+60>: ldr w9, [sp, #0xc]
0x104f19dd4 <+64>: str w9, [x8, #0x10]
0x104f19dd8 <+68>: ldr w9, [sp, #0x8]
0x104f19ddc <+72>: str w9, [x8, #0x14]
// 执行到这里之后,就会把结构体数据全部写入上一个栈空间中
0x104f19de0 <+76>: add sp, sp, #0x20 ; =0x20
0x104f19de4 <+80>: ret
// 这里并没有用x0寄存器作为返回值,如果我们的函数参数大于8个,返回值也会用栈空间来做
小结
函数返回值如果一个寄存器能够放的下,就使用x0或wo寄存器,如果放不下的话就会用到栈空间
三. 函数的局部变量
// ViewController.m文件内容
@implementation ViewController
int funcB(int a,int b){
int c = 6;
return a + b + c;
}
- (void)viewDidLoad {
[super viewDidLoad];
funcB(10, 20);
}
@end
image.png
// 查看funB函数的汇编代码
001--Demo`funcB:
-> 0x1040d5ed4 <+0>: sub sp, sp, #0x10 ; =0x10
0x1040d5ed8 <+4>: str w0, [sp, #0xc]
0x1040d5edc <+8>: str w1, [sp, #0x8]
// 局部变量c 值为6
0x1040d5ee0 <+12>: mov w8, #0x6
// 局部变量6存入栈中,这里是4个字节
0x1040d5ee4 <+16>: str w8, [sp, #0x4]
0x1040d5ee8 <+20>: ldr w8, [sp, #0xc]
0x1040d5eec <+24>: ldr w9, [sp, #0x8]
0x1040d5ef0 <+28>: add w8, w8, w9
// 取出局部变量6,存入w9临时寄存器
0x1040d5ef4 <+32>: ldr w9, [sp, #0x4]
0x1040d5ef8 <+36>: add w0, w8, w9
0x1040d5efc <+40>: add sp, sp, #0x10 ; =0x10
0x1040d5f00 <+44>: ret
局部变量存入栈空间中,参数用的寄存器传递,所有计算都是通过寄存器来完成。一旦执行 add sp, sp, #0x10 栈平衡,所有局部变量就会取不到相当于销毁了,栈平衡销毁局部变量。
如果函数嵌套,参数和局部变量会怎么样呢?下面讨论
四. 函数的嵌套调用
// ViewController.m文件内容
@implementation ViewController
int funcB(int a,int b){
int c = 6;
int d = funcSum(a, b, c);
int e = funcSum(a, b, c);
return e;
}
int funcSum(int a,int b,int c){
int d = a + b + c;
printf("%d",d);
return d;
}
- (void)viewDidLoad {
[super viewDidLoad];
funcB(10, 20);
}
@end
// 查看funB函数的汇编代码
001--Demo`funcB:
// 前面三行汇编类似于函数开始
-> 0x1004f9e38 <+0>: sub sp, sp, #0x30 ; =0x30
// x29/fp寄存器 x30/lr寄存器 入栈
0x1004f9e3c <+4>: stp x29, x30, [sp, #0x20]
// x29指向栈底
0x1004f9e40 <+8>: add x29, sp, #0x20 ; =0x20
// 参数入栈保护w0 w1 寄存器,这里只用到两个参数(funcB的两个参数),如果参数多的话,防止函数调用完改变这两个参数的值,而下面又需要使用这几个参数,就需要把多个参数都保护起来
0x1004f9e44 <+12>: stur w0, [x29, #-0x4]
0x1004f9e48 <+16>: stur w1, [x29, #-0x8]
// 局部变量生成
0x1004f9e4c <+20>: mov w8, #0x6
// 局部变量入栈
0x1004f9e50 <+24>: stur w8, [x29, #-0xc]
// 这里与下面都用到了 w0 w1 w2 三个寄存器,funcSum也用到这三个寄存器,有可能会改变这三个寄存器的值
0x1004f9e54 <+28>: ldur w0, [x29, #-0x4]
0x1004f9e58 <+32>: ldur w1, [x29, #-0x8]
0x1004f9e5c <+36>: ldur w2, [x29, #-0xc]
// 调用新的函数funcSum
0x1004f9e60 <+40>: bl 0x1004f9e8c ; funcSum at ViewController.m:24
// 保存函数funcSum的返回值
0x1004f9e64 <+44>: str w0, [sp, #0x10]
// 这里与上面都用到了 w0 w1 w2 三个寄存器,funcSum也用到这三个寄存器,有可能会改变这三个寄存器的值
0x1004f9e68 <+48>: ldur w0, [x29, #-0x4]
0x1004f9e6c <+52>: ldur w1, [x29, #-0x8]
0x1004f9e70 <+56>: ldur w2, [x29, #-0xc]
0x1004f9e74 <+60>: bl 0x1004f9e8c ; funcSum at ViewController.m:24
// 第二次调用函数funcSum,保存返回值
0x1004f9e78 <+64>: str w0, [sp, #0xc]
0x1004f9e7c <+68>: ldr w0, [sp, #0xc]
// 下面三行相当于函数结束
0x1004f9e80 <+72>: ldp x29, x30, [sp, #0x20]
0x1004f9e84 <+76>: add sp, sp, #0x30 ; =0x30
0x1004f9e88 <+80>: ret
小结
寄存器的保护也叫现场保护,会保护到自己的栈空间中。寄存器保护是保护将来可能会用到这些寄存器。x29 x30 的保护就是这样的
五. 状态寄存器
- CPU内部的寄存器中,有一种特殊的寄存器,这种寄存器在ARM中被称为状态寄存器,也就是CPSR(current program status register)寄存器
- 状态寄存器对于不同的处理器,个数和结构都可能不同
- CPSR和其他寄存器不一样,其他寄存器是用来存放数据的,都是整个寄存器具有一个含义.而CPSR寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息.
注:CPSR寄存器是32位的
- CPSR的低8位(包括I、F、T和M[4:0])称为控制位,程序无法修改,除非CPU运行于特权模式下,程序才能修改控制位!
- N、Z、C、V均为条件码标志位。它们的内容可被算术或逻辑运算的结果所改变,并且可以决定某条指令是否被执行!意义重大!
- 中间27~8 位为保留位,保留的目的是为了升级 更新 扩展等
5.1 案例一
// ViewController.m文件内容
@implementation ViewController
void func(){
int a = 1;
int b = 2;
if (a == b) {
printf("a == b\n");
}else{
printf("error\n");
}
}
- (void)viewDidLoad {
[super viewDidLoad];
func();
}
@end
标志寄存器可以用来调试信息,因为它与程序执行流程有关
// 查看fun函数的汇编代码
001--Demo`func:
-> 0x10274de80 <+0>: sub sp, sp, #0x20 ; =0x20
0x10274de84 <+4>: stp x29, x30, [sp, #0x10]
0x10274de88 <+8>: add x29, sp, #0x10 ; =0x10
0x10274de8c <+12>: mov w8, #0x1
0x10274de90 <+16>: stur w8, [x29, #-0x4]
0x10274de94 <+20>: mov w8, #0x2
0x10274de98 <+24>: str w8, [sp, #0x8]
0x10274de9c <+28>: ldur w8, [x29, #-0x4]
0x10274dea0 <+32>: ldr w9, [sp, #0x8]
// cmp指令 compare比较
0x10274dea4 <+36>: cmp w8, w9
// b指令跳转 .ne 指的是有条件的跳转,这里的跳转就与标志寄存器有关
0x10274dea8 <+40>: b.ne 0x10274debc ; <+60> at ViewController.m
0x10274deac <+44>: adrp x0, 1
0x10274deb0 <+48>: add x0, x0, #0x62f ; =0x62f
0x10274deb4 <+52>: bl 0x10274e56c ; symbol stub for: printf
0x10274deb8 <+56>: b 0x10274dec8 ; <+72> at ViewController.m:77:1
0x10274debc <+60>: adrp x0, 1
0x10274dec0 <+64>: add x0, x0, #0x637 ; =0x637
0x10274dec4 <+68>: bl 0x10274e56c ; symbol stub for: printf
0x10274dec8 <+72>: ldp x29, x30, [sp, #0x10]
0x10274decc <+76>: add sp, sp, #0x20 ; =0x20
0x10274ded0 <+80>: ret
image.png
image.png
上面的if判断中 a=1 b=2,为什么修改了cpsr寄存器的值会执行 a==b 的逻辑? 我们下节课讨论!!!
5.2 案例二
// ViewController.m文件内容
@implementation ViewController
void func(){
asm(
"mov w0,#0xffffffff\n"
//adds 可以改变标志位
"adds w0,w0,#0x0\n"
);
}
- (void)viewDidLoad {
[super viewDidLoad];
func();
}
@end
// 查看fun函数的汇编代码
001--Demo`func:
// 0xffffffff 就是 -1
-> 0x100b6decc <+0>: mov w0, #-0x1
0x100b6ded0 <+4>: adds w0, w0, #0x0 ; =0x0
0x100b6ded4 <+8>: ret
image.png
image.png
N(Negative)标志
CPSR的第31位是 N,符号标志位。它记录相关指令执行后,其结果是否为负.如果为负 N = 1,如果是非负数 N = 0.
- 注意,在ARM64的指令集中,有的指令的执行时影响状态寄存器的,比如add\sub\or等,他们大都是运算指令(进行逻辑或算数运算);
上面执行到adds w0, w0, #0x0 标志寄存器是 0110,那么根据N标志的分析,程序执行完成 0110 第一位的0 应该变成 1
截屏2021-04-10 下午3.24.27.png5.3 案例三
Z(Zero)标志
CPSR的第30位是Z,0标志位。它记录相关指令执行后,其结果是否为0.如果结果为0.那么Z = 1.如果结果不为0,那么Z = 0.
- Z的值,我们可以这样来看,Z标记相关指令的计算结果是否为0,如果为0,则Z要记录下是0这样的肯定信息.在计算机中1表示逻辑真,表示肯定.所以当结果为0的时候Z = 1,表示结果是0.如果结果不为0,则Z要记录下不是0这样的否定信息.在计算机中0表示逻辑假,表示否定,所以当结果不为0的时候Z = 0,表示结果不为0。
根据N Z 标志的分析,如果执行结果为0, 那么前两位N Z标志一定是01,下面进行验证
// ViewController.m文件内容
@implementation ViewController
void func(){
asm(
"mov w0,#0x0\n"
//adds 可以改变标志位
"adds w0,w0,#0x0\n"
);
}
- (void)viewDidLoad {
[super viewDidLoad];
func();
}
@end
image.png
5.4 案例四
// ViewController.m文件内容
@implementation ViewController
void func(){
asm(
"mov w0,#0x0\n"
//adds 可以改变标志位
"adds w0,w0,#0x1\n"
);
}
- (void)viewDidLoad {
[super viewDidLoad];
func();
}
@end
w0 的值为1 是正书,根据上面分析 N Z标志位都是0,运行得到状态寄存器的值为0x00000000 得到验证
C(Carry)标志
- CPSR的第29位是C,进位标志位。一般情况下,进行无符号数的运算。
- 加法运算:当运算结果产生了进位时(无符号数溢出),C=1,否则C=0。
- 减法运算(包括CMP):当运算时产生了借位时(无符号数溢出),C=0,否则C=1。
- 作为有符号位 0111 是正数 1111 是负数,作为无符号位 0111是7,1111 是15 举例无符号位 1111 再加上 1 就会溢出变成 100000000,1溢出到C标志位,同理如果减法运算,不够减就从C标志借来数据
对于位数为N的无符号数来说,其对应的二进制信息的最高位,即第N - 1位,就是它的最高有效位,而假想存在的第N位,就是相对于最高有效位的更高位。如下图所示:
image.png进位
- 我们知道,当两个数据相加的时候,有可能产生从最高有效位想更高位的进位。比如两个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 - 0x000000ff。得到0xffffff01 这个值。由于借了一位,所以C位 用来标记借位。C = 0.比如下面指令:
mov w0,#0x0
subs w0,w0,#0xff ;
subs w0,w0,#0xff
subs w0,w0,#0xff
5.5 案例五
// ViewController.m文件内容
@implementation ViewController
void func(){
asm(
// w0 是4个字节,0xaaaaaaaa 相当于 1010 1010 1010 1010 1010 1010 1010 1010
"mov w0,#0xaaaaaaaa\n"
// 乘以2 相当于左移1位
"adds w0,w0,w0\n"
"adds w0,w0,w0\n"
"adds w0,w0,w0\n"
"adds w0,w0,w0\n"
);
}
- (void)viewDidLoad {
[super viewDidLoad];
func();
}
@end
image.png
上图C为1 相当于溢出,这时修改寄存器cpsr为0x00000000,一步步执行,发现C位不断0 1 替换
V(Overflow)溢出标志
- CPSR的第28位是V,溢出标志位。在进行有符号数运算的时候,如果超过了机器所能标识的范围,称为溢出。
- 正数 + 正数 为负数 溢出
- 负数 + 负数 为正数 溢出
- 正数 + 负数 不可能溢出
注意⚠️这里的计算数都是相同宽度的
上面执行的过程,状态寄存器并不知道两个数是正数还是负数,也不知道是有符号数还是无符号数?
cpsr寄存器的 v位会认为是有符号数运算,c位为无符号数运算,高级语言中有符号数与无符号数的运算,有没有溢出状态寄存器的 V C 位都会给出反馈
六. 面试题
6.1 面试题一
函数A调用函数B,A调用B的时候给B传递了参数,当函数B执行完成后,B函数的参数释放了吗?
这里以上面 函数的返回值 代码举例
- (void)viewDidLoad {
[super viewDidLoad];
struct str str2 = getStr(1, 2, 3, 4, 5, 6);
}
getStr(1, 2, 3, 4, 5, 6)方法中的 1 2 3 4 5 6 对于viewDidLoad方法来说,相当于局部变量,这时候如果参数不是6个而是9个,getStr(1, 2, 3, 4, 5, 6, 7, 8, 9); 其中7 8 9 三个参数是存放在viewDidLoad函数栈中,所以当getStr方法执行完成,这仨参数并没有释放,只有viewDidLoad执行完成才会释放。
6.2 面试题二
getStr函数调用之前开辟空间没?如果开辟了,开辟的是栈空间还是堆空间?
- (void)viewDidLoad {
[super viewDidLoad];
struct str str2 = getStr(1, 2, 3, 4, 5, 6);
}
这里开辟的是栈空间,开辟了栈空间之后,整个结构体都在栈中,占用了24字节内存,getStr执行完成之后,str2结构体依然在栈中。
如果viewDidLoad又返回值,并且返回的是 return &str2; 外界能否访问str2的地址来操纵结构体?
不能,因为str2整个结构体在栈空间中,viewDidLoad执行完成,str2结构体就会释放掉。一般来说不会直接返回结构体,而是返回结构体的指针,要想返回结构体指针,结构体需要开辟在堆空间,开辟完成之后再给结构体赋值,最后再返回结构体指针。