原文链接:http://blog.csdn.net/sonicling/article/details/6706152
从这一篇开始,我们将从源代码的角度来分析GCC如何完成对C语言源文件的处理。GCC的内部构架在GCC Internals(搜“gccint.pdf”,或者见[1])里已经讲述得很详细了,但是如果你只看了gccint就来看代码,还是觉得一头雾水,无法下手,因为你很难把gccint所讲的概念同gcc代码里真实的数据结构联系起来。那么这也是我把我这半年的分析经理写下来的原因,大家可以参照gccint来看。那么gccint中已经详细讲过的概念,在这里就一笔带过,这里只研究GCC的源码。
一、源码组织
GCC的源代码文件非常多,总数大约有好几万。但是很多都是testsuite和lib。首先我们除去所有的testsuite目录,然后lib打头的目录也可以基本上不看,那是各程序语言的gcc版标准库和专为某种语言的编译而设计的库。我们只分析C语言的话,只用看其中的libcpp,它包含了C/C++的词法分析和预处理。剩下的GCC源代码大多集中在config、gcc两个目录下。
config目录是Makefile为各跨平台编译准备的配置目录。
gcc目录下除去gcc/config目录外的其他文件是各个语言的编译器前端源文件,一般放在各自语言命名的目录下,例如cp(C++)、java、fortran等。唯一例外的是C语言,它的前端源文件同GCC的通用文件(包括中间表示、中间优化等)一起,散放在gcc目录下。
gcc/config目录是gcc在各种硬件或操作系统平台下的后端源文件,负责把GCC生成的中间表示转换为各平台相关的机器码、字节码或其他目标语言。
那我们可以从gcc的源代码组织上大致看出gcc之所以能支持众多前端和后端的原因,它将各种语言的源文件按照各自的方法分析完之后,表示为由GENERIC、GIMPLE、RTL组成的统一的中间结构,再由各种后端将统一的结构转换为各自平台对应的目标语言。
二、词法分析
词法分析,通俗讲,就是给源文件断词。我们将源文件看作一个字符流,并交由词法分析器进行断词,词法分析器必须能够输出一个一个的词,也叫做记号(token),每个记号至少有三个属性:
1.值:即断出的那一段字符串
2.类型:关键字、标识符、文字常量、符号等
3.位置:这个记号在当前文件的第几行,用于报错。
在《编译原理》里面,词法分析是和NFA、DFA、正则表达式联系起来的,他们属于III型语言。根据词法定义,我们手头已经有很多工具可以实现词法分析器的自动构造,这些自动构造的代码无一例外的使用了DFA的概念,即构造出来的词法分析器一定是一个DFA,里面包含了初始状态、终结状态和状态的转移,而这些状态都是自动构造中抽象出来的符号或者数字,一般人很难看出这些状态在词法定义中的位置。所以这也是自动构造的缺点——贪图构造的方便,一定带来修改的成本。
而GCC的词法分析是手工构造的,实现在libcpp/lex.c文件中,其中最重要的那个函数是_cpp_lex_direct,他反应了GCC词法分析器的核心结构。代码很长,我只贴一点片段。
- switch (c)
- {
- case ' ': case '\t': case '\f': case '\v': case '\0':
- result->flags |= PREV_WHITE;
- skip_whitespace (pfile, c);
- goto skipped_white;
- case '\n':
- if (buffer->cur < buffer->rlimit)
- CPP_INCREMENT_LINE (pfile, 0);
- buffer->need_line = true;
- goto fresh_line;
- case '0': case '1': case '2': case '3': case '4':
- case '5': case '6': case '7': case '8': case '9':
- {
- struct normalize_state nst = INITIAL_NORMALIZE_STATE;
- result->type = CPP_NUMBER;
- lex_number (pfile, &result->val.str, &nst);
- warn_about_normalization (pfile, result, &nst);
- break;
- }
- case 'L':
- case 'u':
- case 'U':
- case 'R':
- /* 'L', 'u', 'U', 'u8' or 'R' may introduce wide characters,
- wide strings or raw strings. */
- if (c == 'L' || CPP_OPTION (pfile, uliterals))
- {
- if ((*buffer->cur == '\'' && c != 'R')
- || *buffer->cur == '"'
- || (*buffer->cur == 'R'
- && c != 'R'
- && buffer->cur[1] == '"'
- && CPP_OPTION (pfile, uliterals))
- || (*buffer->cur == '8'
- && c == 'u'
- && (buffer->cur[1] == '"'
- || (buffer->cur[1] == 'R' && buffer->cur[2] == '"'))))
- {
- lex_string (pfile, result, buffer->cur - 1);
- break;
- }
- }
- /* Fall through. */
- case '_':
- case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
- case 'g': case 'h': case 'i': case 'j': case 'k': case 'l':
- case 'm': case 'n': case 'o': case 'p': case 'q': case 'r':
- case 's': case 't': case 'v': case 'w': case 'x':
- case 'y': case 'z':
- case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
- case 'G': case 'H': case 'I': case 'J': case 'K':
- case 'M': case 'N': case 'O': case 'P': case 'Q':
- case 'S': case 'T': case 'V': case 'W': case 'X':
- case 'Y': case 'Z':
- result->type = CPP_NAME;
- {
- struct normalize_state nst = INITIAL_NORMALIZE_STATE;
- result->val.node.node = lex_identifier (pfile, buffer->cur - 1, false,
- &nst);
- warn_about_normalization (pfile, result, &nst);
- }
- /* Convert named operators to their proper types. */
- if (result->val.node.node->flags & NODE_OPERATOR)
- {
- result->flags |= NAMED_OP;
- result->type = (enum cpp_ttype) result->val.node.node->directive_index;
- }
- break;
- case '\'':
- case '"':
- lex_string (pfile, result, buffer->cur - 1);
- break;
- case '/':
- /* A potential block or line comment. */
- comment_start = buffer->cur;
- c = *buffer->cur;
- if (c == '*')
- {
- if (_cpp_skip_block_comment (pfile))
- cpp_error (pfile, CPP_DL_ERROR, "unterminated comment");
- }
- else if (c == '/' && (CPP_OPTION (pfile, cplusplus_comments)
- || cpp_in_system_header (pfile)))
- {
- /* Warn about comments only if pedantically GNUC89, and not
- in system headers. */
- if (CPP_OPTION (pfile, lang) == CLK_GNUC89 && CPP_PEDANTIC (pfile)
- && ! buffer->warned_cplusplus_comments)
- {
- cpp_error (pfile, CPP_DL_PEDWARN,
- "C++ style comments are not allowed in ISO C90");
- cpp_error (pfile, CPP_DL_PEDWARN,
- "(this will be reported only once per input file)");
- buffer->warned_cplusplus_comments = 1;
- }
- if (skip_line_comment (pfile) && CPP_OPTION (pfile, warn_comments))
- cpp_error (pfile, CPP_DL_WARNING, "multi-line comment");
- }
- else if (c == '=')
- {
- buffer->cur++;
- result->type = CPP_DIV_EQ;
- break;
- }
- else
- {
- result->type = CPP_DIV;
- break;
- }
- if (!pfile->state.save_comments)
- {
- result->flags |= PREV_WHITE;
- goto update_tokens_line;
- }
- /* Save the comment as a token in its own right. */
- save_comment (pfile, result, comment_start, c);
- break;
- case '<':
- if (pfile->state.angled_headers)
- {
- lex_string (pfile, result, buffer->cur - 1);
- if (result->type != CPP_LESS)
- break;
- }
- result->type = CPP_LESS;
- if (*buffer->cur == '=')
- buffer->cur++, result->type = CPP_LESS_EQ;
- else if (*buffer->cur == '<')
- {
- buffer->cur++;
- IF_NEXT_IS ('=', CPP_LSHIFT_EQ, CPP_LSHIFT);
- }
- else if (CPP_OPTION (pfile, digraphs))
- {
- if (*buffer->cur == ':')
- {
- buffer->cur++;
- result->flags |= DIGRAPH;
- result->type = CPP_OPEN_SQUARE;
- }
- else if (*buffer->cur == '%')
- {
- buffer->cur++;
- result->flags |= DIGRAPH;
- result->type = CPP_OPEN_BRACE;
- }
- }
- break;
- //...还远远没完
- }
有人可能会觉得奇怪,LL(2)不是语法分析算法吗,怎么用来进行词法分析?我们知道,《编译原理》里的语法分析指的是上下文无关文法的分析法,而上下文无关文法属于II型文法,自然兼容III型文法。II型文法和III型文法的分析器区别就是前者的分析器带堆栈,后者的不带,所以前者更加强大,支持递归。但是看完_cpp_token_direct函数,我们没有发现它使用了堆栈,那是因为用正则语言本来就不需要堆栈,如果真的要用LL来分析,只需要一个栈顶空间,因此在手工实现的时候,这个栈的实现就免了,直接用已经读出的那个字符c和即将要读出的*buffer->cur的空间就行了。
手工实现的最大好处是不拘于理论的条条框框,可随意发挥。这种随意性可能也是缺点,那就是代码看起来乱糟糟的。但是对于GCC的词法分析来讲,却是必须的。举一个最简单的例子就明白了:
- template<typename T> T construct(size_t size);
- vector<int> v = construct<vector<int>>(10);
手工实现的词法分析器可以避免这个问题,就是加入一个判断标志来判断是否在识别一个模板参数,如果是则分离,否则就作为右移符号。在gcc-4.5.2中我没有看到类似的处理,因此这个版本的gcc还没有修正这个bug。但是手工构造的词法分析器使得解决这个bug成为可能。而加入这个标志位则表示,该词法不在是一个III型文法,而是一个I型文法,即上下文相关文法,理论上来说应该比上下文无关文法更加复杂,但是如果就事论事的话,也不那么复杂。这就是理论与实践的差距。
C语言的词法分析还包含对源文件的预处理(preprocessing),也就是处理宏的定义与替换。_cpp_lex_direct是被_cpp_lex_token函数调用的,而_cpp_lex_token的作用除了调用词法分析之外,还负责执行宏定义。宏的分类处理是在libcpp/directives.c中实现的,实际的处理动作是在libcpp/macro.c中实现的,cpp_get_token也是在这里定义的,这就是词法分析器对外的接口了。外界得到的token就是已经预处理过的,因此预处理过程对语法分析来说是透明的。
三、语法分析
C语言的语法分析器实现在gcc/c-parser.c中。这个文件的函数很多,但是如果仔细研读过C语言标准的话,这些函数就非常好理解,因为里面用到的名词和标准都是对应的,并且注释里面有大量从标准中摘抄的文法定义片段。
首先在这个文件的前面有几个token相关的函数,那是对词法分析器的一个包装,加入了一个token缓存,把所有peek过一次的token加入到缓存中,下次get或者peek就从缓存中读。缓存的作用一个是加速,另一个是弥补词法分析器无法支持unget的缺陷。
该文件的起始函数是实现在文件末尾的void c_parse_file(void)。它调用了c_parser_translation_unit(),然后按照文法定义一直递归调用下去。因此这是一个典型的递归下降分析法。
递归下降分析法的优点是手工实现非常容易,代码直观简单,可以和文法定义一一对应上来,缺点是能力有限。而自底向上的分析法(SLR、LR、LALR)刚好相反,能力强大,但是代码非常不直观,他们也是把文法定义分为一个一个的状态,用状态迁移来表示分析过程,而这些状态用肉眼看真的是相当吃力。但是就C++来说,真的要用LR来分析,也不一定是件简单事。那么GCC是如何克服分析法的能力限制的呢?
道理其实和词法分析一样,那就是加上下文标志位,并给每个已经的语法结构进行类型分析。前一个措施把上下文无关文法放到上下文相关分析中,逻辑自然简化一些;后一个措施就是解决标准定义的冲突问题。例如同样是identifier,可以归约为type-name、declarator-id、id-expression等等,如果这个identifier前面已经分析过,那么自然可以根据前面的分析结果进行判断;如果这个identifier第一次见到,那么就得根据上下文来判断这个identifier最有可能是个什么东西。
所以还是那句话,理论和实践是有差距的。通过这两项措施,使得递归下降的分析能力不弱于一般的LR分析法。
C语言的语法分析从c_parser_translation_unit开始,往下调用c_parser_declaration_or_fndef。这是一个关键函数,因为我们知道,C语言源文件里,在文件层次上只有两个类对象需要处理:non-function-declaration和function-declaration。在C语言里,函数声明也应该算是一种声明,但是它很特殊,因为它包含有对编译器来说最重要的东西:计算流程。而其他的声明只用作类型检查。
在c_parser_declaration_or_fndef函数里有两个分支,一个处理非函数声明,最后总是调用到了finish_decl函数,而另一个分支用来处理函数声明,最后总是调用到了finish_function函数,这两个函数都实现在c-decl.c文件中。这两个函数开启了接下来的工作:中间层翻译。
[1] GNU Compiler Collection Internals