LLVM关于一维数组的讨论——定义、取第i个下标、函数传参

ask questions

学会提问是件很重要的技能,借助搜索引擎或求助于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,在函数传参部分会再讲到

varDef中处理数组

// SysYParser.g4
varDecl
   : bType varDef (COMMA varDef)* SEMICOLON
   ;

varDef
   : IDENT (L_BRACKT constExp R_BRACKT)* (ASSIGN initVal)?
   ;

上述是g4文件中定义变量的语法,那么我们如何判断变量类型是原始类型还是数组呢?这里我翻车了不少次,记录一些错误的做法:

  1. ctx.L_BRACKT() != null 错误原因在于initVal也可能包含括号,比如int b = a[1]
  2. ctx.IDENT().getText().contains("[") 额,这个很离谱,因为变量名并不包括[
  3. 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,他们的处理方式都不一样,所以我们要修改visitLValvisitLvalStmt

// 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关于一维数组的讨论结束哩!(撒花)

Mixed——小坑

输出

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为空时输出一对中括号

lli ir_file.ll

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(太懒了

你可能感兴趣的:(技术,java,llvm)