C语言函数调用底层实现原理是什么(c语言,开发技术)

时间:2024-05-05 09:40:12 作者 : 石家庄SEO 分类 : 开发技术
  • TAG :

    C%E8%AF%AD%E8%A8%80%E5%87%BD%E6%95%B0%E8%B0%83%E7%94%A8%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88


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 -> 主调函数返回地址 -> 主调函数栈帧基址 -> 被调函数保存寄存器(可选) -> 局部变量 -> 局部变量2

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

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

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

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

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

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

函数调用时的具体操作:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

两种压栈方式区别:

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

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

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

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

函数参数传递顺序和方式

栈的维护方式

名字修饰策略

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

上述约定的特点:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

本文:C语言函数调用底层实现原理是什么的详细内容,希望对您有所帮助,信息来源于网络。
上一篇:GO并发编程使用方法是什么下一篇:

4 人围观 / 0 条评论 ↓快速评论↓

(必须)

(必须,保密)

阿狸1 阿狸2 阿狸3 阿狸4 阿狸5 阿狸6 阿狸7 阿狸8 阿狸9 阿狸10 阿狸11 阿狸12 阿狸13 阿狸14 阿狸15 阿狸16 阿狸17 阿狸18