美文网首页
汇编(二)

汇编(二)

作者: 浅墨入画 | 来源:发表于2021-04-07 20:12 被阅读0次

一. 函数调用栈

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

image.png
  • 传统栈空间存放数据方式:栈空间放入数据之后,栈顶指针挪一位
  • ARM64中在代码编译时,就会开辟好栈空间,栈顶指针不会挪动
  • 局部变量 参数 都是放在栈里面
  • 开辟栈空间存放数据,栈顶方向是指向低地址

1.2 SP和FP寄存器

  • sp寄存器在任意时刻会保存我们栈顶的地址.
  • fp寄存器也称为x29寄存器属于通用寄存器,但是在某些时刻我们利用它保存栈底的地址!

注意: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 关于内存读写指令
注意:读/写 数据是都是往高地址读/写,下面两个指令专门操作栈空间

  • str(store register)指令
    将数据从寄存器中读出来,存到内存栈空间中.
  • ldr(load register)指令
    将数据从内存栈空间中读出来,存到寄存器中

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寄存器中存放的是内存地址

  • 执行sub sp, sp, #0x20之前sp寄存器指向地址
image.png
  • 执行sub sp, sp, #0x20之后sp寄存器指向地址
image.png
  • 执行stp x0, x1, [sp] 意味着sp寄存器往上挪一位,指向0x16b28f948
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指令

  • l表示将下一条指令的地址放入lr(x30)寄存器
  • b表示跳转到标号处执行指令
image.png

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

image.png image.png

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

ret指令

  • 默认使用lr(x30)寄存器的值,通过底层指令提示CPU此处作为下条指令地址!
  • ARM64平台的特色指令,它面向硬件做了优化处理的

x30寄存器

  • x30寄存器存放的是函数的返回地址.当ret指令执行时会寻找x30寄存器保存的地址值!
  • 注意:在函数嵌套调用的时候.需要将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字节对齐的!!

三. 带参数返回值的函数

带参数返回值的函数

  • ARM64下,函数的参数是存放在X0到X7(W0到W7)这8个寄存器里面的.如果超过8个参数,就会入栈.
  • 一般为了提高效率,参数最好不要超过6个,因为中间有两个隐形参数 self 和 selector
  • 如果参数过多,可以使用数组或结构体来提高效率
  • 函数的返回值是放在X0 寄存器里面的.
  • 如果返回值大于8个字节,就会利用内存传递返回值

函数的局部变量

  • 函数的局部变量放在栈里面!

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

相关文章

  • <安全攻防之汇编基础>

    &关于汇编基础请点击 <汇编一> <汇编二> <汇编三> <汇编四> <汇编五> <汇编六> <汇编七> <汇编八...

  • 汇编基础笔记一

    汇编指令 汇编代码,高级语言-》汇编代码-》二进制-》计算机执行 高级语言只能转换成一种汇编代码,汇编代码可能转换...

  • lab1 二进制炸弹

    汇编与反汇编 汇编与反汇编的区别 phase_1 比较字符串是否相同 二进制炸弹 常见汇编指令详解 AT&T 格式...

  • 2,程序的机器级表示

    一, 编译使用命令 1, 汇编命令 2, intel 汇编指令 3, 反编译 命令 二, 汇编指令 1, mov...

  • 汇编(二)

    总线 CPU芯片.png 每一个CPU芯片都有许多管脚,这些管脚和总线相连,CPU通过总线跟外部器件进行交互 总线...

  • 汇编(二)

    总线 每一个CPU芯片都有许多管脚,这些管脚和总线相连,CPU通过总线跟外部器件进行交互 总线:一根根导线的集合 ...

  • 汇编(二)

  • 汇编(二)

    总线 每一个CPU芯片都有许多管脚,这些管脚和总线相连,CPU通过总线跟外部器件进行交互 总线:一根根导线的集合 ...

  • 汇编(二)

    一. 总线 每一个CPU芯片都有许多管脚,这些管脚和总线相连,CPU通过总线跟外部器件进行交互 总线: 一根根导线...

  • 汇编二

    (八)内存分段管理 1.物理地址=基础地址(段地址*0x10)+偏移地址 所以采用分段的内存管理: 2.在编程时可...

网友评论

      本文标题:汇编(二)

      本文链接:https://www.haomeiwen.com/subject/srknhltx.html