函数调用是如何在系统中实现的-以C为例

本文详细解析了在32位Linux环境下使用GCC编译器时,函数调用的具体过程及其实现原理,包括参数传递方式、函数栈帧的创建与回收机制等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

     在开发程序中, 有很多的函数之间的调用,这些调用是如何在OS中实现的呢, 下面试我的理解。

1  首先明确几个概念: 

    1)系统栈:OS在调用每个进程的时候都会分配相应的堆栈给进程

    2)函数栈:在调用每个函数的时候,都会从系统栈中分配相应的栈空间给函数。这个函数栈是如何来确定的呢? 通过ESP(栈顶指针)和EBP(栈底指针)来确定的。

    3)寄存器:EIP。cpu通过这个寄存器的值来确定下一条所执行的指令。

 2 调用过程: 

    比如有个例子:这个例子是在32位linux用gcc编译的结果.

   

int bar(int c, int d)
{
	int e = c + d;
	return e;
}

int foo(int a, int b)
{
	return bar(a, b);
}

int main(void)
{
	foo(2, 3);
	return 0;
}
产生的汇编代码如下:

int bar(int c, int d)
{
 8048394:	55                   	push   %ebp
 8048395:	89 e5                	mov    %esp,%ebp
 8048397:	83 ec 10             	sub    $0x10,%esp
	int e = c + d;
 804839a:	8b 55 0c             	mov    0xc(%ebp),%edx
 804839d:	8b 45 08             	mov    0x8(%ebp),%eax
 80483a0:	01 d0                	add    %edx,%eax
 80483a2:	89 45 fc             	mov    %eax,-0x4(%ebp)
	return e;
 80483a5:	8b 45 fc             	mov    -0x4(%ebp),%eax
}
 80483a8:	c9                   	leave  
 80483a9:	c3                   	ret    

080483aa <foo>:

int foo(int a, int b)
{
 80483aa:	55                   	push   %ebp
 80483ab:	89 e5                	mov    %esp,%ebp
 80483ad:	83 ec 08             	sub    $0x8,%esp
	return bar(a, b);
 80483b0:	8b 45 0c             	mov    0xc(%ebp),%eax
 80483b3:	89 44 24 04          	mov    %eax,0x4(%esp)
 80483b7:	8b 45 08             	mov    0x8(%ebp),%eax
 80483ba:	89 04 24             	mov    %eax,(%esp)
 80483bd:	e8 d2 ff ff ff       	call   8048394 <bar>
}
 80483c2:	c9                   	leave  
 80483c3:	c3                   	ret    

080483c4 <main>:

int main(void)
{
 80483c4:	8d 4c 24 04          	lea    0x4(%esp),%ecx
 80483c8:	83 e4 f0             	and    $0xfffffff0,%esp
 80483cb:	ff 71 fc             	pushl  -0x4(%ecx)
 80483ce:	55                   	push   %ebp
 80483cf:	89 e5                	mov    %esp,%ebp
 80483d1:	51                   	push   %ecx
 80483d2:	83 ec 08             	sub    $0x8,%esp
	foo(2, 3);
 80483d5:	c7 44 24 04 03 00 00 	movl   $0x3,0x4(%esp)
 80483dc:	00 
 80483dd:	c7 04 24 02 00 00 00 	movl   $0x2,(%esp)
 80483e4:	e8 c1 ff ff ff       	call   80483aa <foo>
	return 0;
 80483e9:	b8 00 00 00 00       	mov    $0x0,%eax
}


这个函数的调用过程如下:

1) 调用foo(2,3)之前,把main函数中的临时变量(3,2) 压入堆栈,代码为

 80483d5:	c7 44 24 04 03 00 00 	movl   $0x3,0x4(%esp)
 80483dc:	00 
 80483dd:	c7 04 24 02 00 00 00 	movl   $0x2,(%esp)
2) 调用 函数foo,   
call   80483aa <foo>
call的作用为:1)EIP值指向call的下一条指令, 即return 0, 将这条指令压入堆栈;2)将EIP值指向foo函数的入口处。

所以, 再执行完这条指令后, %esp的值会减去4。总的来说这条指令得作用就是将main函数运行的地址压栈,并将运行地址切入到foo函数的开始处。

3)进入foo后的动作如下:

     1、将main函数的stack frame 的基址%ebp压栈,并将ebp的地址设为esp;将esp-8留出存放临时变量的栈空间

     2 访问main函数中的变量(通过ebp+12和ebp+8),并将这两个值压入foo的函数栈帧中

     3 call bar函数。将foo的运行地址压栈

  4) 进入函数bar,将foo的函数栈帧的基址ebp压栈

     其他动作类似3

  5)返回:

      leave: 这个指令是将栈顶平衡, 回到调用之前的状态。

             mov %ebp %esp

             pop %ebp

       这个指令如下操作: 将ebp付给esp, 恢复上个函数stack frame的 栈顶, 并将存在栈中的上个函数的基址出栈,存入%ebp中

      ret;

        这个指令一个出栈操作,类似  pop %eip. 将栈中的下一条执行地址放入eip中。CPU通过EIP来确定下一条执行的命令。并且esp+4。

以上基本就是整个函数调用的cpu的执行过程。

    堆栈的示意图如下。

函数栈帧示意图

  

3  总结: 

    函数调用和返回过程中的这些规则:

  1. 参数压栈传递,并且是从右向左依次压栈。

  2. ebp总是指向当前栈帧的栈底。

  3. 返回值通过eax寄存器传递。

  4.  函数调用前堆栈操作:1)  push ebp  2) mov ebp esp 3) sub esp xxx

  5. 函数调用恢复堆栈操作:1) mov esp ebp 2) pop ebp  3) ret

  以上程序是在linux+gcc的环境下所产生的汇编代码。在64位和其他编译器例如msvc所产生的汇编代码有可能不同。

    参考文章:

 http://learn.akae.cn/media/ch19s01.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值