c语言函数调用过程

 原文地址在 http://m.blog.chinaunix.net/uid-7390305-id-2057207.html


这是从别处看到的一篇文章,感觉写的非常深入细致,是作者学习汇编语言的笔记,但是我觉得这篇文章对理解C函数调用非常有帮助,于是参考作者的步骤在自己的linux机器上实现了一下, 并对文章做了一些细小的更改,在此对原作者致谢,如果作者觉得这样有些冒犯的话,请通知我我会立即撤掉。


X86汇编语言学习手记(1)

 

作者: Badcoffee

Email: blog.oliver@gmail.com

2004年10月

 

原文出处: http://blog.csdn.net/yayong

版权所有: 转载时请务必以超链接形式标明文章原始出处、作者信息及本声明.

这是作者在学习X86汇编过程中的学习笔记,难免有错误和疏漏之处,欢迎指正。

 

1. 编译环境

 

   OS: Axianux 1.0
   Compiler: gcc 3..2.3

   Linker: Solaris Link Editors 5.x
   Debug Tool: gdb
   Editor: vi

2. 最简C代码分析

为简化问题,来分析一下最简的c代码生成的汇编代码:
    # vi test1.c
     
    int main()
    {
        return 0;
    }  
   
     编译该程序,产生二进制文件:
    # gcc -o start start.c

# file start    

start: ELF 32-bit LSB executable, Intel 80386, version 1(SYSV), for GNU/Linux 2.2.5, dynamically linked (uses shared libs), notstripped
     start是一个ELF格式32位小端(Little Endian)的可执行文件,动态链接并且符号表没有去除。这正是Unix/Linux平台典型的可执行文件格式。

     用gdb反汇编可以观察生成的汇编代码:

[wqf@15h166 attack]$ gdb start

GNU gdb Asianux (6.0post-0.20040223.17.1AX)

Copyright 2004 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General PublicLicense, and you are welcome to change it and/or distribute copies of it undercertain conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for GDB.  Type "show warranty" for details.

This GDB was configured as"i386-asianux-linux-gnu"...(no debugging symbols found)...Using hostlibthread_db library "/lib/tls/libthread_db.so.1".

 

(gdb) disassemble main          --->反汇编main函数

Dump of assembler code for function main:

0x08048310 <main+0>:    push  %ebp   --->ebp寄存器内容压栈,即保存main函数的上级调用函数的栈基地址

0x08048311 <main+1>:    mov   %esp,%ebp  ---> esp值赋给ebp,设置main函数的栈基址

0x08048313 <main+3>:    sub   $0x8,%esp  --->通过ESP-8来分配8字节堆栈空间

0x08048316 <main+6>:    and   $0xfffffff0,%esp --->使栈地址16字节对齐

0x08048319 <main+9>:    mov   $0x0,%eax  --->  无意义

0x0804831e <main+14>:   sub   %eax,%esp  --->  无意义

0x08048320 <main+16>:   mov   $0x0,%eax   ---> 设置函数返回值0

0x08048325 <main+21>:   leave    --->将ebp值赋给esp,pop先前栈内的上级函数栈的基地址给ebp,恢复原栈基址.

0x08048326 <main+22>:   ret   --->main函数返回,回到上级调用.

0x08048327 <main+23>:   nop

End of assembler dump.

注:这里得到的汇编语言语法格式与Intel的手册有很大不同,Unix/Linux采用AT&T汇编格式作为汇编语言的语法格式,如果想了解AT&T汇编可以参考文章 Linux 汇编语言开发指南.

 

问题一:谁调用了 main函数?

在C语言的层面来看,main函数是一个程序的起始入口点,而实际上,ELF可执行文件的入口点并不是main而是_start。

gdb也可以反汇编_start:

 

(gdb)disass _start          --->从_start的地址开始反汇编

Dump of assembler code for function _start:

0x08048264 <_start+0>: xor    %ebp,%ebp

0x08048266 <_start+2>:  pop    %esi

0x08048267 <_start+3>: mov    %esp,%ecx

0x08048269 <_start+5>: and    $0xfffffff0,%esp

0x0804826c <_start+8>: push   %eax

0x0804826d <_start+9>: push   %esp

0x0804826e <_start+10>: push  %edx

0x0804826f <_start+11>: push  $0x8048370

0x08048274 <_start+16>: push  $0x8048328

0x08048279 <_start+21>: push  %ecx

0x0804827a <_start+22>: push  %esi

0x0804827b <_start+23>: push  $0x8048310

0x08048280<_start+28>: call   0x8048254<__libc_start_main>

--->在这里调用了main函数

0x08048285 <_start+33>: hlt

0x08048286 <_start+34>: nop

