2016976438 发表于 2023-4-28 16:33

<luckfollowme> arm 学习篇06 - 调用约定


# luckfollowme arm 学习篇06 - 调用约定



在 arm64 的调用约定中

前八个整数参数依次存储在 x0-x7 中。

其余的参数会入栈

调用完毕后 返回结果会放入在 x0 中



所以您目前需要留意几个寄存器

**x0~x7 储存参数的**

**x29(Frame Pointer)帧指针**

**x30(LR) 链接寄存器用于跳回去的地址**

**x31(SP) 堆栈指针**



## 调整项目

为了更清晰 的理解 arm64 的调用约定,我们把 sum 的传参改成 10位。并把剩下两个参数 改成

**uint64** 这样更能体验 **64位** 的数据储存



```c
#include "stdio.h"
#include "stdint.h"

uint64_t _global_sum;

int sum(int a1, int a2,int a3,int a4,int a5,int a6,int a7,int a8,uint64_t a9,uint64_t a10)
{

      //4. 测试加法 和 局部变量
      int sum = a1 + a2 + a3 + a4 + a5 + a6 + a7+ a8;
      uint64_t sum2 = a9 + a10;
      //5. 测试状态寄存器 和 条件跳转
      if (sum > 10)
      {
          //6. 测试全局变量
          _global_sum = sum + sum2;
      }
      // 7. 测试返回值
      return _global_sum;
}

int main()
{
      //1. 测试b 指令 死循环
      while (true)
      {
          //2. 测试局部变量赋值指令
          uint64_t a9 = 10;
          uint64_t a10 = 20;
          //3. 测试传参 和 调用方法
          int ret = sum(1,2,3,4,5,6,7,8,a9,a10);
          printf("sum:%d\n", ret);

          getchar();
      }
      return 0;
}

```



## 指令集分析

下面是 **main 方法**最终生成的 指令集:

```asm
loc_788                                 ; CODE XREF: main+10↑j
                                        ; main+74↓j
MOV             X8, #0xA
STUR            X8,
MOV             X8, #0x14
STR             X8,
LDUR            X10,
LDR             X8,
MOV             X9, SP
STR             X10,                ; a9
STR             X8,              ; a10
MOV             W0, #1                  ; a1
MOV             W1, #2                  ; a2
MOV             W2, #3                  ; a3
MOV             W3, #4                  ; a4
MOV             W4, #5                  ; a5
MOV             W5, #6                  ; a6
MOV             W6, #7                  ; a7
MOV             W7, #8                  ; a8
BL            _Z3sumiiiiiiiimm      ; sum(int,int,int,int,int,int,int,int,ulong,ulong)

STR             W0,
LDR             W1,
ADRL            X0, aSumD               ; "sum:%d\n"
BL            .printf

BL            .getchar

B               loc_788
```

由于我把之前 **extern C**方法去掉后 ,它就以 **C++** 的形式生成了 **_Z3sumiiiiiiiimm** 方法,因为 **标准C** 的方法时没有方法重载的 ,所以以 **C++** 生成的方法会带上

参数类型 其中 **i 是 int****m是uint64**



我们摘取关键一段传参调用的 ,按照上一章讲解的知识进行分析:

```asm
# 1. 复制栈顶指针
MOV             X9, SP
# 2.储存 a9 a10 变量到 栈
STR             X10,                ; a9
STR             X8,              ; a10
# 3.剩余的 a1~a8 储存在 x0~x7 寄存器
MOV             W0, #1                  ; a1
MOV             W1, #2                  ; a2
MOV             W2, #3                  ; a3
MOV             W3, #4                  ; a4
MOV             W4, #5                  ; a5
MOV             W5, #6                  ; a6
MOV             W6, #7                  ; a7
MOV             W7, #8                  ; a8
# 4. 调用sum方法
BL            _Z3sumiiiiiiiimm
```

这很符合我们所说的调用约定。 **x0~x7 传参 多余的参数放在栈中**

我们将栈结构画个结构,记住下面现在的栈的储存值,后续我们将分析 **sum** 方法如何取出调用的

```
-34        |
-30        |
-2C        |
-28        |
-24        |
-20        |
-1C        |
-18        |
-14        |
-10        |
-C        |
-8        |
-4        |                       
0        |        a9                <--SP指针
4        |
8        |        a10
```



## sum 分析

我们看看 **sum** 方法生成的指令

