ARM64汇编03 - PC寄存器

本文主要讨论 pc 寄存器的一些行为。

我们使用前文编译的 arm32 程序来做实验,由于 arm64 里面已经没有了 mov pc r0 这样的指令,无法直接对 pc 寄存器进行读写操作,所以只能采用 arm32 来演示。

PC寄存器的读写 - arm模式

arduino 复制代码
#include <stdio.h>

int main()
{

    while (1)
    {
        getchar();
        printf("hello\n");
    }

    return 0;
}

在 IDA 上调试程序,我们测试一下写入 PC 的值

此时,调试窗口与寄存器窗口的 PC值为0xE9DAF4E0

我们 patch 程序,将断点位置的指令改为 mov pc, r0 ,在这之前给 r0 赋值 0xE9DAF4EC,也就是往下数3条指令的位置:

当我们按 F7 执行完这条指令后,按照道理来说,程序应该会跳转到 0xE9DAF4EC 这个位置来执行。我们试一下:

程序确实跳转到了这个位置。

我们再来测试一下读取 PC 寄存器的值 ,此时,PC 的值是 0xE9DAF4EC

patch 一下程序,将指令改成 mov r0, pc :

理论上来说,当我们 F7 单步执行这条指令后,r0 的值应该是 0xE9DAF4EC 才对。我们试一下,看看效果:

单步之后,发现 r0 的值,不是0xE9DAF4EC 而是 0xE9DAF4F4 ,这就很让人疑惑了。

这个玩意,我之前也是有所耳闻的。现在网上查的资料,对这个现象的解释都比较统一,就说arm是3级的流水线,根据指令循环来算,当一条指令开始执行的时候,已经开始fetch第3条指令的,所以会导致 pc = pc + 8

之前,我是比较相信这个说法的,但是现在有点存疑。看下面这个实验:

MOV 指令下面有一个 B 操作,是无条件跳转。按照道理来说,fetch 操作应该取的是sub_E9DE237C处的第一条指令,所以此时 PC 的值应该是 0xE9DE237C

但是,我们单步过去发现 pc 的值依然是 pc + 8:

除非arm没有分支预测,不然也太蠢了。

还有一个就是,x86 的 pc 也有类似的行为,pc 是指向下一条指令,但是你要说它的流水线只有2级,那显然不对。不管如何,只需要在读取 PC 的值时,注意即可。

结论:arm模式下,pc = pc + 8。

PC 寄存器读取 - Thumb 模式

单步过去,查看 r0 寄存器的值:

可以看到,在 thumb 模式下, pc = pc + 4。

由于 thumb 是变长指令,那么这个 4 是固定值还是说是下两条指令呢?

我们做一个实验,将当前指令的下面的指令改成4字节长度的:

单步过去,查看 r0 寄存器的值:

发现,r0 的值是 0xB005E616。这就说明了,thumb 模式下 pc 的值就是 pc = pc + 4。不论指令长度,这也从侧面说明了,pc 的值跟流水线没啥关系。流水线不可能读指令读一半。

结论:thumb 模式下 pc 的值就是 pc = pc + 4。

IDA中调试LDR指令读取PC的奇怪行为 - arm 模式

比如,LDR R0, [PC],这条指令的意思是读取内存中pc寄存器储存的地址的位置的值。比较绕,其实就是将 memorypc 的值赋值给 r0 寄存器。

我们使用 ida 来观察一下:

此时,pc 的值是 0xF2E914E0 ,由于 pc = pc + 8,pc 的值就是 01 0A 70 E3 ,单步过去,看下 r0 的值:

与指令表达的意思一致,非常的合理。

我们再看另一个例子(arm指令集中立即数使用 # 前缀):LDR R0, [PC, #-4]

这个指令也好理解,就是读取 memorypc-4 的值,由于 pc = pc + 8,所以最终的结果应该是 memorypc+4 的值,在 IDA 中观察一下:

我们期望单步后,R0 寄存器中的值是 0xE3700A01,单步看看结果:

发现,r0 寄存器的值变成了一个没见过的东西:0xE7F001F0

要是能立马想到这个数字是什么原因导致的,就说明对 IDA 非常熟了。

我们,将这个值 patch 到 pc 位置,让 ida 翻译一下:

发现,这个值代表的指令的意思是 UND。

之前,我们在 IDA 的系列中,讲到过IDA断点的实现逻辑:

当我们在IDA中对代码的某一行设置断点时,调试器会先把这里的本来指令的第一个字节保存起来,然后写入一条INT 3指令,因为INT 3指令的机器码为11001100b(0xCC)当运行到这的时候CPU会捕获一条异常,转去处理异常,CPU会保留上上下文环境,然后中断到调试器,大多数调试器的做法是在被调试程序中断到调试器时,会先将所有断点位置被替换为INT 3的指令恢复成原来的指令,然后再把控制权交给用户。

这个的 UND 就相当于 x86 的 cc 断点。所以,我们单步的时候,读出来的数据是被 IDA 改过的数据。

IDA中调试LDR指令读取PC的奇怪行为 - Thumb 模式

与 arm 一样,在 thumb 中也是使用 UND 指令来实现断点LDR r0, [pc]

步过之后,期望得到 0x00000122 ,但是却得到了 0x0000DE10,其原因就是 DE10 就是 thumb 模式下的 UND 指令。

还有一个特殊的地方,那就是 LDR 会强制访问的地址 4 字节对齐。

比如,我们访问一个不是4字节对齐的地址LDR r0, [pc, #4]

将下面一条指令也改成LDR r0, [pc, #4]

神奇的事情出现了,发现 ida 给我们自动算出来的地址是一样的。说明在 B438C624处与在 B438C626,使用 ldr 访问 pc, #4 处的地址是一样的结果。

这就是因为 LDR 指令会将最终计算出来的地址向下进行 4 字节对齐,B438C626 + 8 = B438C62E,对齐后得到 B438C62C

相关推荐
lichenyang45320 小时前
Socket.IO 原理与技术选型
前端
qq210846295320 小时前
在python中什么是 self 和 cls?
开发语言·前端·python
我会冲击波20 小时前
vibe coding 一个多月,我把 Claude Code + Pi 做成了桌面 IDE,还给它移植了 VS Code 的编辑体验
前端·javascript·后端
kyson_21 小时前
前端性能优化全链路实践指南:从网络、缓存到渲染与监测
前端
前端fighter21 小时前
线上崩溃?使用 TRAE Work 5分钟把混淆日志翻译成人话并输出复盘报告
前端·vue.js·程序员
网安蟹佬霸21 小时前
CTF Web方向解题全攻略:从信息收集到getshell(附实战payload与脚本)
android·前端·网络·安全·web安全·网络安全·网安
前端玩坑坑21 小时前
CSS Container Queries 容器查询入门教程
前端
带多刺的玫瑰21 小时前
Leecode#4刷题之寻找两个正序数组的中位数
java·前端·算法
qq_2676128921 小时前
项目集、里程碑与迭代联动:Gitee项目管理如何支撑多团队规模化交付与进度风险治理
前端·gitee
赵广陆21 小时前
企业实战:Web服务端搭建
前端·langchain·langgraph