是的,他们提出的二进制随机化器需要处理这种情况,因为可能存在混淆的二进制文件,或者由于作者不了解或出于某些奇怪的原因,手写代码可能会做任意事情。
但是不,普通编译器不会为 x86 执行此操作。此答案按书面形式解决了 SO 问题,而不是包含这些声明的论文:
出于性能原因,现代编译器在 PE 和 ELF 二进制文件的代码段中积极交错静态数据
需要引用! 根据我使用 GCC 和 clang 等编译器的经验,以及查看 MSVC 和 ICC 的 asm 输出的一些经验,这对于 x86 来说是完全错误的。
普通编译器将静态只读数据放入section .rodata(ELF 平台)或section .rdata(Windows)。 .rodata 部分(和.text 部分)作为文本段的一部分链接,但所有只读数据因为整个可执行文件或库被组合在一起,所有的代码被单独组合在一起。 What's the difference of section and segment in ELF file format(或者最近,即使在单独的 ELF 段中,所以 .rodata 可以映射为 noexec。)
Intel's optimization guide 表示不要混合代码/数据,尤其是读写数据:
汇编/编译器编码规则 50。(M 影响,L 一般性) 如果(希望是只读的)数据必须
与代码出现在同一页面上,避免在间接跳转后立即放置。例如,
按照最有可能的目标进行间接跳转,并将数据放在无条件分支之后。
汇编/编译器编码规则 51。(H 影响,L 通用性) 始终将代码和数据放在
单独的页面。尽可能避免自我修改代码。如果要修改代码,请尝试在
一次并确保执行修改的代码和被修改的代码都在
单独的 4 KB 页面或单独对齐的 1 KB 子页面。
(有趣的事实:Skylake 实际上具有用于自修改代码管道核的缓存行粒度;在最近的高端 uarch 上,将读/写数据放入 64 字节的代码中是安全的。)
在同一页中混合代码和数据在 x86 上几乎为零的优势,并且浪费了代码字节上的数据 TLB 覆盖,并浪费了数据字节上的指令 TLB 覆盖。同样在 64 字节缓存行中浪费 L1i / L1d 中的空间。唯一的优势是统一缓存(L2 和 L3)的代码+数据局部性,但通常不会这样做。 (例如,在 code-fetch 将一行带入 L2 之后,从同一行获取数据可能会在 L2 中命中,而必须去 RAM 获取来自另一个缓存行的数据。)
但是由于 L1iTLB 和 L1dTLB 分离,以及 L2 TLB 作为统一的受害者缓存 (maybe I think?),x86 CPU 没有为此优化。 iTLB 未命中而在现代 Intel CPU 上从同一缓存行读取字节时,获取“冷”函数并不能防止 dTLB 未命中。
x86 上的代码大小优势为零。 x86-64 的 PC 相对寻址模式是 [RIP + rel32],因此它可以寻址当前位置 +-2GiB 内的任何内容。 32 位 x86 甚至没有 PC 相对寻址模式。
也许作者在考虑 ARM,其中附近的静态数据允许 PC 相对加载(具有小的偏移量)将 32 位常量放入寄存器?(这称为“文字池” " 在 ARM 上,您会在函数之间找到它们。)
我认为它们不是指 立即 数据,例如 mov eax, 12345,其中 32 位 12345 是指令编码的一部分。那不是要使用加载指令加载的静态数据;即时数据是另一回事。
显然它只适用于只读数据;在指令指针附近写入将触发管道清除以处理自修改代码的可能性。而且您通常需要 W^X(写入或执行,而不是两者)作为内存页面。
CPU如何区分代码和数据?
递增。 CPU 在 RIP 中获取字节,并将它们解码为指令。从程序入口点开始后,沿着被取的分支继续执行,然后通过未取的分支等等。
在架构上,它不关心除了当前正在执行的字节之外的字节,或者正在被指令加载/存储为数据的字节。最近执行的字节将保留在 L1-I 缓存中,以防再次需要它们,对于 L1-D 缓存中的数据也是如此。
在无条件分支或ret 之后有数据而不是其他代码并不重要。 函数之间的填充可以是任何东西。如果数据具有某种模式(例如,现代 CPU 在 16 或 32 字节的宽块中获取/解码),可能存在罕见的极端情况,即数据可能会停止预解码或解码阶段,但 CPU 的任何后期阶段都是只查看来自正确路径的实际解码指令。 (或者是对一个分支的错误推测......)
因此,如果执行到达一个字节,则该字节是指令的(部分)。这对 CPU 来说完全没问题,但对于想要查看可执行文件并将每个字节分类为非此即彼的程序却无济于事。
code-fetch 总是检查 TLB 中的权限,所以如果 RIP 指向一个不可执行的页面,它就会出错。 (页表项中的 NX 位)。
但实际上就 CPU 而言,并没有真正的区别。 x86 是冯诺依曼架构。如果需要,指令可以加载自己的代码字节。
例如movzx eax, byte ptr [rip - 1] 设置 EAX 为 0x000000FF,加载 rel32 = -1 = 0xffffffff 位移的最后一个字节。
考虑到代码部分是可执行的并且 CPU 可能会错误地将恶意数据作为代码执行,这对安全性是否非常不利? (也许攻击者将程序重定向到该指令?)
可执行页面中的只读数据可用作 Spectre 小工具,或用于面向返回的编程 (ROP) 攻击的小工具。但通常实际代码中已经有足够多的此类小工具,我认为这没什么大不了的。
但是,是的,与您的其他观点不同,这实际上是有效的一个小反对意见。
最近(2019 年或 2018 年末),GNU Binutils ld 已开始将 .rodata 部分与 .text 部分放在一个单独的页面中,以便它可以只读没有 exec允许。这使得静态只读数据在 x86-64 等 ISA 上无法执行,其中 exec 权限与读取权限是分开的。即在一个单独的 ELF 段中。
你可以使不可执行的东西越多越好,混合代码+常量将要求它们是可执行的。