gimple
by 夏泽民 Sep 14, 2019
GIMPLE中间表示,是GCC中机器无关的中间表示,机器无关的优化基本都在这个层次上做。
http://gcc.gnu.org/onlinedocs/gccint/GIMPLE.html
在编译过程中,GCC使用了三种主要的中间语言来表示程序:GENERIC,GIMPLE和RTL。GENERIC是一种由每个前端生成的语言无关的表示。它用来作为解析器和优化器之间的接口。GENERIC是一种通用表示,能够表示GCC支持的所有语言程序。
GIMPLE和RTL用于优化程序。GIMPLE用于目标和语言无关的优化(例如,内联,常数传播,尾调用消除,冗余消除等)。与GENERIC比较相似,GIMPLE是一种语言无关的树型表示。不过,与GENERIC不同的是GIMPLE的语法有更多的限制:表达式不包含3个以上的操作数(函数调用除外),它没有控制流程结构,并且具有副作用的表达式只允许出现在赋值语句的右端
1 从 GENERIC 到GIMPLE
GENERIC是GCC最顶层的语言无关中间表示。
GCC 利用 “gimplifier” 将 GENERIC 中间表示转换为 GIMPLE中间表示。
因为GENERIC 是语法树形式的,所以这个转换过程是递归的。
对于一个函数,GENERIC中间表示将其存储在FUNCTION_DECL树节点中的DECL_SAVED_TREE域中。
然后通过调用函数gimplify_function_tree将其转换为GIMPLE.
具体的转换过程一般是 gimplify.c: gimplify_function_tree -> gimplify_body -> gimplify_stmt -> gimplify_expr .
GIMPLE中有一个lower的动作,用于将高层次的GIMPLE表示,解析成低层次的,这个lower动作在pass_lower_cf中完成。
比如,嵌套的作用域和表达式。 可以使用选项 -fdump-tree-gimple得到类C的GIMPLE表达形式
如下面程序:
int main()
{
int a;
if (a)
{
int b;
b = 2 + a + b;
}
return 0;
}
使用该选项得到的转换成C语言形式的GIMPLE中间表示为:
main ()
{
int D.1593;
int D.1594;
int a;
if (a != 0) goto ; else goto ;
:
{
int b;
D.1593 = a + 2;
b = D.1593 + b;
}
:
D.1594 = 0;
return D.1594;
}
2 从 GIMPLE 到 RTL
做完机器无关优化之后,GCC会将GIMPLE转换为RTL中间表示。
在RTL上,基本都是机器相关的优化,以及寄存器分配,指令调度等功能。
从GIMPLE到RTL中间表示,则是在expr.c:expand_expr_real中,
GENERIC: 一种高层次的语言无关的表示。
GIMPLE: 一种低层次的树型表示。
注解: 语句和变量的属性。
语句操作数: 由GIMPLE语句所引用的变量。
SSA: 静态单赋值表示。
别名分析: 加载和存储的别名表示。
GCC进行编译的大概步骤:
词法分析 --> 语法分析 --> 生成语法树 --> 高级gimple --> 低级gimple --> cfg --> ssa -->RTL -->目标代码(汇编代码)
在gcc实际的编译过程中,词法分析是在语法分析的驱动下进行的,也即是语法分析在什么时候需要下一个符号,就在词法分析识别下一个符号
函数声明的作用是,函数定义未出现时,一旦这个函数被调用,可以通过声明来确定函数信息,所以他是一种辅助信息,不参与执行,所以,不需要转换为中间代码
main函数转变为高端gimple的过程:
1 以函数为单位进行转化,并且将函数内部的所有变量以及编译器为方便生成运行时结构所创建的临时变量都提高到函数最开始的位置,为计算栈空间和使用寄存器提供依据
2 将函数执行语句集中到一起,并且其顺序与语法树种所表现的顺序一致,为配合运行时结构会增减一些语句
gimple语句:
1 ## 赋值语句用gimple_assign来表示,该节点主要内容的含义如下:
gimple_assign
subcode : 操作码
num_ops:操作数的个数,可以为2 (一元表达式),3 (二元表达式),4(三元表达式)
op[0]: 操作数0
op[1]: 操作数1
op[2]:操作数2
2 ## 函数调用语句 gimple_call
gimple_assign
num_ops(unsigned int)[5]:操作数的个数,可以为2 (一元表达式),3 (二元表达式),4(三元表达式)
op[0]: 函数返回值 m.0
op[1]: 被调用函数 fun
op[3]:第一个参数 i
op[4]:第二个参数 j
3 ## 函数返回值语句
gimple_return
num_ops[1]
op[0]:变量D.1380
"return D.1380;"
语法树到高端gimple的转化是以语法树的节点为单位进行遍历的
高端gimple到低端gimple主要完成数据合并,代码合并和返回语句合并,有利于最后生成更规整的后端代码
主要是将高端gimple结构中的所有gimple_bind节点拆开,所有gimple_bind变量都放到一起,并记录在local_decls中,
为讲了运行时结构的函数开栈空间提供依据,所有的gimple_bind中的语句也都放到一起,并记录在gimple_body中,将来转化成汇编指令
return语句转低端gimple的处理:
1 在gimple_return语句的位置插入一条goto lable跳转语句
2 将gimple_return语句暂存起来,将gimple语句序列中的gimple_return语句删掉
3 待所有的语句都lower gimple转换完之后,再将gimple_return语句做gimple_return的处理
,处理过程是先添加一个标号,以便于第一句的goto lable对应上,然后再把return 语句插入gimple语句序列
其实经过处理得到的低端gimple已经足以支持生成最终的目标代码,确定运行时结构了,但是gcc考虑到优化,
在此基础上转化了cfg和ssa结构,考虑到平台的通用性,又生成了一套通用的RTL结构,将在RTL的基础上转化为目标代码
#### 低端gimple到cfg结构
GCC设计cfg ( control flow graph ) 主要是用于函数内部的控制流转化,跨函数间的逻辑优化由于逻辑比较复杂,GCC目前还没有完成
cfg的主要作用是在低端gimple的基础上将语句分成几个基本块(basic block),在基本块内,代码是顺序执行的,不存在跳转语句,如果有跳转语句,则放在块的最后,保证跳转只发生在块与块之间,即在gimple中,指令跳转的语句就是基本块的边界
### cfg转ssa
为每个变量增加一个版本号,用于数据流的优化,它的结构是跟低端gimple的结构相同的
### 生成RTL
由于GCC是支持多种平台的,在不同的平台上生成的汇编代码的格式肯定是不同的,如果为每个平台的汇编代码都写一套优化逻辑,是不太现实的,为了解决这个问题,GCC提供了一种中间形式的汇编语言RTL(Register Transfer Language),它与具体的平台无关,这样所有的优化都可以基于RTL了,在所有的优化完成之后,再转变成针对不同硬件平台的汇编代码,每一条RTL语句称为一条insn语句
insn语句一共有6类:
insn ## 一般指令
jump_insn ## 跳转指令
call_insn ## 函数调用指令
code_lable ## 标号,用来表示跳转目标
barrier ## 放在insn语句的序列中,用来表示控制流中不可能到达的位置,eg在无条件跳转的后边
note ## 编译指导信息,例如调试和声明信息等
#### 转化为RTL阶段的主要步骤
1 转化为初始的RTL
2 明确初始的RTL中的运行时结构信息,此时把虚拟寄存器更新为真实的寄存器
#### RTL生成目标代码
也就是汇编代码,就是我们所熟悉的