ARM汇编(2)-函数

  目录

1. 关于CPU的补充

1.1 寄存器

CPU除了有控制器、运算器还有寄存器。其中寄存器的作用就是进行数据的临时存储。

CPU的运算速度是非常快的,为了性能CPU在内部开辟一小块临时存储区域,并在进行运算时先将数据从内存复制到这一小块临时存储区域中,运算时就在这一小快临时存储区域内进行。我们称这一小块临时存储区域为寄存器。

对于arm64系的CPU来说, 如果寄存器以x开头则表明的是一个64位的寄存器,如果以w开头则表明是一个32位的寄存器,在系统中没有提供16位和8位的寄存器供访问和使用。其中32位的寄存器是64位寄存器的低32位部分并不是独立存在的。

1.2 高速缓存

iPhoneX上搭载的ARM处理器A11它的1级缓存的容量是64KB,2级缓存的容量8M.

CPU每执行一条指令前都需要从内存中将指令读取到CPU内并执行。而寄存器的运行速度相比内存读写要快很多,为了性能,CPU还集成了一个高速缓存存储区域.当程序在运行时,先将要执行的指令代码以及数据复制到高速缓存中去(由操作系统完成).CPU直接从高速缓存依次读取指令来执行.

1.3 寄存器

1.3.1 数据地址寄存器

数据地址寄存器通常用来做数据计算的临时存储、做累加、计数、地址保存等功能。定义这些寄存器的作用主要是用于在CPU指令中保存操作数,在CPU中当做一些常规变量来使用。
ARM64中:

  • 64位 x0-x30,XZR(零寄存器)
  • 32位 w0-w30,WZR(零寄存器)

1.3.2 浮点和向量寄存器

因为浮点数的存储以及其运算的特殊性,CPU中专门提供浮点数寄存器来处理浮点数。

  • 64位: d0-d31
  • 32位: d0-d31

现在的CPU支持向量运算.(向量运算在图形处理相关的领域用得非常的多)为了支持向量计算系统了也提供了众多的向量寄存器.

向量寄存器 128位:V0-V31

1.3.3 SP、FP寄存器

说这两个,需要先说一下栈。

栈是一种具有特殊的访问方式的存储空间,先进后处,后进先出。(Last In Out First)

  • sp寄存器在任意时刻会保存栈顶的地址。
  • fp寄存器也成为x29寄存器。属于通用寄存器,在默写时刻我们利用它保存栈底的地址。

需要注意的是,ARM64里面对栈的操作是16个字节对齐的。

这个图很好的说明了栈是从高地址往低地址开始读写操作的,堆是从低地址向高地址开始的,当栈不断的开辟空间,堆也不断的开辟空间,导致两个区域重叠,就会导致崩溃。也就是常说的堆栈溢出。(堆、栈上的空间是不固定的)

这里我们说个题外话,是不是所有的死循环都会导致崩溃?答案是否定的,只有不断的开辟空间的死循环才会导致崩溃,上一章我们最后的例子就是很好的说明,因为没有开辟空间。

2. 函数调用栈

以下代码是常见的函数调用开辟和恢复栈空间。

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

这里需要注意的是: 读、写数据都是往高地址读、写。

2.1 内存读写指令

  • str指令:store register,将数据从寄存器中读出来,存在内存中。每次操作8个字节
  • ldr指令:load register,将数据从内存中读出来,存在寄存器中。每次操作8个字节
  • stp指令:str指令的变种,每次操作16个字节。
  • ldp指令:ldr指令的变种,每次操作16个字节。

2.2 堆栈操作

