实验2:C语言调试

截止日期:2月5日星期四晚上11:59:59 PT

在本实验中,请按列出的顺序完成练习。练习之间可能相互依赖。

实验幻灯片

设置

您必须在hive机器上完成此实验。有关使用它们的复习,请参阅实验0

在您的labs目录中,使用以下命令拉取本实验的文件:

git pull starter main

如果您收到如下错误:

fatal: 'starter' does not appear to be a git repository
fatal: Could not read from remote repository.

请按如下方式设置starter远程仓库:

git remote add starter https://github.com/61c-teach/sp26-lab-starter.git

然后再次运行原始命令。

如果您遇到任何git错误,请查看常见错误页面。


练习1:编译器警告和错误

编译器警告用于帮助您发现代码中的潜在错误。在尝试运行代码之前,请确保修复所有编译器警告。这将为您节省大量未来调试时间,因为修复编译器警告比自己查找错误要快得多。

  1. 阅读 ex1_compiler_warnings.c中的代码。

  2. 编译您的程序:gcc -o ex1_compiler_warnings ex1_compiler_warnings.c。您应该看到3个警告。

  3. 阅读第一个警告的第一行。该行以ex1_compiler_warnings.c:13:22开头,表示警告是由ex1_compiler_warnings.c的第13行引起的。警告指出程序试图将char赋值给char *

  4. 打开 ex1_compiler_warnings.c定位到引起警告的行。它试图将char赋值给char *。编译器指出这是一个潜在错误,因为我们不应该将char赋值给char *

  5. 修复此编译器警告。

  6. 重新编译您的代码。现在您可以看到此警告不再出现,还剩下2个警告。

  7. 修复 ex1_compiler_warnings.c中剩余的编译器警告。您的代码现在应该可以正常工作了。

什么是GDB?

以下是来自GDB官方网站的摘录:

GDB(GNU项目调试器)允许您在程序执行时查看其"内部"发生的情况——或者在程序崩溃时查看其当时正在做什么。

GDB可以做四类主要事情(以及支持这些事情的其他操作)来帮助您捕获错误:

  • 启动程序,指定可能影响其行为的任何参数。
  • 使程序在指定条件下停止。
  • 检查程序停止时发生了什么。
  • 更改程序中的内容,以便您可以尝试纠正一个错误的影响并继续了解另一个错误。

在本课程中,我们将使用CGDB,它为gdb提供了一个轻量级界面,使其更易于使用。CGDB已安装在hive机器上,因此无需安装。本课程的其余部分将交替使用CGDB和GDB。

这是一份GDB参考卡片

如果您在使用GDB时遇到任何问题,请参阅下面的常见GDB错误部分

练习2:GDB入门

在本节中,您将学习GDB命令startstepnextfinishprintquit。本节将在过程中解决错误。请确保在继续之前修复代码中的错误。

下表是上述命令的摘要

命令缩写描述
startstart开始运行程序并在main的第1行停止
steps执行当前代码行(此命令将进入函数)
nextn执行当前代码行(此命令不会进入函数)
finishfin执行当前函数的剩余部分并返回到调用函数
print [参数](例如:对于int n=3,print n将输出3)p打印参数的值
quitq退出gdb

您应该在ex2_commands.txt中填写相应的命令。请仅使用上表中的命令。为确保正确性,我们将检查您的ex2_commands.txt的输出是否符合预期输出。建议打开两个SSH窗口,以便同时查看命令文件和cgdb会话。即使您正在向ex2_commands.txt添加内容,也请通过在cgdb中实际运行这些命令来检查您的工作。

  1. 编译您的程序,使用-g标志。这将在可执行文件中包含CGDB所需的额外调试信息。

    gcc -g -o pwd_checker test_pwd_checker.c pwd_checker.c
    
  2. 启动 cgdb。请注意,您应该使用可执行文件(pwd_checker)作为参数,而不是源文件(pwd_checker.c)。

    cgdb pwd_checker
    

    现在您应该看到CGDB已打开。顶部窗口显示我们的代码,底部窗口显示控制台。

