欢迎来到分组备注符号大全! 手机访问:c语言编译符号常量位置

c语言编译符号常量位置

上官刃来自:美国 California.加利福尼?州 文图拉.Ventura  坐标: 96916° 时间:2019-06-12 14:37

c语言编译符号常量位置: c语言在线编译器_语言在线编译器,缺失:c语言编译符号常量位置

我们找到第1篇与c语言在线编译器_语言在线编译器有关的信息,分别包括:

以下是的一些我们精选的c语言在线编译器_语言在线编译器

c语言编译符号常量位置本文为编写 C 语言编译器系列文章的第一篇,为此我先说明一下我要如此做的一些理由:

1. 通过编写过程,你会对抽象语法树(AST)以及程序如何表示和操作其他程序有清晰地了解,并可以熟练地使用 linters、静态分析器与各种各样的元编程。

2. 你将对汇编,调用约定,以及所有相关的技术细节有个系统了解。

3. 由于这是很困难的项目,所以整个过程我都在探索,有不对的地方请大家多指正。

在过去的几个星期里,我一直在借鉴 Abdulaziz Ghuloum 的" 编译器构建的增量方法 ",在自己的 C 语言编译器nqcc进行探索。Ghuloum 的方法的大致是这样的:

第一,本文介绍的只是基本的 X86 汇编语言的一个子集,其中涉及汇编语言的最核心部分,包括寄存器结构,数据表示,基本的操作指令(包括数据传送指令、逻辑计算指令、算数运算指令),以及函数的调用规则。

第二,添加新的语言特性,整个添加过程可以一步一步地慢慢进行。一开始,只会返回常数,而随着步骤的增加,就要处理加减法了。

第三,虽然每一步的进展都很慢,但这保证了你对每一步进行充分的管理,在每一步的末尾,都有一个编译器。

我将在本文中,介绍算术运算,条件语句,局部变量,函数调用等概念。除此以外,我还编写了一些测试程序,以方便验证编译器的每个阶段是否在正常工作。

准备阶段

在编写 C 语言编译器之前,你还需要完成两件事:

1. 决定使用哪种编译器的语言;

2. 如何处理解析和词法分析。

对于编写语言,虽然我没有什么硬性规定,你可以用你喜欢的任何语言来编写编译器,但我的建议是使用具有和类型 ( sum types ) 和模式匹配的语言,比如 OCaml,Haskell 或者 Rust。因为根据我的经验,这些语言在构建和遍历一个 AST 时会更加容易。其实最初,我也用的是 Python,但是到最后,我还是选择了 OCaml。

另外,你还需要决定是编写自己的解析和词法分析器,还是使用自动解析器和扫描生成器(例如 flex 和 bison)。我会在本文中,向你展示如何手动去编写紫的词法分析器(或扫描器)以及递归下降语法分析器。虽然使用 解析生成器可能更容易编写,但我还没有尝试过该方法。你还可以使用扫描生成器来实现词法分析,但前提是要手动编写自己的解析器。

整数分析

一开始,我会编译一个可以返回单个整数的程序。另外,我还将为编译器设置三个基本的路径(pass)。这些被定义的体系结构将为以后添加更多的语言功能便利。

下面就是一个经过编译的程序,我将其称为 return_2.c。

int main ( ) { return 2;}

我只能用一个单一的函数来处理程序 main,它由一个单一的 return 语句组成。唯一不同的是正在返回的整数值,不过我不会处理十六进制或八进制的整数。为了验证你的编译器运行是否正常,你需要编译一个程序,运行它,然后检查它的返回码。

$ ./YOUR_COMPILER return_2.c # compile the source file shown above $ ./gcc -m32 return_2.s -o return_2 # assemble it into an executable $ ./return_2 # run the executable you just compiled $ echo $? # check the return code; it should be 2 2

此时,编译器将生成 x86 汇编。我不会将汇编程序转换成可执行文件,它们是独立的程序。为了了解这个汇编程序,我用 gcc 对它进行了编译。

$ gcc -S -O3 -fno-asynchronous-unwind-tables return_2.c $ cat return_2.s .section __TEXT,__text_startup,regular,pure_instructions .align 4 .globl _main _main: movl $2, %eax ret .subsections_via_symbols

