luckfollowme arm学习篇5 - 认识arm
本帖最后由 2016976438 于 2023-4-26 22:44 编辑# luckfollowme arm学习篇5 - 认识arm
上一章各位都运行起来了吧
这一章让我们一起分析分析 当中的 arm 指令
## 简介
我只能勉强带你入门,因为我也只是刚入门的水准,arm 那么多指令 记不住 根本记不住,只能按照例子来,见到啥说啥。
我只说 arm64 ,剩余 arm32 如果大伙有需要的话我在写一章
貌似 arm 也叫做 aarch 遇到的时候注意点就是的了
## 寄存器
在讲解 指令之前 ,先了解些基本概念。
首先 **arm64** 中有 **x0~x31** 的寄存器。
这 **32** 个寄存器都是**64位**的。
有时候我们只需要存储**32 位**的。还有 **w0~w31** 的写法,他们会存储在**64位中 低32位**的位置
当然您还会看到**cpsr** 这种状态寄存器。
了解过 **x86** 的人来说会比较熟悉。
最常见的就是
**N(Nagative) 结果是否是负数**
**Z(Zero) 结果是否为 0**
**C(Carry) 结果是否进位**
**V(Overflow) 在x86 是 o 标志位**
一般计算结果的标志位可用于 条件判断 。 如 Z = 1 代表 两边结果相等
当然每个寄存器您也不用特意记,后面用到的时候我在提出来,这样记得更清楚一点。
目前您只需要了解最基本的:
x30 链接寄存器
x31 堆栈寄存器
**还有个程序计数器 PC 用于记录当前指令执行的地址**
## 示例
我们将之前的代码改成 如下的:
```c
#include "stdio.h"
int _global_sum;
// 使用 标准 C 约束 生成的 方法名不会变得很奇怪
extern "C"
{
int sum(int a, int b)
{
//4. 测试加法 和 局部变量
int sum = a + b;
//5. 测试状态寄存器 和 条件跳转
if (sum > 10)
{
//6. 测试全局变量
_global_sum = sum;
}
// 7. 测试返回值
return _global_sum;
}
}
int main()
{
//1. 测试b 指令 死循环
while (true)
{
//2. 测试局部变量赋值指令
int a = 10;
int b = 20;
//3. 测试传参 和 调用方法
int ret = sum(a, b);
printf("sum:%d\n", ret);
getchar();
}
return 0;
}
```
然后使用 **ndk_builder.py adb_execute_arm64.bat** 执行起来
**强调一点。 如果您通过 netstat 没有看到 23946 端口 。 那么请重新启动 ida pro android_server64 并执行端口转发 adb forward tcp:23946 tcp:23946**
**如果您的android 模拟器出现问题 。 它的 avd 应该在 Android\sdk\system-images\android-apiLevel 目录下,删掉重新创建即可**
### Ida Pro Debugger
启动完砸门的程序后 您 俺任意键他会重复执行 方便我们调试分析
**然后 用 ida pro 进行 debuger 附加动态分析**
ida pro 当然也支持静态分析(拖入 elf 文件进去即可)
但是我们要看栈的情况来分析接下来的指令,所以用 debugger附加的形式
话不多说,先来分析下ida pro 一些基本界面
1.首先您进来注意右边的寄存器。 注意观察 **x30**还有 **SP(x31) 和 PC**
其中 **X30** 是 linker 地址 可以理解为程序的返回后地址
**SP 是当前的栈顶指针**
**PC 是当前执行指令的地址**
在寄存器的右边 就是 状态寄存器 对应着 **PSR**
在最下面是 **stack view** 栈视图 后面会说
</p>
</p>
2.其次您需要做些准备,进入到main方法后,将我图中被 ida pro 自动分析符号 的 **按Q** 转换成偏移数字。
并在图中所示的地方下个断点。
</p>
</p>
下的断点的地方刚刚是在**死循环**的内部。
为什么不在上面的的代码上:
```asm
SUB SP, SP, #0x20
STP X29, X30,
ADD X29, SP, #0x10
STUR WZR,
B loc_74C717E714
```
因为我们直接启动程序后 直接跑到我们的死循环内部了,上面的代码是不会在执行的了。
其实我们可以通过 ida pro 创建进程来直接调试,这样可以在程序一开始的地方就断下来,但那种有点麻烦。
不过我们可以通过分析砸门另一个 **sum 方法** 来分析上面的指令。
因为 上面的指令如
```asm
SUB SP, SP, #0x20
```
明显是给 sp 栈空间分配大小的,按道理标准的调用约定 都会完成这种指令。
**对于 常见调用约定一般都是部分参数入寄存器 其余参数从右到左入栈 被调用的方法实现栈平衡 随后返回值放入第一个寄存器 如 eax| r0 | x0**
下面是我们将要分析的完整汇编指令,我们将逐步分析:
```asm
MOV W8, #0xA
STR W8,
MOV W8, #0x14
STR W8,
LDR W0,
LDR W1,
BL sum
STR W0,
LDR W1,
ADRL X0, aSumD ; "sum:%d\n"
BL unk_74C717E790
BL unk_74C717E7A0
B loc_74C717E714
```
### MOV
接下来您按 **F9** 运行程序,然后在程序输入任意键重新进入断点
接下来您会断第一条语句上:
```asm
MOV W8, #0xA
```
这段指令的意思是 向 **x8 寄存器**赋值 **0xA** 也就是 16
**注意 w8 意思是低32位**
此时我的 **x8** 寄存器是 **0x000000000000003F** 按 **F8** 步过后 会变成 **0x000000000000000A**
### STR
接下来看 STR 指令:
```asm
STR W8,
```
STR 您也可以读成 store register 意思是储存寄存器。
它可以将 寄存器的值 储存在内存中。
上面意思您可以理解成 = w8
也就是 **栈地址 + 8** 赋值 **w8** 的值
点击 **stack view** 后 在点击 寄存器上的 **SP(x31)** 跳到当前的栈顶:
```
0000007FC2455540000000140000001E
0000007FC2455548000000000000000A
```
**0000007FC2455540** 是当前的栈顶
执行完 **STR W8, ** 指令后 堆栈下面的**0000007FC2455548** 会变成 A,由于上一次循环设置过了,所以我这里本身就是 A
经过上面的说明,您应该也能分析,接下的指令:
```asm
MOV W8, #0x14 # 将 0x14 放到 x8
STR W8, # 将 由于 w8 是低 32 位 所以 sp 只加 4
```
注意一点的是 **sp + 4** 的位置 还是看之前栈的数值:
```
0000007FC2455540000000140000001E
分开32 位是 00000014 0000001E
```
您可能会有疑惑,为什么不是 **0000001E 00000014** 而是反过来的?
这就要设计到小端排序的问题了。
小端排序是什么意思呢?
我们通过 **hex view** 进行分析。
先点击 **hex view** 在点击 寄存器的 **SP** 然后您应该就可以看到如下所示:
</p>
</p>
正确的数值是 : **1E 00 00 00 14 00 00 00**
此时您更懵逼了,但是您可以先抽出32 位的来看。
```
14 00 00 00
```
由于小端排序是低地址储存高位字节。
所以我们取出数据 得从最右边开始取,转换过来就是:
```
00 00 00 14
```
这种结果就是对的。
那如果是 **64** 位呢?
```
1E 00 00 00 14 00 00 00
转换成 00 00 00 14 00 00 00 1E
```
此时就对了。
然后我们整合下面的指令的意思就是 **[ sp + 8 ] = 10** ** = 20**
```asm
MOV W8, #0xA
STR W8,
MOV W8, #0x14
STR W8,
```
对应着 c 命令
```c
//2. 测试局部变量赋值指令
int a = 10;
int b = 20;
```
而且您应该也能分析出来 **局部变量都是放在栈空间的**
### LDR
接下来在分析一下: **LDR** 指令
```asm
MOV W8, #0xA
STR W8,
MOV W8, #0x14
STR W8,
LDR W0,
LDR W1,
BL sum
STR W0,
LDR W1,
ADRL X0, aSumD ; "sum:%d\n"
BL unk_74C717E790
BL unk_74C717E7A0
B loc_74C717E714
```
STR 是从 寄存器 到 内存。
LDR (Store Register) 则是从 内存到寄存器
那么下面您可以直接翻译成:
```asm
LDR W0, # w0 = = 10
LDR W1, # w1 = = 20
```
很明显这是从 栈取出局部变量 给 寄存器
**B**
B (branch) 用于跳转某个地址上,一般用于跳到子方法上
而下面指令 BL 带 L 的可以理解为记录 **linker X30** 寄存器,也就是返回地址,因为调用完返回后我们得返回到之前执行的地方。
接下来我将带我的地址的汇编和寄存器赋值一下让你们参考下:
```
hello2:00000074C717E728 LDR W1,
hello2:00000074C717E72C BL sum
hello2:00000074C717E730 STR W0,
```
```
x29 0000007FC2455550
x30 00000074C717E748
sp 0000007FC2455540
pc 00000074C717E72C
```
然后按 F8 执行完 下面的指令后:
```
hello2:00000074C717E72C BL sum
```
注意看我的寄存器,很明显 x30 也就是 linker 变成 bl 指令下面的地址了:
```
x29 0000007FC2455550
x30 00000074C717E730
sp 0000007FC2455540
pc 00000074C717E730
hello2:00000074C717E730 STR W0,
```
根据调用约定 部分参数放在寄存器中
结合 BL 跳转指令
下面我们将 指令整合起来看
```asm
# 1. 局部变量放在栈中
# = 10
MOV W8, #0xA
STR W8,
# = 20
MOV W8, #0x14
STR W8,
# 2. 取出局部变量 然后传参到 sum 方法
LDR W0,
LDR W1,
BL sum
# 3. 根据调用约定 sum执行结束后会还原栈 随后把返回值放在 x0
# 把返回值放在栈中 也就是局部变量中
STR W0,
```
这些应该对应 c 代码中的
```c
//2. 测试局部变量赋值指令
int a = 10;
int b = 20;
//3. 测试传参 和 调用方法
int ret = sum(a, b);
```
### 伪指令 ADRL
下面一条指令是
```asm
ADRL X0, aSumD
```
这是一条计算地址的指令。 但实际**arm64** 中并没有这条指令
这条指令就是伪指令。
一般来说 **arm** 指令都是 4字节
如何看指令字节码呢?
点击 **Options->General**
在 **opcode bytes** 输入 10
</p>
</p>
</p>
</p>
可以看到它实际指令的字节码是 **00 00 00 90 00 20 15 91**
我们把他放入在线网站转换一下**https://armconverter.com/**
```
ADRL X0, 0x7B13862548
转换成
adrp x0, #0x7b13862000
add x0, x0, #0x548
```
可以看到 **ADRL** 实际是 **adrp 和 add**两个指令而来
**adrp 实际也是个伪指令**
个人感觉
adrp 应该是取当前 pc 内存对齐后的数值,然后行为类似 mov
可能mov 因为字节码长度限制的原因 不能把这么大的数直接放入 x0,所以用 **adrp 伪指令**
整理以下意思应该是这样的:
```
adrp x0, #0x7b13862000 # 获取pc内存对齐后的地址 0x7b13862000 复制给 x0
add x0, x0, #0x548 # x0 = x0 + 0x548
```
这种行为很像获取 **基址 + 属性地址偏移**
我们双击这条指令的 **0x7B13862548**的值进行跳转
```
ADRL X0, 0x7B13862548
```
</p>
</p>
这应该就是我们 **printf** 里面字符串的参数了
```c
printf("sum:%d\n", ret);
```
也可以分析出里面 **DCB 伪指令**就是分配储存数据的
那么 ADRL 伪指令就是将储存 **"sum:%d\n"** 字符串的 指针给放到 X0 寄存器中
```
ADRL X0, 0x7B13862548
```
### 整合分析
```
# 0. 死循环记录地址
loc_7B13862714
#1.设置局部变量 到栈中
MOV W8, #0xA
STR W8,
MOV W8, #0x14
STR W8,
# 2. 取出局部变量 作为参数调用 sum方法
LDR W0,
LDR W1,
BL sum
# 3. 取出调用 sum 的返回值 给 栈中
STR W0,
# 4. 取出sum 返回值 又放到 x1 寄存器
# 取出储存 "sum:%d\n" 数据的 指针放到 x0 寄存器
# 调用 unk_74C717E790(printf) 方法传递 x0 x1 参数
LDR W1,
ADRL X0, aSumD ; "sum:%d\n"
BL unk_74C717E790
# 5. getchar
BL unk_74C717E7A0
# 6. 跳到上面的死循环
B loc_74C717E714
```
下一章我会给大家分析下 sum 内部方法 的 调用约定 和 栈平衡 好的,欢迎科普文章 感谢分享 学到老,活到老, 用心讨论,共获提升!
感谢分享 谢谢分享,学习中 感谢分享 收藏了,有空慢慢没研究~
页:
[1]