* 从C代码到GIMPLE
上一部分我们分析了gcc中的“树”,这些树是源程序在编译器内部的表示形式。本部分
试图来分析gcc是如何将源代码变成这种内部表示的。本文以C语言的翻译过程为例。
首先,我需要找到cc1的控制流的入口点。在gcc目录下grep一下main,发现好多,但是
,根据文件名来看,main.c中的那个最值得怀疑。这个文件的第一行注释为
%{
/* main.c: defines main() for cc1, cc1plus, etc.
{%
可见,这个文件的确定义了cc1的入口控制流,从字面上看,它也定义了其它编译器的入口
控制流。目前推测,大部分编译器都使用同一份主代码生成,这份代码与不同的编译器前端
链接,生成不同的编译器二进制文件。问题是,这种二进制文件包含编译器的多少部分?其
实,每个编译器比较特殊的只有相关于源语言的部分,只要翻译到中间表示,后面的分析和
优化都是一样的了。所以,道理上讲,不会在每个编译器中都包含所有的分析和优化,以及
代码生成的部分,这些部分只要作为动态链接库,为多个编译器共享即可。
从后面的代码可以看到,这里的main函数什么也没有做,只是以相同的参数调用了
toplev_main。注释解释说,这样设计是为了支持每一个前端都可以自己定义main函数,如
果真的需要的话。
下面,我们继续看toplev_main。和它有关的两个文件是toplev.[hc]。先看看.h文件。该文
件基本上就是声明了toplev.c文件中的函数。另外,还定义了一些宏。现在,轮到toplev.c
文件了。toplev.c文件是cc1和c++的最高层的控制流。它会分析命令行参数,打开文件,调
用编译器的各种遍,并且可以记录每一遍使用的时间。错误消息和malloc的低级接口也是在
这里处理的。
在toplev.c文件中,定义了大量的用于控制编译的全局变量:
%{
/* 非0表示在分析的过程中输出调试信息(-dy) */
static int set_yydebug;
/* true表示本次编译不需要后端 */
static bool no_backend;
/* 打印各种选项的值时,每一行的宽度 */
#define MAX_LINE 75
/* 调用的程序的名字,不包括路径名,例如cc1 */
const char *progname;
/* toplev_main的参数向量的副本 */
static const char **save_argv;
/* 顶层源程序(输入给cpp的,例如hello.c)的文件名,一般来自于实际输入(hello.i)
* 开始处的#-命令
* 如果没有这个命令的话,就是输入文件的名字(hello.i) */
const char *main_input_filename;
/* 真正的源文件中的当前位置,其中有两个成员,分别记录文件名和行号 */
location_t input_location;
struct line_maps *line_table;
/* 保存文件信息的栈。虽然输入只有一个文件.i,但是,这个文件是经过预处理器处理过
* 的,其中事实上包含着多个文件,这些文件在#-命令中说明,他们之间是include关系
* 所以用栈来保存,该栈用一个单向链表实现,每个链表元素是一个location_t */
struct file_stack *input_file_stack;
/* input_file_stack每次改变,都增加 */
int input_file_stack_tick;
/* 记录在每一个tick时的input_file_stack */
typedef struct file_stack *fs_p;
DEF_VEC_P(fs_p);
DEF_VEC_ALLOC_P(fs_p,heap);
static VEC(fs_p,heap) *input_file_stack_history;
/* input_file_stack
* 是否被回复成之前出现过的一个状态,Note:注释中说,这意味着之后不会再发生
* pushing,但是,仍然可能push呀? */
static bool input_file_stack_restored;
/* dump输出文件的基文件名 */
const char *dump_base_name;
/* auxiliary输出文件的基文件名 */
const char *aux_base_name;
/* target_flags的掩码,表示命令行参数对这个标志的设置 */
int target_flags_explicit;
/* 调试用到的函数的钩子,依赖于命令行参数 */
const struct gcc_debug_hooks *debug_hooks;
/* 上面的东西的默认值 */
static const struct gcc_debug_hooks *default_debug_hooks;
/* 指定调试输出的类型 */
int rtl_dump_and_exit;
int flag_print_asm_name;
enum graph_dump_types graph_dump_format;
/* -o指定的输出文件名 */
const char *asm_file_name;
/* 非0表示做优化-O.
不同的值代表不同的优化级别,例如-O2对应2。但是,只有典型的优化才被这个值控制
* 其他的优化需要使用他们自己的标志指定 */
int optimize = 0;
/* 非0表示为目标代码的大小做优化-Os.
* 只有0和非0两种情况。
* 非0的时候,optimize的值是2,并且,其中一些可能导致代码膨胀的优化被禁止了*/
int optimize_size = 0;
/* FUNCTION_DECL节点,表示当前正在编译的函数,0表示目前编译在函数之间 */
tree current_function_decl;
/* 设置为当前函数的FUNC_BEGIN标号,NULL,如果没有当前函数。 */
const char * current_function_func_begin_label;
/* 用于临时禁止某些警告信息。当代码是某个系统头文件的代码时,非0*/
int in_system_header = 0;
/* 非0表示收集详细的关于编译的统计信息 */
int flag_detailed_statistics = 0;
/* 随机的字符串,可以被用户覆盖定义 */
static const char *flag_random_seed;
/* 用于记录编译时间的局部时间戳。如果系统不能提供时间,则为0。如果用户指定了一个
* random seed,那么它为-1u*/
unsigned local_tick;
/* -f 标志 */
/* 非0表示'char'类型是有符号的 */
int flag_signed_char;
/* 非0表示仅仅给枚举类型分配它需要的字节数。
* 值为2表示它还没有被初始化*/
int flag_short_enums;
/* 非0表示struct和union应该在内存中返回
* 这个标志会产生较慢的代码,只有必要的时候才设置 */
#ifndef DEFAULT_PCC_STRUCT_RETURN
#define DEFAULT_PCC_STRUCT_RETURN 1
#endif
int flag_pcc_struct_return = DEFAULT_PCC_STRUCT_RETURN;
/* 表示如何处理复数 */
int flag_complex_method = 1;
/* 非0表示-fno-inline,我们本质上就不要inline */
int flag_really_no_inline = 2;
/* 非0表示我们需要把所有的声明保存在一个.X文件中 */
int flag_gen_aux_info = 0;
/* 指定上面那个文件的名字 */
const char *aux_info_file_name;
/* 非0表示我们在为一个共享库编译代码,否则是为一个可执行文件编译 */
int flag_shlib;
/* 为GNU还是NeXT Objective-C运行时环境生成代码 */
#ifdef NEXT_OBJC_RUNTIME
int flag_next_runtime = 1;
#else
int flag_next_runtime = 0;
#endif
/* 设置默认的tls模型 */
enum tls_model flag_tls_default = TLS_MODEL_GLOBAL_DYNAMIC;
/* 非0表示把某些警告变成错误 */
int flag_pedantic_errors = 0;
/* -dA 选项导致在输出的汇编文件中,以注释的方式输出调试信息 */
int flag_debug_asm = 0;
/* -dP 选项导致在汇编文件中,以注释的方式输出rtl */
int flag_dump_rtl_in_asm = 0;
/* 非空的时候表示,栈指针不能超过这个地址。也就是说,如果栈是向下增长的,栈指针
* 必须始终大于等于它,否则,栈指针应该始终小于等于它。当前rtx可能是REG或
* SYMBOL_REF,需要后端支持 */
rtx stack_limit_rtx;
/* 非0表示我们应该跟踪变量。当flag_var_tracking ==
* AUTODETECT_VALUE它会被process_options ()自动设置 */
int flag_var_tracking = AUTODETECT_VALUE;
/* 为真表示用户为当前函数增加了'section'属性 */
bool user_defined_section_attribute = false;
/* -falign-*选项的值: 表示标号怎么对齐。
* 0 :`use default', 1 :`don't align'. */
int align_loops_log;
int align_loops_max_skip;
int align_jumps_log;
int align_jumps_max_skip;
int align_labels_log;
int align_labels_max_skip;
int align_functions_log;
/*该结构体用于表示选项,而且是语言无关的*/
typedef struct
{
const char *const string;
int *const variable;
const int on_value;
}
lang_independent_options;
/* 非0表示表达式必须从左到右计算 */
int flag_evaluation_order = 0;
/* 用户标号的前缀。Note: 什么时候使用? */
const char *user_label_prefix;
/* 本部分定义了GCC运行时的一些参数,例如这些参数可以控制某些优化进行的程度
* 可以指定当一个函数中的指令数大于某个值时,不能进行inline,这些参数使用
* --param 命令行参数指定*/
static const param_info lang_independent_params[] = {
#define DEFPARAM(ENUM, OPTION, HELP, DEFAULT, MIN, MAX) \
{ OPTION, DEFAULT, false, MIN, MAX, HELP },
#include "params.def"
#undef DEFPARAM
{ NULL, 0, false, 0, 0, NULL }
};
/* 输出文件 */
FILE *asm_out_file;
FILE *aux_info_file;
FILE *dump_file = NULL;
const char *dump_file_name;
/* 编译过程中的当前工作目录。*/
static const char *src_pwd;
}%
好多的全局变量呀。而且,大部分还不是文件内部的。
好吧,下面我们就来看看在main.c中被调用的toplev_main。这个函数正式cc1,cc1plus等
编译器的入口点。如果在编译的过程中出现错误,其返回FATAL_EXIT_CODE,否则返回
SUCCESS_EXIT_CODE。这个函数在一次编译过程中只能被调用一次,多次调用可能导致错误
,这个函数的控制流异常简单。
%{
int
toplev_main (unsigned int argc, const char **argv)
{
save_argv = argv;
/* 初始化GCC的运行环境和诊断信息的管理 */
general_init (argv[0]);
/* 根据命令行初始化选项并做简单的处理,基本上就是将未指定的选项设为默认值 */
decode_options (argc, argv);
/*初始化local_tick变量,用于计算时间*/
init_local_tick ();
/* 如果exit_after_options为真,处理完选项后退出,并不进行编译 */
if (!exit_after_options)
do_compile (); /* 真正干活的函数 */
/* 如果出错,返回错误值。Note:errorcount 和 sorrycount有什么区别?*/
if (errorcount || sorrycount)
return (FATAL_EXIT_CODE);
return (SUCCESS_EXIT_CODE);
}
}%
由于本章的目的不是详细地分析每一个GCC的过程,而是看看C代码是怎么被变成GIMPLE的,
所以,越过初始化的部分,我们直接看干活的函数do_compile。这个函数会初始化编译器,
并编译当前的输入文件。这里的初始化应该是对toplev_main中的初始化的补充。
%{
static void
do_compile (void)
{
/* 初始化计时器。所有计时器的类型和用途定义在timeval.def文件中 */
if (time_report || !quiet_flag || flag_detailed_statistics)
timevar_init ();
timevar_start (TV_TOTAL);
/* 处理已经分析过的选项在这个函数里面出现了lang_hooks.post_options调用。这个
* 函数的确在处理选项,只是它不再分析命令行,分析的过程在toplev_main中已经做
* 了。这里是进一步的处理和一些一致性检查。 */
process_options ();
/* 如果已经发生了错误,不必继续做 */
if (!errorcount)
{
/* 该函数必须运行,对于使用非默认浮点格式的目标,计算浮点的预定于宏 */
init_adjust_machine_modes ();
/* 如果需要后端的话,初始化它。该函数只在这里调用一次 */
if (!no_backend)
backend_init ();
/* 语言相关的初始化,成功时返回非0 */
if (lang_dependent_init (main_input_filename))
compile_file (); /*真正用于编译一个文件的函数*/
/*清理函数,完成关闭打开的文件之类的任务*/
finalize ();
}
/* 停止计时,打印时间信息 */
timevar_stop (TV_TOTAL);
timevar_print (stderr);
}
}%
下面,我们来看函数compile_file。该函数完成一个翻译单元(通常是一个.i文件)的编译。
输出汇编语言的文件以及一些调试用的文件。
%{
static void
compile_file (void)
{
/* 初始化其他遍,init_cgraph只是设置了cgraph的dump文件 */
init_cgraph ();
/* 初始化final遍的数据,final遍会将RTL转换成汇编并输出它*/
init_final (main_input_filename);
/* 初始化关于coverage文件的东西,Note: 这是什么文件?*/
coverage_init (aux_base_name);
/* 分析的计时,这个说明分析过程开始了*/
timevar_push (TV_PARSE);
/* 调用分析器,它分析整个文件,并为每个函数调用rest_of_compilation*/
lang_hooks.parse_file (set_yydebug);
/* 在丢失'}'的情况下,使我们回到全局的作用域 */
lang_hooks.clear_binding_stack ();
/* 编译基本结束,还剩下一些将符号表中剩下的部分输出的工作Note:具体是什么? */
timevar_pop (TV_PARSE);
/* 如果该标志非0,不要输出汇编文件,返回即可*/
if (flag_syntax_only)
return;
/* 在编译结束的时候,将所有的全局定义(包括函数)输出到汇编文件中
* 在unit-at-a-time的情况下,所有的分析优化都在这里做*/
lang_hooks.decls.final_write_globals ();
if (errorcount || sorrycount)
return;
/*输出所有在队列中等待输出的变量定义到汇编文件*/
varpool_assemble_pending_decls ();
finish_aliases_2 ();
/* 处理coverage文件的清理工作,关闭图文件等 */
coverage_finish ();
/* 同上 */
if (flag_mudflap)
mudflap_finish_file ();
/* 同上 */
if (!targetm.have_tls)
emutls_finish ();
/* 输出共享常量池的内容*/
output_shared_constant_pool ();
/* 输出所有object_block的定义*/
output_object_blocks ();
/* 输出weak symbol声明 */
weak_finish ();
/* 处理调试符号 */
timevar_push (TV_SYMOUT);
#if defined DWARF2_DEBUGGING_INFO || defined DWARF2_UNWIND_INFO
if (dwarf2out_do_frame ())
dwarf2out_frame_finish ();
#endif
(*debug_hooks->finish) (main_input_filename);
timevar_pop (TV_SYMOUT);
/* 必要时在文件的结束输出一些其他信息 */
dw2_output_indirect_constants ();
/* 处理剩下的汇编语言的外部的指示符(如果有的话) */
process_pending_assemble_externals ();
/* 增加一个.ident指示符,表示生成这份代码的gcc的版本 */
#ifdef IDENT_ASM_OP
if (!flag_no_ident)
{
const char *pkg_version = "(GNU) ";
if (strcmp ("(GCC) ", pkgversion_string))
pkg_version = pkgversion_string;
fprintf (asm_out_file, "%s\"GCC: %s%s\"\n",
IDENT_ASM_OP, pkg_version, version_string);
}
#endif
/* 这个必须放在最后,有些目标要求在汇编文件的最后有一些特殊的指示符,所以,
* GCC 不能在这之后再向汇编文件输出任何东西 */
targetm.asm_out.file_end ();
}
}%
从这个函数的前几行就可以看出来,lang_hooks中的parse_file函数完成了大部分的编译工
作。另外,在其他的地方也多次出现这个lang_hooks变量,所有的出现都在调用其中的函数
。我们可以猜测,这个结构体就是GCC主体和编译器前端的接口。编译器的前端,通过定义
这个结构体中的函数来向GCC注册自己,GCC的主体代码通过这个接口中的函数来处理和语言
相关的东西。另外,parse_file还调用了GCC主体代码中的其它部分,来完成分析和优化。
这个部分很可能是rest_of_compileation那个函数。
好的,目标明确,下面,我们就看看这个langhook接口。在gcc目录下发现三个文件,这三
个文件的名字都包含langhooks,分别为langhools.[ch],langhooks-def.h。
首先,看一下langhooks.h文件。在这个文件中,看到了struct lang_hooks结构的定义。
根据它的注释,我们可以知道,前面的猜测是正确的,和语言相关的所有的回调函数“钩
子函数”都是在这个结构中定义的。每一个前端都应该定义这样类型的一个全局变量,该变
量的名字是lang_hooks。在文件langhooks-def.h和langhooks.c中,给出了这个变量的默认
定义。除了这个变量,每一个语言的前端还应该提供一个函数“add_builtin_function”。
至于每一个钩子应该给一个完成什么功能的函数,在langhooks.h文件中的注释中进行了详
细的说明。
仔细看一下langhooks-def.h,我们发现,在该文件中,对应于每一个lang_hooks的每一个
最底层的成员,都有一个宏与之对应,这些宏目前初始化定义为一个默认的函数(
在langhooks.c中定义)。例如,name成员的默认定义是:
%{
#define LANG_HOOKS_NAME "GNU unknown"
}%
最后,该文件利用这些宏定义了一个LANG_HOOKS_INITIALIZER宏,它是一个结构体struct
lang_hooks的初始化器,初始化的值是上面那些宏的值。这里,又用到了一个小的编程技巧
。如果某个前端不用默认的钩子函数,只需要自己重新定义那个宏就好了。比如,C语言要
这样
%{
#undef LANG_HOOKS_NAME
#define LANG_HOOKS_NAME "GNU C"
#undef LANG_HOOKS_INIT
}%
之后,就可以使用LANG_HOOKS_INITIALIZER来初始化自己的lang_hooks了,没有被重新定义
的,仍然使用默认的函数。
好,弄清楚了lang_hooks这个接口,下面要继续的就是找到C语言对应的这个接口中,
parse_file成员对应的是什么函数了。
首先,在找到了对应于C语言前端的lang_hooks是在文件c-lang.c中定义的。
%{
/* Each front end provides its own lang hook initializer. */
const struct lang_hooks lang_hooks = LANG_HOOKS_INITIALIZER;
}%
这个文件就是用来定义C语言的前端的钩子的。
在文件的一开始,定义了这个前端接受的C语言的变种是C90,C94或C99标准。然后,定义了
这个钩子结构的名字GNU C以及一个钩子函数。我们要找的parse_file并不在这个文件里面
。在这个文件中还定义了C的前端用到的树节点的一些信息(type, length, name),这些数
组中既包括语言无关的,也包括语言相关的。
我们看一下这个文件include进来的文件,很容易看出来,“c-objc-common.h”文件最为可
疑。果然,这个文件中重新定义了大量的宏。这些函数的定义为c语言和objc语言共享的,
而在c-lang.c文件中定义的那两个宏是c语言自己的。我们看到,LANG_HOOKS_PARSE_FILE定
义为函数c_common_parse_file,这个函数定义在c-opts.c文件中。
=====================
经过跟踪,终于把C语言的预处理部分弄明白了。首先,在上面所说的“多个文件”,一般情
况下只是一个文件。这个文件是一个.c文件,还没有经过预处理。完成预处理部分的代码是
在上层目录的libcpp目录下。同时,libcpp也提供了一个词法分析器,在gcc目录下,只要
调用这个词法分析器就行了,预处理的过程对于编译器而言是透明的。下面,以文件
c-parse.c为核心,分析C语言的分析器。这个文件可是有8000多行呀...
%{
/* 该结构用于保存保留字/关键字表 */
struct resword
{
const char *word;
ENUM_BITFIELD(rid) rid : 16; /* 这个域的所有可能值定义在c-common.h中*/
unsigned int disable : 16;
};
/* if (reswords[i].disable & mask) 为真,则该关键字不起作用。有这个东西,主要是
* 因为该文件中的parse用于分析好几种c的方言*/
#define D_C89 0x01 /* not in C89 */
#define D_EXT 0x02 /* GCC extension */
#define D_EXT89 0x04 /* GCC extension incorporated in C99 */
#define D_OBJC 0x08 /* Objective C only */
/* 这个数组中保存了所有可能的关键字 */
static const struct resword reswords[] =
}%
之后,有一个初始化分析器的函数。这个函数是c_parse_init,其主要作用就是初始化数组
ridpointers,里面存放了所有关键字对应的树节点(identifer节点)。这个初始化被
c-init-decl-processing调用,后者被c_objc_common_init调用。
c的词法分析器需要cpplib中的词法分析器,c-lex.c以及c的语法分析器配合完成词法分析
。对于c的而言,语法分析器的结构中存储了关于词法分析器的信息,而没有为词法分析器
单独定义一个结构。
%{
/* 由于cpp_ttype中没有定义关键字类型,为了表示这种类型,我们定义它*/
#define CPP_KEYWORD ((enum cpp_ttype) (N_TTYPES + 1))
}%
下面的代码把标志符进一步分类了
%{
/* 所有的标志符都是CPP_NAME类型的token */
typedef enum c_id_kind {
/* 普通的标志符 */
C_ID_ID,
/* typedef定义的类型名 */
C_ID_TYPENAME,
/* Objective-C中的类名 */
C_ID_CLASSNAME,
/* 不是标志符 */
C_ID_NONE
} c_id_kind;
}%
关于token的定义,用于保存一个token,这些token经过字符常量的合并以及预处理
%{
typedef struct c_token GTY (())
{
/* token的类型,这个类型是cpp定义的类型 */
ENUM_BITFIELD (cpp_ttype) type : 8;
/* 如果这个token是CPP_NAME,该域表示其类型,否则,它的值为C_ID_NONE */
ENUM_BITFIELD (c_id_kind) id_kind : 8;
/* 如果该token是一个关键字,该域表示哪一个关键字,否则为RID_MAX. */
ENUM_BITFIELD (rid) keyword : 8;
/* 如果是一个CPP_PRAGMA, 该域表示了PRAGMA的类型,否则为PRAGMA_NONE. */
ENUM_BITFIELD (pragma_kind) pragma_kind : 7;
/* 为真表示这个token来自某个系统头文件 */
BOOL_BITFIELD in_system_header : 1;
/* 和这个token相关的值,如果有的话 */
tree value;
/* 这个token的位置 */
location_t location;
} c_token;
}%
parser的结构保存了关于分析状态和上下文的信息,还有可能的两个lookahead,对于c语言
不需要更多的lookahead。
%{
typedef struct c_parser GTY(())
{
/* look-ahead tokens. */
c_token tokens[2];
/* 当前有多少个lookahead符号 (0, 1 or 2). */
short tokens_avail;
/* 真表示正在从一个语法错误中恢复。
c_parser_error会设置这个标志,当消耗了足够多的符号后,应该清除这个位 */
BOOL_BITFIELD error : 1;
/* 为真表示我们正在处理一个pragma, 不应该自动消耗CPP_PRAGMA_EOL. */
BOOL_BITFIELD in_pragma : 1;
/* 真表示我们正在处理if块的最外层的块 */
BOOL_BITFIELD in_if_block : 1;
/* True if we want to lex an untranslated string.Note:这是什么? */
BOOL_BITFIELD lex_untranslated_string : 1;
/* Objective-C 使用的信息 */
BOOL_BITFIELD objc_pq_context : 1;
BOOL_BITFIELD objc_need_raw_identifier : 1;
} c_parser;
}%
该文件中定义了一个c_parser类型的静态指针变量the_parser,这个就是真正使用的分析器
了。
下面这个函数会分析一个完整的c的源文件。
%{
void
c_parse_file (void)
{
/* 使用局部内存 */
c_parser tparser;
memset (&tparser, 0, sizeof tparser);
the_parser = &tparser;
/* 如果是#pragma GCC pch_preprocess,那么会加载一个pch文件 */
if (c_parser_peek_token (&tparser)->pragma_kind == PRAGMA_GCC_PCH_PREPROCESS)
c_parser_pragma_pch_preprocess (&tparser);
the_parser = GGC_NEW (c_parser);
*the_parser = tparser;
c_parser_translation_unit (the_parser);
the_parser = NULL;
}
/* 返回指向parser中下一个token的指针,必要的话读入它 */
static inline c_token *
c_parser_peek_token (c_parser *parser)
{
if (parser->tokens_avail == 0)
{
c_lex_one_token (parser, &parser->tokens[0]);
parser->tokens_avail = 1;
}
return &parser->tokens[0];
}
}%
在这个函数中,仅仅处理第一个token是pragma并且需要加载PCH文件的情况,它会调用函数
加载这个文件。其他的翻译过程都在c_parser_translation_unit函数中。同时,我们也解
释了一下c_parser_peek_token函数,它并不消耗任何token。
c_parser_translation_unit函数分析一个翻译单元,其实就是一个文件。
首先看看transalation_unit的语法。
%{
translation-unit:
external-declarations
external-declarations:
external-declaration
external-declarations external-declaration
GNU extensions:
translation-unit:
empty
}%
可以看出,一个翻译单元其实就是一些外部声明构成的序列。用于分析它的函数是:
%{
/*如果下一个token有指定的类型,返回真*/
static inline bool
c_parser_next_token_is (c_parser *parser, enum cpp_ttype type)
{
return c_parser_peek_token (parser)->type == type;
}
static void
c_parser_translation_unit (c_parser *parser)
{
/* CPP_EOF代表文件结束*/
if (c_parser_next_token_is (parser, CPP_EOF))
{
if (pedantic)
pedwarn ("%HISO C forbids an empty source file",
&c_parser_peek_token (parser)->location);
}
else
{
void *obstack_position = obstack_alloc (&parser_obstack, 0);
/*这个循环完成分析工作,每次分析一个外部声明,直到文件结束*/
do
{
ggc_collect ();
c_parser_external_declaration (parser);
obstack_free (&parser_obstack, obstack_position);
}
while (c_parser_next_token_is_not (parser, CPP_EOF));
}
}
}%
其中,obstack是一个用宏实现的栈,用于存储管理,而parser_obstack是用于保存和分析
相关的对象的栈。c_parser_external_declaration函数用于分析一个外部声明。现在可以
推测,这个分析器是递归下降分析器。呵呵,其实当发现这个分析器是手写的时候,就可以
这样推测了,只是当时还觉得像RM这样的家伙没准真的发疯写一个LR的出来。下面是外部声
明的语法
%{
external-declaration:
function-definition
declaration
}%
可见,外部声明只有两种情况,一种是函数定义,另外一种是变量声明。
当然,这里面没有包含其他的扩展情况。
%{
static void
c_parser_external_declaration (c_parser *parser)
{
int ext;
/* 根据下一个符号的类型来区别对待*/
switch (c_parser_peek_token (parser)->type)
{
case CPP_KEYWORD:
/*关键字的话,再根据不同的关键字区别对待*/
switch (c_parser_peek_token (parser)->keyword)
{
case RID_EXTENSION:
/* c的_extension_关键字,首先取消某些警告,消耗掉这个关键字,其后应该
* 是一个正常的外部声明,分析完后,回复原来的警告标志*/
ext = disable_extension_diagnostics ();
c_parser_consume_token (parser);
c_parser_external_declaration (parser);
restore_extension_diagnostics (ext);
break;
case RID_ASM:
/* 分析asm定义, 分析的结果是一个树,这个树被加入到callgraph中*/
c_parser_asm_definition (parser);
break;
/*下面几个,都是对object-c的处理*/
case RID_AT_INTERFACE:
case RID_AT_IMPLEMENTATION:
gcc_assert (c_dialect_objc ());
c_parser_objc_class_definition (parser);
break;
case RID_AT_CLASS:
gcc_assert (c_dialect_objc ());
c_parser_objc_class_declaration (parser);
break;
case RID_AT_ALIAS:
gcc_assert (c_dialect_objc ());
c_parser_objc_alias_declaration (parser);
break;
case RID_AT_PROTOCOL:
gcc_assert (c_dialect_objc ());
c_parser_objc_protocol_definition (parser);
break;
case RID_AT_END:
gcc_assert (c_dialect_objc ());
c_parser_consume_token (parser);
objc_finish_implementation ();
break;
default:
goto decl_or_fndef;
}
break;
case CPP_SEMICOLON:
if (pedantic)
pedwarn ("%HISO C does not allow extra %<;%> outside of a function",
&c_parser_peek_token (parser)->location);
c_parser_consume_token (parser);
break;
case CPP_PRAGMA:
c_parser_pragma (parser, pragma_external);
break;
case CPP_PLUS:
case CPP_MINUS:
if (c_dialect_objc ())
{
c_parser_objc_method_definition (parser);
break;
}
/* 上面处理的事特殊情况,所有一般的情况,包括声明和函数定义,都在这里处理
* 在进一步分析之前,我们并不能判断是变量声明还是函数定义 */
/* 只有分析了第一个declarator之后才能分别出来是什么声明*/
default:
decl_or_fndef:
/*这个是主要干活的函数*/
c_parser_declaration_or_fndef (parser, true, true, false, true);
break;
}
}
}%
先看一下声明和函数的语法
%{
declaration:
declaration-specifiers init-declarator-list[opt] ;
function-definition:
declaration-specifiers[opt] declarator declaration-list[opt]
compound-statement
declaration-list:
declaration
declaration-list declaration
init-declarator-list:
init-declarator
init-declarator-list , init-declarator
init-declarator:
declarator simple-asm-expr[opt] attributes[opt]
declarator simple-asm-expr[opt] attributes[opt] = initializer
}%
其中,simple-asm-expr,attributes都是gnu的扩展。除此之外,就是我们最熟悉的c的语法
了。
这个函数比较长。
%{
static void
c_parser_declaration_or_fndef (c_parser *parser, bool fndef_ok, bool empty_ok,
bool nested, bool start_attr_ok)
{
/* 这个结构体代表了C语言中的一个声明序列,定义在c-tree.h中,它可以表示一个声明
* 的类型以及所有的修饰符 */
struct c_declspecs *specs;
tree prefix_attrs;
tree all_prefix_attrs;
bool diagnosed_no_specs = false;
location_t here = c_parser_peek_token (parser)->location;
/* 返回一个c_declspecs,对应于一个空的声明列表*/
specs = build_null_declspecs ();
/* 这个函数用于分析一个声明序列,它尽量读更多的属于声明序列的东西,并将所有的
* 信息记录在specs中*/
c_parser_declspecs (parser, specs, true, true, start_attr_ok);
/* 出错处理,暂时不用理会 */
if (parser->error)
{
c_parser_skip_to_end_of_block_or_statement (parser);
return;
}
if (nested && !specs->declspecs_seen_p)
{
c_parser_error (parser, "expected declaration specifiers");
c_parser_skip_to_end_of_block_or_statement (parser);
return;
}
/* 该函数计算specs中的type字段,将signed或是long等关键字于int等类型结合起来*/
finish_declspecs (specs);
/* 处理空的声明 */
if (c_parser_next_token_is (parser, CPP_SEMICOLON))
{
if (empty_ok)
shadow_tag (specs);
else
{
shadow_tag_warned (specs, 1);
pedwarn ("%Hempty declaration", &here);
}
c_parser_consume_token (parser);
return;
}
/* 如果前面的结构体、联合体、枚举有错误,在此打印错
* 误消息。这个必须放在空声明处理的后面 */
pending_xref_error ();
prefix_attrs = specs->attrs;
all_prefix_attrs = prefix_attrs;
specs->attrs = NULL_TREE;
/* 这个循环处理剩下的所有的东西,即被声明的东西。*/
while (true)
{
/*用于记录一个被声明的实体,可以是标识符、函数等*/
struct c_declarator *declarator;
bool dummy = false;
tree fnbody;
/* 分析一个或多个变量声明,或者分析一个函数定义 */
declarator = c_parser_declarator (parser, specs->type_seen_p,
C_DTR_NORMAL, &dummy);
if (declarator == NULL)
{
c_parser_skip_to_end_of_block_or_statement (parser);
return;
}
if (c_parser_next_token_is (parser, CPP_EQ)
|| c_parser_next_token_is (parser, CPP_COMMA)
|| c_parser_next_token_is (parser, CPP_SEMICOLON)
|| c_parser_next_token_is_keyword (parser, RID_ASM)
|| c_parser_next_token_is_keyword (parser, RID_ATTRIBUTE))
{
/* 这是一个数据定义,而不是函数定义 */
tree asm_name = NULL_TREE;
tree postfix_attrs = NULL_TREE;
if (!diagnosed_no_specs && !specs->declspecs_seen_p)
{
diagnosed_no_specs = true;
pedwarn ("%Hdata definition has no type or storage class",
&here);
}
fndef_ok = false;
/* 声明后有一个asm属性,分析一下它 */
if (c_parser_next_token_is_keyword (parser, RID_ASM))
asm_name = c_parser_simple_asm_expr (parser);
/* 声明后有attribute属性说明,分析 */
if (c_parser_next_token_is_keyword (parser, RID_ATTRIBUTE))
postfix_attrs = c_parser_attributes (parser);
if (c_parser_next_token_is (parser, CPP_EQ))
{
/* 表示有初始化表达式 */
tree d;
struct c_expr init;
c_parser_consume_token (parser);
/* 分析初始化器时,被定义的变量是有效的 */
/* 该函数分析一个declarator,当类型信息和变量名都已经分析完的时候
* 调用(此时,如果有初始化器,还没有分析),这个函数中会创建_DECL
* 树节点,填入其类型,并把它加入当前上下文的声明列表中,返回值也
* 是这个节点。*/
d = start_decl (declarator, specs, true,
chainon (postfix_attrs, all_prefix_attrs));
if (!d)
d = error_mark_node;
/* 分析初始化器 */
start_init (d, asm_name, global_bindings_p ());
init = c_parser_initializer (parser);
finish_init ();
if (d != error_mark_node)
{
maybe_warn_string_init (TREE_TYPE (d), init);
/* 最终完成一个声明节点*/
finish_decl (d, init.value, asm_name);
}
}
else
{
/*没有初始化器*/
tree d = start_decl (declarator, specs, false,
chainon (postfix_attrs,
all_prefix_attrs));
if (d)
finish_decl (d, NULL_TREE, asm_name);
}
if (c_parser_next_token_is (parser, CPP_COMMA))
{
/*逗号,还有下一个声明,但是,在此先把一个属性分析了*/
c_parser_consume_token (parser);
if (c_parser_next_token_is_keyword (parser, RID_ATTRIBUTE))
all_prefix_attrs = chainon (c_parser_attributes (parser),
prefix_attrs);
else
all_prefix_attrs = prefix_attrs;
continue;
}
else if (c_parser_next_token_is (parser, CPP_SEMICOLON))
{
/*一个声明结束了*/
c_parser_consume_token (parser);
return;
}
else
{
/*出错*/
c_parser_error (parser, "expected %<,%> or %<;%>");
c_parser_skip_to_end_of_block_or_statement (parser);
return;
}
}
else if (!fndef_ok)
{
/* 不接受函数定义 */
c_parser_error (parser, "expected %<=%>, %<,%>, %<;%>, "
"%
or %<__attribute__%>");
c_parser_skip_to_end_of_block_or_statement (parser);
return;
}
/* 函数定义 */
if (nested)
{
/* 嵌套函数定义,标准c中是不允许的 */
if (pedantic)
pedwarn ("%HISO C forbids nested functions", &here);
push_function_context ();
}
/* start_function函数为一个函数定义创建FUNCTION_DECL节点,
* 该函数为其对应的函数体创建一个绑定上下文,同时会设置
* current_function_decl全局变量*/
if (!start_function (specs, declarator, all_prefix_attrs))
{
/* This can appear in many cases looking nothing like a
function definition, so we don't give a more specific
error suggesting there was one. */
c_parser_error (parser, "expected %<=%>, %<,%>, %<;%>, % "
"or %<__attribute__%>");
if (nested)
pop_function_context ();
break;
}
/* 处理老式的参数声明 */
while (c_parser_next_token_is_not (parser, CPP_EOF)
&& c_parser_next_token_is_not (parser, CPP_OPEN_BRACE))
c_parser_declaration_or_fndef (parser, false, false, true, false);
/* 设置位置 */
DECL_SOURCE_LOCATION (current_function_decl)
= c_parser_peek_token (parser)->location;
/* 将当前函数的参数声明存储到函数声明中去 */
store_parm_decls ();
/* 分析一个复合语句, fnbody 是一个BIND表达式 */
fnbody = c_parser_compound_statement (parser);
if (nested)
{
tree decl = current_function_decl;
add_stmt (fnbody);
finish_function ();
pop_function_context ();
add_stmt (build_stmt (DECL_EXPR, decl));
}
else
{
/* 将函数加入到当前的语句列表中 */
add_stmt (fnbody);
/* 完成一个函数的编译,它会把该函数编译成汇编语言输出,释放函数定义所
* 占用的空间,在分析完函数体后调用*/
finish_function ();
}
break;
}
}
}%
事实上,在调用finish_function函数之前,所有的分析都是生成语言相关的GENERIC树的。
剩下的所有的事情,包括所有的优化,都在finish_function中完成。根据这个函数的注释
当它结束的时候,已经把当前函数的编译成了汇编代码,并释放了所有用于编译当前函数
的存储空间。问题是,如果真是这样,那么过程间的分析和优化是如何进行的?
总之,我们先来看看这个函数把。
%{
void
finish_function (void)
{
tree fndecl = current_function_decl;
/* 弹出这两个stack的最后一个元素 */
label_context_stack_se = label_context_stack_se->next;
label_context_stack_vm = label_context_stack_vm->next;
/* 整型提升,这里,在满足条件的情况下将所有的整形参数和返回值的位数提升到
* interger_type_node,这个需要目标机器的信息*/
if (TREE_CODE (fndecl) == FUNCTION_DECL
&& targetm.calls.promote_prototypes (TREE_TYPE (fndecl)))
{
tree args = DECL_ARGUMENTS (fndecl);
for (; args; args = TREE_CHAIN (args))
{
tree type = TREE_TYPE (args);
if (INTEGRAL_TYPE_P (type)
&& TYPE_PRECISION (type) < TYPE_PRECISION (integer_type_node))
DECL_ARG_TYPE (args) = integer_type_node;
}
}
/* 一些关联工作,将返回值、函数体关联到当前函数 */
if (DECL_INITIAL (fndecl) && DECL_INITIAL (fndecl) != error_mark_node)
BLOCK_SUPERCONTEXT (DECL_INITIAL (fndecl)) = fndecl;
if (DECL_RESULT (fndecl) && DECL_RESULT (fndecl) != error_mark_node)
DECL_CONTEXT (DECL_RESULT (fndecl)) = fndecl;
/* 处理main函数 */
if (MAIN_NAME_P (DECL_NAME (fndecl)) && flag_hosted)
{
if (TYPE_MAIN_VARIANT (TREE_TYPE (TREE_TYPE (fndecl)))
!= integer_type_node)
{
/* If warn_main is 1 (-Wmain) or 2 (-Wall), we have already warned.
If warn_main is -1 (-Wno-main) we don't want to be warned. */
if (!warn_main)
pedwarn ("return type of %q+D is not %", fndecl);
}
else
{
if (flag_isoc99)
{
tree stmt = c_finish_return (integer_zero_node);
#ifdef USE_MAPPED_LOCATION
/* Hack. We don't want the middle-end to warn that this return
is unreachable, so we mark its location as special. Using
UNKNOWN_LOCATION has the problem that it gets clobbered in
annotate_one_with_locus. A cleaner solution might be to
ensure ! should_carry_locus_p (stmt), but that needs a flag.
*/
SET_EXPR_LOCATION (stmt, BUILTINS_LOCATION);
#else
/* Hack. We don't want the middle-end to warn that this
return is unreachable, so put the statement on the
special line 0. */
annotate_with_file_line (stmt, input_filename, 0);
#endif
}
}
}
/* 对函数的语句树作一些处理 */
DECL_SAVED_TREE (fndecl) = pop_stmt_list (DECL_SAVED_TREE (fndecl));
/* 完成当前的绑定的处理,将它们加入到函数的语句树中。
finish_fname_decls ();
/* 没有返回语句,输出诊断信息 */
if (warn_return_type
&& TREE_CODE (TREE_TYPE (TREE_TYPE (fndecl))) != VOID_TYPE
&& !current_function_returns_value && !current_function_returns_null
/* Don't complain if we are no-return. */
&& !current_function_returns_abnormally
/* Don't warn for main(). */
&& !MAIN_NAME_P (DECL_NAME (fndecl))
/* Or if they didn't actually specify a return type. */
&& !C_FUNCTION_IMPLICIT_INT (fndecl)
/* Normally, with -Wreturn-type, flow will complain. Unless we're an
inline function, as we might never be compiled separately. */
&& DECL_INLINE (fndecl))
{
warning (OPT_Wreturn_type,
"no return statement in function returning non-void");
TREE_NO_WARNING (fndecl) = 1;
}
/* 保存函数的结束位置,cfun结构中保存了当前函数的所有信息 */
cfun->function_end_locus = input_location;
/* 确定elf的可见性信息,参考elf文件格式 */
c_determine_visibility (fndecl);
/* inline 关键字的处理 */
if (DECL_EXTERNAL (fndecl)
&& DECL_DECLARED_INLINE_P (fndecl))
DECL_DISREGARD_INLINE_LIMITS (fndecl) = 1;
/* 这里,将函数的表示形式变为GENERIC.Note:难道不是已经是这种形式的吗? */
if (DECL_INITIAL (fndecl) && DECL_INITIAL (fndecl) != error_mark_node
&& !undef_nested_function)
{
if (!decl_function_context (fndecl))
{
/* 非嵌套函数 */
/* 这个函数的名字似乎是转换成GENERIC, 其实,和我们预想的一样,
* c的前端构造的树已经是GENERIC了,在这个函数中会将这些树转换
* 成gimple*/
c_genericize (fndecl);
c_gimple_diagnostics_recursively (fndecl);
/* 这个部分用于处理Objc在finalize函数之后还可能插入新的函数
* 这部分代码不应该这样用。cgraph_add_new_function是为
* middle-end 准备的函数,而不是前端*/
if (cgraph_global_info_ready)
{
cgraph_add_new_function (fndecl, false);
return;
}
cgraph_finalize_function (fndecl, false);
}
else
{
/* 嵌套函数 */
(void) cgraph_node (fndecl);
}
}
if (!decl_function_context (fndecl))
undef_nested_function = false;
/* We're leaving the context of this function, so zap cfun.
It's still in DECL_STRUCT_FUNCTION, and we'll restore it in
tree_rest_of_compilation. */
set_cfun (NULL);
current_function_decl = NULL;
}
}%
到此,我们看到了怎么样从c的代码翻译到GIMPLE中间表示。
所有剩下的工作,都是在函数cgraph_finalize_function完成的(在非unit-at-a-time的情况下,所有的分析和优化都在这里做)。