现在,你就可以看到汇编程序的全貌了。不过,我忽略了 .section,.align 和 .subsections_via_symbols 指令,如果你删除它们,你仍然可以组装并运行

return_2.s。 .globl _main 表示 _main 符号应该对链接器可见,否则你无法找到程序的入口点。另外,其中还附有汇编指令: _main: ; label for start of "main" function movl $2, %eax ; move constant "2" into the EAX register ret ; return from function

当函数返回时,EAX 寄存器将包含它的返回值,而 main 函数的返回值将是程序的出口代码。

这个汇编程序代码中唯一可以改变的是返回值。因此,一个非常简单的方法就是使用正则表达式从源代码中提取返回值,然后将其插入汇编程序中,你可以利用以下这 20 行的 Python 脚本来实现。

import sys, os, re #expected form of a C program, without line breaks source_re = r"int mains* ( s* ) s*{s*returns+ ( ?P [ 0-9 ] + ) s*;s*}" assembly_format = """ .globl _main _main: movl ${}, %eax ret """ source_file = sys.argv [ 1 ] assembly_file = os.path.splitext ( source_file ) [ 0 ] + ".s" with open ( source_file, ’ r ’ ) as infile, open ( assembly_file, ’ w ’ ) as outfile: source = infile.read ( ) .strip ( ) match = re.match ( source_re, source ) # extract the named "ret" group, containing the return value retval = match.group ( ’ ret ’ ) outfile.write ( assembly_format.format ( retval ) )

由于用正则表达式来解析整个程序并不是一个长期可行的方法,所以我将编译器分成三个阶段:词法分析,解析和代码生成,这是一个非常标准的编译器结构。

词法分析

词法分析器(也称为扫描器或标记器)是编译器将字符串(源代码)分解为标记列表(list of token)的阶段。标记记号是词法分析器可以理解的最小单位,许多标记就是用空格分隔的单个单词。变量名,关键字和常量以及大括号等标点符号都是标记记号。以下是 return_2.c 中所有标记的列表:

int 关键字 标识符 "main" 左括号 右括号 前大括号 返回关键字 常量 "2" 分号 后大括号

请注意,某些标记只有一个值(例如,常量标记的值为 "2"),另外请注意,没有空格标记。在某些语言中,如 Python,空格是很重要的,而且你需要对它进行标记。

以下是你的词法分析器需要识别的所有标记,以及定义每个标记的正则表达式:

前大括号 { 后大括号 } 左括号 ( 右括号 ) 分号 Int 关键字 int 返回关键字 return 标识符 [ a-zA-Z ] w* 整数值 [ 0-9 ] +

如果你愿意,你可以只使用一个 " 关键字 " 标记,而不是为每个关键字都进行不同标记。

可以进行的编写

此时,我就可以编写一个接受文件的 lex 函数并返回一个标记列表。它应该适用于测试套件中的所有起始示例,包括无效的示例。 所谓无效的示例就是在解析器中引发错误,而不是词法分析器。为了简单起见,我只使用十进制整数。如果你喜欢,你可以扩展你的词法分析器来处理八进制和十六进制整数。不过要注意,没有负整数。

解析过程

现在将我的令牌列表转换为 AST, AST 是表示程序结构的一种方式。在大多数编程语言中,像条件语句和函数语句这样的语言结构是由简单的结构组成的,比如变量和常量。 而 AST 可以捕捉到这种关系,AST 的根就是整个程序,每个节点都有代表其组成部分的子节点。比如下面这个示例:

if ( a ’ , operand_1=Var ( a ) , operand_2=Var ( b ) ) //create if body assign = Assignment ( var=Var ( c ) , rhs=Const ( 2 ) ) return = Return ( val=Var ( c ) ) if_body = [ assign, return ] //create else body assign_else = Assignment ( var=Var ( c ) , rhs=Const ( 3 ) ) else_body = [ assign_else ] //construct if statement if = If ( condition=cond, body=if_body, else=else_body )

但现在,我不需要担心条件,变量赋值或二元运算符。现在,我需要支持的唯一 AST 节点是程序,函数语句,语句和表达式。以下是我将如何定义它们的方法:

program = Program ( function_declaration ) function_declaration = Function ( string, statement ) //string is the function name statement = Return ( exp ) exp = Constant ( int )

