最近,我需要为一个研究项目分析一些RISC-V汇编代码,然后计算一些基本指标,但我找不到合适的工具。好的,我将从一个较小的开放源码汇编器中获取一个解析器。这并不成功,因为我看到的那些使用粗糙的、基于正则表达式的解析器,这些解析器不维护有关结构的信息。
没问题,我会实现我自己的!让我查一下RISC-V汇编的语法...。哦,不,没有正式的吗?!等。除了指令集之外,语言本身甚至没有标准化,而且每个主要汇编语言都支持不同的符号和功能?
接受挑战!我的目标是编写一个支持GNU汇编程序(GAS)语法的解析器。
如果您正在寻找用于RISC-V程序集的手写词法分析器和解析器,可以构建解析树,并且没有任何第三方依赖项(例如,ANTLR或Yacc),那么这是为您准备的。在为RISC-V制作自己的短绒、美容剂或汇编器时,它可能会很方便。或者,如果您需要更深入的解析教程,请参阅我的教程系列的第一部分:让我们做一个Teeny Tiny编译器。
解析器的源代码可以在GitHub上找到。如果你想了解更多关于RISC-V组件的知识,请看我同事的课堂讲稿。
大多数汇编语言的核心都非常简单。源文件由零个或多个标签、指令、指令和注释组成。我们可以用EBNF表示它,如下所示:
这样的语言将非常容易解析。在每行的开头,确定它是标签、指令、指令还是注释。让我们继续这样做,并扩展这些术语:
程序::={Label|Instructive|Directive|Comment}标签::=Symbol';:';指令::=Symbol[Symbol{';,';Symbol}]指令::=';.';Symbol[Symbol{';,';Symbol}]
标签很简单。指令是一个符号,后跟以逗号分隔的零个或多个操作数。在这里,我使用符号作为一个包罗万象的词。它可以是数字、寄存器、指令或标签。指令本质上与指令相同,但它以句点开头。
我们准备好实现解析器了吗?不完全是。虽然这对于RISC-V组装来说几乎足够了,但它仍然缺少相当多的功能。仅举几例:标签可以与指令位于同一行,值可以具有前导正号或负号,并且可以为操作数指定偏移量。
程序::={[Label][指令|指令][注释]newline}标签::=Symbol';:';指令::=';.';|';+';|';-';]Symbol{';,';[';+';|';-';]Symbol}]指令::=Symbol[Operand{';,';Operand}]操作数::=[';+';|';-';](Symbol';(';Symbol';)';|Symbol)#正则表示法的词法分析器规则。newline::=[\n\r]+COMMENT::=#[^\n\r]+Symbol::=([a-Za-Z0-9](';.';?[A-Za-Z0-9])*)|(\";(\\[^\n]|[^";\n])*\";)#空格、逗号、冒号和圆括号是标记分隔符。#空格和制表符可以互换和连续使用。
但是等等!我们如何知道指令或寄存器是否正确使用?我决定尽量减少语法,并在以后验证这些内容。这意味着该语法将接受一些不允许的代码,但是在下一步我们可以删除。例如,";xor xor,xor&34;可以很好地解析,尽管它不是合法的RISC-V程序集。我们可以解决这个问题。
如果您对将解析器用于您自己的项目感兴趣,那么您可能需要添加一些东西。即,类似于编译器中的语义分析步骤的验证步骤,其检查以下内容中的一些:
文字值的形式和大小正确(例如,十六进制或可以用16位表示)。
您可以通过修改语法函数Label()、Directive()、Instruction()和Operand()在解析器中检查这些标记,以检查标记的内容。
Void Parser::Instruction(){NextToken();//是否至少有一个操作数?IF(!CheckToken(TokenType::newline)&;&;!CheckToken(TokenType::Comment)){Operand();//零个或多个操作数。While(CheckToken(TokenType::Comma)){NextToken();Operand();}
Void Parser::Instruction(){int numOperands=verifier->;LookupInstruction(token->;literal);if(numOperands==-1){ReportError(";Invalid Instruction.";);}NextToken();//是否至少有一个操作数?Int actNumOperands=0;if(!CheckToken(TokenType::Newline)&;&;!CheckToken(TokenType::Comment)){Operand();actNumOperands++;//零个或多个操作数。While(CheckToken(TokenType::Comma)){NextToken();Operand();actNumOperands++;}//验证操作数是否正确。If(numOperands!=actNumOperands){ReportError(";操作数不正确。";);}。
瞧啊!现在验证了指令和操作数的数量。要真正做到这一点,我建议实现一个Verizer类,该类具有查找指令/指令是否存在以及每个指令的操作数的数量和格式的函数,以及用于验证文字、寄存器和标签的函数。
同样,如果您正在构建汇编器,则可以添加发射器步骤。在此方案中,您将查找每条指令和操作数的相应二进制表示形式,并在解析时将其发送到文件。您可以在我的let‘s make a Teeny Tiny编译器教程的发出部分看到编译器是如何做到这一点的。
这个解析器不能解析的一个值得注意的特性是数学表达式。幸运的是,这是一个相当容易添加的东西!我以前写过一篇关于解析的教程,涵盖了数学表达式。请参阅“让我们做一个小小的编译器”的第1部分。GNU汇编器支持两个前缀运算符-和~用于否定和互补,并且这些二元运算符从最高优先级到最低优先级依次为:*、/、%、<;、<;、>;、>;>;、|、&;、^、!、+、-。
表达式::=按位{(";-";|";+";)按位}按位:=Term{(';|';|';&;';|';^';)Term}Term::=一元{(";/";|";*";)一元}符号::=[";+";|";-";]符号。
预处理器特性(即#include和#define)和多行注释留给读者作为练习……。🙂。
汇编器的核心确实非常简单。它们只做字符串替换,所以解析应该很简单。汇编语言甚至不是递归的!您只需将指令助记符和操作数替换为它们各自的二进制表示形式。然而,主流汇编器允许构造使解析稍微复杂一些,而且汇编语言没有标准化。尽管如此,本文应该证明,如果你循序渐进,这些问题是可以解决的。
希望这个解析器可以帮助您处理RISC-V项目。如果你使用它,给我发一封电子邮件,让我知道你的项目!您可以在GitHub上找到源代码。