gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-04 17:06:26 24 4
gpt4 key购买 nike

所以我在引用这篇论文:

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

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


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


enter image description here

但是我有一些问题:


如何提高程序速度?我只能想象这只会使cpu的执行更加复杂吗?
CPU如何区分代码和数据?因为据我所知,除非存在跳转类型的指令,否则cpu将以线性方式依次执行每个指令,那么cpu怎么知道代码中的哪些指令是代码,哪些指令是数据?
考虑到代码段是可执行的,并且CPU可能会错误地将恶意数据作为代码执行,因此这对安全性是否非常不利? (也许攻击者将程序重定向到该指令?)

最佳答案

是的,他们提出的二进制随机化器需要处理这种情况,因为可能存在混淆的二进制文件,或者手写代码可能会执行任意操作,因为作者并不了解或出于某种奇怪的原因。
但是,不,普通编译器不会针对x86执行此操作。该答案针对书面提出的SO问题,而不是包含这些主张的论文:

出于性能原因,现代编译器在PE和ELF二进制文件的代码段中积极插入静态数据

需要引用!在我使用GCC和clang之类的编译器的经验中,对于x86来说,这完全是错误的,并且有些经验是从MSVC和ICC看asm输出的。
普通的编译器将静态只读数据放入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实际上具有用于自修改代码管道核的高速缓存行粒度;在最近的高端用户中,将读/写数据放在64字节的代码内是安全的。)

在同一页面中混合使用代码和数据在x86上具有接近零的优势,并且浪费了代码字节的data-TLB覆盖范围,浪费了数据字节的指令-TLB覆盖范围。在64字节高速缓存行中也是如此,以浪费L1i / L1d中的空间。唯一的优势是统一缓存(L2和L3)的代码+数据局部性,但是通常不会这样做。 (例如,在代码提取将一行插入L2之后,从同一行中提取数据可能会进入L2,而不得不从另一缓存行进入RAM来获取数据。)
但是,使用分离的L1iTLB和L1dTLB,以及将L2 TLB作为统一的受害者缓存(maybe I think?),x86 CPU并未为此进行优化。从现代Intel CPU上的同一高速缓存行读取字节时,在获取“冷”功能时出现iTLB丢失并不能防止dTLB丢失。
x86上的代码大小优势为零。 x86-64的PC相对寻址模式为[RIP + rel32],因此它可以寻址当前位置+ -2GiB之内的任何内容。 32位x86甚至没有PC相关的寻址模式。
也许作者想到的是ARM,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小工具,也可以用作面向返回编程(ROP)攻击的小工具。但是我认为通常在真实代码中已经有足够多的小工具了,这没什么大不了的。
但是,是的,与您的其他观点不同,这实际上是一个次要的反对。
最近(2019年或2018年末),GNU Binutils ld已开始将.rodata部分与.text部分放在单独的页面中,因此无需执行权限即可将其只读。这使得静态只读数据在x86-64之类的ISA上不可执行,而exec权限与读取权限是分开的。即在单独的ELF细分中。
您可以使不可执行的事情越多越好,并且将代码+常量混合在一起将要求它们是可执行的。

关于x86 - 为什么编译器将数据放在PE和ELF文件的.text(code)部分中,并且CPU如何区分数据和代码?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55607052/

24 4 0