C和C++安全编码笔记:整数安全
5.1 整數安全導論:整數由包括0的自然數(0, 1, 2, 3, …)和非零自然數的負數(-1, -2, -3, …)構成。
5.2 整數數據類型:整數類型提供了整數數學集合的一個有限子集的模型。一個具有整數類型的對象的值是附著在這個對象上的數學值。一個具有整數類型的對象的值的表示方式(representation)是在為該對象分配的存儲空間中該值的特定位模式編碼。
在C中每個整數類型的對象需要一個固定的存儲字節數。<limits.h>頭文件中的常量表達式CHAR_BIT,給出了一個字節中的位數,它必須至少為8,但可能會更大,這取決于具體的實現。除unsigned char型外,不是所有的位都必須用來表示值,未使用的位被稱為填充(padding)。
標準的整數類型由一組有符號的整數類型和相應的無符號整數類型組成。
無符號整數類型:C要求無符號整數類型值使用無偏移的純二進制系統表示。無符號整數是計數器的自然選擇。標準的無符號整數類型(按照它們的長度非遞減排序)是:unsigned char、unsigned short int、unsigned int、unsigned long int、unsigned long long int,關鍵字int可以省略,除非它是唯一存在的整數類型的關鍵字。
特定于編譯器和平臺的整數極值記錄在<limits.h>頭文件中。牢記這些值都是特定于平臺的。出于可移植性的考慮,在代碼中應該使用具名常量而不是實際的值。
回繞:涉及無符號操作數的計算永遠不會溢出,因為不能用結果為無符號整數類型表示的結果值被該類型可以表示的最大值加1之和取模減(reduced modulo)。因為回繞,一個無符號整數表達式永遠無法求出小于零的值。
// 回繞:涉及無符號操作數的計算永遠不會溢出
void test_integer_security_wrap_around()
{unsigned int ui = UINT_MAX; fprintf(stdout, "ui value 1: %u\n", ui); // 4294967295ui++; fprintf(stdout, "ui value 2: %u\n", ui); // 0ui = 0; fprintf(stdout, "ui value 3: %u\n", ui); // 0ui--; fprintf(stdout, "ui value 4: %u\n", ui); // 4294967295//for (unsigned i = n; --i >= 0; ) // 此循環將永遠不會終止unsigned int i = 0, j = 0, sum = 0;// ... 對i, j, sum進行一些賦值運算操作if (sum + i > UINT_MAX) { } // 不會發生,因為sum+i回繞了if (i > UINT_MAX - sum) { } // 好很多if (sum - j < 0) { } // 不會發生,因為sum-j回繞了if (j > sum) { } // 正確
}
除非使用<stdint.h>中明確指定寬度(exact-width)的類型,否則回繞使用的寬度取決于實現,這意味著結果會因平臺而異。
有符號整數類型:有符號整數用于表示正值和負值,其值的范圍取決于為該類型分配的位數及其表示方式。在C中,除了_Bool類型以外,每種無符號類型都有一種對應的占用相同存儲空間的有符號類型。標準的有符號整數類型(按照長度非遞減排序,例如,long long int不能短于long int)包括如下類型:signed char、short int、int、long int、long long int,除了char類型,signed可以忽略(無修飾的char的表現要么如同unsigned char,要么如同signed char,這取決于實現,并且出于歷史原因,它被視為一個單獨的類型)。int可以省略,除非它是唯一存在的關鍵字。
所有足夠小的非負值在對應的有符號和無符號類型中有同樣的表示方式。一個稱為符號位的位被當作最高位,用于指示所表示的值是否為負。C標準允許的負數表示方法有三種,分別是原碼表示法(sign and magnitude)、反碼表示法(one’s complement)和補碼表示法(two’s complement):
(1).原碼表示法:符號位表示值為負(符號位設置為1)還是為正(符號位設置為0),其它值位(非填充)表示以純二進制表示法(與無符號類型相同)表示的該值的幅值。若要取一個原碼的相反數,只要改變符號位。例如,在純二進制表示法或原碼中,二進制數0000101011等于十進制數43,要取該值的相反數,只要設置符號位:二進制數1000101011等于十進制數-43。
(2).反碼表示法:符號位具有權數-(2^(N-1) - 1),其它值位的權數與無符號類型相同。例如,在反碼中,二進制數1111010100等于十進制數-43。假定寬度是10位,符號位具有權數-(2^9 - 1)即-511,其余位等于468,于是468-511=-43。若要取一個反碼的相反數,需要改變每一位(包括符號位)。
(3).補碼表示法:符號位具有權數-(2^(N-1)),其它值位的權數與無符號類型相同。例如,在補碼中,二進制數1111010101等于十進制數-43.假定寬度是10位,符號位具有權數-(2^9)即-512,其余位等于469,于是469-512=-43.若要取一個補碼的相反數,首先構造反碼的相反數,然后再加1(在需要時進位)。
對于數學值0,原碼和反碼都有兩種表示方式:正常0和負0(negative zero)。邏輯操作可能產生負0,但任何算術操作都不允許結果是負0,除非其中一個操作數具有一個負0表示方式。下表展示了假定在10位寬并忽略填充位時,一些有趣值的原碼、反碼和補碼表示:在使用補碼表示法的計算機上,有符號整數的取值范圍是-2^(N-1) ~ 2^(N-1) - 1。當使用反碼表示法和原碼表示法時,其取值范圍的下界變成-2^(N-1) + 1,而上界則保持不變。
有符號整數的取值范圍:下表中的”最小值”列確定每個標準有符號整數類型保證的可移植范圍。這些幅值被實現定義的具有相同符號的幅值所取代,如那些為x86-32架構所示的幅值。C標準要求標準有符號類型的最小寬度分別是:signed char(8)、short(16)、int(16)、long(32)、long long(64)。一個給定實現的實際寬度可以用<limits.h>中定義的最大可表示值作參考。這些類型對象的大小(存儲的字節數)可以由sizeof(typename)確定,這個大小包含填充位(如果有的話)。一個整數類型的最小值和最大值取決于該類型的表示方式、符號性和寬度。
整數溢出:當一個有符號整數運算的結果值不能用結果類型表示時就會發生溢出。在C中有符號整數溢出是未定義的行為,從而允許實現默默地回繞(最常見的行為)、陷阱,或兩者兼而有之。用補碼表示的一個給定類型最小負值的相反數不能以那種類型表示。
// 有符號整數溢出
void test_integer_security_overflow()
{int i = INT_MAX; // 2147483647, int最大值i++; fprintf(stdout, "i = %d\n", i); // -2147483648, int最小值i = INT_MIN; // -2147483648, int最小值i--; fprintf(stdout, "i = %d\n", i); // 2147483647, int最大值std::cout << "abs(INT_MIN): " << std::abs(INT_MIN) << std::endl; // -2147483648// 因為二進制補碼表示是不對稱的,數值0被表示為”正”數,所以用補碼表示的一個給定類型最小負值的相反數不能以那種類型表示// 對最小的負值而言,結果是未定義的或錯誤的#define abs(n) ((n) < 0 ? -(n) : (n))#undef abs
}
字符類型:在把char型用于數值時僅使用明確的signed char或unsigned char型。建議僅使用signed char和unsigned char類型存儲和使用小數值(也就是范圍分別在SCHAR_MIN和SCHAR_MAX之間,或0和UCHAR_MAX之間的值),因為這是可移植的保證數據的符號字符類型的唯一方式。平凡的char不應該被用來存儲數值,因為編譯器有定義char的自由,使其要么與signed char,要么與unsigned char具有相同的范圍、表示和行為。
// 字符類型
void test_integer_security_char()
{
{// char類型的變量c可能是有符號或無符號的// 初始值200(它具有signed char類型)無法在(有符號的)char類型中表示(這是未定義的行為)// 許多編譯器將用標準的由無符號轉換到有符號的模字大小(modulo-word-size)規則把200轉換為-56char c = 200;int i = 1000;fprintf(stdout, "i/c = %d\n", i / c); // 在windows/linux上會輸出-17, 1000/-56=-17
}{// 聲明unsigned char型變量c,使后面的除法操作與char的符號性無關,因此它有一個可預見的結果unsigned char c = 200;int i = 1000;fprintf(stdout, "i/c = %d\n", i / c); // 5
}
}
數據模型:對于一個給定的編譯器,數據模型定義了為標準數據類型分配的大小。這些數據模型通常使用一個XXXn的模式命名,其中每個X都指一個C類型,而n指的是大小(通常為32或64),通常命名為:ILP64:int、long和指針類型是64位寬;LP32:long和指針是32位寬。
其它整數類型:C也在標準頭文件<stdint.h>、<stdtypes.h>和<stddef.h>中定義了其它整數類型。這些類型包括擴展的整數類型(extended integer type),它們是可選的、由實現定義的、完全支持的擴展,與標準的整數類型一起,組成整數類型的一般類。標準頭文件中諸如whatever_t定義的標識符都是typedef(類型定義),也就是說,它們是現有類型的同義詞,而不是新類型。
size_t:是無符號整數類型的sizeof運算符的結果,它在標準頭文件<stddef.h>中被定義。size_t類型的變量保證有足夠的精度來表示一個對象的大小。size_t的最大值由SIZE_MAX宏指定。
ptrdiff_t:是一種有符號整數類型,它表示兩個指針相減的結果,并被定義在標準頭文件<stddef.h>中。當兩個指針相減時,其結果是兩個數組元素的下標之差。其結果的大小是實現定義的,且它的類型(一種有符號整數類型)是ptrdiff_t。ptrdiff_t的下限和上限分別由PRTDIFF_MIN和PTRDIFF_MAX定義。
void test_integer_security_ptrdiff_t()
{int i = 5, j = 6;typedef int T;T *p = &i, *q = &j;ptrdiff_t d = p - q;fprintf(stdout, "pointer diff: %lld\n", d);fprintf(stdout, "sizeof(ptrdiff_t): %d\n", sizeof(ptrdiff_t)); // 8
}
intmax_t和uintmax_t:是具有最大寬度的整數類型,它們可以表示任何其它具有相同符號性的整數類型所能表示的任何值,允許在程序員定義的整數類型(相同符號性)與intmax_t和uintmax_t類型之間進行轉換。
void test_integer_security_intmax_t()
{typedef unsigned long long mytypedef_t; // 假設mytypedef_t是個128位的無符號整數,其實它并不是fprintf(stdout, "mytypedef_t length: %d\n", sizeof(mytypedef_t));mytypedef_t x = 0xffff;uintmax_t temp;temp = x; // 始終是安全的mytypedef_t x2 = 0xffffffffffffffff;fprintf(stdout, "x2: %ju\n", (uintmax_t)x2); // 將保證打印正確的x2值,無論它的長度是多少
}
格式化I/O函數可用于輸入和輸出最大寬度的整數類型值。在格式字符串中的j長度修飾符表明以下d、i、o、u、x、X或n轉換說明符將適用于一個類型為intmax_t或unitmax_t的參數。
intptr_t和uintptr_t:C標準不保證存在一個整數類型,它大到足以容納一個指向對象的指針。然而,如果確實存在這樣的類型,那么它的有符號版本稱為intptr_t,它的無符號版本稱為uintptr_t。這些類型的算術運算并不保證產生一個有用的值。
獨立于平臺的控制寬度的整數類型:C語言在頭文件<stdint.h>和<inttypes.h>中引入了整數類型,它為程序員提供typedef以便他們更好地控制寬度。這些整數類型是實現定義的,并包括以下幾種類型:
(1).int#_t、uint#_t:其中#代表一個確切的寬度,如int8_t、uint32_t。
(2).int_least#_t、uint_least#_t:其中#代表寬度值,如int_least32_t、uint_least16_t.
(3).int_fast#_t、uint_fast#_t:其中#代表最快的整數類型寬度的值,如int_fast16_t、uint_fast64_t。
頭文件<stdint.h>還為擴展類型定義了表示相應的最大值(對于有符號類型,還有最小值)的常數宏。
特定于平臺的整數類型:除了在C標準中定義的整數類型,供應商通常還定義了特定于平臺的整數類型。例如,Microsoft Windows API定義了大量的整數類型,包括__int8、__int16、BOOL、CHAR、LONG64等。
5.3 整數轉換
轉換整數:轉換是一種用于表示賦值、類型強制轉換或者計算的結果值的底層數據類型的改變。從具有某個寬度的類型向一種具有更大寬度的類型轉換,通常會保留數學值。然而,相反方向的轉換很容易導致高位的損失(涉及有符號整數類型時甚至會更糟),除非該值的幅值一直足夠小,可以被正確地表示。轉換是強制轉換時顯式發生的或作為一個操作的需要而隱式發生的。雖然隱式轉換簡化了編程,但也可能會導致數據丟失或錯誤解釋。
C標準規定了C編譯器應該如何處理轉換操作,包括:整數類型提升(integer promotion)、整數轉換級別(integer conversion rank)以及普通算術轉換(usual arithmetic conversion)。
整數轉換級別:每一種整數類型都有一個相應的整數轉換級別,它決定了轉換操作將會如何執行。下面列出了C標準定義的用于決定整數轉換級別的規則:
(1).沒有任何兩種不同的有符號整數類型具有相同的級別,即使它們的表示法相同。
(2).有符號整數類型的級別比任何精度比它低的有符號整數類型的級別高。
(3).long long int類型的級別比long int高;long int的級別比int高;int的級別比short int高;short int的級別比signed char高。
(4).無符號整數類型的級別與對應的有符號整數類型的級別相同(如果相應的有符號整數類型存在的話)。
(5).標準整數類型的級別高于具有同樣寬度的擴展整數類型的級別。
(6)._Bool類型的級別應當低于所有其它標準整數類型。
(7).char、signed char和unsigned char三種類型的級別相同。
(8).與”其它具有相同精度的擴展有符號整數類型”相關的任何擴展有符號整數類型的級別由具體實現定義,但它們仍然要遵從用于決定整數轉換級別的其它規則。
(9).對T1、T2、T3三種整數類型,如果T1的級別比T2高,T2的級別又比T3高,那么T1的級別也比T3高。
C標準建議用于size_t和ptrdiff_t類型的整數轉換級別不應高于signed long int,除非該實現支持足夠大的對象使得這成為必要。
整數類型提升:如果一個整數類型具有低于或等于int或unsigned int的整數轉換級別,那么它的對象或表達式在用于一個需要int或unsigned int的表達式時,就會被提升。整數類型提升被作為普通算術轉換的一個組成部分。
void test_integer_security_promotion()
{
{int sum = 0;char c1 = 'a', c2 = 'b';// 整數類型提升規則要求把c1和c2都提升到int類型// 然后把這兩個int類型的數據相加,得到一個int類型的值,并且該結果被保存在整數類型變量sum中sum = c1 + c2;fprintf(stdout, "sum: %d\n", sum); // 195
}{signed char cresult, c1, c2, c3;c1 = 100; c2 = 3; c3 = 4;// 在用8位補碼表示signed char的平臺上,c1與c2相乘的結果可能會因超過這些平臺上signed char類型的最大值(+127)// 而引起signed char類型的溢出.然而,由于發生了整數類型提升,c1, c2和c3都被轉換為int,因此整個表達式的結果// 能夠被成功地計算出來.該結果隨后被截斷,并被存儲在cresult中.由于結果位于signed char類型的取值范圍內,因// 此該截斷操作并不會導致數據丟失或數據解釋錯誤 cresult = c1 * c2 / c3;fprintf(stdout, "cresult: %d\n", cresult); // 75
}{unsigned char uc = UCHAR_MAX; // 0xFF// 當uc用作求反運算符"~"的操作數時,通過使用零擴展把它擴展為32位,它被提升為signed int類型,因此,在// x86-32架構平臺中,該操作始終產生一個類型為signed int的負值int i = ~uc;fprintf(stdout, "i: %0x\n", i); // 0xffffff00
}
}
整數提升保留值,其中包括符號。如果在所有的原始值中,較小的類型可以被表示為一個int,那么:原始值較小的類型會被轉換成int;否則,它被轉換成unsigned int。
之所以需要整數類型提升,主要是為了防止運算過程中中間結果發生溢出而導致算術錯誤,也為了在該架構中以自然的大小執行操作。
普通算術轉換:是一套規則。一致性轉換涉及不同類型的兩個操作數。其中一個操作數或者兩個操作數都可能被轉換。很多接受整數操作數的運算符都采用普通算術轉換(usual arithmetic conversion)對其操作數進行轉換。這些運算符包括*、/、%、+、-、<、>、<=、>=、==、!=、&、^、|和條件運算符(?:)。當整數類型提升規則被同時應用到兩個操作數之后,以下規則會被應用到已提升的操作數上:
(1).如果兩個操作數具有相同的類型,則不需要進一步的轉換。
(2).如果兩個操作數擁有相同的整數類型(有符號或無符號),具有較低整數轉換級別的類型的操作數會被轉換到擁有較高級別的操作數的類型。例如,如果一個signed int操作數和一個signed long操作數并列,那么signed int操作數被轉換為signed long。
(3).如果無符號整數類型操作數的級別大于或等于另一個操作數類型的級別,則有符號整數類型操作數將被轉換為無符號整數類型操作數的類型。例如,如果一個signed int操作數和一個unsigned int操作數并列,那么signed int操作數將轉換為unsigned int。
(4).如果有符號整數類型操作數類型能夠表示無符號整數類型操作數類型的所有可能值,則無符號整數類型操作數將被轉換為有符號整數類型操作數的類型。例如,如果一個64位補碼signed long操作數和一個32補碼unsigned int操作數并列,那么unsigned int操作數將轉換為signed long。
(5).否則,兩個操作數都將轉換為與有符號整數類型操作數類型相對應的無符號整數類型。
由無符號整數類型轉換:從較小的無符號整數類型轉換到較大的無符號整數類型始終是安全的,通常通過對其值進行零擴展(zero-extending)而完成。當表達式包含不同寬度的無符號整數操作數時,C標準要求每個操作的結果都具有其中較寬的操作數的類型(和表示范圍)。假設相應的數學運算產生一個在結果類型能表示的范圍內的結果,則得到的表示值就是那個數學值。如果數學結果值不能用結果類型表示,發生的情況有兩類:無符號,損失精度;無符號值轉換成有符號值:
void test_integer_security_unsigned_conversion()
{
{ // 無符號,損失精度unsigned int ui = 300;// 當uc被賦予存儲在ui中的值時,值300以模2^8取余,或300-256=44unsigned char uc = ui;fprintf(stdout, "uc: %u\n", uc); // 44
}{ // 無符號值轉換成有符號值unsigned long int ul = ULONG_MAX;signed char sc;sc = ul; // 可能會導致截斷錯誤fprintf(stdout, "sc: %d\n", sc); // -1
}{ // 當從一個無符號類型轉換為有符號類型時,應驗證范圍unsigned long int ul = ULONG_MAX;signed char sc;if (ul <= SCHAR_MAX) {sc = (signed char)ul; // 使用強制轉換來消除警告} else { // 處理錯誤情況fprintf(stderr, "fail\n");}
}
}
(1).無符號,損失精度:僅對無符號整數類型而言,C規定:值是以模2^w(type)取余,其中2^w(type)是比可以用結果類型表示的最大值大1的數。把一個無符號整數類型的值轉換為較窄的寬度的值被良好地定義為以較窄的寬度為模取余。這是通過截斷較大值并保留其低位實現的。如果該值不能在新的類型中表示,那么數據就會丟失。當一個值不能在新的類型中表示時,任何大小的有符號和無符號整數類型之間發生的轉換都可能會導致數據丟失或錯誤解釋。
(2).無符號值轉換成有符號值:當一個大的無符號值轉換成寬度相同的有符號類型時,C標準規定,當起始值不能在新的(有符號)類型中表示時:結果是由實現定義的,或發出一個實現定義的信號。從一個無符號的類型轉換為有符號類型時,可能發生類型范圍錯誤,包括損失數據(截斷)和損失符號(符號錯誤)。當把一個大的無符號整數轉換為一個較小的有符號整數類型時,值會被截斷,且最高位變成符號位。由此產生的值可能是負的或正的,這取決于截斷后的高位值。如果該值不能在新的類型中表示,數據就會丟失(或錯誤解釋)。當從一個無符號類型轉換為有符號類型時,應驗證范圍。
下表總結了x86-32架構中無符號整數類型的轉換:
由有符號整數類型轉換:從較小的有符號整數類型轉換為較大的有符號整數類型始終是安全的,并可以采用對該值進行符號擴展的方法在補碼表示中實現:
void test_integer_security_signed_conversion()
{
{ // 有符號,損失精度signed long int sl = LONG_MAX;signed char sc = (signed char)sl; // 強制轉換消除了警告fprintf(stdout, "sc: %d\n", sc); // -1
}{ // 當從一個有符號類型轉換到精度較低的有符號類型時,應驗證范圍signed long int sl = LONG_MAX;signed char sc;if ((sl < SCHAR_MIN) || (sl > SCHAR_MAX)) { // 處理錯誤情況fprintf(stderr, "fail\n");} else {sc = (signed char)sl; // 使用強制轉換來消除警告fprintf(stdout, "sc: %d\n", sc);}
}{ // 負值和無符號值的比較固有問題unsigned int ui = UINT_MAX;signed char c = -1;// 由于整數提升,c被轉換為unsigned int類型的值0xFFFFFFFF,即4294967295if (c == ui) {fprintf(stderr, "why is -1 = 4294967295\n");}
}{ // 從有符號類型轉換為無符號類型時,可能發生類型范圍錯誤,包括數據丟失(截斷)和損失符號(符號錯誤)signed int si = INT_MIN;// 導致損失符號unsigned int ui = (unsigned int)si; // 強制轉換消除了警告fprintf(stderr, "ui: %u\n", ui); // 2147483648
}{ // 從有符號類型轉換為無符號類型時,應驗證取值范圍signed int si = INT_MIN;unsigned int ui;if (si < 0) { // 處理錯誤情況fprintf(stderr, "fail\n");} else {ui = (unsigned int)si; // 強制轉換消除了警告fprintf(stdout, "ui: %u\n", ui);}
}
}
(1).有符號,損失精度:把有符號整數類型的值轉換為更窄寬度的結果是實現定義的,或者可能引發一個實現定義的信號。一個常見的實現是截斷成較小者的尺寸。在這種情況下,所得到的值可能是負的或正的,視截斷后的高位值而定。如果該值不能在新的類型中表示,那么數據將會丟失(或錯誤解釋)。當從一個有符號類型轉換到精度較低的有符號類型時,應驗證范圍。從較高精度的有符號類型轉換為較低精度的有符號類型需要同時對上限和下限進行檢查。
(2).從有符號轉換到無符號:當有符號和無符號整數類型混合操作時,由普通算術轉換確定常見的類型,這個類型至少將具有所涉及的類型中最寬的寬度。C要求如果數學的結果能夠用那個寬度表示,那么會產生該值。當將一個有符號整數類型轉換為無符號整數類型時,反復加上或減去新類型的寬度(2^N)會使結果落在能夠表示的范圍內。當把一個有符號整數的值轉換為一個寬度相等或更大的無符號整數的值并且有符號整數的值不為負時,該值是不變的。
當將一個有符號整數類型轉換為一個寬度相等的無符號整數類型時,不會丟失任何數據,因為保留了位模式。然而,高位失去了它的符號位功能。如果有符號整數的值不為負,則該值不變。如果該值為負,則得到的無符號的值被求值為一個大的有符號整數。如果有符號的值是-2,那么相應的無符號的int值是UINT_MAX-1。從有符號類型轉換為無符號類型時,應驗證取值范圍。
下表總結了x86-32平臺上有符號整數類型的轉換:
轉換的影響:隱式轉換簡化了C語言編程。然而,轉換存在潛在的數據丟失或錯誤解釋問題。需避免導致下列結果的轉換:(1).損失值:轉換為值的大小不能表示的一種類型;(2).損失符號:從有符號類型轉換為無符號類型,導致損失符號。
唯一的對所有數據值和所有符號標準的實現都保證安全的整數類型轉換是轉換為符號相同而寬度更寬的類型。
5.4 整數操作:可能會導致異常情況下的錯誤,如溢出、回繞和截斷。當某個操作產生的結果不能在操作結果類型中表示時,就會發生異常情況。下表表示執行整數操作時可能的異常情況,不包括在操作數統一到常見的類型時應用普通算術轉換所造成的錯誤:
賦值:在簡單的賦值(=)中,右操作數的值被轉換為賦值表達式的類型并替換存儲在左操作數所指定的對象的值。用一個有符號整數為一個無符號整數賦值,或者用一個無符號整數為一個寬度相等的有符號整數賦值,都可能導致所產生的值被誤解。當從一個具有較大寬度的類型向較小寬度的類型賦值或強制類型轉換時,就會導致發生截斷。如果該值不能用結果類型表示,那么數據可能會丟失。
int f_5_4(void) { return 66; }
void test_integer_security_assignment()
{
{char c;// 函數f_5_4返回的int值可能在存儲到char時被截斷,然后在比較之前將其轉換回int寬度// 在"普通"char具有與unsigned char相同的取值范圍的實現中,轉換的結果不能為負,所以下面比較的操作數// 永遠無法比較為相等,因此,為了有充分的可移植性,變量c應聲明為int類型if ((c = f_5_4()) == -1) {}
}{char c = 'a';int i = 1;long l;// i的值被轉換為c=i賦值表達式的類型,那就是char類型,然后包含在括號中的表達式的值被轉換為括號外的賦值// 表達式的類型,即long int型.如果i的值不在char的取值范圍內,那么在這一系列的分配后,比較表達式// l == i是不會為真的l = (c = i);
}{// 用一個有符號整數為一個無符號整數賦值,或者用一個無符號整數為一個寬度相等的有符號整數賦值,// 都可能導致所產生的值被誤解int si = -3;// 因為新的類型是無符號的,那么通過反復增加或減去比新的類型可以表示的最大值大1的數,該值可以被轉換,// 直到該值落在新的類型的取值范圍內.如果作為無符號值訪問,結果值會被誤解為一個大的正值unsigned int ui = si;fprintf(stdout, "ui = %u\n", ui); // 4294967293fprintf(stdout, "ui = %d\n", ui); // -3// 在大多數實現中,通過逆向操作可以輕易地恢復原來的值si = ui;fprintf(stdout, "si = %d\n", si); // -3
}{unsigned char sum, c1, c2;c1 = 200; c2 = 90;// c1和c2相加產生的值在unsigned char的取值范圍之外,把結果賦值給sum時會被截斷sum = c1 + c2;fprintf(stdout, "sum = %u\n", sum); // 34
}
}
加法:可以用來將兩個算術操作數或者將一個指針與一個整數相加。如果兩個操作數都是算術類型,那么將會對它們執行普通算術轉換。二元的”+”運算符的結果就是其操作數的和。遞增與加1等價。如果表達式是將一個整數類型加到一個指針上,那么其結果將是一個指針,這稱為指針算術運算。兩個整數相加的結果總是能夠用比兩個操作數中較大者的寬度大1位的數來表示。任何整數操作的結果都可以用任何比其中較大者的寬度大1的類型表示。如果結果整數類型占用的位數不足以表示其結果,那么整數加法就會導致溢出或回繞。
void test_integer_security_add()
{
{ // 先驗條件測試,補碼表示: 用來檢測有符號溢出,該解決方案只適用于使用補碼表示的架構signed int si1, si2, sum;si1 = -40; si2 = 30;unsigned int usum = (unsigned int)si1 + si2;fprintf(stdout, "usm = %x, si1 = %x, si2 = %x, int_min = %x\n", usum, si1, si2, INT_MIN);// 異或可以被當作一個按位的"不等"操作,由于只關心符號位置,因此把表達式用INT_MIN進行掩碼,// 這使得只有符號位被設置if ((usum ^ si1) & (usum ^ si2) & INT_MIN) { // 處理錯誤情況fprintf(stderr, "fail\n");} else {sum = si1 + si2;fprintf(stdout, "sum = %d\n", sum);}
}{ // 一般的先驗條件測試signed int si1, si2, sum;si1 = -40; si2 = 30;if ((si2 > 0 && si1 > INT_MAX - si2) || (si2 < 0 && si1 < INT_MIN - si2)) { // 處理錯誤情況fprintf(stderr, "fail\n");} else {sum = si1 + si2;fprintf(stdout, "sum = %d\n", sum); }
}{ // 先驗條件測試:保證沒有回繞的可能性unsigned int ui1, ui2, usum;ui1 = 10; ui2 = 20;if (UINT_MAX - ui1 < ui2) { // 處理錯誤情況fprintf(stderr, "fail\n");} else {usum = ui1 + ui2;fprintf(stdout, "usum = %u\n", usum);}
}{ // 后驗條件測試unsigned int ui1, ui2, usum;ui1 = 10; ui2 = 20;usum = ui1 + ui2;if (usum < ui1) { // 處理錯誤情況fprintf(stderr, "fail\n");}
}
}
避免或檢測加法產生的有符號溢出:在C中有符號溢出是未定義的行為,允許實現默默地回繞(最常見的行為)、陷阱、飽和(固定在最大值/最小值中),或執行實現選擇的其它任何行為。
從一個更大的類型向下強制轉換:寬度為w的任意兩個有符號的整數值真正的和始終可以用w+1位表示。因此,在另外一個寬度更大的類型中執行加法將始終成功。可以對由此產生的值進行范圍檢查,然后再向下強制轉換到原來的類型。一般來說,這種解決方案是依賴于實現的,因為C標準并不能保證任何一個標準的整數類型比另一個整理類型大。
避免或檢測加法造成的回繞:對兩個無符號的值相加時,如果操作數之和大于結果類型所能存儲的最大值,就會發生回繞。雖然無符號整數回繞在C標準中被良好地定義為取模行為,但意外的回繞已導致眾多的軟件漏洞。
后驗條件測試:在操作被執行后進行,它測試操作所得到的值,以確定它是否在有效的范圍內。如果一個異常情況可能會導致顯然有效的值,那么這種做法是無效的,然而,無符號加法始終可以用于測試回繞。
減法:與加法類型,減法也是一種加法操作。對減法而言,兩個操作數都必須是算術類型或指向兼容對象類型的指針。從一個指針中減去一個整數也是合法的。遞減操作等價于減1操作。如果兩個操作之差是負數,那么無符號減法會產生回繞。
void test_integer_security_substruction()
{
{ // 先驗條件測試:兩個正數相減或兩個負數相減都不會發生溢出signed int si1, si2, result;si1 = 10; si2 = -20;// 如果兩個操作數異號,并且結果的符號與第一個操作數不同,則已發生減法溢出// 異或用作一個按位的"不等"操作.要測試符號位置,表達式用INT_MIN進行掩碼,這使得只有符號位被設置// 該解決方案只適用于適用補碼表示的架構if ((si1 ^ si2) & (((unsigned int)si1 - si2) ^ si1) & INT_MIN) { // 處理錯誤條件fprintf(stderr, "fail\n");} else {result = si1 - si2;fprintf(stdout, "result = %d\n", result);}// 可移植的先驗條件測試if ((si2 > 0 && si1 < INT_MIN + si2) || (si2 < 0 && si1 > INT_MAX + si2)) { // 處理錯誤條件fprintf(stderr, "fail\n");} else {result = si1 - si2;fprintf(stdout, "result = %d\n", result); }
}{ // 無符號操作數的減法操作的先驗條件測試,以保證不存在無符號回繞現象unsigned int ui1, ui2, udiff;ui1 = 10; ui2 = 20;if (ui1 < ui2) { // 處理錯誤條件fprintf(stderr, "fail\n");} else {udiff = ui1 - ui2;fprintf(stdout, "udiff = %u\n", udiff);}
}{ // 后驗條件測試unsigned int ui1, ui2, udiff;ui1 = 10; ui2 = 20;udiff = ui1 - ui2;if (udiff > ui1) { // 處理錯誤情況fprintf(stderr, "fail\n");}
}
}
乘法:在C中乘法可以通過使用二元運算符”*”來得到操作數的積。二元運算符”*”的每個操作數都是算術類型。操作數執行普通算術轉換。乘法容易產生溢出錯誤,因為相對較小的操作數相乘時,都可能導致一個指定的整數類型溢出。一般情況下,兩個整數的操作數的積總是可以用兩個操作數中較大的那個所用的位數的兩倍來表示。這意味著,例如,兩個8位操作數的積總是可以使用16位類表示,而兩個16位操作數的積總是可以使用32位來表示。
void test_integer_security_multiplication()
{
{ // 在無符號乘法的情況下,如果需要高位來表示兩個操作數的積,那么結果以及回繞了unsigned int ui1 = 10;unsigned int ui2 = 20;unsigned int product;static_assert(sizeof(unsigned long long) >= 2 * sizeof(unsigned int), "Unable to detect wrapping after multiplication");unsigned long long tmp = (unsigned long long)ui1 * (unsigned long long)ui2;if (tmp > UINT_MAX) { // 處理無符號回繞fprintf(stderr, "fail\n");} else {product = (unsigned int)tmp;fprintf(stdout, "product = %u\n", product);}
}{ // 保證在long long寬度至少是int寬度兩倍的系統上,不可能產生符號溢出signed int si1 = 20, si2 = 10;signed int result;static_assert(sizeof(long long) >= 2 * sizeof(int),"Unable to detect overflow after multiplication");long long tmp = (long long)si1 * (long long)si2;if ((tmp > INT_MAX) || (tmp < INT_MIN)) { // 處理有符號溢出fprintf(stderr, "fail\n");} else {result = (int)tmp;fprintf(stdout, "result = %d\n", result);}
}{ // 一般的先驗調試測試unsigned int ui1 = 10, ui2 = 20;unsigned int product;if (ui1 > UINT_MAX / ui2) { // 處理無符號回繞fprintf(stderr, "fail\n");} else {product = ui1 * ui2;fprintf(stdout, "product = %u\n", product);}
}{ // 可以防止有符號溢出,而不需要向上強制類型轉換到現有位數的兩倍的整數類型signed int si1 = 10, si2 = 20;signed int product;if (si1 > 0) { // si1是正數if (si2 > 0) { // si1和si2都是正數if (si1 > (INT_MAX / si2)) { // 處理錯誤情況fprintf(stderr, "fail\n");}} // end if si1和si2都是正數else { // si1是正數,si2不是正數if (si2 < (INT_MIN / si1)) { // 處理錯誤情況fprintf(stderr, "fail\n");}} // end if si1是正數,si2不是正數} // end fif si1是正數else { // si1不是正數if (si2 > 0) { // si1不是正數,si2是正數if (si1 < (INT_MIN / si2)) { // 處理錯誤情況fprintf(stderr, "fail\n");}} // end if si1不是正數,si2是正數else { // si1和si2都不是正數if ((si1 != 0) && (si2 < (INT_MAX / si1))) { // 處理錯誤情況fprintf(stderr, "fail\n");}} // end if si1和si2都不是正數} // end if si1不是正數product = si1 * si2;fprintf(stdout, "product = %d\n", product);
}
}
使用靜態斷言static_assert來測試一個常數表達式的值。
除法和求余:整數相除時,”/”運算符的結果是代數商的整數部分,任何小數部分都被丟棄,而”%”運算符的結果是余數。這通常稱為向零截斷(truncation toward zero)。在這兩種運算中,如果第二個操作數的值是0,則該行為是未定義的。無符號整數除法不可能產生回繞,因為商總是小于或等于被除數。但并不總是顯而易見的是,有符號整數除法也可能導致溢出,因為你可能認為商數始終小于被除數。然而,補碼的最小值除以-1時會出現整數溢出。
void test_integer_security_division_remainder()
{// 先驗條件:可以通過檢查分子是否為整數類型的最小值以及檢查分母是否為-1來防止有符號整數除法溢出的發生// 只要確保除數不為0,就可以保證不發生除以零錯誤signed long sl1 = 100, sl2 = 5;signed long quotient, result;// 此先驗條件也可測試余數操作數,以保證不可能有一個除以零錯誤或(內部)溢出錯誤if ((sl2 == 0) || ((sl1 == LONG_MIN) && (sl2 == -1))) { // 處理錯誤情況fprintf(stderr, "fail\n");} else {quotient = sl1 / sl2;result = sl1 % sl2;fprintf(stdout, "quotient = %ld, result = %ld\n", quotient, result);}
}
C11標準規定:如果a/b的商可表示,那么表達式(a/b)*b + a%b應等于a,否則,a/b和a%b的行為都是未定義的。
許多硬件平臺上把求余實現為除法運算符的一部分,它可能產生溢出。當被除數等于有符號的整數類型的最小值(負)并且除數等于-1時,求余運算過程中可能會發生溢出。
后驗條件:普通的C++異常處理機制并不允許應用程序從一個硬件異常、諸如存取違例或除以零錯誤一類的故障中恢復。微軟確實為處理這類硬件和其它異常情況提供了名為結構化異常處理(Structured Exception Handing, SEH)的設施。結構化異常處理是操作系統提供的一項設施,它不同于C++的異常處理機制。微軟為C語言提供了一套擴展,從而使C程序可以處理Win32結構化異常。在Linux環境中,類似于除法錯誤這樣的硬件異常是使用信號機制進行處理的。在Linux環境中,類似于除法錯誤這樣的硬件異常是使用信號機制進行處理的。尤其是,如果除數為0,或者商對于目的寄存器而言值太大,系統將會產生一個浮點異常(Floating Point Exception, SIGFPE)。即使是整數運算,而不是一個浮點運算所產生的異常也引發這種類型的信號。為了防止程序在這種情況下非正常終止,可以利用signal函數調用安裝一個信號處理器。
一元反(-):對一個補碼表示的有符號的整數求反,也可能產生一個符號錯誤,因為有符號整數類型的可能值范圍是不對稱的。
移位:此操作包括左移位和右移位。移位會在操作數上執行整數提升,其中每個操作數都具有整數類型。結果類型是提升后的左操作數類型。移位運算符右邊的操作數提供移動的位數。如果該數值為負值或者大于或等于結果類型的位數,那么該行為是未定義的。在幾乎所有情況下,試圖移動一個負的位數或試圖移動比操作數中存在的位數更多的位都表明一個錯誤(邏輯錯誤)。這與溢出是不同的,后者是一個表示不足。不要移動一個負的位數或移動比操作數中存在的位數更多的位。
void test_integer_security_shift()
{
{ // 消除了無符號整數左移位操作造成的未定義行為的可能性unsigned int ui1 = 1, ui2 = 31;unsigned int uresult;if (ui2 >= sizeof(unsigned int) * CHAR_BIT) { // 處理錯誤情況fprintf(stderr, "fail\n");} else {uresult = ui1 << ui2;fprintf(stdout, "uresult = %u\n", uresult);}
}{int rc = 0;//int stringify = 0x80000000; // windows/liunx will crash in sprintf functionunsigned int stringify = 0x80000000;char buf[sizeof("256")] = {0};rc = sprintf(buf, "%u", stringify >> 24);if (rc == -1 || rc >= sizeof(buf)) { // 處理錯誤fprintf(stderr, "fail\n");} else {fprintf(stdout, "value: %s\n", buf); // 128}
}
}
左移:E1<<E2的結果是E1左移E2位的位置,空出的位以0填充。如果E1是有符號類型并且是非負值,且E1 * 2E2能夠在結果類型中表示,那么這就是結果值,否則,該行為是未定義的。移位運算符和其它位運算符應僅用于無符號整數操作數。左移位可以用于代替2的冪次數的乘法運算。移位的速度會比乘法快,最好只有當目標是位操作時才使用左移位。
右移:E1>>E2的結果是E1右移E2位的位置。如果E1是一個無符號類型或有符號類型的一個非負的值,則該值的結果是E1/2E2的商的整數部分。如果E1是有符號類型的負值,那么由此產生的值是實現定義的,它可以是算術(有符號)移位。
由于左移位可以取代2的冪次數的乘法,人們通常認為右移位可以取代2的冪次數的除法。然而,只有移位的數值為正時才是如此,原因有兩個,首先,對負值右移位是算術還是邏輯移位是實現定義的。其次,即使在已知執行算術右移位的一個平臺上,其結果與除法也是不同的。此外,現代編譯器可以判斷何時使用移位代替除法是安全的,并會在移位在它們的目標架構上更快時做這種替換。出于這些原因,并為了保持代碼清晰且易于閱讀,只有在我們的目標是位操作時才應該用左移位,在執行傳統的算術時,應使用除法。
5.5 整數漏洞:安全缺陷可能是由于硬件層的整數錯誤或者是跟整數有關的不完善邏輯所造成的。當這些安全缺陷與其它情形結合起來時,就可能會產生漏洞。
回繞:并非所有無符號整數回繞都是安全缺陷。精心定義的無符號整數算術求模屬性經常被特意使用,例如,在散列算法和C標準里rand()的示例實現中就都用到了這個屬性。
void test_integer_security_wrap_around2()
{
{ // 展示了一個無符號整數回繞導致的實際漏洞的例子size_t len = 1;char* src = "comment";size_t size;size = len - 2;fprintf(stderr, "size = %u, %x, %x, %d\n", size, size, size+1, size+1); // 4294967295, ffffffff, 0, 0char* comment = (char*)malloc(size + 1);//memcpy(comment, src, size); // crashfree(comment);
}{int element_t;int count = 10;// 庫函數calloc接受兩個參數:存儲元素類型所需要的空間和元素的個數.為了求出所需內存的大小,使用元素個數// 乘以該元素類型所需的單位空間來計算.如果計算所得結果無法用類型為size_t的無符號整數表示,那么,盡管分// 配程序看上去能夠成功地執行,但實際上它只會分配非常小的內存空間.結果,應用程序對分配的緩沖區的寫操作// 可能會越界,從而導致基于堆的緩沖區溢出char* p = (char*)calloc(sizeof(element_t), count);free(p);
}{int off = 1, len = 2;int type_name;// 這里的off和len都聲明為signed int.因為根據C標準的定義,sizeof運算符返回的是一個無符號整數類型(size_t),// 整數轉換規則要求在那些signed int的寬度等于size_t的寬度的實現上,len - sizeof(type_name)被計算為無符號// 的值,如果len比sizeof運算符返回的值小,那么減法操作會回繞并產生一個巨大的正值std::cout<<"len - sizeof(type_name): "<<len - sizeof(type_name)<<std::endl; // 18446744073709551614if (off > len - sizeof(type_name)) return;// 要消除以上問題,可以把整數范圍檢查編寫為下列替代形式// 程序員仍然必須保證這里的加法操作不會導致回繞,這是通過保證off的值在一個已定義的范圍內實現的.為了消除// 潛在的轉換錯誤,在本例中也應當把off和len都聲明為size_t類型if ((off + sizeof(type_name)) > len) return;
}
}
轉換和截斷錯誤:
void test_integer_security_conversion_truncation()
{
{ // 由轉換錯誤導致的安全漏洞int size = 5;int MAX_ARRAY_SIZE = 10;// 如果size為負數,此檢查將通過,而malloc()函數將被傳入一個為負的大小.因為malloc()需要size_t類型的參數,// 所以size會被轉換成一個巨大的無符號數.當有符號整數類型被轉換為一個無符號的整數類型時,會重復加上或減去// 新類型的寬度(2^N),以使結果落在可表示的范圍之內.因此,這種轉換可能會導致大于MAX_ARRAY_SIZE的值.這種// 錯誤可以通過把size聲明為size_t而不是int來消除if (size < MAX_ARRAY_SIZE) { // 初始化數組char* array = (char*)malloc(size);free(array);} else { // 處理錯誤fprintf(stderr, "fail\n");}
}{ // 由整數截斷錯誤導致的緩沖區溢出漏洞char* argv[3] = {"", "abc", "123"};unsigned short int total;// 攻擊者可能會提供兩個總長度無法用unsigned short整數total表示的字符做參數,這樣,總長度值將會用比結果// 類型所能表示的最大值大1的數取模截斷,函數strlen返回一個無符號整數類型size_t的結果,對于大多數實現而言,// size_t的寬度大于unsigned short的寬度,必然要進行降級操作,strcpy和strcat的執行將導致緩沖區溢出total = strlen(argv[1]) + strlen(argv[2]) + 1;char* buff = (char*)malloc(total);strcpy(buff, argv[1]);strcat(buff, argv[2]);fprintf(stdout, "buff: %s\n", buff);free(buff);
}
}
非異常的整數邏輯錯誤:
void test_integer_security_integer_logic()
{int* table = nullptr;int pos = 50, value = 10;if (!table) {table = (int*)malloc(sizeof(int) * 100);}// 由于對插入位置pos缺乏必要的范圍檢查,因此將會導致一個漏洞.因為pos開始時被聲明為有符號整數,即傳遞// 到函數中的值既可正又可負if (pos > 99) return;// 如果pos是一個負值,那么value將會被寫入實際緩沖區起始地址pos*sizeof(int)字節之前的位置// 消除安全缺陷:將形式參數pos聲明為無符號整數類型,或者把同時檢查上屆和下界作為范圍檢查的一部分table[pos] = value; // 等價于: *(int*)((char*)table+(pos*sizeof(int))) = value;free(table);
}
5.6 緩解策略:整數漏洞是由整數類型范圍錯誤(integer type range error)所引起的。例如,發生整數溢出是因為在整數操作時產生了超過特定整數類型表示范圍的數值。發生截斷錯誤是因為結果被存放在一個對它而言過小的類型中。數據轉換,特別是那些由于賦值或強制類型轉換產生的轉換,會導致轉換后的值超出結果類型范圍。
整數類型的選擇:應使用無符號整數表示不可能是負數的整數值,而且應使用有符號整數值表示可以為負的值。在一般情況下,應該使用完全可以代表任何特定變量可能值的范圍的最小的有符號或無符號類型,以節省內存。當內存消耗不是問題時,你可以決定把變量聲明為signed int或unsigned int,以盡量減少潛在的轉換錯誤。
void test_integer_security_type_selection()
{char* argv = "";// 次優的:首先,大小不會是負值,因此,沒有必要使用一個有符號整數類型;其次,short整數類型對于可能的對象// 大小可能不具有足夠的范圍short total1 = strlen(argv) + 1;// 無符號size_t類型,是C標準委員會為了表示對象大小而引入的,此類型的變量都保證有足夠的精度來表示一個對象的大小size_t total2 = strlen(argv) + 1;// C11附錄K引入一個新類型rsize_t,它被定義為size_t,但明確地用于保存單個對象的大小
#ifdef _MSC_VERrsize_t total3 = strlen(argv) + 1;
#endif
}
任何用于表示一個對象大小的變量,包括用作大小、索引、循環計數器和長度的整數值,如果可以,都應該聲明為rsize_t,或聲明為size_t。
抽象數據類型:數據抽象可以用標準和擴展的整數類型無法做到的方式支持數據的范圍。
任意精度算術:有效地提供了一個新的整數類型,其寬度只受主機系統可用內存限制。有很多任意精度算術的包可供使用,盡管它們主要用于科學計算,然而它們也能用于解決由于表示法缺少精度而引起的整數類型范圍錯誤問題。
GNU多精度算術庫(GMP):GNU Multiple-Precision Arithmetic library,是一個用C編寫的可移植的庫,用于對整數、有理數以及浮點數進行任意精度的算術運算。
C語言解決方案:可以通過在編譯器的類型系統中添加任意精度的整數來實現一種防止整數算術溢出的語言解決方案。
范圍檢查:《C安全編碼標準》有一些防止范圍錯誤的規則:
(1).確保無符號整數運算不回繞;
(2).確保整數的轉換不會導致數據丟失或錯誤解釋;
(3).確保對有符號整數的操作不會導致溢出。
在不可能發生范圍錯誤的情況下,提供范圍檢查是不太重要的。
前提條件和后驗條件測試:
void test_integer_security_conditions_test()
{
{ // 兩個無符號整數加法是否回繞的先驗條件測試unsigned int ui1, ui2, usum;ui1 = 10; ui2 = 20;if (UINT_MAX - ui1 < ui2) { // 處理錯誤情況fprintf(stderr, "fail\n");} else {usum = ui1 + ui2;}
}{ // 確保有符號的乘法運算不會導致溢出的嚴格的符合性測試signed int si1, si2, result;si1 = 10; si2 = -20;if (si1 > 0) {if (si2 > 0) {if (si1 > (INT_MAX / si2)) { // 處理錯誤情況fprintf(stderr, "fail\n");}} else {if (si2 < (INT_MIN / si1)) { // 處理錯誤情況fprintf(stderr, "fail\n");}}} else {if (si2 > 0) {if (si1 < (INT_MAX / si2)) { // 處理錯誤情況fprintf(stderr, "fail\n");}} else {if ((si1 != 0) && (si2 < (INT_MAX / si1))) { // 處理錯誤情況fprintf(stderr, "fail\n");}}}result = si1 * si2;
}{ // 后驗條件測試可用于檢測無符號整數回繞,因為這些操作被定義為取模操作unsigned int ui1, ui2, usum;ui1 = 10; ui2 = 20;usum = ui1 + ui2;// 用這種方式檢測范圍錯誤代價可能相對較高if (usum < ui1) { // 處理錯誤情況fprintf(stderr, "fail\n");}
}
}
安全整數庫:可以用來提供安全的整數運算,它們要么成功,要么報告錯誤。
溢出檢測:C標準定義了<fenv.h>頭文件來支持浮點異常狀態標志、IEC60559和類似的浮點狀態信息所需的定向舍入控制模式。
編譯器生成的運行時檢查:
(1).微軟Visual Studio運行時錯誤檢查:可用/RTCc編譯標志啟用本機運行時檢查,它檢測導致數據丟失的賦值。在程序的發行(優化)版構建中運行時錯誤檢查不工作。
(2).GCC -ftrapv標志:GCC提供了一個-ftrapv編譯器選項,該選項對在運行時檢測整數溢出提供了有限的支持。
可驗證范圍操作:飽和(saturation)和取模回繞(modwrap)算法和限制范圍內使用的技術產生的整數結果總是在定義的范圍內。這個范圍位于整數值MIN和MAX(含)之間,這里MIN和MAX是兩個可表示的整數,且MIN比MAX小。
仿佛無限范圍整數模型:為了使程序行為與程序員常用的數學推理有更大的一致性,仿佛無限范圍(As-If Infinitely Ranged, AIR)整數模型保證,要么整數值相當于使用無限范圍的整數得到的,要么就發生運行時異常。
測試和分析:靜態分析,無論是由編譯器還是一個靜態分析儀執行的,都可用于檢測源代碼中潛在的整數范圍錯誤。這些問題一旦被確定,就可以通過使用適當的整數類型或添加邏輯修改你的程序,以確保可能值的范圍在你所使用的類型范圍內,從而修正它們。靜態分析容易產生誤報(false positive)。誤報是被編譯器或分析儀錯誤地診斷為錯誤的編程結構。提供既可靠(無漏報)又完備(無誤報)的分析是很難(或不可能)的。免費提供的開源靜態分析工具的兩個例子是ROSE和Splint。
以上代碼段的完整code見:GitHub/Messy_Test
GitHub:https://github.com/fengbingchun/Messy_Test
總結
以上是生活随笔為你收集整理的C和C++安全编码笔记:整数安全的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 对称加密算法AES之GCM模式简介及在O
- 下一篇: ASN.1简介及OpenSSL中ASN.