例如,PUSH imm32
的操作码为68h。是否可以使用另一个数字(例如69h)来“表示”此指令(假设其他指令未使用此数字)?
通过“表示”,我的意思是在程序集中有PUSH指令的地方,69h将出现在二进制可执行文件中。当它最终被CPU取出并执行时,它将被转移回68h。
我理解每个操作码都是根据CPU电路专门设计的,但我是否可能只想使用另一个十六进制数作为代理?
当然我不会对CPU进行任何更改,我仍然希望在x86架构上执行该指令。
更新:为什么我会问这个问题?
可能你知道面向返回的攻击,它故意误解机器语言流并利用标准库中有许多C3(即ret)。我最初的想法是,如果我们能够将返回的操作码从C3更改为其他代码,最好是2个字节,那么ROA将无法工作。我不是建筑领域的专家,我发现我的想法在现实中是行不通的。感谢您的所有回复。
答案 0 :(得分:3)
理论上是的......
您可以使用未定义的操作码例外,以防您找到备用操作码(尽管不是很多免费点)。异常处理程序将使用正确的操作码修改内存位置并重新执行处理。
但它会在这个内存位置留下“好”的操作码。 你可以设置单步中断处理程序来“修复”存储在内存中的操作码,以便在执行“好”操作码后“伪造”一个操作码并在之后禁用它以便不影响性能。
此外,伪操作码必须与正确的操作码大小相同(或者更长),否则您必须备份后面的指令被损坏(由“好”操作码覆盖)。 如果假的长度超过真正的替换指令,则额外的spced可以是NOP填充。
我不必提它是麻烦的AF。对于现代操作系统来说,它在DOS中非常简单,它几乎是不可行的解决方案。
答案 1 :(得分:2)
我最初的想法是,如果我们能够将return的操作码从C3更改为其他代码(最好是2个字节),那么ROA将无法正常工作。
不,x86指令编码是固定的,并且大多数情况下硬连接到CPU内部解码器的芯片中。 (Micro-coded instructions重定向到微码ROM来定义指令,但是仍然被认为是指令的操作码是硬连线的。)
我认为,即使是Intel或AMD进行的微码更新也无法将其现有CPU更改为 not 将C3
解码为ret
。 (尽管它们可能使其他一些多字节序列也解码为非常慢的微编码ret
,但可能仅是接管了现有微编码指令的编码。)
没有将C3
解码为ret
的CPU将不再是x86 CPU。或者我想您可以将其设置为一种新模式,其中指令编码是不同的。不过,它将不再与x86二进制兼容。
这是一个有趣的想法。 x86上的单字节RET使将小工具链接在一起(https://en.wikipedia.org/wiki/Return-oriented_programming#On_the_x86-architecture)非常容易。 (或者意味着可以链接更多的小工具,从而为您提供更大的工具箱。)
我不会屏住呼吸等待CPU供应商提供一种新模式,其中ret
使用2字节操作码。不过,这是有可能的(对于CPU供应商来说,要进行新设计,对于您来说,不是可以入侵您现有的CPU)。通过将其设置为单独的模式(例如64位内核下的64位长模式与32位兼容模式,以及32位内核的“传统模式”),操作系统仍然可以在此类CPU上运行,并且您可以在同一内核下混合/匹配用户空间进程,其中一些是为x86编译的,而有些是为new86编译的。
如果供应商打算引入一种新的不兼容模式,该模式不能运行现有的二进制文件,则希望他们可以对指令集进行其他清理。例如通过使变量计数移位始终保持FLAGS(即使计数= 0)来消除对FLAGS的错误依赖。或者完全重做操作码,以免在1字节xchg eax, r32
上花费太多编码空间,并缩短了编码SIMD指令。但随后,它们无法与常规x86解码器共享尽可能多的解码器晶体管。而且任何类似EFLAGS语义转换的更改都可能需要在后端进行更改,而不仅仅是解码器。
它们还可以使[rsp+disp8/32]
寻址模式缩短1个字节,也许使用另一种寄存器作为即使没有索引也总是需要SIB字节的寄存器。 (-fomit-frame-pointer
现在很典型,因此,相对于堆栈指针的寻址需要额外的字节。)
有关x86指令的混乱程度,请参见Agner Fog的Stop the instruction set war博客文章。
要使c3
开始要求2字节为00
的2字节指令的开始,至少需要对CPU电路设计进行多少更改? >
Intel CPU分多个阶段进行解码:
指令长度的预解码器查找指令边界,将指令字节放入队列中(每个周期最多处理16个字节或6条指令,以较低者为准)。有关框图,请参见https://www.realworldtech.com/sandy-bridge/3/。
解码器从该队列中获取4条(或Skylake中的5条)指令,并将其并行地馈送到实际的解码器。每个输出1或多个微码。 (请参阅David Kanter的SnB文章的下一页。)
某些CPU在L1i高速缓存中标记指令边界,并在行从L2到达时进行此解码。 (AMD比Intel最近这样做,但IIRC Ryzen没有这样做,并且Intel不在P6或SnB系列中。请参阅Agner Fog's microarch guide。)
c3
是一个没有后继字节的一字节操作码这一事实被硬连线到指令长度解码器中,因此必须进行更改。
但是接下来如何处理第二个字节?您可以让获得c3 xx
的解码器检查xx == 00
并引发#UD
异常(未定义指令,也称为非法指令)。
或者可以将其解码为imm8
操作数,并让执行单元检查操作数是否为0。
让解码器在下一个字节上进行这种与模式相关的检查可能会更容易,因为无论如何对于不同的模式,它们必须对其他insn进行不同的解码。
00
不是“特殊”的。常规解码器可能在宽输入中接收指令字节,该输入可能长15个字节(最大x86指令长度)。但是没有理由假设他们会查看超出指令长度的位/字节,如果没有将其扩展为零,则会出错。可以这样设计,但是像c3
这样的1字节操作码的处理是硬连线的,并且没有与任何操作码位进行AND,ORed或XOR的任何更高位。 / p>
操作码或整个insn不是必须零扩展的整数。您不能假设存在“指令寄存器”之类的东西。
使c3 xx
不能解码为xx!= 0的ret
仍然会破坏基本上所有现有的二进制文件,并且如果您要使CPU能够以这种方式工作,则仍然需要一种新的模式。
在标记L1i缓存中指令边界的CPU上,始终将ret
视为2字节指令(不包括前缀)将不起作用。 ret
之后的字节成为跳转目标或其他函数的情况并不罕见。从高速缓存行的那一点开始,跳转到另一条指令的“中间”将迫使此类CPU重做指令边界标记,然后再次运行ret
时会遇到另一个问题。
此外,页面的最后一个字节中的c3
后面必须是未映射的页面,也不能页面错误。但是,如果指令长度解码阶段始终在c3
之后获取另一个字节然后再进行解码,则会发生这种情况。 (从不可缓存的内存中运行代码也将这一变化视为可观察到的变化。UC是volatile
的CPU等效值)
我想如果在00
是单字节的模式下运行,对于解码器来说,您可能对假的ret
字节有一个长度解码阶段的限制。 ret
是无条件跳转,但是如果[rsp]
不可读,则可能会出错。但是我认为异常帧只是指令的起始地址,而不是长度。因此,当流水线的其余部分实际上只有1个时,可以认为它是2字节指令。
但是它仍然必须以某种方式进入uop缓存,并且uop缓存需要关心insn起始/结束地址,即使是无条件跳转也是如此。对于跨越64字节缓存行边界的指令,如果其中任何一条更改,则需要使该指令无效。
我的理解是,现实生活中的CPU设计总是比您想像David Kanter的文章中的框图所想象的要难和复杂。
顺便说一句,与解码器的变化量有多小无关。 只有CPU供应商才能在新设计中进行此更改,这一事实使您的想法完全脱离了指令集设计的想法。这比完全重组x86机器代码似乎更合理,因为它仍然可以共享现有模式下的几乎所有解码器晶体管。
为此提供一种全新的模式非常重要,需要更改CPU的代码段描述符(GDT条目)解码。
创建一个总是 要求c3
后跟00
的CPU会容易得多,但是那不是x86,不会运行绝大多数代码。英特尔或AMD出售这样的CPU的可能性为零。