C/C++函数调用方式

本文详细阐述了函数在操作系统层面的调用过程,包括参数传递、返回地址保存、基址压栈以及局部变量处理。通过示例代码和反编译结果,揭示了CALL指令如何执行以及RET和LEAVE指令在函数返回时的角色。

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

函数在OS层面的调用主要涉及4个对象:

对象解释操作
函数调用栈本质上一段连续的内存地址,只是其写内存的方式按照栈的方式,FILO地址按逆序增长,进栈栈顶指针指向地址减小,退栈栈顶指针指向地址变大
esp寄存器用来存储函数调用栈的当前栈栈顶地址,指向栈顶随进栈和出栈其寄存器存储地址值变化
ebp寄存器用来存储当前调用函数的调用栈基地址,指向栈底在当前函数调用完成前保持不变
eip寄存器存储下一条待执行的指令地址OS自动更新

函数调用过程

示例代码如下:

#include<stdio.h>

unsigned int test(int a,int b)
{
    int c,d;
    c = a;
    d = b;
    return c;
}

int main()
{
    unsigned int r;

    r = test(1,2);

    return 0;
}

反编译代码如下:

08048394 <test>:
#include<stdio.h>

unsigned int test(int a,int b)
{
 8048394:    55                       push   %ebp				// 将ebp寄存器存储地址压栈,被
 																// 调用函数基地址存储。
 8048395:    89 e5                    mov    %esp,%ebp			// 将esp存储地址赋值给ebp寄存器
 																// 当前调用的被调用函数基地址存储
 8048397:    83 ec 10                 sub    $0x10,%esp			// esp自减0x10
    int c,d;
    c = a;
 804839a:    8b 45 08                 mov    0x8(%ebp),%eax		// 将[ebp+0x8]地址里的内容赋给eax
 804839d:    89 45 fc                 mov    %eax,-0x4(%ebp)	// 把eax的值放到[ebp-4]这个地址里
    d = b;
 80483a0:    8b 45 0c                 mov    0xc(%ebp),%eax		// 将[ebp+0xc]地址里的内容赋给eax
 80483a3:    89 45 f8                 mov    %eax,-0x8(%ebp)	// 把eax的值放到[ebp-8]这个地址里
    return c;
 80483a6:    8b 45 fc                 mov    -0x4(%ebp),%eax	// 通过eax寄存器保存函数的返回值
}
 80483a9:    c9                       leave
 80483aa:    c3                       ret

080483ab <main>:

int main()
{
 80483ab:    55                       push   %ebp
 80483ac:    89 e5                    mov    %esp,%ebp
 80483ae:    83 ec 18                 sub    $0x18,%esp		// esp的值自减0x18
    unsigned int r;

    r = test(1,2);
 80483b1:    c7 44 24 04 02 00 00     movl   $0x2,0x4(%esp) // 将0x2(第2个参数)放到esp寄存器存储的
 														 	// 地址+0x4中,即第二个参数压栈
 80483b8:    00
 80483b9:    c7 04 24 01 00 00 00     movl   $0x1,(%esp)	// 将0x1(第1个参数)放到esp寄存器存储的
 														 	// 地址中,即第一个参数压栈
 80483c0:    e8 cf ff ff ff           call   8048394 <test> // 调用test函数,此时会将断点(返回地址)
 															// 压栈, 跳转到test函数,开始执行
 80483c5:    89 45 fc                 mov    %eax,-0x4(%ebp)

    return 0;
 80483c8:    b8 00 00 00 00           mov    $0x0,%eax
}
 80483cd:    c9                       leave
 80483ce:    c3                       ret
 80483cf:    90                       nop
  1. 被调用函数传入参数逆序压栈。 main
  2. 调用函数的下一条执行指令进栈,即返回地址进栈。 main
  3. 调用函数的基地址压栈(0x80483ab),即读当前ebp寄存器存储地址,进行压栈。main
  4. 更新ebp寄存器的值,为被调用函数,即当前执行函数,的基地址(0x8048394)。test
  5. 局部变量压栈。test

调用函数返回地址进栈–>返回后的eip,调用函数的ebp进栈–>返回后的ebp,被调用函数的执行完成后的栈顶–>返回后的esp

PUSH 等价于:

subl $4, %esp

movl %ebp (%esp)

POP 等价于:

movl (%esp), %ead

addl $4, %esp

CALL指令的步骤:首先是将返回地址(也就是call指令要执行时EIP的值)压入栈顶,然后是将程序跳转到当前调用的方法的起始地址。执行push和jump指令。

RET指令则是将栈顶的返回地址弹出到EIP,然后按照EIP此时指示的指令地址继续执行程序。

LEAVE指令是将栈指针指向帧指针,然后POP备份的原帧指针到%EBP。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值