# 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`
- UIUC CS241 系统编程中文讲义
- 0. 简介
- #Informal 词汇表
- #Piazza:何时以及如何寻求帮助
- 编程技巧,第 1 部分
- 系统编程短篇小说和歌曲
- 1.学习 C
- C 编程,第 1 部分:简介
- C 编程,第 2 部分:文本输入和输出
- C 编程,第 3 部分:常见问题
- C 编程,第 4 部分:字符串和结构
- C 编程,第 5 部分:调试
- C 编程,复习题
- 2.进程
- 进程,第 1 部分:简介
- 分叉,第 1 部分:简介
- 分叉,第 2 部分:Fork,Exec,等等
- 进程控制,第 1 部分:使用信号等待宏
- 进程复习题
- 3.内存和分配器
- 内存,第 1 部分:堆内存简介
- 内存,第 2 部分:实现内存分配器
- 内存,第 3 部分:粉碎堆栈示例
- 内存复习题
- 4.介绍 Pthreads
- Pthreads,第 1 部分:简介
- Pthreads,第 2 部分:实践中的用法
- Pthreads,第 3 部分:并行问题(奖金)
- Pthread 复习题
- 5.同步
- 同步,第 1 部分:互斥锁
- 同步,第 2 部分:计算信号量
- 同步,第 3 部分:使用互斥锁和信号量
- 同步,第 4 部分:临界区问题
- 同步,第 5 部分:条件变量
- 同步,第 6 部分:实现障碍
- 同步,第 7 部分:读者编写器问题
- 同步,第 8 部分:环形缓冲区示例
- 同步复习题
- 6.死锁
- 死锁,第 1 部分:资源分配图
- 死锁,第 2 部分:死锁条件
- 死锁,第 3 部分:餐饮哲学家
- 死锁复习题
- 7.进程间通信&amp;调度
- 虚拟内存,第 1 部分:虚拟内存简介
- 管道,第 1 部分:管道介绍
- 管道,第 2 部分:管道编程秘密
- 文件,第 1 部分:使用文件
- 调度,第 1 部分:调度过程
- 调度,第 2 部分:调度过程:算法
- IPC 复习题
- 8.网络
- POSIX,第 1 部分:错误处理
- 网络,第 1 部分:简介
- 网络,第 2 部分:使用 getaddrinfo
- 网络,第 3 部分:构建一个简单的 TCP 客户端
- 网络,第 4 部分:构建一个简单的 TCP 服务器
- 网络,第 5 部分:关闭端口,重用端口和其他技巧
- 网络,第 6 部分:创建 UDP 服务器
- 网络,第 7 部分:非阻塞 I O,select()和 epoll
- RPC,第 1 部分:远程过程调用简介
- 网络复习题
- 9.文件系统
- 文件系统,第 1 部分:简介
- 文件系统,第 2 部分:文件是 inode(其他一切只是数据...)
- 文件系统,第 3 部分:权限
- 文件系统,第 4 部分:使用目录
- 文件系统,第 5 部分:虚拟文件系统
- 文件系统,第 6 部分:内存映射文件和共享内存
- 文件系统,第 7 部分:可扩展且可靠的文件系统
- 文件系统,第 8 部分:从 Android 设备中删除预装的恶意软件
- 文件系统,第 9 部分:磁盘块示例
- 文件系统复习题
- 10.信号
- 过程控制,第 1 部分:使用信号等待宏
- 信号,第 2 部分:待处理的信号和信号掩码
- 信号,第 3 部分:提高信号
- 信号,第 4 部分:信号
- 信号复习题
- 考试练习题
- 考试主题
- C 编程:复习题
- 多线程编程:复习题
- 同步概念:复习题
- 记忆:复习题
- 管道:复习题
- 文件系统:复习题
- 网络:复习题
- 信号:复习题
- 系统编程笑话