关于进程中的栈
来源:互联网 发布:淘宝主店铺子店铺 编辑:程序博客网 时间:2024/05/20 04:46
最近看了CU上的一个贴子,楼主想在函数里返回一个数组,有人提到了用返回栈的方法.
栈在C程序员口中常常提及.由其在变量的内存分配时说的最多,比如:
在函数中申请的变量放在栈中,而用malloc分配的空间放在堆中。那么到底什么是进程中栈呢?到底进程中栈有什么用呢?
本文以结合X86 32位linux系统为例来,来对栈及栈相关寄存器进行说明。
本人水平有限,可能存在理解上的错误,本文只是本人的一些心德体会。写出来是希望与大家一起讨论,提高自已的水平。新手要本着怀疑的态度看待本文,同时希望高手多多指点。
一、什么是进程中栈
本人理解的进程中栈其实栈就是一块可以存数据的内存,用来保存程序中用到的变量。在程序运行时,操作系统会为每个的进程分配一个固定大小栈。
二、栈寄存器
esp寄存器指向栈的开始地址。(当然还要涉及到ss这个寄存器,因而要引出实模式和保护模式这里不打印详细说明,以保护模式下的编程为例)。
在现代编译器中,如GCC中,还经常用到别二个与栈有关的寄存器ebp及eax。ebp总是用来保存每个函数中最开始的esp的值。而eax用来保存函数返回的值。
三、栈使用的一个例子:
假设有如下小程序 test.c :
- int sum(int param_a, int param_b, int param_c)
- {
- int ret;
- ret = param_a + param_b + param_c;
- return ret;
- }
- int test()
- {
- int a;
- int b;
- int c;
- int s;
- a = 100;
- b = 200;
- c = 300;
- s = sum( b, a, c);
- return s;
- }
- int main()
- {
- test();
- return 0;
- }
我们用gcc -S test.c 来转换成汇编语言文件test.s 如下:
- .file "test.c"
- .text
- .globl _sum
- .def _sum; .scl 2; .type 32; .endef
- _sum:
- pushl %ebp
- movl %esp, %ebp
- subl $4, %esp
- movl 12(%ebp), %eax
- addl 8(%ebp), %eax
- addl 16(%ebp), %eax
- movl %eax, -4(%ebp)
- movl -4(%ebp), %eax
- leave
- ret
- .globl _test
- .def _test; .scl 2; .type 32; .endef
- _test:
- pushl %ebp
- movl %esp, %ebp
- subl $28, %esp
- movl $100, -4(%ebp)
- movl $200, -8(%ebp)
- movl $300, -12(%ebp)
- movl -12(%ebp), %eax
- movl %eax, 8(%esp)
- movl -4(%ebp), %eax
- movl %eax, 4(%esp)
- movl -8(%ebp), %eax
- movl %eax, (%esp)
- call _sum
- movl %eax, -16(%ebp)
- movl -16(%ebp), %eax
- leave
- ret
- .def ___main; .scl 2; .type 32; .endef
- .globl _main
- .def _main; .scl 2; .type 32; .endef
- _main:
- pushl %ebp
- movl %esp, %ebp
- subl $8, %esp
- andl $-16, %esp
- movl $0, %eax
- addl $15, %eax
- addl $15, %eax
- shrl $4, %eax
- sall $4, %eax
- movl %eax, -4(%ebp)
- movl -4(%ebp), %eax
- call __alloca
- call ___main
- call _test
- movl $0, %eax
- leave
- ret
为了追求简单这里我们不打算从main中的esp的操作。我们从函数test讲起。
- pushl %ebp
- movl %esp, %ebp
- .....
- leave
- ret
首先我们先保存ebp的值,把ebp压入栈中。然后,我们把esp保存在ebp中。这样,ebp保存了进入test函数的esp的值。此后ebp的将不回改变,当函数结束时,即可通过movl %ebp, %esp来恢复esp的值。因为esp的值已经恢复,所以可以通过popl %ebp来恢复ebp的值。即当函数退出后,原函数的调用者的esp及ebp的原值都得到了恢复。汇编指令 leave的作用相相当于上述movl %ebp, %esp 及 popl %ebp 二条指令。最后通过汇编ret返回函数。
然后 subl $28, %esp 指令。令原来的esp减小28个字节。因为x86的栈是自顶而下生长的。即esp一开始指向一个较高的内存地址,当压入栈时。esp的值将减小。因些上述指令相当于压入了一个28个字节的数据,那么数据具体是什么呢。别着急,下面的几条指令将设置压入的数据。
我们知道ebp指向test函数最开始的栈顶。所以:
- movl $100, -4(%ebp)
- movl $200, -8(%ebp)
- movl $300, -12(%ebp)
分别把从栈顶开始的。第四个字节,第八个字节,第12个字节指向的内存,分别设为100, 200,300.即相当于c语言中的
a = 100;
b = 200;
c = 300;
这里还要讲一下,因为这里的100是int型的。在当前环境中占四个字节,同时I386是小端字节序,即内存的高位保存整数中较高位的数字,所以int 100在内存中的表示如下图:
- +-----+ 高
- | 00 |
- +-----+
- | 00 |
- +-----+
- | 00 |
- +-----+
- | 0x64|
- +-----+ 低
而在ebp-4指向的内存中保存100, 即低位保存4字节中最小的字节,而内存的高位保存其它高位的字节。在本例中将是如下型式:
- +-----+ esp
- | 00 |
- +-----+
- | 00 |
- +-----+
- | 00 |
- +-----+
- | 0x64|
- +-----+ esp-4
为简便,我们将其简化如下图:
- +-----+ ebp(a)
- | 100 |
- +-----+ ebp-4 (b)
- | 200 |
- +-----+ ebp-8 (c)
- | 300 |
- +-----+ ebp-12(s)
- | |
- +-----+ ebp-16
- | |
- +-----+ ebp-20
- | |
- +-----+ esp-24
- | |
- +-----+ ebp-28(esp)
接下来我们为调用sum函数准备参数了。
- movl -12(%ebp), %eax
- movl %eax, 8(%esp)
首先把ebp-12的值取出,放入eax中,如上面所说的,ebp-12保存的是300。
接下来把eax的值存入esp+8指向的内存中,即把300保存在esp+8的位置。
接下来的
- movl -4(%ebp), %eax
- movl %eax, 4(%esp)
- movl -8(%ebp), %eax
- movl %eax, (%esp)
如上面所讲的类似。最后的结果即: esp+8 = 300 esp+4 = 100 esp = 200
- +-----+ ebp(a)
- | 100 |
- +-----+ ebp-4 (b)
- | 200 |
- +-----+ ebp-8 (c)
- | 300 |
- +-----+ ebp-12(s)
- | |
- +-----+ ebp-16
- | 300 |
- +-----+ esp+8
- | 100 |
- +-----+ esp+4
- | 200 |
- +-----+ esp(ebp-28)
对应我们的调用sum函数的C语句s = sum( b, a, c); 进程处理实参的顺序是从右向左的。即先处理参数c再处理a,最后是b.这和我们的阅读习惯是不同的。同时,压入栈中的实参的顺序也是从右向左的。esp的指针的顺序倒和我们的人类阅读相同,即esp指向保存b的内存地址, esp+4指向保存a的地址,esp+8保存指向c的地址。
接下来 call _sum程序将跳转到sum函数中执行。32位保护模式下,先把eip指向指令的下一条指令的地址再压入栈中,esp再减4.这样sum函数的最后一条语句ret返回时,会把原来压入的eip的值还原回来,这样eip即指向调用完sum函数的下一条指令。
我们先不跟踪到sum中,我们向下看
- movl %eax, -16(%ebp)
很明显把寄存器eax的保存在ebp-16的位置,即C中的变量s。我们在前面曾经说过,寄存器eax用来返回函数的值。
再接下来 是C语句return s; GCC把它转成如下汇编代码:
- movl -16(%ebp), %eax
- leave
- ret
从上面我们可以看到。ebp-16指向的内存地址即存保的是s的值,我们return s;首先把s的值放到eax中,然后返回。寄存器eax用来返回函数的值,再一次在这条指令中得以验证。
leave前面已经讲过,相当于把ebp的值传入esp中,再从栈中弹出ebp。这样ebp及esp都得到了恢复。ret指令返回调用者。这条指令我没查,但我想应当是把eip恢复成为调用都原来指令的存储位置加1处。
整个test讲解完了。接下来我们看看sum函数。
- pushl %ebp
- movl %esp, %ebp
- subl $4, %esp
- movl 12(%ebp), %eax
- addl 8(%ebp), %eax
- addl 16(%ebp), %eax
- movl %eax, -4(%ebp)
- movl -4(%ebp), %eax
- leave
- ret
我们知道,在test里,esp的指向已经改变了,由原来的位置-28个字节。而在sum里面。我们再次用ebp保存esp的指向,然后我们可以改变esp的指向。 为本次函数服务,当本函数退出时,又可以用上面讲的相同的方法恢复。
即:
- pushl %ebp ,
- movl %esp, %ebp
用来保存test的ebp 及 esp
接下来sum改变esp的值,记其减少4。即,又分配sum函数中 int ret; 的空间。
我们把上面test的内存与sum的内存连起来如下图所示:
- +-----+
- | 100 |
- +-----+
- | 200 |
- +-----+
- | 300 |
- +-----+
- | |
- +-----+
- | 300 |
- +-----+ ebp+16
- | 100 |
- +-----+ ebp+12
- | 200 |
- +-----+ ebp+8
- | (eip)|
- +-----+ ebp+4
- |(ebp)|
- +-----+ ebp
- | |
- +-----+ esp (ebp-4)
注意, 由于指令pushl %ebp 现再在的ebp+4指向的内存中保存着原来ebp的值。
接下来
- movl 12(%ebp), %eax
- addl 8(%ebp), %eax
- addl 16(%ebp), %eax
我们看到用ebp上逆 12 ,8 ,16, 得到sum要传入的三个参数100,200,300。
上面三条汇编对应着C语句。
param_a + param_b + param_c
从调用上我们回头看一下test调用时sum( 200, 100, 100)即可得到
型参param_a=200 param_b=100; param_c=300
而从这个取值序顺序上我们要以看出。 C语言是先处理 param_b + param_c以的。
即先处理 param_b 对应的100 与 param_c 对应的是200 相加,他们的结果再与 param_a相加即300相加结果存入eax中。
然后
- movl %eax, -4(%ebp)
把ebp-4即C语言中的ret变量中存入结果。
接下来处理返回。return ret;
我们说过当函数返回时eax存入返回结果。所以先把ret中的结果存入eax中,再返回。
我们看到
- movl %eax, -4(%ebp)
- movl -4(%ebp), %eax
是个反过程,所以从优化的角度上看。上面的C代码如果用
- return param_a + param_b + param_c;
将会减少一条指令。 即可直接返回eax中的值。
接下的
- leave
- ret
二条汇编我们已经产过多次了。最后栈中的情况恢复到调用前的情况。
如图:
- +-----+ ebp(a)
- | 100 |
- +-----+ ebp-4 (b)
- | 200 |
- +-----+ ebp-8 (c)
- | 300 |
- +-----+ ebp-12(s)
- | |
- +-----+ ebp-16
- | 300 |
- +-----+ esp+8
- | 100 |
- +-----+ esp+4
- | 200 |
- +-----+ esp(ebp-28)
通过上例我们可以看到。函数中的变量,实际上就是栈中存储的数据。而返回一个函数中的局部变量指针,相当于返回了一个栈中的地址,由于esp的移动,栈指向的内存是可以被再次改写的。所以如果我们返回的局部变量,很有可能在再次调用其它函数时改变其内存中的数据。因此不能在函数中返回局部的变量。
三、栈的大小
在X86上,进程一开始,操作系统会为其分配一个固定的栈的大小,然后esp指向最大可用栈地址的最内存地址。
通过上面可以看出,每当分配一个变量或调用一个函数时,可用的栈空间都会减少,每当从函数返回时,栈指针得到恢复,可用的栈空间将会增大。
如果一个函数分配很大的局部变量或递归函数次数过多可能能将栈空间耗尽,就会产生常说的段错误。
linux可以用ulimit -s来查看栈的大小。
- 关于进程中的栈
- 关于网站标准化进程中的居中问题
- 关于杀掉linux中的php进程
- 关于python中的多进程模块multiprocessing
- 关于rac环境中的alter ext进程名, begin now
- 关于QProcess的进程中的运行先后测试
- 关于Java中的进程和线程的理解
- 关于进程
- 关于进程
- 关于进程
- 关于进程
- 关于进程
- 关于进程
- 关于进程
- 关于进程
- 关于进程
- 关于子进程中的exit-----子进程中使用exit对于子进程复制父进程数据段、堆栈的影响
- 进程中的用户堆栈、内核栈
- Python 标准库 BaseHTTPServer 中文翻译
- hdu 1070 Milk
- Unity3D研究院transform.parent = parent坐标就乱了
- android 双服务常驻后台:防止意见清理
- Palindrome Number
- 关于进程中的栈
- Builder 建造者 (创建型)
- 16脚的12864液晶的使用方法和体验
- 使用Latex编译简历
- [leetcode] 173.Binary Search Tree Iterator
- 【Java】只允许使用加号,实现整数的减法,乘法,除法
- Android context空指针异常
- 如何合并多个excel报表到同一表中,只保留一个表头
- POJ