现在,一个程序由一个单一的函数 main 组成,稍后我会将一个程序定义为函数列表。一开始,每个函数只有一个名称和一个主体。后来,又多了一个参数列表。对于投入运行的 C 编译器来说,我也需要存储函数的返回类型,不过现在我只有整数类型。函数体是一个单独的语句,稍后我会将它变成语句列表(list of statements)。现在,它只有一种类型的语句——返回语句。稍后我将添加其他类型的语句,如条件和变量语句。返回语句有一个子节点,一个表达式即返回的值。现在表达式只能是一个整数常量。稍后我将让表达式包含算术运算,这将允许我解析像 return 2+2 这样的语句。

当我添加新的语言结构时,我将更新我的 AST 节点的定义。例如,当我添加一个新类型的语句——变量赋值,我会在 statement 定义中添加一个新的范式(form ):

statement = Return ( exp ) | Assign ( variable, exp )

以下是 return_2.c 的 AST 图:

c语言编译符号常量位置最后,我需要一个正式的语法,它负责定义如何将一系列的标记组合起来形成完整的语言结构。我将在巴科斯范式中定义它:

::= ::= "int" " ( " " ) " "{" "}" ::= "return" ";" ::=

上面的每一行都表示一个运行规则,定义了如何从其他语言结构和标记构建语言结构。运行规则左侧出现的每个符号(例如,,,)都是非终结符号。而单个标记(例如,关键字,标点符等)则是终端符号。需要注意的是,虽然这个语法让我可以构建一个有效的 C 程序,但它并没有告诉我如何将这个程序转换成一个 AST,例如,在 AST 中没有对应于常量节点的运行规则。当重写我的语法来为常量创建一个运行规则后,

我发现语法就变得非常简单,起初,每个非终端符号只需一个运行规则。例如,如果我添加了对变量语句的支持,则可以用以下规则来派生语句:

::= "return" ";" | "int" "=" ";"

为了将令牌列表转换成 AST,我将使用一种称为递归下降解析的技术。我将定义一个函数来解析语法中的每个非终结符号,并返回相应的 AST 节点。解析符号 S 的函数应该从列表的开始处删除标记,直到形成 S. If 的有效派生。如果在解析完成之前,就碰到了一个不在 S 的运行规则中的标记,将会派生失败。如果 S 的规则包含其它非终结符,则应该调用其他函数来解析它们。

以下是解析语句的伪代码:

def parse_statement ( tokens ) : tok = tokens.next ( ) if tok.type != "RETURN_KEYWORD": fail ( ) tok = tokens.next ( ) if tok.type != "INT" fail ( ) exp = parse_exp ( tokens ) //parse_exp will pop off more tokens statement = Return ( exp ) tok = tokens.next ( ) if tok.type != "SEMICOLON": fail ( ) return statement

稍后,运行规则将是递归的(例如,一个算术表达式可以包含其他表达式),这意味着解析函数也将是递归的,因此此方法就顺理地被称为递归下降解析。

编写一个解析函数,从而接受一个标记列表并返回一个基于程序节点的 AST。该函数应该为所有有效的示例构建正确的 AST,并在所有无效的示例中引发错误。

有许多表示 AST 的代码,具体取决于编译器所使用的语言。例如,可以将 AST 节点定义为 OCaml 数据类型:

type exp = Const ( int ) type statement = Return ( exp ) type fun_decl = Fun ( string, statement ) type prog = Prog ( fun_decl )

代码的生成

现在我已经构建了一个 AST,并准备生成一些汇编程序,我只需要发送四行汇编即可。为了发送,我将遍历 AST,这意味着我将按以下顺序访问:

1. 函数名称(不是一个真正的节点,而是函数定义中的第一项);

2. 返回值;

3. 返回语句。

这个遍历可以确保在使用汇编程序之前预定义一个值,例如,我在返回语句中引用之前生成的返回值。

以下就是我需要的汇编程序:

1. 生成一个函数(例如函数 "foo"):

.globl _foo _foo:

2. 生成返回语句(例如,return 3;):

movl $3, %eax ret

可以进行的编写

编写一个接受 AST 并生成汇编程序的生成函数。它可以将程序集作为字符串返回,也可以直接将其写入文件。它应该为所有有效示例生成正确的汇编程序。

输出

此时,你可能需要一个实用程序函数来打印出 AST,以帮助进行调试。你可以现在就编写或者等到你需要的时候再写。下面是使用 nqcc 输出的 return_2.c。

