为什么指令指针不能像普通寄存器一样使用MOV或ADD指令?

30

关于x86汇编语言的维基百科文章指出,“程序员不能直接访问IP寄存器。”

直接意味着使用像movadd这样的指令,就像我们可以读写EAX一样。

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


有特殊的指令如jmp用于设置它,以及call用于在设置新值之前推送旧值。(而在x86-64中,则使用RIP相对寻址模式使用LEA进行读取。)有关详细信息,请参见直接读取程序计数器


2
也许你可以只用 jmp XXX 来做同样的事情。 - Mysticial
@Mysticial 这是可能的,但是你会间接地访问它。 - user142019
2
请参见https://dev59.com/VHRB5IYBdhLWcg3wgXhV以及相关问题。 - Jim Mischel
4个回答

34

因为没有合法的使用情况,所以您无法直接访问它。任何任意指令改变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

1
@Amir 再读一遍,它是正确的。call 指令将指令指针推入堆栈顶部,而 pop eax 将指令指针移动到 eax 中并将堆栈恢复到其正确位置。请注意,没有 ret 指令;在这种情况下,call 指令不打算用作调用,因此将返回指针留在堆栈上会使其失衡,并在下一个 ret 上导致无限循环(或崩溃)。 - Polynomial
11
你的第一段是错误的。ARM有其程序计数器完全暴露以供读/写,即R15(请参见http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0473f/Babbdajb.html)。ARM64取消了这个特性,但这并没有使ARM32不可能。分支预测的部分需要在指令被解码之前完成,以避免提取气泡。在解码时,检测EIP是否为目标寄存器并将其标记为分支不是特别困难的。对于安全性来说,没有任何影响, 因为安全性不依赖于扫描指令流以检测分支指令。 - Peter Cordes
7
对于指令缓存、分支预测和其他花哨的东西的任何解释对我来说都似乎很可疑,因为一个简单的原因:x86最初是作为微控制器架构诞生的,没有这些装饰。这不像是因为它使得向超标量架构转移变得困难而被削减了IP访问 - 它根本就没有从一开始就存在。可能是因为已经有了jmp来设置它,也没有足够紧迫的用例来添加一个特定的读取它的指令或从通用目的指令的mod-reg-rm字节中窃取宝贵的位数。 - Matteo Italia
2
这并不是一个很好的论据;实际上有一个非常真实的用例,就是作为标签的替代品。如果不能直接读取 EIP 并保存它,则需要计算字节。而有了获取指令指针的能力,就不需要使用相对“call”来表述它了。 - Dmytro
2
@Dmitry:不需要可写的EIP(除了jmp之外的指令),而且您不希望它使用其中一个8个通用寄存器编码。但是,能够有效地读取它对于位置无关代码来说是很好的。幸运的是,x86-64通过RIP相对寻址修复了这个问题,包括lea rax,[rip],否则call/pop通常是最好的选择。(有趣的事实:call +=(目标=下一条指令)是一种特殊情况,并且不会使返回地址预测器堆栈失衡。http://blog.stuffedcow.net/2018/04/ras-microbenchmarks/#call0,因此call/pop并不差。) - Peter Cordes
显示剩余19条评论

19
那可能是x86的一个设计方案。ARM不同寻常地将其程序计数器作为R15读写公开。这允许非常紧凑的函数前奏/后奏,以及能够使用单个指令推入或弹出多个寄存器:进入时push {r5, lr}返回时pop {r5, pc}(将保存的链接寄存器值弹出到程序计数器中)。但是,这使得高性能/乱序的ARM实现不太方便,并且已被AArch64放弃。
所以它是可能的,但会使用一个寄存器。32位ARM有16个整数寄存器(包括PC),因此在ARM机器码中编码一个寄存器号需要4位。另一个寄存器几乎总是作为堆栈指针占用,因此ARM有14个通用整数寄存器。(LR可以保存到堆栈中,因此在函数体内它可以被用作通用寄存器)。
大多数现代x86都继承自8086。它采用相当紧凑的可变长度指令编码,并且只有8个寄存器,每个src和dst寄存器在机器码中只需要3位。
在最初的8086中,它们并不是非常通用,16位模式下不支持SP相对寻址,因此基本上有2个寄存器(SP和BP)用于堆栈操作。这只留下了6个相对通用的寄存器,而将其中一个作为PC而不是通用寄存器将大大减少可用寄存器数量,在典型代码中增加了溢出/重新加载的量。
AMD64新增了r8-r15寄存器以及RIP相对寻址模式。使用lea rsi, [rip+whatever]和RIP相对寻址模式可以直接访问静态数据和常量,并且非常适用于高效的位置无关代码。间接JMP指令完全足够用于写入RIP。
没有真正的好处可以通过允许任意指令读取或写入PC来获得,因为您始终可以使用整数寄存器和间接跳转执行相同的操作。对于x86-64的R15与RIP相同实际上几乎是负面效果,特别是对于作为编译器目标架构的性能。(手动编写汇编程序在2000年时已经非常不常见,当AMD64设计时它已经是一个非常不寻常的小众领域。)
因此,AMD64是x86第一次可能获得像ARM一样完全暴露的程序计数器,但有很多很好的理由不这样做。

1
相关:在8086汇编中是否可能操纵指令指针?:是的,使用jmp进行写入,使用call进行读取。 - Peter Cordes
1
原来 call +0 是可以的,不会破坏返回地址预测器,所以 call/pop 实际上是最好的选择。http://blog.stuffedcow.net/2018/04/ras-microbenchmarks/#call0. - Peter Cordes
更新/脚注:ARM不是唯一一个将PC作为“通用”寄存器之一的ISA:PDP-11也这样做了:https://en.wikipedia.org/wiki/PDP-11_architecture#CPU_registers。在早期的机器中,8个寄存器可能比你想要构建的还要多,因此,你可以将PC可寻址而不是有8个GPR和一个单独的PC。当设计PDP-11时,流水线实现可能甚至还没有出现。 - Peter Cordes

4

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


4

jmp指令将设置EIP寄存器。

这段代码将把eip设置为00401000:

mov eax, 00401000
jmp eax ;set Eip to 00401000

获取 EIP 的方法如下:

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

1
你如何在不使用标签、不计算字节或编写自己的高级语言来自动计算字节的情况下完成这个任务? - Dmytro
@Dmitry:你必须知道你要跳到哪里,所以你需要一个绝对数值地址,或者你需要使用标签。(或者计算字节,但是认真地说,只需使用本地标签,这就是它们的用途。) - Peter Cordes
这是一种错误的二分法;汇编有许多跳转方式,例如在此处列出的:https://c9x.me/x86/html/file_module_x86_id_147.html,虽然它们不受我所知道的任何汇编器支持(或者在文档中不容易找到),但您可以通过创建定义代码内联字节的宏来强制执行它们,例如 db 0xeb, 0x0 用于相对近距离跳转到当前ip。如果汇编器知道如何在预处理器级别上计算 sizeof(nop;nop;nop;nop),我们可以内联计算偏移量以避免计数错误。 - Dmytro
1
原来 call +0 是可以的,不会破坏返回地址预测器,所以 call/pop 实际上是最好的选择。 http://blog.stuffedcow.net/2018/04/ras-microbenchmarks/#call0. - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接