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); // 4294967295
	ui++; fprintf(stdout, "ui value 2: %u\n", ui); // 0
	ui = 0; fprintf(stdout, "ui value 3: %u\n", ui); // 0
	ui--; 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轉換爲-56
	char 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=44
	unsigned 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,即4294967295
	if (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); // 4294967293
	fprintf(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 function
	unsigned 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, 0
	char* comment = (char*)malloc(size + 1);
	//memcpy(comment, src, size); // crash
	free(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; // 18446744073709551614
	if (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_VER
	rsize_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

GitHubhttps://github.com/fengbingchun/Messy_Test

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章