# PW6 实验报告 学号PB21000179 姓名 吴书让 学号PB21111627 姓名 罗胤玻 ## 问题回答 ### Task1 **手工代码不具代表性,以下分析针对使用** `clang -S -emit-llvm` **编译得到的IR代码** > 1-1 请给出while语句对应的LLVM IR的代码布局特点,重点解释其中涉及的几个`br`指令的含义(包含各个参数的含义) **1-1:** 生成代码并对主要部分加入注释如下: ```llvm @b = dso_local global i32 0, align 4 ; 定义一个全局变量b,初始值为0,对齐方式为4 @a = dso_local global i32 0, align 4 ; 定义一个全局变量a,初始值为0,对齐方式为4 ; Function Attrs: noinline nounwind optnone uwtable define dso_local i32 @main() #0 { ; 定义一个名为main的函数,返回类型为i32 %1 = alloca i32, align 4 ; 在栈上分配一个i32类型的空间,对齐方式为4 store i32 0, i32* %1, align 4 ; 将0存储到%1指向的地址,对齐方式为4 store i32 0, i32* @b, align 4 ; 将0存储到全局变量b,对齐方式为4 store i32 3, i32* @a, align 4 ; 将3存储到全局变量a,对齐方式为4 br label %2 ; 无条件跳转到标签2 2: ; 标签2(循环条件判断基本块) %3 = load i32, i32* @a, align 4 ; 从全局变量a中加载一个i32类型的值,对齐方式为4 %4 = icmp sgt i32 %3, 0 ; 比较%3和0的大小关系,结果存储在%4中 br i1 %4, label %5, label %11 ; 根据%4的值进行条件跳转,如果为真跳转到标签5,否则跳转到标签11 5: ; 标签5(循环体基本块) %6 = load i32, i32* @b, align 4 ; 从全局变量b中加载一个i32类型的值,对齐方式为4 %7 = load i32, i32* @a, align 4 ; 从全局变量a中加载一个i32类型的值,对齐方式为4 %8 = add nsw i32 %6, %7 ; 将%6和%7相加,结果存储在%8中 store i32 %8, i32* @b, align 4 ; 将%8存储到全局变量b,对齐方式为4 %9 = load i32, i32* @a, align 4 ; 从全局变量a中加载一个i32类型的值,对齐方式为4 %10 = sub nsw i32 %9, 1 ; 将%9减1,结果存储在%10中 store i32 %10, i32* @a, align 4 ; 将%10存储到全局变量a,对齐方式为4 br label %2 ; 无条件跳转到标签2 11: ; 标签11(循环结束后基本块) %12 = load i32, i32* @b, align 4 ; 从全局变量b中加载一个i32类型的值,对齐方式为4 ret i32 %12 ; 返回%12作为函数的返回值 } ``` 注意到如下的while语句对应的LLVM IR的代码布局特点: 1. while语句的条件判断部分和循环体部分分别对应一个基本块, 2. 循环体部分的最后一条语句为无条件跳转到条件判断部分的基本块, 3. 条件判断部分的最后一条语句为条件跳转到循环体部分的基本块或者跳转到循环体部分之后的基本块。 > 1-2 请简述函数调用语句对应的LLVM IR的代码特点 **1-2:** 生成代码并对主要部分加入注释如下: ```llvm define dso_local i32 @add(i32 %0, i32 %1) #0 { %3 = alloca i32, align 4 ; 在栈上分配一个i32类型的空间,对齐方式为4 %4 = alloca i32, align 4 ; 在栈上分配另一个i32类型的空间,对齐方式为4 store i32 %0, i32* %3, align 4 ; 将参数%0的值存储到%3指向的地址中,对齐方式为4 store i32 %1, i32* %4, align 4 ; 将参数%1的值存储到%4指向的地址中,对齐方式为4 %5 = load i32, i32* %3, align 4 ; 从%3指向的地址中加载一个i32类型的值,对齐方式为4 %6 = load i32, i32* %4, align 4 ; 从%4指向的地址中加载一个i32类型的值,对齐方式为4 %7 = add nsw i32 %5, %6 ; 将%5和%6相加,结果存储在%7中 %8 = sub nsw i32 %7, 1 ; 将%7减1,结果存储在%8中 ret i32 %8 ; 返回%8 } define dso_local i32 @main() #0 { ; 定义一个名为main的函数,返回类型为i32 %1 = alloca i32, align 4 ; 在栈上分配一个i32类型的空间,对齐方式为4 store i32 3, i32* %1, align 4 ; 将值3存储到%1指向的地址中,对齐方式为4 %2 = alloca i32, align 4 ; 在栈上分配另一个i32类型的空间,对齐方式为4 store i32 2, i32* %2, align 4 ; 将值2存储到%2指向的地址中,对齐方式为4 %3 = load i32, i32* %1, align 4 ; 从%1指向的地址中加载一个i32类型的值,对齐方式为4 %4 = load i32, i32* %2, align 4 ; 从%2指向的地址中加载一个i32类型的值,对齐方式为4 %5 = call i32 @add(i32 %3, i32 %4) ; 调用add函数,传入参数%3和%4,并将返回值存储到%5中 %6 = add nsw i32 %5, 1 ; 将%5加1,结果存储在%6中 ret i32 %6 ; 从函数中返回%6的值 } ``` 注意到如下的函数调用语句对应的LLVM IR的代码特点: 1. 函数调用语句对应的LLVM IR的代码中,会先将函数的参数存储到栈上分配的空间中,然后再调用函数。 2. 函数调用语句对应的LLVM IR的代码中,会将函数的返回值存储到一个临时变量中,然后再对临时变量进行操作。 3. 调用者使用`call`指令调用被调用者,被调用者使用`ret`指令返回调用者。 ------ ### Task2 > 2-1 请给出`SysYFIR.md`中提到的两种getelementptr用法的区别, 并解释原因: **2-1:** - `getelementptr`实际上是一条指针计算语句,不进行任何数据的访问或修改,作用是计算指针并修改计算后指针的类型。 - 第一个参数为要进行计算原始指针的类型; - 第二个参数是原始指针,往往是一个结构体指针,或数组首地址指针。 - 第二个参数及以后的参数,都称为index,表示要进行计算的参数,作用在第二个参数给出的初始指针,如结构体的第几个元素,数组的第几个元素。 - **第一种用法**:获取数组元素所在地址,一个偏移0得到%1指向数组的第一个,用第二个偏移0得到首元素向后偏移量,返回此地址 - **第二种用法**:获取指针地址,第一个偏移0计算得到%1所指的变量,并返回该变量的地址 ----- ### Task3 > 3-1. 在`scope`内单独处理`func`的好处有哪些。 **3-1:** 在不同的scope中可以使用相同的变量名,而不必担心命名冲突的问题,同时也可以提高代码的可读性和可维护性。 ## 实验设计 ## 实验难点及解决方案 ## 实验总结 ## 实验反馈 ## 组间交流