Calling Conventions

来自osdev
跳到导航 跳到搜索

在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]、PEELF

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 移动到 EAXfoe 移动到 BL,然后 fum 进入 EAXEDX,你将在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中的返回值是使用EAXEDX传递的。 如果值超过64位,则必须将其作为指针传递。

另见

外部链接

de:Aufrufkonventionen