China Open source community
站内导航:

 
 
 
当前位置: 首页 >> 程序设计 >> GCC 的编译流程及中间表示层 RTL 的初步探索
 

GCC 的编译流程及中间表示层 RTL 的初步探索

作者:王逸      来源:ibm     发表时间:2006-04-06     浏览次数:      字号:    

内容摘要 本文将以 C 语言为例,介绍 gcc 在接受一个 .c 文件的输入之后,其前端是如何进行处理并得到一个中间表示并转交给后端处理。然后,在了解了 gcc[1] 的工作流程后,介绍一下作者尝试在 gcc 内部的 RTL 表示层中 hack gcc 的过程,与大家分享一些经验,希望能给对有兴趣研究和开发 gcc 的读者有所帮助。

在gcc的编译过程中,有三次比较重要的转换:

  1. 待编译的源代码―<gcc抽象语法树表示。
  2. gcc抽象语法树―<RTL表示。
  3. RTL表示-<汇编代码输出。

RTL是gcc内部使用的中间表示语言,为了对其有一个直观点的印象,我们可以把它dump出来看一看。使用

$ gcc -dr test.c

就可以得到test.c的RTL表示,文件名一般为test.c.00.rtl。

RTL的设计据说是从LISP语言得到了灵感,所以我们dump出来的.rtl文件看起来也像是一个LISP程序,每条RTL语句都是用来描述需要输出的指令的,可以对照我们dump出的.rtl文件以及上面提到的文档来深入学习RTL。但我们的要求不仅如此,我们需要插入自己的RTL语句来hack cc,必须阅读gcc源代码提供的RTL操作的接口,这个过程比较繁琐而且没有文档可以参考,唯一有帮助的就是已有的在RTL表示层上对gcc做的补丁,以吸取其他gcc hackers的经验,作者在尝试自己的补丁时曾经参考过StackGuard [8] 的代码,另外可以在gcc的maillist上看到有些hacker提供的patch,这些已有的工作对于gcc hacker newbie来说是很有裨益的。


[8] StackGuard实际上是一个做过补丁的GCC,可以做为在RTL表示层上hack GCC的一个例子来参考。

仅仅这么多文字来介绍RTL还远远不够,但是如果希望把RTL描述得十分清楚,那应该由另外一篇文章来完成了,本文就不再详述了。

4. Let's hack gcc!
下面进入hack gcc的实战阶段了,先说一下我的目的:我希望使用修改过的gcc编译程序的时候,能够在每个函数的开始和结束的地方插入一个函数调用语句,也就是说,在每个函数的第一条指令之前,由编译器强制插入一个函数调用,在函数最后一条指令结束之后,也要插入一个函数调用。下面用两段C语言代码来表达这个补丁的效果:

int foo()
{
    first statement;
    …
    …
    …
    last statement;
}
int foo()
{
    my_function_begin;
    first statement;
    …
    last statement;
    my_function_end;
}

左边一列是程序员正常编写的普通函数,我希望使用修改过的gcc编译该函数后,能够得到相当于编译右边这段函数的结果,就是对程序员透明地在每个函数的第一条语句之前和最后一条语句之后自动插入两个函数调用:my_function_begin和my_function_end。当然,这两个函数具体实现什么功能可以由程序员来编写,最简单的实现可以仅仅在标准输出上分别打印一句话表示该函数确实被调用了即可。

gcc中生成抽象语法树表示和RTL表示都是以一个完整的函数定义或者top level的声明为单位的,这也就意味着在tree_rest_of_compilation这个函数调用了一系列用于生成RTL表示的函数之后,我们所得到的只是当前正在被编译的函数的RTL表示,而并不是整个源程序的RTL表示,这正好方便我们以函数为单位来进行修改。

我们在tree_rest_of_compilation函数中调用rest_of_compilation之前插入一条语句,调用一个新函数modify_rtl来对gcc生成的RTL表示做一些处理。函数modify_rtl的定义放在function.c文件中,这是因为gcc在生成RTL表示时需要的相关函数大部分都定义在这个文件中,我们的补丁也可以看作是gcc生成RTL表示的一部分工作,所以把modify_rtl放到这个文件中定义是最合适的。

接下来工作的关键就集中到如何定义modify_rtl函数了。现在我们得到了当前编译函数的RTL表示,我们可以对这个RTL单元进行扫描,找到合适的位置分别调用my_function_begin和my_function_end函数即可。函数的RTL表示是一个双向连接的链表结构,其中每个节点称为一个insn [9] ,有的insn可能表示一条真实的汇编指令,有的则表示jump指令跳转的标签或者其它各种声明信息。为了简便起见,这里直接给出一个常用的gcc所提供的访问insn的宏和函数列表,并给出它们的功能:


