操作系统的虚拟内存。(见文章尾)
类型转换:有符号无符号的类型转换注意不同类型的转换方式。长度更长,精度更高的方向转换。
sizeof判断的依据:根据变量的类型判断,属于运算符,而不是函数。编译时,sizeof类型的大小就已经确定。
宏定义和函数的区别:宏定义只是在预处理阶段进行符号替换。
书写规范的宏定义:完备的括号
static类型的函数和变量的特点:
static函数只能在本源文件内被调用;static全局变量,只能在本源文件内被使用。因此他们都可以在不同源文件中重复定义而不会冲突(他们占用不同的内存空间)。static局部变量,生命周期在程序运行期间都有效。(因此可以把static局部变量的地址作为返回值。例如getenv)
如何看懂复杂的变量或函数声明:从左向右找到第一个标识符,该标志符就是我们要分析的符号。以该标识符为中心,从内向外分析。先分析该标识符的右边,若是()或[],表明他是个函数或数组;然后分析该标识符的左边,例如,*或类型说明符。
对于const、volatile等限定符,判断它限定的是什么类型。先看限定符的右边,若右边是类型说明符比如char,则它限定的是右边的类型,否则他限定的是左边的类型。
restrict:是c99才支持的,形参指针用restrict修饰的函数,告诉编译器在该函数内只通过这一个指针引用他所指的内存,即,告诉编译器对该指针的操作可以代码优化。编译时需要在gcc后面加 -std=c99
volatile:用该符号限定变量,是告诉编译器每次使用到变量的时候,都要从内存里去取它的值。对于那些值容易变化的变量,需要加这个限定符。通常用在多线程编程中。
restrict和volatile都是为了通知编译器生成相应的机器指令。
对齐规则:C语言标准未严格规定该如何对齐。因此在不同的系统对齐规则略有不同。通常,基本类型char、double等,的首地址是该类型大小的倍数。结构体对齐规则,按结构体成员中类型大小最大的对齐。比如linux/IA32下单个double变量都是8字节对齐(无论全局变量还是局部变量),当在结构体里时,若有类型大小小于8的成员,结构体及在结构体内的类型最大以4字节对齐。
因为有对齐规则,所以会出现为了对齐两个不同类型的变量之间会有填充。结构体中通常可以按类型从大到小或从小到大的顺序排列。
大端小端模式:一个基本类型的字节序的先后顺序的差异。产生原因:网络传输数据的需要。intel和arm架构都使用小端模式。
位操作:熟悉常用的位操作
指针和数组的异同
栈的典型分布(《深入理解计算机系统》8.4.5节)
函数栈的特点:一块连续的地址空间,实现了“后进先出”的结构。类似于叠在一起的盘子,只能从顶部取出。
栈段的用途:
1) 栈为函数内部声明的局部变量提供存储空间。
2) 进行函数调用时,栈存储与此相关的一些维护性信息。这些信息称为栈结构或栈帧(即是上面例子里的盘子)。这些信息包括函数调用地址(即当所调用的函数结束后跳回的地方)、任何不适合装入寄存器的参数以及一些寄存器值的保存。
3) 栈可被用作暂时存储区。有时候程序需要一些临时存储,比如计算一个很长的算术表达式时,可以把中间结果暂时存储到栈中,用的时候再取出。
对于函数调用的分析,有必要知道系统如何利用栈,来跟踪哪些函数调用了哪些函数,当函数执行return语句后,控制将返回到哪里。当产生函数调用时(例如调用函数fun),系统给被调用的函数(fun)分配一个栈帧,在这里可以记录上面提到三方面的信息。
对于下面的代码,当函数caller调用swap_add时,caller的函数栈空间如右图所示。
为何不能从函数中返回一个指向该函数局部变量的指针。
若想返回一个指向在函数内部定义的变量的指针,要把那个变量声明为static。这样该静态局部变量就的存储位置在数据段中而不是栈中了。(在高级编程中有一些函数会使用此法来返回指向静态局部变量的值。)
如何分析递归程序
知道每次递归都重新给局部变量分配空间,局部变量的初始化操作在运行时进行。而静态变量的初始化操作在编译时。
例子演示
int a=5; void fun() { int b; printf("局部变量 b的地址: %d\n",&b); --a; if(a>0) fun(); } void a(int i){ if(i>0) a(--i); else printf("i has reached zero\n"); return; } void main(){ a(1); } |
------------------------
内存泄露
导致段错误的几个原因
1) 解除引用一个包含非法值的指针
2) 解除引用一个空指针(常常由于从系统程序返回空指针,并未经检查就是用)
3) 未得到正确的权限时进行访问(例如,试图往一个只读的文本段存储值就会引起段错误)
4) 用完了堆和栈的空间(虚拟内存虽然大但绝非无限)
(《深入理解计算机系统》第九章9.11节《C专家编程》第七章)
虚拟内存
由存储单元(字节或字)组成的一维连续的地址空间,简称内存空间。用来存放当前正在运行程序的代码及数据。
逻辑地址(相对地址,虚拟地址):用户的程序经过汇编或编译后形成目标代码,目标代码通常采用相对地址的形式。CPU执行指令时看到的是逻辑地址。
Ø 其首地址为0,其余指令中的地址都相对于首地址来编址。
Ø 不能用逻辑地址在内存中读取信息。
物理地址(绝对地址,实地址):物理内存中存储单元的地址。物理地址可直接寻址。
地址映射:将用户程序中的逻辑地址转换为运行时由机器直接寻址的物理地址。
当程序运行,即需要装入内存时,操作系统要为该程序分配一个合适的内存空间,由于程序的逻辑地址与分配到内存物理地址不一致,所以当cpu访问内存时,先要进行地址转换。
页表:记录逻辑地址和物理地址的对应关系。
内存管理单元MMU:该硬件读取页表,进行地址映射和内存保护。
程序的虚拟地址范围与系统有关。这个范围的大小由CPU的地址总线位数决定,例如一个32位地址总线的CPU,它可寻址的地址范围是0~0xFFFFFFFF (4G),而对于一个64位的CPU,它的地址范围为0~0xFFFFFFFFFFFFFFFF (64T)。这个范围就是我们的程序能够使用的地址范围,我们把这个地址范围称为虚拟地址空间,该空间中的某一个地址我们称之为虚拟地址。对于一台内存为256M的32bit x86主机来说,它的虚拟地址空间范围是0~0xFFFFFFFF(4G),而物理地址空间范围是0x000000000~0x0FFFFFFF(256M)。
现代的操作系统都使用一种称为分页(paging)机制。虚拟地址空间划分成称为页(page)的单位,而相应的物理地址空间也被进行划分,单位是页桢(frame)。页和页桢的大小必须相同。例如,若页的大小为4K,则页桢大小也为4K——这点是必须保证的,因为内存和外围存储器(磁盘等)之间的传输总是以页为单位的。对应4G的虚拟地址和256M的物理存储器,他们分别包含了1M个页和64K个页桢。页表就是记录页与物理内存的页帧的对应关系的。注意:并不是每个页都对应一个页帧。若一个页不对应任何页帧,则该页不可访问,试图去访问就会出现常见的段错误。例如,linux系统保证对0号页不对应任何页帧,即虚地址0X0不对应任何物理地址,因此对虚地址0X0的访问会出现段错误。
每个进程都有独立的虚拟地址空间。以32bit x86主机来说,每个进程都有4G的虚拟地址空间。这个功能是通过页表(将不同进程相同的虚拟地址映射到不同的物理地址上)来实现的。例如,进程A在地址0X8049000写一个字符’m’,进程B同样在地址0X8049000写字符’n’,如果A进程读地址0X8049000,读到的是进程A写入的’m’,而进程B读取地址0X8049000时,则读到的是B写入的’n’。两者不会冲突。这里,进程A的虚拟地址0X8049000和进程B的虚拟地址0X8049000对应的物理地址不同,因此他们之间没有干扰。
进程的4G虚拟地址空间又分为用户空间和系统空间。
用户空间:0~0Xbfffffff的地址空间,用于存放用户的程序和数据。
系统空间:0xc0000000~0xffffffff的地址空间,用于存放内核和内核数据。
下面是一个linux的4G空间的典型布局。其中阴影部分代表未被使用。(《深入理解计算机系统》第九章)
下面对各个段介绍。linux系统对不同的段赋予不同的权限。
.text文本段:包含程序的指令,这个段是可读和可执行的。在32位系统中,文本段固定存放在虚拟地址0X8048000开始的地方。
.data已初始化数据段:存放已经初始化的全局变量和静态变量。
.bss未初始化数据段:存放未初始化的全局变量和静态变量。
.data段和.bss段统称为数据段,数据段在文本段的下一个页开始的地方。数据段的下个页开始是堆,堆向上生长,即向高地址方向延伸。
栈(又称为堆栈段):用于函数调用,存放局部变量,传递参数,返回地址等。在接近系统空间的地方,栈向下生长,栈的大小是有限制的,一般12M大小。
下面是C源文件和可执行程序的对应关系。
可执行文件和内存中的各的对应关系。(《C专家编程》第六章)