代碼靜態分析工具——splint的學習與使用

引言html

最近在項目中使用了靜態程序分析工具PC-Lint,體會到它在項目實施中帶給開發人員的方便。PC-Lint是一款針對C/C++語言、windows平臺的靜態分析工具,FlexeLint是針對其餘平臺的PC-Lint版本。因爲PC-Lint/FlexeLint是商業的程序分析工具,不便於你們對其進行學習和使用,於是下面我將介紹一個針對C語言的開源程序靜態分析工具——splint編程

靜態程序分析windows

先來講說什麼是「靜態程序分析(Static program analysis)」,靜態程序分析是指使用自動化工具軟件對程序源代碼進行檢查,以分析程序行爲的技術,應用於程序的正確性檢查、安全缺陷檢測、程序優化等。它的特色就是不執行程序,相反,經過在真實或模擬環境中執行程序進行分析的方法稱爲「動態程序分析(Dynamic program analysis)」。數組

那在什麼狀況下須要進行靜態程序分析呢?靜態程序分析每每做爲一個多人蔘與的項目中代碼審查過程的一個階段,因編寫完一部分代碼以後就能夠進行靜態分析,分析過程不須要執行整個程序,這有助於在項目早期發現如下問題:變量聲明瞭但未使用、變量類型不匹配、變量在使用前未定義、不可達代碼、死循環、數組越界、內存泄漏等。下圖說明了靜態程序分析在進行項目編碼過程當中所處的位置:緩存

靜態分析工具位置

從上圖能夠知道,靜態分析工具在代碼經過編譯以後再對代碼進行分析。咱們會問:靜態分析工具與編譯器相比,所作的工做有什麼不一樣?靜態分析工具相比編譯器,對代碼進行了更加嚴格的檢查,像數組越界訪問、內存泄漏、使用不當的類型轉換等問題,均可以經過靜態分析工具檢查出來,咱們甚至能夠在分析工具的分析標準裏定義代碼的編寫規範,在檢測到不符合編寫規範的代碼時拋出告警,這些功能都是編譯器沒有的。安全

 

既然靜態分析工具發揮了不小的做用,何不在編譯器裏兼備靜態分析的功能?對於這個問題,S. C. Johnson(他是最古老的靜態分析工具Lint的做者)在其1978年發表的論文《Lint, a C Program Checker》中給出了他的答案:「Lint與C編譯器在功能上的分離既有歷史緣由,也有現實的意義。編譯器負責把C源程序快速、高效地轉變爲可執行文件,不對代碼作類型檢查(特別是對分別編譯的程序),有益於作到快速與高效。而Lint沒有「高效」的要求,能夠花更多時間對代碼進行更深刻、仔細的檢查。」函數

 

針對空指針提取、未定義變量使用、類型轉換、內存管理、函數接口定義等,咱們能夠在靜態分析工具裏制定不一樣的檢測標準,如下曲線圖說明了在使用splint進行分析時,檢測標準與splint運行的開銷所對應的關係,從另外一個角度看,也說明了靜態分析工具與編譯器的關係:工具

splint effort-benefit curve

splintpost

掌握了「靜態分析」等概念以後,咱們再來看splint。學習

在Linux命令行下,splint的使用很簡單,檢測文件*.c,只要這樣使用就能夠了:

splint *.c

1.splint消息

咱們經過如下例子來認識典型的splint告警信息:

複製代碼
 1 //splint_msg.c
2 int func_splint_msg1(void)
3 {
4 int a;
5 return0;
6 }
7 int func_splint_msg2(void)
8 {
9 int* a = (int*)malloc(sizeof(int));
10 a = NULL;
11 return0;
12 }
複製代碼

運行splint splint_msg.c以後,咱們來看輸出的告警信息:

複製代碼
splint_msg.c: (in function func_splint_msg1)
splint_msg.c:4:6: Variable a declared but not used
A variable is declared but never used. Use /*@unused@*/ in front of
declaration to suppress message. (Use -varuse to inhibit warning)

splint_msg.c: (in function func_splint_msg2)
splint_msg.c:10:2: Fresh storage a (type int *) not released before assignment:
a = NULL
A memory leak has been detected. Storage allocated locally is not released
before the last reference to it is lost. (Use -mustfreefresh to inhibit
warning)
splint_msg.c:9:37: Fresh storage a created

Finished checking --- 2 code warnings
複製代碼