对于以下每个步骤,将您执行的CGDB命令添加到ex2_commands.txt中。每个命令应单独一行。下面的每个步骤将需要一个或多个CGDB命令。

  1. 启动您的程序,使其位于main的第一行,使用一个命令。

  2. main中的第一行是对printf的调用。我们不想进入此函数。跳过程序中的这一行。

  3. 逐步执行直到程序位于check_password调用处。请注意,箭头旁边的行是我们当前所在的行,但尚未执行。

  4. 进入 check_password

  5. 进入 check_lower

  6. 打印 password的值(password是一个字符串)。

  7. 立即退出 check_lower。不要逐步执行直到函数返回。

  8. 进入 check_length

  9. 逐步执行到函数的最后一行。

  10. 打印函数的返回值。返回值应该是false

  11. 打印 length的值。看起来length是正确的,所以第24行一定有一些逻辑问题。

  12. 退出 CGDB。CGDB可能会询问您是否要退出,输入y(但不要将y添加到ex2_commands.txt)。

此时,您的ex2_commands.txt应包含上述步骤中的命令列表。您不需要将以下步骤中的任何内容添加到ex2_commands.txt中。

  1. 修复第24行的错误。

  2. 编译运行您的代码。

  3. 程序仍然失败。再次打开并逐步执行cgdb,您应该看到check_number现在失败了。我们将在下一个练习中解决这个问题。

练习3:更多GDB

在本节中,您将学习gdb命令breakconditional breakruncontinue。本节将在过程中解决错误。请确保在继续之前修复代码中的错误。

下表是上述命令的摘要

命令缩写描述
break [行号或函数名]b在指定位置设置断点,使用filename.c:linenum在特定文件中设置断点
条件断点
(例如:break 3 if n==4)
(例如:b 3 if n==4)仅当满足给定条件时在指定位置设置断点
runr执行程序直到终止或到达断点
continuec继续执行已暂停的程序
backtracebt为堆栈中的每个帧打印一行

您应该在ex3_commands.txt中填写相应的命令。请仅使用上表和练习2中的命令。为确保正确性,我们将检查您的ex3_commands.txt的输出是否符合预期输出。建议打开两个SSH窗口,以便同时查看命令文件和cgdb会话。即使您正在向ex3_commands.txt添加内容,也请通过在cgdb中实际运行这些命令来检查您的工作。

  1. 重新编译并运行您的代码。您应该看到断言number失败

  2. 启动 cgdb

    cgdb pwd_checker
    

对于以下每个步骤,将您执行的CGDB命令添加到ex3_commands.txt中。每个命令应单独一行。下面的每个步骤将需要一个或多个CGDB命令。

  1. 设置断点,使用函数名(不是文件名或行号)直接跳转到check_number函数。您的断点不应在check_password中。

  2. 运行程序。 您的代码应运行到我们刚刚设置的断点处停止。

  3. 进入 check_range

  4. 让我们看看我们是如何到达这里的。显示程序的backtrace(回溯)。

  5. 回想一下,数字直到密码的后面才出现。与其逐步执行密码开头的所有非数字字符,我们可以使用条件断点直接跳转到代码中比较数字的位置。条件断点只会在满足给定条件时停止程序。密码中的第一个数字是0,因此我们可以在letter'0'时设置断点。letter'0'时在第31行设置断点

    我们使用单引号是因为0是一个字符。

  6. 在断点处停止后继续执行您的代码。

  7. 代码已在条件断点处停止。为验证这一点,打印 letter

    它应该打印48 '0',这是一个十进制数后跟其对应的ASCII表示。如果您查看ASCII表,您可以看到48是字符0的十进制表示。

  8. 让我们看看check_range的返回值。打印 is_in_range。结果是false。这很奇怪。'0'应该在范围内。

  9. 让我们看看范围的上下限。打印 lower

  10. 打印 upper

  11. 啊哈!lower的ASCII表示是\000(空终止符),upper的ASCII表示是\t。看起来我们传入的是数字09而不是字符'0''9'

  12. 退出 CGDB。CGDB可能会询问您是否要退出,输入y(但不要将y添加到ex3_commands.txt)。