FUN INT main: params: ( ) body: RETURN Int

这个示例包含了 AST 不需要的一些信息,比如返回类型和函数参数列表。

用一种可读的方式写一个预打印的函数,它以 AST 的形式打印出来。

编写一个接受 C 源文件并输出可执行文件的程序,该程序要实现的功能如下:

1. 阅读文件;

2. 词法分析;

3. 解析;

4. 生成汇编程序;

5. 将汇编程序写入文件;

6. 调用 GCC 命令将汇编程序转换为可执行文件:

gcc -m32 assembly.s -o out

在此命令中,"assembly.s" 是汇编程序文件的名称,"out" 是要生成的可执行文件的名称。 -m32 选项会调用 GCC 命令构建一个 32 位的二进制文件。如果需要,可以省略该选项并构建 64 位二进制文件,但稍后需要对代码生成步骤进行一些更改(例如,使用 64 位寄存器)。

7. 删除汇编程序文件。

测试

你可以通过测试你的编译器查看是否正确地使用了测试脚本,正常情况下,它会使用你的编译器编译一组测试程序,然后执行它们,并确保它们返回正确的值。

调用过程如下:

./test_compiler.sh /path/to/your/compiler

要用脚本测试,你的编译器就需要遵循下面这 3 个标准:

1. 可以从命令行调用一个 C 源文件作为参数,例如:./YOUR_COMPILER /path/to/program.c。

2. 当通过 program.c 时,它会在同一个目录下生成可执行程序。

3. 如果解析失败,它不会生成程序集或可执行文件,因为这是测试脚本检查无效测试程序的方法。

要注意的是,虽然该脚本不负责检查编译器是否输出了合理的错误信息,但可以使用无效的测试程序来手动测试。

c语言编译符号常量位置: c语言编译器: 如何编写属于自己的 C 语言编译器,缺失:c语言编译符号常量位置

我们找到第1篇与c语言编译器: 如何编写属于自己的 C 语言编译器有关的信息,分别包括:

以下是的一些我们精选的c语言编译器: 如何编写属于自己的 C 语言编译器

c语言编译符号常量位置本文为编写 C 语言编译器系列文章的第一篇,为此我先说明一下我要如此做的一些理由:

1. 通过编写过程,你会对抽象语法树(AST)以及程序如何表示和操作其他程序有清晰地了解,并可以熟练地使用 linters、静态分析器与各种各样的元编程。

2. 你将对汇编,调用约定,以及所有相关的技术细节有个系统了解。

3. 由于这是很困难的项目,所以整个过程我都在探索,有不对的地方请大家多指正。

在过去的几个星期里,我一直在借鉴 Abdulaziz Ghuloum 的" 编译器构建的增量方法 ",在自己的 C 语言编译器nqcc进行探索。Ghuloum 的方法的大致是这样的:

第一,本文介绍的只是基本的 X86 汇编语言的一个子集,其中涉及汇编语言的最核心部分,包括寄存器结构,数据表示,基本的操作指令(包括数据传送指令、逻辑计算指令、算数运算指令),以及函数的调用规则。

第二,添加新的语言特性,整个添加过程可以一步一步地慢慢进行。一开始,只会返回常数,而随着步骤的增加,就要处理加减法了。

第三,虽然每一步的进展都很慢,但这保证了你对每一步进行充分的管理,在每一步的末尾,都有一个编译器。

我将在本文中,介绍算术运算,条件语句,局部变量,函数调用等概念。除此以外,我还编写了一些测试程序,以方便验证编译器的每个阶段是否在正常工作。

准备阶段

在编写 C 语言编译器之前,你还需要完成两件事:

1. 决定使用哪种编译器的语言;

2. 如何处理解析和词法分析。

对于编写语言,虽然我没有什么硬性规定,你可以用你喜欢的任何语言来编写编译器,但我的建议是使用具有和类型 ( sum types ) 和模式匹配的语言,比如 OCaml,Haskell 或者 Rust。因为根据我的经验,这些语言在构建和遍历一个 AST 时会更加容易。其实最初,我也用的是 Python,但是到最后,我还是选择了 OCaml。

