汇编002-函数本质(上)

上节回顾

  • 汇编概述
    • 使用助记符代替机器指令的一种编程语言
    • 汇编和机器指令一一对应的关系,拿到二进制能够反汇编
    • 因为汇编和CPU指令集是对应的,因此汇编不具有移植性
  • 总线:是一堆导线的集合
    • 地址总线:越宽寻址能力越强
    • 数据总线:宽度决定了CPU数据的吞吐量
    • 控制总线
  • 进制
    • 任意进制都是由对应个数的符号组成的,符号能够自定义
    • 2/8/16是相对完美的进制,他们之间的关系为
      • 3个2进制使用一个8进制标识
      • 4个2进制使用一个16进制标识
      • 2个16进制位能够标识一个字节
    • 数量单位
      • 1024=1k; 1024k=1M; 1024M=1G
      • B:byte(字节) 1B=8bit
      • bit(比特):1个二进制位
    • 数据的宽度
      • 计算机中的数据是有宽度的,超过了就会溢出
  • 寄存器:CPU为了性能,在内部开辟了一小块临时存储区域
    • 浮点向量寄存器
    • 异常状态寄存器
    • 通用寄存器:除了存储数据有的时候也有特殊用途
      • ARM64拥有32个64位的通用寄存器x0-x30以及XZR(零寄存器)
      • 为了兼容32位,因此ARM64拥有w0-w28\WZR30个32位寄存器
      • 32位寄存器并非独立存在的,好比w0是x0的低32位
    • PC寄存器:指令指针寄存器
      • PC寄存器里面的值保存的就是CPU接下来须要执行的指令地址
      • 改变PC的值能够改变程序的执行流程
      • CPU执行过的指令必定被PC寄存器指向过

栈是一种具备特殊访问方式的存储空间(后进先出,Last In First Out,LIFO)编程

15193998892055.jpg

问:上节课最后的例子进入死循环,那么死循环必定会形成崩溃吗??数组

状况一,每循环一次就会拉伸栈空间,当堆和栈碰头了之后会形成OOM(Out Of Memory)崩溃markdown

注意此时称为堆栈溢出,没有单独的堆溢出和栈溢出,堆从低地址向高地址延伸,栈空间从高地址向低地址延伸,系统给每一个进程分配必定的虚拟空间,当系统内存紧张或者进程本身的虚拟空间快用完时会以必定的策略决定先杀死那些进程编程语言

.text
.global _A


