汇编(二)

2021-04-07  本文已影响0人  浅墨入画

一. 函数调用栈

1.1 栈:是一种具有特殊的访问方式的存储空间(后进先出, Last In Out Firt,LIFO)

image.png

1.2 SP和FP寄存器

注意:ARM64开始,取消32位的 LDM,STM,PUSH,POP指令! 取而代之的是ldr\ldp str\stp,ARM64里面 对栈的操作是16字节对齐的!!

1.3 函数调用栈
常见的函数调用开辟和恢复栈空间

sub    sp, sp, #0x40             ; 拉伸0x40(64字节)空间
stp    x29, x30, [sp, #0x30]     ;x29\x30 寄存器入栈保护
add    x29, sp, #0x30            ; x29指向栈帧的底部
... 
ldp    x29, x30, [sp, #0x30]     ;恢复x29/x30 寄存器的值
add    sp, sp, #0x40             ; 栈平衡
ret
// 注意 add    sp, sp, #0x40; 栈平衡恢复的值不需要销毁,等待下一次开辟栈空间覆盖
// 我们不能直接拿到sp寄存器的值,必须先拉伸栈空间之后,才能拿到sp寄存器的值

1.4 关于内存读写指令
注意:读/写 数据是都是往高地址读/写,下面两个指令专门操作栈空间

ldr 和 str 的变种stp和ldp,可以操作2个寄存器。
str ldr可以操作64位,stp ldp 可以操作128位

1.5 堆栈操作练习
现在我们回到汇编(一)中的demo,修改汇编代码如下

// asm.s文件内容
.text
.global _B

_B:
    // 使用32个字节空间作为这段程序的栈空间,然后利用栈将x0和x1的值进行交换.
    sub    sp, sp, #0x20    ;拉伸栈空间32个字节
    // 这里要操作两个寄存器,所以使用stp
    stp    x0, x1, [sp, #0x10] ;sp往上加16个字节,存放x0 和 x1
    ldp    x1, x0, [sp, #0x10] ;将sp偏移16个字节的值取出来,放入x1 和 x0,寄存器里面的值进行交换
    add    sp, sp, #0x20    ;栈平衡恢复栈空间32个字节
    ret 

// ViewController.m 中调用
@implementation ViewController

// 汇编B函数声明
int B();

- (void)viewDidLoad {
    [super viewDidLoad];
    B();
    // Do any additional setup after loading the view.
}
@end

我们来分析下上面汇编这样写 stp x0, x1, [sp] 会不会有问题?其中sp寄存器中存放的是内存地址

image.png image.png image.png

上面写法stp x0, x1, [sp]没有问题
sub sp, sp, #0x20 ;拉伸栈空间32个字节,开辟的内存空间如下图

image.png

stp x0, x1, [sp, #0x10] ;sp往上加16个字节,存放x0 和 x1,存放地址如下图

image.png

ldp x1, x0, [sp, #0x10] ;将sp偏移16个字节的值取出来放入x1 和 x0,寄存器里面的值进行交换,sp寄存器与内存数据都没有变化,变化的只是x0 x1 寄存器

运行工程打断点执行到汇编代码
执行完sub sp, sp, #0x20 ;拉伸栈空间32个字节

image.png

执行完stp x0, x1, [sp, #0x10] ;sp往上加16个字节,存放x0 和 x1

image.png

执行完ldp x1, x0, [sp, #0x10] ;

image.png image.png

执行完add sp, sp, #0x20 ;栈平衡恢复栈空间32个字节

image.png

这个时候a b的值还在内存地址中,等待下一轮栈空间拉伸之后写入数据,覆盖掉之前的值

1.6 问题探讨
请问死循环,程序是否会崩溃?
不会开辟内存空间的时候,死循环不会崩溃,如果汇编代码写成下面这样,会发生堆栈溢出崩溃

.text
.global _A,_B

_B:
    sub    sp, sp, #0x20    ;拉伸栈空间32个字节
    // 这里要操作两个寄存器,所以使用stp
    stp    x0, x1, [sp, #0x10] ;sp往上加16个字节,存放x0 和 x1
    ldp    x1, x0, [sp, #0x10] ;将sp偏移16个字节的值取出来,放入x1 和 x0,寄存器里面的值进行交换
    // bl _0 死循环一直调用_B,此时会一直拉伸栈空间,堆栈溢出就会导致崩溃
    bl _0 
    add    sp, sp, #0x20    ;栈平衡恢复栈空间32个字节
    ret

如果死循环不断开辟空间,当堆空间与栈空间发生碰撞时,堆栈溢出

.png

二. bl和ret指令

bl指令

image.png

举例 当_B执行完成之后,要跳转到ldp指令,如下图

image.png image.png

当执行到bl指令时,就会把下一条指令地址存入lr中,lr中存放回家的路

ret指令

x30寄存器

接下来我们来进行练习

// asm.s文件内容
.text
.global _A,_B

_A:
    mov x0,#0xaaaa
    bl _B
    mov x0,#0xaaaa
    ret

_B:
    mov x0,#0xbbbb
    ret

// ViewController.m 中调用
@implementation ViewController

// 汇编函数声明
int B();
int A();

- (void)viewDidLoad {
    [super viewDidLoad];
    printf("A");
    A();
    printf("B");
    // Do any additional setup after loading the view.
}
@end

运行程序,准备执行A函数

image.png

lr中保存的是0x0000000100d01f2c,与上图对应

image.png

继续执行跳入B函数,可以发现lr的内存地址为0x100d01ec8

image.png

继续执行ret返回到A函数中,再次执行A函数中ret发现死循环(此时的死循环没有拉伸栈空间,所以不会崩溃)对应了上节课遗留的问题!!! 原因是lr的内存地址一直为0x100d01ec8,执行完ret就会回到0x100d01ec8的位置,产生死循环。

lr寄存器与pc寄存器的区别?
pc寄存器指的是我们接下来要执行的内存地址,ret寄存器指的是让CPU将lr作为接下来执行的地址,此时会把lr的地址赋值给pc寄存器,如下图

image.png

接下来我们探讨怎么从函数A返回viewDidLoad?

// ViewController.m 中调用,我们修改代码如下,查看系统是怎么保存lr寄存器的
@implementation ViewController

void d() { 
}

void c() {
    d();
    return;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    c();
    // Do any additional setup after loading the view.
}
@end
image.png

-> 0x100289ef4 <+0>: stp x29, x30, [sp, #-0x10]! 从图中可以看出系统是这样保护lr寄存器的,!表示将[sp, #-0x10]算出来的结果赋值给sp,如下图

赋值前 赋值后

0x100289f00 <+12>: ldp x29, x30, [sp], #0x10,是从sp读取x29 x30,读完之后sp 再加上#0x10 还原回去保持栈平衡。通过上面我们可以发现当我们进行函数嵌套时,是把x30存入栈的形式来保存回家的路

.text
.global _A,_B

_A:
    sub sp,sp,#0x10
    // 保护lr寄存器,这里不能把lr寄存器的地址存入其他寄存器,因为函数嵌套过深,担心其他寄存器的值会被修改
    str x30,[sp]
    mov x0,#0xaaaa
    bl _B
    mov x0,#0xaaaa
    ldr x30,[sp]
    add sp,sp,#0x10
    ret

_B:
    mov x0,#0xbbbb
    ret

// 上面_A也可以这样写
_A:
    str x30,[sp,#-0x10]!
    mov x0,#0xaaaa
    bl _B
    mov x0,#0xaaaa
    ldr x30,[sp],#0x10
    ret


// viewDidLoad中打断点,会发现viewDidLoad之前有很多函数嵌套调用
(lldb) bt 
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 5.1
  * frame #0: 0x00000001027b9eec demo`-[ViewController viewDidLoad](self=0x0000000102d08530, _cmd="viewDidLoad") at ViewController.m:21:5
    frame #1: 0x0000000199522adc UIKitCore`-[UIViewController _sendViewDidLoadWithAppearanceProxyObjectTaggingEnabled] + 104
... 

运行demo,此时lr内存地址如下

image.png

继续执行,如下图所示

image.png image.png image.png

从上图可以看出,lr成功从栈里取出,sp也恢复栈平衡,成功回到viewDidLoad

如果我们修改上面汇编sub sp,sp,#0x8 会发现最后取数据时崩溃ldr x30,[sp]。为什么存数据没问题,取数据崩溃?原因是ARM64里面 对栈的操作是16字节对齐的!!

三. 带参数返回值的函数

带参数返回值的函数

函数的局部变量

3.1 先查看系统汇编实现

// ViewController.m中函数调用
@implementation ViewController

int sum(int a, int b) {
    return a + b;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    sum(10, 20);
}
@end
image.png

// sum函数的汇编代码如下
demo`sum:
->  0x100281ee4 <+0>:  sub    sp, sp, #0x10             ; =0x10 
    0x100281ee8 <+4>:  str    w0, [sp, #0xc]    //写入内存
    0x100281eec <+8>:  str    w1, [sp, #0x8]
    0x100281ef0 <+12>: ldr    w8, [sp, #0xc]    //从内存中取出 
    0x100281ef4 <+16>: ldr    w9, [sp, #0x8]
    0x100281ef8 <+20>: add    w0, w8, w9
    0x100281efc <+24>: add    sp, sp, #0x10             ; =0x10 
    0x100281f00 <+28>: ret 

3.2 实现简写版汇编

// ViewController.m中函数调用
@implementation ViewController

int suma(int a, int b);

- (void)viewDidLoad {
    [super viewDidLoad];
    suma(10, 20);
    printf("%d",suma(10,20));
}
@end

// asm.s文件内容
.text
.global _suma

_suma:
    add x0,x0,x1
    ret
// 参数放在寄存器中,返回值放在x0寄存器中
// 运行成功打印30
上一篇 下一篇

猜你喜欢

热点阅读