ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# C 编程,第 5 部分:调试 > 原文:[Processes, Part 1: Introduction](https://github.com/angrave/SystemProgramming/wiki/Processes%2C-Part-1%3A-Introduction) > 校验:[_stund](https://github.com/hqiwen) > 自豪地采用[谷歌翻译](https://translate.google.cn/) ## Hitchhiker 调试 C 程序指南 这将成为帮助您调试 C 程序的重要指南。有不同的层次的方法可以检查错误,我们将覆盖大多数方法。放松地去接受这些在调试 C 程序中有用的方法,包括但不限于调试器使用,识别常见错误类型,陷阱和有效的 Google 搜索提示。 ## 代码内调试 ### 简化代码 使用辅助函数使代码模块化。如果有重复的任务(例如,获取指向 MP2 中连续块的指针),请将它们作为辅助函数。并确保每个函数都能很好地完成一件事,这样您就不必再调试两次了。 假设我们通过查找每次迭代的最小元素来进行选择排序, ```c void selection_sort(int *a, long len){ for(long i = len-1; i > 0; --i){ long max_index = i; for(long j = len-1; j >= 0; --j){ if(a[max_index] < a[j]){ max_index = j; } } int temp = a[i]; a[i] = a[max_index]; a[max_index] = temp; } } ``` 许多人可以看到代码中的错误,但它可以帮助重构上述方法 ```c long max_index(int *a, long start, long end); void swap(int *a, long idx1, long idx2); void selection_sort(int *a, long len); ``` 并且错误特别在一个函数中。 最后,我们不是关于重构/调试代码的类 - 事实上,大多数系统代码都是如此残酷,以至于您不想阅读它。但是为了调试,从长远来看,采用某些做法可能会对您有所帮助。 ### 断言! 使用断言来确保您的代码在某一点上起作用 - 更重要的是,确保您以后不要破坏它。例如,如果您的数据结构是双向链表,您可以执行`assert(node -> size == node -> next -> prev -> size)`来断言下一个节点有一个指向当前节点的指针。您还可以检查指针指向预期的内存地址范围,而不是 `null`,`->size`是合理的等等。NDEBUG 宏将禁用所有断言,因此在完成调试后不要忘记设置它。 [http://www.cplusplus.com/reference/cassert/assert/](http://www.cplusplus.com/reference/cassert/assert/) 断言的一个简单例子,我正在使用 `memcpy` 编写代码 ```c assert(!(src < dest+n && dest < src+n)); // 检查溢出 memcpy(dest, src, n); ``` 这个检查可以在编译时关闭,但会节省你**成吨**的调试问题的时间! ### 用 printfs 当所有其他方法都失败时,打印是一个好选择!你的每个函数都应该知道它将要做什么(例如 find_min 更好地找到最小元素)。您希望测试每个函数是否正在执行它要执行的操作,并确切地查看代码中断的位置。在有竞争条件的情况下,`tsan` 可能会提供帮助,但让每个线程在特定时间打印出数据可以帮助您识别竞争条件。 ## Valgrind `Valgrind` 是一系列用来调试和收集信息的工具,可以使你的程序更加正确和发现一些运行时的问题。最有用的工具是`Memcheck`,它可以发现一些内存相关的错误,通常在c和c++ 里面常犯的、导致程序崩溃的和不能预测的行为(比如,没有释放内存缓存)。 在你的程序上使用`valgrind`: > valgrind --leak-check=yes myprogram arg1 arg2 或者 > valgrind ./myprogram 参数是可选的,默认运行工具是`memcheck`.输出将用表格的方式展示出来,包括内存的分配大小和释放内存的大小,错误的数量。 这里是一个例子来帮助你解释这些结果。假设你有一个简单的像这样的程序: ```c #include <stdlib.h> void dummy_function() { int* x = malloc(10 * sizeof(int)); x[10] = 0; // error 1:as you can see here we write to an out of bound memory address } // error 2: memory leak the allocated x not freed int main(void) { dummy_function(); return 0; } ``` 让我们看一下`Valgrind`的输出(这个程序编译和运行没有错误)。 ```log ==29515== Memcheck, a memory error detector ==29515== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al. ==29515== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info ==29515== Command: ./a ==29515== ==29515== Invalid write of size 4 ==29515== at 0x400544: dummy_function (in /home/rafi/projects/exocpp/a) ==29515== by 0x40055A: main (in /home/rafi/projects/exocpp/a) ==29515== Address 0x5203068 is 0 bytes after a block of size 40 alloc'd ==29515== at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==29515== by 0x400537: dummy_function (in /home/rafi/projects/exocpp/a) ==29515== by 0x40055A: main (in /home/rafi/projects/exocpp/a) ==29515== ==29515== ==29515== HEAP SUMMARY: ==29515== in use at exit: 40 bytes in 1 blocks ==29515== total heap usage: 1 allocs, 0 frees, 40 bytes allocated ==29515== ==29515== LEAK SUMMARY: ==29515== definitely lost: 40 bytes in 1 blocks ==29515== indirectly lost: 0 bytes in 0 blocks ==29515== possibly lost: 0 bytes in 0 blocks ==29515== still reachable: 0 bytes in 0 blocks ==29515== suppressed: 0 bytes in 0 blocks ==29515== Rerun with --leak-check=full to see details of leaked memory ==29515== ==29515== For counts of detected and suppressed errors, rerun with: -v ==29515== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0) ``` **不合格的写入**:它发现我们的堆块超过限度了(在分配块外写入) **绝对损失**:内存泄露——你可能忘记释放内存块了 Valgrind 是一个有效的工具来检查在运行时的错误。C特别存在这种问题,所以在编译你的程序之后你可以使用Valgrind来修复编译时未能捕获的和经常发生在运行时的错误。 更多的信息请参考![官网](http://valgrind.org/docs/manual/quick-start.html) ## Tsan ThreadSanitizer 是 Google 的一个工具,内置于 clang(和 gcc)中,可帮助您检测代码中的竞争条件。有关该工具的更多信息,请参阅 Github wiki。 请注意,使用 tsan 运行会降低代码的速度。 ```c #include <pthread.h> #include <stdio.h> int Global; void *Thread1(void *x) { Global++; return NULL; } int main() { pthread_t t[2]; pthread_create(&t[0], NULL, Thread1, NULL); Global = 100; pthread_join(t[0], NULL); } // compile with gcc -fsanitize=thread -pie -fPIC -ltsan -g simple_race.c ``` 我们可以看到变量 Global 存在竞争条件。主线程和使用 pthread_create 创建的线程都会尝试同时更改该值。但是,ThreadSantizer 能抓住它吗? ``` $ ./a.out ================== WARNING: ThreadSanitizer: data race (pid=28888) Read of size 4 at 0x7f73ed91c078 by thread T1: #0 Thread1 /home/zmick2/simple_race.c:7 (exe+0x000000000a50) #1 :0 (libtsan.so.0+0x00000001b459) Previous write of size 4 at 0x7f73ed91c078 by main thread: #0 main /home/zmick2/simple_race.c:14 (exe+0x000000000ac8) Thread T1 (tid=28889, running) created by main thread at: #0 :0 (libtsan.so.0+0x00000001f6ab) #1 main /home/zmick2/simple_race.c:13 (exe+0x000000000ab8) SUMMARY: ThreadSanitizer: data race /home/zmick2/simple_race.c:7 Thread1 ================== ThreadSanitizer: reported 1 warnings ``` 如果我们用 debug 标志编译,那么它也会给我们变量名。 ## GDB 简介: [http://www.cs.cmu.edu/~gilpin/tutorial/](http://www.cs.cmu.edu/%7Egilpin/tutorial/) ### 以编程方式设置断点 使用 GDB 调试复杂的 C 程序时,一个非常有用的技巧是在源代码中设置断点。 ```c int main() { int val = 1; val = 42; asm("int $3"); // 在这里设置一个断点 val = 7; } ``` ```source-shell $ gcc main.c -g -o main && ./main (gdb) r [...] Program received signal SIGTRAP, Trace/breakpoint trap. main () at main.c:6 6 val = 7; (gdb) p val $1 = 42 ``` #### 检查内存内容 [http://www.delorie.com/gnu/docs/gdb/gdb_56.html](http://www.delorie.com/gnu/docs/gdb/gdb_56.html) 例如, ```c int main() { char bad_string[3] = {'C', 'a', 't'}; printf("%s", bad_string); } ``` ```source-shell $ gcc main.c -g -o main && ./main $ Cat ZVQ�� $ ``` ```source-shell (gdb) l 1 #include <stdio.h> 2 int main() { 3 char bad_string[3] = {'C', 'a', 't'}; 4 printf("%s", bad_string); 5 } (gdb) b 4 Breakpoint 1 at 0x100000f57: file main.c, line 4. (gdb) r [...] Breakpoint 1, main () at main.c:4 4 printf("%s", bad_string); (gdb) x/16xb bad_string 0x7fff5fbff9cd: 0x63 0x61 0x74 0xe0 0xf9 0xbf 0x5f 0xff 0x7fff5fbff9d5: 0x7f 0x00 0x00 0xfd 0xb5 0x23 0x89 0xff (gdb) ``` 这里,通过使用带有参数`16xb`的`x`命令,我们可以看到从内存地址`0x7fff5fbff9c`(`bad_string`的值)开始,printf 实际上会将以下字节序列视为字符串,因为我们提供了格式错误的字符串,没有空终结符。 `0x43 0x61 0x74 0xe0 0xf9 0xbf 0x5f 0xff 0x7f 0x00`