3.2 變數值跟蹤


3.2.1 變數值初始化跟蹤


    早期的變數值跟蹤技術主要是對變數值的初始化進行跟蹤,和變數初始化相關的LINT消息主要是644, 645 ("變數可能沒有初始化"), 771, 772 ("不可靠的初始化"), 530 ("未初始化的"), and 1401 - 1403 ("成員 ... 未初始化")。以下面的程式碼為例:
if( a ) b = 6;
else c = b;    // 530 message
a = c;        // 645 message


假設bc在之前都沒有初始化,PC-Lint就會報告b沒有初始化(在給c賦值的時候)和c可能沒有被初始化(在給a賦值的時候)的消息。而whilefor迴圈語句和上面的if語句稍微有所不同,比較下面的程式碼:
while ( n-- )
{
b = 6;
...
}
c = b;  //772 message


假設b在使用之前沒有被初始化,這裏會報告b可能沒有初始化的消息(當給c賦值時)。之所以 會有這樣的區別,是因為程式設計者可能知道這樣的循環體總是會被至少執行一次。相反,前面的if語句,對於程式設計者來說比較難以確定if語句是否總會被 執行,因為如果是這樣的話,這樣的if語句就是多餘的,應該被去掉。While語句和if比較相似,看下面的例子:
switch ( k )
{
case 1: b = 2; break;
case 2: b = 3;
/* Fall Through */
case 3: a = 4; break;
default: error();
}
c = b;   //645 message


儘管b在兩個不同的地方被賦值,但是仍然存在b沒有被初始化的可能。因此,當b賦值給c的時候,就會產生可能沒有初始化的消息。為了解決這個問題, 你可以在switch語句之前給b賦一個預設值。這樣PC-Lint就不會產生警告消息,但是我們也失去了讓PC-Lint檢查後續的程式碼修改引起的變數 初始化問題的機會。更好的方法是修改沒有給b賦值的case語句。
    如果error()語句代表那些不可能發生的事情發生了,那麼我們可以讓PC-Lint知道這一段其實是不可能執行的,下面的程式碼表明了這一點:
switch ( k )
{
case 1: b = 2; break;
case 2:
case 3: b = 3; a = 4; break;
default: error();
/*lint -unreachable */
}
c = b;
注意:這裏的-unreachable應該放在error()後面,break的前面。另外一個產生沒有初始化警告的方式是傳遞一個指標給free(或者採用相似的方法)。比如:
if( n ) free( p );
...
p->value = 3;
在訪問p的時候會產生p可能沒有被初始化的消息。對於goto語句,前向的goto可能產生沒有初始化消息,而向後的goto 會被忽略掉這種檢查。
if ( a ) goto label;
b = 0;
label: c = b;
當在一個大的專案中使用未初始化變數檢查時,可能會產生一些錯誤的報告。這種報告的產生,很大一部分來自於不好的程式設計風格,或者包括下面的結構:
if( x ) initialize y
...
if( x ) use y
當出現這種情況時,可以採用給y賦初始值的方式,或者利用選項-esym(644,y)關掉變數y上面的初始化檢查。


3.2.2 變數值跟蹤


    變數值跟蹤技術從賦值語句、初始化和條件語句中收集資訊,而函數的參數被默認為在正確的範圍內,只有在從函數中可以收集到的資訊與此不符的情況下才產生警告。與變數值跟蹤相關的消息有:
