Lab 1: C & CGDB
Learning Goals
- 练习使用指针、字符串和结构体。
- 学会基础的调试技巧:编译警告消除、断言以及 GDB 。
Exercise 1 是一个 C 语言的 Warm Up,实现几个简易函数即可。
Exercise 2 需要我们使用 GDB,发现并修改程序中的一些逻辑 BUG,这里我使用的是 CGDB,相比于 GDB 来说可以更加方便的在源码上下断点跟进。
注意,如果我们需要用 CGDB 在源码层面上进行调试,需要在编译时指定 -g 参数,gcc 会在编译时做以下额外操作:
- 创建调试符号表,符号表包含了程序中使用的变量名称等信息。
- 关闭所有的优化机制,以便程序严格按照源码流程执行。
下面是CGDB的一些常用指令
空格 断点
run r 运行
start 从第一行开始单步运行
next n 单步调试 F8
step s 进入内部 F10
until u 快速运行完循环、until localtion 运行到目的行
continue 运行到下一个断点 F6
finish 跳出当前栈帧 F7
info locals 打印本地变量
print p 显示变量
display 持续打印某个变量
watch
o 打开文件列表
Exercise 3 还是对 CGDB 的练习,需要我们通过调试,找到并修改链表中的一个 segfaults (段错误)。
Exercise 4 需要我们实现一个链表有环的判断,用双指针即可。
int ll_has_cycle(node *head) {
/* TODO: Implement ll_has_cycle */
node * fast = head;
node * slow = head;
while (fast && fast->next){
slow = slow ->next;
fast = fast->next->next;
if(fast == slow){
return 1;
}
}
return 0;
}
Lab 2: C Memory Management, Valgrind
Learning Goals
- 练习简单的位操作。
- 学会使用 Valgrind 进行内存泄漏检查。
- 练习编写安全的程序。
Exercise 1: Bit Operations
需要我们只通过简单的位运算符实现以下三个函数:
/* Returns the Nth bit of X. Assumes 0 <= N <= 31. */
unsigned get_bit(unsigned x, unsigned n) {
/* YOUR CODE HERE */
return -1; /* UPDATE WITH THE CORRECT RETURN VALUE*/
}
/* Set the nth bit of the value of x to v. Assumes 0 <= N <= 31, and V is 0 or 1 */
void set_bit(unsigned *x, unsigned n, unsigned v) {
/* YOUR CODE HERE */
}
/* Flips the Nth bit in X. Assumes 0 <= N <= 31.*/
void flip_bit(unsigned *x, unsigned n) {
/* YOUR CODE HERE */
}
这一关和 CSAPP 里位运算的那一个 lab 很类似,算是简化版,稍微想一下就能很快写出来。
Exercise 2: Valgrind
Valgrind 是一款用于程序调试和分析的开源工具,可以用于检测和追溯内存泄漏。
在编译时,指定 -g 参数,可有助于调试时定位到具体代码行,这里我们以一个创建链表为例:
int main(int argc, char **argv) {
for (int i = 0; i < 5; ++i) {
add_to_front(&head, i);
}
}
运行 valgrind --leak-check=full ./linked_list
输出:
Congrats! All of the test cases passed!
==58781==
==58781== HEAP SUMMARY:
==58781== in use at exit: 80 bytes in 5 blocks
==58781== total heap usage: 6 allocs, 1 frees, 1,104 bytes allocated
==58781==
==58781== 80 (16 direct, 64 indirect) bytes in 1 blocks are definitely lost in loss record 2 of 2
==58781== at 0x4849D8C: malloc (in /usr/lib/aarch64-linux-gnu/valgrind/vgpreload_memcheck-arm64-linux.so)
==58781== by 0x1089BF: create_node (linked_list.c:8)
==58781== by 0x108A67: add_to_front (linked_list.c:35)
==58781== by 0x108C77: main (test_linked_list.c:14)
==58781==
==58781== LEAK SUMMARY:
==58781== definitely lost: 16 bytes in 1 blocks
==58781== indirectly lost: 64 bytes in 4 blocks
==58781== possibly lost: 0 bytes in 0 blocks
==58781== still reachable: 0 bytes in 0 blocks
==58781== suppressed: 0 bytes in 0 blocks
==58781==
==58781== For lists of detected and suppressed errors, rerun with: -s
==58781== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
从上面信息可以定位到一共有 5 个内存泄漏点,其中 1 个为直接泄漏,即 head 指向的那一个节点;4 个为间接泄漏,即之后四个节点。
下面通过几个案例进一步学习 Valgrind的使用。
访问未初始化的内存:
int main(void){
int *p;
int a = *p;
}
内存释放后读写
#include "malloc.h"
int main(void){
int *a = malloc(sizeof(int));
*a = 10;
free(a);
int *b = malloc(sizeof(int));
*a = 20;
}
动态内存越界读写
double free
#include "malloc.h"
int main(void){
int *p = malloc(0x4);
free(p);
free(p);
}
Exercise 3: Memory Management
这部分需要我们补充一个缺陷的 vector,非常的基础,可以结合之前所学的 CGDB 调试以及 Valgrind 检测内存情况。
Lab 3: RISC-V Assembly
Learning Goals
- 熟悉使用 Venus 模拟 RISC-V 的运行。
- 了解 C 代码和 RISC -V 之间的相互转化。
- 编写正确的 RISC -V 函数调用。
Exercise 1 需要我们配置好 venus 的环境,简单来说就是把虚拟机或者 docker 里的 lab 文件夹映射到某个 web 路径下,然后通过 venus 的网站 进行访问,从而对本地文件进行调试分析。
但是后来发现 vscode 自身就有相关的插件,也还不错,效果如下:
Exercise 2 通过一个斐波那契的例子,熟悉 Venus 对 RISC-V 调试分析。
Exercise 3 还是一个阅读理解,通过给出的 c 和 RISC-V 代码,找到两者的联系。
Exercise 4 需要我们使用 RISC-V 来实现阶乘,算是对之前所学的函数调用以及循环控制的一个练习。
Exercise 5: RISC-V function calling with map
这一小节需要我们完善一个链表以及 map
函数,该函数可以穿入一个链表以及函数指针来修改链表的内容。
struct node {
int value;
struct node *next;
};
void map(struct node *head, int (*f)(int))
{
if (!head) { return; }
head->value = f(head->value);
map(head->next,f);
}
这里提一下链表,我们在 C 语言中如果想创建一个9->8->7...->2->1
的链表,一般是按顺序创建结点和修改指向
node_9->data=9;
node_9->next=node_8;
node_8->data=8;
node_8->next=node_7...
node_1->data=1;
node_1->next=NULL;
按照这个思路,我们需要把头节点暂存,然后每次把尾节点指向一个 malloc 分配的空间,重复操作,再返回头节点,这些许有些麻烦。
代码框架是从用从 1 到 9 的创建顺序,这样可以省去很多寄存器的使用,并且最后返回的就是头节点。
li a0, 8 # molloc 8 bytes on heap
jal ra, malloc # Allocate 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
Lab 4: RISC-V Functions, Pointers
Exercise 1 需要我们补充 f
函数,使得调用传入数组和不同的 key ,输出不同的 value。
Exercise 2 需要我们修复 cc_test.s
中的访问错误,主要涉及到 s 寄存器的保存。
Exercise 3 要去我们使用规定的寄存器完成对以下 node 的遍历以及修改。
struct node {
int *arr;
int size;
struct node *next;
};
这一节的坑点还是挺多的,主要是需要灵活的在不同场景使用不同寄存器。
Lab 5: Logisim
这一节算是给 project3 去做一个铺垫,也是终于进入了硬件部分,实验采用了 logisim 软件模拟和仿真数字电路。
Exercise 2: Sub-Circuits
需要我们只用 AND
, OR
,NOT
三种门电路实现NAND、NOR、XOR、MUX,前面几个比较简单,最后一个需要我们实现 4-to-1 Multiplexor,需要我们复用用上一个实现的 2-to-1 Multiplexor。
Exercise 3: Storing State 这一节照着示意图做就可以,让我们用寄存器和加法器去理解存储的这个中间状态。
Exercise 4: Rotate Right
这一节需要我们实现算术右移 RotRight
,根据输入的 INPUT0 ,将其整体右移 AMOUNT 位。
这里我们可以先分别实现右移1位、2位、4位、8位,再用 MUX 组合起来。
单个的 rot 可以利用 Splitters 组件简单实现 bit 交换。
Lab 6: CPU, Pipelining
Exercise 1 - Constructing Immediates
这一节需要我们生成 S型指令中的立即数,S型指令结构如下图:
最后还需要进行一个 12 位到 32 位的符号拓展:
Exercise 2 - Constructing the BrUn Control Signal
这一节需要我们根据 B型指令中的 funct3 属性判断 branch 的跳转比较是有符号还是无符号,如下图:
如果是无符号 BrUn 输出1,有符号则输出0,这里直接拿第13位进行:
官网的提示想让我们用比较器,去综合比较 funct3 的值,如下也可以:
Exercise 4 初步引入了 pipeline 的概念,如果一条指令依赖于前一条指令的输出,可以采用流水线的理念分层进行。