C function call stack frame principle and function analysis (rpm)

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
 
Figure 1: push operation

Let us look at an example of pop operation:

// 将0x50弹出栈
pop

 

 
Figure 2: a pop operation


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:

 
Figure 3: a schematic view of the 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) 我们便可以给 ab 和 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

Guess you like

Origin www.cnblogs.com/gaogch/p/11441368.html