找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 7468|回复: 6

详解Windows x64上的fastcall调用约定

[复制链接]
发表于 2018-6-26 13:31:56 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×
在32位的系统上,调用约定繁多,一旦定义错了基本上就GG。不过在64位上,调用约定基本上就分为fastcall和usercall两类了。不论你把函数定义为fastcall stdcall cdecl,最终出来的都是fastcall。
所谓usercall,就是瞎JB定义。爱怎么传参数怎么传参数,爱怎么返回值怎么返回值。

而在x64的fastcall上,假定参数均为64位整数,微软有下列规定:

第一,前四个参数分别放入rcx, rdx, r8, r9四个寄存器中。
这个就很爽了,寄存器操作是相当快的。

第二,不论有几个参数,必须在栈上分配至少32个字节的“阴影空间”,用于放置第五到第八个参数。
理解起来可能有点困难,简单说明一下:
如果没有第五个参数,则调用前必须要有类似sub rsp,20h这样的代码。
如果没有第六个参数,则调用前必须要有类似sub rsp,18h这样的代码。
至于第七第八,不必多说。
由于这个阴影空间是调用者分配的,调用完成后必须释放:
如果没有第五个参数,则返回后必须要有类似add rsp,20h这样的代码。
第六第七第八则依次类推。

当有第八个参数的时候,阴影空间被占满,不需要额外分配阴影空间。
当参数超过八个的时候,阴影空间分配量需要增加,同样释放量也要增加。

上述是理论内容,实际应用不会这么干,在禁用优化的情况下,微软的编译器做法如下:
不在调用前才去分配阴影空间,而是直接在函数头分配出一大段栈空间作为阴影空间去放参数变量。阴影空间的分配会被直接忽略。
至于开了优化。。。不管它了,编译器生成的代码列表不是凡人看的懂的,需要拖进IDA才行。

追加内容:
MASM64上的调用约定使用老版的stdcall风格,即每个参数都要从右至左放进栈里。此外汇编器不再会为ret指令自动计算栈弹出长度,需要手动指定。因此如果想在MASM64里定义参数并被C代码正常调用,需要另写一层thunk函数。
寄存器的易失性由常用调用约定决定,其中rax, rcx, rdx, r8, r9, r10, r11被视为易失性寄存器,而rbx, rsp, rbp, rsi, rdi, r12, r13, r14, r15被视为非易失性寄存器。
回复

使用道具 举报

 楼主| 发表于 2020-2-2 06:06:24 | 显示全部楼层
watermelon 发表于 2020-2-1 20:45
小弟我也根据本贴和0xAA55大佬的另一个帖子:https://www.0xaa55.com/forum.php?mod=viewthread&tid=1434#l ...


1. 不知道你为什么用%p打印64位值,怎么兼容编译到32位程序呢,用%llx %lld啥的不好么。
2. 声明个外部函数不用extern,此外没有返回值不能算完整练习了调用约定。
3. 我觉得你可以试试在汇编里玩玩printf和scanf来练习一下。
4. 用64位值也不需要包含windows.h,直接(un)signed __int64,可以定义:
  1. typedef unsigned __int64 u64;
  2. typedef signed __int64 i64;
复制代码

5. 虽然不建议玩内联汇编,不过你可以用Intel Parallel Studio XE的编译器。安装后在Visual Studio里选用Intel的编译器,于是你可以在64位程序中使用内联汇编,语法和MSVC一样,"__asm"即可。虽然Intel编译器是要钱的,不过如果注册帐号时使用edu邮箱,就可以免费注册使用正版。
6. 我习惯上在分配栈空间时把sub rsp写在函数的第一句,直到ret前再add rsp。加减多少的最小值取决于过程中所调用参数最多的那个函数。
回复 赞! 1 靠! 0

使用道具 举报

发表于 2018-6-26 20:51:47 | 显示全部楼层
一般来说 调用者 再调用api之前会对所有api的参数进行预计,然后对栈进行一次性的分配
例如有如何几个函数foo1 foo4 foo foo7 foo12 foo23
函数demo在调用这几个函数时进行参数预计 比如foo23有23个参数
demo proc
  prolog..
  sub rsp,8*23
  ...
  call foo1
  ...
  call foo4
  ...
  call foo
  ...
  call foo7
  ...
  call foo32
  ...
  call foo12
  ...
  call foo4
  ...
  add rsp,8*23
  epilog
demo endp

如果不调用foo23就会重新查找参数最多的函数进行栈分配
回复 赞! 靠!

使用道具 举报

发表于 2018-6-30 18:56:19 | 显示全部楼层
这里可以帮你补充一个具体的例子。
  1. void very_simple_process(int foo, int bar, int baz)
  2. {
  3.         char buf[256];
  4.         printf("Process start.\n");
  5.         fprintf(stdout, "This program should not be compiled by using link-time optimization.\n");
  6.         sprintf(buf, "Because it may be inlined due to it will be called only few times.\n\n");
  7.         fputs(buf, stdout);

  8.         printf("The result of foo + bar + baz could be %d + %d + %d,\n", foo, bar, baz);
  9.         printf("Which should be %d.\n", foo + bar + baz);
  10.         printf("Process end.\n");
  11. }

  12. // 如果我用中文写
  13. void 非常简单的一个程序(int 甲, int 乙, int 丙)
  14. {
  15.         char 缓冲区[256];
  16.         printf("程序开始。\n");
  17.         fprintf(stdout, "你不能用链接时间优化来优化这个程序。\n");
  18.         sprintf(缓冲区, "因为这个程序大概会因为被调用的次数太少而被内联。\n\n");
  19.         fputs(缓冲区, stdout);

  20.         printf("甲+乙+丙的结果,应该是%d + %d + %d,\n", foo, bar, baz);
  21.         printf("它大概应该等于%d\n", foo + bar + baz);
  22.         printf("程序结束。\n");
  23. }