```asm
SUB             SP, SP, #0x40
LDR             X9,
LDR             X8,
STR             W0,
STR             W1,
STR             W2,
STR             W3,
STR             W4,
STR             W5,
STR             W6,
STR             W7,
STR             X9,
STR             X8,
LDR             W8,
LDR             W9,
ADD             W8, W8, W9
LDR             W9,
ADD             W8, W8, W9
LDR             W9,
ADD             W8, W8, W9
LDR             W9,
ADD             W8, W8, W9
LDR             W9,
ADD             W8, W8, W9
LDR             W9,
ADD             W8, W8, W9
LDR             W9,
ADD             W8, W8, W9
STR             W8,
LDR             X8,
LDR             X9,
ADD             X8, X8, X9
STR             X8,
LDR             W8,
SUBS            W8, W8, #0xA
B.LE            loc_760

B               loc_748

; ---------------------------------------------------------------------------

loc_748                              
LDRSW         X8,
LDR             X9,
ADD             X8, X8, X9
ADRP            X9, #0x2000
STR             X8,
B               loc_760

; ---------------------------------------------------------------------------

loc_760                                 
                                       
ADRP            X8, #0x2000
LDR             X8,
MOV             W0, W8
ADD             SP, SP, #0x40
RET
```

### 分配局部变量空间

在第一行 SUB 命令就是分配栈空间

```asm
SUB             SP, SP, #0x40
```

sub(Subtraction) 意思是相减。

上面实际就是 sp = sp - 0x40

按照之前的栈分析的话,此时的栈顶**SP**指针位置在 **-40** 的地方

```
-40        |        <--SP指针
-3c        |
-38        |
-34        |
-30        |
-2C        |
-28        |
-24        |
-20        |
-1C        |
-18        |
-14        |
-10        |
-C        |
-8        |
-4        |                       
0        |        a9       
4        |
8        |        a10
```

### 读取栈空间

下面两个 **LDR** 就是读取之前 通过栈传参的 **a9 和 a10**



```asm
# 此时的 就是 a9 就是a10
# x9 = = a9
# x8 = = a10
LDR             X9,
LDR             X8,
```

### 储存到栈空间

下面一些列的 **STR** 全部是储存到栈空间

```asm
# 将 x0~x7 参数放入 到栈空间中
# 也就是 a1 ~ a8
STR             W0,
STR             W1,
STR             W2,
STR             W3,
STR             W4,
STR             W5,
STR             W6,
STR             W7,
# 将 a9 ~ a10 也放入栈空间中
STR             X9,
STR             X8,
```

此时的栈空间应该是这种样子

```
-40        |        <--SP指针
-3c        |   
-38        |
-34        |
-30        |        a10
-2C        |       
-28        |        a9
-24        |       
-20        |        a8
-1C        |        a7
-18        |        a6
-14        |        a5
-10        |        a4
-C        |        a3
-8        |        a2
-4        |        a1                               
0        |        a9       
4        |
8        |        a10
```



很明显在 **a9 和 a10** 之间 **空了4 字节**。 因为它们的类型是 **uint64** 占了 **8字节**

个人觉得 **a1 - a10** 入堆栈是下面方法的参数,它们应该也算做局部变量

```c
int sum(int a1, int a2,int a3,int a4,int a5,int a6,int a7,int a8,uint64_t a9,uint64_t a10)
```

所以**a9 a10** 出现了两次。 一个在 **main** 进行 **sum**的传参中

一个是 **sum** 的方法参数 (算作局部变量)

### 储存相加结果 sum

接下里就枯燥的取值相加了,我就直接写上分析的结果

```asm
# x8 = a1 + a2
LDR             W8,
LDR             W9,
ADD             W8, W8, W9
# x8 = x8 + a3
LDR             W9,
ADD             W8, W8, W9
# x8 = x8 + a4
LDR             W9,
ADD             W8, W8, W9
# x8 = x8 + a5
LDR             W9,
ADD             W8, W8, W9
# x8 = x8 + a6
LDR             W9,
ADD             W8, W8, W9
# x8 = x8 + a7
LDR             W9,
ADD             W8, W8, W9
# x8 = x8 + a8
LDR             W9,
ADD             W8, W8, W9
# = x8
STR             W8,
```

这些代码应该对应 C代码中的

```asm
int sum = a1 + a2 + a3 + a4 + a5 + a6 + a7+ a8;
```

**sum 应该在 -40 + C = -34 的位置**

```
-40        |        <--SP指针
-3c        |   
-38        |
-34        |        sum
-30        |        a10
-2C        |       
-28        |        a9
-24        |       
-20        |        a8
-1C        |        a7
-18        |        a6
-14        |        a5
-10        |        a4
-C        |        a3
-8        |        a2
-4        |        a1                               
0        |        a9       
4        |
8        |        a10
```

### 储存相加结果 sum2

下面就对应着 sum2的:

```asm
# x8 = a9 + a10
LDR             X8,
LDR             X9,
ADD             X8, X8, X9
# 储存在 sp 上
STR             X8,
```

对应 c代码的

```asm
uint64_t sum2 = a9 + a10;
```

栈中储存的地方为:

```
-40        |        sum2<--SP指针
-3c        |   
-38        |
-34        |        sum
-30        |        a10
-2C        |       
-28        |        a9
-24        |       
-20        |        a8
-1C        |        a7
-18        |        a6
-14        |        a5
-10        |        a4
-C        |        a3
-8        |        a2
-4        |        a1                               
0        |        a9       
4        |
8        |        a10
```

### PSR 和 条件助记符

PSR 之前谈到过 。它表示着状态寄存器。

在 ARM 中 ,常见的状态标记为:

**N (Negative) 计算结果是否为负数**

**C (Carry) 结果是否进位**

**V (oVerflow) 结果是否溢出**

**Z ((Zero)) 结果是否为0**

它们一般用作于 条件分支指令

```
EQ:等于,当零标志位(Z)被设置时为真。
NE:不等于,当零标志位(Z)未被设置时为真。
CS(或HS):带进位(或有符号数大于或等于),当进位标志位(C)被设置时为真。
CC(或LO):无进位(或有符号数小于),当进位标志位(C)未被设置时为真。
MI:负数,当负数标志位(N)被设置时为真。
PL:正数或零,当负数标志位(N)未被设置时为真。
VS:溢出,当溢出标志位(V)被设置时为真。
VC:未溢出,当溢出标志位(V)未被设置时为真。
HI:无符号数大于,当进位标志位(C)被设置且零标志位(Z)未被设置时为真。
LS:无符号数小于或等于,当进位标志位(C)未被设置或零标志位(Z)被设置时为真。
GE:有符号数大于或等于,当负数标志位(N)与溢出标志位(V)的值相同(都是0或都是1)时为真。
LT:有符号数小于,当负数标志位(N)与溢出标志位(V)的值不同时为真。
GT:有符号数大于,当零标志位(Z)未被设置且负数标志位(N)与溢出标志位(V)的值相同且都是0时为真。
LE:有符号数小于或等于,当零标志位(Z)被设置或负数标志位(N)与溢出标志位(V)的值不同时为真。
```



举几个例子:



1.**BEQ** 代表着 相等跳转 而 Z = 1 代表 结果是 0 才执行,什么意思呢?

```asm
# x0 - x1 == 0   标志位 Z = 1 那么它们相等
subs x0,x0,x1        # sub 代表相减 s 代表影响 PSR
```



2.**BLT** 代表着 less than 也就是小于跳转 标记符判断是 **N!=V** 这是什么意思呢?

首先 **N** 是作为判断小于的标准,下面有一个例子,其中 **x0 是 3 x1 是 5 那么 N状态是 1**

```asm
# 3 -5 = -2此时 N = 1
subs x0,x0,x1
BLE address
```

明明 **N 就足够判断 x0 是否小于 x1 , 为什么 还需要 V 标志位**?

您首先先了解 **V** 标志位的意思

**V** 标志表示 结果的符号位是否跟 两个寄存器不同,我举一个例子:

```asm
# 假设寄存器的大小只有 1 字节
# r0 r1 的二进制数都是 b1000 0000 = -128
ADD r0, r0 , r1
# r0 结果按道理是 1 0000 0000可是我说假设是由 1个字节
# r0 被截取后 变成了 0
# 此时代表了溢出 V = 1
```

那这跟我们 **LE** 有什么关系呢?



首先 **N = 1** 一般来说 肯定是 **x0 小于 x1** 的

但由于符号位问题可能会溢出成正数。

如下:

```asm
# 假设 寄存器还是只有 1个 字节
# r0 是 b1000 0000 = -128 && r1 = b0000 0001 = 1
subs r0, r0 , r1
# 由于寄存器最大是 1字节 负数最大只能是 -128
# b1000 0000 -b0000 0001 = b0111 1111 = 127
# 此时变成了负数且溢出 此时 N = 0 V = 1
# 但 r0 实际 比 r1 小
```

所以 N!=V 用于 有符号位的 大小判断。



3.**BGT** (greater than ) 大于跳转判断标志位 **N = V**

正常不溢出 且 r0 - r1不会成负数 ,那么就代表 r0 > r1



### 条件跳转

下面是最后剩余片段,我们只看它是如何进行条件跳转的