_A:
    sub sp,sp,#0x20
    stp x0,x1,[sp,#0x10]
    ldp x1,x0,[sp,#0x10]
    bl _A
    add sp,sp,#0x20      ;栈平衡
    ret
_
复制代码

状况二,每次循环都能栈平衡,那么就会一直执行,不会崩溃函数

.text
.global _A


_A:
    sub sp,sp,#0x20
    stp x0,x1,[sp,#0x10]
    ldp x1,x0,[sp,#0x10]
    add sp,sp,#0x20        ;栈平衡
    bl _A
    ret
_
复制代码

SP和FP寄存器

  • SP寄存器在任意时刻会保存咱们栈顶的地址
  • FP寄存器也称为x29寄存器,在函数嵌套时利用它来保存栈底的地址

ARM64开始,取消32位的LDM、STM、PUSH、POP指令,取而代之的是LDR、LDP、STR、STP,ARM64里面,对栈的操做是16字节对齐的post

函数调用栈

栈地址是从高地址向底地址开辟的,因此开辟地址是对SP指针减sub,回收栈空间是对SP指针作加add性能

sub sp,sp,#0x40           ;开辟了0x40(64字节)空间
stp x29,x30,[sp,#0x30]    ;x29/x30寄存器入栈保护
add x29,x29,#0x30         ;x20(fp)寄存器指向栈底的位置
...
ldp x29,x30,[sp,#0x30]    ;恢复x20、x30寄存器的值
add sp,sp,#0x40           ;栈平衡
ret
复制代码

关于内存的读写指令

注意,读写数据都是往高地址读写,例如开辟的32字节空间可是存储16字节的数据,那么先存储高地址的16字节优化

str(store register)指令

将数据从寄存器中读出来,存到内存中spa

ldr(load register)指令

将数据从内存中读出来,存到寄存器中,此ldr和str的变种ldp和stp能够同时操做两个寄存器,例如我想将x0寄存器的值存储到栈空间能够这样写3d

sub sp,sp,#0x10
str x0,[sp]        ;将x0寄存器的值存储到sp指向的栈空间
ldr x0,[sp]        ;将sp指向的栈上的值恢复到x0寄存器
add sp,sp,#0x10
复制代码

若是我想交换x0、x1两个寄存器的值,能够这样写

sub sp,sp,#0x20
stp x0,x1,[sp,#0x10]    ;将x0、x1寄存器的值存储到sp指向的栈空间
ldp x1,x0,[sp,#0x10]    ;将sp指向的栈空间的值存储到x一、x0寄存器上
add sp,sp,#0x10
复制代码
  • str\stp、ldr\ldp是专门用来操做寄存器和内存的指令
  • 咱们拿到sp指针之后先拉伸栈空间,再操做栈空间

练习

咱们新建工程并新建文件命名为asm.s

.text
.global _A

_A:
    sub sp,sp,#0x20
    mov x0,#0xaaaa
    mov x1,#0xbbbb
    stp x0,x1,[sp,#0x10]
    ldp x1,x0,[sp,#0x10]
    add sp,sp,#0x20
    ret
复制代码

image.png

咱们单步执行之,此时咱们即将拉伸栈空间,此时sp = 0x000000016af3dc50

image.png

栈空间拉伸32字节以后sp = 0x000000016af3dc30

image.png

此时咱们查看内存状况Debug -> Debug Workflow -> View Memory,那么这32字节就是咱们拉伸的栈空间

image.png

继续单步执行,咱们能够看到寄存器x0和x1被赋值了

image.png

image.png

继续单步执行指令stp x0, x1, [sp, #0x10]能够看到寄存器x0和x1的值已经被存储到栈空间上了

image.png

再次单步执行能够看到寄存器x0和x1的值交换了

image.png

栈平衡

image.png

能够看到此时栈上的值还在,栈平衡之后这就成了垃圾数据,下次拉伸栈的时候会将内存的值覆盖掉

image.png

bl和ret指令

bl指令

  • 将下一条指令的地址放入lr(x30)寄存器
  • 转到标号处指令

bl有两层含义,一是修改lr(x30)的值,另外一个是跳转

ret指令

  • 默认使用lr(x30)寄存器的值,经过底层指令提示CPU此处做为下条指令地址

ARM64平台的特点指令,它面向硬件作了优化处理

bl指令和ret指令是成对出现的,当遇到bl指令的时候lr存储下一条指令的地址,直到遇到ret指令会触发lr寄存器中的指令执行

练习

.text
.global _A,_B


_A:
    sub sp,sp,#0x20
    mov x0,#0xa
    mov x1,#0xb
    bl _B
    add sp,sp,#0x20
    ret


_B:
    mov x0,#0xb
    mov x1,#0xa
    ret
复制代码

image.png

咱们看到在遇到bl指令之前lr寄存器和pc寄存器存储的地址值是同样的 image.png

当遇到bl指令之后lr寄存器的值就再也不改变,直到遇到下一条bl指令或者ret指令,pc寄存器的值仍然指向即将执行的指令地址 image.png

image.png

image.png

再次遇到bl指令的以后lr的值发生了改变,保存了返回_A函数的地址 image.png

当遇到ret指令之后触发lr寄存器中存储的指令 image.png

image.png

再次遇到ret指令仍然会触发lr寄存器中存储的指令,此时问题就来了,lr跳转到_B函数之后保存了返回_A函数的地址,可是没有记录返回ViewDidLoad函数的地址,因而形成了死循环 image.png

一直循环 image.png

这就找到了上节中死循环的缘由

保存回家的路(lr寄存器)

函数跳转关系为ViewDidLoad -> _A -> _B

  • ViewDidLoad -> _A时lr寄存器保存了返回ViewDidLoad函数的地址
  • _A -> _B时lr寄存器保存了返回_A函数的地址
  • _A <- _B时能够正常经过lr寄存器保存的指令返回到_A函数
  • ViewDidLoad <- _A此时lr寄存器仍然存储的是回到_A函数的地址,因而形成死循环

解决方案就是当函数嵌套调用的时候保存一下回家的路(lr寄存器的值),那么咱们是否是能够保存在另外的寄存器中呢???这是不行的,谁也不肯定寄存器在以后的调用中会不会被使用到,因此咱们应该将lr寄存器保存在当前函数的栈空间中,做为局部变量保存起来,咱们能够这样修改

.text
.global _A,_B


_A:
    sub sp,sp,#0x20
    stp x29,x30,[sp,#0x10]    ;保存x29,x30的值
    mov x0,#0xa
    mov x1,#0xb
    bl _B
    ldp x29,x30,[sp,#0x10]    ;恢复x29,x30的值
    add sp,sp,#0x20
    ret


_B:
    mov x0,#0xb
    mov x1,#0xa
    ret
复制代码

此时lr保存的是返回_A函数的指令地址 image.png

此时从_A函数的栈中恢复了lr寄存器的值 image.png

遇到ret指令之后 image.png

能够正常返回ViewDidLoad函数,上节遗留的死循环问题解决🎉 image.png

这两句指令能够优化为一行指令

sub sp,sp,#0x10           ;拉伸栈空间
    stp x29,x30,[sp]    ;保存x29,x30的值
复制代码
stp x29,x30,[sp,#-0x10]!    ;拉伸栈空间并赋值
复制代码

一样如下这两句指令能够优化为一行指令

ldp x29,x30,[sp,#0x10]    ;恢复x29,x30的值
    add sp,sp,#0x10           ;栈平衡
复制代码
ldp x29,x30,[sp],#0x10    ;恢复x29,x30的值并恢复栈平衡
复制代码

再次强调一下对栈的操做是以16字节对齐的,切记切记

sub sp,sp,0x8
str x0,[sp]
ldr x0,[sp]      ;这是会出问题的
add sp,sp,0x8
复制代码

lr和pc小结

经过以上练习咱们知道当没有遇到bl指令时lr寄存器和pc寄存器保存的都是即将执行的指令地址,可是遇到bl指令之后lr寄存器的值就再也不改变,直到遇到ret指令或者另外一条bl指令才会改变,lr寄存器能够理解为函数嵌套调用时返回上一级函数的路径,pc寄存器只是简单指向下一条即将执行的指令。 当函数只有一级嵌套时咱们不须要对lr寄存器作操做,可是当函数多级嵌套时咱们就须要手动保存lr寄存器的值,不然会形成死循环

带参数的函数

不会写不要紧,写个高级函数看看系统怎么生成的

image.png

首先将参数保存在寄存器w0,w1中 image.png

先将寄存器w0,w1的值保存到栈上,再从栈上读取到寄存器w8,w9上,对w8,w9作加法结果保存到w0,函数执行结束,看起来很啰嗦,这或许跟编译时没有编译优化又关系 image.png

那么咱们就能够这样实现一个带参数的函数

.text
.global _A


_A:
    add x0,x0,x1
    ret
复制代码

执行结果没问题撒花鼓掌🎉👏 image.png

ARM64下,函数的参数时存放在x0-x7(w0-w7)这8个寄存器里面的,若是超过8个参数就会入栈,函数的返回值时存放在x0寄存器里面的,若是8字节装不下也会放在栈空间

为了效率考虑,咱们在写OC代码时参数总数最好不要超过6个,由于函数自己有两个隐形参数self和selector,若是必须超过6个最好使用数组或者结构体指针

相关文章
相关标签/搜索