Skip to content

Files

Latest commit

 

History

History
257 lines (191 loc) · 9.04 KB

12.md

File metadata and controls

257 lines (191 loc) · 9.04 KB

C 编程,第 5 部分:调试

原文:Processes, Part 1: Introduction

校验:_stund

自豪地采用谷歌翻译

Hitchhiker 调试 C 程序指南

这将成为帮助您调试 C 程序的重要指南。有不同的层次的方法可以检查错误,我们将覆盖大多数方法。放松地去接受这些在调试 C 程序中有用的方法,包括但不限于调试器使用,识别常见错误类型,陷阱和有效的 Google 搜索提示。

代码内调试

简化代码

使用辅助函数使代码模块化。如果有重复的任务(例如,获取指向 MP2 中连续块的指针),请将它们作为辅助函数。并确保每个函数都能很好地完成一件事,这样您就不必再调试两次了。

假设我们通过查找每次迭代的最小元素来进行选择排序,

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;
     }

}

许多人可以看到代码中的错误,但它可以帮助重构上述方法

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/

断言的一个简单例子,我正在使用 memcpy 编写代码

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.输出将用表格的方式展示出来,包括内存的分配大小和释放内存的大小,错误的数量。

这里是一个例子来帮助你解释这些结果。假设你有一个简单的像这样的程序:

#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的输出(这个程序编译和运行没有错误)。

==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来修复编译时未能捕获的和经常发生在运行时的错误。

更多的信息请参考官网

Tsan

ThreadSanitizer 是 Google 的一个工具,内置于 clang(和 gcc)中,可帮助您检测代码中的竞争条件。有关该工具的更多信息,请参阅 Github wiki。

请注意,使用 tsan 运行会降低代码的速度。

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

以编程方式设置断点

使用 GDB 调试复杂的 C 程序时,一个非常有用的技巧是在源代码中设置断点。

int main() {
    int val = 1;
    val = 42;
    asm("int $3"); // 在这里设置一个断点
    val = 7;
}
$ 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

例如,

int main() {
    char bad_string[3] = {'C', 'a', 't'};
    printf("%s", bad_string);
}
$ gcc main.c -g -o main && ./main
$ Cat ZVQ�� $
(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)

这里,通过使用带有参数16xbx命令,我们可以看到从内存地址0x7fff5fbff9cbad_string的值)开始,printf 实际上会将以下字节序列视为字符串,因为我们提供了格式错误的字符串,没有空终结符。

0x43 0x61 0x74 0xe0 0xf9 0xbf 0x5f 0xff 0x7f 0x00