```asm
# 1. 从栈中取出 sum
LDR             W8,
# 2. 跟 10 做比较 并影响 psr 状态寄存器
SUBS            W8, W8, #0xA
# 3. 如果 N!=V 也就是说 sum < 10 跳到 loc_7770038760 地址上
B.LE            loc_7770038760

# 4. 反之sum >=0 直接跳到 loc_7770038748 上
B               loc_7770038748

; ---------------------------------------------------------------------------
# 5. 跳到 loc_7770038748 标记
loc_7770038748                        
LDRSW         X8,
LDR             X9,
ADD             X8, X8, X9
ADRP            X9, #0x777003A000
STR             X8,
B               loc_7770038760

; ---------------------------------------------------------------------------
# 6. 跳到 loc_7770038760 标记
loc_7770038760                        
                                       
ADRP            X8, #0x777003A000
LDR             X8,
MOV             W0, W8
ADD             SP, SP, #0x40 ; '@'
RET
```



对应着 **c++** 的代码:

```cpp
if (sum > 10)
{
    //6. 测试全局变量
    _global_sum = sum + sum2;
}
    // 7. 测试返回值
    return _global_sum;
```



### 全局变量

在回顾一下 目前栈储存的值:

```
-40        |        sum2<--SP指针
-3c        |   
-38        |
-34        |        sum
-30        |        a10
-2C        |       
-28        |        a9
-24        |       
-20        |        a8
-1C        |        a7
-18        |        a6
-14        |        a5
-10        |        a4
-C        |        a3
-8        |        a2
-4        |        a1                               
0        |        a9       
4        |
8        |        a10
```

结果值肯定 是 **sum > 10** 的,我们只看摘取的一部分:

```asm
# 反之sum >=0 直接跳到 loc_7770038748 上
B               loc_7770038748

; ---------------------------------------------------------------------------
# 跳到 loc_7770038748 标记
loc_7770038748                        
LDRSW         X8,
LDR             X9,
ADD             X8, X8, X9
ADRP            X9, #0x777003A000
STR             X8,
B               loc_7770038760
```

1.**LDRSW**

LDRSW 是 LDR (Load register) 的扩充指令,也是从内存中取出数据到寄存器中。

后面的**SW**代表着 signed word (with optional Extend , 意思是 带符号扩充到 64 位。 也就是说 32位的数据扩充到 64位 最高位符号不变。

```asm
# 获取 sum 并转换 64 位 放入 x8 中
LDRSW         X8,
```

2.**ADD**

```asm
# 获取 sum2 放入 x9 中
LDR             X9,
# sum + sum2
ADD             X8, X8, X9
```

3.**ADRP** 和 **PC** 和 **STR**

adrp 获取 基于pc 和 目标偏移 的 4kb 对齐地址

pc 是当前指令执行的地址

str 是储存寄存器到内存地址

整合起来的意思是:

```asm
# 将当前 pc 内存对齐的地址 到 x9 中
ADRP            X9, #0x777003A000
# = sum + sum2
# 就是全局变量的指针
STR             X8,
```

我们看看 **0x777003A000 + 0xac0** 在内存中是什么样子的

</p>



</p>

由于是小端排序,**且是 64 位的 uint64**:

```
       42 00 00 00 00 00 00 00
转换        00 00 00 00 00 00 00 42        
```

**0x42 的结果是 66**就是我们 **sum + sum2** 的结果。

它们换算成代码就是:

```cpp
_global_sum = sum + sum2;
```



### RET

最后就讲解下返回值了

**RET 实际类似与我们使用 return 命令**

```cpp
return _global_sum;
```

来看看最后的汇编做了什么:

```asm
# 1. 获取全局变量 _global_sum
ADRP            X8, #0x777003A000
LDR             X8,
# 2. 将全局变量放在 x0 中用于返回
MOV             W0, W8
# 3. 释放局部变量空间
ADD             SP, SP, #0x40 ; '@'
# 4. 返回到 x30 寄存器指向的地址
RET
```

此时的栈空间就又变成原始的:

```
.....(这些被释放掉了)       
0        |        a9                <--SP指针
4        |
8        |        a10
```



**RET** 指令的话依赖于 **X30** 寄存器。 也就是 **linker** 地址。

一般在使用 BL 会计算下一条指令的位置,用图展示方便大伙分析:

</p>



</p>

用鼠标点击下 x30 寄存器跳过去

</p>



</p>

你会发现**ida pro** 给你翻译成数据了

此时你按 **C 翻译成代码**

</p>



</p>

到此你就会发现 它就是我们 **BL sum 指令**下的位置



下一章我们就讲解 **dobby inline Hook**

debug_cat 发表于 2023-4-28 22:18

感谢分享,支持支持
页: [1]
查看完整版本: <luckfollowme> arm 学习篇06 - 调用约定