藍色字體部分:給出告警所在函數名,在函數的第一個警告消息報告前打印;

紅色字體部分:消息的正文,文件名、行號、列號顯示在的警告的正文前;

黑色字體部分:是有關該可疑錯誤的詳細信息,包含一些怎樣去掉這個消息的信息;

綠色字體部分:給出格外的位置信息,這裏消息給出了是在哪裏申請了這個可能泄露的內存。

2.檢查控制

splint提供了三種方式可進行檢查的控制,分別是.splintrc配置文件、flags標誌和格式化註釋。

flags:splint支持幾百個標誌用來控制檢查和消息報告,使用時標誌前加’+‘或’-’,'+'標誌開啓這個標誌,'-'表示關閉此標誌,下面例子展現了flags標誌的用法:

splint -showcol a.c //在檢測a.c時,告警消息中列數不被打印
splint -varuse a.c //在檢測a.c時,告警消息中未使用變量告警不被打印

.splintrc配置文件:在使用源碼安裝splint以後,.splintrc文件將被安裝在主目錄下,.splintrc文件中對一些標誌做了默認的設定,命令行中指定的flags標誌會覆蓋.splintrc文件中的標誌。

格式化註釋:格式化註釋提供一個類型、變量或函數的格外的信息,能夠控制標誌設置,增長檢查效果,全部格式化註釋都以/*@開始,@*/結束,好比在函數參數前加/*@null@*/,表示該參數多是NULL,作檢測時,splint會增強對該參數的值的檢測。

3.檢測分析內容

1.解引用空指針(Null Dereferences)

在Unix操做系統中,解引用空指針將致使咱們在程序運行時產生段錯誤(Segmentation fault),一個簡單的解引用空指針例子以下:

複製代碼
1 //null_dereferences.c
2 int func_null_dereferences(void)
3 {
4 int* a = NULL;
5 return*a;
6 }
複製代碼

執行splint null_dereference.c命令,將產生如下告警消息:

複製代碼
null_dereference.c: (in function func_null_dereferences)
null_dereference.c:5:10: Dereference of null pointer a: *a
A possibly null pointer is dereferenced. Value is either the result of a
function which may return null (in which case, code should check it is not
null), or a global, parameter or structure field declared with the null
qualifier. (Use -nullderef to inhibit warning)
null_dereference.c:4:11: Storage a becomes null

Finished checking --- 1 code warnin
複製代碼

2.類型(Types)

咱們在編程中常常用到強制類型轉換,將有符號值轉換爲無符號值、大範圍類型值賦值給小範圍類型,程序運行的結果會出無咱們的預料。

複製代碼
1 //types.c
2 void splint_types(void)
3 {
4 short a =0;
5 long b =32768;
6 a = b;
7 return;
8 }
複製代碼

執行splint types.c命令,將產生如下告警消息:

types.c: (in function splint_types)
types.c:6:2: Assignment of longint to shortint: a = b
To ignore type qualifiers in type comparisons use +ignorequals.

Finished checking ---1 code warning

3.內存管理(Memory Management)

C語言程序中,將近半數的bug歸功於內存管理問題,關乎內存的bug難以發現而且會給程序帶來致命的破壞。由內存釋放所產生的問題,咱們能夠將其分爲兩種:

  • 當尚有其餘指針引用的時候,釋放一塊空間
複製代碼
1 //memory_management1.c
2 void memory_management1(void)
3 {
4 int* a = (int*)malloc(sizeof(int));
5 int* b = a;
6 free(a);
7 *b =0;
8 return;
9 }
複製代碼

在上面這個例子中,指針a與b指向同一塊內存,但在內存釋放以後仍對b指向的內容進行賦值操做,咱們來看splint
memory_management1.c的結果:

複製代碼
memory_management1.c: (in function memory_management1)
memory_management1.c:7:3: Variable b used after being released
Memory is used after it has been released (either by passing as an only param
or assigning to an only global). (Use -usereleased to inhibit warning)
memory_management1.c:6:7: Storage b released
memory_management1.c:7:3: Dereference of possibly null pointer b: *b
A possibly null pointer is dereferenced. Value is either the result of a
function which may returnnull (in which case, code should check it is not
null), or a global, parameter or structure field declared with the null
qualifier. (Use -nullderef to inhibit warning)
memory_management1.c:5:11: Storage b may become null

Finished checking ---2 code warnings
複製代碼

