C语言函数调用底层实现原理分析

 2024-02-29 01:08:48  阅读 0

目录 函数调用约定 调用约定影响x86函数参数传递方式 x86函数返回值传递方式总结

前言

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

运行程序时,系统通过程序入口调用主函数,并在主函数中不断调用其他函数。

程序的每个进程都包含一个调用堆栈结构(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()为前缀,表示寄存器大小为32位。

该架构以字母r为前缀,表示寄存器大小为64位。

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

寄存器使用约定

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

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

栈帧结构

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

函数调用由堆栈处理,每个函数在堆栈上占用一个单独且连续的区域。 该区域称为每个函数的堆栈帧。 堆栈帧是堆栈的逻辑段。

堆栈帧存储传递的参数局部变量以及用于返回上一个堆栈帧的信息。

栈帧的边界由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。

堆栈操作函数调用流程

调用该函数时的具体操作:

调用函数按照约定将参数压入堆栈。 (x86将参数压入栈帧,有16个通用寄存器。前6个参数通常保存在寄存器中,其余参数压入堆栈。)调用函数将控制权转移给被调用函数,返回地址(EIP)保存在堆栈上(在call指令中执行)。 被调用函数设置栈帧基地址,即使用ESP给EBP赋值。 如有必要,将数据保存在被调用函数希望保留的寄存器中。 被调用函数修改栈顶指针,为局部变量保留空间。 并开始向低地址方向存储局部变量和临时变量。 被调用的函数执行任务。 如果被调用的函数有返回值,一般会存储在EAX中。 栈顶指针指向EBP,释放局部变量空间。 恢复4中保存的调用函数寄存器中的数据。并恢复3中的栈帧基地址。被调用函数的控制权返回给调用函数(ret指令),参数也可能被清除。 调用函数获取控制器,并可能清除堆栈上的参数。函数调用常用命令

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

Pop:取出栈顶的指针数据,ESP增加4个字节。

c语言中调用是什么意思_c语言中调用语句_c语言中的调用

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

Leave:恢复调用函数栈帧,相当于mov ebp esp,pop ebp

返回(ret):对应call,将返回地址从栈顶弹出到EIP。 继续程序。

C调用约定的典型函数顺序和函数尾声如下:

指令顺序含义

函数顺序()

推%ebp

将调用函数的栈基指针ebp压入栈,即保存旧栈帧的基地址,以便函数返回时可以恢复旧栈帧。

移动%esp%ebp

将调用函数栈顶指针赋给ebp。 此时,ebp执行被调用函数的栈帧底部。

子%esp

将栈顶指针向下移动,为局部变量腾出空间。 n 通常是 16 的倍数,以便于编译优化的字节对齐。

可选,如有必要,被调用函数保存某些寄存器的值(ebx、edi、esi)

函数post()

流行®

可选,如有必要,被调用的函数恢复某些寄存器的值(ebx、edi、esi)

移动%ebp%esp*

恢复调用函数栈顶指针esp,使其指向被调用函数栈底。 局部变量空间被释放,但数据并没有被清除。

弹出%ebp

恢复调用函数的堆栈帧基地址。 此时esp指向返回地址存储位置。

雷特

将返回地址从堆栈弹出到eip并继续执行调用函数。 然后调用函数恢复堆栈。

*:这两个指令序列也可以通过leave来实现,具体方法由编译器决定。

C语言推送函数的两种方式:

推法一 推法二

推送 4推送 3推送 2推送 1调用 $16, %ebp

子 $16, % $4, 12(%esp)mov $3, 8(%esp)mov $2, 4(%esp)mov $1, (%esp)call

两种堆叠方式的区别:

第一种方法是传统方法,将一个参数压入堆栈,然后调用,最后释放堆栈。

第二种方法是提前开辟空间,然后将参数复制到空间中,最后就没有回收的空间了。

函数调用约定

创建堆栈帧最重要的一步是参数的传递。 函数选择特定的调用约定并以特定的方式传递参数。 调用约定还规定了调用函数或被调用函数在函数调用结束后是否清理堆栈。

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

常用调用约定调用约定

Alias C调用约定,C/C++编译器的默认调用约定。

默认情况下,所有非 C++ 成员函数以及未使用和声明的函数均由 cdecl 调用。

参数按从右到左的顺序入栈,调用函数负责清栈,返回值存放在EAX中。

cdecl 调用支持可变参数函数。 对于C函数,名称修改是在函数名前添加_。

对于 C++,有不同的名称修饰方法,除非您使用“C”修饰。

调用约定(微软命名)

程序默认的调用方式也大多采用这种调用约定。

参数从右向左入栈,被调用函数负责清栈,返回值存放在EAX中。

它只适用于参数数量固定的函数,因为被调用的函数无法知道堆栈上参数的数量。

在C函数中,最好的名称修饰是在名称前添加_,在名称后添加@和参数大小。

调用约定

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

被调用函数负责清除堆栈中的参数。 返回值保存在EAX中。

函数名两边用@修饰,参数列表大小(字节)在末尾以十进制表示。

调用约定

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

c语言中调用是什么意思_c语言中的调用_c语言中调用语句

参数按从右到左的顺序压入堆栈。

如果参数个数固定,则通过ECX传递this指针,被调用函数负责清栈。

如果参数个数不固定,所有参数入栈后,this指针入栈,调用函数清空栈。

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

裸调用调用约定

当使用裸调用进行调用时,编译器不会生成保存和恢复寄存器的代码。 也不能使用语句。

只能使用嵌入式程序集返回结果。 对于一些特殊场合,例如非C/C++上下文中的函数,程序员需要编写内联汇编指令来进行初始化和堆栈清除。

调用约定

语言调用约定,参数从右到左压入堆栈。 仅支持固定数量的参数。

被调用的函数清除堆栈,并且函数名称未修改且全部大写。

上述协议的特点:

调用方法 (Win32) l (C++) 裸调用

参数推送顺序

从右到左

从右到左

自定义,Arg1在ecx中,Arg2在edx中

从右到左,这个指针在ecx中

定制

参数位置

堆栈+寄存器

堆栈、寄存器ecx

定制

负责清除堆栈函数

被调用函数

主功能

被调用函数

被调用函数

定制

支持可变参数

是的

定制

函数名称格式

_姓名@

_姓名

@姓名@

定制

参数表启动特性

“@@YG”

“@@亚”

“@@义”

定制

注意:C++ 由于支持函数重载、命名空间、成员函数等语法特性,因此采用了更为复杂的名称修改策略。 C++函数修饰名以“?”开头,后面是根据类型代码拼出的函数名、参数列表的开头以及返回值参数列表。 例如,函数int(char *var1, long)对应的修饰名是“?@@@Z”。

可以直接在函数声明前添加关键字或其他标识符来确定函数的调用方法,例如int func()。

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

当被调用函数分别声明为cdecl和时,比较汇编代码:

调用函数职责

子 $0xc, % $0x33, 0x8(%esp)mov $0x22, 0x4(%esp)mov $0x11,(%esp)call

子 $0xc, % $0x33, 0x8(%esp)mov $0x22, 0x4(%esp)mov $0x11,(%esp)调用子 $0xc, %esp

sub $0x4,%esp movl $0x33,(%esp) mov $0x22,%edx mov $0x11,%ecx 调用 sub $0x4,%esp

被调用函数职责

推 % %ebp % 0xc(%ebp), % 0x8(%ebp), % 0x10(%ebp), % %

push % %ebp % 0xc(%ebp), % 0x8(%ebp), % 0x10(%ebp), % % $0xc 执行ret指令,清空参数占用的栈(栈顶指针上移参数个数*4=12字节释放推送的参数)

推 %ebp mov %esp,%ebp sub $0x8,%esp mov %ecx,(%ebp) mov %edx,(%ebp) mov (%ebp),%eax 添加 (%ebp),%eax 添加 0x8( %ebp),%eax left ret $0x4 //ret。如果参数不超过两个,ret指令不会取立即数,因为没有参数入栈。

通话约定影响

不同的编译器以不同的方式生成堆栈帧。 调用函数可能无法完成清理堆栈的工作,但被调用函数肯定可以。

同时,为了保证不同平台上的栈都正常,一般都会采用调用的方式。 (通常用于A语言调用B语言函数)

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

这是因为:两种语言中的函数名修饰符不同。 解决办法是用“C”来修改被调用的函数。

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

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

x86函数传参方法

x86处理器的ABI规范规定所有参数从右到左压入堆栈。

整数和指针参数传递

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

下表展示了这两种类型在栈帧中的位置关系:

调用语句参数栈帧地址

尾部(1,2,3,(无效*)0);

8(%ebp)

12(%ebp)

16(%ebp)

(无效*)0

20(%ebp)

浮点参数传递

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

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

下表为浮点参数在栈中的位置关系:

调用语句参数栈帧地址

尾部(1.414,2,3.);

字 0:1.414

8(%ebp)

字 1: 1.414

12(%ebp)

16(%ebp)

字 0: 3。

20(%ebp)

字1:3。

24(%ebp)

结构和联合参数传递

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

x86处理器堆栈宽度为4字节,因此堆栈上结构的大小是4的倍数。

编译器将适当地填充该结构,以便该结构是 4 字节对齐的。

对于其他处理器,参数传递并非全部通过堆栈完成。 结构可以通过指针传递。

x86函数返回值传递方法

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

如果返回值不超过4个字节(int、指针),通常存储在EAX中。 如果返回值大于4个字节但不超过8个字节(long long),通常存储在EAX+EDX中。 EDX 存储高 4 个字节,EAX 存储低 4 个字节。 如果返回值是浮点类型(float),则从专用协处理器浮点寄存器堆栈的顶部返回。 如果返回值是结构体或联合体,则调用函数会传递一个附加参数,该参数是保存返回值的空间地址。

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

总结

以上是我的个人经验,希望能给大家一个参考,也希望大家支持编程网。

如本站内容信息有侵犯到您的权益请联系我们删除,谢谢!!


Copyright © 2020 All Rights Reserved 京ICP5741267-1号 统计代码