• 范文大全
  • 公文写作
  • 工作总结
  • 工作报告
  • 医院总结
  • 合同范文
  • 党团范文
  • 心得体会
  • 讲话稿
  • 安全管理
  • 广播稿
  • 条据书信
  • 优秀作文
  • 口号大全
  • 简历范文
  • 应急预案
  • 经典范文
  • 入党志愿书
  • 感恩演讲
  • 发言稿
  • 工作计划
  • 党建材料
  • 脱贫攻坚
  • 党课下载
  • 民主生活会
  • 不忘初心
  • 主题教育
  • 对照材料
  • 您现在的位置:雨月范文网 > 经典范文 > 正文

    C和C++安全编码笔记:整数安全.docx

    来源:雨月范文网 时间:2020-09-03 点击:

     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),其它值位(非填充)表示以纯二进制表示法(与无符号类型相同)表示的该值的幅值。若要取一个原码的相反数,只要改变符号位。例如,在纯二进制表示法或原码中,0数 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).如果两个操作数拥有相同的整数类型(有符号或无符号),具有较低整数转换级别的类型的操作数会被转换到拥有较高级别的操作数的类型。例如,如果一个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 的幂次数的除法。然而,只有移位的数值为正时才是如此,原因有两个,首先,对负值右移位是算术还是逻辑移位是实现定义的。其次,即使在已知执行算术右移位的一个平台上,其结果与除法也是不同的。此外,现代编译器可以判断何时使用移位代替除法是安全的,并会在移位在它们的目标架构上更快时做这种替换。出于这些原因,并...

    推荐访问:整数 编码 笔记