ASM调用约定

12

我一直在阅读关于汇编调用约定的内容,到目前为止我了解到:

          x86(userland)    x86(kernel)    x64(userland)    x64(kernel)

1st arg           Stack           EBX               RDI            RDI
2nd arg           Stack           ECX               RSI            RSI
3rd arg           Stack           EDX               RDX            RDX
4th arg           Stack           ESI               RCX            R10
5th arg           Stack           EDI               R8             R8
6th arg           Stack           EBP               R9             R9

result            EAX             EAX               RAX            RAX

我的问题是:

  1. 目前我学到的内容是否正确?

  2. 如何在x86(内核)和x64(两者)中传递超过6个参数?使用栈吗?能否给我展示一个小例子?

  3. 我有一个内核模块,想从ASM中调用该模块中的函数。应该使用哪种约定?内核还是用户空间?


我的理解是第四列(标记为“x64(kernel)”)在用户和内核之间的syscall接口中使用。对于内核内部函数之间的调用,使用标准ABI(标记为“x64(userland)”)。然而,我不是这方面的专家,所以如果我有错误,请有经验的人纠正我。 - Michael Burr
1
对于一些特殊情况,研究 __syscall。 - Marco van de Voort
@MarcovandeVoort:你能详细说明一下吗? - Michael Burr
1
至少在FreeBSD/x86上,如果其中一个参数是64位,则必须使用__Syscall。(即:off_t,因此mmap、lseek、ftruncate)。Pipe也有特殊的语义,而(在Linux上)clone也很奇怪。 - Marco van de Voort
4个回答

4

1) 是的,似乎只适用于Linux。我认为您可以依赖此处描述的Linux惯例:http://www.x86-64.org/documentation/abi.pdf。但实际上,您可以按照Intel汇编手册第6.3.3章节所述的方式传递参数。

2) 使用栈是编译器使用的方法:

int func(int i, int j, int k, int l, int m, int n, int o, int p, int q) { return q; }
void func2() { func(1, 2, 3, 4, 5, 6, 7, 8, 9); }

然后:

$ gcc -c func.c && objdump -d func.o 

在我的x86_64机器上输出:

0000000000000000 <func>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   89 7d fc                mov    %edi,-0x4(%rbp)
   7:   89 75 f8                mov    %esi,-0x8(%rbp)
   a:   89 55 f4                mov    %edx,-0xc(%rbp)
   d:   89 4d f0                mov    %ecx,-0x10(%rbp)
  10:   44 89 45 ec             mov    %r8d,-0x14(%rbp)
  14:   44 89 4d e8             mov    %r9d,-0x18(%rbp)
  18:   8b 45 20                mov    0x20(%rbp),%eax
  1b:   5d                      pop    %rbp
  1c:   c3                      retq   

000000000000001d <func2>:
  1d:   55                      push   %rbp
  1e:   48 89 e5                mov    %rsp,%rbp
  21:   48 83 ec 18             sub    $0x18,%rsp
  25:   c7 44 24 10 09 00 00    movl   $0x9,0x10(%rsp)
  2c:   00 
  2d:   c7 44 24 08 08 00 00    movl   $0x8,0x8(%rsp)
  34:   00 
  35:   c7 04 24 07 00 00 00    movl   $0x7,(%rsp)
  3c:   41 b9 06 00 00 00       mov    $0x6,%r9d
  42:   41 b8 05 00 00 00       mov    $0x5,%r8d
  48:   b9 04 00 00 00          mov    $0x4,%ecx
  4d:   ba 03 00 00 00          mov    $0x3,%edx
  52:   be 02 00 00 00          mov    $0x2,%esi
  57:   bf 01 00 00 00          mov    $0x1,%edi
  5c:   e8 00 00 00 00          callq  61 <func2+0x44>
  61:   c9                      leaveq 
  62:   c3                      retq   

3) 如果你是在内核模块中调用该函数,我会说它是内核函数。要有一个完整的有效示例,你可以在模块中从 C 中调用你的函数,并像我一样对 .ko 进行反汇编,以了解编译器如何处理它。这应该很简单。


