您的位置:首页 > Web前端

GCC源码分析(二)——前端

2014-07-31 22:06 127 查看
原文链接: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词法分析器的核心结构。代码很长,我只贴一点片段。

[cpp] view
plaincopy

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;

//...还远远没完

}

  这个switch是_cpp_token_direct函数的核心。它通过当前符号(switch(c))来判断未来有哪些可能,如果只有一种可能,就调用对应的处理函数把接下来的字符都处理掉,例如前三组case。如果有多种可能,那么就预读下一个字符(*buffer->cur)来确定到底是那种可能。我粗略扫了一下这个switch,貌似没有预读下下个字符。因此这是一个典型的LL(2)分析法,它通过读取最多头两个字符来决定后面的若干个字符是个怎样类型的组合,从而调用这种类型的记号生成函数来生成记号,并把已经读取的位置记录在cpp_reader类型的参数里,以供下一次继续分析。

  有人可能会觉得奇怪,LL(2)不是语法分析算法吗,怎么用来进行词法分析?我们知道,《编译原理》里的语法分析指的是上下文无关文法的分析法,而上下文无关文法属于II型文法,自然兼容III型文法。II型文法和III型文法的分析器区别就是前者的分析器带堆栈,后者的不带,所以前者更加强大,支持递归。但是看完_cpp_token_direct函数,我们没有发现它使用了堆栈,那是因为用正则语言本来就不需要堆栈,如果真的要用LL来分析,只需要一个栈顶空间,因此在手工实现的时候,这个栈的实现就免了,直接用已经读出的那个字符c和即将要读出的*buffer->cur的空间就行了。

  手工实现的最大好处是不拘于理论的条条框框,可随意发挥。这种随意性可能也是缺点,那就是代码看起来乱糟糟的。但是对于GCC的词法分析来讲,却是必须的。举一个最简单的例子就明白了:

[html] view
plaincopy

template<typename T> T construct(size_t size);

vector<int> v = construct<vector<int>>(10);

  看到这,大家肯定就明白了,这是C++最经典的词法bug,就是那个“>>”,如果中间没有空格,它会被整体当作算术右移符号,从而产生莫名其妙的错误:“error: '>>' should be '> >' within a nested template argument list”。这个bug在DFA上是很难消除的,因为一个状态机只要情况允许,总是尽可能的进行状态迁移,这本身就是一种贪婪思想。如果总是将右移符号看作两个反括号,那么上面这个例子就可以通过了,但是如果遇到
a > > 10(中间带空格),那也会被识别为右移符号,尽管写代码的人可能也是这么想的,但是这违反了C++的标准!

  手工实现的词法分析器可以避免这个问题,就是加入一个判断标志来判断是否在识别一个模板参数,如果是则分离,否则就作为右移符号。在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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: