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