此时,您的ex3_commands.txt应包含上述步骤中的命令列表。您不需要将以下步骤中的任何内容添加到ex3_commands.txt中。

  1. 修复错误。

  2. 编译运行您的代码。还有一个错误,您将在练习4中找到。

练习4:调试

  1. 调试 check_upper,使用您刚刚学到的命令自己完成。尽管有大写字母,该函数似乎仍返回false。提示:错误本身可能不在check_upper中。

  2. 修复错误。

练习5:调试段错误

GDB可以做的一件非常重要的事情是调试段错误。虽然不使用这些GDB工具也可以完成此练习,但习惯使用它对实验室未来的问题以及项目1非常有帮助。尽最大努力按照说明操作,并使用GDB终端在不查看源代码的情况下找出答案。

在本练习中,您应该填写ex5_answers.txt

  1. 编译 ex5_segfault.c。注意没有编译器错误或警告,并且我们使用-g标志以防将来需要调试此程序。

    gcc -g -o ex5_segfault ex5_segfault.c
    
  2. 运行 ex5_segfault。程序应该会因段错误而崩溃。

  3. ex5_segfault运行 cgdb,并run直到出现段错误,然后显示程序的backtrace

  4. 仔细阅读输出,并在ex5_answers.txt中回答以下问题。请不要更改文件的格式。

    1. 段错误发生在哪个函数中?(答案应该是函数名称)
    2. 导致段错误的行号是多少?(答案应该是一个没有任何单位的数字)

Valgrind

即使使用调试器,我们也可能无法捕获所有错误。有些错误我们称之为"玻尔错误"(bohrbugs),意思是它们在一组定义良好但可能未知的条件下可靠地表现出来。其他错误我们称之为"海森堡错误"(heisenbugs),它们不是确定性的,当人们试图研究它们时,它们会消失或改变行为。我们可以用调试器检测第一种错误,但第二种错误可能会逃过我们的注意,因为它们(至少在C语言中)通常是由于内存管理不当造成的。请记住,与其他编程语言不同,C语言要求您(程序员)手动管理内存。

我们可以使用一个名为Valgrind的工具来帮助捕获"海森堡错误"和"玻尔错误"。Valgrind是一个模拟CPU并跟踪内存访问的程序。这会减慢您运行的进程(这就是为什么我们不总是在Valgrind中运行所有可执行文件),但它也可以暴露那些可能只在特定情况下才表现出明显错误行为的bug。

让我们看看bork翻译程序!Bork是一种与英语非常相似的古老语言。要将一个单词翻译成Bork,您需要取英语单词并在单词的每个元音后添加一个'f'。

让我们看看是否能理解一些Bork语言。使用以下命令编译并运行bork

gcc -g -o bork bork.c
./bork hello

下面提供了一个示例输出。请注意,您的输出可能看起来不同。

Input string: "hello"
Length of translated string: 21
Translate to Bork: "hefl2?^?Ul2?^?Uof?^?U"

嗯,Bork是一种古老的语言,但不应该有这些奇怪的字符。看来古人可能在他们的程序中留下了一些bug!我们要不要踏上一段修复bug的旅程,揭开Bork语言的真正魅力?

如果我们简要看一下main,我们可以看到我们正在取一个输入字符串(src_str)并将其翻译成Bork(dest_str)。如果我们滚动到顶部,我们可以看到我们有一个函数(alloc_str)用于在堆上为字符串分配空间,一个包含字符串及其长度的Str结构体,一个创建Str结构体并初始化其datalen字段的make_str函数,以及一个释放结构体数据的函数。还有一个将两个字符串连接在一起的函数和另一个将字母翻译成Bork的函数。这是一个相当长的程序需要调试。

如果有一个工具能给我们一个好的切入点,那不是很好吗?

事实证明,有几个这样的工具,valgrind就是其中之一!

让我们使用以下命令在我们的程序上运行valgrind