复制代码
如果我用汇编写,我大概会这样写:
  1. segment .rdata
  2. str1 db "Process start.", 0xa
  3. str2 db "This program should not be compiled by using link-time optimization.", 0xa
  4. str3 db "Because it may be inlined due to it will be called only few times.", 0xa, 0xa
  5. str4 db "The result of foo + bar + baz could be %d + %d + %d,", 0xa
  6. str5 db "Which should be %d.", 0xa
  7. str6 db "Process end.", 0xa

  8. segment .text
  9. _very_simple_process:

  10. push rbx
  11. push rsi
  12. push rdi

  13. sub rsp, 256 ;buf

  14. mov rbx, rcx
  15. mov rsi, rdx
  16. mov rdi, r8

  17. mov rcx, str1
  18. call _printf

  19. mov rcx, [_stdout]
  20. mov rdx, str2
  21. call _fprintf

  22. lea rcx, [rsp] ;指向buf
  23. mov rdx, str3
  24. call _sprintf

  25. lea rcx, [rsp] ;指向buf
  26. mov rdx, [_stdout]
  27. call _fputs

  28. mov rcx, str4
  29. mov edx, ebx
  30. mov r8d, esi
  31. mov r9d, edi
  32. call _printf

  33. mov rcx, str5
  34. lea edx, [ebx + esi]
  35. add edx, edi
  36. call _printf

  37. mov rcx, str6
  38. call _printf

  39. add rsp, 256

  40. pop rdi
  41. pop rsi
  42. pop rbx

  43. ret
复制代码
在这个示例里面,栈上的内存布局大概如下图所示:
stack.png

然后rsp的位置在最左边。
回复 赞! 靠!

使用道具 举报

发表于 2020-2-1 20:45:28 | 显示全部楼层
小弟我也根据本贴和0xAA55大佬的另一个帖子:https://www.0xaa55.com/forum.php ... p;tid=1434#lastpost学习了一下
小弟我是这么认为的,x64中每次调用要手动来平衡栈,要16字节对齐,且call指令还要用8个字节的栈空间来存放它返回的地址;则比如当有4个参数时候,参数需要的栈空间是4*8 = 32,call返回的地址需要8个字节的栈空间,则一共需要32 + 8 = 40个字节的栈空间由于40无法被16整除而需要至少加上8个字节变成48字节,此时可以被16整除,所以此时需要48(0x30)字节,而我们需要手动分配的是40(0x28)字节空间。
小弟实验了以下几个例子(C语言内嵌汇编),将在C语言中写了func函数代替一下0xAA55大佬帖子中的被调用的messagebox函数,同时为了简单分析问题,参数全部选了8字节长度的ULONG64变量,传递参数为4个参数时候可以参考帖子中的例子。
当传入5个参数时候可以使用push来压栈的方法和sub rsp, xxx + mov qword ptr[rsp+xxx],yyy的两种方法。
首先是push的方法,我认为需要手动在纸上先进行演算rsp所指向的位置和变化:

使用push方法传入第5个参数

使用push方法传入第5个参数

当使用push方法传入6个参数时候就不好使了,因为push方法只能将最后一个参数(第六个)传进去,所以第5个参数要想访问就比较麻烦了(也可能是我错了)。

使用push方法传入第5,6个参数

使用push方法传入第5,6个参数

Visual Studio2013中调试64位应用程序可以看到反汇编代码很少使用push方法传递参数的,基本都是使用sub rsp, xxx + mov qword ptr[rsp+xxx],yyy来进行参数的传递,这种方法也更加好算,更加稳定,我以后就用这种办法了。
在使用前同样我需要在纸上先算一下栈空间的分配;

使用sub+mov方法传递第5个参数

使用sub+mov方法传递第5个参数

使用sub+mov方法传递第5,6个参数

使用sub+mov方法传递第5,6个参数


学习了!orz
回复 赞! 靠!

使用道具 举报

发表于 2020-2-2 10:12:53 | 显示全部楼层
本帖最后由 watermelon 于 2020-2-2 10:15 编辑
tangptr@126.com 发表于 2020-2-2 06:06
1. 不知道你为什么用%p打印64位值,怎么兼容编译到32位程序呢,用%llx %lld啥的不好么。
2. 声明个外部函 ...


哦哦是的,一般的代码生成的反汇编也是在开头就sub rsp分配了被调用函数需要的最多的那个栈空间,然后在末尾再add rsp,很有道理,我再练习一下,orz
回复 赞! 靠!

使用道具 举报

发表于 2022-4-28 15:42:23 | 显示全部楼层
进入call之前,rsp必须0x10对齐
x64反汇编下最麻烦的就是call外面看不出参数个数了,必须进入call里面看参数的使用情况才知道
回复 赞! 靠!

使用道具 举报

本版积分规则

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-12-22 21:09 , Processed in 0.036230 second(s), 28 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表