- html - 出于某种原因,IE8 对我的 Sass 文件中继承的 html5 CSS 不友好?
- JMeter 在响应断言中使用 span 标签的问题
- html - 在 :hover and :active? 上具有不同效果的 CSS 动画
- html - 相对于居中的 html 内容固定的 CSS 重复背景?
我有一个抽象语法树,需要将其转换为虚拟机的程序集。我不知道如何最好地做到这一点,所以我开始使用一系列字符串模板。我的意思的伪代码示例,假设需要编译一个具有一个条件的简单 if 语句:
std::string compile_if(Node* n) {
std::string str = "";
curLabel = nLabels++;
str += compile_comparison(n->getChild(0));
str += ".true"+curLabel+":";
str += compile_block(n->getChild(1));
str += ".false"+curLabel+":";
return str;
}
每个compile_*都会根据当前/下一个AST节点生成一个汇编字符串。然后最终的字符串通过汇编器运行。这看起来很草率并且难以维护,当然这不是大多数编译器所做的。这是一个坏主意,我应该改变它吗?大多数其他编译器如何生成虚拟汇编代码/机器代码?
最佳答案
免责声明:我只有 X86 机器代码的经验。例如,其他指令集可能具有不同的寻址功能,因此我的部分建议可能不适用。抱歉,我现在没有时间研究指令集。
<小时/>首先,大多数编译器不会将程序集生成为文本,因为将代码序列化为程序集只是让汇编器立即解析它,效率有点低,正如您可能已经意识到的那样。有单独的编译和汇编阶段是合理的,但不是必需的。
在编译阶段,我会考虑的两种策略是:
(a) 将程序集生成为指令对象的树/数组,它们可以象征性地相互引用。在汇编阶段,这些需要被序列化为字节码/机器码。我推荐这种方法,即使它会使编译器的体系结构变得更加复杂。
(b) 将程序集作为机器码/字节码生成到缓冲区中,并带有一些辅助函数;在这种情况下,您实际上没有单独的组装阶段。我亲自尝试过这种方法,在单个函数的范围内它还不错,但由于不知道函数在组装之前会有多大,可能会造成一些额外的困难。
我猜想(a)是GCC等优化编译器使用的方法,而(b)是TCC等高速编译器使用的方法。
<小时/>让我们通过检查现有编译器为简单的 if/else
分支生成的代码来再次考虑 if
示例:
请注意反汇编中的重叠跳转 - 一个跳过“taken” block ,另一个跳过“not-taken” block 。
这些是相对跳转,因此为了组装它们,我们需要知道跳转指令和目标之间有多少字节指令。
以下是使用策略(a)的编译函数的示例:
Instruction[] compile_if(IfNode n) {
Instruction[] code;
code ~= compile_condition(n.condition);
Instruction skip_taken = new JumpInstruction(`jz`);
code ~= skip_taken;
code ~= compile_block(n.taken_block);
Instruction skip_nottaken = new JumpInstruction(`jmp`);
code ~= skip_nottaken;
Instruction[] nottaken_code = compile_block(n.nottaken_block);
skip_taken.destination = nottaken_code[0];
code ~= nottaken_code;
Instruction end = new NopInstruction();
skip_nottaken.destination = end;
code ~= end;
return code;
};
这应该是不言自明的。
请注意指令如何以符号方式相互引用 (skip_taken.destination = nottaken_code[0]
),而不是像序列化机器代码那样通过字节偏移来引用。我们将这些偏移计算留给汇编器。
另请注意,只有在 JumpInstruction
的目的地可用时,我们才设置它们。
最后的NopInstruction
只是给skip_nottaken
跳转一些引用。
现在,我们如何将这些跳转实际组装成真正的机器码/字节码?这是一种可能性(一个非常基本的示例):
byte[2] assemble_jz(Instruction[] code, int idx) {
// assemble the jz instruction at code[idx]
JumpInstruction jump = code[idx];
++idx;
byte jump_offset = 0;
while (code[idx] != jump.destination) {
jump_offset += size_of_instruction(code[idx]);
++idx;
};
byte[2] machinecode = [
0x74, // jz short
jump_offset
];
return machinecode;
};
由于汇编器可以访问所有指令对象,因此它可以通过向前扫描直到找到目标指令来计算相对跳转的实际偏移量。
<小时/>我希望这个简短的介绍可以帮助您开始设计自己的编译器后端。显然,我并不是建议您完全按照我的示例编写编译器,但它应该为您提供一些如何解决编译和汇编非线性指令 block 的一般问题的想法。
您可能还想查看一些现有的汇编器 API,例如 https://github.com/asmjit/asmjit .
祝你好运。
关于assembly - 将 AST 编译为汇编,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42445522/
我试图在图形模式下打印一个字符。通常当我打印我正在做的一个字符时: mov ah,14 ; ah=14 mov al,'x' int 10h ; print the character 这
我试图通过更改其中的一个字节来修改存储在内存中的字符串。我为此使用了 movb,但由于某种原因,给定内存位置的字节没有改变。 在 gdb 调试器上: 14 movb %al, (%r10) # nex
我一直在阅读一些汇编代码,并且开始发现调用指令实际上是与程序计数器相关的。 但是,每当我使用 Visual Studio 或 Windbg 进行调试时,它总是显示 call 0xFFFFFF ...这
我最近一直在使用 Visual C++ 中的内联汇编,我想知道是否可以直接向堆栈上的局部变量添加值,例如: push 5 add [esp], 7 这样做可以吗?我问这个问题是因为我在执行此操作时随机
我有下一个代码: mov al, -5 add al, 132 add al, 1 据我检查,溢出标志和进位标志将在第一个操作中设置,而在第二个操作中,仅设置溢出。 但我不明白为什么: 在无符号数中,
在 64 位 x86 汇编 nasm 中,如何将单个字节从寄存器移动到 .data 节中定义的内存位置? 我知道这有效 global _main section .data quotient db 0
我的汇编代码有问题。我想打印存储在寄存器 cx 中的数字,但是当我尝试打印它时,它打印的是 ascii 字符而不是 ascii 数字,所以我决定编写一个程序将 ascii char 转换为 ascii
为什么第 1B 行的跳转指令(例如)变成了 EBBD? 我知道“jmp”= EB但是BD是怎么计算的呢? 最佳答案 短跳转使用一个带符号的偏移量添加到 JMP 之后的指令地址。 例如,第一个 JMP
以下两者有什么区别: mov eax, [eax+4] 和 add eax, 4 mov eax, [eax] 如果不是,那么汇编器是否会选择哪个来进行某种优化? 最佳答案 这
看《The Shellcoder's Handbook》中的一些汇编和反汇编代码,发现一条指令的序列操作数是不一样的。 例如,在 assembly 上: mov ebx,0 并且,在反汇编时: mov
我有这个非常简单的汇编代码: start: add ax, 100 ; if ax overflow add to bx 1 jmp start 但我不知道如何检测 ax 寄存器溢出,有人可以帮
在 64 位 x86 汇编 nasm 中,如何将单个字节从寄存器移动到 .data 节中定义的内存位置? 我知道这有效 global _main section .data quotient db 0
我的汇编代码有问题。我想打印存储在寄存器 cx 中的数字,但是当我尝试打印它时,它打印的是 ascii 字符而不是 ascii 数字,所以我决定编写一个程序将 ascii char 转换为 ascii
我正在学习一些关于操作系统开发的教程,我发现了一篇关于多重引导 header 。这些是您必须定义的一些“神奇”值才能使用GRUB2。这些是命令: # Declare constants used f
为什么第 1B 行的跳转指令(例如)变成了 EBBD? 我知道“jmp”= EB但是BD是怎么计算的呢? 最佳答案 短跳转使用一个带符号的偏移量添加到 JMP 之后的指令地址。 例如,第一个 JMP
我正在尝试从内存中复制一些单词并使用汇编将其保存到另一个内存地址。我正在尝试为其编写代码,但我不确定其中的某些部分。我将简要描述我想要做什么。 源地址、目标地址和要复制的字数是函数的输入参数。 最佳答
当我们想要像这样创建一个初始化变量时: name db 'zara ali' 我们创建了一个字节大小变量,但我们在其中存储了一个字符串 这怎么可能?? 当我们使用这条指令时: MOV ecx, nam
我还是汇编的新手,我还不知道汇编中的许多命令代码。我想在 16 位寄存器中进行除法。我想打印它的内容。我知道我需要将寄存器的内容转换为 ASCII 进行打印,但同样,我的问题是除法。请帮我。 比如cx
使用有什么区别: c.eq.s $1, $2 bc1t L2 并使用: beq $1, $2, L2 如果他们做同样的事情,为什么有两种分支方式?如果它们不同,那么它们各自的好处是什么
源代码: int main() { int i; for(i=0, i : push rbp 2. 0x000055555555463b :
我是一名优秀的程序员,十分优秀!