对栈操作的理解
一个CPU具有八个通用寄存器:EAX,EDX,ECX,ESI,EDI,EBP,ESP,EBX。这八个通用寄存器中的每一个都被安排了特定用途,CPU在执行某些特定指令时需要特定的寄存器协作以高效的完成其指令执行过程。
其中有两个重要的寄存器负责栈的处理: EBP(扩展基址指针)和 ESP(扩展栈顶指针)。
除上面的八个通用寄存器,下面还会提到 EIP(扩展指令指针)。
当调用一个函数时:
①:调用程序首先将函数参数压入栈中,从而对函数调用进行设置。
②:将EIP保存到栈上,这样程序在函数返回后就能够在之前离开的地方继续执行。这个地址被称为返回地址。
③:执行call命令,将该函数的地址放入EIP中并执行。
而被调用函数的职责是:
①:将调用程序的EBP寄存器保存到栈上。
②:将当前ESP寄存器保存到EBP寄存器。(设置当前栈帧)
③:将ESP寄存器减小,从而为该函数的本地变量腾出空间。
④:上面的语句执行完毕后,函数才可以执行自己的语句。
被调用函数返回到调用程序之前:
①:将ESP增加到EBP,从而将栈清空。
②:从栈中弹出所保存的EIP。
说了这么多,其实不用知道这些,我们只需要知道函数调用的工作原理就行了。
函数调用的工作原理就是:压栈 - 操作 - 弹栈 ,但是有上面的解释,我们可以更好的理解函数调用的原理。
而压栈的本质就是记录压栈前的状态,弹栈是恢复到压栈前的状态。而这个记录状态的东西(想不出来叫什么,暂且叫它东西吧),这个东西当然要能在弹栈时恢复的跟压栈前一模一样,这样,我们就需要知道在函数执行过程中哪些量变了,哪些量没变,所因为我们没必要将没变的量压入栈中。现在,重点已经不是压栈和弹栈了,重点是如何恢复到函数调用前的那个状态。下面贴上两段代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n = 2;
print(n + 1);
printf("%d\n",n);
}
int print(int n)
{
printf("%d\n",n);
// n++;
return n;
}
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n = 2;
{
n += 1;
printf("%d\n",n);
// n++;
n -= 1;
}
printf("%d\n",n);
}
我们先不看注释掉的部分。第一段代码调用了函数来输出,而第二段代码中没有调用函数,只是用{}括起来了一部分代码,两段代码的结果一模一样,而第二段代码没有用栈就实现了第一段代码中用栈的效果,可想而知,效率肯定是比用栈高的。这样做的原理其实很简单,函数调用时的栈操作就是保存调用的参数的信息,并在函数操作结束时还原参数信息。而第二段代码的思想就是在压栈前(printf函数执行前)对“参数”进行更改,并在弹栈后(printf函数执行完后)将“参数”恢复为原来的值。所以可以实现和第一段代码相同的功能。
现在我们可以看看注释的部分,如果加上注释的部分,两段代码的结果就不同了。第一段代码的运行结果并没有改变,但是第二段代码的结果变了。原因是第二段代码在{}内的代码段里,没能将n改回原先的值。
看到这里应该有些小收获吧,把第二段代码改一下就不会出现n不能变回原值的问题了。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int temp,n = 2;
{
temp = n;
printf("%d\n",n);
n++;
n = temp;
}
printf("%d\n",n);
}
这样子,用temp记录下了n的初始值,接下来在{}内部,n想怎么改就怎么改,不用在担心n不能变回原始值的问题了。
哈啊啊,这样子就体现出了栈的三大特性(这三大特性是我自己总结的,可能并不是很准确。)之一:记录。而其他两大特性:追踪 和 保密 并没有体现出来。
追踪无法体现的原因是:假设还有temp1,temp2,……tempn,由于各个temp并不是连续定义的,所以他们在内存中的地址不连续,如果知道temp1的地址,无法根据temp1找到temp2的地址,以此类推,这样子的话就无法实现连续压栈,连续弹栈的操作。
保密无法体现的原因是:temp定义在这个代码段之外,也就是说temp的访问权限在main函数手里,而不是在这个代码段手里。这样子,main函数中的其他操作也可以更改temp,这样会造成潜在的错误。这里的感觉有点像面向对象的封装,的确是有封装的意思,但是最致命的并不是没有封装,致命的是如果一个栈不能保密的话,访问栈的人就可以获取栈顶的地址,而找到栈顶地址就意味着整个栈的信息都可以通过访问地址访问到,而通过访问地址访问栈的元素就已经脱离了栈的规则,也就是说访问者可以随意修改你的栈,当然也可以让你的栈崩溃。
其他的我就不多说了,我所理解的栈的三大特性:记录,追踪 和 保密 ,以后还会在针对这三点做些文章的。
希望大家看了之后能有收获。
http://my.csdn.net/restlessssh
一个CPU具有八个通用寄存器:EAX,EDX,ECX,ESI,EDI,EBP,ESP,EBX。这八个通用寄存器中的每一个都被安排了特定用途,CPU在执行某些特定指令时需要特定的寄存器协作以高效的完成其指令执行过程。
其中有两个重要的寄存器负责栈的处理: EBP(扩展基址指针)和 ESP(扩展栈顶指针)。
除上面的八个通用寄存器,下面还会提到 EIP(扩展指令指针)。
当调用一个函数时:
①:调用程序首先将函数参数压入栈中,从而对函数调用进行设置。
②:将EIP保存到栈上,这样程序在函数返回后就能够在之前离开的地方继续执行。这个地址被称为返回地址。
③:执行call命令,将该函数的地址放入EIP中并执行。
而被调用函数的职责是:
①:将调用程序的EBP寄存器保存到栈上。
②:将当前ESP寄存器保存到EBP寄存器。(设置当前栈帧)
③:将ESP寄存器减小,从而为该函数的本地变量腾出空间。
④:上面的语句执行完毕后,函数才可以执行自己的语句。
被调用函数返回到调用程序之前:
①:将ESP增加到EBP,从而将栈清空。
②:从栈中弹出所保存的EIP。
说了这么多,其实不用知道这些,我们只需要知道函数调用的工作原理就行了。
函数调用的工作原理就是:压栈 - 操作 - 弹栈 ,但是有上面的解释,我们可以更好的理解函数调用的原理。
而压栈的本质就是记录压栈前的状态,弹栈是恢复到压栈前的状态。而这个记录状态的东西(想不出来叫什么,暂且叫它东西吧),这个东西当然要能在弹栈时恢复的跟压栈前一模一样,这样,我们就需要知道在函数执行过程中哪些量变了,哪些量没变,所因为我们没必要将没变的量压入栈中。现在,重点已经不是压栈和弹栈了,重点是如何恢复到函数调用前的那个状态。下面贴上两段代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n = 2;
print(n + 1);
printf("%d\n",n);
}
int print(int n)
{
printf("%d\n",n);
// n++;
return n;
}
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n = 2;
{
n += 1;
printf("%d\n",n);
// n++;
n -= 1;
}
printf("%d\n",n);
}
我们先不看注释掉的部分。第一段代码调用了函数来输出,而第二段代码中没有调用函数,只是用{}括起来了一部分代码,两段代码的结果一模一样,而第二段代码没有用栈就实现了第一段代码中用栈的效果,可想而知,效率肯定是比用栈高的。这样做的原理其实很简单,函数调用时的栈操作就是保存调用的参数的信息,并在函数操作结束时还原参数信息。而第二段代码的思想就是在压栈前(printf函数执行前)对“参数”进行更改,并在弹栈后(printf函数执行完后)将“参数”恢复为原来的值。所以可以实现和第一段代码相同的功能。
现在我们可以看看注释的部分,如果加上注释的部分,两段代码的结果就不同了。第一段代码的运行结果并没有改变,但是第二段代码的结果变了。原因是第二段代码在{}内的代码段里,没能将n改回原先的值。
看到这里应该有些小收获吧,把第二段代码改一下就不会出现n不能变回原值的问题了。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int temp,n = 2;
{
temp = n;
printf("%d\n",n);
n++;
n = temp;
}
printf("%d\n",n);
}
这样子,用temp记录下了n的初始值,接下来在{}内部,n想怎么改就怎么改,不用在担心n不能变回原始值的问题了。
哈啊啊,这样子就体现出了栈的三大特性(这三大特性是我自己总结的,可能并不是很准确。)之一:记录。而其他两大特性:追踪 和 保密 并没有体现出来。
追踪无法体现的原因是:假设还有temp1,temp2,……tempn,由于各个temp并不是连续定义的,所以他们在内存中的地址不连续,如果知道temp1的地址,无法根据temp1找到temp2的地址,以此类推,这样子的话就无法实现连续压栈,连续弹栈的操作。
保密无法体现的原因是:temp定义在这个代码段之外,也就是说temp的访问权限在main函数手里,而不是在这个代码段手里。这样子,main函数中的其他操作也可以更改temp,这样会造成潜在的错误。这里的感觉有点像面向对象的封装,的确是有封装的意思,但是最致命的并不是没有封装,致命的是如果一个栈不能保密的话,访问栈的人就可以获取栈顶的地址,而找到栈顶地址就意味着整个栈的信息都可以通过访问地址访问到,而通过访问地址访问栈的元素就已经脱离了栈的规则,也就是说访问者可以随意修改你的栈,当然也可以让你的栈崩溃。
其他的我就不多说了,我所理解的栈的三大特性:记录,追踪 和 保密 ,以后还会在针对这三点做些文章的。
希望大家看了之后能有收获。
http://my.csdn.net/restlessssh