<font style="background:yellow">
- 『2021.7.14』
- 编译技术入门与实战——RISCV-GNU-GCC
(一)基础介绍
- 总的来说,前端是与我们的高级语言还相关
- 中间表示,是和高级语言和最后运行的机器都无关
- 后端是和目标机器相关
- GCC最主要的工作就是将前端的支持高级语法特性的源代码,通过一系列转化,最终生成可以被目标机器支持的汇编指令
- https://github.com/riscv-collab/riscv-gcc
- https://desktop.github.com/
- 仓库下载
- https://code.visualstudio.com/
- vscode代码阅读
- po手册
- gcc文件夹下有一个po子文件夹,里面的
.po
文件记录了各种语言的gcc错误说明
XXXX@plct:~/riscv-gnu-toolchain/riscv-gcc/gcc/ po# ls
be.po de.po EXCLUDES fr.po id.po ru.po tr.po zh_CN.po
ChangeLog el.po exgettext gcc.pot ja.po sr.po uk.po zh_TW.po
da.po es.po fi.po hr.po nl.po sv.po vi.po
- 给大家一个小的工具,gcc文件夹下1个po子文件夹
# : cfgrtl.c :2797` #~ msgid "error while parsing constant pool \n"
msgid "flow control insn inside a basic block" #~ msgstr “词法分析常量池时出错\n"
msgstr“基本块内有流程控制指令"
#: cfgrtl.c: 3029 #~ msgid "error in constant pool entry #%d \n"
msgid "wrong insn in the fallthru edge" #~msgstr"常量池条目#%d出错\n"
msgstr "直通边上的错误指令" #~msgid "error while parsing fields \n"
# : cfgrtl.c : 3085
#~msgstr “词法分析字段时出错\n" msgid "insn outside basic block "
msgstr "基本块外出现指令" #~msgid "error while parsing methods \n"#~msgstr“词法分析方法时出错\n"
#: cfgrtl.c : 3093
msgid "return not followed by barrier" #~ msgid "error while parsing final attributes \n"
msgstr "return后没有屏障" #~ msgstr "词法分析final属性时出错\n "
c-parser.h
- pragma_kind,编译制导类型。
- 就是他到底是要进行编译还是要预处理?选择是什么操作?
- Ctrl+左键是去
- Alt+左键是返回
- riscv-gnu-toochain
-
Using the GNU Compiler Collection (GCC) gcc.gnu.org/onlinedocs/gcc-11.1.0/gcc
-
《深入分析GCC》机械工业出版社,王亚刚
-
《编译器设计》(第二版) K.D.Cooper
-
GNU Compiler Collection (GCC) Internals gcc.gnu.org/onlinedocs/gccint
-
我们整个课程是根据-《编译器设计》的目录和结构
- 『2021.7.21』
- 『2021.7.28』
- 『2021.08.11』
-
GCC在完成前端的词法、语法分析后,得到抽象语法树AST,然后将其转换为了对应的GIMPLE序列。《 第1-3节课讲述的内容』
-
随后,GCC对GIMPLE中间表示形式进行了一系列的处理,包括GIMPLE的低级化、GIMPLE优化以及RTL生成等。
-
这些处理过程中,尤其是优化处理纷繁复杂,为了便于组织,GCC对这些操作使用一种称为**==Pass==**的管理策略。
-
也就是说,GCC将这些处理==划分==成一个一个的Pass过程,每个处理过程完成一种特定的处理,其输出结果 将作为下一个处理过程的输入(有些类似于Unix系统中的管道处理的概念)
Pass 的内容在gcc/tree-pass.h
中主要分为:
-
1.GIMPLE Pass
-
2.RTL Pass
-
3.IPA Pass
- 这其中IPA Pass又分为Small IPA Pass和IPA Pass
视频1.33:whoway的疑问:A small IPA pass is a pass derived from
simple_ipa_opt_pass
.-
A small IPA pass 是从simple_ipa_opt_pass派生的
-
参考资料:https://gcc.gnu.org/onlinedocs/gccint/Small-IPA-passes.html
GIMPLE_PASS
、SIMPLE_IPA_PASS
和IPA_PASS
都是以GIMPLE中间表示为处理对象RTL_PASS
以RTL为处理对象
在今天这节课中,我们主要讲述的是。。
以:GIMPLE中间表示,的GIMPLE_PASS
、SIMPLE_IPA_PASS
和IPA_PASS
/* optimization pass type.*/enum opt pass type
{ 上
GINPLE_PASS,RTL_PASS,
SINPLE_IPA_PASS,
IPA_PASS
)
-
其中最重要的是这3个函数
-
然后,最主要的是这个
opt_pass
这是最主要的『基类』
最粽要的2个函数
- gate函数
- 决定了zhibuzhixingz
- excute
- 决定了怎样执行?
GIMPLE PASS上执行的是机器无关的优化。GIMPLE PASS可以遍历当前函数的全部GIMPLE语句。
在我们接下来讲的lowering pass中的控制流生成之前, GIMPLE PASS只能从cfun『读c function』里遍历全部的GIMPLE,因为此时它们还没有被组织成控制流图的形式,之后的GIMPLE PASS就可以使用FOR_EACH_BB这样的宏逐块扫描GIMPLE语句。
RTL Pass基本上是优化的后半部分,是机器相关的。由于RTL有寄存器、字长等GIMPLE没有的底层概念,因此属于底层优化,侧重点也不同。
RTL Pass也可以使用FOR_EACH_BB对控制流进行遍历,但是用FOR_BB_INSNS对基本块中的insn『读作instr cike』进行处理,而GIMPLE Pass是通过gsi_start_bb来获取gimple语句列表。
IPA的全称是Inter-Procedural Analysis。
在gcc里它有两层意思:
- 一个是跨函数的分析
- 一个是全局变量(夹在函数间的变量)的分析。
IPA所使用的工具是==cgraph (call graph,调用图)==。调用图记录了函数之间的调用关系。
进程间分析的重点就是函数间的变量传递(参数)和依赖关系(全局变量,调用关系)。
==IPA pass在每次执行时也只是针对一个函数==,因此它在执行时也可访问current_ function_decl和cfun,并通过它们获取对应的cgraph_ node,由此可以得到当前函数与cgraph里的其他函数之间的关系。
- GCC中定义了==294个 (10.2.0)个pass过程==
这部分定义在
gcc/tree_pass.h
中。
具体而言,这些Pass又可以分为如下几种:
-
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_last_ipa_passes
: 不过他这个是一个非常小的优化,仅仅是包含了3条pass过程,因此我没有把他列出来。 -
all_passes
:- exception handling,GIMPLE优化(SSA优化,dead call,别名分析,if合并,循环优化等等),GIMPLE→RTL,RTL优化(复写传播,dead call,寄存器合并,条件跳转优化等等)
-
这是在
passess.def
文件下,写下的passess是怎么执行的?
普通的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.通知plugin准备execute: invoke_plugin_callbacs(PLUGIN_PASS_EXECUTION, pass);
- 4.执行pass预定的TODO list: execute_todo (pass->todo_flags_start);
- 5.==检查函数的property是否和pass的相符==: do_per_function(verify_curr_properties,(void *)(size_t)pass-> properties_required);
- 6.执行pass: todo_after = pass->execute ();
- 7.执行pass指定的结束Todo list: execute_todo (todo_after | pass->todo_flags_finish);
- 8.==如果是regular IPA Pass==,记录该pass到当前函数的IPA Transform列表中。还有一些debug用的dumpfile操作就不提了。
- 『和前面的普通Pass有一些区别,但主要来说的话,有3次执行机会』
- 分别是他自身结构体中的
generate_summary
→execute
→transform
执行Regular IPA Pass的函数就不再作详细介绍了,他们的执行流程被分散为3轮,每一轮的步骤都差不多,而且比普通pass的执行过程简单。每个IPA pass都有三次机会来执行。
generate_summary
→execute
→transform
前两次机会基本都差不多,都是用来扫描和准备参数,最后一次机会就是对cgraph实施改变。
比如pass_ipa_inline
,在generate_summary
里面计算所有函数的大小,在execute里面根据大小和其他信息来判定哪些函数可以内联,在transform里面对所有标记为内联的函数进行内联,并更新cgraph。
尽管如此,generate_summary
和execute
还是有作用上的区别。
由于前者先执行,而后者是否执行被gate()
控制,并且transform是按照每个函数上挂载的ipa_pass_list
来执行的。
如果execute不执行的话,该pass也不会被挂载到当前函数上,因此generate_summary
可以用来通过gate()
控制后两者是否被执行。
执行完所有Pass之后,gcc就进入了最后的阶段:目标代码生成。『这一部分会在下一节课中来讲』
- 『2021.8.18』
- 『2021.9.1』
的内容和一些规范。回顾一下上节课的内容。讲解RTL的一些内容。
RTL分为IR-RTL与MD-RTL,
- IR-RTL是GIMPLE转换成的一种中间表示形式
- MD-RTL用于描述后端机器
RTL有很多对象内容。
RTL对象类型包含表达式,向量,字符串,整数,其机器模式如下
『最重要的就是经常用的,需要记住他,定义的一些机器模式』
BImode | QImode | HImode | SImode | DImode | SFmode | DFmode |
---|---|---|---|---|---|---|
表示单个位『就是bit』 | 1/4整数 | 半整数 | 单整数 | 双整数 | 单精度浮点数 | 双精度浮点数 |
-
quarter Int,n.四分之一
- 1/4整数『如果是以32位为基础的,那么1/4就是8bits,也就是一个Byte』
-
half Int,n.一半
-
single Int mode
-
Double Int
-
Single Float
-
我们这节课是接着上节课将,上节课是IR-RTL的内容,这节课将一些和后端机器相关的内容。
后端机器的特性描述分为两个部分,包括
- ==指令模式==文件{target.md}与
- 宏定义文件{target.c / target.h}
如果是RISC-V机器的话,riscv.md啥的
.md文件中包含目标机器支持的每条指令的==指令模式==
- 也就是说我们是通过.md文件来生成汇编代码的。
总的来看,GCC中发生了四种主要的转换
- 1.前端读取源代码,构建语法树
- ⒉.语法树转换为结构更为简单的GIMPLE,并通过各种pass完成后端无关的优化
- 3.优化后的GIMPLE根据==指令模式==,构造RTL表示的insn序列
- 4.然后insn序列里的RTL通过==匹配『.md文件中定义的』RTL模板==,来生成汇编代码
- 这里的话,所有的『指令模板』都定义在.md文件中,就叫machine descripe『机器描述』文件。
- 机器描述文件也使用RTL进行编写,主要包括:
- 指令模式,常量,属性,自定义断言,自定义约束,迭代器,流水线,窥孔优化的定义
他定义的内容很多。但是他==主要的目的==是:用来生成对应的汇编指令
.md 文件 |
作用 | RTX形式 | 备注 |
---|---|---|---|
指令模板 | 1、通过指令模板生成RTL序列。『也就是我们所说的==IR-RTL==』2、匹配指令模板中的RTL模板生成汇编代码。『如果匹配成功的话,就进行汇编代码的生成』 | define_insn、==define_expand==、define_split、define_insn_and_split | 2个作用,也是md文件中最主要的作用『有4种RTX形式』 |
常量 | 这个常量包括我们需要定义的寄存器编号,和一个未定义的叫UNSPEC编号『UNSPEC就是一些非规范的一些操作』 | define_constans | |
属性 | 主要是定义机器指令的类别,比如说我们添加B扩展的话,他的属性都是Bitmenew..K扩展的话,就是Cracpt。。都是需要自己定义的,比如说如果有个算术操作的话,他定义的属性可能就是PLUS、ADD、加减之类的。 | ||
断言 | 自定义断言,多用来判断操作数 | define_predicate | |
约束 | 自定义约束条件,包括目标约束与寄存器约束『同样是用来判断一个操作数条件的,但是他==比断言更加的细致==』 | ||
迭代器 | 包括机器模式、RTX code,整数迭代器『定义多个机器的迭代,『6分30秒 | ||
流水线 | |||
窥孔优化 |
define_insn
- 用来生成一条insn
define_expand
- 则是生成insn的另一种形式
define_split
- 对于较复杂的insn会通过split来划分成几条,相对简单一点的形式。
define_insn_and_split
- 和前面2个一样『指着insn和split』??啥意思
- define_constans
- 指令模式通常包括:名称、RTL模板、条件、输出模板、属性
看一个加法的指令模式
(define_insn "addsi3"
[set() ]
)
指令模式名称及命名规则
- 指令模式名称是指令模式的标识,它具有唯一性,在.md文件中,声明为define_insn和define_expand的指令模式会用来生成insn『最常用的是2种』
定义2个指令模式,如果有相同的名称的话,就会报一些冲突。
- 1.指令模式名称可以是空字符串""
- 2.指令模式名称可以是"*”开头的字符串,以星号开头的字符串不参与生成insn,但其RTL模板仍会匹配insn生成对应的汇编指令
- 3.指令模式名称通常由:{名称}{机器模式}{操作数个数}三部分组成
- 比如add si 3
- addsi3
- RTL模板主要包含指令操作数及副作用的声明,其功能分为两种,用来==构造insn==或==匹配insn==
- 在==构造过程==中,RTL模板用于描述如何利用给定的操作数来实现insn,即从GIMPLE生成insn时,如何从GIMPLE中提取操作数并替换为RTL模板中的操作数占位符
- 在==匹配过程==中,通常利用RTL匹配特定模式以及如何找到它们的操作数,匹配发生在insn序列已经生成,准备利用insn序列生成目标机器上的汇编代码时,如果匹配成功,则判断条件后进入输出模板,否则匹配下一条指令模式
RTL模板中的==匹配==形式
通常由match_*
来完成RTX的匹配表达式,不同匹配形式有不同的限制
一共定义了7种匹配形式,这些匹配主要是有些不同的限制
“=r,r”
表示,
- scratch和reg表达式
- scratch更加偏向于一个实际分配的寄存器
- reg可以是虚拟的或者是实际的寄存器
断言是确定操作数是否满足某种条件的判断过程,GCC中断言分为与目标机器无关的断言(gcc/recog.c)及与目标相关的断言(gcc/config/{target}/predicates.md)
在match_operand中,可以指定操作数的约束条件,对操作数进行更细致的描述。约束包括系统约束和自定义约束,约束个数分为单一约束和多替代约束,标准约束定义在gcc/common.md,自定义约束条件包含在gcc/config/{target}/constraints.md
约束字符 | 作用 |
---|---|
空 | 无约束 |
==r== | 寄存器操作数,且使用通用寄存器 |
f | 寄存器操作数,且使用浮点数寄存器 |
m | 内存操作数,允许机器支持的所有地址类型 |
I,J,K,...P | 描述不同范围内的整数立即数 |
E,F,G,H | 描述不同范围内的浮点数立即数 |
有时,一条指令有多个可选的可能操作数集。如add指令允许两个寄存器进行相加,也允许寄存器与立即数相加,这时可以定义多个约束条件,来表示操作数的选择方案,注意单个指令的所有操作数必须具有相同数量的替代项。
『要注意的是,要写成相同的数量来替代』图片中的划线
多代替约束还有另一个功能:它们使编译器能够决定虚拟寄存器最适合分配给哪种硬件寄存器。编译器检查适用于使用虚拟寄存器的insn的约束,寻找与机器相关的字母,然后进行寄存器的分配。
( define_insn "*movsf_hardfloat"
[(set (match_operand:SF 0 "nonimmediate_operand" "=f,f,f,m,m,*f,*r, *r,*r,*m" )
(match_operand:SF 1 "move_operand" " f,G,m,f,G,*r,*f,*G*r,*m,*r"))]
通常条件用来判断==目标机器的位长==和是否满足==指令集特性==,当条件匹配则进入输出模板,否则移动至下一条指令模板进行匹配,也可以对操作数进行特殊判断,来满足某些功能
输出模板主要在指令模式匹配成功后进行汇编代码的生成,除了一些特殊字符需要替换,其他部分正常输出
指令模式可以对属性进行设置,这些属性通常在优化中有较多的使用,目标机器的属性主要定义在.md文件中
在某些目标机器上,一些用于RTL生成的标准模式名称不能用单个insn处理,但一系列RTL insn『==意思是要多个IR-RTL?》==』可以表示它们。对于这些目标机器,可以通过编写define_expand
来指定如何生成RTL的序列。
一个define_expand
中有四个操作数:名字、RTL模板、条件、准备语句
- 和
define_insn
不同的是,那个输出模板变成了,准备语句
在准备语句中定义了**==两个特殊的宏: DONE和FALL==**
使用DONE会结束RTL生成。在这种情况下会通过在准备语句显式调用==emit_insn==中生成RTL insn,不会生成RTL模板。
当使用FAIL时,这意味着该模式不是真正可用的编译器将尝试使用其他模式生成代码的其他策略
( define_expand "call"
[(parallel [(call (match_operand 0 "")
( match_operand 1 ""))
(use (match_operand 2 "" )) ;; next_arg_reg
(use (match_operand 3 ""))])] ;; struct_value_size_rtx
""
{
rtx target = riscv_legitimize_call_address (XEXP (operands[0], 0));
emit_call_insn (gen_call_internal (target, operands[1]));
DONE;
})
- 这些函数一般会定义在targert.c和.h文件中『函数的使用』
- 『2021.9.8』
- 『2021.9.15』
- 实战部分
- 内容大致包括
GCC指令添加
- 1.intrinsic接口添加
- 2.迭代器使用
- 3.标准模板名指令的使用
- 4.自定义constraint,predicate(来限定一些情况来选择比较好的模板)
- 5.寄存器限制
- 6.添加一个条件跳转指令
- 希望这次分享,弄明白,这2个大致的区别
- constraint,predicate
intrinsic,意思是:adj.固有的;内在的;体内的;本质的;基本的;精华的
- 当你在.md中描述之后,我们会发现自动生成了一个
CODE_FOR
的东西 gcc/config/riscv/riscv-builtins.c
- B站,[完结] 零基础入门 RISC-V GCC 编译器开发 暨 编译技术入门与实战第四季