[9] StackGuard实际上是一个做过补丁的GCC,可以做为在RTL表示层上hack GCC的一个例子来参考。

宏(函数)名 功能
INSN_UID(insn) 获取该insn的id
PREV_INSN(insn) 获取insn链表中该insn的前一个insn
NEXT_INSN(insn) 获取insn链表中该insn的后一个insn
GET_CODE(insn) 获取该insn的code
NOTE_LINE_NUMBER(insn) 如果insn的code是NOTE,则返回该insn对应源代码的行号,否则返回一个负数
Get_insns() 获取当前函数RTL表示的第一个insn
Get_last_insn() 返回当前函数RTL表示的最后一个insn

表2. 部分gcc提供的insn操作接口列表

一个函数完整的、未被优化的RTL表示中会有两个note insn表示函数的开始和结束,gcc定义了两个全局变量NOTE_INSN_FUNCTION_BEGIN和NOTE_INSN_FUNCTION_END来表示这两个note insn的行数。这样我们就可以扫描当前RTL单元,当碰到这两个note insn的时候,就可以插入相应的函数调用语句了。

gcc提供了emit_library_call函数来插入一个函数调用,这个函数返回的是一个表示函数调用的RTL表达式,并默认地把这个RTL表达式插入到当前RTL单元的最后一个insn之后。所以如果直接调用emit_library_call,就会把函数调用语句插入到RTL单元最后一个insn之后,而不是我们所希望的函数开始和结束的地方,我们可以使用start_sequence和end_sequence函数,它们产生一个相对独立的sequence并把函数调用语句保存到一个RTL表达式中以备后用。

我们已经找到插入函数调用的点,并且也生成了表示函数调用的RTL语句,现在就可以使用gcc提供的emit_insn_before和emit_insn_after函数来插入RTL语句了。

到这里,modify_rtl函数的实现基本已经成型了,下面这段示例代码就可以完成在每个函数的开始处插入RTL语句的功能:


int modify_rtl()
{
	rtx insn;
	rtx seq;

	//emit my_function_begin at the beginnig of each function
	start_sequence();
	emit_libarary_call(gen_rtx(SYMBOL_REF, Pmode, my_function_begin),
									0, VOIDmode, 0);
	seq = get_insns();
	end_sequence();

	for(insn = get_insns(); ; insn = NEXT_INSN(insn))
		if((GET_CODE(insn) == NOTE)
			&& (NOTE_LINE_NUMBER(insn) ==
				NOTE_INSN_FUNCTION_BEGIN))
		break;

	emit_insn_after(seq, insn);

	…
}

这段代码中所使用数据结构、函数的具体功能和用法,属于十分细节的内容,无须在这里描述清楚,请读者参考gcc源代码。

对于在函数结束的地方插入my_function_end函数同样如此,我们可以用get_last_insn得到RTL单元的最后一个insn,然后使用PREV_INSN(insn)开始向前扫描,遇到行号为NOTE_INSN_FUNCTION_END的note insn时,用emit_insn_before把相应的函数调用RTL表达式插入到这个insn之前即可。

现在这个patch的基本功能已经完成了,我们还可以再做一些工作使得它功能更强大和实用一些,比如加入一个编译选项(比如-finsert-function)来指定是否启用这个patch的,当编译的命令行参数中没有提供这个编译选项时,我们所作的补丁就不起作用。关于如何增加编译选项,我们可以参考opts.c中的decode-options函数,在此就不详细分析了。

在modify_rtl中调用current_function_name函数可以得到当前正在被编译的函数名,我们可以把这些函数名写到一个文件中去,这样可以记录我们对哪些函数做了修改;还可以实现一个过滤器,在启用了patch的情况下,对于指定的函数,我们还可以将其过滤掉,不对其做处理,这些功能也是很容易实现的。

我们还可以再实现一些功能,比如在扫描RTL的时候,如果发现一条call_insn,可以把这条call指令所调用的函数名记录下来,这样我们甚至可以得到一个程序运行时刻的动态的函数调用关系图,这就可以描绘程序的实际运行轨迹。

最后,还需要把my_function_begin和my_function_end两个函数实现一下,可以把它们的功能扩展一下,不是仅仅输出一条语句到标准输出,而是记录一些信息到文件中,这样就可以得到一个以函数为粒度的运行时刻日志,甚至可以使这两个函数与linux内核联系起来,做一些特殊的检查工作等等,这样就使得我们的patch有一些实用性了。这两个函数我们可以在mylib.c中实现,编译成一个shared object,使用如下命令编译:


