函数调用栈-知识点纠错和补充
前言
看完王爽的《汇编语言》后,以及上学期的c语言学习之后,再次深入学习函数调用栈原理时,发现自己的笔记有很多错误和疏漏,所以在此进行纠正和补充。
也为了更深入更清晰的学习pwn的基础知识,毕竟pwn追求一个深度,对这些知识点不清晰明了是学不好的。
下面的知识都用x86(32位)来进行举例
在理解这些的基础上,我们要有几个概念:
1:栈这个概念是人来定义的,cpu和内存不知道有“栈”这个概念,cpu只知道一步一步地执行指令
2:栈不是一个固定的空间,我们定义栈在哪儿(通过修改ebp、esp的值),有多大(但需要在一开始申请内存,不能超出这个内存,否则覆盖了某些重要的数据/代码,就会让程序崩溃,实际上申请的空间也是很小一块),栈就是怎样的
3:栈的模式是从高地址往低地址增长,也可以说压栈是把数据先压入栈(我们用ebp和esp定义的内存范围)这个空间里最高地址的内存单元中,并且遵守小端序的规则
理解几个名词
栈帧:函数调用栈在内存中创造出来的临时空间,一个函数调用一个栈帧,函数调用完毕后栈帧自动销毁
ebp:寄存器,存放函数栈底的地址的值(永远指向栈底)
esp:寄存器,存放函数栈顶的地址的值(永远指向栈顶)
(一个32位架构下栈帧示意图)
Return address(返回地址):位于父函数栈帧和子函数栈帧交界处,存放的是:在子函数调用完之后,eip返回到的地址,也就是返回到父函数调用子函数的下一条指令
Stack frame pointer:位于父函数栈帧和子函数栈帧交界处,但位置比返回地址高,存放上一个栈帧的栈底的值(也就是父函数栈帧的栈底的地址)
Local variables:存放局部变量的地方
arguments(参数):32位架构下,子函数所用参数实际上是保存在父函数栈帧的底部的,参数部分保存的也就是子函数用到的形参(c语言函数里的形参),并且压入栈是逆序压入的,从右往左,这是需要注意地方
函数调用栈过程
先简单概括下:
(此时是父函数的栈帧)
第一步:把子函数需要的形参压入栈内(逆序)
第二步:将调用子函数这条指令的下一条指令的地址压入栈内,称为返回地址
第三步:把当前ebp的值(父函数栈底的地址)压入栈内,同时把当前栈顶的地址送入ebp(也就是mov ebp,esp)
,这样ebp就成功成为了子函数的栈底
(此时是子函数的栈帧)
第四步:把子函数的局部变量压入栈内
然后开始执行子函数的各种指令,在这期间会调用存入的局部变量,与本次周报“函数调用栈“的主线不怎么相关
执行完任务之后,如何销毁子函数的栈帧,回到父函数继续执行父函数的下一步指令呢?
第五步:将esp不断增大,此时esp会一路向栈底移动,直到指向的地址与栈底(ebp)指向的值相同
此时栈顶存放的值是什么?是父函数栈底的地址
第六步:于是pop ebp,让ebp的值被覆盖为父函数的栈底地址,于是ebp再次指向父函数的栈底,esp也顺势指向父函数栈顶
第七步:ret(pop eip),将栈内存放的父函数下一条指令的地址传送到eip,于是继续执行调用子函数这条指令的下一条指令,一切又回到调用子函数前的状态
至此,一次简单的函数调用栈完成
几个值得注意的点:
1.在第五步中,我们并不是将局部变量的值弹出了栈,而是通过改变栈顶的位置,来改变栈的大小,实际上,存储局部变量的内存单元的值没有改变!这也和我们一开始要理解的几点概念相吻合:栈在哪,有多大,由人来定义
2.pop和push都是一个字长的值,32位字长是4字节,64位字长是8字节
3.第二步中,用的汇编指令是call指令,而非jmp,call指令的好处是会在栈中压入父函数下一条指令的地址,也就是return address(返回地址)
4.ret指令相当于pop eip(不能这样写),但是不能用pop指令直接把值送入该寄存器(如果能直接送,会导致很严重的后果,基于安全问题),所以只能用ret指令,所以我们通过修改返回地址来达到控制程序执行流的目的,这也就是最基本的栈溢出