想好好熟悉一下llvm开发一个新后端都要干什么,于是参考了老师的系列文章:
代码在这里(还没来得及准备,先用网盘暂存一下):
链接: https://pan.baidu.com/s/1V_tZkt9uvxo5bnUufhMQ_Q?pwd=ggu5 提取码: ggu5
之前的章节只实现了 int 和 32 位的 long 类型数据,这一章会新增一些更复杂的数据类型,比如 char, bool, short, long long,还会增加结构体,浮点,和向量类型。这一部分内容相对比较简单,其实这些类型也都是标准语言都支持的类型,所以 LLVM 自身已经实现了很大一部分功能,只要我们的后端不那么奇怪,就很容易填补缺失的内容。
目录
[1.1 Cpu0ISelDAGToDAG.cpp](#1.1 Cpu0ISelDAGToDAG.cpp)
[1.2 Cpu0ISelLowering.cpp/.h](#1.2 Cpu0ISelLowering.cpp/.h)
[1.3 Cpu0InstrInfo.td](#1.3 Cpu0InstrInfo.td)
[1.4 Cpu0SEISelDAGToDAG.cpp/.h](#1.4 Cpu0SEISelDAGToDAG.cpp/.h)
[1.5 MCTargetDesc/Cpu0InstPrinter.cpp/.h](#1.5 MCTargetDesc/Cpu0InstPrinter.cpp/.h)
[2.1 局部指针](#2.1 局部指针)
[2.2 char类型](#2.2 char类型)
[2.3 bool类型](#2.3 bool类型)
[2.4 short](#2.4 short)
[2.5 long long 类型](#2.5 long long 类型)
[2.6 局部数组、结构体](#2.6 局部数组、结构体)
[2.7 全局数组、结构体](#2.7 全局数组、结构体)
[2.8 向量](#2.8 向量)
[2.9 cl指令](#2.9 cl指令)
一、修改的文件
1.1 Cpu0ISelDAGToDAG.cpp
在SelectAddr接口内增加对于基址+常量偏移这种地址形式的处理,对于全局基址加常量偏移的情况,提取其基址和偏移。。
1.2 Cpu0ISelLowering.cpp/.h
有关于对 bool 类型的处理,这里增加了一些对 i1 类型 Promote 的合法化描述,告诉 LLVM 在遇到对 i1 类型的 extend 时要做 Promote。Promote 是将较小宽度的数据类型扩展成对应的能够支持的更宽的数据宽度类型,在指令选择的类型合法化阶段会起到作用。在 long long 实现中,在 Lowering 的位置还需要增加对 long long 类型的移位操作合法化。
覆盖一个函数 isOffsetFoldingLegal(),直接返回 false,避免带偏移的全局地址展开,Cpu0 和 Mips 一样无法处理这种情况。我们实现的 getAddrNonPIC() 方法中,将全局符号地址展开成一条加法指令,对地址的高低位做加法运算。所以实际上我们会将一条全局地址带偏移的寻址展开成加法运算 base,然后再把结果与 offset 相加的 DAG(在 Cpu0ISelDAGToDAG.cpp 中的 SelectAddr 中提取这种情况的 node Value,此时就已经是两个 add node 了)。
最后,还需要对向量类型的支持做一小部分改动,覆盖 getSetCCResultType() 方法,如果是向量类型,使用 VT.changeVectorElementTypeToInteger() 方法返回 CC 值。
1.3 Cpu0InstrInfo.td
到目前,因为我们添加数据类型的很多实现代码已经在公共 LLVM 代码中实现,所以实际上大多数修改都在 td 文件中。
新增一个 mem_ea 的操作数类型,这是一个 complexpattern,会定义其 encoding 操作和 printinst 等操作,它用来描述指令 pattern 中的地址表示;然后要定义一个 LEA_ADDiu 的模式,这是一个不会输出成指令的模式,它实际上是计算地址+偏移的结果,这和 sparc 处理器中的 LEA_ADDRi 是一样的效果。
新增 i8 和 i16 相关的 extend 类型以及对应的 ld/st,命名为 LB, LBu, SB, LH, LHu, SH。LB, LH 处理有符号的 i8/i16 类型 load,LBu, LHu 处理无符号的 i8/i16 类型 load,SB, SH 处理i8/i16 类型的store。
新增 CountLeading0 和 CountLeading1 的 pattern,用来选择到计算前导 0 和计算前导 1 的指令,llvm 内置了 ctlz 的 node(count leading zero),可以直接把 clz 指令接过去,不过对于 count leading 1 是没有对应的 node 的,不过可以通过先对值取反然后求前导 0 的方式实现前导 1 的计算,即 ctlz (not RC:$rb)。
因为 C 语言没有对求前导 0 和前导 1 的原生语法,所以实际上会使用 builtin 接口来实现,也就是说,在 C 语言描述中,为了实现这种功能,需要调用 __builtin_clz() 函数(ctls 就是先对参数取反再调用 ctlz 的 builtin),因为我们使用了内置的 node,所以这部分是 llvm 帮我们实现了。
1.4 Cpu0SEISelDAGToDAG.cpp/.h
定义了一个 selectAddESubE() 方法,用来处理带进位的加减法运算的指令选择。在 trySelect() 方法中,将对 ISD::SUBE, ISD::ADDE 的情况选择用 selectAddESubE() 来处理。
selectAddESubE() 方法为符合条件的 node 新增了一个操作数节点,该节点会读取状态字中进位是否是 1,并将结果叠加到运算中;在 Cpu032I 处理器中,使用 CMP 指令和 ANDi 指令来获取进位状态,在 Cpu032II 处理器中,则使用 SLT 指令直接判断进位。
另外,还要处理 SMUL_LOHI 和 UMUL_LOHI 节点,这是能够直接返回两个运算结果的节点(高低位)。
1.5 MCTargetDesc/Cpu0InstPrinter.cpp/.h
增加mem_ea 的printinst操作的实现。
二、实现结果
2.1 局部指针
cpp
int test_local_pointer() {
int b = 3;
int *p = &b;
return *p;
}
bash
addiu $sp, $sp, -8 # 扩栈
addiu $2, $zero, 3 # 将3存到寄存器
st $2, 4($sp) # 将其存到栈上
addiu $2, $sp, 4 # 读出栈中局部变量的地址
st $2, 0($sp) # 将这个地址存到栈上
ld $2, 0($sp) # 读出这个地址
ld $2, 0($2) # 读出地址里的内容
addiu $sp, $sp, 8 # 回栈
ret $lr # 返回
2.2 char类型
cpp
struct Date
{
short year;
char month;
char day;
char hour;
char minute;
char second;
};
unsigned char b[4] = {'a', 'b', 'c', '\0'};
int test_char()
{
unsigned char a = b[1];
char c = (char)b[1];
struct Date date1 = {2021, (char)2, (char)27, (char)17, (char)22, (char)10};
char m = date1.month;
char s = date1.second;
return 0;
}
bash
addiu $sp, $sp, -24
lui $2, %got_hi(b)
addu $2, $2, $gp
ld $2, %got_lo(b)($2) # 与上边搭配加载全局变量b的首地址
lbu $3, 1($2) # 计算b[1]的地址,存到寄存器3
sb $3, 20($sp) # 将寄存器3内的地址存到栈上
lbu $2, 1($2) # 再次计算b[1]的地址,存到寄存器2
sb $2, 16($sp) # 将寄存器2内的地址存到栈上
ld $2, %got($__const.test_char.date1)($gp)
ori $2, $2, %lo($__const.test_char.date1) # 获取要写入局部变量对象的常量的地址
lhu $3, 6($2) # 将偏移6处的内容load到寄存器3中,lhu是i16的,就是load范围是2字节
lhu $4, 4($2) # 将偏移4处的内容load到寄存器4中,lhu是i16的,就是load范围是2字节
shl $4, $4, 16
or $3, $4, $3 # 这两条是将上述load出来的两个2字节的内容拼成一个4字节的
st $3, 12($sp) # 将这4字节存到栈上,也就是存放hour, minute, second到 date1
lhu $3, 2($2) # 这里是相同的逻辑
lhu $2, 0($2)
shl $2, $2, 16
or $2, $2, $3
st $2, 8($sp) # 将这4字节存到栈上,也就是存放year, month, day到 date1
lbu $2, 10($sp) # 从偏移10的位置读出date1.month(我们知道year, month, day在偏移8的位置,year两个字节,因此month在偏移10的位置,这里很正确)
sb $2, 4($sp) # 将其存到栈上(m)
lbu $2, 14($sp) # 从偏移14的位置读出date1.second
sb $2, 0($sp) # 将其存到栈上(s)
addiu $2, $zero, 0
addiu $sp, $sp, 24
ret $lr
2.3 bool类型
cpp
bool test_load_bool()
{
int a = 1;
if (a < 0)
return false;
return true;
}
这里涉及到跳转我们当前可能编不过,下一节的内容加上之后我们就可以编过了,我们先提前看一下效果。
bash
_Z14test_load_boolv:
# %bb.0:
addiu $sp, $sp, -8
addiu $2, $zero, 1
st $2, 0($sp)
ld $2, 0($sp)
addiu $3, $zero, -1
slt $2, $3, $2
bne $2, $zero, $BB0_2
nop
# %bb.1:
addiu $2, $zero, 0
sb $2, 7($sp) # 使用 sb 将 bool 类型的 0 写入栈
jmp $BB0_3
$BB0_2:
addiu $2, $zero, 1
sb $2, 7($sp) # 使用 sb 将 bool 类型的 1 写入栈
$BB0_3:
lbu $2, 7($sp)
addiu $sp, $sp, 8
ret $lr
2.4 short
cpp
int test_signed_char()
{
char a = 0x80;
int i = (signed int)a;
i = i + 2; // i = (-128 + 2) = -126
return i;
}
int test_unsigned_char()
{
unsigned char c = 0x80;
unsigned int ui = (unsigned int)c;
ui = ui + 2; // ui = (128 + 2) = 130
return (int)ui;
}
int test_signed_short()
{
short a = 0x8000;
int i = (signed int)a;
i = i + 2; // i = (-32768 + 2) = -32766
return i;
}
int test_unsigned_short()
{
unsigned short c = 0x8000;
unsigned int ui = (unsigned int)c;
ui = ui + 2; // ui = (32768 + 2) = 32770
return (int)ui;
}
bash
st_signed_short
...
addiu $sp, $sp, -8
ori $2, $zero, 32768
sh $2, 4($sp)
lh $2, 4($sp)
st $2, 0($sp)
ld $2, 0($sp)
addiu $2, $2, 2
st $2, 0($sp)
ld $2, 0($sp)
addiu $sp, $sp, 8
ret $lr
...
test_unsigned_short:
...
addiu $sp, $sp, -8
ori $2, $zero, 32768
sh $2, 4($sp)
lhu $2, 4($sp)
st $2, 0($sp)
ld $2, 0($sp)
addiu $2, $2, 2
st $2, 0($sp)
ld $2, 0($sp)
addiu $sp, $sp, 8
ret $lr
...
汇编还是很好理解的,这里就不进行详细的分析了。
2.5 long long 类型
cpp
long long test_longlong()
{
long long a = 0x300000002;
long long b = 0x100000001;
int a1 = 0x30010000;
int b1 = 0x20010000;
long long c = a + b; // c = 0x00000004,00000003
long long d = a - b; // d = 0x00000002,00000001
long long e = a * b; // e = 0x00000005,00000002
long long f = (long long)a1 * (long long)b1; // f = 0x00060050,01000000
return (c+d+e+f); // (0x0006005b,01000006) = (393307,16777222)
}
bash
addiu $sp, $sp, -56
addiu $2, $zero, 2
st $2, 52($sp) # a的低位
addiu $2, $zero, 3
st $2, 48($sp) # a的高位
addiu $2, $zero, 1
st $2, 44($sp) # b的低位
st $2, 40($sp) # b的高位
lui $2, 12289
st $2, 36($sp) # a1
lui $2, 8193
st $2, 32($sp) # b1
ld $2, 52($sp) # a的低位
ld $3, 48($sp) # a的高位
ld $4, 44($sp) # b的低位
ld $5, 40($sp) # b的高位
addu $3, $3, $5 # 高位相加
addu $4, $2, $4 # 低位相加
sltu $2, $4, $2 # 判断低位加法是否有进位
addu $2, $3, $2 # 将进位与高位结果相加
st $4, 28($sp) # 下同
st $2, 24($sp)
ld $2, 48($sp)
ld $3, 52($sp)
ld $4, 40($sp)
ld $5, 44($sp)
sltu $6, $3, $5
subu $2, $2, $4
subu $2, $2, $6
subu $3, $3, $5
st $3, 20($sp)
st $2, 16($sp)
ld $2, 48($sp)
ld $3, 52($sp)
ld $4, 44($sp)
ld $5, 40($sp)
mul $5, $3, $5
multu $3, $4
mflo $3
mfhi $6
addu $5, $6, $5
mul $2, $2, $4
addu $2, $5, $2
st $3, 12($sp)
st $2, 8($sp)
ld $2, 36($sp)
ld $3, 32($sp)
mult $2, $3
mflo $2
mfhi $3
st $2, 4($sp)
st $3, 0($sp)
ld $2, 28($sp)
ld $3, 24($sp)
ld $4, 20($sp)
ld $5, 16($sp)
addu $3, $3, $5
addu $4, $2, $4
sltu $2, $4, $2
addu $2, $3, $2
ld $3, 8($sp)
ld $5, 12($sp)
addu $5, $4, $5
sltu $4, $5, $4
addu $2, $2, $3
addu $2, $2, $4
ld $3, 4($sp)
ld $4, 0($sp)
addu $2, $2, $4
addu $3, $5, $3
sltu $4, $3, $5
addu $2, $2, $4
addiu $sp, $sp, 56
ret $lr
2.6 局部数组、结构体
与2.2中的局部结构体类似。
2.7 全局数组、结构体
cpp
struct Date
{
int year;
int month;
int day;
};
struct Date date = {2021, 2, 27};
int a[3] = {2021, 2, 27};
int test_struct()
{
int day = date.day;
int i = a[1];
return (i+day); // 2 + 27 = 29
}
bash
addiu $sp, $sp, -8
lui $2, %got_hi(date)
addu $2, $2, $gp
ld $2, %got_lo(date)($2) # 从got表中取全局变量date的地址
ld $2, 8($2) # 从偏移8的地方load出date.day,(year和month各占4字节)
st $2, 4($sp) # 存到栈中(day)
lui $2, %got_hi(a) # 下同
addu $2, $2, $gp
ld $2, %got_lo(a)($2)
ld $2, 4($2)
st $2, 0($sp)
ld $2, 0($sp)
ld $3, 4($sp)
addu $2, $2, $3
addiu $sp, $sp, 8
ret $lr
2.8 向量
cpp
typedef long vector8long __attribute__((__vector_size__(32)));
typedef long vector8short __attribute__((__vector_size__(16)));
int test_cmplt_short()
{
volatile vector8short a0 = {0, 1, 2, 3};
volatile vector8short b0 = {2, 2, 2, 2};
volatile vector8short c0;
c0 = a0 < b0;
return (int)(c0[0] + c0[1] + c0[2] + c0[3]);
}
int test_cmplt_long()
{
volatile vector8long a0 = {2, 2, 2, 2, 1, 1, 1, 1};
volatile vector8long b0 = {1, 1, 1, 1, 2, 2, 2, 2};
volatile vector8long c0;
c0 = a0 < b0;
return (c0[0] + c0[1] + c0[2] + c0[3] + c0[4] + c0[5] + c0[6] + c0[7]);
}
下述是test_cmplt_short函数的汇编,我们看个稍微短一点的:
bash
addiu $sp, $sp, -64
st $10, 60($sp) # 4-byte Folded Spill
st $9, 56($sp) # 4-byte Folded Spill
addiu $2, $zero, 3
st $2, 44($sp)
addiu $3, $zero, 2
st $3, 40($sp)
addiu $2, $zero, 1
st $2, 36($sp)
addiu $2, $zero, 0
st $2, 32($sp)
st $3, 28($sp)
st $3, 24($sp)
st $3, 20($sp)
st $3, 16($sp)
ld $3, 44($sp)
ld $4, 40($sp)
ld $5, 36($sp)
ld $6, 32($sp)
ld $7, 28($sp)
ld $8, 24($sp)
ld $9, 20($sp)
ld $10, 16($sp)
slt $6, $6, $10
subu $6, $2, $6
slt $5, $5, $9
subu $5, $2, $5
slt $4, $4, $8
subu $4, $2, $4
slt $3, $3, $7
subu $2, $2, $3
st $2, 12($sp)
st $4, 8($sp)
st $5, 4($sp)
st $6, 0($sp)
ld $2, 12($sp)
ld $2, 8($sp)
ld $2, 4($sp)
ld $2, 0($sp)
ld $3, 12($sp)
ld $3, 8($sp)
ld $3, 0($sp)
ld $3, 4($sp)
addu $2, $2, $3
ld $3, 12($sp)
ld $3, 4($sp)
ld $3, 0($sp)
ld $3, 8($sp)
addu $2, $2, $3
ld $3, 8($sp)
ld $3, 4($sp)
ld $3, 0($sp)
ld $3, 12($sp)
addu $2, $2, $3
ld $9, 56($sp) # 4-byte Folded Reload
ld $10, 60($sp) # 4-byte Folded Reload
addiu $sp, $sp, 64
ret $lr
其实整体逻辑是很简单的。
2.9 cl指令
cpp
int countLeadingZero() {
int a, b;
b = __builtin_clz(a);
return b;
}
int countLeadingOne() {
int a, b;
b = __builtin_clz(~a);
return b;
}
bash
countLeadingZero:
addiu $sp, $sp, -8
ld $2, 4($sp)
clz $2, $2
st $2, 0($sp)
ld $2, 0($sp)
addiu $sp, $sp, 8
ret $lr
countLeadingOne:
addiu $sp, $sp, -8
ld $2, 4($sp)
clo $2, $2
st $2, 0($sp)
ld $2, 0($sp)
addiu $sp, $sp, 8
ret $lr