建议和规则
建议:
- 理解编译器所使用的数据模型
- 使用rsize_t或size_t类型表示所有表示对象长度的整数值
- 理解整数转换规则
- 使用安全的整数库
- 对来自不信任来源的整数值实行限制
- 如果输入函数无法处理所有可能出现的错误就不要用它们转换字符数据
- 使用strtol()或相关函数把字符串标记换换为整数
- 只使用显式的有符号或无符号char类型表示数值
- 验证所有的整数值位于范围内
- 保证枚举常量映射到唯一的值
- 使用%操作符时不要假设余数总是正的
- 把指针转换为整数或者把整数转换为指针时需要小心
- 当普通的整数位段用于表达式时,不要对它的类型作出假设
- 只对无符号操作数使用位操作符
- 避免在同一个数据上执行位操作和算术运算
- 在程序员定义的整数类型的格式化I/O中使用intmax_t或者unitmax_t
规则:
- 保证无符号整数运算不产生回绕
- 保证整型转换不会丢失或错误解释数据
- 保证有符号整数运算不会产生溢出
- 保证除法和求模运算不会导致除零错误
- 移位的数量不能是负数或大于操作数的位数
- 把整型表达式比较或赋值为一种较大类型之前用这种较大类型对它进行求值
本文地址:http://wuyudong.com/2016/09/11/2713.html,转载请注明源地址。
理解编译器所使用的数据模型
<limits.h>除了提供特定类型的位数信息之外,还定义了一些宏,可以用于确定任何遵循标准的编译器所使用的标准整数类型数的整型范围
e.g. UNIT_MAX是unsigned int可以出现的最大值,LONG_MIN是long int可以出现的最小值
<sdtint.h>头文件在类型中引入了特定的长度限制,可用于避免对特定数据模型的依赖
e.g. int_least32_t是编译器所支持的最小有符号整数类型,它包含了至少32位
所有编译器要求提供的类型:
<inttypes.h>声明了用于操纵最大宽度整数以及把数值字符串转换为最大宽度整数的函数
代码:
unsigned int a, b; unsigned long c; //初始化a和b c = (unsigned long)a * b; //可能会溢出
解决方案:
#if UNIT_MAX > UNITMAX / UNIT_MAX
#error No safe type is available.
#endif
unsigned int a, b;
unsigned long c;
//初始化a和b
c = (unitmax_t)a * b;
使用rsize_t或size_t类型表示所有表示对象长度的整数值
size_t类型表示sizeof操作符执行结果的无符号整型类型。size_t类型的变量保证具有足够的精度,能够表示一个对象的长度。2007年引入了一种新的类型rsize_t,它被定义为size_t,但是明确用于保存单个对象的长度,任何表示对象长度的变量,包括作为大小、索引、循环计数器和长度的整数值,如果有可能,都应该声明为rsize_t,否则就声明为size_t
代码:
char *copy(size_t n, const char *str) { int i; char *p; if(n == 0) { //error... } p = (char*)malloc(n); if(p == NULL) { //error... } for(i = 0; i < n; ++i) { p[i] = *str++; } return p; } char *p = copy(9, "hi there");
当n > INT_MAX,i超过INT_MAX时,i的值将是从(INT_MIN)开始的负值,p[i]所引用的内存位置是在p所引用的内存之前,导致写入发生在数组边界之外
解决方案:
char *copy(rsize_t n, const char *str) { int i; char *p; if(n == 0 || n > RSIZE_MAX) { //error... } p = (char*)malloc(n); if(p == NULL) { //error... } for(i = 0; i < n; ++i) { p[i] = *str++; } return p; } char *p = copy(9, "hi there");
理解整数转换规则
转换可以作为类型转换的结果显式或隐式地发生,C99的整数转换规则定义了C编译器如何处理转换,这些规则包括
整型提升、整型转换秩和寻常算术转换。
目的是保证转换结果是相同的值,并且这些值对计算所产生的影响是最小的
整型提升
在执行算术运算的时候,小于int的整数类型将会提升,较小的转换为int类型,否则转换为unsigned int类型
代码:
#include<stdio.h>
int main(void){
signed char cresult, c1, c2, c3;
c1 = 100;
c2 = 3;
c3 = 4;
cresult = c1 * c2 / c3;
printf("cresult= %d\n", cresult); //cresult= 75
return 0;
}
假设signed char是用8位值表示,c1 * c2 = 300就无法表示,但是由于整数提升的缘故,c1,c2,c3都转换为int类型
使用安全的整数库
安全整数库的一个例子是IntegerLib,可以免费使用,这个库的目标是提供一些工具函数的集合,在编写C程序的时候帮助软件开发人员避免整数溢出、整数截断和符号错误
对来自不信任来源的整数值实行限制
所有不信任来源的整数值都应该求值,以确定是否存在可确认的上界和下界
代码:
int create_table(size_t length) { char **table; if(sizeof(char *) > SIZE_MAX / length) { return -1; //溢出 } size_t table_length = length * sizeof(char*); table = (char**)malloc(table_length); if(table == NULL) { return -1; //error } /*...*/ return 0; }
length可接受的范围定义为[1, MAX_TABLE_LENGTH]
解决方案:
enum {MAX_TABLE_LENGTH = 256}; int create_table(size_t length) { size_t table_length; char **table; if(length == 0 || length > MAX_TABLE_LENGTH) { // 假设MAX_TABLE_LENGTH * sizeof(char *) < SIZE_MAX return -1; //error } assert(length <= SIZE_MAX / sizeof(char *)); table_length = length * sizeof(char*); table = (char**)malloc(table_length); if(table == NULL) { return -1; //error } /*...*/ return 0; }
如果输入函数无法处理所有可能出现的错误就不要用它们转换字符数据
如果输入函数无法处理所有可能出现的输入,就不要使用它们把输入值转换为整数。比如:scanf()、fscanf()、vscanf()、vfscanf()这些函数可以处理合法的整数值,但是缺乏对非法值的健壮错误处理功能。另一种方法是把字符数据输入以null字符结尾的字节字符串形式,并利用strtol()或者相关函数把它转换为一个整数值。
代码:
long sl;
if(scanf("%ld", &sl) != 1) {
/*处理错误*/
}
一般而言,不要使用scanf对输入字符串的整数或浮点数进行解析,因为输入中可能包含实参类型无法表示的数
解决方案:
char buff[25]; char *end_ptr; long sl; if(fgets(buf, sizeof(buff), stdin) == NULL) { if(puts("EOF or read error\n") == EOF) { //处理错误 } } else { errno = 0; sl = strtol(buff, &end_ptr, 10); if(ERANGE == errno) { if(puts("number out of range\n") == EOF) { //处理错误 } } else if(end_ptr == buf) { if(puts("not valid numberic input\n") == EOF) { //处理错误 } } else if('\n' != *end_ptr && '\0' != *end_ptr) { if(puts("extra characters on input line\n") == EOF) { //处理错误 } } }
只使用显式的有符号或无符号char类型表示数值
代码:
char c = 200;
int i = 1000;
printf("i/c = %d\n", i/c); //" i/c = -17 "
解决方案:
unsigned char c = 200;
int i = 1000;
printf("i/c = %d\n", i/c); //" i/c = 5 "
验证所有的整数值位于范围内
保证枚举常量映射到唯一的值
代码:
enum {red = 4, orange, yellow, green, blue, indigo = 6, violet}; //yellow=indigo=6
解决方案:
enum {red, orange, yellow, green, blue, indigo, violet}; //不提供显式的整数赋值 enum {red, orange, yellow, green, blue, indigo, violet}; //只提供第一个成员赋值 enum { //为所有的成员赋值 red = 4, orange = 5, yellow = 6, green = 7, blue = 8, indigo = 6, violet = 7 };
使用%操作符时不要假设余数总是正的
C99对%操作符的定义提示了下列行为:
17%3=2
17%(-3)=2
-17%3=-2
-17%(-3)=-2
结果与被除数具有相同的符号
代码:
int insert(int index, int *list, int size, int value) { if(size != 0) { index = (index + 1) % size; list[index] = value; return index; } else { return -1; } }
解决方案1(使用一个修正的取模内联函数):
inline imod(int i, int j) { return (i % j) < 0 ? (i % j) + (j < 0 ? -j : j) : i % j; }
解决方案2(使用size_t):
int insert(size_t index, int *list, size_t size, int value) { if(size != 0) { index = (index + 1) % size; list[index] = value; return index; } else { return -1; } }
保证无符号整数运算不产生回绕
C99规定(无符号整数将会回绕):涉及无符号操作数的计算不会溢出,因为无法由最终的无符号整数类型表示的结果将会根据这种最终类型可以表示的最大值加1执行求模操作
整数运算:
来自不信任来源的整数值如果按下面方式之一使用,就不允许回绕:
- 作为数组索引
- 在任何指针运算中
- 作为对象的长度或大小
- 作为数组的上届(例如循环计数器)
- 作为内存分配函数的实参
- 在安全关键代码中
代码:
unsigned int ui1, ui2, sum;
//初始化ui1, ui2
sum = ui1 +ui2;
解决方案:
unsigned int ui1, ui2, sum; //初始化ui1, ui2 if(UINT_MAX - ui1 < ui2) { //处理错误 } else { sum = ui1 +ui2; }
参考资料
《C安全编码标准》
Comments