广告
返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >C语言函数调用底层实现原理是什么
  • 100
分享到

C语言函数调用底层实现原理是什么

2023-07-05 06:07:59 100人浏览 独家记忆
摘要

本文小编为大家详细介绍“C语言函数调用底层实现原理是什么”,内容详细,步骤清晰,细节处理妥当,希望这篇“C语言函数调用底层实现原理是什么”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。前言C语言程序执行实质上的函数

本文小编为大家详细介绍“C语言函数调用底层实现原理是什么”,内容详细,步骤清晰,细节处理妥当,希望这篇“C语言函数调用底层实现原理是什么”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。

      前言

      C语言程序执行实质上的函数的连续调用

      运行程序时,系统通过程序入口调用main函数,在main函数中又不断调用其它函数。

      程序的每个进程都包括一个调用栈结构(Call Stack)

      调用栈的作用:

      • 传递函数参数

      • 保存返回地址

      • 临时保存寄存器原有值(保存现场)

      寄存器分配

      寄存器指CPU中可以进行高速运算的缓冲区。用于存放程序执行中用到的数据和指令。

      Intel 32位结构寄存器(IA32)包含8个通用寄存器,每个寄存器4个字节(32位)

      通用寄存器按照AT&T语法,寄存器名以**%e**开头。

      若按照Intel语法,寄存器名直接按e开头。

      通用寄存器包括:EAX、EBX、ECX、EDX、ESI、EDI、ESP、EBP

      数据寄存器:EAX、EBX、ECX、EDX

      变址寄存器:ESI、EDI

      指针寄存器:ESP、EBP

      X86架构中,EIP寄存器指向下一条待执行的命令地址

      ESP是栈指针寄存器,指向当前栈帧的栈顶

      EBP是栈帧基址寄存器,指向当前栈帧的基地址

      不同架构的cpu寄存器名前缀不同。

      例如:x86架构的寄存器用字母e作为前缀(extended),表明寄存器大小是32位。

      x86_64架构用字母r作为前缀,表明寄存器大小是64位。

      ABI协议规定了寄存器、堆栈的使用规则以及参数传递规则。用于约束硬件与系统之间的通信协议。编译器必须按照ABI给出的寄存器功能定义,将C程序转为汇编程序。

      寄存器使用约定

      寄存器是唯一能被被所有函数共享的资源。因此,在函数中调用其它函数时,需要考虑到数据的保存与覆盖问题(即防止被调函数直接修改寄存器导致主调函数的数据被覆盖)。

      IA32采用了统一的寄存器使用约定,所有函数必须遵守。

      • EAX、ECX、EDX为主调函数保存寄存器,即在调用被调函数之前,主调函数如果希望保存这三个寄存器的数据,需要将数据保存到堆栈中,然后调用被调函数。

      • EBX、ESI、EDI是被调函数保存寄存器,被调函数如果向使用这三个寄存器,需要先将其中的数据保存到堆栈中,然后操作寄存器,最后将堆栈中的数据还原

      • EBP和ESP指向当前的栈,每个函数对应一个栈帧。被调函在返回前,需将主调函数的栈帧还原。即恢复到调用前的状态。

      栈帧结构

      注意,程序的栈从高地址向低地址增长!

      函数调用由堆栈进行处理,每个函数都单独在堆栈中占用一块连续的区域。这块区域叫做每个函数的栈帧。栈帧是堆栈的逻辑片段

      栈帧中保存 传入的参数 局部变量 和 用于返回上一栈帧的信息。

      栈帧的边界由EBP和ESP决定。EBP指向栈帧的底部(高地址)ESP指向栈顶地址(低地址)。ESP可以看作是EBP的偏移量,始终指向栈帧的顶部。

      EBP为帧基指针,ESP为栈顶指针。

      函数调用栈演示如下:

      参数2
      参数1
      主调函数返回地址(EIP)
      主调函数栈帧基址(EBP)
      被调函数保存寄存器(可选)
      局部变量1
      局部变量2

      函数被调用时,压栈的顺序:

      参数2 -> 参数1 -> 主调函数返回地址 -> 主调函数栈帧基址 -> 被调函数保存寄存器(可选) -> 局部变量 -> 局部变量2

      注意,参数是从右向左依次入栈。

      参数压栈完成后,紧接着被压入的是EIP指针所指向的地址,也就是主调函数下一个要执行的命令的地址。(用于被调函数执行完后继续执行程序)

      然后,将主调函数EBP栈帧基地址压入栈帧,用于还原现场。并把ESP赋值给EBP,使EBP成为被调函数的栈帧基地址

      继续,改变SP的值,给被调函数局部变量预留空间

      这时候,EBP指向被调函数的栈底,向上是主调函数返回地址,向下是局部变量。该地址还保存主调函数的栈帧基址

      函数调用结束后,EBP赋值给ESP,使ESP指向被调函数栈底,释放被调函数局部变量。再将主调函数栈帧基地址弹出给EBP,并弹出返回地址到EIP

      堆栈操作

      函数调用流程

      函数调用时的具体操作:

      1. 主调函数按照约定,将参数压入栈中。(x86将参数压入栈帧,x86_64具有16个通用寄存器,前六个参数通常由寄存器保存,其余参数压入栈中。)

      2. 主调函数将控制权转给被调函数,返回地址(EIP)保存在栈中(在call指令中执行)。

      3. 被调函数设置栈帧基址,即用ESP给EBP赋值。

      4. 若有必要,保存被调函数希望保持的寄存器的数据。

      5. 被调函数修改栈顶指针,为局部变量预留空间。并向低地址方向开始存放局部变量和临时变量。

      6. 被调函数执行任务,若被调函数返回值,一般存放在EAX中。

      7. 栈顶指针指向EBP,释放局部变量空间。

      8. 恢复4中保存的主调函数寄存器中的数据。并恢复3中的栈帧基址。

      9. 被调函数控制权交还给主调函数(ret指令),也可能清除参数。

      10. 主调函数得到控制器,可能将栈上的参数清除。

      函数调用常用命令

      压栈(push):栈顶指针减小4个字节,以字节为单位将数据压入栈中。(不足补0)

      出栈(pop):栈顶指针数据被取回,ESP增大4个字节

      调用(call):将EIP(call的下一条指令地址)压入栈帧,然后EIP指向被调函数代码开始处。

      离开(leave):恢复主调函数栈帧,等价于 mov ebp esp 、pop ebp

      返回(ret):与call对应,从栈顶弹出返回地址给EIP。继续执行程序。

      C调用约定典型的函数序函数跋如下:


      指令序列含义
      函数序(prologue)push %ebp将主调函数栈基指针ebp压栈,即保存旧栈帧基址以便函数返回时恢复旧栈帧。
      mov %esp %ebp将主调函数栈顶指针赋值给ebp,此时,ebp执行被调函数栈帧底部。
      sub %esp将栈顶指针下移,为局部变量开辟空间,n通常为16的倍数,以便于字节对齐进行编译优化
      push可选,如有必要,被调函数保存某些寄存器的值(ebx,edi,esi)
      函数跋(epilogue)pop®可选,如有必要,被调函数恢复某些寄存器的值(ebx,edi,esi)
      mov %ebp %esp*恢复主调函数栈顶指针esp,将其指向被调函数栈底。局部变量空间被释放,但数据未清除。
      pop %ebp恢复主调函数栈帧基地址,此时,esp指向返回地址存放处。
      ret从栈中弹出返回地址到eip,继续执行主调函数。再由主调函数恢复栈。
      *:这两条指令序列也可以由leave实现,具体方式由编译器决定。

      C语言函数调用的两种压栈方式:

      压栈方式一压栈方式二
      push 4push 3push 2push 1call CdeclDemoadd $16, %ebpsub $16, %espmov $4, 12(%esp)mov $3, 8(%esp)mov $2, 4(%esp)mov $1, (%esp)call CdeclDemo

      两种压栈方式区别:

      方式一是传统方式,一个参数一个参数的压栈,然后调用,最后释放栈

      方式二是预先开辟空间,然后将参数复制到空间,最后没有回收空间

      函数调用约定

      创建栈帧最重要的步骤是参数的传递。函数选择特定调用约定,以特定方式进行参数传递。调用约定还规定在函数调用结束后,由主调函数还是被调函数对栈进行清理。

      函数调用约定包括以下方面:

      • 函数参数传递顺序和方式

      • 栈的维护方式

      • 名字修饰策略

      常见调用约定

      cdecl调用约定

      别名 C调用约定,C/C++编译器默认调用约定

      所有非C++成员函数,和未使用stdcall、fastcall声明的函数默认都是cdecl调用。

      参数按照从右向左的顺序入栈,主调函数负责清空栈返回值保存在EAX中。

      cdecl调用支持可变参数函数,对于C函数,名字修饰是在函数名前加 _

      对于c++,除非使用**extern"C"**修饰,否则有不同的名字修饰方法。

      stdcall调用约定(微软命名)

      Pascal程序缺省调用方式,Winapi也多采用该调用约定。

      参数从右向左入栈,被调函数负责清空栈返回值保存在EAX

      stdcall仅适用于参数个数固定的函数,因为被调函数无法知道栈上参数个数。

      C函数中,stdcall的名字修饰是在名字前加_,在名字后加@和参数大小。

      fastcall调用约定

      stdcall的变形,通常使用ECX、EDX寄存器传递前两个DWORD(四字节双字)类型或更少的字节的函数参数,其余从右向左入栈。

      被调函数负责清空栈中参数。返回值保存在EAX中。

      函数名两边使用@修饰,并在后面用十进制表示参数列表大小(字节)

      thiscall调用约定

      C++类的非静态成员函数必须接收一个主调对象的指针(this指针),并频繁的使用该指针。编译器默认使用thiscall调用约定提高调用效率。

      参数按照从右向左的顺序入栈。

      若参数数目固定,this指针通过ECX传递,被调函数负责清理堆栈。

      若参数数目不固定,this指针在所有参数入栈后再入栈主调函数清理堆栈。

      thiscall不是C++关键字,不能用于修饰函数,只能由编译器使用。

      naked call调用约定

      naked call调用,编译器不产生保存和恢复寄存器的代码。也不能使用return语句。

      只能使用内嵌的汇编返回结果。用于某些特殊场合,如非C/C++上下文中的函数,程序员需自行编写初始化和清栈的内嵌汇编指令。

      pascal调用约定

      Pascal语言调用约定,参数从右向左入栈。只支持固定数量参数。

      被调函数清理堆栈,函数名称无修饰且全部大写。

      上述约定的特点:

      调用方式stdcall(Win32)cdeclfastcallthiscall(C++)naked call
      参数压栈顺序从右至左从右至左自定义,Arg1在ecx,Arg2在edx从右至左,this指针在ecx自定义
      参数位置栈 + 寄存器栈,寄存器ecx自定义
      负责清栈函数被调函数主调函数被调函数被调函数自定义
      支持可变参数自定义
      函数名字格式_name@number_name@name@number
      自定义
      参数表开始特征“@@YG”“@@YA”“@@YI”
      自定义
      注:C++因支撑函数重载、命名空间和成员函数等语法特征,采用更为复杂的名字修饰策略。C++函数修饰名以"?“开始,后面紧跟函数名、参数表开始标识和按照类型代号拼出的返回值参数表。例如,函数int Function(char *var1,unsigned long)对应的stdcall修饰名为”?Function@@YGHPADK@Z"。




      windows下可直接在函数声明前添加关键字__stdcall、__cdecl或__fastcall等标识确定函数的调用方式,如int __stdcall func()。

      linux下可借用函数attribute 机制,如int attribute((stdcall)) func()。

      被调函数CalleeFunc分别声明为cdecl、stdcall和fastcall约定时,汇编代码比较:


      cdeclstdcallfastcall
      主调函数职责sub $0xc, %espmov $0x33, 0x8(%esp)mov $0x22, 0x4(%esp)mov $0x11,(%esp)call 8048354sub $0xc, %espmov $0x33, 0x8(%esp)mov $0x22, 0x4(%esp)mov $0x11,(%esp)call 8048354 sub $0xc, %espsub $0x4,%esp movl $0x33,(%esp) mov $0x22,%edx mov $0x11,%ecx call 8048354 sub $0x4,%esp
      被调函数职责push %ebpmov %ebp %espmov 0xc(%ebp), %eaxadd 0x8(%ebp), %eaxadd 0x10(%ebp), %eaxpop %ebpretpush %ebpmov %ebp %espmov 0xc(%ebp), %eaxadd 0x8(%ebp), %eaxadd 0x10(%ebp), %eaxpop %ebpret $0xc 执行ret指令并清理参数占用的堆栈(栈顶指针上移参数个数*4=12个字节,以释放压栈的参数)push %ebp mov %esp,%ebp sub $0x8,%esp mov %ecx,0xfffffffc(%ebp) mov %edx,0xfffffff8(%ebp) mov 0xfffffff8(%ebp),%eax add 0xfffffffc(%ebp),%eax add 0x8(%ebp),%eax leave ret $0x4 //ret <压栈参数字节数>。若参数不超过两个,则ret指令不带立即数,因为无参数被压栈

      调用约定影响

      不同编译器产生栈帧的方式不尽相同,主调函数不一定能完成清理堆栈的工作,而被调函数一定可以。

      同时,为了保证不同平台堆栈正常,一般使用stdcall调用。(通常用于A语言调用B语言函数

      此外,主调函数和被调函数采用相同调用约定,但分别使用C和C++时,会出现链接错误。

      这是因为:两种语言函数名称修饰符不一样。解决方法是使用**extern “C”**修饰被调函数。

      同时应该考虑,被调函数也有可能是C++编译的。通常这样声明头文件:

      #ifdef _cplusplus     extern "C" {#endif     type Func(type para);#ifdef _cplusplus     }#endif

      x86函数传递参数方法

      x86处理器的ABI规范中规定,所有参数从右向左压入栈中

      整型和指针参数传递

      整型参数指针参数传递方式相同,在32位的x86处理器上整型与指针大小相同(四个字节)。

      下表给出这两种类型在栈帧中位置关系:

      调用语句参数栈帧地址
      tail(1, 2, 3, (void *)0);18(%ebp)
      212(%ebp)
      316(%ebp)
      (void *)020(%ebp)

      浮点参数传递

      浮点参数的传递与整型类似,区别在于参数大小。

      x86处理器浮点类型占8个字节,因此在栈中也需要占8个字节。

      下表给出浮点参数在栈中位置关系:

      调用语句参数栈帧地址
      tail(1.414, 2, 3.998e10);Word 0: 1.4148(%ebp)
      word 1: 1.41412(%ebp)
      216(%ebp)
      word 0: 3.998e1020(%ebp)
      word 1: 3.998e1024(%ebp)

      结构体和联合体参数传递

      结构体和联合体的传递与整型、浮点型类似,只是占用大小不同。

      x86处理器栈宽是4字节,故结构体在栈上大小是4的倍数

      编译器会对结构体进行适当的填充使得结构体4字节对齐

      对于其它处理器,参数传递并不全部通过栈进行。结构体可能通过指针传递

      x86函数返回值传递方法

      函数返回值可通过寄存器传递:

      1. 若返回值不超过4字节(int、指针),通常保存在EAX中。

      2. 若返回值大于4字节但不超过8字节(long long),通常保存在EAX+EDX,EDX保存高4字节,EAX保存低4字节。

      3. 若返回值为浮点类型(float double),则通过专用的协处理器浮点数寄存器栈的栈顶返回。

      4. 若返回值为结构体或联合体,主调函数额外传递一个参数,该参数是一个保存返回值的空间地址。

      注意:函数如何保存结构体或联合体返回值取决于具体实现。

      读到这里,这篇“C语言函数调用底层实现原理是什么”文章已经介绍完毕,想要掌握这篇文章的知识点还需要大家自己动手实践使用过才能领会,如果想了解更多相关内容的文章,欢迎关注编程网其他教程频道。

      --结束END--

      本文标题: C语言函数调用底层实现原理是什么

      本文链接: https://www.lsjlt.com/news/349947.html(转载时请注明来源链接)

      有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

      本篇文章演示代码以及资料文档资料下载

      下载Word文档到电脑,方便收藏和打印~

      下载Word文档
      猜你喜欢
      • C语言函数调用底层实现原理是什么
        本文小编为大家详细介绍“C语言函数调用底层实现原理是什么”,内容详细,步骤清晰,细节处理妥当,希望这篇“C语言函数调用底层实现原理是什么”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。前言C语言程序执行实质上的函数...
        99+
        2023-07-05
      • C语言函数调用底层实现原理分析
        目录前言寄存器分配寄存器使用约定栈帧结构堆栈操作函数调用流程函数调用常用命令函数调用约定常见调用约定cdecl调用约定stdcall调用约定(微软命名)fastcall调用约定thi...
        99+
        2023-02-24
        C语言函数调用 C语言函数 函数底层实现原理
      • C语言函数的调用原理是什么
        C语言函数的调用原理是通过栈来实现的。当一个函数被调用时,系统会为该函数分配一块内存空间,这块空间被称为栈帧。栈帧包含了函数的参数、...
        99+
        2023-09-04
        C语言
      • js数组底层实现原理是什么
        JavaScript数组底层实现原理可以分为两种情况:稠密数组和稀疏数组。1. 稠密数组(Dense Array):稠密数组是指数组...
        99+
        2023-09-12
        js
      • php数组底层实现原理是什么
        这篇文章主要介绍php数组底层实现原理是什么,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!php 数组底层实现原理1、底层实现是通过散列表(hash table) + 双向链表(解决...
        99+
        2022-10-19
      • go语言中slice,map,channl底层原理是什么
        今天小编给大家分享一下go语言中slice,map,channl底层原理是什么的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。...
        99+
        2023-06-30
      • chatgpt底层实现的原理是什么
        chatgpt底层实现的原理是通过人工的标注方式来训练出一种强化学习的冷启动模型和reward反馈模型,然后再通过强化学习的模式来学...
        99+
        2023-02-09
        chatgpt
      • Synchronized的底层实现原理是什么
        Synchronized的底层实现原理是什么,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。(1)给静态方法加锁public&n...
        99+
        2022-10-19
      • HashMap的底层实现原理是什么
        这篇文章给大家介绍HashMap的底层实现原理是什么,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。1.HashMap的常用方法//  Hashmap存值:----------------------...
        99+
        2023-06-06
      • golang map底层实现原理是什么
        Golang中的map是基于散列表(hash table)实现的。散列表是一种用于存储键值对的数据结构,它通过将键映射到数组的索引来...
        99+
        2023-10-21
        golang
      • C语言函数调用的作用是什么
        C语言函数调用的作用是将程序的执行过程分割成更小的可重用模块,提高代码的可读性和可维护性。函数调用可以将一组操作封装到一个函数中,并...
        99+
        2023-09-13
        C语言
      • C++引用怎么使用及底层原理是什么
        本篇内容介绍了“C++引用怎么使用及底层原理是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!引用引用不是定义一个新变量,而是给已存在的变...
        99+
        2023-06-30
      • MySQL索引的底层实现原理是什么
        这篇文章主要介绍MySQL索引的底层实现原理是什么,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!MySQL索引的底层实现原理1、Hash索引2、BTree索引和B+Tree索引3、全...
        99+
        2022-10-18
      • spring注解的底层实现原理是什么
        Spring注解的底层实现原理主要依赖于Java的反射机制。在Spring中,通过使用注解来标识类、方法或字段,从而告诉Spring...
        99+
        2023-10-09
        spring
      • c语言子函数调用的方法是什么
        在C语言中,可以使用以下两种方法调用子函数:1. 值传递(Call by Value):将实参的值复制给形参,子函数中对形参的修改不...
        99+
        2023-09-13
        c语言
      • c语言调用子函数的方法是什么
        在C语言中,调用子函数的方法可以通过以下步骤实现:1. 首先,在主函数或其他需要调用子函数的函数中,声明子函数的原型。原型包括子函数...
        99+
        2023-10-08
        c语言
      • mysql的索引底层之实现原理是什么
        这篇文章主要介绍了mysql的索引底层之实现原理是什么,具有一定借鉴价值,需要的朋友可以参考下。希望大家阅读完这篇文章后大有收获。下面让小编带着大家一起了解一下。MySQL索引背后的数据结构及算法原理一、定...
        99+
        2022-10-18
      • MySQL中索引的底层实现原理是什么
        本篇文章为大家展示了MySQL中索引的底层实现原理是什么,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。  MySQL索引底层实现原理  MySQL官方对索引的定义为...
        99+
        2022-10-18
      • c语言函数的递归调用方法是什么
        C语言函数的递归调用方法是指在函数内部调用自身的过程。递归调用函数可以让程序重复执行相同的操作,直到满足某个条件才停止。递归调用函数...
        99+
        2023-09-04
        c语言
      • c语言解释器的实现原理是什么
        C语言解释器的实现原理是将C语言源代码转换为可执行的机器代码并执行。下面是C语言解释器的基本实现原理:1. 词法分析:将源代码分解为...
        99+
        2023-08-08
        c语言
      软考高级职称资格查询
      编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
      • 官方手机版

      • 微信公众号

      • 商务合作