Exercise 1: Familiarizing yourself with Venus
.data
.word 2, 4, 6, 8
n: .word 9
.text
main:
#
add t0, x0, x0
# addi 是 "add immediate"(立即数加法)的缩写,表示这是一个加法指令,其中一个加数是一个立即数(即直接在指令中给出的数)
# 将 x0 寄存器的值(即 0)和立即数 1 相加,然后将结果(0 + 1 = 1)存储在 t1 寄存器中。
# 简而言之,这条指令将 t1 寄存器的值初始化为 1
addi t1, x0, 1
# la 是 "load address" 的缩写,表示这是一个加载地址的指令
# 将标签 n 的地址加载到寄存器 t3 中
la t3, n
# lw 代表 "load word"(加载字),用于从内存中加载一个 32 位的字(word)到寄存器中。
# 0(t3) 指定了要从哪里加载字的内存地址。这里使用了基址加偏移量的寻址方式,其中 0 是偏移量,t3 是基址寄存器
# 执行这条指令之后,t3 不再持有地址,而是持有该地址处的数据
lw t3, 0(t3)
fib:
# beq 代表 "branch if equal"(如果相等则跳转)。这是一个条件分支指令,用于比较两个寄存器的值,如果它们相等,则执行跳转到指定的标签。
# 如果 t3 寄存器中的值等于 0(因为 x0 的值永远是 0),那么程序将跳转到标签 finish 指示的位置。
# 如果 t3 的值不是 0,程序将不会跳转,而是继续执行下一条指令。
beq t3, x0, finish
add t2, t1, t0
# 是将 t1 寄存器中的数据复制到 t0 寄存器中。
mv t0, t1
mv t1, t2
addi t3, t3, -1
# j 是 "jump"(跳转)的缩写,表示这是一个无条件跳转指令,用于将程序的控制流跳转到指定的位置。
# 使程序跳转到标签 fib 所在的代码位置,继续从那里执行
j fib
finish:
# addi a0, x0, 1 这条指令将 a0 寄存器的值设置为 1。
# 在 RISC-V 架构中,进行系统调用(ecall)时,a0 寄存器通常用于指定系统调用的类型或功能代码。
# 这里,将 a0 设置为 1 通常表示执行的是打印整数的系统调用
addi a0, x0, 1
# addi a1, t0, 0 这条指令将 t0 寄存器的值复制到 a1 寄存器中。
# 在进行打印整数的系统调用时,a1 寄存器用于存储要打印的整数值。
addi a1, t0, 0
# 接着执行 ecall 指令时,系统将根据 a0 和 a1 寄存器的值来执行相应的操作。
# 由于 a0 被设置为 1(表示打印整数的系统调用),并且 a1 包含了要打印的整数(来自 t0),
# 因此 ecall 指令会导致打印出 a1 中的整数值
ecall # print integer ecall
# 将 a0 设置为 10 通常表示执行的是一个终止程序的系统调用
addi a0, x0, 10
# 随后执行的 ecall 指令,系统将根据 a0 寄存器的值来执行相应的操作
ecall # terminate ecall
Paste the contents of ex1.s
in Venus and record your answers to the following questions. Some of the questions will require you to run the RISC-V code using Venus’ simulator tab.
- What do the
.data
,.word
,.text
directives mean (i.e. what do you use them for)? Hint: think about the 4 sections of memory.
A: In the context of assembly language, particularly for systems like RISC-V or MIPS, the directives .data
, .word
, and .text
are used to organize and manage different sections of a program’s memory. These align with the typical memory sections in a compiled program:
- .data Directive:
- Purpose: The
.data
directive is used to declare the data segment of the program. This segment is for initializing variables that retain their values throughout the program’s execution. It’s primarily used for static and global variables. - Memory Section: It corresponds to the data segment in memory, which is different from the stack and heap segments. This segment is used for static data allocation.
- Purpose: The
- .word Directive:
- Purpose: The
.word
directive is used within the data segment to define 32-bit words. Each.word
allocates 4 bytes of memory (since a word is 32 bits) and can be used to initialize these words with specific values. For example,.word 2, 4, 6, 8
will allocate 16 bytes of memory and initialize them with the given values. - Memory Section: These words are part of the data segment. This directive helps in reserving memory space and initializing data in this segment.
- Purpose: The
- .text Directive:
- Purpose: The
.text
directive indicates the start of the code segment of the program. This is where the actual instructions of the program are written. It’s the executable part of the program. - Memory Section: It corresponds to the text (or code) segment in memory. This segment is used for the executable instructions of the program and is distinct from data, stack, and heap segments.
- Purpose: The
These directives help in organizing a program into distinct sections, each with its own role in the program’s structure and execution. The .data
segment for initializing and storing data, the .word
directive for defining 32-bit integers in the data segment, and the .text
segment for the actual executable instructions. This organization reflects the typical division of memory in compiled programs into segments like text (code), data, stack (for function calls and local variables), and heap (for dynamically allocated memory).
- Run the program to completion. What number did the program output? What does this number represent?
A: The program output the number 34. This number represents the 9th number in the Fibonacci sequence.
In the Fibonacci sequence, each number is the sum of the two preceding ones, starting from 0 and 1. So, the sequence goes 0, 1, 1, 2, 3, 5, 8, 13, 21, 34,… and so on. The number 34 is the 9th term in this sequence (if we start counting from 0 as the first term).
- At what address is
n
stored in memory? Hint: Look at the contents of the registers.
A: after “la t3, n” , t3这个地方变成了 268435464
Exercise 2: Translating from C to RISC-V
C程序:
int source[] = {3, 1, 4, 1, 5, 9, 0};
int dest[10];
int fun(int x) {
return -x * (x + 1);
}
int main() {
int k;
int sum = 0;
for (k = 0; source[k] != 0; k++) {
dest[k] = fun(source[k]);
sum += dest[k];
}
return sum;
}
对应的汇编语言:
.data
source:
.word 3
.word 1
.word 4
.word 1
.word 5
.word 9
.word 0
dest:
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.text
main:
addi t0, x0, 0
addi s0, x0, 0
# load address , 将source的内存地址加载到 寄存器s1中
la s1, source
la s2, dest
loop:
# slli:这是一个指令操作码,代表“Shift Left Logical Immediate”。它表示将一个寄存器中的值向左逻辑移位(即在右边补零)一个立即数指定的位数
# 将 t0 寄存器中的值向左移动 2 位,然后将结果存入 s3 寄存器
slli s3, t0, 2
# add 是寄存器中的值相加
add t1, s1, s3
lw t2, 0(t1)
beq t2, x0, exit
add a0, x0, t2
# 用于对栈指针(Stack Pointer)进行操作
# addi sp, sp, -8 的操作是将栈指针 sp 的当前值减去 8,它实际上在栈上为新的数据或变量预留了 8 个字节的空间
addi sp, sp, -8
# 代表“Store Word”。它用于将一个字(word)的数据从寄存器存储到内存中。
# 将寄存器 t0 中的数据存储到栈指针 sp 当前指向的内存地址处。由于偏移量是 0,这意味着数据被存储在栈顶
sw t0, 0(sp)
sw t2, 4(sp)
# Jump and Link”。这个指令用于跳转到程序中的另一个位置(在这个例子中是标签 square 所指示的位置),同时将返回地址(即跳转之后下一条指令的地址)保存在一个特定的寄存器中
jal square
lw t0, 0(sp)
lw t2, 4(sp)
addi sp, sp, 8
add t2, x0, a0
add t3, s2, s3
sw t2, 0(t3)
add s0, s0, t2
addi t0, t0, 1
# jal x0, loop 的操作是跳转到标签 loop 指示的代码位置,但不保存返回地址(因为它写入了零寄存器 x0)
jal x0, loop
square:
add t0, a0, x0
add t1, a0, x0
addi t0, t0, 1
addi t2, x0, -1
mul t1, t1, t2
mul a0, t0, t1
# jr ra 的操作是从 ra 寄存器中读取存储的地址,并跳转到那个地址继续执行程序。
# 这种指令通常用于函数的末尾,用来返回到调用该函数的地方。
# 当一个函数执行完毕,通过 jr ra 指令,程序控制权会返回到调用该函数的下一条指令处继续执行
# Jump Register(操作指令) 和 Return Address寄存器
jr ra
exit:
add a0, x0, s0
add a1, x0, x0
ecall # Terminate ecall
我的回答:
- The register representing the variable
k
Answer: t0
- The register representing the variable
sum
.
Answer: s0
- The registers acting as pointers to the
source
anddest
arrays.
Answer: s1 和 s2
- The assembly code for the loop found in the C code.
Answer: loop
- How the pointers are manipulated in the assembly code.
Answer: 使用偏移
gpt4的回答(更详细,举了例子,和我的答案是一样)
The register representing the variable k
- The variable
k
seems to be represented by the registert0
. This is inferred from the lineaddi t0, x0, 0
, which initializest0
to 0 (similar tok = 0
in C), and the lineaddi t0, t0, 1
, which incrementst0
by 1 (similar tok++
in a loop).
The register representing the variable sum
- The variable
sum
appears to be represented by the registers0
. This is based on the lineaddi s0, x0, 0
(initializings0
to 0, likesum = 0
), and the lineadd s0, s0, t2
, which adds the value int2
tos0
(akin to accumulating values insum
).
The registers acting as pointers to the source
and dest
arrays
s1
is used as a pointer to thesource
array (la s1, source
).s2
is used as a pointer to thedest
array (la s2, dest
).
The assembly code for the loop found in the C code
- The loop in the assembly code is represented by the label
loop:
and includes instructions from this label down tojal x0, loop
. The loop terminates whenbeq t2, x0, exit
is executed, which happens whent2
(loaded from thesource
array) is zero, effectively translating to a check likeif (source[k] == 0) break;
in C.
How the pointers are manipulated in the assembly code
- The pointers (
s1
forsource
ands2
fordest
) are manipulated using offset calculations. For instance,slli s3, t0, 2
andadd t1, s1, s3
calculate the address to read from or write to in thesource
ordest
arrays. Theslli s3, t0, 2
instruction shiftst0
left by 2 bits, effectively multiplyingk
by 4 (since eachword
is 4 bytes). This value is then added tos1
ors2
to get the correct address in the array for the current loop iteration.
Exercise 3: Factorial
完成factorial.s, 实现求阶乘的功能
我的解法:
.globl factorial
.data
n: .word 8
.text
main:
la t0, n
lw a0, 0(t0)
lw t0, 0(t0)
# 跳转到标签 factorial 指示的地址去执行代码,并将下一条指令的地址保存在 ra 寄存器中
jal ra, factorial
finish:
addi a1, a0, 0
addi a0, x0, 1 #a0设置为1,表示打印整数
ecall # Print Result
addi a1, x0, '\n'
addi a0, x0, 11 # a0设置为11,表示打印字符
ecall # Print newline
# a0设置为10,表示终止程序
addi a0, x0, 10
ecall # Exit
factorial:
# YOUR CODE HERE
addi t1, t0, -1
beq t1, x0, finish
mul a0, a0, t1
add t0, t1, x0
j factorial
安装java环境后,测试代码是否正确:
Exercise 4: RISC-V function calling with map
.globl map
.text
main:
# 跳转到标签 create_default_list 指示的地址去执行代码,并将下一条指令的地址保存在 ra 寄存器中
jal ra, create_default_list
add s0, a0, x0 # a0 = s0 is head of node list
#print the list
add a0, s0, x0
jal ra, print_list
# print a newline
jal ra, print_newline
# load your args
add a0, s0, x0 # load the address of the first node into a0
# load the address of the function in question into a1 (check out la on the green sheet)
### YOUR CODE HERE ###
la a1, square # 加载 square 函数的地址到 a1
# issue the call to map
jal ra, map
# print the list
add a0, s0, x0
jal ra, print_list
# print another newline
jal ra, print_newline
addi a0, x0, 10
ecall #Terminate the program
map:
# Prologue: Make space on the stack and back-up registers
### YOUR CODE HERE ###
addi sp, sp, -12 # 为三个寄存器分配栈空间
sw ra, 0(sp) # 保存 ra
sw s0, 4(sp) # 保存 s0
sw s1, 8(sp) # 保存 s1
beq a0, x0, done # If we were given a null pointer (address 0), we're done.
add s0, a0, x0 # Save address of this node in s0
add s1, a1, x0 # Save address of function in s1
# Remember that each node is 8 bytes long: 4 for the value followed by 4 for the pointer to next.
# What does this tell you about how you access the value and how you access the pointer to next?
# load the value of the current node into a0
# THINK: why a0?
### YOUR CODE HERE ###
lw a0,0(s0) # 加载当前节点的值到 a0
# Call the function in question on that value. DO NOT use a label (be prepared to answer why).
# What function? Recall the parameters of "map"
### YOUR CODE HERE ###
# 这条指令的功能是从一个寄存器中读取地址,并跳转到该地址执行,同时将下一条指令的地址(即返回地址)保存在默认的链接寄存器 ra(返回地址寄存器)
jalr a1 # 调用 a1 寄存器中的函数
# store the returned value back into the node
# Where can you assume the returned value is?
### YOUR CODE HERE ###
sw a0, 0(s0) # 将返回值存回当前节点
# Load the address of the next node into a0
# The Address of the next node is an attribute of the current node.
# Think about how structs are organized in memory.
### YOUR CODE HERE ###
lw a0, 4(s0) # 加载下一个节点的地址到 a0
# Put the address of the function back into a1 to prepare for the recursion
# THINK: why a1? What about a0?
### YOUR CODE HERE ###
add a1, s1, x0 # 恢复函数地址到 a1
# recurse
### YOUR CODE HERE ###
jal ra, map # 递归调用 map
done:
# Epilogue: Restore register values and free space from the stack
### YOUR CODE HERE ###
lw ra, 0(sp) # 恢复 ra
lw s0, 4(sp) # 恢复 s0
lw s1, 8(sp) # 恢复 s1
addi sp, sp, 12 # 释放栈空间
jr ra # Return to caller
square:
add t0, a0, x0
mul a0 ,t0, t0
jr ra
create_default_list:
addi sp, sp, -12
sw ra, 0(sp)
sw s0, 4(sp)
sw s1, 8(sp)
# li 指令的全称是 "Load Immediate"。它用于将一个立即数(即直接指定的数值)加载到一个寄存器中。这个指令通常用于初始化寄存器的值
li s0, 0 # pointer to the last node we handled
li s1, 0 # number of nodes handled
loop: #do...
li a0, 8
jal ra, malloc # get memory for the next node
sw s1, 0(a0) # node->value = i
sw s0, 4(a0) # node->next = last
add s0, a0, x0 # last = node
addi s1, s1, 1 # i++
addi t0, x0, 10
bne s1, t0, loop # ... while i!= 10
lw ra, 0(sp)
lw s0, 4(sp)
lw s1, 8(sp)
addi sp, sp, 12
jr ra
print_list:
# bne: 这是一个条件分支指令,代表“branch if not equal”。它的作用是比较两个寄存器的值,如果它们不相等,则跳转到指定的标签或地址。
bne a0, x0, printMeAndRecurse
jr ra # nothing to print
printMeAndRecurse:
add t0, a0, x0 # t0 gets current node address
lw a1, 0(t0) # a1 gets value in current node
addi a0, x0, 1 # prepare for print integer ecall
ecall
addi a1, x0, ' ' # a0 gets address of string containing space
addi a0, x0, 11 # prepare for print string syscall
ecall
lw a0, 4(t0) # a0 gets address of next node
jal x0, print_list # recurse. We don't have to use jal because we already have where we want to return to in ra
print_newline:
addi a1, x0, '\n' # Load in ascii code for newline
addi a0, x0, 11
ecall
jr ra
malloc:
addi a1, a0, 0
addi a0, x0 9
ecall
jr ra