另外,你还需要决定是编写自己的解析和词法分析器,还是使用自动解析器和扫描生成器(例如 flex 和 bison)。我会在本文中,向你展示如何手动去编写紫的词法分析器(或扫描器)以及递归下降语法分析器。虽然使用 解析生成器可能更容易编写,但我还没有尝试过该方法。你还可以使用扫描生成器来实现词法分析,但前提是要手动编写自己的解析器。

整数分析

一开始,我会编译一个可以返回单个整数的程序。另外,我还将为编译器设置三个基本的路径(pass)。这些被定义的体系结构将为以后添加更多的语言功能便利。

下面就是一个经过编译的程序,我将其称为 return_2.c。

int main ( ) { return 2;}

我只能用一个单一的函数来处理程序 main,它由一个单一的 return 语句组成。唯一不同的是正在返回的整数值,不过我不会处理十六进制或八进制的整数。为了验证你的编译器运行是否正常,你需要编译一个程序,运行它,然后检查它的返回码。

$ ./YOUR_COMPILER return_2.c # compile the source file shown above $ ./gcc -m32 return_2.s -o return_2 # assemble it into an executable $ ./return_2 # run the executable you just compiled $ echo $? # check the return code; it should be 2 2

此时,编译器将生成 x86 汇编。我不会将汇编程序转换成可执行文件,它们是独立的程序。为了了解这个汇编程序,我用 gcc 对它进行了编译。

$ gcc -S -O3 -fno-asynchronous-unwind-tables return_2.c $ cat return_2.s .section __TEXT,__text_startup,regular,pure_instructions .align 4 .globl _main _main: movl $2, %eax ret .subsections_via_symbols

现在,你就可以看到汇编程序的全貌了。不过,我忽略了 .section,.align 和 .subsections_via_symbols 指令,如果你删除它们,你仍然可以组装并运行

return_2.s。 .globl _main 表示 _main 符号应该对链接器可见,否则你无法找到程序的入口点。另外,其中还附有汇编指令: _main: ; label for start of "main" function movl $2, %eax ; move constant "2" into the EAX register ret ; return from function

当函数返回时,EAX 寄存器将包含它的返回值,而 main 函数的返回值将是程序的出口代码。

这个汇编程序代码中唯一可以改变的是返回值。因此,一个非常简单的方法就是使用正则表达式从源代码中提取返回值,然后将其插入汇编程序中,你可以利用以下这 20 行的 Python 脚本来实现。

import sys, os, re #expected form of a C program, without line breaks source_re = r"int mains* ( s* ) s*{s*returns+ ( ?P [ 0-9 ] + ) s*;s*}" assembly_format = """ .globl _main _main: movl ${}, %eax ret """ source_file = sys.argv [ 1 ] assembly_file = os.path.splitext ( source_file ) [ 0 ] + ".s" with open ( source_file, ’ r ’ ) as infile, open ( assembly_file, ’ w ’ ) as outfile: source = infile.read ( ) .strip ( ) match = re.match ( source_re, source ) # extract the named "ret" group, containing the return value retval = match.group ( ’ ret ’ ) outfile.write ( assembly_format.format ( retval ) )

由于用正则表达式来解析整个程序并不是一个长期可行的方法,所以我将编译器分成三个阶段:词法分析,解析和代码生成,这是一个非常标准的编译器结构。

词法分析

词法分析器(也称为扫描器或标记器)是编译器将字符串(源代码)分解为标记列表(list of token)的阶段。标记记号是词法分析器可以理解的最小单位,许多标记就是用空格分隔的单个单词。变量名,关键字和常量以及大括号等标点符号都是标记记号。以下是 return_2.c 中所有标记的列表:

int 关键字 标识符 "main" 左括号 右括号 前大括号 返回关键字 常量 "2" 分号 后大括号

请注意,某些标记只有一个值(例如,常量标记的值为 "2"),另外请注意,没有空格标记。在某些语言中,如 Python,空格是很重要的,而且你需要对它进行标记。

以下是你的词法分析器需要识别的所有标记,以及定义每个标记的正则表达式:

前大括号 { 后大括号 } 左括号 ( 右括号 ) 分号 Int 关键字 int 返回关键字 return 标识符 [ a-zA-Z ] w* 整数值 [ 0-9 ] +

如果你愿意,你可以只使用一个 " 关键字 " 标记,而不是为每个关键字都进行不同标记。

可以进行的编写

