gcc源码应用例子,让FPGA版cpu支持c语言(1)

前几篇文章讲了gcc的词法分析和宏,下面先来个小应用实例,简单介绍一下后端。
我相信也有不少大神,自己写过虚拟机或者基于fpga实现过cpu,毕竟由那本自制cpu在嘛(笑)。
对于自制的cpu,只能用汇编语言编写代码,那是无论如何也不能忍受的,今天介绍怎么把gcc编译器打包到自制的cpu上,使其支持c和c++。
gcc是一个多前端和多后端的编译器集合,如果能驾驭它,就能把一大堆语言打包到任意机器上面。
gcc源码应用例子,让FPGA版cpu支持c语言(1)
是不是爽歪歪啊,如果能得到gcc的支持,哪怕把整个linux系统打包上来都可能。那么我们来看一下gcc后端。
gcc源码应用例子,让FPGA版cpu支持c语言(1)
我们从这些后端里面,选择fr30这个后端进行分析,因为它足够简单。
gcc源码应用例子,让FPGA版cpu支持c语言(1)
先来介绍几个文件,首先打开fr30.md,这个文件里面定义了目标机器的汇编代码。

gcc源码应用例子,让FPGA版cpu支持c语言(1)

这是一种名为lisp的编程语言,被用在gcc后端,用来匹配指令模式的,gcc里面把这个叫rtl(Register Transfer Language),意思为寄存器传送。
lisp是一个前置运算符,且没有优先级的语言,就像这样"(+ 5 6)",计算两个数的相加,他会把括号里面第一个元素作为运算符,后面全部是操作数。
rtl总共有五种数据类型,分别是:
1.表达式
2.整数
3.计算机字,跟寄存器宽度相当
4.字符串
5.向量,使用方括号定义,方括号里面元素可以是任意rtl类型,元素与元素之间使用空格隔开
define_insn定义一条指令,一般格式是:
(define_insn 指令名称 匹配模式 验证脚本 输出字符)
其中匹配模式是一个向量。
验证脚本就是用户定义了该语句后,gcc在编译过程中会执行这个脚本,并对rtl做一些验证,可以不写。
输出字符,就是最后需要输出的汇编代码。
所以,最简单的办法,就是保持原有的框架不变,直接把输出的汇编给换了(笑),当然大部分情况下,是不能用的,除非你的汇编语言和它原来的相似度达到百分之99。
我们来看一个具体的实例:
st %0, @-r15
st是汇编指令,往某个地址写入数据,%0是编译器传递过来的第一个参数,r15一个寄存器名称。
那么这个参数是怎么传递进来的呢?我们来看普配规则。
[(set (mem:SI (pre_dec:SI (reg:SI 15)))
(match_operand:SI 0 “register_operand” “a”))]
一共两句,我们一句一句分析。
(set (mem:SI (pre_dec:SI (reg:SI 15)))
reg:SI 表示一个32位的伪寄存器
pre_dec:SI 表示减去32位的偏移量
mem:SI 表示寻址一个32位的地址
顺便简单说明一下模式:
BI 1位
QI 8位整数
HI 16位整数
PSI 32位指针
SI 32位整数
PDI 64位指针
DI 64位整数
HF 16位浮点
SF 32位浮点
DF 64位浮点
(match_operand:SI 0 “register_operand” “a”))
match_operand:SI 表示匹配一个32位的模式
0 表示匹配上的对象放入0参数,可以用%0打印出来
register_operand 表示匹配对象是寄存器
“a” 表示一种匹配条件,这是fr30自定义的条件,表示匹配所有寄存器
我们先来介绍一下标准的匹配模式:
m 允许内存操作数
o 允许内存操作数,但必须是可偏移地址
V 允许内存操作数,但必须是不可偏移地址
< 允许自减的内存操作数
/> 允许自增的内存操作数(>前面没有/)
r 如果寄存器操作数在通用寄存器中,则允许该操作数
i 允许整型操作数
n 允许整型操作数,但是必须已知
I…P 允许自定义的范围的整型操作数
E 允许浮点操作数,但格式必须与机器相同
F 允许浮点操作数
G,H 允许自定义范围的浮点操作数
s 允许未知数值的整数立即数
g 允许任意通用寄存器,内存,整数立即数
X 允许任意操作数
0…9 允许指定操作数,但数字优先级在字母之后
P 不允许使用有效内存地址
非标准的在constraints.md文件中定义:

gcc源码应用例子,让FPGA版cpu支持c语言(1)
我们可以看到a的定义。
就这样,前端语义模型,一旦跟后端的rtl匹配成功,就会输出相应的汇编语句。
gcc源码应用例子,让FPGA版cpu支持c语言(1)
我们在上面的截图中,还能看到,匹配条件a前面有一个=,所以,我们这里再介绍一下匹配条件的修饰符:
= 表示此操作数是由指令写入,前一个数值会被新的覆盖
/+ 表示此操作数由指令读写(前面没有/)
& 在特定替代方案中,表示这是一个earlyclobber操作数
% 声明两个操作数的指令可交换,只有只读操作数可以使用%
/# 忽略所有字符,直到遇到第一个’,’(前面没有/)
/* 表示首选寄存器时,应该忽略以下字符(前面没/)
然后再来介绍另一个命令,define_expand。

gcc源码应用例子,让FPGA版cpu支持c语言(1)
它的语句,跟define_insn十分相似,但是它仅仅用于rtl的生成,可以生成多个insn,相当于c语义里面的宏。
上面是单条件匹配模式,下面我们继续往下看,能发现多条件匹配模式:

gcc源码应用例子,让FPGA版cpu支持c语言(1)
它拥有多个条件,条件与条件使用逗号分隔,同时也有多条输出语句,语句和语句之间使用换行符分隔。
单条语句写在双引号里面,而定义多条语句,需要再第一个双引号后面加上@说明符。
我们可以看到,这个命令里面,拥有两个参数,这个语句的条件匹配是这样的:
1.优先匹配第二个条件,假设匹配上r,根据上面的信息,我们知道r是寄存器
2.第二个条件匹配成功后,然后匹配第一个。我们知道第二个条件中,r是第三个条件,那么对应的,跟第一个表达式里面的第三个条件进行匹配。第二个表达式中的第三个条件是m,m表示立即数。
3.然后就去找第三个输出语句,“stb \t%1, %0”,意思是把一个八位的立即数,写入到寄存器中,例如"stb r15 0x12"类似这样的语句。
接下来,我们介绍一些谓词,再前面我们看到诸如"register_operand"这样的表达式,这就是寄存器模式,初次之外还有其他模式:
常量模式谓词
immediate_operand 匹配任意常量,一般由常量操作指令使用
const_int_operand 匹配整型常量,一般由不允许使用符号和标签的直接操作数使用
const_double_operand 匹配浮点常量,如果模式设定为VOIDmode,也能匹配整型常量
寄存器模式谓词
register_operand 匹配寄存器表达式,常用于risc上的算数指令
pmode_register_operand 突破机器描述读取器中的限制,不知道什么用
scratch_operand 匹配寄存器和临时表达式,但是不接受伪寄存器,只能在match_scratch内部使用
内存模式谓词
memory_operand 匹配内存访问
address_operand 匹配地址引用
indirect_operand 严格内存操作数模式,只能匹配立即数作为地址
push_operand 匹配一个刚送入堆栈数值的内存引用
pop_operand 匹配一个刚弹出堆栈数值的内存引用
nonmemory_operand 匹配寄存器,立即数
nonimmediate_operand 匹配寄存器,内存操作数
general_operand 匹配寄存器,立即数或内存操作数
通用模式谓词
comparison_operator 匹配任意算数比较表达式
ordered_comparison_operator 匹配任意对整数有效的算数表达式
除了系统内置的谓词之外,还有很多自定义的谓词,在predicates.md文件中:

gcc源码应用例子,让FPGA版cpu支持c语言(1)
到目前为止,在不考虑优化的情况下,我们可以编写一个简单的rtl后端了。
fr30.opt暂时不做介绍,因为这个文件大部分情况下可以不动。
下面我们来看fr30的头文件:

gcc源码应用例子,让FPGA版cpu支持c语言(1)
在文件的一开始,定义了一些宏和内置的指令。
我们知道,不同平台的代码,可以用宏来区分,例如有_WIN32_,__MIPS__这样的内置宏,这些宏就定义在这里,这边是fr30。
下面是链接是,gcc会自动调用的指令。

gcc源码应用例子,让FPGA版cpu支持c语言(1)
gcc源码应用例子,让FPGA版cpu支持c语言(1)
对于int是16位还是32位,地址之间的对齐,主要看这部分的宏定义。
下面是寄存器相关,有充分的注释,这边就不在另外介绍了:

gcc源码应用例子,让FPGA版cpu支持c语言(1)
对于一个处理器,有哪些寄存器,这些寄存器怎么分配,定义在这里:
gcc源码应用例子,让FPGA版cpu支持c语言(1)
这里的数字介绍一下,其中1表示这个寄存器有特定作用,0表示是一个通用寄存器。例如,r0是用来保存临时值的,我就不能把它随意的分配给变量使用,所以把它标注为1,还有一些特殊作用的寄存器,例如sp,ap等,也不能被随意使用。
然后还有一部分寄存器是在函数调用中,被作为参数传递的,使用CALL_USED_REGISTERS宏另外标注。
内容挺多的,一章介绍不玩,打算分三章写,我们下一章继续(笑)。我我是手残,排版困难,怎么办啊啊啊啊?