0前置知识0。1程序加载和数据存储 程序运行前要将代码加载到内存的代码区,包括全局变量和静态变量也要同时加载。 堆区内存可以在程序运行时动态申请。 栈区是由程序重复利用的存储区域,通过两个寄存器ebp和esp存储栈区的相对地址来控制栈区空间的重复使用。函数调用时,开辟一个函数需要的栈区空间,称为一个栈帧。函数返回时,偏移ebp和esp的值,让栈空间可以重新使用此前函数调用占用的空间。 以下是windows平台一个程序运行时的内存分配机制: 0。2函数调用 函数调用看似简单,其实是一个挺复杂的过程。当涉及到逐级调用时,代码需要能够回到最初的调用点。 例如一个人旅行,当经过若干十字路口时,如何正确回到最初开始位置?一个可行的方案是,每经过一个路口,用一张扑克牌记下当时路口的状态,每经过一个路口即使用一张牌,并放在已使用的扑克牌上面,当返回时,依次从上面拿牌读取信息即可回溯到原来位置。这种扑克牌的放与取就是所谓的栈的“后进先出”机制。 函数调用也要使用类似的机制,由编译器完成。每一级函数调用对应一个栈帧(如同扑克牌),记录参数值,返回地址,一些寄存器状态,局部变量值。主调函数与被调函数如何分工完成这些任务,不同的调用约定有不同的分工。 0。3数组名在一定的上下文中转换为指向数组首元素的指针 数组也是如此,看似简单,实则复杂。 数组名在一定上下文中会转换为一个指向数组首元素的指针,为什么会这样,当然有其合理性。 首先了解一下指针的算术运算。(栈内存空间地址是逆增长的) voidfoo(inta){intf45;(ptr1)12;编译通过,运行时错误,试图修改ebp(ptr2)34;编译通过,运行时错误,试图修改函数返回地址(ptr3)56;试图修改保存函数实参值的栈内存单元intp(int)malloc(sizeof(int)5);(p3)23;p〔3〕23;}pn,是一个相对于ptr的偏移n个int地址空间的地址值。 编译器会计算psizeof(int),至于偏移的是否是有效合法的地址,C编译器不管。 ptrn也是如此。 如有数组intarr〔64〕{0}; 数组名是一个基地址,其索引表示地址的偏移量。arr〔i〕写法是指针算术运算(arri)的语法糖。编译器要考虑指针的算术运算有实用的意义,C编译器的规定是arri,其地址不是简单偏移i个字节,这样没有意义,而是sizeof(指向的类型)的长度的字节数,这样才有意义,其计算由编译器完成,当指针类型是数组时,偏移i个数组的长度没有任何意义,偏移i个数组元素的长度才有实际意义,这也是C编译器的规定。如: inta〔3〕〔4〕〔5〕;a的类型是int〔3〕〔4〕〔5〕,元素的类型是int〔4〕〔5〕,int(p)〔4〕〔5〕a;a2;偏移的地址是a2sizeof(int)45intb〔4〕〔5〕;b的类型是int〔4〕〔5〕,元素的类型是int〔5〕,int(p)〔5〕b;b2;偏移的地址是b2sizeof(int)5intc〔5〕;c的类型是int〔5〕,元素的类型是int,c2;偏移的地址是b2sizeof(int)C编译器只负责偏移地址的计算,对于数组是否越界,地址是否合法并不在编译时检查。 arr〔i〕的i可以是任意signedint,越界时就会访问到相邻栈内存。 0。4一些简单的汇编代码 1、mov传送指令 定义:把一个字节、字或双字的操作数从源位置传送到目的位置,可以实现立即数到通用寄存器或主存的传送,通用寄存器与通用寄存器、主存或段寄存器之间的传送,主存与段寄存器之间的传送。 举例:mvebp,esp 解释:相当于C语言中赋值语句,ebpesp 2、push进栈指令 定义:进栈指令push先将ESP减小作为当前栈顶,然后可以将立即数、通用寄存器和段寄存器或存储器操作数传送到当前栈顶。 格式:pushsrc 举例:pushebp 解释:相当于C语言中esp4,espebp 作用:为ebp当前存放的地址,在栈顶开辟空间存入它,用作调用子函数时的现场保护 3、pop出栈指令 定义:与入栈指令相反,它先将栈顶的数据传送到通用寄存器、存储单元或段寄存器中,然后ESP增加作为当前栈顶。 格式:popsrc 举例:popebp 解释:相当于C语言中ebpesp,esp4 作用:调用子函数结束后,恢复主函数的ebp 4、add加法指令 格式:adddest,src 解释:相当于destsrc 5、sub减法指令 格式:subdest,src 解释:相当于destsrc 6、call函数调用指令 格式:call函数名 作用:(1)将程序当前执行的位置IP压入堆栈中;(2)转移到调用的子程序。 其它: RET指令从堆栈把返回地址弹回到指令指针寄存器。ret相当于popEIP。 repstosdwordptr〔edi〕rep是重复其上面的指令,ECX是重复的次数。 ILT是INCREMENTALLINKTABLE的缩写,这个ILT其实就是一个静态函数跳转的表,它记录了一些函数的入口然后跳过去,每个跳转jmp占一个字节,然后就是一个四字节的内存地址,加起为五个字节。 LEA(LoadEffectiveAddress)取有效地址指令,取源操作数地址的偏移量,并把它传送到目的操作数所在的单元 0。5函数栈 0。5。1栈空间增长方式:从高地址向低地址扩展,是一片让程序重复利用的数据空间; 栈空间由编译器维护(需要在Debug模式下才可以跟踪); 0。5。2栈空间对齐方式:X86按4个字节来对齐,X64按8个字节来对齐; 0。5。3两个跟踪栈空间的寄存器ESP和EBP 对栈的操作由ESP跟踪,用来指示栈顶。push、call时,esp4,pop、ret时,esp4 EBP用来引用函数参数和局部变量。EBP相当于一个“基准指针”。从主调函数传递到被调函数的参数以及被调函数本身的局部变量都可以通过这个基准指针为参考,加上偏移量找到。 ebp(表示栈地址ebp对应的值)上一个ebp的值(栈地址); ebp4第一个局部变量地址; ebp4函数返回地址; ebp8函数第一个参数地址; 0。5。4不同的函数调用约定,在参数的入栈顺序,堆栈的恢复(由caller还是callee负责)、函数的命名上会有所不同。 0。5。5一个完整的函数帧包括:函数参数、返回地址,上一个EBP的值,局部变量空间,3个寄存器。在函数内部代码执行前,一个完整的函数帧已经建立。 下面通过一个完整实例来理解函数调用的栈帧机制与数组向栈底方向越界的分析: 看以下代码: include intarrayBound(inta){intb1;〔ebp4〕intarr〔98〕{0};〔ebp18Ch〕intc1;〔ebp190h〕,栈是逆增长,栈顶地址值栈顶arr〔1〕数组向栈顶方向越界访问,arr〔1〕对应intcarr〔98〕数组向栈底方向越界访问,arr〔98〕对应intbprintf(ddn,b,c);55}intmain(){intearrayBound(5);5printf(dn,e);return0;}主函数main()的栈帧: 主函数main()调用arraybound(),此时的汇编代码: 15:intearrayBound(5);50040D4D8push50040D4DAcallILT10(arrayBound)(0040100f)0040D4DFaddesp,40040D4E2movdwordptr〔ebp4〕,eax16:printf(dn,e);17:return0;0040D4E5xoreax,eax18:}1主调函数调用被调函数时函数参数压栈此时的栈指针值: ebp0x0012ff48ebp是栈底指针 esp0x0012fef8esp是栈底指针,栈的push和pop操作会同时改变esp的值(esp移动) 此时的栈顶指针附近的内存映像: 0012FEEC302F42008300000068201F000B。。。。。h。。 0012FEF8000000000000000000F0FD7F。。。。。。。。。瘕。 0012FF04CCCCCCCCCCCCCCCCCCCCCCCC烫烫烫烫烫烫 执行汇编: 0040D4D8push5esp40x0012fef4,esp5栈帧内存: 0012FEEC302F420083000000050000000B。。。。。。。。。 2返回地址压栈汇编指令call对应两个操作:push返回地址和jmp指令。 0040D4DAcallILT10(arrayBound)(0040100f)0012FEEC302F4200DFD44000050000000B。咴。。。。。返回地址是0040D4DF esp0x0012fef0,esp0040D4DF 0040100FjmparrayBound(0040d820)代码跳转: 0040D820pushebp0040D821movebp,esp0040D823subesp,1D0h0040D829pushebx0040D82Apushesi0040D82Bpushedi0040D82Cleaedi,〔ebp1D0h〕0040D832movecx,74h0040D837moveax,0CCCCCCCCh0040D83Crepstosdwordptr〔edi〕5:intb1;〔ebp4〕0040D83Emovdwordptr〔ebp4〕,0FFFFFFFFhebp值压栈: 0040D820pushebpebp赋值前先压栈保存先前状态esp0x0012feec,espebp0012FEEC48FF1200DFD4400005000000H。。。咴。。。。。 0040D821movebp,espebpesp0x0012feec栈区: 3函数栈帧空间分配0040D823subesp,1D0h1D0h46440064,esp0x0012fd1c此时栈顶指针附近的内存随机值: 0012FD10FEFFFFFFFE60757776A37177。。。。uwvw 0012FD1C00001F0063010050D35D6E77。。。。c。。P覿nw 0012FD28CF7914750000000000000000蟳。u。。。。。。。。 3。1寄存器压栈 寄存器状态保持(压栈) 0040D829pushebx0040D82Apushesi0040D82Bpushediesp0012FD10,espedi栈内存: 0012FD1048FF12000000000000F0FD7FH。。。。。。。。瘕。 此时esp0x0012fd10,三个寄存器使用的栈内存是464个字节以外的栈内存。 栈区: 3。2栈帧分配的空间每个字节全部置为0xCC 0040D82Cleaedi,〔ebp1D0h〕0040D832movecx,74h0040D837moveax,0CCCCCCCCh0040D83Crepstosdwordptr〔edi〕此时esp和ebp之间的栈空间: 0012FD1048FF12000000000000F0FD7FH。。。。。。。。瘕。 0012FD1CCCCCCCCCCCCCCCCCCCCCCCCC烫烫烫烫烫烫 0012FEE0CCCCCCCCCCCCCCCCCCCCCCCC烫烫烫烫烫烫 0012FEEC48FF1200DFD4400005000000H。。。咴。。。。。 3。3栈空间为局部变量从ebp处开始偏移进行初始化操作 5:intb1;〔ebp4〕0040D83Emovdwordptr〔ebp4〕,0FFFFFFFFh此时的栈空间: 0012FEE0CCCCCCCCCCCCCCCCFFFFFFFF烫烫烫烫。。。。 0012FEEC48FF1200DFD4400005000000H。。。咴。。。。。 汇编代码继续: 6:intarr〔98〕{0};〔ebp18Ch〕0040D845movdwordptr〔ebp18Ch〕,00040D84Fmovecx,61h0040D854xoreax,eax0040D856leaedi,〔ebp188h〕0040D85Crepstosdwordptr〔edi〕7:intc1;〔ebp190h〕,栈是逆增长,栈顶地址值栈顶0040D85Emovdwordptr〔ebp190h〕,1此时的栈空间: 0012FD50CCCCCCCCCCCCCCCCCCCCCCCC烫烫烫烫烫烫 0012FD5C010000000000000000000000。。。。。。。。。。。。 0012FD68000000000000000000000000。。。。。。。。。。。。 8:arr〔1〕数组向栈顶方向越界访问 0040D868moveax,dwordptr〔ebp190h〕0040D86Eimuleax,dwordptr〔ebp8〕0040D872leaecx,〔ebp18Ch〕0040D878movdwordptr〔ecx4〕,eax此时的栈空间: 0012FD50CCCCCCCCCCCCCCCCCCCCCCCC烫烫烫烫烫烫 0012FD5C050000000000000000000000。。。。。。。。。。。。 0012FD68000000000000000000000000。。。。。。。。。。。。 9:arr〔98〕数组向栈底方向越界访问 0040D87Bmovedx,dwordptr〔ebp4〕0040D87Eimuledx,dwordptr〔ebp8〕0040D882movdwordptr〔ebp4〕,edx此时的栈空间: 0012FEDC000000000000000000000000。。。。。。。。。。。。 0012FEE8FBFFFFFF48FF1200DFD44000。。。。H。。。咴。 0012FEF4050000000000000000000000。。。。。。。。。。。。 栈区: 3。4值返回 10:printf(ddn,b,c);5511:0040D885moveax,dwordptr〔ebp4〕返回值保存在寄存器eax中3。5寄存器状态恢复 此时esp0x0012fd10 0040D888popediesp4,ediesp0040D889popesi0040D88Apopebx以上的栈空间是栈帧空间464以外的空间。 此时esp0x0012fd1c ebp0x0012feec 0040D88Bmovesp,ebp栈顶指针更新,栈空间回收此时ebp对应的栈空间 0012FEEC48FF1200DFD4400005000000H。。。咴。。。。。 0040D88Dpopebp此时esp0x0012fef0 ebp0x0012ff48 0040D88Eret函数返回: 0040D4DFaddesp,44是实参压栈时使用的字节数。如果压了3个int,则是ch0040D4E2movdwordptr〔ebp4〕,eax0012FF3CCCCCCCCCCCCCCCCCFBFFFFFF烫烫烫烫。。。。 0012FF4888FF1200F911400001000000。。。。。。。。。。。 ebp4是主调函数局部变量e的栈内存保存位置。 如果函数的返回值是一个复合类型,超过了两个寄存器所能容纳的大小,会在主调函数的栈帧开辟空间,以供值返回。 4数组越界访问向栈底方向越界1个int空间:对应局部变量 向栈底方向越界2个int空间:对应ebp本身; 向栈底方向越界3个int空间:对应函数返回地址〔ebp4〕; 向栈底方向越界4个int空间:对应函数实参存储位置〔ebp8〕: include voidarrayBound(inta){intb1;intarr〔98〕{0};intc1;arr〔98〕a;arr〔101〕555;printf(dn,b);555}intmain(){intc5;arrayBound(c);printf(dn,c);5while(1);return0;}向栈底方向越界3个int空间:对应函数返回地址: arr〔100〕0040D4DF;数组向栈底方向越界访问 当arr〔100〕被赋的值是一个合法的内存地址时,正常运行,否则,运行出错。 5缓冲区溢出有如下代码: include voidfunc(){charbuff〔4〕{0};printf(someinput:);gets(buff);puts(buff);}intmain(){func();return0;}当运行到gets(buff)时的栈区: 如果输入abc,则刚好填充buff,其中buff〔3〕0 如果输入“abcdefg”,则efg会填充〔ebp〕指向的值。 如果输入“abcdefghijk”,则“ijk”会填充〔ebp4〕的值,也就是函数func()的返回地址。 End