程序的环境由内存、运行库和系统调用(API)等构成。
系统调用和API是程序与内核之间交互的中介。
C/C++语言中指针的大小是4个字节是有原因的,因为指针实际上是个地址值,地址值决定于内存空间大小,而CPU支持的最大内存容量是由地址总线的宽度决定的,因为32位地址总线的宽度是32位,即4字节,所以指针的大小也是4字节。
内存中有一部分称为内核空间,普通的应用程序是无法访问的。
除此之外,内存还有栈空间、堆空间、可执行映像、保留区。
栈:用于维护函数调用的上下文。
堆:用于动态分配空间,它通常位于栈的下方。
可执行映像:用于装载可执行文件在内存中的映像。
保留区:内存中受保护而禁止访问的区域,如NULL。
内存中还有一段动态链接库映射区,用于装载动态链接库。
栈总是向低地址方向增长的。
栈最重要的功能是保存一个函数调用所需要的信息,这信息通常被叫做堆栈帧或活动记录。
堆栈帧一般包括:
1、函数的返回地址和参数。
2、临时变量,包括非晶态局部变量和编译期自动生成的临时变量。
3、保存的上下文,包括函数调用前后需要保持不变的寄存器。
ESP寄存器总是指向栈顶,EBP(栈指针)寄存器总是指向函数活动记录的固定位置。
P313描述了i386函数调用的过程。
为什么未初始化的数组元素被打印出来以后是汉字的“烫”?
这是因为它们在DEBUG模式下被初始化为0xCC,而两个连续的0xCC就是0xCCCC,即,汉字的“烫”。
还有的时候是使用0xCD初始化未初始化的数组,这时候显示的汉字是“屯”。
“钩子”函数可以通过替换掉Windows函数的汇编序列前面几条“无用”的指令实现。
是指函数的调用方和被调用方都遵守同样的函数调用方式。
它一般规定如下几个方面的内容:
1、函数参数的传递方式和顺序。大部分函数参数是通过栈传递,有些是通过寄存器。
2、栈的维护方式。就是调用完了就把被调方全部弹出栈。
3、名字修饰策略。调用惯例会对函数本身的名字进行修饰。
_cdecl就是一种调用惯例。
栈在函数调用过程中的变化是这样的:
1、首先将参数压入栈中。
2、继续压入被调函数在主调函数中的下一条指令的地址,改地址在内存中就是返回地址。比如:

现在假设f是被调函数,那么return
0的地址就是要被压入栈中的地址。
3、跳转到函数体执行。
现在就P322的图说明几点问题。
old ebp是啥意思?首先要明确图中的意思是个值,即ebp寄存器中的值,这个值有着特殊的意义。这个值是个地址,而这个地址是当前函数的调用函数的ebp的值在内存中的位置。所以这就是个无限循环。
EBP(Extended Base Pointer):扩展基址指针寄存器。
由此可见EBP其实起到了一个基址的作用,它通常与ESP(Extended
Stack Pointer)搭配使用,前者是基址,后者是偏移量,这样的话就可以指向某函数的寄存器和局部变量了。
保存这个旧的EBP值的作用在于它能够恢复调用函数的EBP值,调用函数的EBP值是个绝对地址,如果你不恢复它,那么调用函数就无法再利用ESP的值来指向它自己的寄存器和全局变量了。
C++中的this指针存放在ECX(Extended
Counter miX,扩展计数寄存器)中。
EAX(Extended Accumulator miX,扩展累加寄存器)负责传递返回值,它能返回4字节的返回值。
对于小于等于8字节并且大于4字节的返回值,可以搭配EDX(Extended
Data miX,扩展数据寄存器)进行返回。
如果返回值太大,那就必须开辟一段栈空间来临时存放返回值。首先产生返回值的函数会把返回值写到这段空间中,然后这段空间中的值再写到真正的用于装载这个返回值的变量中,这个变量是我们常说的返回值,至于该段中所说的返回值就是指需要被返回的数据。也就是说在这个过程中会产生两次复制。
堆的处理过程比栈要复杂,因为程序在执行过程中随时可能申请空间和释放空间,而且申请和释放的空间大小也不确定。
堆是一块巨大的内存空间,占据了虚拟内存的绝大部分。
堆可以将数据传递到函数外面,而且能动态地产生对象。
直接操作堆空间的是程序的运行库,而不是系统调用,因为这样做比较高效。
有两种方式:
1、通过调整数据段的结束地址来调整堆空间的大小。
2、直接申请一段内存空间用作堆空间。
申请的空间的起始地址和大小都是页大小的整数倍。
在Linux系统下,堆可以存在于从BSS段到共享库装载的位置和从共享库结束位置到栈这两部分空间,现在的话能有2.9G大小。也就是说现在你用malloc申请堆空间至多能申请2.9G左右。
Windows下的堆和栈空间是非常零散的,因为每个线程在建立的时候都需要自己的栈空间,堆空间就是在剩下的零碎的空间中分配的。
它的对空间大小和起始地址也必须是页的整数倍。
无论是Linux还是Windows都有自己的堆分配算法。
每个进程在创建的时候,系统都会给它分配一个堆,并在进程结束的时候销毁堆。
堆空间并不是连续的。
堆空间不一定都是向上增长的。
它就是负责管理堆空间的申请、分配和释放的算法。
1、空闲链表法。
2、位图。把整个堆划分成大小相同的块,第一个块叫头,其余的称为主体,用一个数组来记录块的使用情况,每个块有三种状态,即,头、主体和空闲。
3、对象池。如果每次对象申请的堆空间大小相同,就可以把堆划分成对象大小的若干块,每次只需要取一块分配出去即可。