透过源码领悟GCC到底在干些什么(收集整理)
GCC源码分析(一)——介绍与安装目录(?)[-]一GCC的作用和运行机制二GCC的安装 上半年一直在做有关GCC和LD的项目,到现在还没做完。最近几天编程的那台电脑坏了,所以趁此间隙写一点相关的分析和经验之类的跟大家共享。一、GCC的作用和运行机制 GCC是Linux下重要的编
上半年一直在做有关GCC和LD的项目,到现在还没做完。最近几天编程的那台电脑坏了,所以趁此间隙写一点相关的分析和经验之类的跟大家共享。
一、GCC的作用和运行机制
GCC是Linux下重要的编译工具,用法这里就不说了,满大街都找得到。这里我重点介绍GCC的运作机制,作为代码分析的铺垫。全篇使用C语言子部件来作分析,因为我对其他语言的编译没有研究。
根据编译原理,语言的编译分为这么几个步骤:词法分析、语法分析、语义分析、中间语言生成、优化、目标代码生成等。然而从编译器使用的角度来看,要把源代码翻译为可执行文件要经过编译和连接两步,与此对应,一个完整的编译系统一定包含编译器和连接器两大功能部件。编译器要完成编译原理中提到的那些任务;连接器要把编译器生成的代码片段拼接成一个完整的可执行程序。之所以需要连接器,是因为一般的程序都是多源文件的,而编译器一次只编译一个源文件(称之为翻译单元translation unit),因此需要连接器把所有翻译单元对应的输出合并成一个可执行文件。
如果一切顺利,可执行程序就可以正确的生成出来。但是一旦源代码存在某些问题,错误就会被报告出来。编译器报告的错误一般都是局部错误,它会指明错误在哪个文件第几行;连接器报告的错误一般都是全局错误,而且绝大多数都是多胳膊少腿的问题,比如函数重定义,无法解决的外部符号等,这些错误无法定位到某一行。
GCC就是这里的编译器。准确来说,GCC是一个编译驱动器,驱动cc1、as和ld三个部件完成编译、汇编和连接的工作。cc1将C语言源文件编译为汇编文件(.s)。而将汇编代码转换为二进制指令的工作由AS完成,生成大家都很熟悉的对象文件(.o);生成的这些对象文件再由AR程序打包成静态库(.a),或者由LD程序连接成可执行程序(elf、.so或其他格式)。而LD就是所谓的连接器。AS、AR、LD是属于另外一个叫做binutils的软件包的程序,所以要让GCC能够有效运作起来,除了在系统中安装GCC外,还要安装binutils才行。
以下是cc1、as、ld各司其责的配合完成一个编译过程。
- gcc test.c -S -o test.S
- as test.S -o test.o
- ld test.o -o test
通常所用的“gcc -c”就相当于“gcc -S” + as,而对于编译单个源文件一步到位生成可执行“gcc test.c -o test”相当于上面三个步骤的组合,中间文件被放置在临时目录下。从这一点看来,GCC除去编译的功能外,更像是个driver,它可以驱动as和ld完成整个的编译,特别是gcc也接受对象文件(.o)和静态库(.a)作为参数用于生成可执行程序,其实背后就是调用的LD,还可以用“-Wl,”选项给LD传递自定义参数。所以在大多数软件的Makefile里,你很难找到AS和LD的字眼,gcc已经给你包办了。
GCC源代码里包含的主要就是cc1这部分(还包括一些其他的辅助工具,比如collect2等)。
二、GCC的安装
要学习和修改GCC源码,首先第一步是在自己的机器上用GCC源代码编译出一个选定版本的GCC(这里以gcc-4.5.2.tar.bz2为例,源码可以从http://gcc.gnu.org去下载)。除此之外,GCC依赖于gmp、mpfr、mpc三个库,如果你机器上没有,或者版本太老以至于无法支持新的GCC,那么你还得去把这三个库下载下来。
一般来说,下载GCC是从GNU的FTP镜像网站去下载,gcc的代码包一般放置在/release/gcc-x.y目录下,而那三个依赖库一般放置在/infrastructure/目录下。
1、把依赖库和GCC解包
- tar -vjxf gmp-4.3.2.tar.bz2 -C /usr/src/
- tar -vjxf mpfr-2.4.2.tar.bz2 -C /usr/src/
- tar -vxf mpc-0.8.1.tar.gz -C /usr/src/
- tar -vjxf gcc-4.5.2.tar.bz2 -C /usr/src/
2、到自己的home目录下编译依赖库
- cd ~
- mkdir gmp-build
- cd gmp-build
- /usr/src/gmp-4.3.2/configure --prefix=/usr/local/gmp-4.3.2 #指定安装位置
- make
- make check
- make install
-
- cd ~
- mkdir mpfr-build
- cd mpfr-build
- /usr/src/mpfr-2.4.2/configure --prefix=/usr/local/mpfr-2.4.2 --with-gmp=/usr/local/gmp-4.3.2
- make
- make check
- make install
-
-
- cd ~
- mkdir mpc-build
- cd mpfr-build
- /usr/src/mpc-0.8.1/configure --prefix=/usr/local/mpc-0.8.1 --with-mpfr=/usr/local/mpfr-2.4.2 --with-gmp=/usr/local/gmp-4.3.2
- make
- make check
- make install
3、编译GCC
- cd ~
- mkdir gcc-build
- cd gcc-build
- /usr/src/gcc-4.5.2/configure --prefix=/usr/local/gcc-4.5.2 --with-mpc=/usr/local/mpc-0.8.1 --with-mpfr=/usr/local/mpfr-2.4.2 --with-gmp=/usr/local/gmp-4.3.2 --enable-languages=c,c++
- make
- make install
漫长等待过后GCC就被安装到/usr/local/gcc-4.5.2目录下了,然后ln -s /usr/local/gcc-4.5.2/bin/gcc /usr/local/bin/gcc,最后gcc -v看看,版本号是不是换了?
上半年一直在做有关GCC和LD的项目,到现在还没做完。最近几天编程的那台电脑坏了,所以趁此间隙写一点相关的分析和经验之类的跟大家共享。
一、GCC的作用和运行机制
GCC是Linux下重要的编译工具,用法这里就不说了,满大街都找得到。这里我重点介绍GCC的运作机制,作为代码分析的铺垫。全篇使用C语言子部件来作分析,因为我对其他语言的编译没有研究。
根据编译原理,语言的编译分为这么几个步骤:词法分析、语法分析、语义分析、中间语言生成、优化、目标代码生成等。然而从编译器使用的角度来看,要把源代码翻译为可执行文件要经过编译和连接两步,与此对应,一个完整的编译系统一定包含编译器和连接器两大功能部件。编译器要完成编译原理中提到的那些任务;连接器要把编译器生成的代码片段拼接成一个完整的可执行程序。之所以需要连接器,是因为一般的程序都是多源文件的,而编译器一次只编译一个源文件(称之为翻译单元translation unit),因此需要连接器把所有翻译单元对应的输出合并成一个可执行文件。
如果一切顺利,可执行程序就可以正确的生成出来。但是一旦源代码存在某些问题,错误就会被报告出来。编译器报告的错误一般都是局部错误,它会指明错误在哪个文件第几行;连接器报告的错误一般都是全局错误,而且绝大多数都是多胳膊少腿的问题,比如函数重定义,无法解决的外部符号等,这些错误无法定位到某一行。
GCC就是这里的编译器。准确来说,GCC是一个编译驱动器,驱动cc1、as和ld三个部件完成编译、汇编和连接的工作。cc1将C语言源文件编译为汇编文件(.s)。而将汇编代码转换为二进制指令的工作由AS完成,生成大家都很熟悉的对象文件(.o);生成的这些对象文件再由AR程序打包成静态库(.a),或者由LD程序连接成可执行程序(elf、.so或其他格式)。而LD就是所谓的连接器。AS、AR、LD是属于另外一个叫做binutils的软件包的程序,所以要让GCC能够有效运作起来,除了在系统中安装GCC外,还要安装binutils才行。
以下是cc1、as、ld各司其责的配合完成一个编译过程。
- gcc test.c -S -o test.S
- as test.S -o test.o
- ld test.o -o test
通常所用的“gcc -c”就相当于“gcc -S” + as,而对于编译单个源文件一步到位生成可执行“gcc test.c -o test”相当于上面三个步骤的组合,中间文件被放置在临时目录下。从这一点看来,GCC除去编译的功能外,更像是个driver,它可以驱动as和ld完成整个的编译,特别是gcc也接受对象文件(.o)和静态库(.a)作为参数用于生成可执行程序,其实背后就是调用的LD,还可以用“-Wl,”选项给LD传递自定义参数。所以在大多数软件的Makefile里,你很难找到AS和LD的字眼,gcc已经给你包办了。
GCC源代码里包含的主要就是cc1这部分(还包括一些其他的辅助工具,比如collect2等)。
二、GCC的安装
要学习和修改GCC源码,首先第一步是在自己的机器上用GCC源代码编译出一个选定版本的GCC(这里以gcc-4.5.2.tar.bz2为例,源码可以从http://gcc.gnu.org去下载)。除此之外,GCC依赖于gmp、mpfr、mpc三个库,如果你机器上没有,或者版本太老以至于无法支持新的GCC,那么你还得去把这三个库下载下来。
一般来说,下载GCC是从GNU的FTP镜像网站去下载,gcc的代码包一般放置在/release/gcc-x.y目录下,而那三个依赖库一般放置在/infrastructure/目录下。
1、把依赖库和GCC解包
- tar -vjxf gmp-4.3.2.tar.bz2 -C /usr/src/
- tar -vjxf mpfr-2.4.2.tar.bz2 -C /usr/src/
- tar -vxf mpc-0.8.1.tar.gz -C /usr/src/
- tar -vjxf gcc-4.5.2.tar.bz2 -C /usr/src/
2、到自己的home目录下编译依赖库
- cd ~
- mkdir gmp-build
- cd gmp-build
- /usr/src/gmp-4.3.2/configure --prefix=/usr/local/gmp-4.3.2 #指定安装位置
- make
- make check
- make install
- cd ~
- mkdir mpfr-build
- cd mpfr-build
- /usr/src/mpfr-2.4.2/configure --prefix=/usr/local/mpfr-2.4.2 --with-gmp=/usr/local/gmp-4.3.2
- make
- make check
- make install
- cd ~
- mkdir mpc-build
- cd mpfr-build
- /usr/src/mpc-0.8.1/configure --prefix=/usr/local/mpc-0.8.1 --with-mpfr=/usr/local/mpfr-2.4.2 --with-gmp=/usr/local/gmp-4.3.2
- make
- make check
- make install
3、编译GCC
- cd ~
- mkdir gcc-build
- cd gcc-build
- /usr/src/gcc-4.5.2/configure --prefix=/usr/local/gcc-4.5.2 --with-mpc=/usr/local/mpc-0.8.1 --with-mpfr=/usr/local/mpfr-2.4.2 --with-gmp=/usr/local/gmp-4.3.2 --enable-languages=c,c++
- make
- make install
漫长等待过后GCC就被安装到/usr/local/gcc-4.5.2目录下了,然后ln -s /usr/local/gcc-4.5.2/bin/gcc /usr/local/bin/gcc,最后gcc -v看看,版本号是不是换了?
一、源码组织
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文件中。这两个函数开启了接下来的工作:中间层翻译。
一、前言
很忙,很久没更新博客了,继续没写完的gcc分析,争取在传说将要用C++重写的gcc 5出来之前初略分析完。
二、符号表(GENERIC)
GENERIC的节点都定义在gcc/tree.h头文件里。它将GENERIC按类别分为若干类:
- enum tree_code_class {
- tcc_exceptional, /* An exceptional code (fits no category). */
- tcc_constant, /* A constant. */
- /* Order of tcc_type and tcc_declaration is important. */
- tcc_type, /* A type object code. */
- tcc_declaration, /* A declaration (also serving as variable refs). */
- tcc_reference, /* A reference to storage. */
- tcc_comparison, /* A comparison expression. */
- tcc_unary, /* A unary arithmetic expression. */
- tcc_binary, /* A binary arithmetic expression. */
- tcc_statement, /* A statement expression, which have side effects
- but usually no interesting value. */
- tcc_vl_exp, /* A function call or other expression with a
- variable-length operand vector. */
- tcc_expression /* Any other expression. */
- };
这个all-tree.def是编译期自动生成的文件,主要来源于tree.def文件,还包含一些其它语言特定的TREE类型。每个TREE变量代表一个节点。
三、控制流图(Control Flow Graph)
每个函数翻译为GENERIC的语法树之后,会进行gimplification(gimple化,gimple在下节介绍),在这一过程中函数的语法树被翻译为了控制流图的形式。每个函数对应一个控制流图。
- /* For iterating over basic blocks. */
- #define FOR_BB_BETWEEN(BB, FROM, TO, DIR) \
- for (BB = FROM; BB != TO; BB = BB->DIR)
-
- #define FOR_EACH_BB_FN(BB, FN) \
- FOR_BB_BETWEEN (BB, (FN)->cfg->x_entry_block_ptr->next_bb, (FN)->cfg->x_exit_block_ptr, next_bb) // for循环遍历链表。
-
- #define FOR_EACH_BB(BB) FOR_EACH_BB_FN (BB, cfun) // cfun就是current_function_decl,是一个TREE
四、GIMPLE和RTL
RTL的指令在gcc中称之为insn,insn是有语法和语义的,它被gcc的生成工具所识别和处理,并生成对应的.c文件作为gcc的一部分一同编译到gcc的执行文件中。这部分的细节在后序篇幅中再做介绍。
五、总结
一、前言
很忙,很久没更新博客了,继续没写完的gcc分析,争取在传说将要用C++重写的gcc 5出来之前初略分析完。
二、符号表(GENERIC)
GENERIC的节点都定义在gcc/tree.h头文件里。它将GENERIC按类别分为若干类:
- enum tree_code_class {
- tcc_exceptional, /* An exceptional code (fits no category). */
- tcc_constant, /* A constant. */
- /* Order of tcc_type and tcc_declaration is important. */
- tcc_type, /* A type object code. */
- tcc_declaration, /* A declaration (also serving as variable refs). */
- tcc_reference, /* A reference to storage. */
- tcc_comparison, /* A comparison expression. */
- tcc_unary, /* A unary arithmetic expression. */
- tcc_binary, /* A binary arithmetic expression. */
- tcc_statement, /* A statement expression, which have side effects
- but usually no interesting value. */
- tcc_vl_exp, /* A function call or other expression with a
- variable-length operand vector. */
- tcc_expression /* Any other expression. */
- };
这个all-tree.def是编译期自动生成的文件,主要来源于tree.def文件,还包含一些其它语言特定的TREE类型。每个TREE变量代表一个节点。
三、控制流图(Control Flow Graph)
每个函数翻译为GENERIC的语法树之后,会进行gimplification(gimple化,gimple在下节介绍),在这一过程中函数的语法树被翻译为了控制流图的形式。每个函数对应一个控制流图。
- /* For iterating over basic blocks. */
- #define FOR_BB_BETWEEN(BB, FROM, TO, DIR) \
- for (BB = FROM; BB != TO; BB = BB->DIR)
- #define FOR_EACH_BB_FN(BB, FN) \
- FOR_BB_BETWEEN (BB, (FN)->cfg->x_entry_block_ptr->next_bb, (FN)->cfg->x_exit_block_ptr, next_bb) // for循环遍历链表。
- #define FOR_EACH_BB(BB) FOR_EACH_BB_FN (BB, cfun) // cfun就是current_function_decl,是一个TREE
四、GIMPLE和RTL
RTL的指令在gcc中称之为insn,insn是有语法和语义的,它被gcc的生成工具所识别和处理,并生成对应的.c文件作为gcc的一部分一同编译到gcc的执行文件中。这部分的细节在后序篇幅中再做介绍。
五、总结
一、前言
本篇只介绍一下框架,就不具体介绍每个步骤了。
二、Pass框架
上一篇已经讲了gcc的中间语言的表现形式。gcc 对中间语言的每一步处理叫做一个pass。从一个函数的GENERIC树刚被转换为GIMPLE之后,接下来的工作就由一连串的pass来完成。这些pass环环相扣,最终完成整个程序的优化工作,为目标代码生成做最后的准备。
GCC的pass结构定义在gcc/tree-pass.h头文件中:
- /* Optimization pass type. */
- enum opt_pass_type // 四种pass类型对应的枚举
- {
- GIMPLE_PASS,
- RTL_PASS,
- SIMPLE_IPA_PASS,
- IPA_PASS
- };
- /* Describe one pass; this is the common part shared across different pass
- types. */
- struct opt_pass // pass的基本结构
- {
- /* Optimization pass type. */
- enum opt_pass_type type;
- /* Terse name of the pass used as a fragment of the dump file
- name. If the name starts with a star, no dump happens. */
- const char *name; // pass名字
- /* If non-null, this pass and all sub-passes are executed only if
- the function returns true. */
- bool (*gate) (void); // 是否应该执行此pass?
- /* This is the code to run. If null, then there should be sub-passes
- otherwise this pass does nothing. The return value contains
- TODOs to execute in addition to those in TODO_flags_finish. */
- unsigned int (*execute) (void); // 执行此pass!
- /* A list of sub-passes to run, dependent on gate predicate. */
- struct opt_pass *sub; // 子pass。如果此pass被关闭,子pass也被一起关闭。
- /* Next in the list of passes to run, independent of gate predicate. */
- struct opt_pass *next; // 后面的pass。
- /* Static pass number, used as a fragment of the dump file name. */
- int static_pass_number; // 一个唯一的pass号
- /* The timevar id associated with this pass. */
- /* ??? Ideally would be dynamically assigned. */
- timevar_id_t tv_id; // 一个唯一的ID。
- /* Sets of properties input and output from this pass. */
- unsigned int properties_required; // 这些是要被检查的property
- unsigned int properties_provided;
- unsigned int properties_destroyed;
- /* Flags indicating common sets things to do before and after. */
- unsigned int todo_flags_start; // 这些是在执行此pass之前/之后的附加动作
- unsigned int todo_flags_finish;
- };
之后用继承的方式定义了四个pass的子类型:
- struct gimple_opt_pass // gimple pass
- {
- struct opt_pass pass;
- };
- struct rtl_opt_pass // rtl pass
- {
- struct opt_pass pass;
- };
- struct ipa_opt_pass_d // ipa pass
- {
- struct opt_pass pass;
- /* IPA passes can analyze function body and variable initializers
- using this hook and produce summary. */
- void (*generate_summary) (void); // 分析函数体和全局变量的初始化过程
- /* This hook is used to serialize IPA summaries on disk. */
- void (*write_summary) (struct cgraph_node_set_def *); // 将ipa summary写入磁盘
- /* For most ipa passes, the information can only be deserialized in
- one chunk. However, function bodies are read function at a time
- as needed so both calls are necessary. */
- void (*read_summary) (void); // 从磁盘读取ipa summary,下同
- void (*function_read_summary) (struct cgraph_node *);
- /* Hook to convert gimple stmt uids into true gimple statements. The second
- parameter is an array of statements indexed by their uid. */
- void (*stmt_fixup) (struct cgraph_node *, gimple *); // 语句修复
- /* Results of interprocedural propagation of an IPA pass is applied to
- function body via this hook. */
- unsigned int function_transform_todo_flags_start;
- unsigned int (*function_transform) (struct cgraph_node *); // 函数变形
- void (*variable_transform) (struct varpool_node *); // 变量变形
- };
- struct simple_ipa_opt_pass // simple ipa pass
- {
- struct opt_pass pass;
- };
这四种pass,只有ipa pass比较特殊,其他的除了类型不一样之外,其余都一样。当然,在初始化对应结构体时,opt_pass::type必须用对应的枚举来初始化。最后一种ipa pass区别于simple ipa pass,叫做regular ipa pass,在后面进一步介绍。
大部分常用的pass都实现在gcc目录下的某些文件中,这些文件的特点是声明了一个全局的xxx_pass结构体变量,而这些变量在tree-pass.h中用extern声明一遍,并在passes.c中的 init_optimizations() 函数中串在一起。该函数通过使用NEXT_PASS()宏,初始化了5串pass:
- /* The root of the compilation pass tree, once constructed. */
- extern struct opt_pass *all_passes, *all_small_ipa_passes, *all_lowering_passes,
- *all_regular_ipa_passes, *all_lto_gen_passes;
他们被调用的顺序和被初始化的顺序是一致的:all_lowering_passes -> all_small_ipa_passes -> all_regular_ipa_passes -> all_lto_gen_passes -> all_passes。他们所作的事情大致如下:
all_lowering_passes:降级GIMPLE,从GIMPLE生成控制流图,内联形参...
all_small_ipa_passes:内联函数(early inline),内联形参,重建cgraph边,重建函数属性,建立SSA,复写传播(copy propagation),清理...
all_regular_ipa_passes:内联函数(inline),常量判定(pure const),Escape分析,进程间Point-to分析(IPA PTA)...
all_lto_gen_passes:Link time optimization...
all_passes:exception handling,GIMPLE优化(SSA优化,dead call,别名分析,if合并,循环优化等等),GIMPLE->RTL,RTL优化(复写传播,dead call,寄存器合并,条件跳转优化等等)清理...
其中 all_regular_ipa_passes和all_lto_gen_passes都是regular ipa pass,all_small_ipa_passes就是simple ipa pass,all_lowering_passes都是gimple pass,all_passes是由gimple pass和rtl pass组成。
三、三大类Pass
3.1、GIMPLE Pass
所有的Lowering pass和前半部分优化都是gimple pass。gimple pass可以遍历当前函数的全部gimple语句。Lowering pass中的控制流生成之前,gimple pass只能从cfun里遍历全部的gimple,因为此时它们还没有被组织成控制流图的形式,之后的gimple pass就可以使用FOR_EACH_BB这样的宏逐块扫描gimple语句。
3.2、RTL Pass
RTL pass基本上是优化的后半部分。由于RTL有寄存器、字长等GIMPLE没有的底层概念,因此属于底层优化,侧重点也不同。RTL pass也可以使用FOR_EACH_BB对控制流进行遍历,但是用FOR_BB_INSNS对基本块中的insn进行处理,而gimple pass是通过gsi_start_bb来获取gimple语句列表。
3.3、IPA Pass
IPA的全称是Inter-Procedural Analysis。在gcc里它有两层意思:一个是跨函数的分析,一个是全局变量(夹在函数间的变量)的分析。IPA所使用的工具是cgraph(call graph,调用图)。调用图记录了函数之间的调用关系。进程间分析的重点就是函数间的变量传递(参数)和依赖关系(全局变量,调用关系)。
IPA pass在每次执行时也只是针对一个函数,因此它在执行时也可访问 current_function_decl 和 cfun,并通过它们获取对应的cgraph_node,由此可以得到当前函数与cgraph里的其他函数之间的关系。与此同时,gcc将全局变量存放在varpool_nodes里,这也是cgraph的一部分。
四、Pass的执行
Pass被Pass管理器执行。执行每一个pass的代码实现在gcc/passes.c里。
4.1 几个特殊的pass
在gcc/tree-optimize.c中定义了几个特殊的pass,他们的作用如下:
pass_all_optimizations :它是进程内优化pass的第一个pass,也是他们的父pass。它只有gate函数,只做开关之用。
pass_early_local_passes:它是all_small_ipa_passes的后半部分,属于IPA优化部分。它是IPA优化的开关。
pass_all_early_optimizations:它是pass_early_local_passes的一部分,除了做开关,还负责更新cgraph_state。cgraph允许任意时刻添加函数,但是如果在pass的后段添加函数,而这个函数没有被之前的pass处理过,那就有问题,因此cgraph会根据当前的状态来决定是否要对这个函数追加执行之前的pass。
pass_cleanup_cfg,pass_cleanup_cfg_post_optimizing和pass_fixup_cfg:在不同阶段清理控制流图,它是独立的pass,没有gate(默认执行)。
pass_init_datastructures:初始化所有SSA结构,为转换SSA做准备。
4.2 Pass的执行顺序
上一篇大致讲到了Lowering在Optimization之前。在这里,我详细列出他们的调用关系:
cgraph_finalize_compilation_unit()
cgraph_analyze_functions()
cgraph_analyze_function()
gimplify_function_tree() -> gimplification。
cgraph_lower_function() -> lowering
cgraph_optimize()
ipa_passes()
if (!in_lto_p) execute_ipa_pass_list (all_small_ipa_passes); -> small IPA execute
if (!in_lto_p) execute_ipa_summary_passes(all_regular_ipa_passes) -> regular IPA summary
execute_ipa_summary_passes (all_lto_gen_passes); -> lto summary
if (!flag_ltrans) execute_ipa_pass_list (all_regular_ipa_passes); -> regular IPA (include LTO) execute
cgraph_expand_all_functions()
cgraph_expand_all_function()
tree_rest_of_compilation()
execute_all_ipa_transforms() -> regular IPA transform (include LTO) transform
execute_pass_list (all_passes) -> 进程内优化
4.3 普通Pass的执行
普通的pass由gcc/passes.c中的execute_one_pass()函数来负责调用。该函数的代码就不贴了,具体来说,它是这么来调用每个普通pass的:
1. 检查gate:gate_status = (pass->gate == NULL) ? true : pass->gate();
2. plugin复查gate:invoke_plugin_callbacks (PLUGIN_OVERRIDE_GATE, &gate_status);
3. 如果不需要执行,就退出。
4. 通知plugin准备execute:invoke_plugin_callbacks (PLUGIN_PASS_EXECUTION, pass);
5. 执行pass预定的TODO list:execute_todo (pass->todo_flags_start);
6. 检查函数的property是否和pass的相符:do_per_function (verify_curr_properties, (void *)(size_t)pass->properties_required);
7. 执行pass:todo_after = pass->execute ();
8. 执行pass指定的结束TODO list:execute_todo (todo_after | pass->todo_flags_finish);
9. 如果是regular IPA Pass,记录该pass到当前函数的IPA Transform列表中。
还有一些debug用的dumpfile操作就不提了。
4.3 Regular IPA Pass的执行
执行Regular IPA Pass的函数就不再作详细介绍了,他们的执行流程被分散为3轮,每一轮的步骤都差不多,而且比普通pass的执行过程简单。
每个IPA pass都有三次机会来执行。generate_summaries() -> execute() -> function_transform()。前两次机会基本都差不多,都是用来扫描和准备参数,最后一次机会就是对cgraph实施改变。比如pass_ipa_inline,在generate_summaries里面计算所有函数的大小,在execute里面根据大小和其他信息来判定哪些函数可以内联,在transform里面对所有标记为内联的函数进行内联,并更新cgraph。
尽管如此,generate_summaries() 和 execute() 还是有作用上的区别。由于前者先执行,而后者是否执行被gate()控制,并且transform是按照每个函数上挂载的ipa_pass 列表来执行,如果execute不执行的话,该pass也不会被挂载到当前函数上,因此 generate_summaries() 可以用来通过 gate() 控制后两者是否被执行。
五、之后
执行完所有Pass之后,gcc就进入了最后的阶段:目标代码生成。敬请期待下篇。
一、前言
又有好久没写了,的确很忙。前篇介绍了GCC的pass格局,它是GCC中间语言部分的核心架构,也是贯穿整个编译流程的核心。在完成优化处理之后,GCC必须做的最后一步就是生成最后的编译结果,通常情况下就是汇编文件(文本或者二进制并不重要)。
前面也讲到了,GCC中间语言的核心数据结构是GENERIC、GIMPLE和RTL。其中的RTL就是和指令紧密相关的一种结构,它是指令生成的起点。
二、RTL和INSN
2.1 什么是RTL,什么是INSN
RTL叫做寄存器转移语言(Register Transfering Language)。说是寄存器,其实也包含内存操作。RTL被设计成一种函数式语言,由表达式和对象构成。其中对象指的是寄存器、内存和值(常数或者表达式的值),表达式就是对对象和子表达式的操作。这些在gcc internal里面都有介绍。
RTL对象和操作组成RTL表达式,子表达式加上操作组成复合RTL表达式。当一个RTL表达式表示一条中间语言指令时,这个RTL表达式叫做INSN。RTL表达式(RTL Expression)在gcc代码中缩写为RTX,代码中的rtx类型就是指向RTL表达式的指针。所以insn就是rtx,但是rtx不一定是insn。
2.2 INSN的生成
RTL是由gimple生成的,从gimple到RTL的转换叫做“expand”。在整个优化的pass链中,这一步由pass_expand完成。该pass实现在gcc/cfgexpand.c中。它的execute函数gimple_expand_cfg很长,但是核心工作是对每个basic block进行转换:
- FOR_BB_BETWEEN (bb, init_block->next_bb, EXIT_BLOCK_PTR, next_bb)
- bb = expand_gimple_basic_block (bb);
此外,每个expand_xxx函数只负责一部分工作,有些函数有rtx类型的返回值,有些函数没有返回值。那些有返回值的函数通常也不会有变量来保存它们返回的insn。那么就有另外一个问题:那些展开的insn到哪里去了?
为了弄清楚这两个问题,首先要找到生成insn的地方。这是一项工程浩大的体力活,不妨从某个点来研究这个问题,比如就从函数调用的语句来入手吧。我们可以从expand_gimple_basic_block开始顺藤摸瓜,来看看一个GIMPLE_CALL是如何翻译成insn的。
首先,expand_gimple_basic_block里有一个对basic block里的gimple statement的遍历循环,在这个循环里面,首先判断了一些特殊的情况,比如debug之类的,忽略之。直到循环最后一部分才进入正题:
- if (is_gimple_call (stmt) && gimple_call_tail_p (stmt)) // 尾调用,特殊情况,忽略之
- {
- bool can_fallthru;
- new_bb = expand_gimple_tailcall (bb, stmt, &can_fallthru);
- if (new_bb)
- {
- if (can_fallthru)
- bb = new_bb;
- else
- return new_bb;
- }
- }
- else
- {
- def_operand_p def_p;
- def_p = SINGLE_SSA_DEF_OPERAND (stmt, SSA_OP_DEF);
- if (def_p != NULL)
- {
- /* Ignore this stmt if it is in the list of
- replaceable expressions. */
- if (SA.values
- && bitmap_bit_p (SA.values,
- SSA_NAME_VERSION (DEF_FROM_PTR (def_p))))
- continue;
- }
- last = expand_gimple_stmt (stmt); //<strong> </strong>这是真正干活的地方
- maybe_dump_rtl_for_gimple_stmt (stmt, last);
- }
进入到expand_gimple_stmt里面,这个函数不长,一眼可以看出来,核心是expand_gimple_stmt_1 (stmt);,这个函数分情况展开了stmt。其中GIMPLE_CALL对应的是expand_call_stmt。这个函数也不长,关键在最后。
- if (lhs)
- expand_assignment (lhs, exp, false); // lhs = func(args)
- else
- expand_expr_real_1 (exp, const0_rtx, VOIDmode, EXPAND_NORMAL, NULL); // func(args)
gimple call语句形如 lhs = func ( args ); 。其中,lhs是可以没有的。所以如果存在lhs的话,就按赋值语句展开。否则的话就按表达式展开。赋值语句的右边也是表达式,因此按赋值语句展开最终也会将“func(args)”部分按表达式展开。
expand_gimple_expr_1函数很长,因为要处理的表达式类型比较多。其中我们关注的是case CALL_EXPR:分支:
- case CALL_EXPR:
- /* All valid uses of __builtin_va_arg_pack () are removed during
- inlining. */
- if (CALL_EXPR_VA_ARG_PACK (exp))
- error ("%Kinvalid use of %<__builtin_va_arg_pack ()%>", exp);
- {
- tree fndecl = get_callee_fndecl (exp), attr;
- if (fndecl
- && (attr = lookup_attribute ("error",
- DECL_ATTRIBUTES (fndecl))) != NULL)
- error ("%Kcall to %qs declared with attribute error: %s",
- exp, identifier_to_locale (lang_hooks.decl_printable_name (fndecl, 1)),
- TREE_STRING_POINTER (TREE_VALUE (TREE_VALUE (attr))));
- if (fndecl
- && (attr = lookup_attribute ("warning",
- DECL_ATTRIBUTES (fndecl))) != NULL)
- warning_at (tree_nonartificial_location (exp),
- 0, "%Kcall to %qs declared with attribute warning: %s",
- exp, identifier_to_locale (lang_hooks.decl_printable_name (fndecl, 1)),
- TREE_STRING_POINTER (TREE_VALUE (TREE_VALUE (attr))));
- /* Check for a built-in function. */
- if (fndecl && DECL_BUILT_IN (fndecl))
- {
- gcc_assert (DECL_BUILT_IN_CLASS (fndecl) != BUILT_IN_FRONTEND);
- return expand_builtin (exp, target, subtarget, tmode, ignore); // 内置函数
- }
- }
- return expand_call (exp, target, ignore); // 普通函数
内置函数有内置函数的展开方法,这个以后有机会再讲。这里还是分析一下普通函数。前面的那个if 是用来检查的,展开是由expand_call函数来完成。这个函数相当长,因为函数的参数、堆栈等等事务很繁琐。但是至少可以确定的是,一句普通的函数调用绝对不是一个简单的insn能实现的,它应该对应了一串insn,而且至少包括压栈、调用、退栈这三部分。那么这一串insn在哪里?
为了弄清楚这一串insn在代码中的哪个地方,就必须提到start_sequence ()、get_insns()、end_sequence()这三个没有参数的函数。第一个函数开启了一个新的insn sequence,第二个函数获取这个sequence的第一个insn,因为sequence是双链表,所以由第一个insn就可以访问到后面的所有insn。最后一个函数关闭这个sequence,之后就不能再通过emit_xxx往这个sequence里面插入insn了。原因现在还说不清楚,因为这个跟第二个问题相关,就是insn去哪里了?
那么insn到哪里去了?在expand_call这个函数最后就有答案:
- /* If tail call production succeeded, we need to remove REG_EQUIV notes on
- arguments too, as argument area is now clobbered by the call. */
- if (tail_call_insns)
- {
- emit_insn (tail_call_insns); // 尾调用的rtx
- crtl->tail_call_emit = true;
- }
- else
- emit_insn (normal_call_insns); // 正常调用的rtx
- currently_expanding_call--;
- if (stack_usage_map_buf)
- free (stack_usage_map_buf);
- return target;
所谓尾调用就相当于 return tail_call(...);。这个是有专门优化的。但不管怎么优化,最后的insn被发射(emit)了:
- rtx
- emit_insn (rtx x)
- {
- rtx last = last_insn;
- rtx insn;
- if (x == NULL_RTX)
- return last;
- switch (GET_CODE (x))
- {
- // 忽略那些特殊的case
- default:
- last = make_insn_raw (x);
- add_insn (last); // 这里
- break;
- }
- return last;
- }
- void
- add_insn (rtx insn) // 一个标准的双链表插入算法
- {
- PREV_INSN (insn) = last_insn;
- NEXT_INSN (insn) = 0;
- if (NULL != last_insn)
- NEXT_INSN (last_insn) = insn;
- if (NULL == first_insn)
- first_insn = insn;
- last_insn = insn;
- }
其中first_insn和last_insn是宏定义:
- #define first_insn (crtl->emit.x_first_insn)
- #define last_insn (crtl->emit.x_last_insn)
- /* Datastructures maintained for currently processed<strong> function</strong> in RTL form. */
- struct rtl_data x_rtl;
- // 在function.h中定义的宏
- #define crtl (&x_rtl)
原来,生成的insns被插入了当前函数的insn链表中。这个链表包含了当前函数的所有insn,而且是按存储顺序存放的。如果有跳转的话,会有对应的jump insn和label insn。如果把insn就看作是汇编的话,这个链表其实就是“汇编”序列了。
ok,回到前面提到的start_sequence/get_insns/end_sequence这一组函数。由于emit_xxx函数都是向first_insn/last_insn插入,而新的sequence也要借助于emit_xxx来插入,也就是说在start_sequence和end_sequence这两个调用中间,所有的emit_xxx必须向这个sequence发射insn。方法只有一个:那就是让first_insn/last_insn指向当前正在构建的sequence,当这个sequence构建完成之后,再把它还原。(相当笨拙而无奈的设计,因为emit_xxx数量众多,不容得罪)
至此,insn去哪里的问题解决了,但是第一个问题还在:insn如何被构建出来的?继续顺藤摸瓜。在expand_call函数中,有一句特别显眼:
- /* Generate the actual call instruction. */
- emit_call_1 (funexp, exp, fndecl, funtype, unadjusted_args_size,
- adjusted_args_size.constant, struct_value_size,
- next_arg_reg, valreg, old_inhibit_defer_pop, call_fusage,
- flags, & args_so_far);
看不懂代码,看注释也明白了,这不就是生成一个call insn吗?进入看看:
- #if defined (HAVE_call) && defined (HAVE_call_value)
- if (HAVE_call && HAVE_call_value)
- {
- if (valreg)
- emit_call_insn (GEN_CALL_VALUE (valreg,
- gen_rtx_MEM (FUNCTION_MODE, funexp),
- rounded_stack_size_rtx, next_arg_reg,
- NULL_RTX));
- else
- emit_call_insn (GEN_CALL (gen_rtx_MEM (FUNCTION_MODE, funexp),
- rounded_stack_size_rtx, next_arg_reg,
- GEN_INT (struct_value_size)));
- }
- else
- #endif
这只是emit_call_1的一小部分。gen_rtx_MEM就是创建一个内存地址对应的rtx,这里用来获取被调用的函数地址(注意,这里的地址使用符号表示,因为函数到底会被安排在哪里目前还不知道,给它安排个符号,让汇编器和连接器去翻译成真实的地址)。那么这个GEN_CALL是什么?至少在gcc 被 built 之前是不知道的。但是可以告诉你的是,它由一个叫做Machine Description的东西来决定。这里的GEN_CALL调用的是gen_call函数,这个函数定义在insn-emit.c中,而这个文件实在build的时候由Machine Description生成的。在i386平台的Machine Description中,gen_call函数转而去调用ix86_expand_call,因此真正的call insn是由这个函数来完成的。而这个函数又调用了一堆 gen_rtx_XXX来组装insn,这一堆gen_rtx_XXX是从gcc/rtl.def文件自动生成的。
rtl.def 文件是由一串宏组成的,这个宏形如DEF_RTL_EXPR(ENUM, NAME, FORMAT, CLASS)。ENUM是枚举名,gen_rtx_XXX中的XXX部分就是这个枚举名;NAME是识别名,用在其他地方识别rtl;FORMAT是参数格式,代表这个rtx有多少个参数,每个参数是什么类型。比如0代表常数0,e代表表达式等等。CLASS是类型。
在gcc目录下有个叫做gengenrtl.c的文件,他有自己的main函数,所以是一个独立的程序。该程序就是将rtl.def翻译成genrtl.h和genrtl.c两个文件,前者声明了gen_rtx_XXX到gen_rtx_fmt_FFF_stat的对应关系,其中FFF就是宏里面的FORMAT参数,gen_rtx_CALL对应的就是gen_rtx_fmt_ee_stat;后者定义了gen_rtx_fmt_FFF_stat的实现。
- /* Write the declarations for the routine to allocate RTL with FORMAT. */
- static void
- gendecl (const char *format) // <strong>为每个gen_rtx_fmt_FFF_stat创建声明</strong>
- {
- const char *p;
- int i, pos;
- printf ("extern rtx gen_rtx_fmt_%s_stat\t (RTX_CODE, ", format);
- printf ("enum machine_mode mode");
- /* Write each parameter that is needed and start a new line when the line
- would overflow. */
- for (p = format, i = 0, pos = 75; *p != 0; p++)
- if (*p != '0')
- {
- int ourlen = strlen (type_from_format (*p)) + 6 + (i > 9);
- printf (",");
- if (pos + ourlen > 76)
- printf ("\n\t\t\t\t "), pos = 39;
- printf (" %sarg%d", type_from_format (*p), i++);
- pos += ourlen;
- }
- printf (" MEM_STAT_DECL");
- printf (");\n");
- printf ("#define gen_rtx_fmt_%s(c, m", format); // <strong>定义gen_rtx_fmt_FFF 到 gen_rtx_fmt_FFF_stat</strong>
- for (p = format, i = 0; *p != 0; p++)
- if (*p != '0')
- printf (", p%i",i++);
- printf (")\\\n gen_rtx_fmt_%s_stat (c, m", format);
- for (p = format, i = 0; *p != 0; p++)
- if (*p != '0')
- printf (", p%i",i++);
- printf (" MEM_STAT_INFO)\n\n");
- }
- /* Generate macros to generate RTL of code IDX using the functions we
- write. */
- static void
- genmacro (int idx)
- {
- const char *p;
- int i;
- /* We write a macro that defines gen_rtx_RTLCODE to be an equivalent to
- gen_rtx_fmt_FORMAT where FORMAT is the RTX_FORMAT of RTLCODE. */
- if (excluded_rtx (idx))
- /* Don't define a macro for this code. */
- return;
- printf ("#define gen_rtx_%s%s(MODE",
- special_rtx (idx) ? "raw_" : "", defs[idx].enumname); // <strong>定义gen_rtx_ENUM 到 gen_rtx_fmt_FFF</strong>
- for (p = defs[idx].format, i = 0; *p != 0; p++)
- if (*p != '0')
- printf (", ARG%d", i++);
- printf (") \\\n gen_rtx_fmt_%s (%s, (MODE)",
- defs[idx].format, defs[idx].enumname);
- for (p = defs[idx].format, i = 0; *p != 0; p++)
- if (*p != '0')
- printf (", (ARG%d)", i++);
- puts (")");
- }
- /* Generate the code for the function to generate RTL whose
- format is FORMAT. */
- static void
- gendef (const char *format) // <strong>为每个gen_rtx_fmt_FFF_stat创建定义</strong>
- {
- const char *p;
- int i, j;
- /* Start by writing the definition of the function name and the types
- of the arguments. */
- printf ("rtx\ngen_rtx_fmt_%s_stat (RTX_CODE code, enum machine_mode mode", format);
- for (p = format, i = 0; *p != 0; p++) // <strong>遍历format中的字符,每个字符对应一个参数</strong>
- if (*p != '0')
- printf (",\n\t%sarg%d", type_from_format (*p), i++);
- puts (" MEM_STAT_DECL)");
- /* Now write out the body of the function itself, which allocates
- the memory and initializes it. */
- puts ("{");
- puts (" rtx rt;");
- puts (" rt = rtx_alloc_stat (code PASS_MEM_STAT);\n");
- puts (" PUT_MODE (rt, mode);");
- for (p = format, i = j = 0; *p ; ++p, ++i) // <strong>每个参数对应一个insn成员赋值语句。</strong>
- if (*p != '0')
- printf (" %s (rt, %d) = arg%d;\n", accessor_from_format (*p), i, j++);
- else
- printf (" X0EXP (rt, %d) = NULL_RTX;\n", i);
- puts ("\n return rt;\n}\n");
- }
2.3 Basic Block中的insn
前面提到过,basic block中有两套指令系统:gimple和RTL。那么basic block中的RTL是从哪里来的呢?还是回到expand_gimple_basic_block函数:
- if (stmt || elt)
- {
- last = get_last_insn ();
- // 此处省略若干字
- /* Java emits line number notes in the top of labels.
- ??? Make this go away once line number notes are obsoleted. */
- BB_HEAD (bb) = NEXT_INSN (last);
- if (NOTE_P (BB_HEAD (bb)))
- BB_HEAD (bb) = NEXT_INSN (BB_HEAD (bb)); // <strong>看这里</strong>
- note = emit_note_after (NOTE_INSN_BASIC_BLOCK, BB_HEAD (bb));
- maybe_dump_rtl_for_gimple_stmt (stmt, last);
- }
- else
- note = BB_HEAD (bb) = emit_note (NOTE_INSN_BASIC_BLOCK); // <strong>或者这里</strong>
- // 此处省略1000字
- last = get_last_insn ();
- if (BARRIER_P (last))
- last = PREV_INSN (last);
- if (JUMP_TABLE_DATA_P (last))
- last = PREV_INSN (PREV_INSN (last));
- BB_END (bb) = last; // <strong>还有这里</strong>
对应的,在函数体中间也有对BB_HEAD(bb)的赋值,是设置basic block的insn序列的起始。BB_HEAD 排除了基本块开头的LABEL,BB_END排除了基本块最后的跳转表。所以每个基本块的insn序列就是函数insn序列的子序列。不同基本块的insn序列不会相交,甚至可能不会连着,因为中间还隔着LABEL和跳转表。
pass_expand之后的pass基本上都是RTL Pass了。这些pass要么通过get_first_insn()/get_last_insn()来遍历整个函数的insn列表(包含Label和跳转),要么用FOREACH_BB、BB_HEAD、BB_END来遍历每个基本块内部的insn(不包含Label和跳转)。
三、Machine Description
针对每个CPU平台,gcc有对应的Machine Description用指导指令生成。这些代码放在gcc/config/<平台名称>的目录下,比如intel平台的在gcc/config/i386/。一个Machine Description文件是对应平台的核心,比如gcc/config/i386/i386.md文件。
一个md文件中可以定义很多东西,比如constant、attr、insn、expand等等。constant是给一个名字起一个编号,其他地方如果要用到这个编号,可以用名字代替。比如i386.md中每个寄存器有一个编号;attr是目标平台的属性,比如有些什么扩展指令集、有些什么功能、或者被禁用了那些功能等等;insn和expand是md文件的主体,用来定义insn,不同的是前者的输出是asm,用于指令生成;后者的输出是insn sequence;用于GIMPLE转RTL。
每个insn和expand有这么几个要素:名字、RTL模板、条件、输出模板。名字是insn的识别名,比如rtl.def中CALL的识别名是call,所以对应的insn就是md文件里的define_expand call;RTL模板是RTX的规格,它有两个作用:1.判断是否匹配某个insn,2.指出每个操作数的属性(大小、使用情况,前置后置条件);条件被用来检查该insn的前置条件,如果不符合,那就有问题;输出模板是该insn的汇编输出格式,用于最后的指令发射。
要注意的是md文件定义的是insn pattern,具体的insn是由expand_xxx、emit_xxx、gen_rtx_xxx、gen_xxx那一堆函数生成的。所以md文件里的insn只有两个作用:1.检查insn;2.输出asm
那么md文件是如何融入到gcc中的呢?还是靠build!和前面讲的rtl.def生成genrtl.h、genrtl.c类似,md文件被一系列工具翻译成不同作用的代码:
- [root@localhost gcc]# ls insn-*.h
- insn-attr.h insn-codes.h insn-config.h insn-constants.h insn-flags.h insn-modes.h
- [root@localhost gcc]# ls insn-*.c
- insn-attrtab.c insn-emit.c insn-modes.c insn-output.c insn-preds.c
- insn-automata.c insn-extract.c insn-opinit.c insn-peep.c insn-recog.c
- (define_expand "call"
- [(call (match_operand:QI 0 "" "")
- (match_operand 1 "" ""))
- (use (match_operand 2 "" ""))]
- ""
- {
- ix86_expand_call (NULL, operands[0], operands[1], operands[2], NULL, 0);
- DONE;
- })
这个call insn要求第一个操作数是一个整数(QI),第二个和第三个参数自便,但是第三个参数是程序要使用的。从expand_call可以看出,第一个操作数是调用函数的地址,第二个操作数是参数堆栈大小,第三个操作数是参数列表(所有参数都在这第三个操作数里)。这个expand被用于gimple_call到insn的转换。
这条md定义被genemit工具转换成了一个叫做gen_call的函数,函数体中除了准备参数之外,最核心的就是调用ix86_expand_call。这是转换之后的结果:
- /* /usr/src/develop/gcc-4.5.2/gcc/config/i386/i386.md:13574 */
- rtx
- gen_call (rtx operand0,
- rtx operand1,
- rtx operand2)
- {
- rtx _val = 0;
- start_sequence ();
- {
- rtx operands[3];
- operands[0] = operand0;
- operands[1] = operand1;
- operands[2] = operand2;
- #line 13579 "/usr/src/develop/gcc-4.5.2/gcc/config/i386/i386.md"
- {
- ix86_expand_call (NULL, operands[0], operands[1], operands[2], NULL, 0); // expand 的输出代码会出现在gen_xxx函数中
- DONE;
- }
- operand0 = operands[0];
- operand1 = operands[1];
- operand2 = operands[2];
- }
- emit_call_insn (gen_rtx_CALL (VOIDmode,
- operand0,
- operand1));
- emit_insn (gen_rtx_USE (VOIDmode,
- operand2));
- _val = get_insns ();
- end_sequence ();
- return _val;
- }
这是一个expand,用来生成insn,所以没有对应的output。再看一个insn的例子:
- (define_insn "x86_fnstsw_1"
- [(set (match_operand:HI 0 "register_operand" "=a")
- (unspec:HI [(reg:CCFP FPSR_REG)] UNSPEC_FNSTSW))]
- "TARGET_80387" // 只能在允许80387指令情况下使用
- "fnstsw\t%0" // asm指令模板
- [(set (attr "length") (symbol_ref "ix86_attr_length_address_default (insn) + 2"))
- (set_attr "mode" "SI")
- (set_attr "unit" "i387")])
转换成gen_xxx之后变成:
- /* /usr/src/develop/gcc-4.5.2/gcc/config/i386/i386.md:1361 */
- rtx
- gen_x86_fnstsw_1 (rtx operand0 ATTRIBUTE_UNUSED)
- {
- return gen_rtx_SET (VOIDmode,
- operand0,
- gen_rtx_UNSPEC (HImode,
- gen_rtvec (1,
- gen_rtx_REG (CCFPmode,
- 18)),
- 31));
- }
asm模板不会出现在gen_xxx中,因为这个函数pass_expand是用来构建insn的。asm模板会转换到insn-output.c中:
- // struct insn_data 的初始化。
- /* /usr/src/develop/gcc-4.5.2/gcc/config/i386/i386.md:1361 */
- {
- "x86_fnstsw_1",
- #if HAVE_DESIGNATED_INITIALIZERS
- { .single = // 单一的指令对应single,如果是多行指令,会生成对应的output函数,这里就是 .function = { output_nnn }
- #else
- {
- #endif
- "fnstsw\t%0", // ASM输出模板
- #if HAVE_DESIGNATED_INITIALIZERS
- },
- #else
- 0,
- 0
- },
- #endif
- (insn_gen_fn) gen_x86_fnstsw_1,
- &operand_data[24],
- 1,
- 0,
- 1,
- 1
- }
四、指令生成
在优化的pass序列的最后,有一个叫做pass_final的RTL Pass,这个pass负责将RTL翻译为ASM。它的execute函数最核心的三行:
- final_start_function (get_insns (), asm_out_file, optimize);
- final (get_insns (), asm_out_file, optimize);
- final_end_function ();
第一行输出函数的头,包括函数的汇编说明、stack frame的建立。第二行输出指令序列;第三行结束函数,包括stack frame的销毁、结束说明等。
final函数遍历整个函数的insn序列,调用final_scan_insn输出每一个insn。这个函数太长,要处理note、debug、frame等等乱七八糟的东西。但是中间最关键的一段是调用Machine Description来输出ASM:
- insn_code_number = recog_memoized (insn); // 找insn code number,就是insn的编号
- cleanup_subreg_operands (insn);
- // 此处省略若干行
- /* Find the proper template for this insn. */
- templ = get_insn_template (insn_code_number, insn); // 获取define_insn的ASM输出模板
- /* If the C code returns 0, it means that it is a jump insn
- which follows a deleted test insn, and that test insn
- needs to be reinserted. */
- if (templ == 0)
- {
- rtx prev;
- // 继续省略若干行
- return prev;
- }
- /* If the template is the string "#", it means that this insn must
- be split. */
- if (templ[0] == '#' && templ[1] == '\0')
- {
- rtx new_rtx = try_split (body, insn, 0); // 去调用define_split
- //又省略若干行
- return new_rtx;
- }
- // 无关紧要的还是省略吧
- /* Output assembler code from the template. */
- output_asm_insn (templ, recog_data.operand); // 按照模板输出asm
指令生成的最关键一步是这段代码的第一个工作:识别insn。这一个工作很令人费解:既然insn是由md来生成的,那么生成的时候就应该知道这个insn该由md里面的哪一条定义提供asm输出,为什么还要识别呢?因为有的insn并不是全靠RTL来生成。就比如上面说的call,虽然他提供了expand的方法,但是真实的工作是由定义在gcc/config/i386/i386.c文件中的ix86_expand_call函数来完成。这个函数手工生成了一系列insn来完成函数调用的工作,那么这些insn如何来输出?
所以gcc提供了genrecog生成recog函数来完成insn的识别。识别的方法就是将md文件中的所有RTL表达式当作模式串集合,看真实的insn复合哪一个RTL表达式,那么这个insn就有对应的定义输出。recog函数返回对应insn的编号,然后按这个编号去找md的定义,并找到asm输出模板,于是有了上面这段输出代码。
recog函数的核心就是一棵硬编码的决策树。genrecog首先会扫描全部的md定义,抽取所有的RTL模式串,分解为一串predicates,然后将这些predicates插入到决策树中。recog函数就是一边输入未知insn的predicates,一边从树根开始做决策(其实就是跳转),直到遇到树叶完成决策。
在此之后的两个pass只是清理一下数据结构。由此整个pass链调用完毕,gcc完成了从GENERIC到GIMPLE,再到RTL,最后到ASM的转换。
五、总结
这个系列对gcc从输入到输出的流程进行了粗略的分析。一个编译器最核心的是优化部分。具体的优化步骤在本系列中没有提到,因为太多、太繁琐、也太理论。以后可以考虑把教科书中提到的优化挑出来分析一下,但最近是没有时间了,就此告一段落。
原内容来自http://blog.csdn.net/sonicling/article/details/6706152
更多推荐
所有评论(0)