Splint(Secure Programming Lint)是經典 C 語言靜態分析工具,透過原始碼註釋(Annotations)擴展類型系統,能檢測空指標解引用、未初始化變數、型別不匹配、緩衝區溢位風險等編譯器難以捕捉的缺陷。雖然現代有 Clang Static Analyzer、Cppcheck、Coverity,但 Splint 的註釋系統對理解「契約式設計」極具教學價值。本文涵蓋安裝、核心註釋語法、三大類檢查實戰。
# 下載源碼(官網 splint.org 已停維,使用鏡像)
wget https://github.com/splint/splint/archive/refs/tags/3.1.2.tar.gz -O splint-3.1.2.tar.gz
tar -zxvf splint-3.1.2.tar.gz
cd splint-3.1.2
./configure --prefix=/usr/local/splint
make && make install
# 環境變數(~/.bashrc)
export LARCH_PATH=/usr/local/splint/share/splint/lib
export LCLIMPORTDIR=/usr/local/splint/share/splint/import
export PATH=/usr/local/splint/bin:$PATH
source ~/.bashrc
splint -version # 驗證
| 註釋 | 位置 | 語意 | 範例 |
|---|---|---|---|
/*@null@*/ | 參數/返回值 | 允許為 NULL | void foo(/*@null@*/ char *p); |
/*@notnull@*/ | 參數/返回值 | 保證非 NULL | /*@notnull@*/ char *bar(); |
/*@in@*/ | 參數 | 調用前必須已初始化 | void read(/*@in@*/ int *p); |
/*@out@*/ | 參數 | 調用後保證初始化 | void write(/*@out@*/ int *p); |
/*@inout@*/ | 參數 | 調用前初始化、調用後可能修改 | void swap(/*@inout@*/ int *a, /*@inout@*/ int *b); |
/*@observer@*/ | 參數 | 僅讀取,不獲取所有權 | void print(/*@observer@*/ FILE *f); |
/*@only@*/ | 參數/返回值 | 轉移所有權(呼叫者不再負責釋放) | /*@only@*/ char *alloc(); |
/*@owned@*/ | 參數 | 呼叫者保留所有權 | void free(/*@owned@*/ char *p); |
/*@temp@*/ | 參數 | 暫時借用,函數返回前釋放 | void use(/*@temp@*/ char *buf); |
/*@dependent@*/ | 結構成員 | 生命週期依附宿主物件 | struct { /*@dependent@*/ char *name; } |
// null.c
char firstChar1(/*@null@*/ char *s) {
return *s; // 警告:可能解引用 NULL
}
char firstChar2(/*@null@*/ char *s) {
if (s == NULL) return '\0';
return *s; // 無警告:已檢查
}
$ splint null.c
null.c:3:11: Dereference of possibly null pointer s: *s
// usedef.c
extern void setVal(/*@out@*/ int *x);
extern int getVal(/*@in@*/ int *x);
extern int mysteryVal(int *x);
int dumbfunc(/*@out@*/ int *x, int i) {
if (i > 3) return *x; // 警告:x 未初始化就讀取
else if (i > 1) return getVal(x); // 警告:傳遞未初始化 x 給 @in@ 參數
else if (i == 0) return mysteryVal(x); // 警告
else {
setVal(x); // @out@ 保證初始化
return *x; // OK
}
}
$ splint usedef.c
usedef.c:7:19: Value *x used before definition
usedef.c:9:18: Passed storage x not completely defined
usedef.c:11:22: Passed storage x not completely defined
// bool.c
int f(int i, char *s, bool b1, bool b2) {
if (i = 3) return b1; // 警告:條件為賦值而非比較
if (!i || s) return i; // 警告:! 運算元非布爾、|| 右運算元非布爾
if (s) return 7; // OK:指標作為布爾語境隱式轉換
if (b1 == b2) return 3; // 警告:bool 直接 == 比較不可靠,建議用 &&/||
return 2;
}
$ splint bool.c
bool.c:4:5: Test expression for if is assignment expression: i = 3
bool.c:4:5: Test expression for if not boolean, type int: i = 3
bool.c:4:8: Return value type bool does not match declared type int: b1
bool.c:5:6: Operand of ! is non-boolean (int): !i
bool.c:5:11: Right operand of || is non-boolean (char *): !i || s
bool.c:7:5: Use of == with boolean variables (risks inconsistency)
# 抑制特定警告類別
splint -predassign # 忽略條件賦值警告
splint -predboolint # 忽略條件非布爾警告
splint -boolops # 忽略布爾運算元警告
splint -boolcompare # 忽略 bool == 比較警告
splint -type # 忽略型別不匹配警告
splint -usedef # 忽略未初始化警告
splint -compdef # 忽略未完全定義警告
splint -weak file.c(僅標準檢查),再逐步加入註釋。splint +weak -warnposix *.c 作為建置門檻。clang --analyze / scan-build:LLVM 原生、路徑敏感、支援 C++、零註釋即可用。cppcheck --enable=all --std=c11:速度快、誤報低、支援 MISRA/CERT 規則集。Coverity Scan / CodeQL:企業級、跨語言、資料流分析更深。Splint 以「註釋即契約」思想,強迫開發者在介面層面明確所有權、空值、初始化責任,這種嚴謹思維可移植至任何語言的 API 設計。雖工具本身已較少維護,但其核心概念仍是靜態分析與安全編碼的基石。