_ABTest:
    sub sp, sp, #0x20       ; 开辟栈空间,在当前sp所在的位置减去32个字节。
    stp x0, x1, [sp, #0x10] ; 之所以用[],是因为sp存的是一个地址,这里的操作是寻址,把x0,x1的值放在对应的位置,但是栈的读写都是在高位,所以这里还需要加上一个值,写在高位
    ldp x1, x0, [sp, #0x10] ; 这里是交换x0,x1的值。注意,当前的操作不会改变sp的值,寄存器中的值进行交换
    add sp, sp, #0x20       ; 这里恢复栈空间。
    ret
  1. 我们将上面的代码放在“.s”文件中,在ViewControler中声明int ABTest();方法.
  2. 在viewDidLoad中调用ABTest();,并在这一行打上断点。运行触发断点之后,按住ctrl键的同时点击小箭头,进入汇编,(按住ctrl是为了不让程序执行下一步)
  3. 在右下命令行中输入register read sp查看当前sp所在的位置,是sp = 0x000000016fbf1290
  4. 点击下一步,开辟栈空间,重复第3步的操作,查看sp = 0x000000016fbf1270
  5. 进入View Memory,定位到sp所在的位置,查看在0x000000016fbf1280位置的值是什么。
  6. 这个时候,分别执行register write x0 0x0aregister write x1 0x0b,修改x0,x1的值,执行下一步。
  7. 发现在左边通用寄存器中x0,x1的值已经发生变化。这时候重复第5步操作。查看是否已经发生变化。(需要切换页)
  8. 执行下一步,交换x0,x1的值。我们发现左边,通用寄存器中x0,x1的值已经发生了变化,这时候重复第5步,查看内存中的值是否有变化?是没有发生变化的哈~
  9. 销毁当前栈空间。重复第3步,查看当前sp的地址。是sp = 0x000000016fbf1290

如图:

3. bl和ret指令

3.1 bl

bl其实存在两个操作:

  1. 将下一条指令的地址放入lr(x30)寄存器。也就是保存回家的路。
  2. 转到对应的跳转中执行指令,当指令执行完成后,会根据lr中的地址,返回继续执行。

通俗的讲就是离家出走了,执行ret的时候,根据lr中的地址,找到回家的路。

3.2 ret

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

3.3 x30寄存器(lr寄存器)

x30寄存器存放的是函数的返回地址,当ret指令执行时,会寻找x30寄存器保存的地址值。

这也就是,为啥上一章,最后的代码会造成循环引用的原因,因为x30寄存器的地址指向的就是当前bl的下一行代码。

3.4 操作

我们简写一下上一章的代码

_A:
    mov x0, 0xaa
    bl _B
    mov x0, 0xaa
    ret

_B:
    mov x0, #0xbb
    ret
  1. 在ViewDidLoad中执行A(),并打断点。执行上面的代码。按住ctrl键点击小剪头,进入A的汇编。查看当前lr寄存器中存放的地址是谁。然后按照下图所示进行操作,进入ViewDidLoad的汇编。
  2. 我们看到了19行执行了 bl A的操作,也就是在ViewDidLoad中执行A()操作。而lr寄存器所存储的地址就是第20行所在的位置,也就是存储了执行A之后返回ViewDidLoad的地址。0x1003ce56c
  3. 点击继续执行,修改x0寄存器的值,继续下一步。执行bl B
  4. 这时候我们发现lr寄存器中存储的值已经被修改了,变成了A汇编代码中bl B下一行的地址。lr = 0x1003ce904,这里修改了x0的值。
  5. 下一步。继续执行B中的ret操作,发现回到了A,回到了0x1003ce904,继续执行发现修改了x0的值。
  6. 下一步,执行ret,发现又回到了A中的0x1003ce904,不断的执行,发现压根回不去ViewDidLoad了。

这就是上一章中说的问题,lr寄存器的值被修改了,导致回不去了。那我们应该怎么处理呢?

最合理的方案是在执行bl操作之前,将bl的下一行地址存放在栈中。如果将值存放在其他寄存器中是绝对不安全的,因为你不知道什么时候就会被系统覆盖。

3.4.1 解决死循环

我们为了解决上面的问题,我们查看系统是怎么处理这个问题的。

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

void d() {

}

同样,在ViewDidLoad中执行c()

Demo`c:
->  0x1005464e0 <+0>:  stp    x29, x30, [sp, #-0x10]!
    0x1005464e4 <+4>:  mov    x29, sp
    0x1005464e8 <+8>:  bl     0x1005464f4               ; d at ViewController.m:38:1
    0x1005464ec <+12>: ldp    x29, x30, [sp], #0x10
    0x1005464f0 <+16>: ret 

在c的汇编里头,我们仔细看下系统是什么处理lr寄存器的。
我们看到了x29和x30两个寄存器。x29是fp寄存器,指向栈底;x30寄存器就是lr寄存器。

  1. stp x29, x30, [sp, #-0x10]! 这是汇编代码简写的形式的。这句话的意思是sp -= 0x10开辟空间,把x29和x30寄存器的值存放在开辟的空间里。“!”的操作是针对sp的,“[]”的操作是针对x29,x30寻址的。需要注意的是,先存值,在改变sp。
  2. mov x29, sp 将sp的值赋给x29寄存器。啥意思,fp跟sp指向相同的位置。栈顶栈底指向同一位置,啥情况?之后说哈~
  3. bl操作,执行d()
  4. ldp x29, x30, [sp], #0x10 跟第一句差不多,“[]”就是寻址,将sp对应的两个地址的值赋值给x29,x30。第一步是存,这一步是取。然后执行 sp += 0x10的操作,释放栈空间。
  5. 执行ret操作,我们就能轻松的回到ViewDidLoad了。因为lr寄存器中的地址正是我们一开始存的值。

在执行的过程中,我们一步步查看lr寄存器的值看是怎么变化的。就能清晰明了了。

这个时候,我们就可以修改上面的代码了

_A:
    str x30, [sp, #-0x10]!  ;仿造系统方法,因为x29寄存器暂时没有用处,所以只使用x30。
    mov x0, 0xaa
    bl _B
    mov x0, 0xaa
    ldr x30, [sp], #0x10
    ret

_B:
    mov x0, #0xbb
    ret

执行该代码,我们按照栈操作3.4的流程,查看整体流程,看x30寄存器存放读取的过程,配合View Memory使用会更爽哈~

这里把代码做一下修改,在A中str x30, [sp, #-0x8]!将16个字节改成8个字节会怎样?跑一遍试试看

会发生crash对不对。因为在ARM64里面,对栈的操作是16个字节对齐的。所以开辟空间操作一定是16字节的倍数来进行的。

4. 函数的参数和返回值

ARM64下,函数的参数是存放在x0-x7(32位w0-w7)这个8个寄存器里面的。如果超过8个参数,就会入栈。
函数的返回值是放在x0(32位是w0)寄存器里的。

这里有一个点,在OC中,一般情况下,定义函数最多可以有几个参数?这里有一个小坑哈~
在runtime里,我们知道,函数调用都是通过objc_msgsend来处理的,而这里个里头已经存在了两个默认参数,一个是self,一个obj

当我们不知道怎么处理带参数的函数时,就看系统是怎么实现的。

/// 我们定义一个函数,在viewDidLoad中执行。
int sumA(int a, int b) {
    return a + b;
}

执行之后,按住control点击进汇编:

首先我们来到viewDidLoad中,
-[ViewController viewDidLoad]:
    ; 这里我们有看到赋值,sumA(10+20),我们看到w0=10,w1=20
    0x104d125d4 <+68>: mov    w0, #0xa          
    0x104d125d8 <+72>: mov    w1, #0x14
->  0x104d125dc <+76>: bl     0x104d12570               ; sumA at ViewController.m:16  ; 这里有bl指令,继续执行跳转到sumA操作。
    0x104d125e0 <+80>: ldp    x29, x30, [sp, #0x20]
    0x104d125e4 <+84>: add    sp, sp, #0x30             ; =0x30 
    0x104d125e8 <+88>: ret    
    
---------------------------------------------------------------------
FunctionDemo sumA:
->  0x100d3a4dc <+0>:  sub    sp, sp, #0x10     ; 开辟16个字节的空间
    0x100d3a4e0 <+4>:  str    w0, [sp, #0xc]    ; 寻址把w0存放在sp+0xC的位置
    0x100d3a4e4 <+8>:  str    w1, [sp, #0x8]    ; 寻址把w1存放在sp+0x8的位置
    0x100d3a4e8 <+12>: ldr    w8, [sp, #0xc]    ; 把sp+0xC位置的值给w8
    0x100d3a4ec <+16>: ldr    w9, [sp, #0x8]    ; 把sp+0x8位置的值给w9
    0x100d3a4f0 <+20>: add    w0, w8, w9        ; 执行加法操作,并赋值给w0
    0x100d3a4f4 <+24>: add    sp, sp, #0x10     ; 释放栈空间
    0x100d3a4f8 <+28>: ret    

通过上面汇编之后的代码,我们可以看到整个的流程,相当于生成了两个临时量变去存储传进来的值,然后把返回值存储在w0寄存器里。

/// 我们定义一个函数,在viewDidLoad中执行。
int sumA(int a, int b) {
    int a1 = 1;     // 生成局部变量a1,b1
    int b1 = b;
    return a1 + b1;
}

通过上面系统的实现方案,我们就可以自己写一个带有参数,返回值的方法。在“.s”文件中实现

.global _sumB

_sumB:
    add x0, x0, x1
    ret

4.2 验证超过8个参数的情况

多余的参数会存放在调用方法所在的栈空间里,然后在调用的方法里去取别人的栈中存放的参数。

int test(int a, int b, int c, int d, int e, int f, int g, int h, int i) {
    return a+b+c+d+e+f+g+h+i;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    test(1,2,3,4,5,6,7,8,9);
}

执行代码,我们看汇编之后的代码:

-[ViewController viewDidLoad]:
...
... 这中间省略了一大部分代码,我们直接从这里看
    ; 这里打印 sp = 0x000000016f4c53c0
    0x10093e594 <+64>:  bl     0x10093e9b4               ; symbol stub for: objc_msgSendSuper2  
    ; 这个是调用super viewDidLoad
    0x10093e598 <+68>:  mov    w0, #0x1     ; 将1存到w0寄存器中
    0x10093e59c <+72>:  mov    w1, #0x2
    0x10093e5a0 <+76>:  mov    w2, #0x3
    0x10093e5a4 <+80>:  mov    w3, #0x4
    0x10093e5a8 <+84>:  mov    w4, #0x5
    0x10093e5ac <+88>:  mov    w5, #0x6
    0x10093e5b0 <+92>:  mov    w6, #0x7     ; 这些值我们是可以在通用寄存器里看到的
    0x10093e5b4 <+96>:  mov    w7, #0x8     ; 将8存到w7寄存器中
    ; x8 = 0x0000000100940ce8  "viewDidLoad"
    0x10093e5b8 <+100>: mov    x8, sp       ; 这里是把sp栈顶的位置放在x8寄存器中。
    ; x8 = 0x000000016f4c53c0
    0x10093e5bc <+104>: mov    w10, #0x9    ; 把9放在w10寄存器
    0x10093e5c0 <+108>: str    w10, [x8]    ; 把w10寄存器中的值,放在x8寄存器所在的地址里
    ; 也就是在sp的位置,存放了9这个变量。
->  0x10093e5c4 <+112>: bl     0x10093e4dc               ; sumA at ViewController.m:16      ; 这里执行 sumA
    0x10093e5c8 <+116>: ldp    x29, x30, [sp, #0x30]    ; x29,x30取值,是为了函数返回
    0x10093e5cc <+120>: add    sp, sp, #0x40             ; =0x40 ; 释放栈空间
    0x10093e5d0 <+124>: ret

接下来,我们看test的汇编代码情况:

test:
    ; 开辟空间之前 sp = 0x000000016f4c53c0
->  0x10093e4dc <+0>:   sub    sp, sp, #0x30 ; =0x30
    ; 开辟栈空间后,sp=0x000000016f4c5390
    0x10093e4e0 <+4>:   ldr    w8, [sp, #0x30]  ; 这是从sp+0x30的位置取值,放在w8寄存器里。
    ; sp+0x30就是开辟当前栈空间之前的位置,也就是viewDidLoad开辟空间的栈顶位置,这个位置是x8寄存器指向的位置,存放的是变量9
    0x10093e4e4 <+8>:   str    w0, [sp, #0x2c]  ; 把w0寄存器的值存放在栈sp+0x2c里头,也就是sp偏移4个字节,正好存放一个int类型的数据。
    0x10093e4e8 <+12>:  str    w1, [sp, #0x28]
    0x10093e4ec <+16>:  str    w2, [sp, #0x24]
    0x10093e4f0 <+20>:  str    w3, [sp, #0x20]
    0x10093e4f4 <+24>:  str    w4, [sp, #0x1c]
    0x10093e4f8 <+28>:  str    w5, [sp, #0x18]
    0x10093e4fc <+32>:  str    w6, [sp, #0x14]
    0x10093e500 <+36>:  str    w7, [sp, #0x10]
    0x10093e504 <+40>:  str    w8, [sp, #0xc]   ; w8寄存器的值放在sp+0xc里,w8=9
    0x10093e508 <+44>:  ldr    w8, [sp, #0x2c]  ; 赋值操作 w8=1
    0x10093e50c <+48>:  ldr    w9, [sp, #0x28]  ; w9 = 2
    0x10093e510 <+52>:  add    w8, w8, w9       ; w8 = w8+w9 = 1+2 = 3
    0x10093e514 <+56>:  ldr    w9, [sp, #0x24]  ; w9 = 3
    0x10093e518 <+60>:  add    w8, w8, w9       ; w8 += w9 = 3 + 3
    0x10093e51c <+64>:  ldr    w9, [sp, #0x20]
    0x10093e520 <+68>:  add    w8, w8, w9
    0x10093e524 <+72>:  ldr    w9, [sp, #0x1c]
    0x10093e528 <+76>:  add    w8, w8, w9
    0x10093e52c <+80>:  ldr    w9, [sp, #0x18]
    0x10093e530 <+84>:  add    w8, w8, w9
    0x10093e534 <+88>:  ldr    w9, [sp, #0x14]
    0x10093e538 <+92>:  add    w8, w8, w9
    0x10093e53c <+96>:  ldr    w9, [sp, #0x10]
    0x10093e540 <+100>: add    w8, w8, w9
    0x10093e544 <+104>: ldr    w9, [sp, #0xc]
    0x10093e548 <+108>: add    w0, w8, w9       ; 计算完成
    0x10093e54c <+112>: add    sp, sp, #0x30    ; =0x30 ,释放栈空间
    0x10093e550 <+116>: ret

这里会把9这个参数存放在viewDidLoad所开辟的栈空间里。执行test后,1-8会存放在test函数所开辟的空间中,然后把9这个参数从viewDidLoad所开辟的栈空间里拿回来,是通过x8寄存器来定位地址获取9这个参数的。相当于从别人家借东西,会存在sp计算的问题,会影响效率。

我们一定要知道的一点是,栈的读写都是从高位往低位进行读写,栈空间的读写都是基于上述原则进行操作的。
以上操作,配合View Memory查看内存中的数据会更清晰。

4.2.1 release下操作

我们的这一系列操作都是在debug模式下进行的,加法的计算产生的汇编代码竟然是如此繁杂。如果我们切换到release下运行,会有什么情况发生?

在release下,编译器会进行优化,我们的test方法,只是做了调用,没有任何实际意义,所以在release下根本不会有bl指令。

如果我们执行printf("%d", sumA(1,2,3,4,5,6,7,8,9));呢?

其实差别不大,经过系统优化之后,就只剩下mov w8, #0x2d这一句代码了,0x2d = 45。就是这么简单直接。

4.3 验证返回值

如果返回值超过8个字节,x0寄存器存不下的时候,会通过栈空间来返回。

struct NumA getStructA(int a, int b, int c, int d, int e, int f) {
    struct NumA num;
    num.a = a;
    num.b = b;
    num.c = c;
    num.d = d;
    num.e = e;
    num.f = f;
    return num;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    struct NumA num = getStructA(1,2,3,4,5,6);
}

这里呢,我们返回一个结构体,正常来说,结构体的大小是根据结构体中的变量决定的。这里有6个int类型的变量也就是24个字节。

getStructA:
->  0x1025424a0 <+0>:  sub    sp, sp, #0x20    ; =0x20 开辟栈空间
    0x1025424a4 <+4>:  str    w0, [sp, #0x1c]
    0x1025424a8 <+8>:  str    w1, [sp, #0x18]
    0x1025424ac <+12>: str    w2, [sp, #0x14]
    0x1025424b0 <+16>: str    w3, [sp, #0x10]
    0x1025424b4 <+20>: str    w4, [sp, #0xc]
    0x1025424b8 <+24>: str    w5, [sp, #0x8]
    0x1025424bc <+28>: ldr    w9, [sp, #0x1c]
    0x1025424c0 <+32>: str    w9, [x8]
    0x1025424c4 <+36>: ldr    w9, [sp, #0x18]
    0x1025424c8 <+40>: str    w9, [x8, #0x4]
    0x1025424cc <+44>: ldr    w9, [sp, #0x14]
    0x1025424d0 <+48>: str    w9, [x8, #0x8]
    0x1025424d4 <+52>: ldr    w9, [sp, #0x10]
    0x1025424d8 <+56>: str    w9, [x8, #0xc]
    0x1025424dc <+60>: ldr    w9, [sp, #0xc]
    0x1025424e0 <+64>: str    w9, [x8, #0x10]
    0x1025424e4 <+68>: ldr    w9, [sp, #0x8]
    0x1025424e8 <+72>: str    w9, [x8, #0x14]
    0x1025424ec <+76>: add    sp, sp, #0x20             ; =0x20 
    0x1025424f0 <+80>: ret  

这里我们又看到了一个熟悉的x8寄存器。然后通过w9寄存器,不断的赋值给x8寄存器对应的空间里。那这个x8寄存器是怎么个情况呢,我们返回viewDidLoad对应的汇编代码

-[ViewController viewDidLoad]:
...
... ;这里也是截取部分代码
0x1025425ac <+64>:  bl     0x1025429b4               ; symbol stub for: objc_msgSendSuper2
    0x1025425b0 <+68>:  add    x8, sp, #0x8              ; =0x8 
    0x1025425b4 <+72>:  mov    w0, #0x1
    0x1025425b8 <+76>:  mov    w1, #0x2
    0x1025425bc <+80>:  mov    w2, #0x3
    0x1025425c0 <+84>:  mov    w3, #0x4
    0x1025425c4 <+88>:  mov    w4, #0x5
    0x1025425c8 <+92>:  mov    w5, #0x6
    0x1025425cc <+96>:  bl     0x1025424a0               ; getStructB at ViewController.m:46
->  0x1025425d0 <+100>: ldp    x29, x30, [sp, #0x40]
    0x1025425d4 <+104>: add    sp, sp, #0x50             ; =0x50 
    0x1025425d8 <+108>: ret    

我们看到x8寄存器的位置是sp偏移8个字节。也就是返回值所在的空间是在viewDidLoad开辟的栈空间里。

这里会当前返回值存放在viewDidLoad所开辟的栈空间里,因为知道返回的是什么类型的数据,在viewDidLoad开辟空间时,就已经把返回值所需要的空间给预留出来了。通过x8寄存器来定位返回值所在的空间。

那么,这里为什么要偏移8个字节?

我们知道,ARM64对栈的操作是16个字节进行对齐的。而结构体占有24个字节,我们只能通过补齐来确保是16个字节的倍数来开辟空间。

执行对应的方法,对返回值的变量进行存储(根据x8寄存器来定位相应的地址存储变量的值)。

5. 函数的局部变量

int sumC(int a, int b) {
    int c = 10;
    return a+b+c;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    sumC(1,2);
  }

运行,进入汇编模式

Demo`-[ViewController viewDidLoad]:
    0x1026ae45c <+68>: mov    w0, #0x1
    0x1026ae460 <+72>: mov    w1, #0x2
->  0x1026ae464 <+76>: bl     0x1026ae3e8               ; sumC at ViewController.m:75
    0x1026ae468 <+80>: ldp    x29, x30, [sp, #0x20]
    0x1026ae46c <+84>: add    sp, sp, #0x30             ; =0x30 
    0x1026ae470 <+88>: ret

sumC(1, 2):1和2分别放在了w0、w1寄存器中。然后执行bl,进入函数sumC

Demo`sumC:
->  0x1026ae3e8 <+0>:  sub    sp, sp, #0x10             ; =0x10 
    0x1026ae3ec <+4>:  str    w0, [sp, #0xc]
    0x1026ae3f0 <+8>:  str    w1, [sp, #0x8]
    0x1026ae3f4 <+12>: mov    w8, #0xa
    0x1026ae3f8 <+16>: str    w8, [sp, #0x4]
    0x1026ae3fc <+20>: ldr    w8, [sp, #0xc]
    0x1026ae400 <+24>: ldr    w9, [sp, #0x8]
    0x1026ae404 <+28>: add    w8, w8, w9
    0x1026ae408 <+32>: ldr    w9, [sp, #0x4]
    0x1026ae40c <+36>: add    w0, w8, w9
    0x1026ae410 <+40>: add    sp, sp, #0x10             ; =0x10 
    0x1026ae414 <+44>: ret
  1. 开辟16个字节的内存空间
  2. 把w0放在[sp+0xc],w1放在[sp+0x8]
  3. w8赋值等于0xa,这里就是我们的局部变量c=10
  4. 然后把w8放在[sp+0x4]里头
  5. 一堆操作,ret

看到了吧,函数的参数和局部变量都是放在栈里的。

6. 函数嵌套

int funcSum(int a, int b, int c) {
    int d = a + b + c;
    printf("%d", d);
    return d;
}

int totalSum(int a, int b) {
    int c = 10;
    int d = funcSum(a, b, c);
    return d;
}


- (void)viewDidLoad {
    [super viewDidLoad];
    totalSum(1, 2);
}

我们执行上面的含有局部变量的嵌套函数,看是怎么在汇编下执行的。

Demo`-[ViewController viewDidLoad]:
    ...
    ...
    0x1002fa43c <+64>: bl     0x1002fa8d0               ; symbol stub for: objc_msgSendSuper2
    // totalSum(1, 2)
    0x1002fa440 <+68>: mov    w0, #0x1  // 将1存在w0寄存器里
    0x1002fa444 <+72>: mov    w1, #0x2  // 2存放在w1寄存器里
->  0x1002fa448 <+76>: bl     0x1002fa3bc               ; totalSum at ViewController.m:86
    0x1002fa44c <+80>: ldp    x29, x30, [sp, #0x20]     ; x29、x30寄存器取值(lr寄存器获取回家的路)
    0x1002fa450 <+84>: add    sp, sp, #0x30             ; =0x30 
    0x1002fa454 <+88>: ret

这一坨汇编代码,已经看过无数次了,这里不细说了,直接走totalSum看看是怎么处理的。

Demo`totalSum:
->  0x1002fa3bc <+0>:  sub    sp, sp, #0x20             ; =0x20 
    0x1002fa3c0 <+4>:  stp    x29, x30, [sp, #0x10]
    0x1002fa3c4 <+8>:  add    x29, sp, #0x10            ; =0x10 
    0x1002fa3c8 <+12>: stur   w0, [x29, #-0x4]  ; 把totalSum的参数w0存放在栈底的位置
    0x1002fa3cc <+16>: str    w1, [sp, #0x8]    ; 把w1的值放在栈顶+8个字节的位置
    0x1002fa3d0 <+20>: mov    w8, #0xa          ; 获取局部变量10,放在w8寄存器
    0x1002fa3d4 <+24>: str    w8, [sp, #0x4]    ; w8的值放在sp+4个字节的位置
    0x1002fa3d8 <+28>: ldur   w0, [x29, #-0x4]  ; 重新对w0赋值,取值的位置就是之前w0存放的位置 w0=1
    0x1002fa3dc <+32>: ldr    w1, [sp, #0x8]    ; w1取值w1=2
    0x1002fa3e0 <+36>: ldr    w2, [sp, #0x4]    ; w2 = 10
    0x1002fa3e4 <+40>: bl     0x1002fa35c       ; funcSum at ViewController.m:80 ;执行嵌套函数 funcSum。
    0x1002fa3e8 <+44>: str    w0, [sp]          ; 把w0的值存在sp对应的位置。
    0x1002fa3ec <+48>: ldr    w0, [sp]          ; 获取w0
    0x1002fa3f0 <+52>: ldp    x29, x30, [sp, #0x10] ; 找到回家的路
    0x1002fa3f4 <+56>: add    sp, sp, #0x20     ; =0x20 释放
    0x1002fa3f8 <+60>: ret

这里用到了sturldur。这两个的本质与strldr没有区别,只是带u的偏移的是一个负值。

这里也有用到x29寄存器,还有印象吗?x29寄存器就是fp寄存器,指向的是栈底的位置。从栈的存储空间来看,栈底的地址比栈顶大,所以sp栈顶开辟空间都是减去一个值,而用栈底fp做关键值时,要想获取数据都必须在sp-fp之间拿值,所以基于fp的操作都是【减】。

这里为什么把局部变量的值存在w8里面,就是因为w0-w7是存放函数参数的参数,之前说过,w8用来获取局部变量。

funcSum函数的汇编就不说了,与之前的没什么区别。

这里需要提一句的是,为啥要把参数先存放在内存里,然后再取出来,难道就不嫌麻烦吗?其主要目的就是为了保护参数,防止被改变。

到最后w0/x0寄存器还是用来存放返回值。

7. 补充内容

  1. 一个函数的参数,在函数执行完毕之后,是否能拿到这个参数的值?我们用4.2小结的代码来解释一下。
int test(int a, int b, int c, int d, int e, int f, int g, int h, int i) {
    return a+b+c+d+e+f+g+h+i;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    test(1,2,3,4,5,6,7,8,9);
}
这个test函数有9个参数,我们知道,x0-x7(w0-w7)这个8个寄存器是存放函数变量的,如果超过8个参数,则会存放在viewDidLoad函数开辟的栈空间内,也就是说1-8这8个参数是在test函数开辟的栈空间。这8个参数在test函数执行完毕之后,随着空间的释放就拿不到了,而9这个参数存放在`viewDidLoad`的栈空间,我们还可以拿到。
  1. 在4.3小结,我们返回的是一个结构体,而不是一个指针,假如,我们添加一个函数,来调用这个返回的结构体,这个结构体能不能用。
struct NumA getStructA(int a, int b, int c, int d, int e, int f) {
    struct NumA num;
    num.a = a;
    num.b = b;
    num.c = c;
    num.d = d;
    num.e = e;
    num.f = f;
    return num;
}

struct NumA returnStruct() {
    struct NumA num = getStructA(1,2,3,4,5,6);
    return num;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    struct NumB num = returnStruct();
    printf("a = %d\n", num.a);  // 这里是否能输出,还是会crash
}
肯定是可以输出的,在`viewDidLoad`函数执行时,就已经创建了`struct NumB`所需要的空间了,返回的数据都存在于`viewDidLoad`的栈空间里,所以还是可以正常执行的。

总结

  1. 栈:引出SP、FP寄存器。SP:保存栈顶地址,FP:保存栈底的地址。(栈顶的地址比栈底的地址小,所以获取栈顶的值都是通过sub sp, sp #0x10,是减去一个空间,在存值的时候一般都是[sp+#0x08])
  2. stp/str 存值(16个字节/8个字节)
  3. ldp/ldr 取值(16个字节/8个字节)
  4. stur/ldur 本质上与str/ldr没有区别,带【u】的操作的是一个负值。
  5. bl指令:通过lr(x30)寄存器,保存回家的路,bl跳转到对应的方法
  6. lr寄存器的值会通过保存在栈空间,来确保能够正确的返回。
  7. 函数的参数:存放在x0-x7寄存器,超过8个,则放在栈里。
  8. 返回值:使用x0寄存器保存,如果大于8个字节,会利用栈空间传递。
  9. 函数的局部变量放在栈里,嵌套函数的值也是放在栈里
  10. 会把变量的值放在内存里保护起来,用的时候在去取值

文章作者: 改变世界
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 改变世界 !
评论