征服C指针 摘录 第2章--- C语言是怎样使用内存的
虚拟地址
如今的计算机等的运行环境,对于应用程序的每个进程都会分配独立的虚拟地址空间以避免出现BUG而影响其他进程。这是操作系统和CPU协同工作的结果。要想实际储存数据,还需要物理内存。操作系统负责将物理内存分配给虚拟地址空间。操作系统也会对区域设置只读或者可读写的属性。
未定义行为是关于没有使用可移植性或不正确的程序组成元素时的行为或使用不正确的数据时的行为,本标准无任何强制要求。未指定行为是本标准会提供两种以上的可能性,关于不同场合下选择哪种可能性不做强制要求。实现定义行为是指各实现方式对从未指定行为中选定的行为进行文档化。
C语言的变量可以基于作用域和储存期两个维度进行分类。
C语言变量的作用域:
- 全局变量在函数外部定义的变量默认成为全局变量,全局变量对程序的任何地方都是可见的。
- 文件内的static变量。即便是像全球变量那样在函数外部定义的变量,一旦加上static,其作用域就只限定在当前源文件内。指定为static的变量,对于其他源文件是不可见的。
- 局部变量是在函数中声明的变量,局部变量只能在其声明的代码块中被引用。局部变量在离开相应代码块时被释放。
C语言变量的储存期
- 静态存储期:全局变量、文件内的static变量以及带static限定的局部变量都具有静态存储区,这些变量有时也称为静态变量。具有静态存储器的变量,拥有从程序开始到结束为止的生命周期。它一直存在于内存的同一地址上。
- 自动存储器不带static限定的局部变量具有自动存储期,这样的变量称为自动变量。具有自动存储期的变量,在程序进入其所在代码块时,分配内存空间,在程序离开的代码块时,内存空间释放。
内存空间的生命周期
- 静态变量生命周期从程序运行开始,到程序关闭时结束。
- 自动变量生命周期直至程序离开该变量声明所在代码块为止。
- 通过malloc()分配的内存空间生命周期直至free()被调用为止
所有的静态变量都被配置在相当接近的内存区域中,离他们稍远的是一些存放通过malloc()分配到内存空间的区域,更远的只是存放自动变量的内存区域。各个区域之间存在空隙,但空隙部分不会被分配物理内存。而是被用于虚拟内存。
只读内存区域。由于在如今的大多数操作系统中,函数主体与字符串字面变量,并配置在同一个只读内存区域中的。
指向函数的指针。
表达式中的函数会被解读成指向函数的指针,可以将它赋给指向函数的指针类型的变量。 指向函数的指针类型,根据对象函数的返回值及参数的不同而不同。
#include <stdio.h>
void func1(double d)//对参数加1.0后并输出。
{
printf("func1:d + 1.0 = %f\n", d + 1.0);
}
void func2(double d)//对参数加2.0后并输出。
{
printf("func2:d + 2.0 = %f\n",d + 2.0);
}
int main(void)
{
void (*func_p)(double);//定义形参为double的无类型指针。
func_p = func1;//对无类型指针赋值
func_p(1.0);
func_p = func2;
func_p(1.0);
return 0;
}
输出结果为:
func1:d + 1.0 = 2.000000
func2:d + 2.0 = 3.000000
静态变量是从程序启动直至结束为止一直存在的变量,因此在虚拟地址空间上静态变量占有固定区。静态变量包含全局变量、文件内的static变量以及带static限定的局部变量。由于作用域各不相同,所以这些变量在编译或连接时具有不同的意义,但在运行时会被当做相似的对象处理。
在C语言中,一个程序可以由多个源文件构成,并且这源文件可以在分别编译后连接起来。对于函数和全局变量,只要名称上相同,即便位于不同的源文件中,也会被当做相同的对象处理,这项处理工作由一个叫做连接器的程序完成。
为了通过连接器将名称连接起来。多数情况下,各个目标文件会被有符号表。连接器通过符号表中信息,来给原先只是个名称的对象分配具体的地址。
自动变量。
自动变量的内存空间在退出函数后可以被其他函数调用重复使用。自动变量地址因函数调用方式而异,并不是固定的。在C语言中,自动变量通常被分配到栈中
函数调用的朴素思路
- 调用方将实参的值从后往前按顺序压入栈中。
- 将与函数调用相关的恢复信息加入栈中
- 跳转至作为调用对象的函数的地址。
- 在栈中申请该函数所用的自动变量所需的内存空间。
- 在函数中执行。
- 一旦函数执行结束,局部变量占用的内存空间,就会被释放,程序利用恢复信息返回到原来的地址。
- 在栈中弹出调用方的参数。
对于非局部变量通常其变量开灯。看看。名也不残留在编译后的目标文件中。自动变量的地址是在运行时决定的,所以它不在连接器的管辖范围内。
基指针会以其指向的地址为基准访问局部变量。栈指针指针会指向栈顶。局部变量是通过相对于基地址的偏移量来引用的。
数组也是保存在栈中的。如果没有对数组进行范围检查,向超出数组的内存空间的地方进行了写入操作。则连函数的恢复信息都有可能会遭到破坏,这也就意味着函数将无法返回。导致程序崩溃甚至还是好的结果。假如自动变量的数组溢出导致恢复信息被覆盖,甚至会引发安全漏洞。
如果没有认真的对程序进行数组范围检查,那么当恶意攻击者故意导入大量数据时,返回地址就会被恶意数据替换掉,然后当该函数执行结果时,后期处理就会从这个伪装的返回地址开始继续执行,因此如果在其中放入攻击用的机器码攻击者,就可以让程序执行任意的机器码,这称为缓冲区溢出漏洞。
缓冲区溢出漏洞有可能变成可支持任意代码的漏洞。在C语言中,经常会由程序员忽视数组范围检查这一非常常见的bug而导致该漏洞的产生。因为这是一种非常危险的漏洞,所以除了依靠程序员的准确判断,人们还在操作系统层面采取了对策。
地址空间布局随机化。这一功能用于在程序启动时,在一定程度上随机决定栈或堆的地址。
数据执行保护。最功能利用CPU的功能使栈或堆中的机器码无法执行。在这样的程序中,在从堆中获取分配内存,获取内存分配时,需要使用不设执行保护标志的特别的内存分配函数。
可变长参数。
在C语言中可以编写,可变长参数函数。其中比较典型的是printf()。他可根据第1个参数中包含的转移字符的个数来确定第2个参数及其后的内容。
printf("%d,%s\n",100,str);

