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 用于记录当前指令执行的地址
示例
我们将之前的代码改成 如下的:
#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>
下的断点的地方刚刚是在死循环的内部。
为什么不在上面的的代码上:
SUB SP, SP, #0x20
STP X29, X30, [SP,#0x10]
ADD X29, SP, #0x10
STUR WZR, [X29,#-4]
B loc_74C717E714
因为我们直接启动程序后 直接跑到我们的死循环内部了,上面的代码是不会在执行的了。
其实我们可以通过 ida pro 创建进程来直接调试,这样可以在程序一开始的地方就断下来,但那种有点麻烦。
不过我们可以通过分析砸门另一个 sum 方法 来分析上面的指令。
因为 上面的指令如
SUB SP, SP, #0x20
明显是给 sp 栈空间分配大小的,按道理标准的调用约定 都会完成这种指令。
对于 常见调用约定 一般都是部分参数入寄存器 其余参数从右到左入栈 被调用的方法实现栈平衡 随后返回值放入第一个寄存器 如 eax| r0 | x0
下面是我们将要分析的完整汇编指令,我们将逐步分析:
MOV W8, #0xA
STR W8, [SP,#8]
MOV W8, #0x14
STR W8, [SP,#4]
LDR W0, [SP,#8]
LDR W1, [SP,#4]
BL sum
STR W0, [SP]
LDR W1, [SP]
ADRL X0, aSumD ; "sum:%d\n"
BL unk_74C717E790
BL unk_74C717E7A0
B loc_74C717E714
MOV
接下来您按 F9 运行程序,然后在程序输入任意键重新进入断点
接下来您会断第一条语句上:
MOV W8, #0xA
这段指令的意思是 向 x8 寄存器赋值 0xA 也就是 16
注意 w8 意思是低32位
此时我的 x8 寄存器是 0x000000000000003F 按 F8 步过后 会变成 0x000000000000000A
STR
接下来看 STR 指令:
STR W8, [SP,#8]
STR 您也可以读成 store register 意思是储存寄存器。
它可以将 寄存器的值 储存在内存中。
上面意思您可以理解成 [SP + 0x8] = w8
也就是 栈地址 + 8 赋值 w8 的值
点击 stack view 后 在点击 寄存器上的 SP(x31) 跳到当前的栈顶:
0000007FC2455540 000000140000001E
0000007FC2455548 000000000000000A
0000007FC2455540 是当前的栈顶
执行完 STR W8, [SP,#8] 指令后 堆栈下面的 0000007FC2455548 会变成 A,由于上一次循环设置过了,所以我这里本身就是 A
经过上面的说明,您应该也能分析,接下的指令:
MOV W8, #0x14 # 将 0x14 放到 x8
STR W8, [SP,#4] # 将 由于 w8 是低 32 位 所以 sp 只加 4
注意一点的是 sp + 4 的位置 还是看之前栈的数值:
0000007FC2455540 000000140000001E
分开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 [sp+4] = 20
MOV W8, #0xA
STR W8, [SP,#8]
MOV W8, #0x14
STR W8, [SP,#4]
对应着 c 命令
//2. 测试局部变量赋值指令
int a = 10;
int b = 20;
而且您应该也能分析出来 局部变量都是放在栈空间的
LDR
接下来在分析一下: LDR 指令
MOV W8, #0xA
STR W8, [SP,#8]
MOV W8, #0x14
STR W8, [SP,#4]
LDR W0, [SP,#8]
LDR W1, [SP,#4]
BL sum
STR W0, [SP]
LDR W1, [SP]
ADRL X0, aSumD ; "sum:%d\n"
BL unk_74C717E790
BL unk_74C717E7A0
B loc_74C717E714
STR 是从 寄存器 到 内存。
LDR (Store Register) 则是从 内存到寄存器
那么下面您可以直接翻译成:
LDR W0, [SP,#8] # w0 = [sp+0x8] = 10
LDR W1, [SP,#4] # w1 = [sp+0x4] = 20
很明显这是从 栈取出局部变量 给 寄存器
B
B (branch) 用于跳转某个地址上,一般用于跳到子方法上
而下面指令 BL 带 L 的可以理解为记录 linker X30 寄存器,也就是返回地址,因为调用完返回后我们得返回到之前执行的地方。
接下来我将带我的地址的汇编和寄存器赋值一下让你们参考下:
hello2:00000074C717E728 LDR W1, [SP,#4]
hello2:00000074C717E72C BL sum
hello2:00000074C717E730 STR W0, [SP]
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, [SP]
根据调用约定 部分参数放在寄存器中
结合 BL 跳转指令
下面我们将 指令整合起来看
# 1. 局部变量放在栈中
# [sp + 8] = 10
MOV W8, #0xA
STR W8, [SP,#8]
# [sp + 4] = 20
MOV W8, #0x14
STR W8, [SP,#4]
# 2. 取出局部变量 然后传参到 sum 方法
LDR W0, [SP,#8]
LDR W1, [SP,#4]
BL sum
# 3. 根据调用约定 sum执行结束后会还原栈 随后把返回值放在 x0
# 把返回值放在栈中 也就是局部变量中
STR W0, [SP]
这些应该对应 c 代码中的
//2. 测试局部变量赋值指令
int a = 10;
int b = 20;
//3. 测试传参 和 调用方法
int ret = sum(a, b);
伪指令 ADRL
下面一条指令是
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 里面字符串的参数了
printf("sum:%d\n", ret);
也可以分析出里面 DCB 伪指令就是分配储存数据的
那么 ADRL 伪指令就是将储存 "sum:%d\n" 字符串的 指针给放到 X0 寄存器中
ADRL X0, 0x7B13862548
整合分析
# 0. 死循环记录地址
loc_7B13862714
#1.设置局部变量 到栈中
MOV W8, #0xA
STR W8, [SP,#8]
MOV W8, #0x14
STR W8, [SP,#4]
# 2. 取出局部变量 作为参数调用 sum方法
LDR W0, [SP,#8]
LDR W1, [SP,#4]
BL sum
# 3. 取出调用 sum 的返回值 给 栈中
STR W0, [SP]
# 4. 取出sum 返回值 又放到 x1 寄存器
# 取出储存 "sum:%d\n" 数据的 指针放到 x0 寄存器
# 调用 unk_74C717E790(printf) 方法 传递 x0 x1 参数
LDR W1, [SP]
ADRL X0, aSumD ; "sum:%d\n"
BL unk_74C717E790
# 5. getchar
BL unk_74C717E7A0
# 6. 跳到上面的死循环
B loc_74C717E714
下一章我会给大家分析下 sum 内部方法 的 调用约定 和 栈平衡