建设工程检测中心网站,网站建设相关参考资料,wordpress微信登录插件下载,wordpress获取json内容我们每次在调用函数的时候#xff0c;都说会进行传参。每次创建函数#xff0c;或者进行递归的时候#xff0c;也会说会进行压栈。 那么#xff0c;今天我们就来具体看看函数到底是如何进行压栈#xff0c;传参的操作。
什么是栈#xff1f; 首先我们要知道#xff0c;… 我们每次在调用函数的时候都说会进行传参。每次创建函数或者进行递归的时候也会说会进行压栈。 那么今天我们就来具体看看函数到底是如何进行压栈传参的操作。
什么是栈 首先我们要知道我们将内存一般划分为三个区域
静态区堆区栈区 我们平时创建的临时变量函数都会在栈区中占据空间 此时我们也要知道栈区的使用规则从高地址向低地址使用
栈的使用规则 我们知道抢的弹夹我们要逐个把子弹往里面压之后如果取出子弹就需要将上一次压入的子弹取出之后逐个取出子弹并只能按照顺序取出。 栈就是这样的使用规则遵循先进后出后进先出。 此时你会想不能把任意的数据取出必须一个一个拿这种结构真的好用吗 起初我也这样认为但是计算机就喜欢用这种结构。 在内存中栈区的使用规则是从高地址向低地址使用的。
函数的栈帧 C语言中我们要想观察函数栈帧就需要用到调试。当我们调试时所在的函数此时函数未运行完每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。 因为我们知道只要运行函数就会进行压栈操作所以分析出一下信息
栈帧是一块因函数运行而创建的的临时空间。每调用一次函数都会创建一个独立的函数栈帧。栈帧中存放着函数重要信息如局部变量函数返回地址函数参数等。当函数运行完毕后栈帧会销毁。 既然会创建函数栈帧那么就会维护其空间计算机使用寄存器维护空间。
什么是寄存器 这里牵扯很多内容我们只给出笼统解释寄存器是集成到CPU上的是独立的寄存器可以暂存指令地址和数据。所以寄存器也可以理解为指针。 我们会使用很多寄存器要理解清楚函数栈帧就必须理解ebp和esp这两个寄存器中存放的是地址这两个地址是用来维护函数栈帧的。我们详细来讲esp和ebp两个寄存器。其余寄存器混个脸熟即可一会会用到。
esp寄存器 维护栈顶始终指向栈顶此时esp寄存器存储栈顶地址。
ebp寄存器 维护当前函数栈帧的栈低此时ebp寄存器存储函数栈帧栈低地址。
几个必要的汇编指令 我们观察函数栈帧的创建和销毁就要知道几个汇编指令这样可以更好的阅读以下内容。 记不住没关系我们一下会一一讲解。
图解 这里我们使用VS2013来观察由于VS2022太过高级有些内部细节就会看不到所以用VS2013来观察。此时我们执行以下代码
int Add(int x, int y)
{int z 0;z x y;return z;
}int main()
{int a 10;int b 20;int c 0;c Add(a, b);printf(%d\n, c);return 0;
} esp和ebp就是维护当前调用的函数。 点击F10开始调试。 接下来我们看main函数被谁调用了我们进入main函数并直接执行完。 在VS2013中main函数也是被其他函数调用的。没想到main函数也是被调用的函数也理解了为什么每次都要有返回值。 mainCRTStartup函数调用 __tmainCRTStartup函数__tmainCRTStartup函数调用main函数。 之后我们按住F10之后右击鼠标找到“转到反编汇”就可以找到C语言所对应的汇编代码箭头的指向就可以一行一行的执行我们来逐过程分析 push ebp将ebp压入栈区。因为esp维护栈顶所以esp指向改变。我们可以观察其存放地址的改变。 之后执行movemove是把后面的值赋到前面去。 此时ebp和esp指向的位置相同。 之后就要创建函数的栈帧了执行sub就是将其寄存器存放地址减去一个地址因为栈区是高地址到底地址所以该地址向上。 此时esp维护main函数的栈顶ebp维护main函数的栈低。我们可以看内存 这些内存都是为main函数开辟的空间。 之后有执行了3次pushpush时会有一个动作就是栈顶指针esp会变一直指向栈顶。 我们通过内存窗口来观察 因为我们说过寄存器既可以存地址也可以存数据此时ebx存的是数据就是内存里面的内容而esp存放的是ebx的地址。压入ebx以后继续将esi、edi压入。 之后执行lea : load effective address 加载有效地址。 此时会发现正好是加载的空间正好是最开始esp减去的空间就是main函数栈帧的低地址。 之后执行的命令我们就需要先讲解一下了。 我们应该听过字节的概念1字节等于8比特位那么字和字节又什么关系一个字等于两个字节。 比特记为bit字节记为Byte字记为word所以有如下关系
1Byte8bits1word2Bytes16bits dword一个word是两个字节d代表double就是双字就是4个字节。 此时我们要看其以下的三个步骤 此时edi里面存放main函数栈帧的低地址。 这样就可以理解为什么每次打印未初始化的空间打印出来的字符都是一个汉字“烫烫烫烫”了。 此时才会开始执行有效的代码。在此之前都是为main函数开辟的空间。 此时就要调用Add函数了一样的我们要改变ebp和esp的指向因为进入Add函数就需要维护Add函数栈帧了但是还是要做以下准备就是传参我们来看形式参数的创建。 这两个动作相当于传参之后执行call就是调用函数要记住call的地址此时点击F11才能进入Add函数。 我们可以发现就在ecx的下一个地址里面存储了call指令下一个执行的地址。为什么要记录地址我们先埋个伏笔此时我们会先进入Add函数流程如下 注意此时main函数的函数栈帧已经增长到call指令的下一个地址了。 此时我们来观察Add函数的细节将esp减去一个地址改变指向 之后还是main函数栈帧的那一套操作压入3个寄存器并初始化空间并将z初始化为0 此时先将 ebp 8 的值赋给 eax 此时 eax 10之后又执行add将 ebp 12 的值等于30最后将eax的值赋给 ebp - 8 此时 ebp - 8 地址的值是30. 我们可以发现我们使用Add函数并没有创建形参在我们传参时其实已经压栈过了而且参数是从右向左传参的。 返回的话z会被销毁我们来观察其如何返回。 我们将结果放入eax寄存器当中此时就不用担心函数销毁。 此时将上面的3个寄存器弹出栈顶。 之后mov esp的位置esp的指向改变 此时弹出ebpebp弹出以后会指向main函数的栈低因为之前记录着mian函数的栈低。 当前栈顶元素为call指令的下一个指令的地址ret这条指令就是找到之前call指令记录的地址并pop一次栈顶元素。 此时执行add esp8 因为没有dword 所以是改变指向。 此时将形参x,y的空间还给操作系统。此时又执行mov将eax存放的值赋给 ebp - 20h 就是给c赋值。 此时main函数执行完也是以上步骤我们不再赘述。
总结 我们通过观察函数栈帧的创建和销毁最后返回值是由寄存器带回来的也可以理解为什么局部变量的值是随机的形参和实参的关系确实是一份临时拷贝。希望大家下去多加练习逐渐就会顿悟其中的原理。 爆肝一整天点点赞吧呜呜~