我有兴趣为一个爱好项目编写一个x86汇编程序。
起初它对我来说似乎相当直接,但我读到的越多,我发现自己遇到的问题就越多。我并非完全缺乏经验:我已经使用了相当数量的MIPs汇编,我在学校为C的一个子集编写了一个玩具编译器。
我的目标是编写一个简单但功能强大的x86汇编程序。我不打算建立一个商业上可行的汇编程序,而只是一个业余爱好项目,以加强我在某些领域的知识。所以我不介意我是否没有实现所有可用的功能和操作。
我有很多问题,例如:我应该使用一次通过还是两次通过?我应该使用ad-hoc解析还是定义正式语法并使用解析器生成器来执行我的指令?在什么阶段,我如何解决我的符号的地址?
根据我的要求,是否有人可以建议我在宠物项目汇编程序中使用的方法的一般指导原则?
答案 0 :(得分:7)
David Salomon有一本关于如何建造装配工和装载机的优秀免费电子书(pdf)。您可以在以下网址找到它:
答案 1 :(得分:6)
您可能会发现dragon book有帮助。
实际标题为Compilers: Principles, Techniques, and Tools(amazon.com)。
查看Intel Architectures Software Developer's Manuals以获取IA-32和IA-64指令集的完整文档。
AMD's architecture technical documents也可以在其网站上找到。
Linkers and Loaders(amazon.com)是对象格式和链接问题的良好介绍。 (unedited original manuscript也可在线获取。)
答案 2 :(得分:4)
虽然许多人建议使用临时解析器,但我认为现在应该使用解析器生成器,因为它确实简化了构建有趣的现代汇编程序所需的所有复杂语法的问题。请参阅我的BNF示例/ StackOverflow: Z80 ASM BNF的答案。
“一次通过”与“两次通过”是指您是否两次阅读源代码本身。你总是可以做一个通道汇编程序。这有两种方式:
1)动态生成二进制结果(将这些结果视为抽象中的对,往往具有单调递增的地址),并在找到允许您解析前向引用的信息时将补丁作为修正发回(将这些视为只需将地址用于覆盖先前发出的位置的对。对于JMP,在遇到JMP操作码时提交它的类型/大小。根据品味甚至是汇编程序选项,默认值可以是short或long。编码器输入的一小段语法“使用另一种”或“我坚持这种”就足够了(例如,“ JMP long target”)来处理汇编程序默认选择的情况是错的。 (这是汇编程序,可以使用时髦的规则)。
2)在(第一次)传递中,将数据生成到内存中的缓冲区。短路偏移的默认JMP(以及其他与跨度相关的指令)。记录所有JMP的位置(跨度依赖指令等)。在这个通行证的最后,回到JMP并修改那些“太短”而不是更长的那些;洗牌并调整其他JMP。 1978年的一篇论文中提出了一个聪明的方案来实现这一目标并实现几乎最优的短JMP集合: Assembling code for machines with span-dependent instructions/Szymanski
答案 3 :(得分:2)
要回答你的一个问题,一遍不可行,除非你在通过后发出代码。
想象一下:
JMP some_label
.. code here
some_label:
你发出什么作为JMP指令的距离值?你发出哪个JMP指令,需要一个接近值的指令,还是标签很远?
所以双通应该是最小的。
答案 4 :(得分:1)
您需要编写词法分析器和解析器来读取源代码并输出抽象语法树(AST)。然后可以遍历AST以生成字节代码输出。
我建议研究编写编译器的书籍。这通常是大学班级,所以应该有很多书。对不起,我不能特别推荐一个。
您还可以阅读ANTLR工具。它可以采用各种语言的语法规则和输出代码来为您提供词法分析器/解析器。
在一遍或两遍:你需要一个双程编译器来解析前向引用。如果这不重要,那么一次通过即可。我建议你保持简单,因为这是你的第一个编译器。
答案 5 :(得分:1)
鉴于这是一个爱好项目,你的很多问题实际上都归结为“你最感兴趣的是什么方面的问题?”如果您有兴趣了解解析工具如何映射到汇编程序的问题(特别是因为它涉及宏处理等),您应该使用它们。另一方面,如果你对这些问题不太感兴趣并且只是想进入指令打包和布局的问题并且满足于拥有一个没有宏的最小汇编程序,那么解析的快速和肮脏可能是要走的路。
对于单通道和多通道 - 您是否有兴趣使用最小化内存占用的快速汇编程序?如果是这样,这个问题变得相关。如果没有,只需将整个程序啜饮到内存中,在那里处理它,在内存中创建一个对象图像,然后将其写出来。没有必要担心“通行证”。在这个模型中,您可以更轻松地以不同的顺序处理事务,看看权衡取舍,这是业余爱好项目的重点。
答案 6 :(得分:1)
我经常幻想尝试构建(又一种)高级计算机语言。目标是试图推动发展的快速性和结果的表现。我会尝试构建一些最小的,相当高度优化的操作的库,然后尝试以这样的方式开发语言规则:语言中可表达的任何语句或表达式将产生最佳代码..除非表达的是本质上不是最理想的。
它将编译为字节代码,它将被分发,然后在安装时或在处理器环境发生变化时加工到机器代码。因此,当加载可执行文件时,会有一个加载器部件检查处理器和对象中的几个字节的控制数据,如果两者匹配,则可以直接加载对象的可执行部分,但如果没有,然后必须重新编译该对象的字节代码,并更新可执行部分。 (所以它不是Just In Time编译 - 它是在程序安装或CPU更改编译。)加载器部分将非常简短和甜蜜,它将在'386代码,所以它不需要编译。它只会在需要时加载字节码编译器,如果需要,它会加载一个小而紧的编译器对象,并针对检测到的架构进行优化。理想情况下,加载器和编译器在加载后将保持驻留状态,并且只有两个实例。
无论如何,我想回应你必须至少有两次传球的想法 - 我不认为我完全同意。是的,我会使用第二次通过编译的代码,但不是通过源代码。
当你遇到符号时,检查符号哈希表,如果没有条目,则创建一个,并在编译代码中存储一个“前向引用”标记,并带有指向表项的指针。当您遇到标签和符号的定义时,请更新(或将新数据放入)符号表。
单个编译对象永远不会太大,以至于它们占用了大量内存,所以,绝对所有已编译的代码都应保存在内存中,直到整个内容准备好写出来。保持内存占用打印的方式很简单,一次只处理一个对象,并且一次不要在内存中保留多个源代码的小缓冲区。也许64k或128k或其他东西。 (与从磁盘读取数据所花费的时间相比,从磁盘加载缓冲区调用所涉及的开销很小,因此优化了流式传输。)
因此,一个传递一个对象的源流,然后你将你的碎片串在一起,在你去的时候从散列表中收集必要的前向参考信息,如果数据不存在 - 那就是编译错误。这是我想尝试的过程。
答案 7 :(得分:1)
使用NASM的表格,并尝试使用表格进行解码来实现更基本的指令
答案 8 :(得分:0)
我写了几个解析器。 我写了几个手工制作的解析器,我也尝试了yacc类型的解析器......
手工解析器提供更大的灵活性。 Yacc提供了一个必须适应或失败的框架.Yacc解析器默认提供快速解析器,但是如果您不熟悉这些方法并且您的解析器环境不是,那么在shift / reduce和reduce / reduce之后可能需要付出相当大的努力。最好的。 关于Yacc的优势。如果您需要,它会为您提供一个系统。手工制作的解析器为您提供自由,但您可以将其填入吗? 汇编语言似乎很简单,可以由yacc或类似的解析器处理。
我的手工解析器将包含一个tokenizer / lexer,我会通过for循环遍历令牌数组并通过在循环中放置ifs或case语句并检查当前令牌或下一个/上一个来执行某种事件处理。有可能我会为表达式使用单独的解析器...... 我会将翻译代码放入字符串数组并“记下”已翻译代码的未计算部分,以便程序可以在以后找到它们并填充空白..可能有空白而不是一切都是事先知道的,当一个人解析码。例如。跳跃的位置。
另一方面,无论您第一次使用解析器并且有时间,都可以将解析器从一种类型转换为另一种类型。根据你的身份,你甚至可能喜欢这样。
还有其他解析器而不是Yacc,它们承诺更多的灵活性和更少的“错误”,但这并不意味着你不会得到错误,它们不会那么明显,而且可能不会那么快。如果这很重要。
顺便说一句,如果存储了令牌,那么甚至可以考虑使用混合yacc和手工制作的解析器。