校验:_stund
自豪地采用谷歌翻译
这将成为帮助您调试 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);
这个检查可以在编译时关闭,但会节省你成吨的调试问题的时间!
当所有其他方法都失败时,打印是一个好选择!你的每个函数都应该知道它将要做什么(例如 find_min 更好地找到最小元素)。您希望测试每个函数是否正在执行它要执行的操作,并确切地查看代码中断的位置。在有竞争条件的情况下,tsan
可能会提供帮助,但让每个线程在特定时间打印出数据可以帮助您识别竞争条件。
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来修复编译时未能捕获的和经常发生在运行时的错误。
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 标志编译,那么它也会给我们变量名。
简介: 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)
这里,通过使用带有参数16xb
的x
命令,我们可以看到从内存地址0x7fff5fbff9c
(bad_string
的值)开始,printf 实际上会将以下字节序列视为字符串,因为我们提供了格式错误的字符串,没有空终结符。
0x43 0x61 0x74 0xe0 0xf9 0xbf 0x5f 0xff 0x7f 0x00