学会提问是件很重要的技能,借助搜索引擎或求助于GPT都需要问出合理的问题,才能找到对应的解决方案。
GPT在生成LLVM IR时会给出很多无关代码,这里我们可以使用**Let’s assume you have already set up the LLVM module, builder, and other necessary objects.**提示语告诉GPT只生成想要的指令代码
定义数组和取下标是不同的操作,简化起见,这里只讨论一维数组
LLVMTypeRef i32Type = LLVMInt32Type();
LLVMTypeRef arrayType = LLVMArrayType(/*数组类型*/i32Type, /*数组长度*/100);
// 分配地址空间
LLVMValueRef arrayPointer = LLVMBuildAlloca(builder, arrayType, "array");
如果要给数组赋初值
// 元素列表
LLVMValueRef[] arrayValues = {LLVMConstInt(i32Type, /*整数值*/0, 0), LLVMConstInt(i32Type, 1, 0), LLVMConstInt(i32Type, 2, 0)};
// 给每个下标对应的元素赋值
for (int i = 0; i < arrayValues.length; i++) {
// 第一个整数的0代表第一维度的索引值,第二个整数的i代表下标,即一维数组中的偏移量
LLVMValueRef[] indices = {LLVMConstInt(i32Type, 0, 0), LLVMConstInt(i32Type, i, 0)};
// 得到指向数组第i个元素的指针
LLVMValueRef elementPointer = LLVMBuildGEP(builder, arrayPointer, new PointerPointer(indices), indices.length, "gep");
// 修改值
LLVMBuildStore(builder, arrayValues[i], elementPointer);
}
对如下SysY文件
// test.sysy
int main() {
int a[3] = {1, 2, 5};
return a[1];
}
生成的中间代码
// test.ll
; ModuleID = 'module'
source_filename = "module"
define i32 @main() {
mainEntry:
%array = alloca [3 x i32], align 4
%gep = getelementptr [3 x i32], [3 x i32]* %array, i32 0, i32 0
store i32 1, i32* %gep, align 4
%gep1 = getelementptr [3 x i32], [3 x i32]* %array, i32 0, i32 1
store i32 2, i32* %gep1, align 4
%gep2 = getelementptr [3 x i32], [3 x i32]* %array, i32 0, i32 2
store i32 3, i32* %gep2, align 4
%arr_index_pointer = getelementptr [3 x i32], [3 x i32]* %array, i32 0, i32 1
%a = load i32, i32* %arr_index_pointer, align 4
ret i32 %a
}
%gep = getelementptr [3 x i32], [3 x i32]* %array, i32 0, i32 0
,我们可以观察到第一个i32 0
在每条gep指令中都存在,对应Java中indices
的第一个整数0,第二个i32 0
是在一维数组的偏移量,这里就是数组的首元素。由此我们猜测多维数组也可以类似得表达,通过修改indices
的值得以使gep取更高维的值?
// 元素列表
LLVMValueRef zero = LLVMConstInt(i32Type, 0, 0);
LLVMValueRef one = LLVMConstInt(i32Type, 1, 0);
LLVMValueRef two = LLVMConstInt(i32Type, 2, 0);
// 声明
LLVMTypeRef arrayType = LLVMArrayType(i32Type, 100);
LLVMValueRef global = LLVMAddGlobal(module, arrayType, "global_arr");
LLVMValueRef[] arrayValues = {zero, one, two};
// 初始化
LLVMSetInitializer(global, LLVMConstArray(i32Type, new PointerPointer(arrayValues), arrayValues.length));
LLVMConstArray
用于创建常量数组,但是不代表global不能修改数组的值
我们在定义数组后,将数组指针存放到当前作用域currentScope.define(varName, arrayPointer)
。使用数组时再取出来
// 取a[i]
LLVMValueRef arrayPointer = currentScope.resolve(varName);
LLVMValueRef[] index = {zero, LLVMConstInt(i32Type, i, 0)};
LLVMValueRef elementPointer = LLVMBuildGEP(builder, arrayPointer, new PointerPointer(index), index.length, "arr_index_pointer");
LLVMValueRef element = LLVMBuildLoad(builder, elementPointer, "element");
arrayPointer
的类型实际上是LLVMPointerTypeKind
,指向的元素类型才是LLVMArrayTypeKind
,在函数传参部分会再讲到
// SysYParser.g4
varDecl
: bType varDef (COMMA varDef)* SEMICOLON
;
varDef
: IDENT (L_BRACKT constExp R_BRACKT)* (ASSIGN initVal)?
;
上述是g4
文件中定义变量的语法,那么我们如何判断变量类型是原始类型还是数组呢?这里我翻车了不少次,记录一些错误的做法:
ctx.L_BRACKT() != null
错误原因在于initVal
也可能包含括号,比如int b = a[1]
ctx.IDENT().getText().contains("[")
额,这个很离谱,因为变量名并不包括[
ctx.constExp() == null
错误原因是ctx.constExp()
一定不为null,至少是空,这里尚不清楚为什么,可能是因为*
匹配?ctx.initVal()
是可以为空的,只有?
匹配,如果没匹配到就直接为空了。但是*
匹配可能需要向后看很远吧,反正*
匹配里的东西不为null,?
匹配里的东西可以为null举一反三:我们再来看个例子
// SysYParser.g4
lVal
: IDENT (L_BRACKT exp R_BRACKT)*
;
stmt:
: ...
: RETURN exp? SEMICOLON // 词法单元一般大写,语法单元一般小写
同样的,由lVal的规约我们知道lVal是左值,可以是标识符也可以是取数组下标。我们判断lVal只是标识符的方法是ctx.exp().isEmpty()
,判断return;
语句的方法是ctx.exp() == null
这部分又要动许多笔墨,写不动了快、、
数组作为函数的参数,实际上传进去的是指针,并不是真正的数组,我们可以参照ll
代码
// test.ll
define void @f(i32* %0) {
fEntry:
;%arr实际上就是i32**类型,之后使用需要先load以下
%arr = alloca i32*, align 8
store i32* %0, i32** %arr, align 8
遍历函数的形参时,我们需要判断参数是否为数组,从而参数列表放不同类型
LLVMPointerType
创建指针类型,第二个参数AddressSpace
设置为0表示默认地址空间
PointerPointer<Pointer> argumentTypes = new PointerPointer<>(0);
...
if 是数组
argumentTypes.put(cnt++, LLVMPointerType(i32Type, 0));
else
argumentTypes.put(cnt++, i32Type);
参数属于函数体的作用域的enclosingScope,我们需要将参数保存下来,这里我们使用LLVMGetParam
获取参数
以保存数组参数为例:
LLVMTypeRef pointerType = LLVMPointerType(i32Type, 0);
LLVMValueRef pointer = LLVMBuildAlloca(builder, pointerType, "arr");
LLVMValueRef var = LLVMGetParam(function, count++);
LLVMBuildStore(builder, var, pointer);
currentScope.define(param.IDENT().getText(), pointer);
符号表保存的参数是指针,而不是数组
定义好参数包含数组的函数后,我们可以用LLVMBuildCall
调用,传入数组和其他变量。注意:这里需要区分变量的类型,因为传入数组参数也只传个标识符,所以我们要修改visitLVal
假如我们这样调用函数:f(a, b, c)
,如何知道传入的是不是数组呢?其实只需要知道他们指向的对象是不是LLVMArrayTypeKind
就可以了
LLVMValueRef valueRef = currentScope.resolve(name);
LLVMTypeRef type = LLVMTypeOf(valueRef);
LLVMTypeRef elementType = LLVMGetElementType(type);
if (LLVMGetTypeKind(elementType) == LLVMArrayTypeKind) {
LLVMValueRef[] indice = {zero, zero};
// 获得指向数组开头的指针
return LLVMBuildGEP(builder, valueRef, new PointerPointer(indice), 2, "call_arr_param");
}
注意:不能直接用LLVMGetTypeKind(type) == LLVMArrayTypeKind
,因为valueRef自身就是个指针,是LLVMPointerTypeKind
指针类型
函数内保存的数组参数和局部数组其实是不同的,所以无论是a[i]
还是a[i] = 1
,他们的处理方式都不一样,所以我们要修改visitLVal
和visitLvalStmt
// visitLVal method
/*
* 处理a[i]
*/
LLVMValueRef array = currentScope.resolve(name);
if array指向的类型是LLVMPointerTypeKind:
// array是数组参数
LLVMValueRef pointer = LLVMBuildLoad(builder, array, "load");
LLVMValueRef[] index = {visit(ctx.exp(0))};
LLVMValueRef element = LLVMBuildGEP(builder, pointer, new PointerPointer(index), 1, "func_arr_pointer");
return LLVMBuildLoad(builder, element, "load");
else
// 局部数组
array是数组参数时,处理稍有不同,array
类型实际上是i32**
,使用前要先load。我们可以看到index
的参数变成了一个,这是由于load过的缘故,pointer
指向了数组的开头,之后只需要提供偏移量就可以了。
生成的gep指令的参数变成了3个:%func_arr_pointer2 = getelementptr i32, i32* %load1, i32 1
tips:可以用
clang -S -emit-llvm main.c -o main.ll -O0
跑下程序,看看生成的代码到底是什么样子,然后参照着写
// visitLvalStmt method
/*
* a[i] = 1;
*/
if 是数组:
if 是数组参数:
...
else:
// 局部数组
else:
// 局部变量
和刚才处理a[i]
是类似的,讨论下a
指向的对象类型即可
至此,lab7关于一维数组的讨论结束哩!(撒花)
List list = new ArrayList();
System.error.println(list); // []
list.add("A");
list.add("B");
System.error.println(list); // [A, B]
在ctx.constExp()
遇到的问题,也就是上面说的是否为空,当初发现constExp()
为空却有输出时吃了一惊,以为serr该什么都不输出,差点忘了list为空时输出一对中括号
LLVM解释器可以运行生成的ll
中间代码,虽然我这报错could not mmap JIT marker
。
GPT解释:这个错误通常与内存保护机制有关。在某些系统上,JIT需要对内存进行映射 (mmap) 以创建可执行代码的区域。如果系统的内存保护设置不允许JIT进行映射操作,就会导致报错 “could not mmap JIT marker”。
不过这个倒是帮我解决了oj上error: expected instruction opcode
的问题,因为当ll
文件有问题时,lli会告诉你哪儿错了
lli: tests/test1.ll:6:1: error: expected instruction opcode
}
^
然后我发现是int型函数无返回值
// test1.ll
; ModuleID = 'module'
source_filename = "module"
define i32 @main() {
mainEntry:
}
发现的一篇可能有用的文章,后续可以看看 My first: LLVM program(太懒了