ASM调用约定

时间:2013-12-11 21:14:19

标签: c assembly linux-kernel x86 x86-64

我一直在阅读有关在ASM中调用约定的内容,这是我到目前为止所做的:

          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调用该模块中的一个函数。我应该使用什么惯例?内核或用户空间?

4 个答案:

答案 0 :(得分:4)

1)是的,它似乎只适用于Linux。我认为你可以依赖这里描述的Linux约定:http://www.x86-64.org/documentation/abi.pdf。但实际上你可以按照intel assembly manual章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,就像我看到编译器如何处理它一样。它应该是直截了当的。

答案 1 :(得分:2)

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

对于3.,如果它是一个内核函数(而不是libc函数),你将使用内核约定(第2列)。

至于1.,更正,除了你不会使用ebx作为第6个参数。假设它是实际的ebp,传统的函数序言会推动这个论点。所以截止实际上是5个论点。

对于2.,如果你有超过5个参数,你会将它们连续存储在内存中,并在ebx中传递指向该内存区域开头的指针。

答案 2 :(得分:1)

对于内核调用约定,它使用寄存器来提高效率。此外,系统调用是需要特权级别更改的特殊调用,需要特权级别更改的调用使用不同的堆栈,因此通常的函数prolog(push ebpmov ebp,esp等)是没用,因为ebp无法访问任何用户参数。

内核函数可能会查看用户堆栈以获取所需的参数,但对于某些体系结构,从内核代码访问用户内存不像x86中那样简单,容易或快速,而Linux意味着可移植到许多架构。因此,寄存器虽然在x86架构中有些限制,但它是一种方便快捷的传递参数的方法。如果系统调用需要六个以上的参数,则其中一个参数将是指向用户内存中保存的struct的指针。如果需要,内核将使用copy_from_user()函数将结构复制到内核内存中(例如,通常使用ioctl()系统调用来完成。)

答案 3 :(得分:1)

我不知道这是否有帮助,但请查看Agner Fog Calling conventions for different C++ compilers and operating systems中的表4和表5。这些给出了C ++不同编译器和操作系统的寄存器使用和调用约定的很好的总结。

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

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

Windows和Linux也有一些不同的返回寄存器。您只有EAXRAX列表,但也有例如XMM0YMMOST(0),...

这些结果与您为ASM撰写的结果类似。