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

查看 66|回复 1
作者:2016976438   
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      
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 |   
读取栈空间
下面两个 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 |   
很明显在 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 |   
储存相加结果 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  
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 =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  
结果值肯定 是 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.ADRPPCSTR
adrp 获取 基于pc 和 目标偏移 的 4kb 对齐地址
pc 是当前指令执行的地址
str 是储存寄存器到内存地址
整合起来的意思是:
# 将当前 pc 内存对齐的地址 到 x9 中
ADRP            X9, #0x777003A000
# [x9 + 0xAC0] = sum + sum2
# [x9 + 0xAC0] 就是全局变量的指针
STR             X8, [X9,#0xAC0]
我们看看 0x777003A000 + 0xac0 在内存中是什么样子的


01.png (31.97 KB, 下载次数: 0)
下载附件
2023-4-28 16:32 上传

由于是小端排序,且是 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      
RET 指令的话依赖于 X30 寄存器。 也就是 linker 地址。
一般在使用 BL 会计算下一条指令的位置,用图展示方便大伙分析:


02.png (63.57 KB, 下载次数: 0)
下载附件
2023-4-28 16:32 上传

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


03.png (66.26 KB, 下载次数: 0)
下载附件
2023-4-28 16:32 上传

你会发现ida pro 给你翻译成数据了
此时你按 C 翻译成代码


04.png (68.66 KB, 下载次数: 0)
下载附件
2023-4-28 16:32 上传

到此你就会发现 它就是我们 BL sum 指令下的位置
下一章我们就讲解 dobby inline Hook

标志, 寄存器

debug_cat   

感谢分享,支持支持
您需要登录后才可以回帖 登录 | 立即注册

返回顶部