调用函数、栈

除了一些最基本的运算,我们在C语言中都是通过调用“函数(function)”来完成各种任务,这是大家都知道的。但是,“调用”函数究竟意味着什么呢?譬如下面的代码:

/*Example C code*/

void

fun(int a, int b)

{

       /*什么都不作*/

}

 

int

main()

{

       fun(100, 200);              //调用函数

       return 0;

}

我们把上面的C代码转成汇编,来看看这个fun函数的调用过程究竟是怎样的。

$gcc –S  test.c            //这条命令让gcc输出汇编代码,对应文件名的后缀为“.s

$cat test.s

.globl fun

       .type       fun, @function

fun:

       pushl       %ebp

       movl      %esp, %ebp

       popl       %ebp

       ret

.globl main

       .type        main, @function

main:

       pushl       %ebp

       movl       %esp, %ebp

       subl        $8, %esp

       andl        $-16, %esp

       movl       $0, %eax

       subl        %eax, %esp

       movl       $100, (%esp)

       movl       $200, 4(%esp)

       call         fun

       movl       $0, %eax

       leave

       ret

.globl fun表示fun是全局可见的,.type fun, @function给出符号(symbolfun的位置,从fun:开始往后就是对应原来C函数fun的代码,直到ret语句从函数返回从而结束函数的执行。同理main也是一个全局函数(global function)。

C程序里fun100 200)调用函数fun对应3条汇编语句(红色标出):

movl       $100, (%esp)         //参数a入栈

movl       $200, 4(%esp)              //参数b入栈

call         fun                       //调用符号fun所在位置的代码

它们做的事情是把参数压栈,然后以“call”指令调用fun的代码,这时指令的执行点就转到fun:符号处开始执行,到执行完fun的时候,通过ret返回,执行点又回到main中的movl $0%eax处。说到这里,大家已经知道“调用一个函数”所发生的事情,就是两个步骤:

首先是参数入栈(通常使用pushmov指令把参数复制到栈里面)然后是指令的执行点转移到函数代码入口(通常用call指令实现跳转)。这里面要注意,C语言标准并没有规定参数入栈的顺序,编译器厂商可以自行决定参数是从右往左还是从左往右压栈。不过,大多数C编译器的参数都是从右往左入栈,即是说,靠右边的参数的内存地址比靠左边的参数的内存地址要高。大家看看上面的汇编代码就会发现,GCC也是这样安排的,参数b的内存地址比参数a的内存地址高。movl $100, (%esp)movl $200, 4(%esp)不就是等同于先pushl $200//参数bpushl $100//参数a嘛,也就是先使b入栈后使a入栈,顺序是从参数列表的右向左压栈。

    现在我们再讨论这个过程所涉及的数据结构:“栈(stack)”。

提起“栈”,第一个反应是它具有后进先出(LIFO)的特性。我们知道,一个程序由数据和代码两大部分构成,而数据有几种类别,一种是“静态”的,也就是说在整个程序运行期间,它在内存中的位置(即地址)是固定的,代码可以对其反复访问,C语言中的外部变量(external variable)、内部静态变量(static internal variable)就属于这种;另一种是“动态”的,它们在内存中的位置不是固定的,例如内部(非静态)变量(internal variable)。通常,内部变量的存取就是对“栈”进行操作。以下面的代码为例,我们探讨在函数调用过程中“栈”的变化:

/*Example C code*/

void

fun(int a, int b)

{

       int local = a;    //对内部变量local进行操作

       a = 3;             //对参数a进行操作

       b = 4;             //对参数b进行操作

}

 

int

main()

{

       fun(100, 200);

       return 0;

}

上面的程序仅仅是在我们前面的程序基础上在fun函数里加了一个内部变量local以及几条赋值语句,这些赋值语句分别对内部变量和参数进行操作,由于内部变量和参数都是放在栈里面的,所以我们可以观察一下其汇编指令是如何操作内存的,借此了解栈的布局:

$gcc –S test.c

$cat test.s

.globl fun

       .type       fun, @function

fun:

       pushl       %ebp

       movl       %esp, %ebp       //这里没有用esp直接寻址,见最后的说明

       subl        $4, %esp

       movl       8(%ebp), %eax

       movl       %eax, -4(%ebp)

       movl       $3, 8(%ebp)

       movl       $4, 12(%ebp)

       leave

       ret

.globl main

       .type       main, @function

main:

       ...

       movl       $100, (%esp)

       movl       $200, 4(%esp)

       call         fun

       ...

进入函数代码后的一系列动作分别是:

寄存器ebp的内容压栈保存,esp的值赋给ebp,划分出用于存放函数内部变量的整个框架,到此,栈的布局如下(其中X代表某个内存地址)所示。

内存地址

存放的内容

寄存器

...

...

ebp

X

X+15

b

esp

X-4

X+14

注意:

1、  ab是在调用fun函数前压栈的。

2、  fun返回后执行的第一条指令的地址由“call fun”指令自动压栈。

3、  每个int4个字节。

4、  通常情况下,栈是往内存的低地址方向增长的,也就是说,先压栈的内容存放在高地址区域,后压栈的内容放在低地址区域。

X+13

X+12

X+11

a

X+10

X+9

X+8

X+7

call fun”后面第一条指令的地址

X+6

X+5

X+4

X+3

原先ebp的值

X+2

X+1

X

X-1

local

X-2

X-3

X-4

...

...

然后,movl 8(%ebp), %eax把参数a的值放入eaxmovl %eax, -4(%ebp)eax的值赋值给localmovl $3, 8(%ebp)movl $4, 12(%ebp)分别对ab赋值。这时栈的内容如下所示:

内存地址

存放的内容

 

 

 

注意:

这里的硬件平台是属于little-endian

...

...

X+15

         0x00

b       0x00

         0x00

         0x04

X+14

X+13

X+12

X+11

         0x00

a       0x00

         0x00

         0x03

X+10

X+9

X+8

X+7

call fun”后面第一条指令的地址,根据实际情况而定

X+6

X+5

X+4

X+3

原先ebp的值,根据实际情况而定

X+2

X+1

X

X-1

0x00

local         0x00

                0x00

                0x64

X-2

X-3

X-4

...

...

 

注:为什么不直接用esp寻址,而要先保存ebp,然后用ebp存放esp的值,再通过ebp来寻址吗?其实直接esp寻址是可以的,但gcc默认输出的汇编代码是用ebp寻址,用esp勾画出整个函数的栈空间。这样做的好处是代码非常清晰,便于分析研究。如果要追求更高的运行效率,例如编译linux内核时你会发现函数内的确是直接用esp寻址的。可以用编译选项指示gcc直接用esp寻址,例如:

$gcc –fomit-frame-pointer example.c

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值