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

时间:2019-04-10 07:31:35

标签: x86 cpu reverse-engineering compiler-optimization elf

所以我要引用这篇论文:

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

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

  

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

enter image description here

但是我有一些问题:

  1. 这如何加速程序?!我只能想象这只会使cpu的执行更加复杂?

  2. ,CPU如何区分代码和数据?因为据我所知,除非有跳转类型的指令,否则cpu将以线性方式依次执行每个指令,那么cpu怎么知道代码中的哪些指令是代码,哪些指令是数据?

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

2 个答案:

答案 0 :(得分:5)

  

出于性能方面的考虑,现代编译器会在PE和ELF二进制文件的代码段中主动插入静态数据

需要引用! 根据我在使用GCC和clang之类的编译器方面的经验,对于x86来说这完全是错误的,并且有些经验涉及从MSVC和ICC输出的asm输出。

普通编译器将静态只读数据放入section .rodata(ELF平台)或section .rdata(Windows)中。 .rodata 部分(和.text部分)被链接为文本 segment 的一部分,但所有整个可执行文件或库的只读数据组合在一起,所有代码分别组合在一起。 What's the difference of section and segment in ELF file format


在同一页面中混合代码和数据具有接近零的优势,并且浪费了代码字节上的数据-TLB覆盖范围,浪费了数据字节上的指令-TLB覆盖范围。在64字节高速缓存行中也是如此,以浪费L1i / L1d中的空间。唯一的优势是统一缓存(L2和L3)的代码+数据局部性,但这通常不是 。 (例如,在代码提取将一行插入L2之后,从同一行中提取数据可能会进入L2,而不得不从另一缓存行中的RAM进入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(写或执行,而不是全部)用于内存页面。

  

以及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小工具,或用作面向返回编程攻击的小工具。但是通常在实际代码中已经有足够多的小工具,这没什么大不了的。

但是,是的,与您的其他观点不同,这实际上是一个次要的反对。

答案 1 :(得分:4)

  1. 交织代码和数据会使数据更接近使用它的代码。这样可以通过更简单,更快捷的说明来访问数据。
  2. CPU不支持,而是由程序员/编译器负责确保将数据放置在实际程序流之外的位置。如果程序流意外进入数据块,CPU将把数据解释为指令。通常,数据放置在函数之间,但有时编译器可以添加一条额外的分支指令,以在函数内部放置数据块。
  3. 通常这不是问题,因为程序员或编译器确保程序段未输入数据部分,但是您部分正确,因为如果攻击者设法诱骗CPU执行数据,则不会被内存保护机制捕获。