# 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`