【问题标题】:Why can't you set the instruction pointer directly?为什么不能直接设置指令指针?
【发布时间】:2012-01-10 03:06:54
【问题描述】:

The Wikipedia article about x86 assembly表示“编程器不能直接访问IP寄存器。”

直接表示使用 mov 和 add 等指令。

为什么不呢?这背后的原因是什么?有哪些技术限制?

【问题讨论】:

标签: x86 cpu-registers program-counter


【解决方案1】:

您无法直接访问它,因为没有合法的用例。任意更改指令eip 会使分支预测变得非常困难,并且可能会引发一系列安全问题。

您可以使用jmpcallret 编辑eip。您只是不能使用正常操作直接读取或写入eip

eip 设置为寄存器就像jmp eax 一样简单。您也可以使用push eax; ret,将eax 的值推入堆栈,然后返回(即弹出和跳转)。第三个选项是call eax,它调用eax中的地址。

阅读可以这样进行:

call get_eip
  get_eip:
pop eax ; eax now contains the address of this instruction

【讨论】:

  • 是的,没错。你会经常看到jmp [eax]jmp [esp+4] 之类的东西用于实现动态调用或调用表。
  • mov eax, offset get_ip 不行吗? mov eax, $ 怎么样?诚然,自从我写任何汇编语言的东西以来,已经有一段时间了。 . .
  • 你的第一段是假的。 ARM has its program-counter totally exposed for read/write as R15。 ARM64 放弃了这一点,但这并没有使 ARM32 成为不可能。部分分支预测需要在指令被解码之前发生,以避免获取气泡。在解码时,检测 EIP 是目标寄存器并将其标记为分支并不是特别困难。没有安全隐患,因为安全不依赖于扫描指令流来检测分支指令。
  • 任何关于指令缓存、分​​支预测和其他花哨的东西的解释对我来说都是可疑的,原因很简单:x86 是作为微控制器架构诞生的,它没有这些装饰。这并不是说他们取消了 ip 访问,因为它使迁移到超标量架构变得困难——它从一开始就不存在。可能他们没有添加它,因为已经有 jmp 来设置它,并且没有足够引人注目的用例来添加特定指令来读取它或从通用的 mod-reg-rm 字节中窃取宝贵的位说明。
  • 这不是一个很好的推理;有一个非常真实的用例,它是标签的替代品。无法直接读取 eip 并保存,需要计算字节数。能够获得指令指针,而无需根据相对“调用”来制定它。
【解决方案2】:

我认为他们的意思是不能像访问其他寄存器一样直接访问 IP 寄存器。程序员绝对可以写入 IP,例如通过发出跳转指令。

【讨论】:

    【解决方案3】:

    jmp 将设置EIP 寄存器。

    此代码会将 eip 设置为 00401000:

    mov eax, 00401000
    jmp eax ;set Eip to 00401000
    

    为了得到EIP

    call GetEIP
    .
    .
    GetEIP:
    mov eax, [esp]
    ret
    

    【讨论】:

    • 以及如何在不使用标签而不计算字节或编写自己的自动计算字节的高级语言的情况下做到这一点?
    • @Dmitry:你必须知道你在哪里跳跃,所以要么你需要一个绝对数字地址,要么你需要使用标签。 (或计算字节数,但认真地只使用本地标签,这就是它们的用途。)
    • 这是错误的二分法;汇编知道许多跳转方式,例如这里列出的那些:c9x.me/x86/html/file_module_x86_id_147.html 虽然我知道的任何汇编程序都不支持它们(或者在文档中不容易找到),但您可以通过创建一个宏来强制它们定义代码内联字节,例如db 0xeb, 0x0 用于近相对跳转到当前ip。如果汇编器知道如何在预处理器级别sizeof(nop;nop;nop;nop),我们可以计算内联偏移以避免计数错误。
    • 事实证明call +0 很好,并且不会使返回地址预测器失衡,所以call/pop 实际上是最好的。 blog.stuffedcow.net/2018/04/ras-microbenchmarks/#call0.
    【解决方案4】:

    这可能是 x86 的一种设计。 ARM 做expose its program counter for read/write as R15。不过,这很不寻常。

    它允许一个非常紧凑的函数序言/结尾,以及使用单个指令推送或弹出多个寄存器的能力:push {r5, lr} 进入,pop {r5, pc} 返回。 (将链接寄存器的保存值弹出到程序计数器中)。

    但是,它使高性能/无序 ARM 实现不太方便,并且在 AArch64 中被删除。


    所以有可能,但会用完其中一个寄存器。 32 位 ARM 有 16 个整数寄存器(包括 PC),因此一个寄存器号需要 4 位来编码为 ARM 机器代码。另一个寄存器几乎总是作为堆栈指针绑定,因此 ARM 有 14 个通用整数寄存器。 (LR可以保存到栈中,所以可以作为函数体内部的通用寄存器使用)。

    大多数现代 x86 都继承自 8086。它采用相当紧凑的可变长度指令编码设计,并且只有 8 个寄存器,机器代码中每个 src 和 dst 寄存器只需要 3 位。

    在最初的 8086 中,它们不是很通用,并且 SP 相对寻址在 16 位模式下是不可能的,因此基本上 2 个寄存器(SP 和 BP)被绑定用于堆栈内容。这样就只剩下 6 个有点通用的寄存器,其中一个是 PC 而不是通用寄存器将大大减少可用寄存器,从而大大增加典型代码中的溢出/重新加载量。


    AMD64 增加了 r8-r15,以及 RIP-relative 寻址模式。 lea rsi, [rip+whatever] 和用于直接访问静态数据和常量的 RIP 相对寻址模式是高效的与位置无关的代码所需要的一切。间接 JMP 指令足以写入 RIP。

    允许使用任意指令来读取或写入 PC 并没有什么好处,因为您总是可以使用整数寄存器和间接跳转来做同样的事情。 x86-64 的 R15 与 RIP 相同,这几乎是纯粹的缺点,特别是对于作为编译器目标的体系结构的性能而言。 (到 2000 年设计 AMD64 时,手写的 asm 奇怪的东西已经非常罕见了。)

    所以 AMD64 确实是 x86 第一次有可能获得像 ARM 这样的完全公开的程序计数器,但有很多充分的理由不这样做。

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-10-16
    • 2021-06-02
    • 1970-01-01
    • 2021-08-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多