Linux c c++ 開發調試技巧
看到一篇介紹 linux c/c++ 開發調試技巧的文章,感覺挺使用,哪來和大家分享。
通向 UNIX 天堂的 10 個階梯
Author: Arpan Sen, 高級技術人員, Systems Documentation, Inc. (SDI)
討論幾種可以幫助 C++ 開發人員節省時間的技巧和免費工具。
C++ 開發人員在日常工作中通常要完成多個任務:開發新軟件、調試其他人的代碼、制訂測試計劃、為每個計劃開發測試、管理衰退軟件(regression suite)等等。在多種角色之間頻繁轉換會消耗大量寶貴的時間。為了幫助緩解這個問題,本文提供 10 種能夠有效提高生產力的方法。本文中的示例使用 tcsh version 6,但是提供的思想適用于所有 UNIX? shell 變體。本文還介紹可以在 UNIX 平臺上使用的幾種開放源碼工具。
保證數據安全
在 shell 提示上使用 rm -rf * 或其變體可能是導致 UNIX 開發人員丟失數小時工作成果的最常見的原因。有幾種方法可以解決這個問題:通過在 $HOME/.aliases 中使用 alias rm、alias cp 或alias mv,把這些命令連接到它們的交互式版本,然后在系統啟動時引用這個文件。根據登錄 shell的不同,這意味著把 source $HOME/.aliases 放在 .cshrc(對于 C/tcsh shell)或 .profile 或.bash_profile(對于 Bourne shell)中,見清單 1。
清單 1. 為 rm、cp 和 mv 設置別名
alias rm 'rm –i'
alias cp 'cp –i'
alias mv 'mv –i'
對于 tcsh shell 的用戶還有另一種方法,在啟動腳本中添加以下行:
set rmstar on
在設置了 rmstar 變量的情況下,如果發出 rm * 命令,系統會提示您確認,見清單 2。
清單 2. 在 tcsh 中使用 rmstar shell 變量
arpan@tintin# pwd
/home/arpan/IBM/documents
arpan@tintin# set rmstar on
arpan@tintin# rm *
Do you really want to delete all files? [n/y] n
但是,如果使用帶 -f 選項的 rm、cp 或 mv 命令,那么會抑制交互模式。更有效的一種方法是,為這些 UNIX 命令創建自己的版本,并使用 $HOME/.recycle_bin 這樣的預定義文件夾保存刪除的數據。清單 3 給出一個稱為 saferm 的示例腳本,它只接受文件和文件夾名。
清單 3. saferm 腳本
#!/usr/bin/tcsh
if (! -d ~/.recycle_bin) then
mkdir ~/.recycle_bin
endif
mv $1 ~/.recycle_bin/$1.`date +%F`
自動備份數據
恢復數據需要全面的策略和措施。根據需求的不同,數據備份可以在每天夜間執行,也可以每隔幾小時執行一次。在默認情況下,應該使用 cron 工具備份 $HOME 及其所有子目錄,并把備份保存在預先指定的文件系統區域中。注意,應該只有系統管理員對備份數據有 讀(Write) 或 執行(Execute)的權限。下面的 cron 腳本簡要演示備份的設置:
0 20 * * * /home/tintin/bin/databackup.csh
這個腳本在每天 20:00 備份數據。數據備份腳本見清單 4。
清單 4. 數據備份腳本
cd /home/tintin/database/src
tar cvzf database-src.tgz.`date +%F` database/ main/ sql-processor/
mv database-src.tgz.`date +%F` ~/.backup/
chmod 000 ~/.backup/database-src.tgz.`date +%F`
另一種策略是在網絡中維護一些名稱簡單明了的文件系統區域,比如/backup_area1、/backup_area2 等等。希望備份數據的開發人員應該在這些區域中創建目錄或文件。另外,一定要注意一點:與 /tmp 相似,這種區域必須打開 sticky 位。
瀏覽源代碼
使用可免費下載的 cscope 實用程序(見 參考資料 中的鏈接)是發現和瀏覽現有源代碼的好方法。cscope 需要一個文件列表(C 或 C++ 頭文件、源代碼文件、flex 和 bison 文件、內聯源代碼[.inl] 文件等等),以便創建它自己的數據庫。創建數據庫之后,它會在一個簡潔的界面中列出源代碼。清單 5 演示如何構建并調用 cscope 數據庫。
清單 5. 使用 cscope 構建并調用源代碼數據庫
arpan@tintin# find . –name “*.[chyli]*” > cscope.files
arpan@tintin# cscope –b –q –k
arpan@tintin# cscope –d
cscope 的 -b 選項讓它創建內部數據庫;-q 選項讓它創建索引文件以加快搜索;-k 選項讓 cscope在搜索時不考慮系統頭文件(否則,即使是最簡單的搜索,也會產生大量結果)。
使用 -d 選項調用 cscope 界面,見清單 6。
清單 6. cscope 界面
Cscope version 15.5 Press the ? key for help
Find this C symbol:
Find this global definition:
Find functions called by this function:
Find functions calling this function:
Find this text string:
Change this text string:
Find this egrep pattern:
Find this file:
Find files #including this file:
按 Ctrl-D 退出 cscope。使用 Tab 鍵在數據 cscope 列表和 cscope 選項(例如,Find C Symbol和 Find file)之間切換。清單 7 給出在搜索名稱包含 database 的文件時的屏幕快照。按 0、1等分別查看各個文件。
清單 7. 在搜索名為 database 的文件時 cscope 的輸出
Cscope version 15.5 Press the ? key for help
File
0 database.cpp
1.database.h
2.databasecomponents.cpp
3.databasecomponents.h
Find this C symbol:
Find this global definition:
Find functions called by this function:
Find functions calling this function:
Find this text string:
Change this text string:
Find this egrep pattern:
Find this file:
Find files #including this file:
用 doxygen 調試遺留代碼
要想有效地調試別人開發的代碼,就要花時間了解現有軟件的總體結構 — 類及其層次結構,全局變量和靜態變量,公共接口例程。在從現有的源代碼中提取類層次結構方面,GNU 實用程序doxygen(見 參考資料 中的鏈接)可能是最好的工具。
要想在項目上運行 doxygen,首先需要在 shell 提示上運行 doxygen -g。這個命令在當前工作目錄中生成一個名為 Doxyfile 的文件,必須手工編輯此文件。編輯之后,對 Doxyfile 再次運行doxygen。清單 8 給出一個運行示例。
清單 8. 運行 doxygen
arpan@tintin# doxygen -g
arpan@tintin# ls
Doxyfile
… [after editing Doxyfile]
arpan@tintin# doxygen Doxyfile
您需要理解 Doxyfile 中的幾個字段。比較重要的字段是:
* OUTPUT_DIRECTORY。保存生成的文檔文件的目錄。
* INPUT。這是一個以空格分隔的列表,其中包含必須為其生成文檔的所有源代碼文件和文件夾。
* RECURSIVE。如果源代碼列表是層次化的,就把這個字段設置為 YES。這樣就不必在 INPUT 中指定所有文件夾,只需在 INPUT 中指定頂層文件夾并把這個字段設置為 YES。
* EXTRACT_ALL。這個字段必須設置為 YES,這告訴 doxygen 應該從沒有文檔的所有類和函數中提取文檔。
* EXTRACT_PRIVATE。這個字段必須設置為 YES,這告訴 doxygen 應該在文檔中包含類的私有數據成員。
* FILE_PATTERNS。除非項目沒有采用一般的 C 或 C++ 源代碼文件擴展名,比如.c、.cpp、.cc、.cxx、.h 或 .hpp,否則不需要在這個字段中添加設置。
注意:Doxyfile 中必須研究的其他字段取決于項目需求和所需的文檔細節。清單 9 給出一個示例Doxyfile。
清單 9. 示例 Doxyfile
OUTPUT_DIRECTORY = /home/tintin/database/docs
INPUT = /home/tintin/project/database
FILE_PATTERNS =
RECURSIVE = yes
EXTRACT_ALL = yes
EXTRACT_PRIVATE = yes
EXTRACT_STATIC = yes
使用 STL 和 gdb
在使用 C++ 開發的軟件中,最復雜的部分常常使用 C Standard Template Library (STL) 中的類。但糟糕的是,調試包含許多 STL 類的代碼并不容易,GNU Debugger (gdb) 常常指出缺少信息,無法顯示相關數據,甚至可能崩潰。為了解決這個問題,可以使用 gdb 的一個高級特性 — 添加用戶定義的命令。例如,請考慮一下清單 10 中的代碼片段,這段代碼使用一個向量并顯示信息。
清單 10. 在 C++ 代碼中使用 STL 向量
#include <vector>
#include <iostream>
using namespace std;
int main ()
{
vector<int> V;
V.push_back(9);
V.push_back(8);
for (int i=0; i < V.size(); i++)
cout << V[i] << "\n";
return 0;
}
現在,在調試這個程序時,如果希望查明向量的長度,可以在 gdb 提示上運行 V._M_finish – V._M_start,其中的 _M_finish 和 _M_start 分別是向量開頭和末尾的指針。但是,這要求您了解STL 的內部原理,而這不總是可行的。我推薦的替代方法是使用可免費下載的 gdb_stl_utils,它在gdb 中定義幾個用戶定義命令,比如 p_stl_vector_size(顯示向量的大小)或 p_stl_vector(顯示向量的內容)。清單 11 說明 p_stl_vector 如何循環遍歷由 _M_start 和 _M_finish 指針指定的數據。
清單 11. 使用 p_stl_vector 顯示向量的內容
define p_stl_vector
set $vec = ($arg0)
set $vec_size = $vec->_M_finish - $vec->_M_start
if ($vec_size != 0)
set $i = 0
while ($i < $vec_size)
printf "Vector Element %d: ", $i
p *($vec->_M_start+$i)
set $i++
end
end
end
在 gdb 提示上運行 help user-defined,就可以看到使用 gdb_stl_utils 定義的命令列表。
加快編譯
對于任何比較復雜的軟件,編譯源代碼都會占用不少時間。在加快編譯過程方面,最好的工具之一是ccache(見 參考資料 中的鏈接)。ccache 是一種編譯器緩存,這意味著如果在編譯期間文件沒有修改過,就從工具的緩存獲取它。如果用戶只修改了一個頭文件并調用 make clean; make,ccache會顯著加快編譯。因為 ccache 不僅僅使用時間戳決定文件是否需要重新編譯,可以更好地節省寶貴的編譯時間。下面是使用 ccache 的一個示例:
arpan@tintin# ccache g__ foo.cxx
ccache 在內部生成一個散列(hash),使用這個散列和其他東西考慮源代碼文件的預處理版本(使用 g++ –E 獲得)、調用編譯器所用的選項等。編譯的對象文件根據這個散列存儲在緩存中。
ccache 定義了幾個可以定制的環境變量:
* CCACHE_DIR。ccache 在這個目錄中存儲緩存的文件。在默認情況下,文件存儲在 $HOME/.ccache中。
* CCACHE_TEMPDIR。ccache 在這個目錄中存儲臨時文件。這個文件夾應該位于與 $CCACHE_DIR 相同的文件系統中。
* CCACHE_READONLY。如果不斷增大的緩存文件夾可能造成問題,那么設置這個環境變量會有幫助。如果啟用這個變量,ccache 在編譯期間不會在緩存中添加任何文件;但是,它使用現有的緩存搜索對象文件。
通過結合使用 gdb 與 Valgrind 和 Electric-Fence 解決內存錯誤
C++ 編程有幾個缺陷 — 最顯著的問題是內存錯誤。有兩個 UNIX 開放源碼工具 — Valgrind 和Electric-Fence — 它們可以與 gdb 結合使用以解決內存錯誤。下面簡要討論如何使用這些工具。
Valgrind
對程序使用 Valgrind 最容易的方法是在 shell 上運行它,然后使用一般的程序選項。注意,為了獲得最佳效果,應該運行程序的調試版本。
arpan@tintin# valgrind <valgrind options>
<program name> <program option1> <program option2> ..
Valgrind 報告一些常見的內存錯誤,比如不正確地釋放內存(應該使用 malloc 分配,使用 delete釋放)、使用尚未初始化的變量以及兩次刪除同一個指針。清單 12 中的示例代碼中有一個明顯的數組覆蓋問題。
清單 12. C++ 內存錯誤示例
int main ()
{
int* p_arr = new int[10];
p_arr[10] = 5;
return 0;
}
Valgrind 和 gdb 可以結合使用。通過在 Valgrind 中使用 -db-attach=yes 選項,可以在運行Valgrind 時直接調用 gdb。例如,如果帶 –db-attach 選項對清單 12 中的代碼調用 Valgrind,在首次遇到內存問題時,它會調用 gdb,見清單 13。
清單 13. 在執行 Valgrind 期間連接 gdb
==5488== Conditional jump or move depends on uninitialised value(s)
==5488== at 0x401206C: strlen (in /lib/ld-2.3.2.so)
==5488== by 0x4004E35: _dl_init_paths (in /lib/ld-2.3.2.so)
==5488== by 0x400305A: dl_main (in /lib/ld-2.3.2.so)
==5488== by 0x400F87D: _dl_sysdep_start (in /lib/ld-2.3.2.so)
==5488== by 0x4001092: _dl_start (in /lib/ld-2.3.2.so)
==5488== by 0x4000C56: (within /lib/ld-2.3.2.so)
==5488==
==5488== ---- Attach to debugger ? --- [Return/N/n/Y/y/C/c] ---- n
==5488==
==5488== Invalid write of size 4
==5488== at 0x8048466: main (test.cc:4)
==5488== Address 0x4245050 is 0 bytes after a block of size 40 alloc'd
==5488== at 0x401ADEB: operator new[](unsigned)
(m_replacemalloc/vg_replace_malloc.c:197)
==5488== by 0x8048459: main (test.cc:3)
==5488==
==5488== ---- Attach to debugger ? --- [Return/N/n/Y/y/C/c] ----
Electric-Fence
Electric-Fence 是一組用于在基于 gdb 的環境中檢測緩沖區上溢出或下溢出的庫。在發生錯誤的內存訪問時,這個工具(與 gdb 結合)會準確地指出源代碼中導致問題的指令。例如,對于 清單 12中的代碼,清單 14 顯示在啟用 Electric-Fence 的情況下 gdb 的表現。
清單 14. Electric-Fence 準確地指出源代碼中導致崩潰的部分
(gdb) efence on
Enabled Electric Fence
(gdb) run
Starting program: /home/tintin/efence/a.out
Electric Fence 2.2.0 Copyright (C) 1987-1999 Bruce Perens <bruce@perens.com>
Electric Fence 2.2.0 Copyright (C) 1987-1999 Bruce Perens <bruce@perens.com>
Program received signal SIGSEGV, Segmentation fault.
0x08048466 in main () at test.cc:4
<b>4 p_arr[10] = 5;</b>
在安裝 Electric-Fence 之后,在 .gdbinit 文件中添加以下行:
define efence
set environment EF_PROTECT_BELOW 0
set environment LD_PRELOAD /usr/lib/libefence.so.0.0
echo Enabled Electric Fence\n
end
使用 gprof 進行代碼覆蓋
最常見的編程任務之一是提高代碼性能。為了完成這個任務,一定要查明代碼的哪些部分花費的執行時間最多。用技術術語來說,這稱為剖析。GNU 剖析工具 gprof(見 參考資料 中的鏈接)很容易使用,提供了大量有用的特性。
為了收集程序的剖析信息,第一步是在調用編譯器時指定 –pg 選項:
arpan@tintin# g++ database.cpp –pg
接下來,像一般情況下一樣運行程序。成功地運行程序之后(也就是,沒有出現崩潰或對 _exit system call 的調用),剖析信息被寫入 gmon.out 文件中。生成 gmon.out 文件之后,對可執行文件運行 gprof(如下所示)。注意,如果沒有指定可執行文件名,默認文件為 a.out。同樣,如果沒有指定剖析數據文件名,默認文件為當前工作目錄中的 gmon.out。
arpan@tintin# gprof <options> <executable name>
<profile-data-file name> > outfile
在默認情況下,gprof 在標準輸出中顯示輸出,所以需要把輸出重定向到一個文件。gprof 提供兩組信息:平面剖析數據和調用圖,它們共同組成輸出文件。平面剖析數據顯示每個函數花費的總時間。Cumulative seconds 表示一個函數花費的總時間加上從這個函數調用的其他函數花費的時間。Self seconds 只計算這個函數本身花費的時間。
在 gdb 中顯示源代碼清單
開發人員常常需要通過非常緩慢的遠程連接調試代碼,所以不支持對 gdb 使用 Data Display Debugger (DDD) 等圖形化界面。在這種情況下,在 gdb 中使用 Ctrl-X-A 組合鍵可以節省時間,因為它會在調試期間顯示源代碼清單。按 Ctrl-W-A 組合鍵就能夠返回到 gdb 提示。另一種方法是用–tui 選項調用 gdb,這會直接啟動文本模式的源代碼清單。清單 15 顯示以文本模式調用 gdb 的情況。
使用文本模式的 gdb 源代碼清單
3using namespace std;
4
5int main ()
6 {
B+>7 vector<int> V;
8 V.push_back(9);
9 V.push_back(8);
10 for (int i=0; i < V.size(); i++)
11 cout << V[i] << "\n";
12 return 0;
13 }
14
--------------------------------------------------------------------------------------
child process 6069 In: main Line: 7 PC: 0x804890e
(gdb) b main
Breakpoint 1 at 0x804890e: file test.cc, line 7.
(gdb) r
使用 CVS 維護有秩序源代碼清單
不同的項目采用不同的編碼風格。例如,一些開發人員喜歡在代碼中使用制表符,而其他人不喜歡這樣做。但重要的是,所有開發人員應該遵守相同的編碼標準。但 是,現實情況常常不是這樣的。通過使用 Concurrent Versions system (CVS) 等版本控制系統,可以針對一組編碼標準檢查要簽入的文件,從而有效地實施統一的編碼標準。為了完成這個任務,CVS 附帶一組預定義的觸發器腳本,當涉及特定的用戶操作時會運行這些腳本。觸發器腳本的格式很簡單:
<REGULAR EXPRESSION> <PROGRAM TO RUN>
預定義的觸發器腳本之一是 $CVSROOT 文件夾中的 commitinfo 文件。為了檢查要簽入的文件是否包含制表符,使用以下 commitinfo 文件語法:
ALL /usr/local/bin/validate-code.pl
commitinfo 文件識別出 ALL 關鍵字(這意味著應該檢查提交的每個文件;也可以指定要檢查的文件集)。然后運行相關聯的腳本,根據源代碼標準檢查文件。
結束語
本文討論了幾種有助于提高 C++ 開發人員的生產力的免費工具。關于各個工具的詳細信息,請查閱 參考資料。