64.5 x86-64

64.5.1 Windows x64

在Win64里面传递函数参数的方法类似fastcall调用约定。前四个参数通过RCX,RDX,R8和R9寄存器传参,其余参数通过栈进行传递。调用者还必须预留32个字节或者4个64位的空间,让被调用者可以保存前四个参数。短函数可能直接使用通过寄存器传过来的值,但更大的可能是保存那些值后在进一步使用。

调用者还必须负责还原栈指针。

这个调用约定也用于Windows x86-64位系统上的DLL(而不是Win32的stdcall)。

例子

  1. #include <stdio.h>
  2. void f1(int a, int b, int c, int d, int e, int f, int g)
  3. {
  4. printf ("%d %d %d %d %d %d %d\n", a, b, c, d, e, f, g);
  5. };
  6. int main()
  7. {
  8. f1(1,2,3,4,5,6,7);
  9. };

Listing 64.6: MSVC 2012 /0b

  1. $SG2937 DB '%d %d %d %d %d %d %d', 0aH, 00H
  2. main PROC
  3. sub rsp, 72 ; 00000048H
  4. mov DWORD PTR [rsp+48], 7
  5. mov DWORD PTR [rsp+40], 6
  6. mov DWORD PTR [rsp+32], 5
  7. mov r9d, 4
  8. mov r8d, 3
  9. mov edx, 2
  10. mov ecx, 1
  11. call f1
  12. xor eax, eax
  13. add rsp, 72 ; 00000048H
  14. ret 0
  15. main ENDP
  16. a$ = 80
  17. b$ = 88
  18. c$ = 96
  19. d$ = 104
  20. e$ = 112
  21. f$ = 120
  22. g$ = 128
  23. f1 PROC
  24. $LN3:
  25. mov DWORD PTR [rsp+32], r9d
  26. mov DWORD PTR [rsp+24], r8d
  27. mov DWORD PTR [rsp+16], edx
  28. mov DWORD PTR [rsp+8], ecx
  29. sub rsp, 72 ; 00000048H
  30. mov eax, DWORD PTR g$[rsp]
  31. mov DWORD PTR [rsp+56], eax
  32. mov eax, DWORD PTR f$[rsp]
  33. mov DWORD PTR [rsp+48], eax
  34. mov eax, DWORD PTR e$[rsp]
  35. mov DWORD PTR [rsp+40], eax
  36. mov eax, DWORD PTR d$[rsp]
  37. mov DWORD PTR [rsp+32], eax
  38. mov r9d, DWORD PTR c$[rsp]
  39. mov r8d, DWORD PTR b$[rsp]
  40. mov edx, DWORD PTR a$[rsp]
  41. lea rcx, OFFSET FLAT:$SG2937
  42. call printf
  43. add rsp, 72 ; 00000048H
  44. ret 0
  45. f1 ENDP

在这里我们可以清楚看到这7个参数是如何传递的:4个参数通过寄存器传递而其余3个通过栈传递。f1()的反汇编代码一开始就把参数保存到“预留”的栈空间之中,这样做的目的是编译器并不能保证有足够的寄存器可以使用,如果不这样做的话这四个寄存器将被参数占用到函数执行结束。最后,预留栈空间是调用者的职责。

Listing 64.7: Optimizing MSVC 2012 /0b

  1. $SG2777 DB '%d %d %d %d %d %d %d', 0aH, 00H
  2. a$ = 80
  3. b$ = 88
  4. c$ = 96
  5. d$ = 104
  6. e$ = 112
  7. f$ = 120
  8. g$ = 128
  9. f1 PROC
  10. $LN3:
  11. sub rsp, 72 ; 00000048H
  12. mov eax, DWORD PTR g$[rsp]
  13. mov DWORD PTR [rsp+56], eax
  14. mov eax, DWORD PTR f$[rsp]
  15. mov DWORD PTR [rsp+48], eax
  16. mov eax, DWORD PTR e$[rsp]
  17. mov DWORD PTR [rsp+40], eax
  18. mov DWORD PTR [rsp+32], r9d
  19. mov r9d, r8d
  20. mov r8d, edx
  21. mov edx, ecx
  22. lea rcx, OFFSET FLAT:$SG2777
  23. call printf
  24. add rsp, 72 ; 00000048H
  25. ret 0
  26. f1 ENDP
  27. main PROC
  28. sub rsp, 72 ; 00000048H
  29. mov edx, 2
  30. mov DWORD PTR [rsp+48], 7
  31. mov DWORD PTR [rsp+40], 6
  32. lea r9d, QWORD PTR [rdx+2]
  33. lea r8d, QWORD PTR [rdx+1]
  34. lea ecx, QWORD PTR [rdx-1]
  35. mov DWORD PTR [rsp+32], 5
  36. call f1
  37. xor eax, eax
  38. add rsp, 72 ; 00000048H
  39. ret 0
  40. main ENDP

如果我们使用了编译优化的开关去编译上面的例子,它的反汇编码几乎是相同的,但是预留的栈空间将不被使用,因为在这里并不需要使用到预留的栈空间。

而且可以看到MSVC 2012是如何利用LEA指令来优化代码(A.6.2)。

我也不确定是否值得这么做。

更多的例子请看(74.1)

this指针的传递(C/C++)

this指针通过RCX传递,成员函数的第一个参数通过RDX传递,更多例子请看(51.1.1)。

64.5.2 Linux x64

Linux x86-64传递参数的方式几乎和Windows一样。但是是通过6个寄存器代替4个寄存器来传参(RDI,RSI,RDX,RCX,R8,R9),另外并没有预留的栈空间这回事。虽然,如果它需要/想要的话,可以把寄存器的值保存到栈之中。

Listing 64.8: Optimizing GCC 4.7.3

  1. .LC0:
  2. .string "%d %d %d %d %d %d %d\n"
  3. f1:
  4. sub rsp, 40
  5. mov eax, DWORD PTR [rsp+48]
  6. mov DWORD PTR [rsp+8], r9d
  7. mov r9d, ecx
  8. mov DWORD PTR [rsp], r8d
  9. mov ecx, esi
  10. mov r8d, edx
  11. mov esi, OFFSET FLAT:.LC0
  12. mov edx, edi
  13. mov edi, 1
  14. mov DWORD PTR [rsp+16], eax
  15. xor eax, eax
  16. call __printf_chk
  17. add rsp, 40
  18. ret
  19. main:
  20. sub rsp, 24
  21. mov r9d, 6
  22. mov r8d, 5
  23. mov DWORD PTR [rsp], 7
  24. mov ecx, 4
  25. mov edx, 3
  26. mov esi, 2
  27. mov edi, 1
  28. call f1
  29. add rsp, 24
  30. ret

注意:这里的值是写入到32-bit的寄存器(EAX…)而不是整个64-bit寄存器(RAX…)。这是因为写入到32-bit寄存器的时候会自动清空高32-bit。据说,这是为了方便把代码移植到x86-64。