1. 程式人生 > >C語言陷阱之差一錯誤

C語言陷阱之差一錯誤

差一錯誤英語Off-by-one error,縮寫OBOE)是在計數時由於邊界條件判斷失誤導致結果多了一或少了一的錯誤,通常指計算機程式設計迴圈多了一次或者少了一次的程式錯誤,屬於邏輯錯誤的一種。比如,程式設計師在迴圈中進行比較的時候,本該使用“小於等於”,但卻使用了“小於”,或者是程式設計師沒有考慮到一個序列是從0而不是1開始(許多程式語言的陣列下標都是這樣)。在數學領域,此錯誤也時有發生。

遍歷陣列

假設現在有一堆物品,按mn(含)依次編號。那麼這堆物品的總數是多少?我們可能會直覺地認為有(n - m)個物品,但這就和正確答案差了一,犯了柵欄錯誤。正確答案應該是(nm + 1)個物品。

因此,計算機領域中,涉及範圍的時候通常用半開區間來表示,從mn(含)的範圍就表示成從mn + 1(不含),以避免柵欄錯誤。例如,一個迭代五次的迴圈可以寫成0到5的半開區間:

for (i = 0; i < 5; i++) 
{
    /* 迴圈體 */
}

迴圈體首次執行時,i等於0,接著i依次變為1、2、3、4。最後,i會變為5,因此i < 5為假(不成立),迴圈結束。然而,如果在比較中用的是<=(小於等於),迴圈體則會執行六次:i依次為0、1、2、3、4、5。同樣,如果i的初值是1而不是0,那麼迴圈體只會執行四次:i依次為1、2、3、4。這些情況都能產生差一錯誤。

還有一種情況就是該用while迴圈的地方卻用了do-while迴圈(反之亦然)。Do-while的迴圈體至少會執行一次。

程式語言間的差異也會產生混淆。從0開始計數是程式語言最為常見的做法,而有些語言中卻以1起始。Pascal語言中可以自定義陣列下標的起始值。

柵欄錯誤

n個間隔的直柵欄有n+1條柵欄柱。

柵欄錯誤(有時也稱為電線杆錯誤或者燈柱錯誤)是差一錯誤的一種。如以下問題:

建造一條直柵欄(即不圍圈),長30米、每條柵欄柱間相隔3米,需要多少條柵欄柱?

最容易想到的答案10是錯的。這個柵欄有10個間隔,11條柵欄柱。

反過來,柱子數給定時,我們也很容易認為間隔數也是這麼多。實際上間隔數要比柱子數少一個。

廣義上這個問題可以這麼表述:

n個電線杆之間有多少個間隔?

如果這一列電線杆不組成一個圈,那麼正確答案是n-1,如果在此前提之下,首尾兩端也計入間隔,那麼正確答案是n+1,如果電線杆圍成一圈,那麼答案則是n。思考之前必須先明確問題的定義,一個情況下的答案可能並不適用於其他情況。柵欄錯誤往往就來源於在數物體還是數間隔的選擇上出了差錯。

除了計算長度,柵欄錯誤還會發生在其他單位中。例如,時間金字塔是一個每10年放置1塊石塊,總共要放置120塊的公共藝術作品,完成這個作品需要花費1190年,而不是1200年。早期的柵欄錯誤也與時間有關,儒略曆最開始計算閏年的方式不正確,導致每三年會有一次閏年(正常情況應該是每四年,即每三年)。

大數的差一錯誤一般都不會引起大問題。正是在小數字上,尤其是精確度要求很高的時候,差一錯誤可能造成災難。如果負責計算的人員“重蹈覆轍”,差一錯誤甚至可以累積。

安全隱患

不當使用C標準庫中的函式常常會導致差一錯誤和安全問題。程式設計師經常認為strncat()在寫入字串結束符時不會超過最大長度。事實上strncat()會在指定的最大長度之後一位元組的位置寫入字串結束符。如下程式碼:

void foo (char *s) 
{
    char buf[15];
    memset(buf, 0, sizeof(buf));
    strncat(buf, s, sizeof(buf)); // 最後一個引數應為 sizeof(buf)-1
}

差一錯誤之所以經常在使用C標準庫的時候出現,是因為C標準庫在需不需要減去一位元組這個問題上標準不統一。fgets()strncpy()這些函式在寫入的時候不會超過最大長度(fgets()會自行把長度減一,只取回(長度 - 1)位元組),而像strncat()這些函式則會越過最大長度。所以程式設計師必須牢記哪些函式需要減去一。

在某些系統上(小端序架構),差一錯誤可導致幀指標的最低位元組被覆蓋,從而使攻擊者能夠劫持呼叫例程的區域性變數,給漏洞攻擊敞開大門。

要避免這類問題,可以使用這些函式的其他改進版本,如strlcat()strlcpy(),這些改進版在寫入時會考慮緩衝區能容納的大小,因此更加“安全”。(上面程式碼的相應部分改成strlcat(buf, s, sizeof(buf))就能解決問題)

總結:

差一錯誤 和 無邊界字串複製產生的後果類似,都會對陣列產生越界寫操作,這個越界寫操作產生嚴重的記憶體溢位問題,而這個記憶體溢位問題在編編譯的時候不會報錯,甚至在執行時也不會出現問題,只是偶爾出現,這就給程式的不穩定性帶來威脅。

void main(void)

{

  char source[]="My name is Benjamin.Franklin";

  char dest[10];

  memset(dest,0,sizeof(dest));

  char *result = strcpy(dest,source);

  cout<<result<<endl;

}

輸出:My name is Benjamin.Franklin

呵呵,沒有出錯而且居然輸出全部字串。這就是無邊界字串複製。和差一錯誤產生相同陣列越界寫操作。