加载中…
个人资料
老徐
老徐
  • 博客等级:
  • 博客积分:0
  • 博客访问:829,052
  • 关注人气:156
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
相关博文
推荐博文
谁看过这篇博文
加载中…
正文 字体大小:

[面试题]EMC易安信-C语言函数调用详细过程

(2012-09-20 12:41:26)
标签:

堆栈

函数调用

it

分类: C/CPlusPlus

作者: Badcoffee

Email: blog.oliver@gmail.com

200410

原文出处: 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), not stripped
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 Public License, and you are welcome to change it and/or distribute copies of it under certain 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 host libthread_db library "/lib/tls/libthread_db.so.1".

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

Dump of assembler code for function main:

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

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

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

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

0x08048319 :    mov    $0x0,�x  --->  无意义

0x0804831e :   sub    �x,%esp  --->  无意义

0x08048320 :   mov    $0x0,�x   ---> 设置函数返回值0

0x08048325 :   leave     --->ebp值赋给esppop先前栈内的上级函数栈的基地址给ebp,恢复原栈基址.

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

0x08048327 :   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    �p,�p

0x08048266 <_start+2>:  pop    %esi

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

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

0x0804826c <_start+8>:  push   �x

0x0804826d <_start+9>:  push   %esp

0x0804826e <_start+10>: push   �x

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

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

0x08048279 <_start+21>: push   �x

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   �p

          movl    %esp,�p

      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) 函数的参数传递和局部变量访问可以通过STPEBP来实现
           
由于栈框架指针永远指向当前函数的栈基地址,参数和局部变量访问通常为如下形式:
             +8+xx(�p)   :函数入口参数的的访问  ebp向高地址处延伸可以访问实参

             -xx(�p)       :函数局部变量访问  ebp向低地址处延伸可以访问局部变量

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

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

汇编主程序在调用函数的时候为什么要先把函数的参数先入栈,然后再把返回地址入栈

除了使用寄存器传递参数外,所有函数参数都要先入栈,再压入返回地址。

why?
call 指令的工作之一就是要压入 eip 寄存器,SO: 返回地址是 call 自动执行的。 那么你必须先将参数放入栈中

push 参数2

push 参数1

call fun     <-- 返回地址由 call 指令压入



>> 如果先把返回地址入栈的话

你可以先将返回地址压入栈,再将参数入栈吗?? 做得到吗?
NO. 不可能做到,因为:返回地址是由 call 指令自动入栈的

概念四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   ---> 希望栈按照2n次的字节边界对齐, n的取值范围是2-12.

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

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

     
(gdb) disass main

Dump of assembler code for function main:

0x08048310 :    push   �p

0x08048311 :    mov    %esp,�p

0x08048313 :    mov    $0x0,�x

0x08048318 :    leave

0x08048319 :    ret

0x0804831a :   nop

0x0804831b :   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 for function main:

0x08048310 :    mov    $0x0,�x

0x08048315 :    ret

0x08048316 :    nop

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

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

3. 小结

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

STP 栈框架指针

Stack aligned 栈对齐

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

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

 

0

阅读 评论 收藏 转载 喜欢 打印举报/Report
  • 评论加载中,请稍候...
发评论

    发评论

    以上网友发言只代表其个人观点,不代表新浪网的观点或立场。

      

    新浪BLOG意见反馈留言板 欢迎批评指正

    新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 会员注册 | 产品答疑

    新浪公司 版权所有