代码是如何在栈中执行
2011-05-17 20:43
120 查看
关于进程中的栈
最近看了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来查看栈的大小。
最近看了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来查看栈的大小。
相关文章推荐
- IAR下如何确定某一段代码的执行时间
- "abc"已经被创建并保存于字符串池中,因此JAVA虚拟机只会在堆中新创建一个String对象,但是它的值(value)是共享前一行代码执行时在栈中创建的三个char型值值'a'、'b'和'c'
- Monkey源码分析2—Monkey代码如何被启动执行
- 180314 如何用Xmanager本地调试代码+远程执行代码
- C# Javascript引擎,如何在C#中执行现有的JS代码?
- 用户态进程如何在堆栈执行代码
- 如何把java代码,打包成jar文件以及转换为exe可执行文件
- 手把手教你如何把java代码,打包成jar文件以及转换为exe可执行文件
- Debug时如何跳过(不执行)某些代码
- Android java代码中如何执行shell命令
- 如何让ajax执行完在执行下一段代码
- 如何在c#代码中执行带GO语句的SQL文件
- 如何使函数不生成执行代码
- PHP代码如何被执行?
- 12个方面讲解如何优化jQuery代码的执行效率
- 如何用C#动态编译、执行代码例程(2)
- 如何跟踪java代码的执行
- 如何快速测试代码的执行效率
- PHP内核探索 —— 解释器的执行过程:引擎是如何执行PHP代码的
- 【Android】如何方便地将代码抛到主线程执行