C进阶指南12整型溢出和类型提升内存申请和管理Word下载.docx
《C进阶指南12整型溢出和类型提升内存申请和管理Word下载.docx》由会员分享,可在线阅读,更多相关《C进阶指南12整型溢出和类型提升内存申请和管理Word下载.docx(23页珍藏版)》请在冰豆网上搜索。
error\n"
return0;
上述代码中,变量i被转换为无符号整型。
这样一来,它的值不再是-1,而是size_t的最大值。
变量i的类型之所以被转换,是因为sizeof操作符的返回类型是无符号的。
具体参见C99/C11标准之常用算术转换一章:
若无符号整型的位数不低于另一操作数,则有符号数转为无符号数的类型。
C标准中,size_t被定义为不低于16位的无符号整型。
通常size_t完全对应于long。
这样一来,int和size_t的大小至少相等,可基于上述准则,强转为无符号整型。
这个故事给了我们一个关于整型大小可移植性的观念。
C标准并未定义short、int、long、longlong的确切大小及其无符号形式。
标准仅限定了它们的最小长度。
以x86_64架构为例,long在Linux环境中是64比特,但在64位Windows系统中是32比特。
为了使代码更具移植性,常见的方法是使用C99的stdint.h文件中定义的、指定长度的特殊类型,包括uint16_t、int32_t等。
此文件定义了三种整型类型:
∙有确切长度的:
uint8_tuint16_t,int32_t等
∙有长度最小值的最短类型:
uint_least8_t,uint_least16_t,int_least32_t等
∙执行效率最高的有长度最小值的类型:
uint_fast8_t,uint_fast16_t,int_fast32_t等
但不幸的是,仅依靠stdint.h并不能根除类型转换的困扰。
C标准中“整型提升规则”中写道:
若int的表达范围足以覆盖所有的基础类型,此值将被转换为int;
否则将转为unsigned
int。
这就叫做整型提升。
整型提升过程中,所有其他的类型保持不变。
下述代码在32位平台中将繁华65536,在16位平台上返回0:
uint32_tsum()
{
uint16_ta=65535;
uint16_tb=1;
returna+b;
无论C语言实现中,是否把未修饰的char看做有符号的,整型提升都连同符号一起把值保留下来。
如何实现char类型通常取决于硬件体系或操作系统,常由其平台的ABI(应用程序二进制接口)指定。
如果你愿意自己尝试的话,char会被转为signedchar,下述代码将打印出-128和-127,而不是128和129。
x86架构中可用GCC的-funsigned-char参数切换到强制无符号提升。
charc=128;
chard=129;
%d,%d\n"
c,d);
二、内存申请和管理
malloc,calloc,realloc,free
使用malloc分配指定字节大小的、未初始化的内存对象。
若入参值为0,其行为取决于操作系统实现,或者说,这是C和POSIX标准均未定义的行为。
若请求的空间大小为0,则结果视具体实现而定:
返回值可以是空指针或特殊指针。
malloc(0)通常返回有效的特殊指针。
或者返回的值可成为free函数的参数,且函数不会错误退出。
例如free函数对NULL指针不做任何操作。
因此,若空间大小参数是某个表达式的结果的话,要确保测试过整型溢出的情况。
size_tcomputed_size;
if(elem_size&
&
num>
SIZE_MAX/elem_size){
errno=ENOMEM;
err(1,"
overflow"
computed_size=elem_size*num;
一般说来,要分配一个元素大小相同的序列,可考虑使用calloc而非用表达式计算大小。
同时calloc将把分配的内存初始化为0.像往常一样使用free释放分配的内存。
realloc将改变已分配内存对象的大小。
此函数返回一个指针,指针可能指向新的内存起始位置,内存或大或小,取决于入参中请求空间大小,内容不变。
若新的空间更大,额外的空间未被初始化。
若realloc入参中,指向旧对象的指针为NULL,并且大小非0,此行为等价于malloc。
若新的大小为0,且提供的指针非空,此时realloc的行为依赖于操作系统。
多数实现将尝试释放对象内存,返回NULL或与malloc(0)相同的返回值。
例如在Windows中,此操作会释放内存并返回NULL。
OpenBSD也会释放内存,但返回的指针指向的空间大小为0。
realloc失败时会返回NULL,也因此断开与旧的内存对象的关联。
所以不但要检查空间大小参数是否存在整型溢出,还要正确处理realloc失败时的对象大小。
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include<
stdio.h>
stdint.h>
malloc.h>
errno.h>
#defineVECTOR_OK
0
#defineVECTOR_NULL_ERROR
1
#defineVECTOR_SIZE_ERROR
2
#defineVECTOR_ALLOC_ERROR
3
structvector{
int*data;
size_tsize;
};
intcreate_vector(structvector*vc,size_tnum){
if(vc==NULL){
returnVECTOR_NULL_ERROR;
vc->
data=0;
size=0;
/*checkforintegerandSIZE_MAXoverflow*/
if(num==0||SIZE_MAX/num<
sizeof(int)){
returnVECTOR_SIZE_ERROR;
data=calloc(num,sizeof(int));
/*callocfaild*/
if(vc->
data==NULL){
returnVECTOR_ALLOC_ERROR;
size=num*sizeof(int);
returnVECTOR_OK;
intgrow_vector(structvector*vc){
void*newptr=0;
size_tnewsize;
size==0||SIZE_MAX/2<
vc->
size){
newsize=vc->
size*2;
newptr=realloc(vc->
data,newsize);
/*reallocfaild;
vectorstaysintactsizewasnotchanged*/
if(newptr==NULL){
/*uponsuccess;
updatenewaddressandsize*/
data=newptr;
size=newsize;
2.1避免致命错误
一般避免动态内存分配问题的方法无非是尽可能把代码写得谨慎、有防御性。
本文列举了一些常见问题和少量避免这些问题的方法。
1)重复释放内存
调用free可能导致此问题,此时入参指针可能为NULL(依照《C++PrimerPlus》,free(0)不会出现问题。
译者注)、未使用malloc类函数分配的指针,或已经调用过free
/
realloc(realloc参数中大小填0,可释放内存。
译者注)的指针。
考虑下列几点可让代码更健壮:
∙指针初始化为NULL,以防不能立即传给它有效值的情况
∙GCC和Clang都有-Wuninitialized参数来警告未初始化的变量
∙静态和动态分配的内存不要用同一个变量
∙调用free后要把指针设回为NULL,这样一来即便无意中重复释放也不会导致错误
∙测试或调试时使用assert之类的断言(如C11中静态断言,译者注)
char*ptr=NULL;
/*...*/
voidnullfree(void**pptr){
void*ptr=*pptr;
assert(ptr!
=NULL)
free(ptr);
*pptr=NULL;
2)访问未初始化的内存或空指针
代码中的检查规则应只用于NULL或有效的指针。
对于去除指针和分配的动态内存间联系的函数或代码块,可在开头检查空指针。
3)越界访问内存
(孔乙己式译者注:
你能说出strcpy
strncpy
strlcpy的区别么,能的话这节就不必看)
访问内存对象边界之外的地方并不一定导致程序崩溃。
程序可能使用损坏了的数据继续运行,其行为可能很危险,也可能是故意而为之,利用此越界操作来改变程序的行为,以此获取其他受限的数据,甚至注入可执行代码。
老套地人工检查数组和动态分配内存的边界是避免此类问题的主要方法。
内存对象边界的相关信息必须人工跟踪。
数组的大小可由sizeof操作符指出,但数组被转换为指针后,函数调用sizeof仅返回指针大小(视机器位数而定,译者注),而非原来的数组大小。
C11标准中边界检查接口AnnexK定义了一些新的库函数集合,这些函数可用于替换标准库(如字符串和I/O操作)常见部分,它们更安全、更易于使用。
例如[theslibclibrary][slibc]都是上述函数的开源实现,但接口不被广泛采用。
基于BSD(或基于MacOSX)的系统提供了strlcpy、strlcat函数来完成更好的字符串操作。
其他系统可通过libbsd库调用它们。
许多操作系统提供了通过内存区域间接控制受保护内存的接口,以防止意外读/写操作,入Posximprotect。
类似的间接访问的保护机制常用于所有的内存页。
2.2避免内存泄露
内存泄露,常由于程序中未释放不再使用的动态分配的内存导致。
因此,真正理解所需要的分配的内存对象的范围大小是很有必要的。
更重要的是,要明白何时调用free。
但当程序复杂度增加时,要确定free的调用时机将变得更加困难。
早期设计决策时,规划内存很重要。
以下是处理内存泄露的技能表:
1)启动时分配
想让内存管理保持简单,一个方法是在启动时在堆中分配所有所需的内存。
程序结束时,释放内存的重任就交给了操作系统。
这种方法在许多场景中的效果令人满意,特别是当程序在一个批量操作中完成对输入的处理的情况。
2)变长数组
如果你需要有着变长大小的临时存储,并且其生命周期在变量内部时,可考虑VLA(VariableLengthArray,变长数组)。
但这有个限制:
每个函数的空间不能超过数百字节。
因为C99指出边长数组能自动存储,它们像其他自动变量一样受限于同一作用域。
即便标准未明确规定,VLA的实现都是把内存数据放到栈中。
VLA的最大长度为SIZE_MAX字节。
考虑到目标平台的栈大小,我们必须更加谨慎小心,以保证程序不会面临栈溢出、下个内存段的数据损坏的尴尬局面。
3)自己编写引用计数
这个技术的想法是对某个内存对象的每次引用、去引用计数。
赋值时,计数器会增加;
去引用时,计数器减少。
当引用计数变为0时,这意味着此内存对象不再被使用,可以释放。
因为C不提供自动析构(事实上,GCC和Clang都支持cleanup语言扩展),
也不是重写赋值运算符,引用计数由调用retain/release的函数手动完成。
更好的方式,是把它作为程序的可变部分,能通过这部分获取和释放一个内存对象的拥有权。
但是,使用这种方法需要很多(编程)规范来防止忘记调用release(停止内存泄露)或不必要地调用释放函数(这将导致内存释放地过早)。
若内存对象的生命期需要外部事件指出,或应用程序的数据结构隐含了某个内存对象的持有权的处理,无论何种情况,都容易导致问题。
下述代码块含有简化了的内存管理引用计数。
stdlib.h>
#defineMAX_REF_OBJ100
#defineRC_ERROR-1
structmem_obj_t{
void*ptr;
uint16_tcount;
staticstructmem_obj_treferences[MAX_REF_OBJ];
staticuint16_treference_count=0;
/*creatememoryobjectandreturnhandle*/
uint16_tcreate(size_tsize){
if(reference_count>
=MAX_REF_OBJ)
returnRC_ERROR;
if(size){
void*ptr=calloc(1,size);
if(ptr!
=NULL){
references[reference_count].ptr=ptr;
references[reference_count].count=0;
returnreference_count++;
/*getmemoryobjectandincrementreferencecounter*/
void*retain(uint16_thandle){
if(handle<
reference_count&
handle>
=0){
references[handle].count++;
returnreferences[handle].ptr;
}else{
returnNULL;
/*decrementreferencecounter*/
voidrelease(uint16_thandle){
release\n"
structmem_obj_t*object=&
references[handle];
if(object->
count<
=1){
released\n"
free(object->
ptr);
reference_count--;
decremented\n"
object->
count--;
如果你关心编译器的兼容性,可用cleanup属性在C中模拟自动析构。
voidcleanup_release(void**pmem){
inti;
for(i=0;
i<
reference_count;
i++){
if(references[i].ptr==*pmem)
release(i);
voidusage(){
int16_tref=create(64);
void*mem=retain(ref);
__attribute__((cleanup(cleanup_release),mem));
上述方案的另一缺陷是提供对象地址让cleanup_release释放,而非引用计数值。
这样一来,cleanup_release必须在references数组中做开销大的查找操作。
一种解决办法是,改变填充的接口为返回一个指向structmem_obj_t的指针。
另一种办法是使用下面的宏集合,这些宏能够创建保存引用计数值的变量并追加clean属性。
/*helpermacros*/
#define__COMB(X,Y)X##Y
#defineCOMB(X,Y)__COMB(X,Y)
#define__CLEANUP_RELEASE__attribute__((cleanup(cleanup_release)))
#defineretain_auto(REF)retain(REF);
int16_t__CLEANUP_RELEASECOMB(__ref,__LINE__)=REF
voidcleanup_release(int16_t*phd){
release(*phd);
void*mem=retain_auto(ref);
(译者注:
##符号源自C99,用于连接两个变量的名称,一般用在宏里。
如inta##b就会定义一个叫做ab的变量;
__LINE__指代码行号,类似的还有__FUNCTION__或__func__和__FILE__,可用于打印调试信息;
__attribute__符号来自gcc,主要用于指导编译器优化,也提供了一些如构造、析构、字节对齐等功能)
4)内存池
若一个程序经过数阶段才能彻底执行,每阶段的开头都分配有内存池,需要分配内存时,就使用内存池的一部分。
内存池的选择,要考虑分配的内存对象的生命周期,以及对象在程序中所属的阶段。
每个阶段一旦结束,整个内存池就要立即释放。
这种方法在记录型运行程序中特别有用,例如守护进程,它可能随着时间减少内存分段。
下述代码是个内存池内存管理的仿真:
structpool_t{
size_tused;
/*creatememorypool*/
structpool_t*create_pool(size_tsize){
structpool_t*pool=calloc(1,sizeof(s