此时,我就可以编写一个接受文件的 lex 函数并返回一个标记列表。它应该适用于测试套件中的所有起始示例,包括无效的示例。 所谓无效的示例就是在解析器中引发错误,而不是词法分析器。为了简单起见,我只使用十进制整数。如果你喜欢,你可以扩展你的词法分析器来处理八进制和十六进制整数。不过要注意,没有负整数。

解析过程

现在将我的令牌列表转换为 AST, AST 是表示程序结构的一种方式。在大多数编程语言中,像条件语句和函数语句这样的语言结构是由简单的结构组成的,比如变量和常量。 而 AST 可以捕捉到这种关系,AST 的根就是整个程序,每个节点都有代表其组成部分的子节点。比如下面这个示例:

if ( a ’ , operand_1=Var ( a ) , operand_2=Var ( b ) ) //create if body assign = Assignment ( var=Var ( c ) , rhs=Const ( 2 ) ) return = Return ( val=Var ( c ) ) if_body = [ assign, return ] //create else body assign_else = Assignment ( var=Var ( c ) , rhs=Const ( 3 ) ) else_body = [ assign_else ] //construct if statement if = If ( condition=cond, body=if_body, else=else_body )

但现在,我不需要担心条件,变量赋值或二元运算符。现在,我需要支持的唯一 AST 节点是程序,函数语句,语句和表达式。以下是我将如何定义它们的方法:

program = Program ( function_declaration ) function_declaration = Function ( string, statement ) //string is the function name statement = Return ( exp ) exp = Constant ( int )

现在,一个程序由一个单一的函数 main 组成,稍后我会将一个程序定义为函数列表。一开始,每个函数只有一个名称和一个主体。后来,又多了一个参数列表。对于投入运行的 C 编译器来说,我也需要存储函数的返回类型,不过现在我只有整数类型。函数体是一个单独的语句,稍后我会将它变成语句列表(list of statements)。现在,它只有一种类型的语句——返回语句。稍后我将添加其他类型的语句,如条件和变量语句。返回语句有一个子节点,一个表达式即返回的值。现在表达式只能是一个整数常量。稍后我将让表达式包含算术运算,这将允许我解析像 return 2+2 这样的语句。

当我添加新的语言结构时,我将更新我的 AST 节点的定义。例如,当我添加一个新类型的语句——变量赋值,我会在 statement 定义中添加一个新的范式(form ):

statement = Return ( exp ) | Assign ( variable, exp )

以下是 return_2.c 的 AST 图:

c语言编译符号常量位置最后,我需要一个正式的语法,它负责定义如何将一系列的标记组合起来形成完整的语言结构。我将在巴科斯范式中定义它:

::= ::= "int" " ( " " ) " "{" "}" ::= "return" ";" ::=

上面的每一行都表示一个运行规则,定义了如何从其他语言结构和标记构建语言结构。运行规则左侧出现的每个符号(例如,,,)都是非终结符号。而单个标记(例如,关键字,标点符等)则是终端符号。需要注意的是,虽然这个语法让我可以构建一个有效的 C 程序,但它并没有告诉我如何将这个程序转换成一个 AST,例如,在 AST 中没有对应于常量节点的运行规则。当重写我的语法来为常量创建一个运行规则后,

我发现语法就变得非常简单,起初,每个非终端符号只需一个运行规则。例如,如果我添加了对变量语句的支持,则可以用以下规则来派生语句:

::= "return" ";" | "int" "=" ";"

为了将令牌列表转换成 AST,我将使用一种称为递归下降解析的技术。我将定义一个函数来解析语法中的每个非终结符号,并返回相应的 AST 节点。解析符号 S 的函数应该从列表的开始处删除标记,直到形成 S. If 的有效派生。如果在解析完成之前,就碰到了一个不在 S 的运行规则中的标记,将会派生失败。如果 S 的规则包含其它非终结符,则应该调用其他函数来解析它们。

以下是解析语句的伪代码:

def parse_statement ( tokens ) : tok = tokens.next ( ) if tok.type != "RETURN_KEYWORD": fail ( ) tok = tokens.next ( ) if tok.type != "INT" fail ( ) exp = parse_exp ( tokens ) //parse_exp will pop off more tokens statement = Return ( exp ) tok = tokens.next ( ) if tok.type != "SEMICOLON": fail ( ) return statement

