关于进程中的栈
时间:2010-08-30
来源:互联网
关于进程中的栈
最近看了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;
- }
- .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
- pushl %ebp
- movl %esp, %ebp
- .....
- leave
- ret
然后 subl $28, %esp 指令。令原来的esp减小28个字节。因为x86的栈是自顶而下生长的。即esp一开始指向一个较高的内存地址,当压入栈时。esp的值将减小。因些上述指令相当于压入了一个28个字节的数据,那么数据具体是什么呢。别着急,下面的几条指令将设置压入的数据。
我们知道ebp指向test函数最开始的栈顶。所以:
- movl $100, -4(%ebp)
- movl $200, -8(%ebp)
- movl $300, -12(%ebp)
a = 100;
b = 200;
c = 300;
这里还要讲一下,因为这里的100是int型的。在当前环境中占四个字节,同时I386是小端字节序,即内存的高位保存整数中较高位的数字,所以int 100在内存中的表示如下图:
- +-----+ 高
- | 00 |
- +-----+
- | 00 |
- +-----+
- | 00 |
- +-----+
- | 0x64|
- +-----+ 低
- +-----+ 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)
- movl -12(%ebp), %eax
- movl %eax, 8(%esp)
接下来把eax的值存入esp+8指向的内存中,即把300保存在esp+8的位置。
接下来的
- movl -4(%ebp), %eax
- movl %eax, 4(%esp)
- movl -8(%ebp), %eax
- movl %eax, (%esp)
- +-----+ 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)
接下来 call _sum程序将跳转到sum函数中执行。我们先不跟踪到sum中,我们向下看
- movl %eax, -16(%ebp)
再接下来 是C语句return s; GCC把它转成如下汇编代码:
- movl -16(%ebp), %eax
- leave
- ret
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
即:
- pushl %ebp ,
- movl %esp, %ebp
接下来sum改变esp的值,记其减少4。即,又分配sum函数中 int ret; 的空间。
我们把上面test的内存与sum的内存连起来如下图所示:
- +-----+
- | 100 |
- +-----+
- | 200 |
- +-----+
- | 300 |
- +-----+
- | |
- +-----+ ebp+16
- | 300 |
- +-----+ ebp+12
- | 100 |
- +-----+ ebp+8
- | 200 |
- +-----+ ebp+4
- |(ebp)|
- +-----+ ebp
- | |
- +-----+ esp (ebp-4)
接下来
- movl 12(%ebp), %eax
- addl 8(%ebp), %eax
- addl 16(%ebp), %eax
上面三条汇编对应着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)
接下来处理返回。return ret;
我们说过当函数返回时eax存入返回结果。所以先把ret中的结果存入eax中,再返回。
我们看到
- movl %eax, -4(%ebp)
- movl -4(%ebp), %eax
- return param_a + param_b + param_c;
接下的
- 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)
三、栈的大小
在X86上,进程一开始,操作系统会为其分配一个固定的栈的大小,然后esp指向最大可用栈地址的最内存地址。
通过上面可以看出,每当分配一个变量或调用一个函数时,可用的栈空间都会减少,每当从函数返回时,栈指针得到恢复,可用的栈空间将会增大。
如果一个函数分配很大的局部变量或递归函数次数过多可能能将栈空间耗尽,就会产生常说的段错误。
linux可以用ulimit -s来查看栈的大小。
作者: zhangsuozhu 发布时间: 2010-08-30
呵呵。。
作者: ddgfff 发布时间: 2010-08-30
作者: davelv 发布时间: 2010-08-30
作者: guanzi2010 发布时间: 2010-08-30
PowerPC为大端
另外网络字节序为大端
作者: lenky0401 发布时间: 2010-08-30
作者: lenky0401 发布时间: 2010-08-30
谢谢提醒,我已经改正
作者: zhangsuozhu 发布时间: 2010-08-30
嗯!
作者: zhangsuozhu 发布时间: 2010-08-30
1>>>如果返回值过大,eax不能保存怎么办?
2>>>首先我们先保存ebp的值,把ebp压入栈中。然后,我们把esp保存在ebp中
===============================================
似乎说保存不怎么准确,说定位似乎更好
作者: gtv 发布时间: 2010-08-30
热门阅读
-
office 2019专业增强版最新2021版激活秘钥/序列号/激活码推荐 附激活工具
阅读:74
-
如何安装mysql8.0
阅读:31
-
Word快速设置标题样式步骤详解
阅读:28
-
20+道必知必会的Vue面试题(附答案解析)
阅读:37
-
HTML如何制作表单
阅读:22
-
百词斩可以改天数吗?当然可以,4个步骤轻松修改天数!
阅读:31
-
ET文件格式和XLS格式文件之间如何转化?
阅读:24
-
react和vue的区别及优缺点是什么
阅读:121
-
支付宝人脸识别如何关闭?
阅读:21
-
腾讯微云怎么修改照片或视频备份路径?
阅读:28