如果可以的话,我会给你的答案点赞几次 :) - alexandernst
嗯...我有一个问题。为什么 callq 61 <func2+0x44> 被转换成了 e8 00 00 00 00?我的意思是,e8 确实意味着 call,但是 61 <func2+0x44> 是从哪来的? - alexandernst
在这种情况下,objdump在二进制对象上运行,因此编译器不知道func1的最终虚拟地址。它将在链接过程中解决。在这种情况下,它被填充为0直到那时。因此,objdump只显示您调用q 61, 61引用下一个指令。这与生成的二进制文件无关(请尝试使用hexdump)。但是,如果您在最终的a.out上运行objdump -d,则会注意到func1的实际虚拟地址而不是0。 在您的情况下,通过运行gcc -S func.c && cat func.s来显示gcc构建的汇编代码可能更有趣。 - Ervadac
禁用优化会使汇编输出变得非常嘈杂。在fun中,大多数指令都将传入的寄存器参数溢出到本地堆栈内存(在红色区域),最终返回一个堆栈参数(在RSP上方)。如果只有这个,那就更清晰了。 - Peter Cordes

2

我只编写x86架构的代码,可以为该架构提供一些反馈(前两列)。

对于第3点,如果它是内核函数(而不是libc函数),则应使用内核约定(您的第2列)。

关于第1点,正确,但您不会使用ebx作为第6个参数。传统的函数序言会推入此参数,假设它是实际的ebp。因此,截止参数实际上是5个。

就第2点而言,如果您有超过5个参数,则会将它们依次存储在内存中,并在ebx中传递指向此内存区域开头的指针。


1
关于内核调用约定,它使用寄存器以提高效率。此外,系统调用是一种需要特权级别更改的特殊调用,需要特权级别更改的调用使用不同的堆栈,因此通常的函数前导语(push ebpmov ebp,esp等)无用,因为ebp无法访问任何用户参数。
内核函数可能会查看用户堆栈以获取所需的参数,但对于某些体系结构来说,从内核代码访问用户内存并不像在x86中那样直接、容易和快速,而Linux旨在可移植到许多体系结构。因此,虽然在x86体系结构中它们有点受限,但寄存器是传递参数的一种方便且快速的方法。如果系统调用需要超过六个参数,则其中一个将是指向用户内存中保存的struct的指针。如果需要(例如通常使用ioctl()系统调用),内核将使用copy_from_user()函数将结构复制到内核内存中。

我将在我的内核模块中运行该ASM代码,因此该代码已经具有特权。 - alexandernst
据我所知,您将使用标准的C调用约定:参数从右到左依次推送到堆栈中。这就是内核模块内定义函数的方式。 - mcleod_ideafix
那我只需要从右到左将所有地址push到变量中,然后call myfunc,每当我需要获取某个变量时就pop一下,对吗? - alexandernst
是的,但在尝试之前你并不确定。你可以使用-S选项编译内核模块,查看生成了什么汇编代码,这样就能确切知道函数在内核代码中的处理方式了。 - mcleod_ideafix

1

我不知道这是否有帮助,但请参考Agner Fog的不同C++编译器和操作系统的调用约定中的表4和表5。它们提供了关于C++不同编译器和操作系统的寄存器使用和调用约定的简要概述。

对于x86-64:Windows和Linux只有一个调用约定,但它们是不同的。Windows使用6个寄存器,而Linux使用14个寄存器。

对于x86:Windows和Linux使用相同的调用约定,但是有几种调用约定:cdecl,stdcall,pascal和fastcall。约定cdecl,stdcall和pascal仅使用堆栈,而fastcall使用2(或3,具体取决于编译器)个整数寄存器。约定cdecl是默认值。

Windows和Linux还有一些不同的返回寄存器。你只列出了EAXRAX,但还有其他的,比如XMM0YMMOST(0)等等。

这些结果与你为ASM编写的内容类似。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接