为什么编译器将数据放在PE和ELF文件的.text(代码)部分中,以及CPU如何区分数据和代码?

问题描述 投票:2回答:2

所以我参考这篇论文:

二进制搅拌:遗留x86二进制代码的自随机指令地址

https://www.utdallas.edu/~hamlen/wartell12ccs.pdf

代码与数据交织:出于性能原因,现代编译器在PE和ELF二进制文件中的代码段内积极地交织静态数据。在编译的二进制文件中,通常没有区分数据字节与代码的方法。无意中随机化数据和代码会破坏二进制文件,给指令级随机化器带来了困难。可行的解决方案必须以某种方式保留数据,同时随机化所有可到达的代码。

enter image description here

但我有一些问题:

  1. 这怎么加速了程序?!我只能想象这只会让cpu执行更复杂吗?
  2. 以及CPU如何区分代码和数据?因为我记得cpu将以线性方式一个接一个地执行每个指令,除非有一个跳转类型的指令,那么cpu如何知道代码中的哪些指令是代码,哪些指令是数据?
  3. 考虑到代码部分是可执行的并且CPU可能错误地将恶意数据作为代码执行,这对于安全性是非常不好的吗? (也许攻击者将程序重定向到该指令?)
x86 cpu reverse-engineering compiler-optimization elf
2个回答
5
投票

出于性能原因,现代编译器积极地在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


在同一页面中混合代码和数据具有接近于零的优势,并且浪费了代码字节上的数据-TLB覆盖,并且浪费了数据字节上的指令-TLB覆盖。在64字节高速缓存行中也是如此,以浪费L1i / L1d中的空间。唯一的优势是统一缓存(L2和L3)的代码+数据位置,但通常不会这样做。 (例如,在代码获取将一行引入L2之后,从同一行获取数据可能会在L2中获取而不必从另一个缓存行中获取数据到RAM。)

但是,对于拆分的L1iTLB和L1dTLB,以及L2 TLB作为统一的牺牲缓存,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(写或exec,而不是两者)。

以及CPU如何区分代码和数据?

增量。 CPU在RIP处获取字节,并将其解码为指令。在程序入口点开始后,执行继续执行分支,并通过未采用的分支等进行。

从架构上讲,它不关心当前正在执行的字节,或者是由指令加载/存储为数据的字节。最近执行的字节将在L1-I高速缓存中保留,以防它们再次需要,并且对于L1-D高速缓存中的数据也是如此。

在无条件分支或ret之后立即拥有数据而不是其他代码并不重要。函数之间的填充可以是任何东西。可能存在罕见的极端情况,如果数据具有某种模式,数据可能会停止预解码或解码阶段(例如,因为现代CPU以16或32字节的宽块获取/解码),但CPU的任何后续阶段都是只查看来自正确路径的实际解码指令。 (或者是对一个分支的错误推测...)

因此,如果执行到达一个字节,那么该字节是指令的一部分。这对于CPU来说完全没问题,但是对于想要查看可执行文件并将每个字节分类为/或的程序没有帮助。

代码获取总是检查TLB中的权限,因此如果RIP指向非可执行页面,它将会出错。 (页表条目中的NX位)。

但就CPU而言,确实没有真正的区别。 x86是冯·诺依曼架构。如果需要,指令可以加载自己的代码字节。

例如movzx eax, byte ptr [rip - 1]将EAX设置为0x000000FF,加载rel32 = -1 = 0xffffffff位移的最后一个字节。


考虑到代码部分是可执行的并且CPU可能错误地将恶意数据作为代码执行,这对于安全性是非常不好的吗? (也许攻击者将程序重定向到该指令?)

可执行页面中的只读数据可用作Spectre小工具,也可用作面向返回编程攻击的小工具。但通常在实际代码中已经有足够的这样的小工具,这不是什么大问题。

但是,是的,这是对此的一个小反对,这实际上是有效的,不像你的其他观点。


4
投票
  1. 交错代码和数据将使数据更接近使用它的代码。这将使更简单,更快速的指令可以访问数据。
  2. CPU没有,由程序员/编译器来确保将数据放在实际程序流程之外的位置。如果程序流意外进入数据块,CPU将把数据解释为指令。通常,数据放在函数之间,但有时编译器可以添加额外的分支指令,以便为函数内的数据块腾出空间。
  3. 通常这不是问题,因为程序员或编译器确保程序流没有输入数据部分,但是你是部分正确的,因为如果攻击者设法诱骗CPU执行数据,这将不会被记忆保护机制。
© www.soinside.com 2019 - 2024. All rights reserved.