$ gcc mylib.c -c -fPIC
$ gcc mylib.o -shared -o libmylib.so

把libmylib.so放到/usr/lib目录下,那么在编译的时候只需加上-lmylib参数就可以使用这个shared object中的函数了。

剩下的工作就是进行调试和测试了,当我们解决了各种问题,使这个修改过的编译器能够完美的运行起来的时候,也许我们就能体会到gcc hacker的那种成就感和喜悦之情了。

5. 经验总结
先说一下我自己尝试的结果,我是基于gcc version 3.4.0工作的,给gcc加入了一个编译选项以选择是否启用添加的补丁,可以在每个函数的开始和结束的时候插入函数调用,也可以在函数调用之前和返回之后插入函数调用,实现了一个过滤器,可以忽略一些函数不对其做处理,并且可以在运行时将一些信息记录到文件中去留待分析。这个补丁的功能基本上就是这些了,实现方法可能和本文中的方法有所不同,文中描述的方法是较早的时候我采用的方法,现在则进行了一些改动,这里就不详加介绍了。我已经成功的使用“我的”gcc编译了emacs和lynx等实用软件,运行正常,补丁功能也正常,可以说是取得了一个小小的成功。但是我没有空间可以上载我的补丁,有兴趣的读者可以通过e-mail向我索取。

最后谈谈我的经验:

在理解gcc的编译流程以及试图找到做补丁的思路的时候,需要多阅读文档,包括学习已有的工作是怎么做的。不要贸然尝试,不要奢望可以凭运气达成目的,尽量找到最合适的实现方法,在确立了一个基本思路之后,可以在gcc的maillist上咨询一下,看看有没有人提供更好的思路,在确信自己思路的可行性之后再开始具体的工作。

在做具体实现的时候,肯定会遇到各种各样的问题,比如在编译自己修改过的gcc时会出错,或者用patch过的gcc编译程序时出错,或者是编译通过运行时刻出错等等,这时候需要耐心地检查代码和进行debug,尽量自己解决问题,不要把一些特别细节地问题拿到maillist上讨论。我记得在maillist上曾经有人严厉地告诫我:“you won't go very far if you ask a question each time you get an error”,自己debug才是解决问题的最好方法,当然如果实在不明白的问题必须拿到maillist上去讨论,这时候要尽量详细的描述自己的目的和问题,才能够得到有效的帮助。

好了,这就是我自己学习和尝试hack gcc的工作过程,希望我的一些经验能够给您帮助,如果对本文中的观点有疑问或者在学习gcc的时候碰到困难,欢迎与我探讨。

参考资料

  1. GCC internals.http://gcc.gnu.org/onlinedocs/gccint/,这是得到公认的除了gcc源码以外,学习gcc最好的材料,虽然比较难读,但它的权威性和全面性是毋庸置疑的,GCC的手册和info也是很好的参考资料。
  2. GCC Front end HOWTO, 通过自己设计一个GCC前端来帮助GCC hacker newbie来学习和了解GCC,http://www.icewalkers.com/Linux/Howto/GCC-Frontend-HOWTO.html#toc1,适合新手阅读,作者还提供了相关程序的软件包。
  3. StackGuard,http://www.immunix.org/stackguard.html,可以作为一个在RTL表示层hack gcc的实例来学习。

关于作者
王逸,2002年秋季入学的南京大学计算机科学与技术系硕士研究生,在分布式系统国家重点实验室学习。对于软件安全、linux系统和无线网络有兴趣,目前工作主要集中在缓冲区溢出攻击的防范上。您可以通过cnnjuwy@hotmail.com与我联系,也可以在南京大学小百合bbs上(http://bbs.nju.edu.cn/或者http://lilybbs.net/)与LinuxLover账号联系。

[1] [2]

编辑 webmaster

 
 
 
评论
 
 
发表
 
姓名: QQ:
性别: MSN:
E-mail: 主页:
评分: 1 2 3 4 5
评论内容:
验证码:
  
  • 请遵守《互联网电子公告服务管理规定》及中华人民共和国其他各项有关法律法规。
  • 严禁发表危害国家安全、损害国家利益、破坏民族团结、破坏国家宗教政策、破坏社会稳定、侮辱、诽谤、教唆、淫秽等内容的评论 。
  • 用户需对自己在使用本站服务过程中的行为承担法律责任(直接或间接导致的)。
  • 本站管理员有权保留或删除评论内容。
  • 评论内容只代表网友个人观点,与本网站立场无关。
  •  
    中国源码网 - WWW.YUANMA.ORG - 中国开放源代码社区