我最初的想法是,如果我们能够将返回的操作码从 C3 更改为其他代码,最好是 2 个字节,那么 ROA 将无法工作。
不,x86 指令编码是固定的,并且大部分是硬连线在 CPU 内部解码器的硅片中。(微码指令重定向到微码 ROM 以定义指令,但被识别为指令的操作码仍然是硬连线的。)
我认为即使是英特尔或 AMD 的微码更新也无法将其现有 CPU 更改为不解码C3
为ret
. (尽管他们可能会使其他一些多字节序列也解码为非常慢的微编码ret
,但可能只能通过接管现有微编码指令的编码。)
没有解码的 CPUC3
将ret
不再是 x86 CPU。或者我猜你可以让它成为一种新模式,其中指令编码不同。不过,它不再与 x86 二进制兼容。
不过,这是一个有趣的想法。x86 上的单字节 RET 使得将小工具链接在一起变得更加容易(https://en.wikipedia.org/wiki/Return-orienting_programming#On_the_x86-architecture)。(或者意味着可以链接更多的小工具,为您提供更大的工具箱。)
我不会屏住呼吸等待 CPU 供应商提供ret
使用 2 字节操作码的新模式。不过,这是有可能的(CPU 供应商可以进行新设计,而不是让您破解现有的 CPU)。通过使其成为单独的模式(如 64 位长模式与 64 位内核下的 32 位兼容模式,与 32 位内核的“传统模式”)操作系统仍然可以在此类 CPU 上运行,并且您可以在同一个内核下混合/匹配用户空间进程,一些为 x86 编译,一些为 new86。
如果供应商要引入无法运行现有二进制文件的新不兼容模式,希望他们会对指令集进行其他清理。例如,即使计数 = 0,也可以通过让它们始终写入 FLAGS 来消除变量计数移位对 FLAGS 的错误依赖。或者完全重做操作码以不在 1-byte 上花费太多编码空间xchg eax, r32
,并缩短 SIMD 指令的编码。但随后他们无法与常规 x86 解码器共享尽可能多的解码器晶体管。任何像 EFLAGS 语义的变化都可能需要后端的变化,而不仅仅是解码器。
他们还可以使[rsp+disp8/32]
寻址模式缩短 1 个字节,可能使用不同的寄存器作为即使没有索引也总是需要 SIB 字节的寄存器。(-fomit-frame-pointer
现在很典型,所以相对于堆栈指针的寻址会花费额外的字节。)
有关x86 指令编码的混乱程度的更多详细信息,请参阅 Agner Fog 的停止指令集战争博客文章。
至少需要对 CPU 电路设计进行多少更改才能c3
启动需要第 2 个字节的 2 字节指令00
?
Intel CPU 分多个阶段解码:
一些 CPU 在 L1i 高速缓存中标记指令边界,并在一行从 L2 到达时进行解码。(AMD 比英特尔最近做到了这一点,但 IIRC Ryzen 没有,而英特尔在 P6 或 SnB 系列中没有。请参阅Agner Fog 的微架构指南。)
没有后续字节的单字节操作码这一事实c3
被硬连接到指令长度解码器中,因此必须改变。
但是那么如何处理第二个字节呢?您可以让解码器对其进行c3 xx
检查,如果没有则xx == 00
引发#UD
异常(未定义指令,即非法指令)。
或者它可以将其解码为imm8
操作数,并让执行单元检查操作数是否为 0。
让解码器对下一个字节进行这种与模式相关的检查可能更容易,因为无论如何它们都必须针对不同的模式对其他 insn 进行不同的解码。
00
不是“特别”。常规解码器可能会在可能有 15 个字节长(最大 x86 指令长度)的宽输入中接收指令字节。但是没有理由假设他们会查看超过指令长度的位/字节,如果它不是零扩展的,则会出现错误。它可能是这样设计的,但是对于 1 字节操作码的处理也可能c3
是硬连线的,并且没有任何高位与任何操作码位进行与、或或异或运算。
操作码或整个 insn 不是必须零扩展的整数。你不能假设有像“指令寄存器”这样的东西。
c3 xx
像 xx!=0 那样不解码ret
仍然会破坏所有现有的二进制文件,如果你正在制作一个可以以这种方式运行的 CPU,仍然需要一个新模式。
在 L1i 高速缓存中标记指令边界的 CPU 上,始终将ret
其视为 2 字节指令(不包括前缀)是行不通的。ret
紧跟在 a 之后的字节成为跳转目标或不同的函数并不罕见。跳转到另一条指令的“中间”会迫使这样的 CPU 从缓存行中的那个点开始重做指令边界标记,然后当你ret
再次运行时你会遇到另一个问题。
此外,c3
页的最后一个字节中的 a,后跟一个未映射的页,不得出现 pagefault。但是,如果指令长度解码阶段总是c3
在让它解码之前获取另一个字节,就会发生这种情况。(从不可缓存的内存中运行代码也会使这被视为可观察到的变化。UC 相当于 CPU volatile
)
我想如果在单字节模式下运行,您可能会在00
解码器的假字节ret
上添加长度解码阶段。 ret
是无条件跳转,但如果[rsp]
不可读,它可能会出错。但我认为异常帧只有指令的起始地址,而不是长度。因此,当它实际上只有 1 时,管道的其余部分可能认为它是 2 字节指令。
但它仍然必须以某种方式进入 uop-cache 中,并且 uop 缓存需要关心 insn 开始/结束地址,即使是无条件跳转也是如此。对于跨越 64 字节高速缓存行边界的指令,如果其中任何一个发生更改,则需要使指令无效。
我的理解是,现实生活中的 CPU 设计总是比您从 David Kanter 的文章等框图中想象的更难、更复杂。
顺便说一句,解码器需要多小的变化并不是特别重要。 只有 CPU 供应商才能在新设计中进行这种更改,这一事实使您的想法在指令集设计想法之外完全无法启动。这比完全重组 x86 机器代码更合理一些,因为它仍然可以与现有模式共享几乎所有的解码器晶体管。
为此支持一种全新的模式将非常重要,需要更改 CPU 的代码段描述符(GDT 条目)解码。
创建一个总是需要c3
跟随的 CPU 将是一个更容易的更改00
,但它不会是 x86 并且无法运行绝大多数代码。英特尔或 AMD 销售这样的 CPU 的可能性为零。