稍后,运行规则将是递归的(例如,一个算术表达式可以包含其他表达式),这意味着解析函数也将是递归的,因此此方法就顺理地被称为递归下降解析。

编写一个解析函数,从而接受一个标记列表并返回一个基于程序节点的 AST。该函数应该为所有有效的示例构建正确的 AST,并在所有无效的示例中引发错误。

有许多表示 AST 的代码,具体取决于编译器所使用的语言。例如,可以将 AST 节点定义为 OCaml 数据类型:

type exp = Const ( int ) type statement = Return ( exp ) type fun_decl = Fun ( string, statement ) type prog = Prog ( fun_decl )

代码的生成

现在我已经构建了一个 AST,并准备生成一些汇编程序,我只需要发送四行汇编即可。为了发送,我将遍历 AST,这意味着我将按以下顺序访问:

1. 函数名称(不是一个真正的节点,而是函数定义中的第一项);

2. 返回值;

3. 返回语句。

这个遍历可以确保在使用汇编程序之前预定义一个值,例如,我在返回语句中引用之前生成的返回值。

以下就是我需要的汇编程序:

1. 生成一个函数(例如函数 "foo"):

.globl _foo _foo:

2. 生成返回语句(例如,return 3;):

movl $3, %eax ret

可以进行的编写

编写一个接受 AST 并生成汇编程序的生成函数。它可以将程序集作为字符串返回,也可以直接将其写入文件。它应该为所有有效示例生成正确的汇编程序。

输出

此时,你可能需要一个实用程序函数来打印出 AST,以帮助进行调试。你可以现在就编写或者等到你需要的时候再写。下面是使用 nqcc 输出的 return_2.c。

FUN INT main: params: ( ) body: RETURN Int

这个示例包含了 AST 不需要的一些信息,比如返回类型和函数参数列表。

用一种可读的方式写一个预打印的函数,它以 AST 的形式打印出来。

编写一个接受 C 源文件并输出可执行文件的程序,该程序要实现的功能如下:

1. 阅读文件;

2. 词法分析;

3. 解析;

4. 生成汇编程序;

5. 将汇编程序写入文件;

6. 调用 GCC 命令将汇编程序转换为可执行文件:

gcc -m32 assembly.s -o out

在此命令中,"assembly.s" 是汇编程序文件的名称,"out" 是要生成的可执行文件的名称。 -m32 选项会调用 GCC 命令构建一个 32 位的二进制文件。如果需要,可以省略该选项并构建 64 位二进制文件,但稍后需要对代码生成步骤进行一些更改(例如,使用 64 位寄存器)。

7. 删除汇编程序文件。

测试

你可以通过测试你的编译器查看是否正确地使用了测试脚本,正常情况下,它会使用你的编译器编译一组测试程序,然后执行它们,并确保它们返回正确的值。

调用过程如下:

./test_compiler.sh /path/to/your/compiler

要用脚本测试,你的编译器就需要遵循下面这 3 个标准:

1. 可以从命令行调用一个 C 源文件作为参数,例如:./YOUR_COMPILER /path/to/program.c。

2. 当通过 program.c 时,它会在同一个目录下生成可执行程序。

3. 如果解析失败,它不会生成程序集或可执行文件,因为这是测试脚本检查无效测试程序的方法。

要注意的是,虽然该脚本不负责检查编译器是否输出了合理的错误信息,但可以使用无效的测试程序来手动测试。

c语言编译符号常量位置: 可爱卖萌男孩图片,缺失:c语言编译符号常量位置

c语言编译符号常量位置c语言编译符号常量位置

c语言编译符号常量位置c语言编译符号常量位置

c语言编译符号常量位置c语言编译符号常量位置

c语言编译符号常量位置c语言编译符号常量位置

c语言编译符号常量位置c语言编译符号常量位置

c语言编译符号常量位置c语言编译符号常量位置

c语言编译符号常量位置

  • c语言编译符号常量位置:混血可爱女宝宝图片

  • c语言编译符号常量位置:酷小孩可爱图片

  • c语言编译符号常量位置:男女小朋友可爱图片

  • c语言编译符号常量位置:恩典可爱图片多儿秀

  • c语言编译符号常量位置:可爱的阿狸桃子图片

  • 甜甜起司猫可爱图片

  • 韩国可爱小女孩图片

  • c语言编译符号常量位置相关文章