汇编(三)

时间:2021-7-3 作者:qvyue

一. 函数的参数

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 :   sub    sp, sp, #0x40             ; =0x40 
    0x10012a57c :   stp    x29, x30, [sp, #0x30]
    0x10012a580 :   add    x29, sp, #0x30            ; =0x30 
    0x10012a584 :  stur   x0, [x29, #-0x8]
    0x10012a588 :  stur   x1, [x29, #-0x10]
    0x10012a58c :  ldur   x8, [x29, #-0x8]
    0x10012a590 :  add    x9, sp, #0x10             ; =0x10 
    0x10012a594 :  str    x8, [sp, #0x10]
    0x10012a598 :  adrp   x8, 2
    0x10012a59c :  add    x8, x8, #0xd18            ; =0xd18 
    0x10012a5a0 :  ldr    x8, [x8]
    0x10012a5a4 :  str    x8, [x9, #0x8]
    0x10012a5a8 :  adrp   x8, 2
    0x10012a5ac :  add    x8, x8, #0xd00            ; =0xd00 
    0x10012a5b0 :  ldr    x1, [x8]
    0x10012a5b4 :  mov    x0, x9
    0x10012a5b8 :  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 :   sub    sp, sp, #0x30             ; =0x30 
    0x104e364d4 :   ldr    w8, [sp, #0x30]   // 从上一个函数栈中取数据,接近函数栈顶的区域,有可能作为函数参数的传递
    0x104e364d8 :   str    w0, [sp, #0x2c]
    0x104e364dc :  str    w1, [sp, #0x28]
    0x104e364e0 :  str    w2, [sp, #0x24]
    0x104e364e4 :  str    w3, [sp, #0x20]
    0x104e364e8 :  str    w4, [sp, #0x1c]
    0x104e364ec :  str    w5, [sp, #0x18]
    0x104e364f0 :  str    w6, [sp, #0x14]
    0x104e364f4 :  str    w7, [sp, #0x10]
    0x104e364f8 :  str    w8, [sp, #0xc]
->  0x104e364fc :  ldr    w8, [sp, #0x2c]
    0x104e36500 :  ldr    w9, [sp, #0x28]
    0x104e36504 :  add    w8, w8, w9
    0x104e36508 :  ldr    w9, [sp, #0x24]
    0x104e3650c :  add    w8, w8, w9
    0x104e36510 :  ldr    w9, [sp, #0x20]
    0x104e36514 :  add    w8, w8, w9
    0x104e36518 :  ldr    w9, [sp, #0x1c]
    0x104e3651c :  add    w8, w8, w9
    0x104e36520 :  ldr    w9, [sp, #0x18]
    0x104e36524 :  add    w8, w8, w9
    0x104e36528 :  ldr    w9, [sp, #0x14]
    0x104e3652c :  add    w8, w8, w9
    0x104e36530 :  ldr    w9, [sp, #0x10]
    0x104e36534 : add    w8, w8, w9
    0x104e36538 : ldr    w9, [sp, #0xc]
    0x104e3653c : add    w0, w8, w9   // 最终相加的结果给了wo寄存器
    0x104e36540 : add    sp, sp, #0x30             ; =0x30 
    0x104e36544 : 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.png

1.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寄存器作为参照,就会找不到创建的结构体。
汇编(三)
image.png
// getStr 方法汇编代码如下
001--Demo`getStr:
->  0x104f19d94 :  sub    sp, sp, #0x20             ; =0x20 
    //  这里把1 ~ 6 分别存入栈中
    0x104f19d98 :  str    w0, [sp, #0x1c]
    0x104f19d9c :  str    w1, [sp, #0x18]
    0x104f19da0 : str    w2, [sp, #0x14]
    0x104f19da4 : str    w3, [sp, #0x10]
    0x104f19da8 : str    w4, [sp, #0xc]
    0x104f19dac : str    w5, [sp, #0x8]
    // 上面是把6个参数放入栈中,是从w0 ~ w6,下面为什么直接跳转到w9寄存器,不用w6寄存器呢?
    因为w0 ~ w7 都是用作参数,参数不够的话,w6 w7 两个寄存器不会用到。w8用作返回值,所以此处用w9寄存器作为临时变量
    // 再从栈中取出放入w9寄存器
    0x104f19db0 : ldr    w9, [sp, #0x1c]
    // 这里以x8寄存器作为参照,把数据写入上一个栈空间中(viewDidLoad栈空间)
    0x104f19db4 : str    w9, [x8]
    // 以下都是以w9寄存器作为参照取出数据,再把数据写入上一个栈空间
    0x104f19db8 : ldr    w9, [sp, #0x18]
    0x104f19dbc : str    w9, [x8, #0x4]
    0x104f19dc0 : ldr    w9, [sp, #0x14]
    0x104f19dc4 : str    w9, [x8, #0x8]
    0x104f19dc8 : ldr    w9, [sp, #0x10]
    0x104f19dcc : str    w9, [x8, #0xc]
    0x104f19dd0 : ldr    w9, [sp, #0xc]
    0x104f19dd4 : str    w9, [x8, #0x10]
    0x104f19dd8 : ldr    w9, [sp, #0x8]
    0x104f19ddc : str    w9, [x8, #0x14]
    // 执行到这里之后,就会把结构体数据全部写入上一个栈空间中
    0x104f19de0 : add    sp, sp, #0x20             ; =0x20 
    0x104f19de4 : 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 :  sub    sp, sp, #0x10             ; =0x10 
    0x1040d5ed8 :  str    w0, [sp, #0xc]
    0x1040d5edc :  str    w1, [sp, #0x8]
    // 局部变量c  值为6
    0x1040d5ee0 : mov    w8, #0x6
    // 局部变量6存入栈中,这里是4个字节
    0x1040d5ee4 : str    w8, [sp, #0x4]
    0x1040d5ee8 : ldr    w8, [sp, #0xc]
    0x1040d5eec : ldr    w9, [sp, #0x8]
    0x1040d5ef0 : add    w8, w8, w9
    // 取出局部变量6,存入w9临时寄存器
    0x1040d5ef4 : ldr    w9, [sp, #0x4]
    0x1040d5ef8 : add    w0, w8, w9
    0x1040d5efc : add    sp, sp, #0x10             ; =0x10 
    0x1040d5f00 : 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 :  sub    sp, sp, #0x30             ; =0x30 
    // x29/fp寄存器 x30/lr寄存器 入栈
    0x1004f9e3c :  stp    x29, x30, [sp, #0x20]
    // x29指向栈底
    0x1004f9e40 :  add    x29, sp, #0x20            ; =0x20 
    // 参数入栈保护w0 w1 寄存器,这里只用到两个参数(funcB的两个参数),如果参数多的话,防止函数调用完改变这两个参数的值,而下面又需要使用这几个参数,就需要把多个参数都保护起来
    0x1004f9e44 : stur   w0, [x29, #-0x4]
    0x1004f9e48 : stur   w1, [x29, #-0x8]
    // 局部变量生成
    0x1004f9e4c : mov    w8, #0x6
    // 局部变量入栈
    0x1004f9e50 : stur   w8, [x29, #-0xc]
    // 这里与下面都用到了 w0 w1 w2 三个寄存器,funcSum也用到这三个寄存器,有可能会改变这三个寄存器的值
    0x1004f9e54 : ldur   w0, [x29, #-0x4]
    0x1004f9e58 : ldur   w1, [x29, #-0x8]
    0x1004f9e5c : ldur   w2, [x29, #-0xc]
    // 调用新的函数funcSum
    0x1004f9e60 : bl     0x1004f9e8c               ; funcSum at ViewController.m:24
    // 保存函数funcSum的返回值
    0x1004f9e64 : str    w0, [sp, #0x10]
    // 这里与上面都用到了 w0 w1 w2 三个寄存器,funcSum也用到这三个寄存器,有可能会改变这三个寄存器的值
    0x1004f9e68 : ldur   w0, [x29, #-0x4]
    0x1004f9e6c : ldur   w1, [x29, #-0x8]
    0x1004f9e70 : ldur   w2, [x29, #-0xc]
    0x1004f9e74 : bl     0x1004f9e8c               ; funcSum at ViewController.m:24
    // 第二次调用函数funcSum,保存返回值
    0x1004f9e78 : str    w0, [sp, #0xc]
    0x1004f9e7c : ldr    w0, [sp, #0xc]
    // 下面三行相当于函数结束
    0x1004f9e80 : ldp    x29, x30, [sp, #0x20]
    0x1004f9e84 : add    sp, sp, #0x30             ; =0x30 
    0x1004f9e88 : 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 位为保留位,保留的目的是为了升级 更新 扩展等
汇编(三)
image.png

5.1 案例一

// ViewController.m文件内容 
@implementation ViewController
void func(){
    int a = 1;
    int b = 2;
    if (a == b) {
        printf("a == bn");
    }else{
        printf("errorn");
    }
}

- (void)viewDidLoad {
    [super viewDidLoad];
    func();
}
@end

标志寄存器可以用来调试信息,因为它与程序执行流程有关

// 查看fun函数的汇编代码
001--Demo`func:
->  0x10274de80 :  sub    sp, sp, #0x20             ; =0x20 
    0x10274de84 :  stp    x29, x30, [sp, #0x10]
    0x10274de88 :  add    x29, sp, #0x10            ; =0x10 
    0x10274de8c : mov    w8, #0x1
    0x10274de90 : stur   w8, [x29, #-0x4]
    0x10274de94 : mov    w8, #0x2
    0x10274de98 : str    w8, [sp, #0x8]
    0x10274de9c : ldur   w8, [x29, #-0x4]
    0x10274dea0 : ldr    w9, [sp, #0x8]
    // cmp指令 compare比较
    0x10274dea4 : cmp    w8, w9
    // b指令跳转 .ne 指的是有条件的跳转,这里的跳转就与标志寄存器有关
    0x10274dea8 : b.ne   0x10274debc               ;  at ViewController.m
    0x10274deac : adrp   x0, 1
    0x10274deb0 : add    x0, x0, #0x62f            ; =0x62f 
    0x10274deb4 : bl     0x10274e56c               ; symbol stub for: printf
    0x10274deb8 : b      0x10274dec8               ;  at ViewController.m:77:1
    0x10274debc : adrp   x0, 1
    0x10274dec0 : add    x0, x0, #0x637            ; =0x637 
    0x10274dec4 : bl     0x10274e56c               ; symbol stub for: printf
    0x10274dec8 : ldp    x29, x30, [sp, #0x10]
    0x10274decc : add    sp, sp, #0x20             ; =0x20 
    0x10274ded0 : ret 
汇编(三)
image.png
汇编(三)
image.png

上面的if判断中 a=1 b=2,为什么修改了cpsr寄存器的值会执行 a==b 的逻辑? 我们下节课讨论!!!

5.2 案例二

// ViewController.m文件内容 
@implementation ViewController
void func(){
    asm(
        "mov w0,#0xffffffffn"
        //adds 可以改变标志位
        "adds w0,w0,#0x0n"
        );
}

- (void)viewDidLoad {
    [super viewDidLoad];
    func();
}
@end

// 查看fun函数的汇编代码
001--Demo`func:
    // 0xffffffff 就是 -1
->  0x100b6decc : mov    w0, #-0x1
    0x100b6ded0 : adds   w0, w0, #0x0              ; =0x0 
    0x100b6ded4 : ret  
汇编(三)
image.png
汇编(三)
image.png

N(Negative)标志

CPSR的第31位是 N,符号标志位。它记录相关指令执行后,其结果是否为负.如果为负 N = 1,如果是非负数 N = 0.

  • 注意,在ARM64的指令集中,有的指令的执行时影响状态寄存器的,比如addsubor等,他们大都是运算指令(进行逻辑或算数运算);

上面执行到adds w0, w0, #0x0 标志寄存器是 0110,那么根据N标志的分析,程序执行完成 0110 第一位的0 应该变成 1

汇编(三)
截屏2021-04-10 下午3.24.27.png

5.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,#0x0n"
        //adds 可以改变标志位
        "adds w0,w0,#0x0n"
        );
}

- (void)viewDidLoad {
    [super viewDidLoad];
    func();
}
@end
汇编(三)
image.png

5.4 案例四

// ViewController.m文件内容 
@implementation ViewController
void func(){
    asm(
        "mov w0,#0x0n"
        //adds 可以改变标志位
        "adds w0,w0,#0x1n"
        );
}

- (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 

借位

  • 当两个数据做减法的时候,有可能向更高位借位。再比如,两个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,#0xaaaaaaaan"
        // 乘以2 相当于左移1位
        "adds w0,w0,w0n"
        "adds w0,w0,w0n"
        "adds w0,w0,w0n"
        "adds w0,w0,w0n"
        );
}

- (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结构体就会释放掉。一般来说不会直接返回结构体,而是返回结构体的指针,要想返回结构体指针,结构体需要开辟在堆空间,开辟完成之后再给结构体赋值,最后再返回结构体指针。

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。