valgrind ./bork hello
==10170== Memcheck, a memory error detector
==10170== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==10170== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==10170== Command: ./bork hello
==10170==
==10170== Invalid read of size 1
==10170==    at 0x4C34D04: strlen (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==10170==    by 0x10879F: make_Str (bork.c:22)
==10170==    by 0x108978: translate_to_bork (bork.c:56)
==10170==    by 0x1089F2: main (bork.c:68)
==10170==  Address 0x522f041 is 0 bytes after a block of size 1 alloc'd
==10170==    at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==10170==    by 0x108781: alloc_str (bork.c:10)
==10170==    by 0x10895E: translate_to_bork (bork.c:54)
==10170==    by 0x1089F2: main (bork.c:68)
==10170==
==10170== Invalid read of size 1
==10170==    at 0x4C34D04: strlen (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==10170==    by 0x10879F: make_Str (bork.c:22)
==10170==    by 0x108952: translate_to_bork (bork.c:51)
==10170==    by 0x1089F2: main (bork.c:68)
==10170==  Address 0x522f0e2 is 0 bytes after a block of size 2 alloc'd
==10170==    at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==10170==    by 0x108781: alloc_str (bork.c:10)
==10170==    by 0x10892D: translate_to_bork (bork.c:48)
==10170==    by 0x1089F2: main (bork.c:68)
==10170==
Input string: "hello"
Length of translated string: 7
==10170== Invalid read of size 1
==10170==    at 0x4C34D04: strlen (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==10170==    by 0x4E9B4A2: vfprintf (vfprintf.c:1643)
==10170==    by 0x4EA2EE5: printf (printf.c:33)
==10170==    by 0x108A6F: main (bork.c:74)
==10170==  Address 0x522f317 is 0 bytes after a block of size 7 alloc'd
==10170==    at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==10170==    by 0x108781: alloc_str (bork.c:10)
==10170==    by 0x108833: concat (bork.c:32)
==10170==    by 0x108A15: main (bork.c:69)
==10170==
Translate to Bork: "hefllof"
==10170==
==10170== HEAP SUMMARY:
==10170==     in use at exit: 7 bytes in 1 blocks
==10170==   total heap usage: 11 allocs, 10 frees, 1,051 bytes allocated
==10170==
==10170== LEAK SUMMARY:
==10170==    definitely lost: 7 bytes in 1 blocks
==10170==    indirectly lost: 0 bytes in 0 blocks
==10170==      possibly lost: 0 bytes in 0 blocks
==10170==    still reachable: 0 bytes in 0 blocks
==10170==         suppressed: 0 bytes in 0 blocks
==10170== Rerun with --leak-check=full to see details of leaked memory
==10170==
==10170== For counts of detected and suppressed errors, rerun with: -v
==10170== ERROR SUMMARY: 6 errors from 3 contexts (suppressed: 0 from 0)

(有趣的旁注:当我们在此valgrind日志中查看正常程序输出时,我们看到正常行为(即打印"hefllof")。这是因为valgrind运行程序的方式与程序"自然"运行(也称为"裸机")的方式不同。我们现在暂不深入讨论这个问题。)

但回到调试:解析大型错误日志时遵循的一个很好的经验法则是只考虑第一条错误消息(忽略其余部分),所以让我们这样做:

==10170== Invalid read of size 1
==10170==    at 0x4C34D04: strlen (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==10170==    by 0x10879F: make_Str (bork.c:22)
==10170==    by 0x108978: translate_to_bork (bork.c:56)
==10170==    by 0x1089F2: main (bork.c:68)

错误消息表明我们正在进行大小为1的无效读取。这意味着什么?无效读取意味着您的程序正在读取它不应该读取的内存位置(这可能导致段错误,但并非总是如此)。大小1意味着我们试图读取1个字节。

由于我们不熟悉这个古老的代码库,并且不想阅读所有代码来找到bug,一个很好的方法是从高级细节开始,逐步深入(基本上就是通过valgrind提供的调用栈来工作)。

让我们看看main中的bork.c第68行(栈的底部):

Str bork_substr = translate_to_bork(src_str.data[i]);

这里有什么奇怪的事情发生吗?看起来我们只是将一个字符传递给translate_to_bork。到目前为止似乎没问题。

让我们沿着调用栈继续深入,看看translate_to_bork中的bork.c第56行:

return make_Str(res);

我们只是在这里调用make_Str。我们应该深入一点。让我们看看bork.c第22行。

return (Str){.data=str,.len=strlen(str)};

这里我们正在创建一个新的Str结构体并设置其datalen参数。这看起来也很正常!

但是valgrindstrlen正在进行无效读取?

嗯,我们正在向它传递一个字符串,对吧?strlen是做什么的来着?它通过遍历每个字符直到遇到空终止符来确定字符串的长度。可能没有空终止符,所以strlen会继续越过字符串的末尾(这意味着它越过了我们为字符串分配的区域)。

让我们通过检查字符串的创建位置来确保它有一个空终止符。

之前,我们在translate_to_bork的第56行看到了这一点。

return make_Str(res);

如果我们向上看两行(第54行),我们可以看到我们通过调用alloc_str为字符串分配空间。让我们看看这个函数。

char *alloc_str(int len) {
    return malloc(len*sizeof(char));
}

嗯。看起来alloc_str只给了我们len大小的内存,这意味着当我们在translate_to_bork中写入字符串时,我们没有足够的空间来存放空终止符!

让我们做以下修改来修复这个问题:

10c10,12
<     return malloc(len*sizeof(char));
---
>     char *data = malloc((len+1)*sizeof(char));
>     data[len] = '\0';
>     return data;

Let's run our program to see if we fixed the problem

./bork hello
Input string: "hello"
Length of translated string: 7
Translate to Bork: "hefllof"

一切看起来都正常工作。但是,可能存在我们无法看到的隐藏错误,所以让我们通过valgrind运行我们的代码,确保没有潜在的问题。

valgrind ./bork hello
==29797== Memcheck, a memory error detector
==29797== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==29797== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==29797== Command: ./bork hello
==29797==
Input string: "hello"
Length of translated string: 7
Translate to Bork: "hefllof"
==29797==
==29797== HEAP SUMMARY:
==29797==     in use at exit: 8 bytes in 1 blocks
==29797==   total heap usage: 11 allocs, 10 frees, 1,061 bytes allocated
==29797==
==29797== LEAK SUMMARY:
==29797==    definitely lost: 8 bytes in 1 blocks
==29797==    indirectly lost: 0 bytes in 0 blocks
==29797==      possibly lost: 0 bytes in 0 blocks
==29797==    still reachable: 0 bytes in 0 blocks
==29797==         suppressed: 0 bytes in 0 blocks
==29797== Rerun with --leak-check=full to see details of leaked memory
==29797==
==29797== For counts of detected and suppressed errors, rerun with: -v
==29797== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

让我们看看下面的堆摘要。它告诉我们退出时有8字节在1个块中被分配。这意味着堆中未释放的内存来自一次分配调用,大小为8字节。

接下来,我们可以看到堆摘要,它显示我们在程序生命周期内进行了11次分配调用和10次释放。

==29797== HEAP SUMMARY:
==29797==     in use at exit: 8 bytes in 1 blocks
==29797==   total heap usage: 11 allocs, 10 frees, 1,061 bytes allocated

现在让我们看看下面的泄漏摘要。它指出我们在1个块中丢失了8字节。

==29797== LEAK SUMMARY:
==29797==    definitely lost: 8 bytes in 1 blocks
==29797==    indirectly lost: 0 bytes in 0 blocks
==29797==      possibly lost: 0 bytes in 0 blocks
==29797==    still reachable: 0 bytes in 0 blocks
==29797==         suppressed: 0 bytes in 0 blocks
==29797== Rerun with --leak-check=full to see details of leaked memory

它告诉我们"使用--leak-check=full重新运行以查看泄漏内存的详细信息",所以让我们这样做。

valgrind --leak-check=full ./bork hello
==32334== Memcheck, a memory error detector
==32334== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==32334== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==32334== Command: ./bork hello
==32334==
Input string: "hello"
Length of translated string: 7
Translate to Bork: "hefllof"
==32334==
==32334== HEAP SUMMARY:
==32334==     in use at exit: 8 bytes in 1 blocks
==32334==   total heap usage: 11 allocs, 10 frees, 1,061 bytes allocated
==32334==
==32334== 8 bytes in 1 blocks are definitely lost in loss record 1 of 1
==32334==    at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==32334==    by 0x108784: alloc_str (in /home/cc/cs61c/fa22/staff/cs61c-tac/bork)
==32334==    by 0x10884E: concat (in /home/cc/cs61c/fa22/staff/cs61c-tac/bork)
==32334==    by 0x108A30: main (in /home/cc/cs61c/fa22/staff/cs61c-tac/bork)
==32334==
==32334== LEAK SUMMARY:
==32334==    definitely lost: 8 bytes in 1 blocks
==32334==    indirectly lost: 0 bytes in 0 blocks
==32334==      possibly lost: 0 bytes in 0 blocks
==32334==    still reachable: 0 bytes in 0 blocks
==32334==         suppressed: 0 bytes in 0 blocks
==32334==
==32334== For counts of detected and suppressed errors, rerun with: -v
==32334== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

现在Valgrind告诉我们未释放块最初分配的位置。让我们看看下面。如果我们跟踪调用栈,我们可以看到malloc是由alloc_str调用的,而alloc_str是由main中的concat调用的。

==32334== 8 bytes in 1 blocks are definitely lost in loss record 1 of 1
==32334==    at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==32334==    by 0x108784: alloc_str (in /home/cc/cs61c/fa22/staff/cs61c-tac/bork)
==32334==    by 0x10884E: concat (in /home/cc/cs61c/fa22/staff/cs61c-tac/bork)
==32334==    by 0x108A30: main (in /home/cc/cs61c/fa22/staff/cs61c-tac/bork)

如果我们查看main,我们可以看到我们通过调用concatdest_str分配空间,但我们从未释放它。我们需要dest_str直到程序结束,所以让我们在从main返回之前释放它。这个结构体是在main的栈上分配的(Str dest_str={};),所以我们不需要释放结构体本身。但是,结构体指向的数据是在堆上分配的。因此,我们只需要释放结构体的这部分。如果你查看程序顶部附近,我们已经提供了一个free_Str函数来释放结构体的已分配部分。让我们在程序末尾调用这个函数。

76a77
>     free_Str(dest_str);

您可能想知道为什么我们不释放src_str。如果我们看看我们在哪里构造了src_strStr src_str = make_Str(argv[1]);),我们可以看到它是使用make_str创建的,它不会调用任何堆空间分配。我们用来创建src_str的字符串来自argv[1]。调用main的程序负责设置argv[1],所以我们不必担心它。

修复错误后,valgrind输出应该如下所示。堆摘要显示退出时没有分配任何块。底部的错误摘要显示没有错误要报告。

valgrind ./bork hello
==10835== Memcheck, a memory error detector
==10835== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==10835== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==10835== Command: ./bork hello
==10835==
Input string: "hello"
Length of translated string: 7
Translate to Bork: "hefllof"
==10835==
==10835== HEAP SUMMARY:
==10835==     in use at exit: 0 bytes in 0 blocks
==10835==   total heap usage: 11 allocs, 11 frees, 1,061 bytes allocated
==10835==
==10835== All heap blocks were freed -- no leaks are possible
==10835==
==10835== For counts of detected and suppressed errors, rerun with: -v
==10835== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

练习6:使用Valgrind查找段错误

上面,我们学习了如何使用GDB调试段错误。现在,我们将使用valgrind做类似的事情。填写此valgrind测验,包含7个valgrind内存错误输出示例,对应7个bug。

通常,要使用valgrind,您需要使用-g标志编译以获取调试信息(即gcc -g -o example example.c)。然后,在编译后的可执行文件上运行valgrind(即valgrind ./example)。练习6的valgrind输出已在表单上提供。

您可以在截止日期前无限次重新参加测验以达到100%;测验应该会即时反馈正确/错误答案。

练习7:内存管理

本练习是可选的。但是,本练习中探讨的概念对于项目1及以后非常重要,因此我们强烈建议您完成此练习以获得经验。

本练习使用ex7_vector.hex7_test_vector.cex7_vector.c,我们为您提供了一个实现变长数组的框架。本练习旨在帮助您熟悉C语言中的结构体和内存管理。

  1. 尝试解释为什么bad_vector_new()是不好的。我们在这里提供了原因,您可以验证您的理解

    bad_vector_new() vector是在栈上创建的,而不是堆上。栈上存储的所有内存在函数运行完毕后立即被释放,因此当函数返回时,我们会丢失我们构造的vector。
  2. 填写 ex7_vector.c中的函数vector_new()vector_delete(),使我们的测试代码ex6_test_vector.c能够无内存管理错误地运行。

    代码中的注释描述了函数应该如何工作。查看我们已填写的函数,了解数据结构应该如何使用。为了保持一致性,假设vector中的所有条目都是0,除非由用户设置。请记住,malloc()不会将其分配的内存清零。

  3. 测试您的vector_new()vector_delete()实现是否正确。

    gcc -g -o ex7_vector ex7_vector.c ex7_test_vector.c
    ./ex7_vector
    
  4. 测试您的vector_new()vector_delete()实现的内存管理。

    valgrind ./ex7_vector
    

任何数量的抑制错误都没关系;它们不会影响我们。

也可以使用CGDB来调试您的代码。

练习8:调试内存泄漏

Valgrind非常擅长帮助我们查找和修复内存泄漏。当我们未能释放已分配的内存时,就会遇到内存泄漏。这很糟糕,因为如果我们不释放已分配的内存,最终会耗尽内存。

在本练习中,您将使用Valgrind调试ex8_double_pointers.c(在继续本练习之前,请确保您已阅读上面的Valgrind部分)。

  1. 编译 ex8_double_pointers.c(记得使用-g标志编译,以便您可以使用Valgrind)。

  2. 运行 valgrind。您应该看到我们有4次分配和3次释放。您还应该看到堆中未释放的内存来自一个16字节大小的分配调用(作为参考,Student结构体是16字节大小)。

  3. 接下来,运行 valgrind,但使用--leak-check=full标志。您应该看到在main的第31行调用malloc时丢失了16个块。

  4. 修复内存泄漏错误。

  5. 编译并运行程序,再次使用valgrind。堆摘要应该显示退出时没有分配任何块。底部的错误摘要也应该显示没有错误要报告。

练习9:反思和反馈表

我们每周都在努力改进课程 - 请填写此调查,告诉我们您到目前为止在CS 61C中的学习体验!


提交

保存、提交并推送您的工作,然后在Gradescope上提交实验2作业。


常见GDB错误

GDB跳过代码行

这可能意味着您的源文件比可执行文件更新。退出GDB,使用-g标志重新编译代码,然后重新启动gdb。

GDB无法加载我的文件

您可能会看到如下错误:"not in executable format: file format not recognized"(不是可执行格式:无法识别文件格式)或"No symbol table loaded. Use the "file" command."(未加载符号表,请使用"file"命令)。

这意味着您在源文件(以.c结尾的文件)上调用了gdb,而不是可执行文件。退出GDB并确保您使用可执行文件调用它。

如何在代码窗口和控制台之间切换?

CGDB提供了一个类似vim的导航界面:按键盘上的i键从代码窗口切换到控制台。按Esc键从控制台切换到代码窗口。

GDB提供了一个类似readline/emacs的导航界面:按Ctrl + X然后O在窗口之间切换。

我卡在代码窗口中

按键盘上的i键。这应该能让您回到控制台。

文本UI乱码

Ctrl + l刷新GDB文本UI。


命令:info locals

打印当前栈帧中所有局部变量的值

命令:command

每次到达断点时执行一系列命令。例如:

设置断点:

b 73

输入commands后跟断点编号:

commands 1

输入要执行的命令列表,用换行符分隔。在命令列表之后,输入end并按Enter键。

p var1
p var2
end