C语言安全性提升
Published:
简介
C语言中数组越界、野指针、内存泄漏一直是让人头疼的问题,这些问题当然可以通过良好的程序设计和编码习惯来减少,但是却很难完全解决,所以出现了安全型语言Rust。
数组越界
C语言数组越界的原因是数组、指针数组索引超过了索引范围。
// Scenario 1
int arr[4];
int a = arr[4];
// Scenario 2
int b = rand();
int c = arr[b];
// Scenario 3
int *p = arr;
int d = p[4];
- Scenario 1
为了保证数组的访问安全,如今编译器可以在编译时进行一些判断,如 “ int a = arr[4];” 这个错误就可以被找出来,原因是编译器知道 arr 的索引范围,如果索引的值是可知的,就能在编译的时候对索引的值进行有效性判断。
- Scenario 2
但是当索引是一个变量,并且这个变量的值未知时编译器就无法发现这个错误,如” int c = arr[b];” 。要解决这个错误就需要在运行时进行检查,在C语言中现在一般的方法是在访问数组之前手动添加代码判断数组索引的值,但是这是一个繁琐的过程,而运行时检查索引这个过程是可以通过编译器自动添加代码来实现的,所以编译器需要实现这个特性。这也是很多语言解决数组越界的方法。
- Scenario 3
最后,如果是指针数组,编译器无法知道索引的有效范围,所以无法通过编译器生成能在运行时检查索引的代码,要实现通过编译器生成能在运行时检查索引的代码,我们需要对指针做改变。在C语言中,指针长度为操作系统的位宽,指针只保存内存的地址,现在我们需要把他改变成2倍操作系统位宽,多出一个操作系统位宽的空间保存数组索引范围。
┌──────────────────┐
int *p = 0x22446688; ┌────┤ memory address │
│ └──────────────────┘
│
│ ┌──────────────────┬──────────────────┐
int *p = 0x22446688 @ 4; └───►│ memory address │ array length │
└──────────────────┴──────────────────┘
指针赋值时,使用 “int *p = 0x22446688 @ 4;” 这样的格式,0x22446688 是指针指向的内存的地址,4 是指针数组索引的范围,即指针数组的长度。这样编译器能就知道指针数组的索引范围,从而生成能在运行时检查索引的代码。
规定
- 直接获取指针 P 的值返回指针指向的内存的地址。
int *p = 0x22446688 @ 4;
unsigned int addr = p; // addr -> 0x22446688
- 如果指针 p 在定义时未被赋值,编译器应该初始化 p 保存的内存地址为0,数组索引范围为0。
int *p;
unsigned int addr = p; // addr -> 0x00000000
- &p 返回指针 p 的内存地址。
int *p; // assumption p is at memory 0x12345678
unsigned int addr = &p; // addr -> 0x12345678
int **pp = &p @ 1; // pp -> 0x12345678 @ 1
- 为获取 array length,增加类似 sizeof() 的关键字 rangeof()。
int *p;
unsigned int len = rangeof(p); // len -> 0
p = 0x22446688 @ 4;
len = rangeof(p); // len -> 4
运行时检查索引
运行时检查索引可能会牺牲部分性能,所以这个特性可以成为一个编译选项,在代码中全局或者局部打开、关闭,例如在代码调试、预览阶段打开,正式阶段关闭,在性能要求高、能保证安全性的代码部分一直关闭,在安全至上的代码部分一直打开,实现性能与安全的兼顾。用户可以根据自己的需要自行配置。
野指针、内存泄漏
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
malloc的内存没有被回收(被永久占用),那就造成了内存泄漏。
// Scenario 1
int *p;
p[4] = 0x00000000;
// Scenario 2
int *p1 = malloc(sizeof(int) * 0x10);
free(p1);
p1[8] = 0x00000000;
// Scenario 3
int *p2 = malloc(sizeof(int) * 0x10);
int *p3 = p2;
free(p2);
p3[8] = 0x00000000;
// Scenario 4
int *p1 = malloc(sizeof(int) * 0x10);
int *p1 = malloc(sizeof(int) * 0x10);
下面在前面更改指针类型的基础上讨论。
- Scenario 1
如果指针 p 在定义时未被赋值,编译器应该初始化 p 保存的内存地址为0,数组索引范围为0。如果可知指针内存地址为0,编译器直接判定为错误。
- Scenario 2
编写新的 free 函数,函数 “void free(void *__ptr)” 改为 “void free(void **__ptr)” ,参数变为保存应该被释放内存地址的指针的地址,free函数除了释放指针保存的内存后,还设置指针保存的内存地址为0,数组索引范围为0。
- Scenario 3
这一个需要良好的设计来避免。
- Scenario 4
这也需要良好的设计来避免。
有一个方法,就是在内存管理系统中实现内存分配记录,当 malloc 被调用时,malloc 获取到调用处的程序地址,以及被调用的时间,并连同分配的内存的地址一起记录下来,当 free 时这删除记录。用户可以通过内存管理系统的记录的时长判断是否可能发生了内存泄漏,再结合记录的程序地址检查调用处代码。 这个方法也会牺牲性能,所以可以用在调试、预览阶段。
2022-10-02
邓博
转载请注明出处