技术宅的结界

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

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 282|回复: 2
收起左侧

详解Windows x64上的fastcall调用约定

[复制链接]

25

主题

65

帖子

1768

积分

用户组: 管理员

UID
1043
精华
8
威望
38 点
宅币
1528 个
贡献
59 次
宅之契约
0 份
在线时间
268 小时
注册时间
2015-8-15
发表于 2018-6-26 13:31:56 | 显示全部楼层 |阅读模式

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

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

x
在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被视为非易失性寄存器。
flowers for Broken spirits - a woman turned into stake will hold the world in the basin of fire.

25

主题

82

帖子

1116

积分

用户组: 版主

UID
1821
精华
6
威望
57 点
宅币
859 个
贡献
31 次
宅之契约
0 份
在线时间
200 小时
注册时间
2016-7-12
发表于 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就会重新查找参数最多的函数进行栈分配

996

主题

2213

帖子

5万

积分

用户组: 管理员

一只技术宅

UID
1
精华
197
威望
261 点
宅币
16510 个
贡献
32858 次
宅之契约
0 份
在线时间
1574 小时
注册时间
2014-1-26
发表于 2018-6-30 18:56:19 | 显示全部楼层
这里可以帮你补充一个具体的例子。
[C] 纯文本查看 复制代码
void very_simple_process(int foo, int bar, int baz)
{
	char buf[256];
	printf("Process start.\n");
	fprintf(stdout, "This program should not be compiled by using link-time optimization.\n");
	sprintf(buf, "Because it may be inlined due to it will be called only few times.\n\n");
	fputs(buf, stdout);

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

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

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

segment .text
_very_simple_process:

push rbx
push rsi
push rdi

sub rsp, 256 ;buf

mov rbx, rcx
mov rsi, rdx
mov rdi, r8

mov rcx, str1
call _printf

mov rcx, [_stdout]
mov rdx, str2
call _fprintf

lea rcx, [rsp] ;指向buf
mov rdx, str3
call _sprintf

lea rcx, [rsp] ;指向buf
mov rdx, [_stdout]
call _fputs

mov rcx, str4
mov edx, ebx
mov r8d, esi
mov r9d, edi
call _printf

mov rcx, str5
lea edx, [ebx + esi]
add edx, edi
call _printf

mov rcx, str6
call _printf

add rsp, 256

pop rdi
pop rsi
pop rbx

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

然后rsp的位置在最左边。

本版积分规则

QQ|申请友链|Archiver|手机版|小黑屋|技术宅的结界 ( 滇ICP备16008837号|网站地图

GMT+8, 2018-10-19 10:34 , Processed in 0.094034 second(s), 37 queries , Gzip On.

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

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