栈帧及可变参数列表

来源:互联网 发布:oracle数据库中文注释 编辑:程序博客网 时间:2024/06/07 16:20

一. 栈帧的概念

     栈帧也叫作过程活动记录,是一个函数的执行环境,在栈帧中记录着函数的变量和返回地址。

     每个栈帧对应一个未完成的函数,每个函数都有其相对应的栈帧。在调用被调函数时,栈帧被创立;被调函数结束后,进行压栈,

     释放临时变量,返回至被调函数的下一步指令的地址处。

     在栈帧中,地址由高向低延伸。栈帧的最高地址为栈底,最低地址为栈顶。


二.使用栈帧的情况

     在函数调用中,有很多情况都可以用栈帧的原理进行解释。

     1. 虚实结合(函数的实参传给形参)

     2. 局部变量的释放

     3. 被调函数结束后的返回地址

     4. return C 的值的带回


三. 分析栈帧的创建过程

     首先,了解关于栈帧的几个名词

     1. ebp—栈底  指针寄存器。其内存放一个指针,该指针指向栈帧的底部。

     2. esp—栈顶  指针寄存器。其内存放一个指针,该指针指向栈帧的顶部。

     3. PC指针 : 永远指向当前运行指令的下一条指令。

     用下面这个简单程序分析栈帧的使用情况

#include<stdio.h>
int fun(int x,int y)
{
   int c = 20;  
   return c;
}
int main()
{
   int a = 1;
   int b = 2;
   int ret = fun (a , b);
   return 0;
}

     栈帧的创建和使用分为三个阶段

     第一阶段:主函数为调用者函数,进入调用者函数,在系统栈中为调用者函数分配栈帧。

                 

                     (1)  在调用者函数的栈帧中为变量 a,b 分配相应的存储空间

                     (2) 程序执行至 int fun(int x,int y) 时,进行实参与形参的虚实结合,形成临时拷贝并将其压入栈中。值的注意的是,

                               实参与形参的虚实结合的顺序是从右向左,先进行参数 b 的临时拷贝,再进行 a 的临时拷贝。

                     (3) 形参实例化之后,执行 call 指令。

                               call 指令有两个重要的作用:① 执行 jmp 指令,跳转至被调函数的入口地址,执行被调函数的相关操作

                                                                             ② 保存被调函数的下一条指令的地址,便于被调函数结束后的返回

                       以上步骤运行结束之后,调用者函数的栈帧创建结束,下面开始进入被调函数的栈帧的创建。

      

     第二阶段:在主函数中运行到被调函数时,为被调函数分配栈帧。


                     (1) push ebp ;   // 保存调用者函数的栈底,放在被调函数的栈底,便于返回

                               被调函数结束后的返回仅靠 call 指令中的记录下一指令的地址是无法实现的,需要将 call 指令和 push 指令结合

                               起来,才能使被调函数结束后能返回到正确的位置。

                     (2) move ebp ,esp;  // 将调用者函数的栈顶赋值给被调函数的栈底

                               此时,被调函数的栈底就是调用者函数的栈顶。

                     (3) sub esp ,occh;  // 给被调函数的栈顶减去一个随机值,指针下移,为被调函数的栈帧创造空间。

                      

     第三阶段:被调函数结束后,压栈,释放局部变量,带回返回值,返回到主函数运行指令的下一步指令。

                     (1) move  eax ,dword ptr [c] ; // 将返回值 c 的值保存在寄存器 eax 中

                     (2) move  esp ,ebp; // 将栈底的地址赋给栈顶的地址。栈顶移动到栈底的位置,释放局部变量。

                     (3) pop ebp; // 将被调函数栈底保存的调用者函数的栈底地址弹出

                     (4) ret ; // 返回地址出栈,返回至主函数的下一指令处。

                     (5) sub  esp , occh; // 被调函数结束后,局部变量被释放,使栈顶指针上移,恢复到未形参实例化之前的位置。

                     (6) move  dword ptr [ret] ,eax ; // 将寄存器中存储的返回值 c 返回至主函数中的变量 ret 中。

     以上就是整个函数调用的栈帧使用原理。


四. 可变参数列表

     在普通函数中,形参的个数是固定的,调用函数时,通过实参与形参的虚实结合实现函数的调用。

     普通函数存在的问题: ① 不对参数个数进行检测

                                          ② 实参与形参不一定一一对应

     为了使函数能够在不同的情况下接收不同数目的参数,我们使用可变参数列表。

     可变参数列表的一般形式:

    (1) # include < stdarg . h >      

              使用可变参数列表时,需要引入的头文件,以下的几个宏都包含在这个头文件中。

    (2) va_list  变量名 ;

              例   va_list  args ;

              这个变量是一个存储可变参数列表的地址的指针,通过这个指针变量找到参数列表的地址,再结合参数类型,得到参数值。

    (3) va_start ( 指针变量名 , 可变参数列表前的最后一个参数 )

              例  va_start ( args ,start );

              将最后一个固定参数的地址传送给指针变量 args ,实现对 va_list 类型变量的初始化。通过指针下移访问下面的参数。 

    (4) va_arg ( 指针变量名 , 下一个参数的类型 );

              例  va_arg ( args , type );

              通过这个宏返回至参数列表的首地址,结合参数类型,得到参数的值。

    (5) va_end ( 指针变量名 );

              例  va_end (args );

              由于被调函数在调用时不知道参数的正确数目,所以需要人为的加上一个结束条件使得函数结束。

     可变参数列表的限制:

     ① 函数不能确定参数的个数

     ② 这些宏不能确定下一个参数的类型,在宏 va_arg 中需谨慎判断参数的类型

     ③ 在可变参数列表中必须存在一个命名参数,否则无法实现 va_start 对 va_list 的初始化

     ④ 使用可变参数列表对参数的访问必须从第一个参数开始,可以中途停止但不能从中间的某一个参数开始访问。

     使用可变参数列表模拟实现 printf 函数

# include < stdio.h >
# include < stdarg.h > 
void  printf ( char *format, ...) 
{
    va_list arg; 
    va_start(arg, format);  
      while(*format != '\0')
    {
        switch(*format)
    {
        case 's':
    {
        char *ret = va_arg(arg, char*);
        while(*ret)
        {
            putchar(*ret);
            ret++;
        }
   }
         break;
      case 'c':

   {
       putchar(va_arg(arg,char));
       break;
       default:
       putchar(*format);
       break;
   }
       format++;
 }
       va_end(arg);
}
int main()
{
      print("sccc\n","he",'l','l','o');
      return 0;

}


五. 大端小端的简单理解

     大端小端又称大尾小尾,顾名思义得

     大端(大尾): 和我们的思维方式相一致,数据的高字节保存在低地址中,数据的低字节保存在高地址中。

     小段(小尾): 小端模式与大端模式相反,数据的低字节保存在低地址中,数据的高字节保存在高地址中。