VLA可变长数组
C99具备VLA功能,可以使自动变量的数组可变长。因为自动变量是被分配到栈中,与具有静态储存区的变量不同,栈在运行时可以延伸。所以可以在栈上配置可变长数组。因此只有自动变量可以使用VLA
由于数组是可变长的,所以这就意味着我们无法通过"参照距离基指针固定长度的位置"的方法访问全部变量。在输入不同的值之后,变量之间的间隔发生变化,为了在这种状态访问局部变量,编译器会生成一段可以在运行时确认变量的值并使参考位置错开的代码。
malloc()
在C语言中可以使用malloc()进行动态内存分配。malloc()是根据参数指定的大小分配内存块并返回指向内存块起始位置指针的函数。内存分配失败的情况下返回NUL通过释放。free()释放
主要应用。
- 动态分配结构体。如数据结构链表。
- 为了到运行时才能确定长度数组分配内存。
C语言不需要强制转换malloc()返回值类型。
malloc()不是系统调用。malloc()先从操作系统那里一次性获得大量内存,再把它分发给应用程序。根据操作系统的不同,从操作系统获取内存的手段也多种多样。
free()之后相应的内存空间并不会立即返还给操作系统,不仅如此,及时执行。free()之后大多数时候你还能看到free()之前设置的值。知道其他地方执行malloc() 使得该内存空间被占用时内容才会发生变化。
若内存变得零零碎碎,出现许多细碎的空块,这样的内存空间事实上是无法使用的,这种现象称为碎片化。在C语言中,只要使用malloc()这样的内存管理例行程序就无法从根本上避免碎片化问题。
void *calloc(size_t nmemb,size_t size) calloc()通过与malloc()相同的方法仅分配nmemb × size的内存空间,并将该内存空间清零返回。清零即把该内存空间的全部位置换成0。
malloc()无法保证其所分配的内存的内容。以块的个数和块带小为参数,然后将它们相乘,一旦乘法运算引发整数溢出。则实际分配内存量会比预想的要小,最终也可能发生缓存区溢出漏洞,进而演变成安全漏洞。
realloc()更改已由malloc()分配的内存空间的大小的函数。
void *relloc(void *ptr,size_t s)
常用于扩充内存空间,如果ptr传递过来的内存空间的后面刚好存在所所需的空闲空间。那么就会直接那么扩充。也没有足够的空闲空间,它就会在别处分配新的内存空间,然后将内容复制过去。
如果向realloc()的ptr传递NULL,那么realloc()的行为将与malloc()完全相同。
p = realloc(p,size)
size == NULL时会出现p永久丢失的问题
当malloc()参数为零时,运行环境中的定义可以从以下两个动作中任选其一。
- 返回空指针。//发生内存不足或者其他错误。
- 返回与参数非零时相同的动作。//不对malloc(0)进行特殊处理,直接返回大小为0的内存空间。
对于通过malloc()分配的内存空间,必须在程序结束之前调用free()释放掉。
对齐
根据硬件的不同,对不同的数唉。据类型能够配置的地址是有限制的,就算能够配置,某些CPU的效率也会变差,在这种情况下编译器会进行适当的边界调整(对齐),向结构体体插入适当的填充。
1 #include <stdio.h> 2 3 typedef struct { 4 char char1; 5 int int1; 6 char char2; 7 double double1; 8 char char3; 9 } Hoge; 10 11 int main(void) 12 { 13 Hoge hoge; 14 15 printf("hoge size..%d\n", (int)sizeof(Hoge)); 16 17 printf("hoge ..%p\n", (void*)&hoge); 18 printf("char1 ..%p\n", (void*)&hoge.char1); 19 printf("int1 ..%p\n", (void*)&hoge.int1); 20 printf("char2 ..%p\n", (void*)&hoge.char2); 21 printf("double1..%p\n", (void*)&hoge.double1); 22 printf("char3 ..%p\n", (void*)&hoge.char3); 23 24 return 0; 25 }
输出结果为
hoge size..32 hoge ..000000000062FE00 char1 ..000000000062FE00 int1 ..000000000062FE04 char2 ..000000000062FE08 double1..000000000062FE10 char3 ..000000000062FE18
填充有时会被放到结构体。的末尾。在创建结构体数组时,填充是必要的。中将sizeof运算符应用到这样的结合体上时,返回的是包含末尾填充部分大小的长度,将结果和原始的个数相乘就可以获取数组整体的长度。
malloc()会配合那些对齐最为严格的类型返回,经过适当调整的地址,局部变量等也会被配置到经过适当调整的内存中。
结构体的成员名称在运行时也是缺失的。对结构体成员的引用是通过距离结构体起始地址的偏移量实现的。如果结构体的定义发生了改变。就必须将使用这个结构体的原文件全部重新编译一遍。
字节序
- 小端:在英特尔系列CPU上,整数类型在内存上倒过来存放,这种存储方式一般称为小端。高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
- 小端:低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
- 双端:可以做大端、小端之间来回切换
内存中的二进制形式会因环境的不同而多种多样。所以有些想法是不可取的,比如试图将内存中的那内容直接写到硬盘上或者传输到网络中,以便在不同的机器上原样读取的想法就是不可取的。

浙公网安备 33010602011771号