栈是一种具备特殊访问方式的存储空间(后进先出,Last In First Out,LIFO)编程
问:上节课最后的例子进入死循环,那么死循环必定会形成崩溃吗??
数组
状况一,每循环一次就会拉伸栈空间,当堆和栈碰头了之后会形成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
_
复制代码
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字节优化
将数据从寄存器中读出来,存到内存中spa
将数据从内存中读出来,存到寄存器中,此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
复制代码
咱们单步执行之,此时咱们即将拉伸栈空间,此时sp = 0x000000016af3dc50
栈空间拉伸32字节以后sp = 0x000000016af3dc30
此时咱们查看内存状况Debug -> Debug Workflow -> View Memory
,那么这32字节就是咱们拉伸的栈空间
继续单步执行,咱们能够看到寄存器x0和x1
被赋值了
继续单步执行指令stp x0, x1, [sp, #0x10]
能够看到寄存器x0和x1
的值已经被存储到栈空间上了
再次单步执行能够看到寄存器x0和x1
的值交换了
栈平衡
能够看到此时栈上的值还在,栈平衡之后这就成了垃圾数据,下次拉伸栈的时候会将内存的值覆盖掉
bl有两层含义,一是修改lr(x30)的值,另外一个是跳转
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
函数的地址,因而形成了死循环
一直循环
这就找到了上节中死循环的缘由
函数跳转关系为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
复制代码
经过以上练习咱们知道当没有遇到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个最好使用数组或者结构体指针