0x08048287 <_start+35>: nop

End of assembler dump.  

问题二:为什么用EAX寄存器保存函数返回值?

      实际上IA32并没有规定用哪个寄存器来保存返回值。但是,如果反汇编Solaris/Linux的二进制文件,就会发现,都用EAX保存函数返回值。

      这不是偶然现象,是操作系统的ABI(Application Binary Interface)来决定的。

      Solaris/Linux操作系统的ABI就是Sytem V ABI

 

概念三SFP (Stack Frame Pointer) 栈帧指针

 

       正确理解SFP必须了解:

       IA32 的栈的概念

       CPU 中32位寄存器ESP/EBP的作用

       PUSH/POP 指令是如何影响栈的

       CALL/RET/LEAVE 等指令是如何影响栈的

 

      如我们所知:

1)   IA32的栈是用来存放临时数据,而且是LIFO,即后进先出的。栈的增长方向是从高地址向低地址增长,按字节为单位编址。

      2) EBP是栈基址的指针,永远指向栈底(高地址),ESP是栈指针,永远指向栈顶(低地址)。

      3) PUSH一个long型数据时,以字节为单位将数据压入栈,从高到低按字节依次将数据存入ESP-1、ESP-2、ESP-3、ESP-4的地址单元。

      4) POP一个long型数据,过程与PUSH相反,依次将ESP-4、ESP-3、ESP-2、ESP-1从栈内弹出,放入一个32位寄存器。

      5) CALL指令用来调用一个函数或过程,此时,下一条指令地址会被压入堆栈,以备返回时能恢复执行下条指令。

      6) RET指令用来从一个函数或过程返回,之前CALL保存的下条指令地址会从栈内弹出到EIP寄存器中,程序转到CALL之前下条指令处执行。

      7) ENTER是建立当前函数的栈框架,即相当于以下两条指令:

          pushl  %ebp

         movl    %esp,%ebp

      8) LEAVE是释放当前函数或者过程的栈框架,即相当于以下两条指令:

         movl ebp, esp

         popl  ebp

 

原来编译器会自动在函数入口和出口处插入创建和释放栈框架的语句。

        函数被调用时:

        1) EIP/EBP成为新函数栈的边界

           函数被调用时,返回时的EIP首先被压入堆栈;创建栈框架时,上级函数栈的EBP被压入堆栈,与EIP一道行成新函数栈框架的边界。        

        2) EBP成为栈帧指针STP,用来指示新函数栈的边界
           
栈帧建立后,EBP指向的栈的内容就是上一级函数栈的EBP,可以想象,通过EBP就可以把层层调用函数的栈都回朔遍历一遍,调试器就是利用这个特性实现backtrace功能的。

        3) ESP总是作为栈指针指向栈顶,用来分配栈空间
            栈分配空间给函数局部变量时的语句通常就是给ESP减去一个常数值,例如,分配一个整型数据就是 ESP-4。

        4) 函数的参数传递和局部变量访问可以通过STP即EBP来实现
           
由于栈框架指针永远指向当前函数的栈基地址,参数和局部变量访问通常为如下形式:
            +8+xx(%ebp)         :函数入口参数的的访问

            -xx(%ebp)             :函数局部变量访问

假如函数A调用函数B,函数B调用函数C ,则函数栈帧及调用关系如下图所示:

       +----------------------+----> 高地址
       | EIP (上级函数返回地址)  |   
       +----------------------+   
  +--> | EBP (上级函数的EBP)    | --+     <------ 当前函数A的EBP (即STP框架指针) 
  |    +----------------------+   +-->偏移量A    
  |    | Local Variables      |   |
  |    |    ..........        | --+     <------ ESP指向函数A新分配的局部变量,局部变量可以通过A的ebp-偏移量A访问   
  | f  +----------------------+
  | r  | Arg n(函数B的第n个参数) | 
  | a  +----------------------+
  | m  | Arg .(函数B的第.个参数) |
  | e  +----------------------+
  |    | Arg 1(函数B的第1个参数) |
  | o  +----------------------+
  | f  | Arg 0(函数B的第0个参数) |   --+   <------ B函数的参数可以由B的ebp+偏移量B访问
  |    +----------------------+     +--> 偏移量B
  | A  | EIP (A函数的返回地址)   |     |
  |    +----------------------+   --+
  +--- | EBP (A函数的EBP)      |<--+  <------ 当前函数B的EBP (即STP框架指针) 
       +----------------------+   |
       | Local Variables      |   |
       |    ..........        |   |  <------ ESP指向函数B新分配的局部变量
       +----------------------+   |
       | Arg n(函数C的第n个参数) |   |
       +----------------------+   |
       | Arg .(函数C的第.个参数) |   |
       +----------------------+   +--> frame of B
       | Arg 1(函数C的第1个参数) |   |
       +----------------------+   |
       | Arg 0(函数C的第0个参数) |   |
       +----------------------+   |
       | EIP (B函数的返回地址)   |   |
       +----------------------+   |
 +-->  | EBP (B函数的EBP)      |---+  <------ 当前函数C的EBP (即STP框架指针) 
 |     +----------------------+
 |     | Local Variables      |
 |     |    ..........        |      <------ ESP指向函数C新分配的局部变量
 |     +----------------------+----> 低地址
