• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2025-10-30 19:57 Aet 隐藏边栏 |   抢沙发  1 
文章评分 1 次,平均分 5.0

概述

  1. C++中的函数调用,本质上就是通过函数地址定位到进程地址空间的代码区中对应的指令序列,然后跳转执行

编译阶段

编译时 - 生成符号和调用指令

  1. 函数声明与符号生成‌
    1. 编译器看到函数 func 的声明(或定义)时,会将其名称作为一个‌符号(Symbol)‌ 记录在目标文件(.obj.o)的符号表中
    2. 同时,编译器将函数 funcC++ 源代码翻译成对应的机器指令序列,并将这些指令放入目标文件的代码段中
  2. 函数调用翻译‌
    1. 当编译器遇到 func(); 这行调用代码时,它并不知道 func 函数最终在内存中的绝对地址
    2. 因为该函数可能定义在另一个编译单元(另一个 .cpp 文件)中
    3. 因此,编译器会生成一条‌调用指令‌(在 x86 上类似 call ),但此时目标地址是‌未知的
    4. 编译器会创建一个‌重定位条目(Relocation Entry
    5. 等到链接时,找到了 func 这个符号的地址,然后会把这个调用指令中的空缺地址给填上

链接阶段

链接时 - 解析符号地址

  1. 符号解析
    1. 链接器收集所有目标文件中的符号。它建立一个全局的符号表
    2. 链接器开始“解决”所有未定义的符号引用
      它遍历所有目标文件,寻找 func 这个符号的定义
  2. 地址分配与重定位‌
    1. 链接器会为整个程序分配虚拟内存地址
      它决定代码段、数据段等各自在进程地址空间中的起始位置
    2. 假设链接器决定程序的代码段从虚拟地址 0x400000 开始,并且计算出 func 函数位于代码段内偏移 0x1000 的位置
      那么,func 的‌绝对虚拟地址‌就确定为 0x401000
    3. 链接器会找到之前编译器留下的所有关于 func 的重定位条目,并将调用指令中的那个“空洞”填上计算好的地址 0x401000(或者是基于该地址的相对偏移量)

运行阶段

运行时 - 加载执行与跳转

  1. 创建进程与地址空间
    1. 当你双击或在命令行启动程序时,操作系统会为其创建一个新的‌进程
    2. 每个进程都有自己独立的、完整的虚拟地址空间(在32位系统上是4GB64位系统则巨大无比)
    3. 操作系统的加载器(Loader)读取可执行文件,将代码段、数据段等内容映射到进程虚拟地址空间的相应区域
    4. 关键点在于:
      链接时确定的地址(如 0x401000)就是这个函数在进程虚拟地址空间中的地址
  2. 指令执行与函数调用
    1. 见函数执行流程

函数执行流程

参数入栈与返回地址

  1. 当程序运行到函数调用语句时(例如 int result = add(x, y);),以下步骤在极短时间内发生:
  2. 参数入栈(从右向左)
    1. 调用者(Caller)会按照从右向左的顺序,将函数参数压入系统栈(Stack
    2. 例如,对于 add(x, y),会先将 y的值压栈,再将 x的值压栈
    3. 如果参数是复杂对象或大小超过寄存器容量,其内容可能会被拷贝到栈上
  3. 压入返回地址
    1. 接着,CPU执行 call指令
      这个指令会做两件关键的事:
    2. 将当前指令指针(EIP/RIP)的下一条指令地址(即返回地址)压入栈顶
      这确保了函数执行完毕后能知道该回到哪里继续执行
    3. 跳转(Jump)到 add函数在代码区的起始内存地址

新函数 —栈帧的建立

  1. 跳转到目标函数后,首要任务是为这个函数的执行创建一个独立的上下文环境,即栈帧(Stack Frame
    1. 保存调用者的栈帧基址:push ebp
      将当前栈底指针寄存器(EBP/RBP)的值压栈保存
      这个值指向的是调用函数栈帧的底部
    2. 设置新栈帧的基址:mov ebp, esp
      将当前栈顶指针(ESP/RSP)的值赋给EBP
      此时,EBP就指向了新栈帧的底部
    3. 为局部变量分配空间:sub esp, XXh
      将栈顶指针ESP向上(低地址方向)移动一定字节(XXh),从而在栈上为当前函数的局部变量预留出空间
  2. 栈帧的建立使得函数可以通过固定的偏移量(相对于EBP)来访问参数和局部变量
    1. 例如,[ebp+8]通常是第一个参数,[ebp-4]可能是第一个局部变量

执行函数体

  1. 栈帧建立好后,便开始执行函数体内的代码
    1. 此时,对局部变量的操作都是在刚刚分配的栈空间上进行
  2. 如果函数有返回值,对于较小的返回值(如基本数据类型),通常会通过 EAX/RAX 寄存器来传递

函数返回与栈帧销毁

  1. 函数执行到 return语句或结束括号 }时,需要清理自己的栈帧,并返回调用者:
    1. 返回值存入寄存器
      如果有返回值,会将其放入EAX寄存器
    2. 拆除栈帧
      mov esp, ebp:将栈顶ESP移回栈底EBP处,这步操作释放了当前函数的所有局部变量空间
      pop ebp:从栈中弹出之前保存的调用者的EBP值,将其恢复EBP寄存器
      这样,EBP就指回了调用函数栈帧的底部
    3. 返回到调用者
      执行 ret指令。该指令会从栈顶弹出之前保存的返回地址,并跳转到该地址继续执行
      CPU的控制权就此交还给调用函数
    4. 清理参数空间
      返回后,调用者负责清理之前压入栈的函数参数
      这通常通过直接调整栈指针完成(例如 add esp, 8,假设两个4字节参数)

关键

三种参数传递方式

  1. 传值调用(Call by Value
    1. 将实参的值副本传给形参
    2. 函数内修改形参不影响实参
  2. 传址调用(Call by Pointer/Address
    1. 将实参的地址传给指针类型的形参
    2. 函数内通过该地址间接操作实参,从而影响实参
  3. 引用调用(Call by Reference
    1. C++特有方式,形参是实参的别名
    2. 语法上像传值,但底层实现通常类似于传址,能直接修改实参

函数指针:间接调用的桥梁

  1. 函数指针(Function Pointer)是理解函数地址概念的绝佳例子
    1. 它是一个指针变量,但其值不是数据地址,而是函数的入口地址
  2. 声明与赋值
    1. 例如,int (*funcPtr)(int, int);声明了一个函数指针,它可以指向任何接受两个int参数并返回int的函数
    2. 通过 funcPtr = add;可以将 add函数的地址赋给它
  3. 间接调用
    1. 通过 funcPtr(1, 2);(*funcPtr)(1, 2);即可调用它所指向的函数
    2. 这个过程和直接调用 add(1, 2)在底层机制上完全一致:
      查找指针保存的地址 -> 压参入栈 -> 跳转执行

声明:本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享