上次的Hello world算是入門了,現在學習一些相關工具的使用
編譯自動化
寫好程序,首先要編譯,就用gcc就好了,基本用法如下
gcc helloworld.c -o helloworld.o
helloworld.c是源碼,helloworld.o是編譯后的可執行文件,運行的話就用 ./helloworld.o
就可以了。
但是如果代碼寫的多了,每次改動完都手動用gcc編譯太麻煩了,所以要用Makefile來 自動化這項工作,在當前目錄下創建Makefile文件,大概如下
helloworld.o: helloworld.c gcc helloworld.c -o helloworld.o .PHONY: lint lint: splint helloworld.c -temptrans -mustfreefresh -usedef .PHONY: run run: ./helloworld.o .PHONY: clean clean: rm *.o
縮進為0每一行表示一個任務,冒號左邊的是目標文件名,冒號后面是生成該目標的依賴 文件,多個的話用逗號隔開,如果依賴文件沒有更改,則不會執行該任務。
縮進為1的行表示任務具體執行的shell語句了,.PHONY修飾的目標表示不管依賴文件 有沒有更改,都執行該任務。
執行對應的任務的話,就是在終端上輸入make 目標名
,如make lint
表示源碼檢查, make clean
表示清理文件,如果只輸入make,則執行第一個目標,對於上面的文件就 是生成helloworld.o了。
現在修改完源碼,值需要輸入一個make回車就行了,Makefile很強大,可以做很多自動化 的任務,甚至測試,部署,生成文檔等都可以用Makefile來自動化,有點像前端的 Grunt和Java里的ant,這樣就比較好理解了。
靜態檢查
靜態檢查可以幫你提前找出不少潛在問題來,經典的靜態檢查工具就是lint,具體到 Linux上就是splint了,可以用yum來安裝上。
具體使用的話就是splint helloworld.c
就行了,它會給出檢查出來的警告和錯誤,還 提供了行號,讓你能很快速的修復。
值得注意的是該工具不支持c99語法,所以寫代碼時需要注意一些地方,比如函數里聲明 變量要放在函數的開始,不能就近聲明,否則splint會報parse error。
靜態檢查工具最好不要忽略warning,但是有一些警告莫名其妙,我看不懂,所以還是 忽略了一些,在使用中我加上了-temptrans -mustfreefresh -usedef
這幾個參數。
單元測試
安裝CUnit
wget http://sourceforge.net/projects/cunit/files/latest/download tar xf CUnit-2.1-3.tar.bz2 cd CUnit-2.1-3 ./bootstrap ./configure make make install
了解下單元測試的概念: 一次測試(registry)可以分成多個suit,一個suit里可以有多個 test case, 每個suit有個setup和teardown函數,分別在執行suit之前或之后調用。
下面的代碼是一個單元測試的架子,這里測試的是庫函數strlen,這里面只有一個suit, 就是testSuite1,testSuit1里里有一特test case,就是testcase,testcase里有一個 測試,就是test_string_length。
整體上就是這么一個架子,suit,test case, test都可以往里擴展。
#include <assert.h> #include <stdlib.h> #include <string.h> #include <CUnit/Basic.h> #include <CUnit/Console.h> #include <CUnit/CUnit.h> #include <CUnit/TestDB.h> // 測試庫函數strlen功能是否正常 void test_string_lenth(void){ char* test = "Hello"; int len = strlen(test); CU_ASSERT_EQUAL(len,5); } // 創建一特test case,里面可以有多個測試 CU_TestInfo testcase[] = { { "test_for_lenth:", test_string_lenth }, CU_TEST_INFO_NULL }; // suite初始化, int suite_success_init(void) { return 0; } // suite 清理 int suite_success_clean(void) { return 0; } // 定義suite集, 里面可以加多個suit CU_SuiteInfo suites[] = { // 以前的版本沒有那兩個NULL參數,新版需要加上,否則就coredump //{"testSuite1", suite_success_init, suite_success_clean, testcase }, {"testSuite1", suite_success_init, suite_success_clean, NULL, NULL, testcase }, CU_SUITE_INFO_NULL }; // 添加測試集, 固定套路 void AddTests(){ assert(NULL != CU_get_registry()); assert(!CU_is_test_running()); if(CUE_SUCCESS != CU_register_suites(suites)){ exit(EXIT_FAILURE); } } int RunTest(){ if(CU_initialize_registry()){ fprintf(stderr, " Initialization of Test Registry failed. "); exit(EXIT_FAILURE); }else{ AddTests(); // 第一種:直接輸出測試結果 CU_basic_set_mode(CU_BRM_VERBOSE); CU_basic_run_tests(); // 第二種:交互式的輸出測試結果 // CU_console_run_tests(); // 第三種:自動生成xml,xlst等文件 //CU_set_output_filename("TestMax"); //CU_list_tests_to_file(); //CU_automated_run_tests(); CU_cleanup_registry(); return CU_get_error(); } } int main(int argc, char* argv[]) { return RunTest(); }
然后Makefile里增加如下代碼
INC=-I /usr/local/include/CUnit LIB=-L /usr/local/lib/ test: testcase.c gcc -o test.o $(INC) $(LIB) -g $^ -l cunit ./test.o .PHONY: test
再執行make test就可以執行單元測試了,結果大約如下
gcc -o test.o -I /usr/local/include/CUnit -L /usr/local/lib/ -g testcase.c -l cunit ./test.o CUnit - A unit testing framework for C - Version 2.1-3 http://cunit.sourceforge.net/ Suite: testSuite1 Test: test_for_lenth: ...passed Run Summary: Type Total Ran Passed Failed Inactive suites 1 1 n/a 0 0 tests 1 1 1 0 0 asserts 1 1 1 0 n/a Elapsed time = 0.000 seconds
可以看到testSuite1下面的test_for_lenth通過測試了。 注意一下,安裝完新的動態庫后記得ldconfig,否則-l cunit可能會報錯 如果還是不行就要 /etc/ld.so.conf 看看有沒有 /usr/local/lib , cunit默認把庫都放這里了。
調試coredump
就上面的單元測試, 如果使用注釋掉那行,執行make test時就會產生coredump。如下
// 定義suite集, 里面可以加多個suit CU_SuiteInfo suites[] = { {"testSuite1", suite_success_init, suite_success_clean, testcase }, //{"testSuite1", suite_success_init, suite_success_clean, NULL, NULL, testcase }, CU_SUITE_INFO_NULL };
但默認coredump不會保存在磁盤上,需要執ulimit -c unlimited
才可以,然后要 指定一下coredump的路徑和格式:
echo "/tmp/core-%e-%p" > /proc/sys/kernel/core_pattern
其中%e是可執行文件名,%p是進程id。然后編譯這段代碼的時候要加上-g的選項,意思 是編譯出調試版本的可執行文件,在調試的時候可以看到行號。
gcc -o test.o -I /usr/local/include/CUnit -L /usr/local/lib/ -g testcase.c -l cunit
在執行./test.o后就會產生一個coredump了,比如是/tmp/core-test.o-16793, 這時候 用gdb去調試該coredump,第一個參數是可執行文件,第二個參數是coredump文件
gdb test.o /tmp/core-test.o-16793
掛上去后默認會有一些輸出,其中有如下
Program terminated with signal 11, Segmentation fault.
說明程序遇到了段錯誤,崩潰了,一般段錯誤都是因為內存訪問引起的, 我們想知道 引起錯誤的調用棧, 輸入bt回車,會看到類似如下的顯示
(gdb) bt #0 0x00007fe1b0b22cb2 in CU_register_nsuites () from /usr/local/lib/libcunit.so.1 #1 0x00007fe1b0b22d28 in CU_register_suites () from /usr/local/lib/libcunit.so.1 #2 0x0000000000400a8a in AddTests () at testcase.c:46 #3 0x0000000000400adf in RunTest () at testcase.c:56 #4 0x0000000000400b13 in main (argc=1, argv=0x7fff4fa51928) at testcase.c:79
這樣大概知道是咋回事了,報錯在testcase.c的46行上,再往里就是cunit的調用棧了, 我們看不到行號,好像得有那個so的調試信息才可以,目前還不會在gdb里動態掛符號文件 ,所以就先不管了,輸入q退出調試器,其它命令用輸入help學習下。
if(CUE_SUCCESS != CU_register_suites(suites)){
就調用了一個CU_register_suites函數,函數本身應該沒有錯誤,可能是傳給他從參數 有問題,就是那個suites,該參數構建的代碼如下:
CU_SuiteInfo suites[] = { {"testSuite1", suite_success_init, suite_success_clean, testcase }, CU_SUITE_INFO_NULL };
是個CU_SuiteInfo的數組,就感覺是構建這個類型沒構建對,然后就看他在哪兒定義 的
# grep -n "CU_SuiteInfo" /usr/local/include/CUnit/* /usr/local/include/CUnit/TestDB.h:696:typedef struct CU_SuiteInfo {
在/usr/local/include/CUnit/TestDB.h的696行,具體如下
typedef struct CU_SuiteInfo { const char *pName; /**< Suite name. */ CU_InitializeFunc pInitFunc; /**< Suite initialization function. */ CU_CleanupFunc pCleanupFunc; /**< Suite cleanup function */ CU_SetUpFunc pSetUpFunc; /**< Pointer to the test SetUp function. */ CU_TearDownFunc pTearDownFunc; /**< Pointer to the test TearDown function. */ CU_TestInfo *pTests; /**< Test case array - must be NULL terminated. */ } CU_SuiteInfo;
可以看到,該結構有6個成員,但我們定義的時候只有4個成員,沒有設置pSetUpFunc和 pTearDownFunc的,所以做如下修改就能修復該問題了。
- {"testSuite1", suite_success_init, suite_success_clean, testcase }, + {"testSuite1", suite_success_init, suite_success_clean, NULL, NULL, testcase },
對了,gdb用yum安裝就行了。
性能剖析
好些時候我們要去分析一個程序的性能,比如哪個函數調用了多少次,被誰調用了, 平均每次調用花費多少時間等。這時候要用gprof,gprof是分析profile輸出的。 要想執行時輸出profile文件編譯時要加-pg選項,
gcc -o helloworld.o -pg -g helloworld.c ./helloworld.o
執行上面語句后會在當前目錄下生成gmon.out文件, 然后用gprof去讀取並顯示出來, 因為可能顯示的比較長,所以可以先重定向到一個文件prof_info.txt里
gprof -b -A -p -q helloworld.o gmon.out >prof_info.txt
參數的含義先這么用,具體可以搜,最后查看prof_info.txt里會有需要的信息, 大概 能看懂,具體可以搜。
Flat profile: Each sample counts as 0.01 seconds. no time accumulated % cumulative self self total time seconds seconds calls Ts/call Ts/call name 0.00 0.00 0.00 15 0.00 0.00 cmp_default 0.00 0.00 0.00 15 0.00 0.00 cmp_reverse 0.00 0.00 0.00 4 0.00 0.00 w_strlen 0.00 0.00 0.00 2 0.00 0.00 sort 0.00 0.00 0.00 1 0.00 0.00 change_str_test 0.00 0.00 0.00 1 0.00 0.00 concat_test 0.00 0.00 0.00 1 0.00 0.00 customer_manager 0.00 0.00 0.00 1 0.00 0.00 hello_world 0.00 0.00 0.00 1 0.00 0.00 n_hello_world 0.00 0.00 0.00 1 0.00 0.00 reverse 0.00 0.00 0.00 1 0.00 0.00 sort_test Call graph granularity: each sample hit covers 2 byte(s) no time propagated index % time self children called name 0.00 0.00 15/15 sort [4] [1] 0.0 0.00 0.00 15 cmp_default [1] ----------------------------------------------- 0.00 0.00 15/15 sort [4] [2] 0.0 0.00 0.00 15 cmp_reverse [2] ----------------------------------------------- 0.00 0.00 1/4 reverse [10] 0.00 0.00 1/4 main [16] 0.00 0.00 2/4 concat_test [6] [3] 0.0 0.00 0.00 4 w_strlen [3] -----------------------------------------------