frame of C

 

 

概念四Stack aligned 栈对齐

      那么,以下语句到底是和作用呢?

      subl    $8,%esp

      andl    $0xfffffff0,%esp     --->通过andl使低4位为0,保证栈地址16字节对齐

表面来看,这条语句最直接的后果是使ESP的地址后4位为0,即16字节对齐,那么为什么这么做呢?

原来,IA32 系列CPU的一些指令分别在4、8、16字节对齐时会有更加的运行速度,因此gcc编译器为提高生成代码在IA32上的运行速度,默认对产生的代码进行16字节对齐.

andl    $0xf0,%esp 的意义很明显,那么 subl   $8,%esp 呢,是必须的吗?这里假设在进入main函数之前,栈是16字节对齐的,那么,进入main函数后,EIP被压入堆栈后,栈地址最末4位必定是0100,esp-8则恰好使后4位地址为0。看来,这也是为保证栈16字节对齐的。

如果查一下gcc的手册,就会发现关于栈对齐的参数设置:

       -mpreferred-stack-boundary=n  ---> 希望栈按照2的n次的字节边界对齐, n的取值范围是2-12.

默认情况下,n是等于4的,也就是说,默认情况下,gcc是16字节对齐,以适应IA32大多数指令的要求。

让我们利用-mpreferred-stack-boundary=2来去除栈对齐指令:

     
(gdb) disass main

Dump of assembler code for function main:

0x08048310 <main+0>:   push   %ebp

0x08048311 <main+1>:   mov    %esp,%ebp

0x08048313 <main+3>:   mov    $0x0,%eax

0x08048318 <main+8>:    leave

0x08048319 <main+9>:    ret

0x0804831a <main+10>:   nop

0x0804831b <main+11>:   nop

End of assembler dump.

可以看到,栈对齐指令没有了,因为,IA32的栈本身就是4字节对齐的,不需要用额外指令进行对齐。

 

问题五:栈框架指针STP是不是必须的呢?

[wqf@15h166attack]$ gcc -mpreferred-stack-boundary=2-fomit-frame-pointer start.c -o start

[wqf@15h166attack]$ gdb  start

(gdb) disass main

Dump of assembler code forfunction main:

0x08048310 <main+0>:    mov   $0x0,%eax

0x08048315 <main+5>:    ret

0x08048316 <main+6>:    nop

0x08048317 <main+7>:    nop

End of assembler dump.

由此可知,-fomit-frame-pointer 可以去除STP。

 

去除STP后有什么缺点呢?

      1)增加调式难度

          由于STP在调试器backtrace的指令中被使用到,因此没有STP该调试指令就无法使用。

       2)降低汇编代码可读性

          函数参数和局部变量的访问,在没有ebp的情况下,都只能通过+xx(esp)的方式访问,而很难区分两种方式,降低了程序的可读性。

去除STP有什么优点呢?

       1)节省栈空间。

      2)减少建立和撤销栈框架的指令后,简化了代码。

      3)使ebp空闲出来,使之作为通用寄存器使用,增加通用寄存器的数量
       4)以上3点使得程序运行速度更快。

 

概念六:Calling Convention 调用约定和ABI (Application Binary Interface) 应用程序二进制接口。

       函数如何找到它的参数?

       函数如何返回结果?

       函数在哪里存放局部变量?

        哪一个硬件寄存器是起始空间?

        哪一个硬件寄存器必须预先保留?

Calling Convention 调用约定对以上问题作出了规定。CallingConvention也是ABI的一部分。因此,遵守相同ABI规范的操作系统,使其相互间实现二进制代码的互操作成为了可能。

例如:由于Solaris、Linux都遵守System V的ABI,Solaris10就提供了直接运行Linux二进制程序的功能。

3. 小结

本文通过最简的C程序,引出以下概念:

STP 栈框架指针

Stack aligned 栈对齐

Calling Convention  调用约定 和 ABI (Application Binary Interface) 应用程序二进制接口

今后,将通过进一步的实验,来深入了解这些概念。通过掌握这些概念,使在汇编级调试程序产生的core dump、掌握C语言高级调试技巧成为了可能。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值