檢查結果中包含了兩個告警,第一個指出咱們使用了b指針,而它所指向的內存已被釋放;第二個是對解引用空指針的告警。

  • 當最後一個指針引用丟失的時候,其指向的空間還沒有釋放
複製代碼
1 //memory_management2.c
2 void memory_management2(void)
3 {
4 int* a = (int*)malloc(sizeof(int));
5 a = NULL;
6 return;
7 }
複製代碼

這個例子中內存還沒有釋放,就將指向它的惟一指針賦值爲NULL,咱們來看splint memory_management2.c的檢測結果:

複製代碼
memory_management2.c: (in function memory_management2)
memory_management2.c:5:2: Fresh storage a (type int*) not released before assignment:
a = NULL
A memory leak has been detected. Storage allocated locally is not released
before the last reference to it is lost. (Use -mustfreefresh to inhibit
warning)
memory_management2.c:4:37: Fresh storage a created

Finished checking ---1 code warning
複製代碼

splint拋出一個告警:類型爲int*的a在進行a = NULL賦值前沒有釋放新分配的空間。

4.緩存邊界(Buffer Sizes)

splint會對數組邊界、字符串邊界做檢測,使用時須要加上+bounds的標誌,咱們來看下面的例子:

複製代碼
1 //bounds1.c
2 void bounds1(void)
3 {
4 int a[10];
5 a[10] =0;
6 return;
7 }
複製代碼

使用splint +bounds bounds1.c命令對其進行檢測,結果以下:

複製代碼
bounds1.c: (in function bounds1)
bounds1.c:5:2: Likely out-of-bounds store: a[10]
Unable to resolve constraint:
requires 9>=10
needed to satisfy precondition:
requires maxSet(a @ bounds1.c:5:2) >=10
A memory write may write to an address beyond the allocated buffer. (Use
-likelyboundswrite to inhibit warning)

Finished checking ---1 code warning
複製代碼

告警消息提示數組越界,訪問超出咱們申請的buffer大小範圍。再看一個例子:

複製代碼
 1 //bounds2.c
2 void bounds2(char* str)
3 {
4 char* tmp = getenv("HOME");
5 if(tmp != NULL)
6 {
7 strcpy(str, tmp);
8 }
9 return;
10 }
複製代碼

不對這個例子進行詳細檢查,可能咱們不能發現其中隱含的問題,執行splint +bounds bounds2.c以後,會拋出以下告警:

複製代碼
bounds2.c: (in function bounds2)
bounds2.c:7:3: Possible out-of-bounds store: strcpy(str, tmp)
Unable to resolve constraint:
requires maxSet(str @ bounds2.c:7:10) >= maxRead(getenv("HOME") @
bounds2.c:4:14)
needed to satisfy precondition:
requires maxSet(str @ bounds2.c:7:10) >= maxRead(tmp @ bounds2.c:7:15)
derived from strcpy precondition: requires maxSet(<parameter 1>) >=
maxRead(<parameter 2>)
A memory write may write to an address beyond the allocated buffer. (Use
-boundswrite to inhibit warning)

Finished checking ---1 code warning
複製代碼

告警消息提示咱們:在使用strcpy(str, tmp)進行字符串複製時,可能出現越界錯誤,由於str的大小可能不足以容納環境變量「HOME」對應的字符串。綠色字體的內容指示瞭如何消除告警消息。

 

小結

這裏僅給出了splint檢查的4種檢測:解引用空指針、類型、內存管理、緩存邊界,除此以外,splint還對宏(Macros)、函數接口(Function Interfaces)、控制流(Control Flow)等內容做檢測,不少檢測標誌和格式化註釋都未在本文中提到,更詳細的內容請查看splint使用手冊

 

無論pc-lint、splint等靜態程序分析工具的功能多麼強大,它們對程序的檢查也有疏漏的地方,工具的使用並不能提升咱們的編程能力,咱們更應該經過它們學習各類編碼錯誤和代碼隱患,憑積累的編碼知識把程序隱患扼殺在搖籃裏。

 

你在項目實施中是否遇到過隱藏的bug,致使返工呢?在你的項目中是否使用了靜態程序分析工具,它起到多大的做用?說出來與你們一塊分享吧~

 

 

 

Reference:《Splint Manual》

                《Lint, a C Program Checker》

https://www.cnblogs.com/bangerlee/archive/2011/09/07/2166593.html

僅供學習!

相關文章
相關標籤/搜索