簡介
在項目中,存在許多不規範的代碼,其一就是將無符號變量賦值給有符號變量。在大多數情況下是不會出現問題的,因爲那些變量值往往小於2147483648
。但是一些特定的接口,如時間獲取接口,可能返回一個較大的無符號值,如果使用
int
變量接收,便可能出現異常。當這些接口在項目中大量使用時,排查起來較爲困難,容易發生遺漏,因此引入代碼掃描工具進行特定接口的使用檢查。後續將針對
TimeGet
函數進行問題的詳細說明。TimeGet 接口聲明
// 獲取時間
// IN: 時間格式(如 TIME_YYMMDDHHMM)
// OUT: 時間值(如 2105301530 -> 21年5月30日15點30分)
unsigned TimeGet(TIME_TYPE type);
問題表現
在進入22年後,TIME_YYMMDDHHMM
格式的時間值便超出了 int
的表示範圍,如果誤用 int
變量進行接收,便可能出現如開始時間未到等各種問題。錯誤用法
- 使用
int
變量接收TimeGet
返回值。如下代碼,在進入22年後,活動仍然處於未開始狀態。int nTime = TimeGet(TIME_YYMMDDHHMM); if (nTime < 2112125959) { ShowMessage("活動未開始。"); return ; }
- 格式串中使用
%d
接收TimeGet
返回值。如下代碼,在進入22年後,會將整張表讀取到內存,實際上需要的可能只有三五條。char szSQL[1024]; sprintf(szSQL , "select * from tbl where out_time > %d" , TimeGet(TIME_YYMMDDHHMM)); auto result = database()->executeQuery(szSQL);
- 通用的數據庫記錄對象可能只提供了
GetInt
和SetInt
接口,在表字段類型爲unsigned
時,調用SetInt( TimeGet(TIME_YYMMDDHHMM) )
是不會出現問題的,但是需要小心使用GetInt
接口。如下代碼就將問題藏得很隱蔽,實際上,GetInt
返回值已經是個負數了,直接賦值給long long
變量,i64Value
仍然是個負數。
這種情況下,加個類型轉換就能解決問題。long long i64Value = data.GetInt(start_time);
long long i64Value = (unsigned)data.GetInt(start_time);
排查難點
TimeGet
在代碼中使用得很頻繁,直接搜索TimeGet(TIME_YYMMDDHHMM)
會出現大幾百行。- 有可能存在
TimeGet
接收時無誤,但後續傳遞過程中出現錯誤的情況,如下代碼,Func
函數正確處理了TimeGet
的返回值,但外部使用Func
卻出現了錯誤。unsigned FuncX() { return TimeGet(TIME_YYMMDDHHMM); } int main() { int nTime = FuncX(); //... return 0; }
代碼靜態掃描工具
考慮到人工排查的困難,這裏引入cppcheck 並自定義規則進行代碼的掃描,通過工具輔助,來度過22年時間溢出帶來的危機。掃描規則
- 確定合法的接收類型:
unsigned unsigned & unsigned long unsigned long & unsigned long long unsigned long long & signed long long signed long long &
- 定位到所有
TimeGet(TIME_YYMMDDHHMM)
調用位置。 - 查找
TimeGet
返回值的接收者,爲了方便理解,這裏直接描述爲向前尋找接收對象(實際實現上使用cppcheck 的語法分析樹查找)。接收者存在如下幾種情況:- 變量賦值
這裏向前查找會遇到賦值符號(可能是=、+=、-=、|=等等),這說明int nTime = TimeGet(TIME_YYMMDDHHMM);
TimeGet
將會賦值給某個變量,這時可以檢查變量的類型是不是合法的。 - 函數返回
這裏向前查找會遇到int func() { return TimeGet(TIME_YYMMDDHHMM); }
return
,這說明TimeGet
返回值將通過函數進一步返回,這時可以檢查函數的返回值類型是不是合法的。 - 函數傳參
這裏向前查找會遇到func( TimeGet(TIME_YYMMDDHHMM) );
func(
,這說明TimeGet
返回值將傳遞給func
的參數,這時檢查對應函數的參數類型。 - 構造
- 匿名構造
這裏向前查找會遇到struct User { User(int nTime) { //... } }; int main() { func ( User( TimeGet(TIME_YYMMDDHHMM) ) ); // ... }
User(
,此處的User
是一個類型名,這說明TimeGet
返回值將傳遞給User
的構造函數,這時檢查對應函數的參數類型。cppcheck 這裏並未直接將User
代碼鏈接到User
類的構造函數,而僅僅認爲此處的User
是一個類型,因此這裏需要自行根據傳入參數索引構造函數。 - 普通構造
這裏向前查找會遇到struct User { User(int nTime) { //... } }; int main() { User user(TimeGet(TIME_YYMMDDHHMM)); }
user(
,此處的user
是一個變量,這說明TimeGet
返回值將傳遞給user
變量所屬類型的構造函數,同樣的,這時需要檢查對應函數的參數類型。 - 標準數據類型構造
這裏向前查找會遇到int(TimeGet(TIME_YYMMDDHHMM))
int(
,此處的int
是一個類型,但其屬於基本類型,無法找到其構造函數,此時應直接判斷類型是否合法。
- 匿名構造
- 取餘、比較
這裏直接向前查找會發生誤判,認爲將時間賦值給int nHHMM = TimeGet(TIME_YYMMDDHHMM) % 10000; bool bZero = TimeGet(TIME_YYMMDDHHMM) == 0;
int
或bool
變量,但是使用語法分析樹判斷時,會先找到%
或==
符號,這裏認爲返回值的性質已經發生了變化,則不應算是錯誤。 - 控制流
這裏最終會查找到if (TimeGet(TIME_YYMMDDHHMM)) { // ... }
if(
,事實上這裏隱含了時間值與 0的判斷,可以認爲返回值性質發生了變化,不應算是錯誤。掃描工具中允許了if
和switch
的控制流關鍵字,其他關鍵字(如while
)則輸出錯誤信息。 - 不定參函數
這裏會查找到char szSQL[1024]; sprintf(szSQL , "select * from tbl where out_time > %d" , TimeGet(TIME_YYMMDDHHMM));
sprintf(
,但與普通的函數傳參不同,這裏的sprintf
是不定參的,即無法正常檢查傳參類型是否合法。不定參函數過於複雜,目前版本只處理系統庫中格式串函數。 - 其他複雜情況
過於複雜的代碼,這裏暫不考慮,目前版本只適用於一般情況。func( { TimeGet(TIME_YYMMDDHHMM) } ); array[0] = TimeGet(TIME_YYMMDDHHMM); *(p + 1) = TimeGet(TIME_YYMMDDHHMM); \\ ...
- 變量賦值
- 如果接收者的類型不合法,則可以簡單地輸出錯誤log 並結束該處
TimeGet
的檢查。如果接收者的類型合法,則需要進行遞歸檢查。遞歸檢查存在如下情況:- 接收者爲變量
此時,需要重新掃描變量的生效區域,針對unsigned uTime = TimeGet(TIME_YYMMDDHHMM); int nTime = uTime;
uTime
進行類似TimeGet
返回值的檢查。值得注意的是,如果接收者是局部變量,則只要搜索當前塊即可,如果接收者是全局變量,則需要搜索全部代碼。
目前版本未針對處理引用變量,如下問題工具無法掃描出來。unsigned uTime = 0; unsigned& rTime = uTime; rTime = TimeGet(TIME_YYMMDDHHMM); int nTime = uTime;
- 接收者爲成員變量
此時,爲了簡化邏輯,將遞歸檢查對象設定爲struct User { unsigned nLoginTime; }; void func(User& user) { user.nLoginTime = TimeGet(TIME_YYMMDDHHMM); }
User::nLoginTime
,即所有User
對象的nLoginTime
都視爲檢查目標,不關心是否真的傳遞過TimeGet
返回值。 - 接收者爲函數返回值
此時,需要檢查unsigned func() { return TimeGet(TIME_YYMMDDHHMM); }
func
所有調用位置。 - 接收者爲函數參數
此時,需要檢查void func(unsigned uTime) { int nTime = uTime; } func(TimeGet(TIME_YYMMDDHHMM));
func
參數列表中的uTime
變量。
- 接收者爲變量
標籤功能
基於cppcheck 的框架,掃描時並沒有一份全部代碼的符號庫,而是遍歷掃描每一個cpp 文件,同時間僅有當前掃描cpp 文件的完整內容,及其關聯頭文件的函數聲明等。這意味着,發現向某函參傳遞TimeGet
時,如果該函數體未被cppcheck 載入分析,此時只能判斷參數類型是否合法,無法跟蹤函數參數後續的使用是否合法。因爲上述問題,引入標籤功能,對於當次運行無法掃描的功能,記錄標籤寫入配置文件,下次運行cppcheck 時讀取標籤文件進行掃描。
標籤類型如下:
- 初始函數
[INIT_FUNCTION]
,即本例中的TimeGet
。
忽略參數可用於控制只檢查個別時間格式,如標籤類型;函數標識;追查堆棧;忽略參數 [INIT-FUNCTION];TimeGet(signed int type) at /project8/base.h;;
-1 |0 TIME_SECOND|0 TIME_DAY
表示不檢查TimeGet()
、TimeGet(TIME_SECOND)
、TimeGet(TIME_DAY)
的使用。 - 普通函數
[FUNCTION]
,即出現return TimeGet
並且返回值類型合法的函數。與[INIT_FUNCTION]
的區別在於後續掃描未增加該標籤,則會被清除(如函數丟失或函數的return
語句不再返回TimeGet
等)。標籤類型;函數標識;追查堆棧 [FUNCTION];Test() at /project8/main.cpp;$G_TimeGet at (/project8/main.cpp, 76)
- 函數參數
[FUNCTION-ARGUMENT]
,即出現func(TimeGet)
並且func
參數類型合法的情況。標籤類型;函數標識;參數編號;參數類型|參數名;追查堆棧 [FUNCTION-ARGUMENT];func(unsigned int uTime) at /project8/base.h;0;unsigned int|uTime;$G_TimeGet at (/project8/main.cpp, 83)
- 變量
[VARIABLE]
,即出現user.time = TimeGet
並且變量類型合法的情況。標籤類型;變量聲明的文件路徑|變量歸屬|變量類型|變量名;追查堆棧 [VARIABLE];/project8/base.h|User|unsigned int|time;$G_TimeGet at (/project8/main.cpp, 83)