luckfollowme arm 学习篇06 - 调用约定
在 arm64 的调用约定中
前八个整数参数依次存储在 x0-x7 中。
其余的参数会入栈
调用完毕后 返回结果会放入在 x0 中
所以您目前需要留意几个寄存器
x0~x7 储存参数的
x29(Frame Pointer) 帧指针
x30(LR) 链接寄存器用于跳回去的地址
x31(SP) 堆栈指针
调整项目
为了更清晰 的理解 arm64 的调用约定,我们把 sum 的传参改成 10位。并把剩下两个参数 改成
uint64 这样更能体验 64位 的数据储存
#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 方法最终生成的 指令集:
loc_788 ; CODE XREF: main+10↑j
; main+74↓j
MOV X8, #0xA
STUR X8, [X29,#-0x10]
MOV X8, #0x14
STR X8, [SP,#0x18]
LDUR X10, [X29,#-0x10]
LDR X8, [SP,#0x18]
MOV X9, SP
STR X10, [X9] ; a9
STR X8, [X9,#8] ; 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, [SP,#0x14]
LDR W1, [SP,#0x14]
ADRL X0, aSumD ; "sum:%d\n"
BL .printf
BL .getchar
B loc_788
由于我把之前 extern C 方法去掉后 ,它就以 C++ 的形式生成了 _Z3sumiiiiiiiimm 方法,因为 标准C 的方法时没有方法重载的 ,所以以 C++ 生成的方法会带上
参数类型 其中 i 是 int m是uint64
我们摘取关键一段传参调用的 ,按照上一章讲解的知识进行分析:
# 1. 复制栈顶指针
MOV X9, SP
# 2.储存 a9 a10 变量到 栈
STR X10, [X9] ; a9
STR X8, [X9,#8] ; 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 方法生成的指令
SUB SP, SP, #0x40
LDR X9, [SP,#0x40]
LDR X8, [SP,#0x48]
STR W0, [SP,#0x3C]
STR W1, [SP,#0x38]
STR W2, [SP,#0x34]
STR W3, [SP,#0x30]
STR W4, [SP,#0x2C]
STR W5, [SP,#0x28]
STR W6, [SP,#0x24]
STR W7, [SP,#0x20]
STR X9, [SP,#0x18]
STR X8, [SP,#0x10]
LDR W8, [SP,#0x3C]
LDR W9, [SP,#0x38]
ADD W8, W8, W9
LDR W9, [SP,#0x34]
ADD W8, W8, W9
LDR W9, [SP,#0x30]
ADD W8, W8, W9
LDR W9, [SP,#0x2C]
ADD W8, W8, W9
LDR W9, [SP,#0x28]
ADD W8, W8, W9
LDR W9, [SP,#0x24]
ADD W8, W8, W9
LDR W9, [SP,#0x20]
ADD W8, W8, W9
STR W8, [SP,#0xC]
LDR X8, [SP,#0x18]
LDR X9, [SP,#0x10]
ADD X8, X8, X9
STR X8, [SP]
LDR W8, [SP,#0xC]
SUBS W8, W8, #0xA
B.LE loc_760
B loc_748
; ---------------------------------------------------------------------------
loc_748
LDRSW X8, [SP,#0xC]
LDR X9, [SP]
ADD X8, X8, X9
ADRP X9, #0x2000
STR X8, [X9,#0xAC0]
B loc_760
; ---------------------------------------------------------------------------
loc_760
ADRP X8, #0x2000
LDR X8, [X8,#0xAC0]
MOV W0, W8
ADD SP, SP, #0x40
RET
分配局部变量空间
在第一行 SUB 命令就是分配栈空间
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
# 此时的 [sp + 40] 就是 a9 [sp+48] 就是a10
# x9 = [sp+40] = a9
# x8 = [sp+48] = a10
LDR X9, [SP,#0x40]
LDR X8, [SP,#0x48]
储存到栈空间
下面一些列的 STR 全部是储存到栈空间
# 将 x0~x7 参数放入 到栈空间中
# 也就是 a1 ~ a8
STR W0, [SP,#0x3C]
STR W1, [SP,#0x38]
STR W2, [SP,#0x34]
STR W3, [SP,#0x30]
STR W4, [SP,#0x2C]
STR W5, [SP,#0x28]
STR W6, [SP,#0x24]
STR W7, [SP,#0x20]
# 将 a9 ~ a10 也放入栈空间中
STR X9, [SP,#0x18]
STR X8, [SP,#0x10]
此时的栈空间应该是这种样子
-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 入堆栈是下面方法的参数,它们应该也算做局部变量
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
接下里就枯燥的取值相加了,我就直接写上分析的结果
# x8 = a1 + a2
LDR W8, [SP,#0x3C]
LDR W9, [SP,#0x38]
ADD W8, W8, W9
# x8 = x8 + a3
LDR W9, [SP,#0x34]
ADD W8, W8, W9
# x8 = x8 + a4
LDR W9, [SP,#0x30]
ADD W8, W8, W9
# x8 = x8 + a5
LDR W9, [SP,#0x2C]
ADD W8, W8, W9
# x8 = x8 + a6
LDR W9, [SP,#0x28]
ADD W8, W8, W9
# x8 = x8 + a7
LDR W9, [SP,#0x24]
ADD W8, W8, W9
# x8 = x8 + a8
LDR W9, [SP,#0x20]
ADD W8, W8, W9
# [sp + 0xC] = x8
STR W8, [SP,#0xC]
这些代码应该对应 C代码中的
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的:
# x8 = a9 + a10
LDR X8, [SP,#0x18]
LDR X9, [SP,#0x10]
ADD X8, X8, X9
# 储存在 sp 上
STR X8, [SP]
对应 c代码的
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 才执行,什么意思呢?
# 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
# 3 -5 = -2 此时 N = 1
subs x0,x0,x1
BLE address
明明 N 就足够判断 x0 是否小于 x1 , 为什么 还需要 V 标志位?
您首先先了解 V 标志位的意思
V 标志表示 结果的符号位是否跟 两个寄存器不同,我举一个例子:
# 假设寄存器的大小只有 1 字节
# r0 r1 的二进制数都是 b1000 0000 = -128
ADD r0, r0 , r1
# r0 结果按道理是 1 0000 0000 可是我说假设是由 1个字节
# r0 被截取后 变成了 0
# 此时代表了溢出 V = 1
那这跟我们 LE 有什么关系呢?
首先 N = 1 一般来说 肯定是 x0 小于 x1 的
但由于符号位问题可能会溢出成正数。
如下:
# 假设 寄存器还是只有 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
条件跳转
下面是最后剩余片段,我们只看它是如何进行条件跳转的
# 1. 从栈中取出 sum
LDR W8, [SP,#0xC]
# 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, [SP,#0xC]
LDR X9, [SP]
ADD X8, X8, X9
ADRP X9, #0x777003A000
STR X8, [X9,#0xAC0]
B loc_7770038760
; ---------------------------------------------------------------------------
# 6. 跳到 loc_7770038760 标记
loc_7770038760
ADRP X8, #0x777003A000
LDR X8, [X8,#0xAC0]
MOV W0, W8
ADD SP, SP, #0x40 ; '@'
RET
对应着 c++ 的代码:
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 的,我们只看摘取的一部分:
# 反之 sum >=0 直接跳到 loc_7770038748 上
B loc_7770038748
; ---------------------------------------------------------------------------
# 跳到 loc_7770038748 标记
loc_7770038748
LDRSW X8, [SP,#0xC]
LDR X9, [SP]
ADD X8, X8, X9
ADRP X9, #0x777003A000
STR X8, [X9,#0xAC0]
B loc_7770038760
1.LDRSW
LDRSW 是 LDR (Load register) 的扩充指令,也是从内存中取出数据到寄存器中。
后面的SW 代表着 signed word (with optional Extend , 意思是 带符号扩充到 64 位。 也就是说 32位的数据扩充到 64位 最高位符号不变。
# 获取 sum 并转换 64 位 放入 x8 中
LDRSW X8, [SP,#0xC]
2.ADD
# 获取 sum2 放入 x9 中
LDR X9, [SP]
# sum + sum2
ADD X8, X8, X9
3.ADRP 和 PC 和 STR
adrp 获取 基于pc 和 目标偏移 的 4kb 对齐地址
pc 是当前指令执行的地址
str 是储存寄存器到内存地址
整合起来的意思是:
# 将当前 pc 内存对齐的地址 到 x9 中
ADRP X9, #0x777003A000
# [x9 + 0xAC0] = sum + sum2
# [x9 + 0xAC0] 就是全局变量的指针
STR X8, [X9,#0xAC0]
我们看看 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 的结果。
它们换算成代码就是:
_global_sum = sum + sum2;
RET
最后就讲解下返回值了
RET 实际类似与我们使用 return 命令
return _global_sum;
来看看最后的汇编做了什么:
# 1. 获取全局变量 _global_sum
ADRP X8, #0x777003A000
LDR X8, [X8,#0xAC0]
# 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