上节回顾
- 汇编概述
- 使用助记符代替机器指令的一种编程语言
- 汇编和机器指令一一对应的关系,拿到二进制可以反汇编
- 由于汇编和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)
问:上节课最后的例子进入死循环,那么死循环一定会造成崩溃吗??
情况一,每循环一次就会拉伸栈空间,当堆和栈碰头了以后会造成OOM(Out Of Memory)崩溃
注意此时称为堆栈溢出,没有单独的堆溢出和栈溢出,堆从低地址向高地址延伸,栈空间从高地址向低地址延伸,系统给每个进程分配一定的虚拟空间,当系统内存紧张或者进程自己的虚拟空间快用完时会以一定的策略决定先杀死那些进程
.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字节对齐的
函数调用栈
栈地址是从高地址向底地址开辟的,所以开辟地址是对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)指令
将数据从寄存器中读出来,存到内存中
ldr(load register)指令
将数据从内存中读出来,存到寄存器中,此ldr和str的变种ldp和stp可以同时操作两个寄存器,例如我想将x0寄存器的值存储到栈空间可以这样写
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指向的栈空间的值存储到x1、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
我们单步执行之,此时我们即将拉伸栈空间,此时sp = 0x000000016af3dc50
栈空间拉伸32字节之后sp = 0x000000016af3dc30
此时我们查看内存情况Debug -> Debug Workflow -> View Memory,那么这32字节就是我们拉伸的栈空间
继续单步执行,我们可以看到寄存器x0和x1被赋值了
继续单步执行指令stp x0, x1, [sp, #0x10]可以看到寄存器x0和x1的值已经被存储到栈空间上了
再次单步执行可以看到寄存器x0和x1的值交换了
栈平衡
可以看到此时栈上的值还在,栈平衡以后这就成了垃圾数据,下次拉伸栈的时候会将内存的值覆盖掉
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
我们看到在遇到bl指令以前lr寄存器和pc寄存器存储的地址值是一样的
当遇到bl指令以后lr寄存器的值就不再改变,直到遇到下一条bl指令或者ret指令,pc寄存器的值仍然指向即将执行的指令地址
再次遇到bl指令的之后lr的值发生了改变,保存了返回_A函数的地址
当遇到ret指令以后触发lr寄存器中存储的指令
再次遇到ret指令仍然会触发lr寄存器中存储的指令,此时问题就来了,lr跳转到_B函数以后保存了返回_A函数的地址,但是没有记录返回ViewDidLoad函数的地址,于是造成了死循环
一直循环
这就找到了上节中死循环的原因
保存回家的路(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函数的指令地址
此时从_A函数的栈中恢复了lr寄存器的值
遇到ret指令以后
可以正常返回ViewDidLoad函数,上节遗留的死循环问题解决🎉
这两句指令可以优化为一行指令
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寄存器的值,否则会造成死循环
带参数的函数
不会写没关系,写个高级函数看看系统怎么生成的
首先将参数保存在寄存器w0,w1中
先将寄存器w0,w1的值保存到栈上,再从栈上读取到寄存器w8,w9上,对w8,w9做加法结果保存到w0,函数执行结束,看起来很啰嗦,这或许跟编译时没有编译优化又关系
那么我们就可以这样实现一个带参数的函数
.text
.global _A
_A:
add x0,x0,x1
ret
执行结果没问题撒花鼓掌🎉👏
ARM64下,函数的参数时存放在x0-x7(w0-w7)这8个寄存器里面的,如果超过8个参数就会入栈,函数的返回值时存放在x0寄存器里面的,如果8字节装不下也会放在栈空间
为了效率考虑,我们在写OC代码时参数总数最好不要超过6个,因为函数本身有两个隐形参数self和selector,如果必须超过6个最好使用数组或者结构体指针