In the x86-based computer system, main memory space stack storage parameters for the function return value, the return address and local variables. All function calls have different data, address onto the stack or pop. Therefore, in order to better understand the function call, we need to take a look at how the stack is working.
What Stack is?
Briefly, a LIFO stack form of data structure, all data is LIFO. This form of embodiment of a data structure just to meet we call the function: parent function to call a subroutine, the former parent function, after the subroutine; return, the first subroutine return, the function returns the parent. Stack supports two basic operations, push and pop. The push data onto the stack, pop pop the stack and the data stored in the designated register or memory.
Here is an example of a push operation. Suppose we have a stack, which is part of the yellow zone data has been written, the green part of the region is not yet written data. We will now 0x50 pushed onto the stack:
// 将0x50的压入栈
push $0x50
Let us look at an example of pop operation:
// 将0x50弹出栈
pop
There are two points to be noted, first, in the above example the stack growth direction is from higher addresses to lower addresses, since talking hereinafter stack frame, the stack is to grow down, so that here also used forms a stack; second, after the pop operation, the data stack has not been cleared, but we can not directly access the data. With a basic knowledge of these stacks, we can now take a look at x86-32bit system, C-language function calls is how the.
What stack frames are?
Stack frame, i.e. stack frame, which essentially is a kind of the stack, but this stack is dedicated to various types of information (parameters, return address, local variables, etc.) stored in the function calling procedure. There are sub-stack stack frame and bottom of the stack, the stack which addresses the minimum and maximum address bottom of the stack, SP (stack pointer) that have been pointed top of the stack. In x86-32bit, we use %ebp
point stack bottom, i.e. base pointer; by %esp
pointing to the stack, i.e. the stack pointer. The following is a schematic view of a stack frame:
一般来说,我们将 %ebp
到 %esp
之间区域当做栈帧(也有人认为该从函数参数开始,不过这不影响分析)。并不是整个栈空间只有一个栈帧,每调用一个函数,就会生成一个新的栈帧。在函数调用过程中,我们将调用函数的函数称为“调用者(caller)”,将被调用的函数称为“被调用者(callee)”。在这个过程中,1)“调用者”需要知道在哪里获取“被调用者”返回的值;2)“被调用者”需要知道传入的参数在哪里,3)返回的地址在哪里。同时,我们需要保证在“被调用者”返回后,%ebp
, %esp
等寄存器的值应该和调用前一致。因此,我们需要使用栈来保存这些数据。
函数调用实例
函数的调用
我们直接通过实例来看函数是如何调用的。这是一个有参数但没有调用任何函数的简单函数,我们假设它被其他函数调用。
int MyFunction(int x, int y, int z) { int a, b, c; a = 10; b = 5; c = 2; ... } int TestFunction() { int x = 1, y = 2, z = 3; MyFunction1(1, 2, 3); ... }
对于这个函数,当调用时,MyFunction()
的汇编代码大致如下:
_MyFunction:
push %ebp ; //保存%ebp的值
movl %esp, $ebp ; //将%esp的值赋给%ebp,使新的%ebp指向栈顶 movl -12(%esp), %esp ; //分配额外空间给本地变量 movl $10, -4(%ebp) ; movl $5, -8(%ebp) ; movl $2, -12(%ebp) ;
光看代码可能还是不太明白,我们先来看看此时的栈是什么样的:
此时调用者做了两件事情:第一,将被调用函数的参数按照从右到左的顺序压入栈中。第二,将返回地址压入栈中。这两件事都是调用者负责的,因此压入的栈应该属于调用者的栈帧。我们再来看看被调用者,它也做了两件事情:第一,将老的(调用者的) %ebp
压入栈,此时 %esp
指向它。第二,将 %esp
的值赋给 %ebp
, %ebp
就有了新的值,它也指向存放老 %ebp
的栈空间。这时,它成了是函数 MyFunction()
栈帧的栈底。这样,我们就保存了“调用者”函数的 %ebp
,并且建立了一个新的栈帧。
只要这步弄明白了,下面的操作就好理解了。在 %ebp
更新后,我们先分配一块0x12字节的空间用于存放本地变量,这步一般都是用 sub
或者 mov
指令实现。在这里使用的是 movl
。通过使用 mov
配合 -4(%ebp)
, -8(%ebp)
和 -12(%ebp)
我们便可以给 a
, b
和 c
赋值了。
函数的返回
上面讲的都是函数的调用过程,我们现在来看看函数是如何返回的。从下面这个例子我们可以看出,和调用函数时正好相反。当函数完成自己的任务后,它会将 %esp
移到 %ebp
处,然后再弹出旧的 %ebp
的值到 %ebp
。这样,%ebp
就恢复到了函数调用前的状态了。
int MyFunction( int x, int y, int z ) { int a, int b, int c; ... return; }
其汇编大致如下:
_MyFunction:
push %ebp
movl %esp, %ebp
movl -12(%esp), %esp
...
mov %ebp, %esp
pop %ebp
ret
我们注意到最后有一个 ret
指令,这个指令相当于 pop + jum
。它首先将数据(返回地址)弹出栈并保存到 %eip
中,然后处理器根据这个地址无条件地跳到相应位置获取新的指令。
总结
到这里,C函数的调用过程就基本讲完了。函数的调用其实不难,只要搞懂了如何保存以及还原 %ebp
和 %esp
,就能明白函数是如何通过栈帧进行调用和返回的了。希望这篇文章对你有帮助!
引用
在我学习栈帧以及写这篇文章的过程中,参考了下面这些文章,在这我感谢他们对我提供的大力的帮助。如果你对这些文章感兴趣,请访问以下链接:
1. x86 Instruction Set Reference
2. x86 Disassembly/Functions and Stack Frames
3. x86 Assembly Guide