C++ 编译器能否生成自修改代码?
通常说,C++ 编译器能否生成自修改代码?,c++,assembly,compiler-optimization,self-modifying,C++,Assembly,Compiler Optimization,Self Modifying,通常说,静态变量初始化被包装在if中,以防止它被多次初始化 对于这个条件和其他一次性条件,让代码在第一次通过自我修改后删除该条件会更有效 是C++编译器允许生成这样的代码,如果不是,为什么?我听说它可能会对缓存产生负面影响,但我不知道细节。没有什么可以阻止编译器实现您的建议,但它是一个非常重要的解决方案,可以解决一个非常小的性能问题 < >为了实现自修改代码,对于在Windows或Linux上运行的典型C++实现,编译器必须插入代码,这些代码会改变代码页上的权限,修改代码,然后恢复权限。这些操作
静态
变量初始化被包装在if
中,以防止它被多次初始化
对于这个条件和其他一次性条件,让代码在第一次通过自我修改后删除该条件会更有效
是C++编译器允许生成这样的代码,如果不是,为什么?我听说它可能会对缓存产生负面影响,但我不知道细节。
没有什么可以阻止编译器实现您的建议,但它是一个非常重要的解决方案,可以解决一个非常小的性能问题 < >为了实现自修改代码,对于在Windows或Linux上运行的典型C++实现,编译器必须插入代码,这些代码会改变代码页上的权限,修改代码,然后恢复权限。这些操作很容易花费比隐含的“if”操作在程序生命周期内花费更多的周期 这也会导致进程之间无法共享修改后的代码页。这看起来似乎无关紧要,但编译器通常对其代码持悲观态度(i386的情况非常糟糕),以便实现位置无关的代码,这些代码可以在运行时加载到不同的地址,而无需修改代码并阻止代码页的共享正如Remy Lebeau和Nathan Oliver在评论中提到的,也有线程安全问题需要考虑,但它们可能会被解决,因为有各种解决方案来修补这样的可执行文件。
回到过去,8086处理器对浮点数学一无所知。您可以添加一个数学协处理器8087,并编写使用它的代码。Fo代码由“陷阱”指令组成,这些指令将控制权转移到8087以执行浮点操作
Borland的编译器可以设置为生成浮点代码,在运行时检测是否安装了协处理器。第一次执行每个fp指令时,它将跳转到一个内部例程,该例程将对该指令进行反向修补,如果有协处理器,则使用8087陷阱指令(后跟几个NOP),如果没有协处理器,则调用适当的库例程。然后,内部例程将跳回已修补的指令 所以,是的,我可以做到。某种程度上。正如各种评论所指出的,现代建筑使这种事情变得困难或不可能早期版本的Windows有一个系统调用,可以在数据和代码之间重新映射内存段选择器。如果您使用数据段选择器调用了
PrestoChangoSelector
(是的,这就是它的名称),它将返回指向相同物理内存的代码段选择器,反之亦然。是的,这是合法的。ISC++对通过函数指针指向<代码>无符号char */code >能够访问数据(机器代码)提供了零保证。在大多数实际实现中,它都有很好的定义,除了在纯哈佛机器上,在那里代码和数据有单独的地址空间
热修补(通常通过外部工具)是一件事,如果编译器生成代码使之变得容易,那么热修补是非常可行的,也就是说,函数从一条足够长的指令开始,该指令可以自动替换
正如罗斯指出的,在大多数C++实现中,自我修改的一个主要障碍是,它们为通常只映射可执行页的OSES制作程序。W^X是避免代码注入的重要安全特性。只有对于具有非常热的代码路径的非常长时间运行的程序,才值得进行必要的系统调用,使页面读+写+执行为临时的,原子地修改指令,然后将其翻转回来
在像OpenBSD这样的系统上不可能真正执行W^X,不让进程mprotect
PROT_WRITE和PROT_EXEC同时保护页面。如果其他线程可以随时调用该函数,则使页面暂时不可执行是行不通的
通常说,静态变量初始化被包装在if中,以防止它被多次初始化
仅适用于非常量初始值设定项,当然也仅适用于静态局部变量。一个类似于static int foo=1的局部变量
将编译为与global scope相同的.long 1
(GCC用于x86,GAS语法),并在其上添加标签
但确实,使用非常量初始值设定项,编译器将发明一个可以测试的保护变量。他们安排了一些事情,因此guard变量是只读的,不像readers/Writer锁,但这仍然需要在快速路径上花费一些额外的指令
e、 g
编撰
因此,在主流CPU上,快速路径检查需要花费2个UOP:一个零扩展字节负载,一个宏融合测试和未执行的分支(test+je
)。但是是的,L1i缓存和解码uop缓存的代码大小都不是零,并且通过前端发出的成本也不是零。还有一个额外的静态数据字节,必须在缓存中保持热状态才能获得良好的性能
通常内联可以忽略不计。如果您实际上是在一开始就用它调用一个函数,那么调用/ret开销的其余部分是一个更大的问题
但是,如果没有廉价的获取负载,ISAs上的情况就不太好。(例如,ARMv8之前的ARM)。初始化静态变量后,不是以某种方式安排一次barrier()所有线程,而是每次检查guard变量都是一个获取负载。但在ARMv7和更早版本上,这是通过一个完整的内存屏障(数据内存屏障:内部共享)完成的,它包括耗尽存储缓冲区,与原子线程围栏(mo_seq_cst)完全相同。(ARMv8有ldar
(word)/ldab
(字节)进行采集
int init();
int foo() {
static int counter = init();
return ++counter;
}
foo(): # with demangled symbol names
movzx eax, BYTE PTR guard variable for foo()::counter[rip]
test al, al
je .L16
mov eax, DWORD PTR foo()::counter[rip]
add eax, 1
mov DWORD PTR foo()::counter[rip], eax
ret
.L16: # slow path
acquire lock, one thread does the init while the others wait
# ARM 32-bit clang 10.0 -O3 -mcpu=cortex-a15
# GCC output is even more verbose because of Cortex-A15 tuning choices.
foo():
push {r4, r5, r11, lr}
add r11, sp, #8
ldr r5, .LCPI0_0 @ load a PC-relative offset to the guard var
.LPC0_0:
add r5, pc, r5
ldrb r0, [r5, #4] @ load the guard var
dmb ish @ full barrier, making it an acquire load
tst r0, #1
beq .LBB0_2 @ go to slow path if low bit of guard var == 0
.LBB0_1:
ldr r0, .LCPI0_1 @ PC-relative load of a PC-relative offset
.LPC0_1:
ldr r0, [pc, r0] @ load counter
add r0, r0, #1 @ ++counter leaving value in return value reg
str r0, [r5] @ store back to memory, IDK why a different addressing mode than the load. Probably a missed optimization.
pop {r4, r5, r11, pc} @ return by popping saved LR into PC