(1) 訪問位址越界消息(消息415661796
(2) 0除消息(54414795
(3) NULL指標的錯誤使用(413613794
(4) 非法指標的建立錯誤(416662797
(5) 冗餘的布林值測試(774


    看下面的例子:
int a[10];
int f()
{
int k;
k = 10;
return a[k]; // Warning 415
}
這個語句會產生警告415(通過 '[' 訪問越界的指針),因為PC-Lint保存了賦給k的值,然後在使用k的時候進行了判斷。如果我們把上面的例子稍加修改:
int a[10];
int f( int n )
{
int k;
if ( n ) k = 10;
else k = 0;
return a[k]; // Warning 661
}
這樣就會產生警告 661 (可能訪問越界指針) 使用可能是因為不是所有的路徑都會把10賦值給kPC-Lint不僅收集賦值語句和初始化,還從條件語句中收集值的資訊。比如下面的例子:
int a[10];
int f( int k, int n )
{
if ( k >= 10 ) a[0] = n;
return a[k]; // Warning 661 -- k could be 10
}
這裏仍然產生661警告,因為PC-Lint檢測到,在使用k的時候,k的值>=10。另外,對於函數來說,它總是假設K是正確的,程式使用者知道他們要做些什麼,所以下面的語句不會產生警告:
int a[10];
int f( int k, int n )
{ return a[k+n]; } // no warning
和檢查變數沒有初始化一樣,還可以檢查變數的值是否正確。比如,如果下面例子中的迴圈一次都沒有運行,k可能會超出範圍。這時候會產生消息796 (可預見的位址訪問越界).
int a[10];
int f(int n, int k)
{
int m = 2;
if( k >= 10 ) m++; // Hmm -- So k could be 10, eh?
while( n-- )
{ m++; k = 0; }
return a[k]; // Info 796 - - k could still be 10
}
下面的例子演示了可能使用NULL指標的問題:
int *f( int *p )
{
if ( p ) printf( "\n" ); // So -- p could be NULL
printf( "%d", *p ); // Warning
return p + 2; // Warning
}
裏會產生兩個警告,因為可能使用了NULL指標,很明顯,這兩個語句應該在if語句的範圍內。為了使你的程式更加健壯,你可能需要打開Pointer- parameter-may-be-NULL這個開關(+fpn)。這個選項假設所有傳遞到函數中的指標都有可能是NULL的。陣列邊界值在高位被檢測, 也就是說
int a[10]; ... a[10] = 0;
被檢測了,而a[-1]卻檢測不到。PC-Lint中有兩個消息是和指標的越界檢查有關的,一個是越界指標的建立,另外一個是越界指標的訪問,也就是通過越界指標獲取值。在ANSI C([1]3.3.6)中,允許建立指向超過陣列末尾一個單元的指標,比如:
int a[10];
f( a + 10 ); // OK
f( a + 11 ); // error
但是上面建立的兩個指標,都是不能訪問的,比如:
int a[10], *p, *q;
p = a + 10; // OK
*p = 0; // Warning (Access error)
p[-1] = 0; // No Warning
q = p + 1; // Warning (creation error)
q[0] = 0; // Warning (access error)
布林條件檢查不象指標檢查那麼嚴格,但是它會對恒真的布林條件產生警告,比如:
if ( n > 0 ) n = 0;
else if ( n <= 0 ) n = -1; // Info 774
上面的程式碼會產生警告(774),因為第二個條件檢查是恒真的,可以忽略。這種冗餘程式碼不會導致問題,但它的產生通常是因為邏輯錯誤或一種錯誤可能發生的徵兆,需要詳細的檢查。


3.2.3 使用assert(斷言)進行補救


    在某些情況下,雖然根據程式碼我們可以知道確切的值,但是PC-Lint卻無法獲取所有情況下變數的值的範圍,這時候會產生一些錯誤的警告資訊,我們可以使用assert語句增加變數取值範圍資訊的方法,來抑制這些錯誤的警告資訊的產生。下面舉例來說明:
char buf[4];
char *p;
strcpy( buf, "a" );
p = buf + strlen( buf ); // p is 'possibly' (buf+3)
p++; // p is 'possibly' (buf+4)
*p = 'a'; // Warning 661 - possible out-of-bounds reference
PC-Lint無法知道在所有情況下變數的值是多少。在上面的例子中,產生警告的語句其實並不會帶來什麼危害。我們可以直接使用
*p = 'a'; //lint !e661
來抑制警告。另外,我們還可以使用assert工具來修正這個問題:
#include
...
char buf[4];
char *p;
strcpy( buf, "a" );
p = buf + strlen( buf );
assert( p < buf + 3 ); // p is 'possibly' (buf+2)
p++; // p is 'possibly' (buf+3)
*p = 'a'; // no problem
由於assertNDEBUG被定義時是一個空操作,所以要保證Lint進行的時候這個宏沒有被定義。


    為了使assert()和你的編譯器自帶的assert.h一起產生上面的效果,你需要在編譯選項檔中添加一個選項。例如,假設assert 是通過以下的編譯器巨集定義實現的:
#define assert(p) ((p) ? (void)0 : __A(...))
考慮到__A()會彈出一個消息並且不會返回,所以這個需要添加的選項就是:
-function( exit, __A )
這個選項將exit函數的一些非返回特徵傳遞給__A函數。做為選擇結果,編譯器可能將assert實現成一個函數,例如:
#define assert(k) _Assert(k,...)
為了讓PC-lint知道_Assert是一個assert函數,你需要使用-function( __assert, _Assert )選項或-function( __assert(1), _Assert(1) )選項複製__assert()函數的語義
許多編譯器的編譯選項檔中已經存在這些選項了,如果沒有的話,你可以複製一個assert.h檔到PC-lint目錄下(這個目錄由於使用了-i選項,檔搜索的順序優先於編譯器的頭檔目錄)。


 

arrow
arrow
    全站熱搜

    立你斯 發表在 痞客邦 留言(0) 人氣()