我已经使用计算的goto(see here,如果不熟悉的话)实现了一个小字节码解释器。
似乎可以通过在标签之间复制内存来进行简单的JITting,从而优化跳转。例如,假设我的解释器中包含以下内容:
op_inc: val++; DISPATCH();
我将其更改为:
op_inc: val++;
op_inc_end:
JITting时,我会将标签之间的内存附加到我的输出中:
memcpy(jit_code+offset, &&op_inc, &&op_inc_end - &&op_inc);
({jit_code
被标记为可以使用mmap
执行)
最后,我将使用计算的goto
跳转到复制的机器代码的开头:
goto *(void*)jit_code
这行得通吗?在我的机器代码思维模型中是否缺少某些东西可以阻止这个想法?
让我们假设代码和数据共享相同的地址空间。我们还假设PIC。
更新
看看linked article中的示例,删除DISPATCH
后,我们得到:
do_inc:
val++;
do_dec:
val--;
do_mul2:
val *= 2;
do_div2:
val /= 2;
do_add7:
val += 7;
do_neg:
val = -val;
do_halt:
return val;
为do_inc
生成的代码(无优化)很简单:
Ltmp0: ## Block address taken
## %bb.1:
movl -20(%rbp), %eax
addl $1, %eax
movl %eax, -20(%rbp)
(后接do_dec
)。似乎可以剪切掉这个小片段。
答案 0 :(得分:5)
这是无法在一种架构上运行的另一个原因:
ARM Thumb代码将离线即时值与PC相对寻址一起使用。像
这样的操作a += 12345;
可以编译为:
ldr r3, [pc, #<offset to constant>]
adds r0, r4, r3
… other unrelated code …
bx lr ; end of the function
.word 12345 ; oh, and here's the constant
复制此函数的一个片段会使对常量的引用未解决;最终将使用内存中其他地方的意外值。
答案 1 :(得分:4)
不,这通常不会起作用。这只是众多原因之一:
考虑AVR架构。它是哈佛架构,因此代码和数据不在同一个地址空间中。在此代码上,您的代码将复制数据存储器中正好存在的任何数据的副本,其地址与您要复制的代码存储器的地址相同,但是无论如何都会忽略该代码,并使用与数据存储器的目标位置相同的地址。
答案 2 :(得分:3)
基础指令集体系结构不仅不能以这种方式工作(正如其他答案所指出的那样); C不能那样工作。没有什么限制编译器以与它们之间的标签相同的方式连续放置源代码级代码路径。可以进行各种转换,包括跨不同路径的通用子表达式消除,概述等。当然,它还可以使用以下假设:代码正在作为其一部分编写的函数中执行,而不是在其他上下文中执行,得出参与变换的不变量。
TL; DR:不是“高级汇编”,因此您不能在需要汇编器的地方使用它。
答案 3 :(得分:3)
更新...似乎可以将这个小片段剪掉。
是的,它可能会被裁剪掉,因为您没有优化就进行了编译,并且代码仅访问堆栈中的本地。
但仅当跳转到与此函数具有相同的堆栈布局时,才可以将其作为函数调用。但是,是的,我想用计算的goto可以行得通。
如果要访问静态存储中的任何内容(常量或全局/静态变量),则x86-64编译器将使用var(%rip)
之类的RIP相对寻址模式,如果更改相对代码的位置,则该模式会中断数据。
对其他函数的调用也将中断,因为它们将编译到目标地址相对于调用站点进行编码的call rel32
。
总体而言,这几乎没有用。您可以通过在可移植的C语言中编写解释器来更高效地完成未优化代码块周围的复制操作。通过启用优化,您的想法将很容易被打破。
例如,如果您想增加n
次,如果您仅复制n
次,引入存储/重载周期将向关键路径添加大约5个存储转发延迟周期。
此外,如果要启用优化,请在复制的范围上使用you need __builtin___clear_cache
。是的,这甚至适用于x86,在x86上,它实际上并未清除缓存,但仍会阻止死存储消除,而不会删除memcpy
。
如果您想花更多的时间来编写不可怕的机器代码,请使用LLVM之类的JIT引擎。
您的想法在某些有限的情况下可以用作玩具/实验,但我强烈建议您不要将其用于任何实际用途,尤其是在您以性能为目标的情况下。
答案 4 :(得分:1)
可以吗?
是;这可以工作。
但是,它仅在以下情况下才适用:
您的函数没有参数。例如,如果您正在使用像Brainfuck这样的语言(请参阅https://en.wikipedia.org/wiki/Brainfuck),则可能会使用诸如“ void increment(void);
和void putchar(void)
之类的函数来修改全局状态(例如struct machine_state { void * ptr; }
)
要么没有位置独立性(全局状态的地址是固定/恒定地址),要么可以告诉编译器保留寄存器以用作“指向全局状态的指针”(由GCC支持)。
目标计算机支持将数据视为代码的某种方式。注意:这是无关紧要的(如果机器不能以某种方式执行此操作,那么您也将无法执行文件)。
使用障碍来防止编译器将代码移出标签。注意:这也是相对不相关的(例如,如果您使用内联程序集定义标签并将内联程序集标记为易失性,然后将所有内容都放入容器列表中,那么..)。
您没有使用“纯便携式C”。注意:您已经在使用编译器特定的扩展名(计算后的goto),所以我认为这也是相对无关的。
对于所有这些限制;在实践中唯一可能重要的是第一个-值得进行JITting的任何事情都太复杂了(例如,您需要像void add(int register_number_1, int register_number_2);
这样的函数),并且一旦您尝试将参数传递给函数,您就会最终将取决于目标特定的调用约定。