“Calling Conventions”的版本间差异
(创建页面,内容为“在C中调用外部函数,并从其他语言调用C函数,是OS编程中的常见问题,尤其是在其他语言是汇编的情况下。(译者注:本页其实讨论了汇编和C语言的互操作问题,但是对其它不同语言间的互操作问题也有一些启发,) 本页将主要关注后一种情况,但也会考虑其他语言。 这里描述的一些内容是由x86架构强加的,有些是GNUGCC工具链所特有的。 有些是可…”) |
|||
第113行: | 第113行: | ||
== 另见 == | == 另见 == | ||
==外部链接=== | ===外部链接=== | ||
*[http://www.delorie.com/djgpp/doc/ug/asm/calling.html DJGPP FAQ: GCC calling conventions] | *[http://www.delorie.com/djgpp/doc/ug/asm/calling.html DJGPP FAQ: GCC calling conventions] | ||
*[http://gul.ime.usp.br/Docs/docs/howto/other-formats/html/HOWTO-INDEX-html/Assembly-HOWTO-5.html Linux Assembly Language HOWTO chapter 5] | *[http://gul.ime.usp.br/Docs/docs/howto/other-formats/html/HOWTO-INDEX-html/Assembly-HOWTO-5.html Linux Assembly Language HOWTO chapter 5] |
2022年3月30日 (三) 02:13的最新版本
在C中调用外部函数,并从其他语言调用C函数,是OS编程中的常见问题,尤其是在其他语言是汇编的情况下。(译者注:本页其实讨论了汇编和C语言的互操作问题,但是对其它不同语言间的互操作问题也有一些启发,) 本页将主要关注后一种情况,但也会考虑其他语言。
这里描述的一些内容是由x86架构强加的,有些是GNUGCC工具链所特有的。 有些是可配置的,你可以制定自己的GCC目标来支持不同的调用约定。 目前,本页面没有区分对待这些问题。
基础知识
通常,可以将遵循C调用约定并在C标头中适当声明 (见下文) 的函数称为普通C函数。 遵循调用规则的大部分负担落在汇编程序上。
备忘单
下面是常见调用约定的快速概述。 请注意,调用约定通常比这里表示的更复杂 (例如,大型结构如何返回? 一个包含两个寄存器的结构怎么办? 那如果是多个变量列表呢?)。 如果要确定,请查找各自的规格说明。 编写测试函数并使用gcc-S查看编译器如何生成代码可能会很有用,这可能会提示如何解释调用约定规范。
平台 | 返回值 | 参数寄存器 | 其他参数 | 栈对齐 | Scratch寄存器 | Preserved寄存器 | Call List |
---|---|---|---|---|---|---|---|
System V i386 | eax, edx | none | stack (right to left)1 | eax, ecx, edx | ebx, esi, edi, ebp, esp | ebp | |
System V X86_642 | rax, rdx | rdi, rsi, rdx, rcx, r8, r9 | stack (right to left)1 | 16-byte at call3 | rax, rdi, rsi, rdx, rcx, r8, r9, r10, r11 | rbx, rsp, rbp, r12, r13, r14, r15 | rbp |
Microsoft x64 | rax | rcx, rdx, r8, r9 | stack (right to left)1 | 16-byte at call3 | rax, rcx, rdx, r8, r9, r10, r11 | rbx, rdi, rsi, rsp, rbp, r12, r13, r14, r15 | rbp |
ARM | r0, r1 | r0, r1, r2, r3 | stack | 8 byte4 | r0, r1, r2, r3, r12 | r4, r5, r6, r7, r8, r9, r10, r11, r13, r14 |
注1: 被调用的函数允许修改堆栈上的参数,并且调用者不得假定堆栈参数被保留。 调用方应该清理堆栈。
注2: 堆栈下方有一个128字节区域,称为 “红色区域(red zone)”,leaf函数可以在不增加 %rsp的情况下使用。 这需要内核根据用户空间中的信号将%rsp额外增加128字节。 这不是由CPU完成的 - 如果中断使用当前堆栈 (与内核代码一样),并且启用了红色区域 (默认),则中断将静默破坏堆栈。 如果中断不遵守红色区域,请始终将-mno-red-zone传递给内核代码(甚至支持嵌入内核的libc库)。
注3:调用时栈是16字节对齐的。该调用推送%rip,因此如果被调用方推送%rbp,堆栈将再次以16字节对齐。
注4: 栈在函数的prologue/epilogue之外的任何时候都是8字节对齐的。
System V ABI
- 正文: System V ABI
SystemV ABI是当今使用的主要ABI之一,在Unix系统中几乎是通用的。 这是i686-elf-gcc and x86_64-elf-gcc等工具链使用的调用约定。
外部引用
为了从C调用外函数,它必须具有正确的C原型。 因此,如果函数fee()以C调用顺序接受参数fie、foe和fum,并返回一个整数值,那么相应的头文件应该具有以下原型:
int fee(int fie, char foe, double fum);
同样,汇编代码中的全局变量必须声明为extern:
extern int frotz;
汇编语言或其他语言中的C函数必须声明为适用于该语言。 例如,在NASM中,C函数
int foo(int bar, char baz, double quux);
会被声明为
extern foo
此外,在大多数汇编语言中,要导出的函数或变量必须声明为全局的:
global foo
global frotz
Name Mangling
在某些目标格式(a.out)中,C函数的名称通过在其前面加上下划线(“_”)来automagically mangled。(译者注Name Mangling-名称打乱,是一种链接时防止重名的机制) 因此,要以这样的格式在汇编中调用C函数 foo(),你必须将其定义为 extern _foo,而不是 extern foo。 此要求不适用于大多数现代格式,如[[COFF]、PE和ELF。
C++的name mangling 要复杂得多,同时C++编译器也将参数列表中的类型信息编码到符号中。 (这是实现C++中的函数重载的首要基础。) Binutils包包含工具c++filt,可用于确定正确的损坏(mangled)名称。
寄存器
通用寄存器EBX, ESI, EDI, EBP, DS, ES, and SS, 必须由被调用的函数保存。 如果使用它们,必须先保存它们,然后再恢复。 相反, EAX and EDX 用于返回值,因此不应保留。 被调用函数不需要保存其他寄存器,但如果调用函数使用到它们,则调用函数应在调用之前保存它们,并在调用之后恢复。
传递函数参数
GCC/x86在堆栈上传递函数参数。 这些参数的推送顺序与参数列表中的顺序相反。 此外,由于x86保护模式堆栈操作是对32位值进行操作,因此即使实际值小于完整的32位值,这些值也始终作为32位值推送。 因此,对于函数 foo(),首先将 quux (48位FP值) 的值作为两个32位值 (低32位值) 推送 ; baz的值被推送到32位值中的第一个字节;最后,bar被作为32位值推送。
要将参数传递给C函数,调用函数必须推送如上所述参数值。 因此,要从 NASM 汇编程序调用foo(),你可以做这样的事情
push eax ; low 32-bit of quux
push edx ; high 32-bit of quux
push bl ; baz
push ecx ; bar
call foo
访问函数参数
在GCC/x86 C调用约定中,任何接受形参的函数都应该做的第一件事是推送 EBP的值(调用函数的帧基指针),然后将ESP的值复制到EBP。 这将设置函数自己的框架指针,该指针用于跟踪参数和 (在C中或在任何适当可重入(reentrant)的汇编代码中) 局部变量。
要访问C函数传递的参数,需要使用EBP一个等于4*(n+2)的偏移量,其中n是参数列表中参数的编号(不是按其推送顺序的编号),索引为零。 +2是调用函数保存的帧指针和返回指针的附加偏移量(由 CALL自动推送,由 RET弹出)。
因此,在函数 fee 中,将 fie 移动到 EAX,foe 移动到 BL,然后 fum 进入 EAX 和 EDX,你将在NASM中这样写:
mov ecx, [ebp + 8] ; fie
mov bl, [ebp + 12] ; foe
mov edx, [ebp + 16] ; low 32-bit of fum
mov eax, [ebp + 20] ; high 32-bit of fum
如前所述,GCC中的返回值是使用EAX和EDX传递的。 如果值超过64位,则必须将其作为指针传递。