# 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來修復編譯時未能捕獲的和經常發生在運行時的錯誤。
更多的信息請參考
## 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 編程:復習題
- 多線程編程:復習題
- 同步概念:復習題
- 記憶:復習題
- 管道:復習題
- 文件系統:復習題
- 網絡:復習題
- 信號:復習題
- 系統編程笑話