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寄存器储存的地址的位置的值。比较绕,其实就是将 memory[pc] 的值赋值给 r0 寄存器。

我们使用 ida 来观察一下:

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

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

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

这个指令也好理解,就是读取 memory[pc-4] 的值,由于 pc = pc + 8,所以最终的结果应该是 memory[pc+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

相关推荐
難釋懷27 分钟前
Vue解决开发环境 Ajax 跨域问题
前端·vue.js·ajax
特立独行的猫a32 分钟前
Nuxt.js 中的路由配置详解
开发语言·前端·javascript·路由·nuxt·nuxtjs
咸虾米34 分钟前
在uniCloud云对象中定义dbJQL的便捷方法
前端·javascript
梨子同志36 分钟前
JavaScript Proxy 和 Reflect
前端·javascript
汤圆炒橘子39 分钟前
状态策略模式的优势分析
前端
90后的晨仔1 小时前
解析鸿蒙 ArkTS 中的 Union 类型与 TypeAliases类型
前端·harmonyos
IT_陈寒1 小时前
Element Plus 2.10.0 重磅发布!新增Splitter组件
前端·人工智能·后端
挑战者6668881 小时前
vue入门环境搭建及demo运行
前端·javascript·vue.js
贩卖纯净水.1 小时前
Webpack的基本使用 - babel
前端·webpack·node.js
用户882093216672 小时前
Vue组件通信全攻略:从父子传参到全局状态管理,一篇搞定!
前端