为什么编译器将数据放入 PE 和 ELF 文件的 .text(code) 部分,CPU 如何区分数据和代码?
Why do Compilers put data inside .text(code) section of the PE and ELF files and how does the CPU distinguish between data and code?
所以我参考了这篇论文:
二进制搅拌:自随机化指令地址
旧版 x86 二进制代码
https://www.utdallas.edu/~hamlen/wartell12ccs.pdf
Code interleaved with data: Modern compilers aggressively interleave
static data within code sections in both PE and ELF binaries for
performance reasons. In the compiled binaries there is generally no
means of distinguishing the data bytes from the code. Inadvertently
randomizing the data along with the code breaks the binary,
introducing difficulties for instruction-level randomizers. Viable
solutions must somehow preserve the data whilst randomizing all the
reachable code.
但我有一些问题:
这如何加速程序?!我只能想象这只会使 cpu 执行更复杂?
以及CPU如何区分代码和数据?因为据我所知 cpu 会以线性方式一个接一个地执行每条指令,除非有跳转类型的指令,所以 cpu 怎么知道代码中的哪些指令是代码,哪些是代码那些是数据?
考虑到代码部分是可执行的并且 CPU 可能会错误地将恶意数据作为代码执行,这对安全性来说不是很糟糕吗? (也许攻击者将程序重定向到该指令?)
- 交织代码和数据将使数据更接近使用它的代码。这将使数据可以通过更简单、更快速的指令访问。
- CPU 没有,programmer/compiler 来确保数据被放置在实际程序流之外的位置。如果程序流意外进入数据块,CPU 会将数据解释为指令。通常数据放在函数之间,但有时编译器可以添加额外的分支指令来为函数内的数据块腾出空间。
- 通常这不是问题,因为程序员或编译器会确保程序流不进入数据部分,但你部分正确,因为如果攻击者设法诱使 CPU 执行内存保护机制不会捕获的数据。
是的,他们提出的二进制随机化器需要处理这种情况,因为混淆的二进制文件可能存在,或者手写代码可能会做任意事情,因为作者不知道或出于某些奇怪的原因。
但是不,普通编译器不会为 x86 执行此操作。这个答案解决了所写的 SO 问题,而不是包含这些声明的论文:
Modern compilers aggressively interleave static data within code sections in both PE and ELF binaries for performance reasons
需要引用! 根据我使用 GCC 和 clang 等编译器的经验,以及查看 MSVC 和 ICC 的 asm 输出的一些经验,这对于 x86 来说完全是错误的。
普通编译器将静态只读数据放入section .rodata
(ELF 平台)或section .rdata
(Windows)。 .rodata
部分(和.text
部分)作为文本的一部分链接段 ,但整个executable或library的所有只读数据都归为一组,所有代码单独归为一组。 What's the difference of section and segment in ELF file format(或者最近,甚至在单独的 ELF 段中,因此 .rodata
可以映射为 noexec。)
Intel's optimization guide表示不要混用code/data,尤其是读+写数据:
Assembly/Compiler Coding Rule 50. (M impact, L generality) If (hopefully read-only) data must
occur on the same page as code, avoid placing it immediately after an indirect jump. For example,
follow an indirect jump with its mostly likely target, and place the data after an unconditional branch.
Assembly/Compiler Coding Rule 51. (H impact, L generality) Always put code and data on
separate pages. Avoid self-modifying code wherever possible. If code is to be modified, try to do it all at
once and make sure the code that performs the modifications and the code being modified are on
separate 4-KByte pages or on separate aligned 1-KByte subpages.
(有趣的事实:Skylake 实际上具有用于自修改代码管道核武器的缓存行粒度;在最近的高端 uarch 上将 read/write 数据放在 64 字节的代码中是安全的。)
在同一页中混合代码和数据在 x86 上的优势几乎为零,并且在代码字节上浪费了数据 TLB 覆盖,在数据字节上浪费了指令 TLB 覆盖。在 L1i / L1d 中浪费 space 的 64 字节缓存行内也是如此。唯一的优势是统一缓存(L2 和 L3)的代码+数据局部性,但这通常是 not 完成的。 (例如,在代码获取将一行带入 L2 之后,从同一行获取数据可能会在 L2 中命中,而不是必须转到 RAM 从另一个缓存行获取数据。)
但是通过拆分 L1iTLB 和 L1dTLB,并将 L2 TLB 作为统一的受害者缓存 (),x86 CPUs 是 not 为此进行了优化。 在现代 Intel CPUs 上从同一缓存行读取字节时,获取“冷”函数时 iTLB 未命中不会阻止 dTLB 未命中。
x86 上的代码大小优势为零。 x86-64 的 PC 相对寻址模式是 [RIP + rel32]
,因此它可以寻址当前位置 +-2GiB 范围内的任何内容。 32 位 x86 甚至没有 PC 相对寻址模式。
也许作者想到了ARM,附近的静态数据允许PC相关的加载(有一个小的偏移量)将32位常量放入寄存器?(这是在 ARM 上称为“文字池”,您会在函数之间找到它们。)
我假设它们不是指 立即 数据,例如 mov eax, 12345
,其中 32 位 12345
是指令编码的一部分。那不是用加载指令加载的静态数据;即时数据是另一回事。
而且显然它只适用于只读数据;在指令指针附近写入将触发清除管道以处理自修改代码的可能性。而且您通常需要 W^X(写入或执行,而不是两者)用于您的内存页面。
and how does the CPU can distinguish between code and data?
递增。 CPU 在 RIP 中获取字节,并将它们解码为指令。从程序入口点开始后,执行会沿着已采取的分支继续进行,并通过未采取的分支等继续执行。
从架构上讲,它不关心当前正在执行的字节以外的字节,或者 loaded/stored 作为指令数据的字节。最近执行的字节将保留在 L1-I 缓存中,以防再次需要它们,L1-D 缓存中的数据也是如此。
在无条件分支或 ret
之后立即使用数据而不是其他代码并不重要。 函数之间的填充可以是任何东西。如果数据具有特定模式(例如,现代 CPUs fetch/decode 在 16 或 32 字节的宽块中,可能会出现罕见的极端情况,但数据可能会停止预解码或解码阶段,但是CPU 的任何后续阶段都只查看来自正确路径的实际解码指令。 (或者来自对分支的错误推测...)
因此,如果执行到一个字节,该字节就是一条指令(的一部分)。这对于 CPU 完全没问题,但对于想要查看 executable 并将每个字节分类为 either/or.
的程序没有帮助
Code-fetch 始终检查 TLB 中的权限,因此如果 RIP 指向非 executable 页面,它将出错。 (页面 table 条目中的 NX 位)。
但实际上就CPU而言,并没有真正的区别。 x86 是冯诺依曼架构。如果需要,指令可以加载自己的代码字节。
例如movzx eax, byte ptr [rip - 1]
设置 EAX 为 0x000000FF,加载 rel32 = -1 = 0xffffffff 位移的最后一个字节。
isnt this VERY bad for security considering that the code section is executable and CPU might by mistake execute a malicious data as code? (maybe attacker redirecting the program to that instruction? )
executable 页面中的只读数据可用作 Spectre 小工具,或 return 面向编程 (ROP) 攻击的小工具。但我认为,实际代码中通常已经有足够多的此类小工具,所以这没什么大不了的。
但是,是的,与您的其他观点不同,这是对这个实际上有效的次要反对意见。
最近(2019 年或 2018 年底),GNU Binutils ld
已开始将 .rodata
部分与 .text
部分放在不同的页面中,因此它可以是只读的 没有 执行权限。这使得静态只读数据不可执行 table,在像 x86-64 这样的 ISA 上,执行权限与读取权限是分开的。即在单独的 ELF 段中。
你可以让非executable的东西越多越好,混合代码+常量需要它们是executable。
所以我参考了这篇论文:
二进制搅拌:自随机化指令地址 旧版 x86 二进制代码
https://www.utdallas.edu/~hamlen/wartell12ccs.pdf
Code interleaved with data: Modern compilers aggressively interleave static data within code sections in both PE and ELF binaries for performance reasons. In the compiled binaries there is generally no means of distinguishing the data bytes from the code. Inadvertently randomizing the data along with the code breaks the binary, introducing difficulties for instruction-level randomizers. Viable solutions must somehow preserve the data whilst randomizing all the reachable code.
但我有一些问题:
这如何加速程序?!我只能想象这只会使 cpu 执行更复杂?
以及CPU如何区分代码和数据?因为据我所知 cpu 会以线性方式一个接一个地执行每条指令,除非有跳转类型的指令,所以 cpu 怎么知道代码中的哪些指令是代码,哪些是代码那些是数据?
考虑到代码部分是可执行的并且 CPU 可能会错误地将恶意数据作为代码执行,这对安全性来说不是很糟糕吗? (也许攻击者将程序重定向到该指令?)
- 交织代码和数据将使数据更接近使用它的代码。这将使数据可以通过更简单、更快速的指令访问。
- CPU 没有,programmer/compiler 来确保数据被放置在实际程序流之外的位置。如果程序流意外进入数据块,CPU 会将数据解释为指令。通常数据放在函数之间,但有时编译器可以添加额外的分支指令来为函数内的数据块腾出空间。
- 通常这不是问题,因为程序员或编译器会确保程序流不进入数据部分,但你部分正确,因为如果攻击者设法诱使 CPU 执行内存保护机制不会捕获的数据。
是的,他们提出的二进制随机化器需要处理这种情况,因为混淆的二进制文件可能存在,或者手写代码可能会做任意事情,因为作者不知道或出于某些奇怪的原因。
但是不,普通编译器不会为 x86 执行此操作。这个答案解决了所写的 SO 问题,而不是包含这些声明的论文:
Modern compilers aggressively interleave static data within code sections in both PE and ELF binaries for performance reasons
需要引用! 根据我使用 GCC 和 clang 等编译器的经验,以及查看 MSVC 和 ICC 的 asm 输出的一些经验,这对于 x86 来说完全是错误的。
普通编译器将静态只读数据放入section .rodata
(ELF 平台)或section .rdata
(Windows)。 .rodata
部分(和.text
部分)作为文本的一部分链接段 ,但整个executable或library的所有只读数据都归为一组,所有代码单独归为一组。 What's the difference of section and segment in ELF file format(或者最近,甚至在单独的 ELF 段中,因此 .rodata
可以映射为 noexec。)
Intel's optimization guide表示不要混用code/data,尤其是读+写数据:
Assembly/Compiler Coding Rule 50. (M impact, L generality) If (hopefully read-only) data must occur on the same page as code, avoid placing it immediately after an indirect jump. For example, follow an indirect jump with its mostly likely target, and place the data after an unconditional branch.
Assembly/Compiler Coding Rule 51. (H impact, L generality) Always put code and data on separate pages. Avoid self-modifying code wherever possible. If code is to be modified, try to do it all at once and make sure the code that performs the modifications and the code being modified are on separate 4-KByte pages or on separate aligned 1-KByte subpages.
(有趣的事实:Skylake 实际上具有用于自修改代码管道核武器的缓存行粒度;在最近的高端 uarch 上将 read/write 数据放在 64 字节的代码中是安全的。)
在同一页中混合代码和数据在 x86 上的优势几乎为零,并且在代码字节上浪费了数据 TLB 覆盖,在数据字节上浪费了指令 TLB 覆盖。在 L1i / L1d 中浪费 space 的 64 字节缓存行内也是如此。唯一的优势是统一缓存(L2 和 L3)的代码+数据局部性,但这通常是 not 完成的。 (例如,在代码获取将一行带入 L2 之后,从同一行获取数据可能会在 L2 中命中,而不是必须转到 RAM 从另一个缓存行获取数据。)
但是通过拆分 L1iTLB 和 L1dTLB,并将 L2 TLB 作为统一的受害者缓存 (
x86 上的代码大小优势为零。 x86-64 的 PC 相对寻址模式是 [RIP + rel32]
,因此它可以寻址当前位置 +-2GiB 范围内的任何内容。 32 位 x86 甚至没有 PC 相对寻址模式。
也许作者想到了ARM,附近的静态数据允许PC相关的加载(有一个小的偏移量)将32位常量放入寄存器?(这是在 ARM 上称为“文字池”,您会在函数之间找到它们。)
我假设它们不是指 立即 数据,例如 mov eax, 12345
,其中 32 位 12345
是指令编码的一部分。那不是用加载指令加载的静态数据;即时数据是另一回事。
而且显然它只适用于只读数据;在指令指针附近写入将触发清除管道以处理自修改代码的可能性。而且您通常需要 W^X(写入或执行,而不是两者)用于您的内存页面。
and how does the CPU can distinguish between code and data?
递增。 CPU 在 RIP 中获取字节,并将它们解码为指令。从程序入口点开始后,执行会沿着已采取的分支继续进行,并通过未采取的分支等继续执行。
从架构上讲,它不关心当前正在执行的字节以外的字节,或者 loaded/stored 作为指令数据的字节。最近执行的字节将保留在 L1-I 缓存中,以防再次需要它们,L1-D 缓存中的数据也是如此。
在无条件分支或 ret
之后立即使用数据而不是其他代码并不重要。 函数之间的填充可以是任何东西。如果数据具有特定模式(例如,现代 CPUs fetch/decode 在 16 或 32 字节的宽块中,可能会出现罕见的极端情况,但数据可能会停止预解码或解码阶段,但是CPU 的任何后续阶段都只查看来自正确路径的实际解码指令。 (或者来自对分支的错误推测...)
因此,如果执行到一个字节,该字节就是一条指令(的一部分)。这对于 CPU 完全没问题,但对于想要查看 executable 并将每个字节分类为 either/or.
的程序没有帮助Code-fetch 始终检查 TLB 中的权限,因此如果 RIP 指向非 executable 页面,它将出错。 (页面 table 条目中的 NX 位)。
但实际上就CPU而言,并没有真正的区别。 x86 是冯诺依曼架构。如果需要,指令可以加载自己的代码字节。
例如movzx eax, byte ptr [rip - 1]
设置 EAX 为 0x000000FF,加载 rel32 = -1 = 0xffffffff 位移的最后一个字节。
isnt this VERY bad for security considering that the code section is executable and CPU might by mistake execute a malicious data as code? (maybe attacker redirecting the program to that instruction? )
executable 页面中的只读数据可用作 Spectre 小工具,或 return 面向编程 (ROP) 攻击的小工具。但我认为,实际代码中通常已经有足够多的此类小工具,所以这没什么大不了的。
但是,是的,与您的其他观点不同,这是对这个实际上有效的次要反对意见。
最近(2019 年或 2018 年底),GNU Binutils ld
已开始将 .rodata
部分与 .text
部分放在不同的页面中,因此它可以是只读的 没有 执行权限。这使得静态只读数据不可执行 table,在像 x86-64 这样的 ISA 上,执行权限与读取权限是分开的。即在单独的 ELF 段中。
你可以让非executable的东西越多越好,混合代码+常量需要它们是executable。