技术宅的结界

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

QQ登录

只需一步,快速开始

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

详解Windows x64上的fastcall调用约定

[复制链接]

37

主题

104

帖子

4530

积分

用户组: 管理员

UID
1043
精华
17
威望
201 点
宅币
3498 个
贡献
441 次
宅之契约
0 份
在线时间
698 小时
注册时间
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被视为非易失性寄存器。

评分

参与人数 2威望 +2 宅币 +62 贡献 +26 收起 理由
watermelon + 1 + 30 + 10 牛;太麻烦了,哭了orz
0xAA55 + 1 + 32 + 16 屌!

查看全部评分

flowers for Broken spirits - a woman turned into stake will hold the world in the basin of fire.
回复

使用道具 举报

37

主题

104

帖子

4530

积分

用户组: 管理员

UID
1043
精华
17
威望
201 点
宅币
3498 个
贡献
441 次
宅之契约
0 份
在线时间
698 小时
注册时间
2015-8-15
 楼主| 发表于 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,可以定义:
[C] 纯文本查看 复制代码
typedef unsigned __int64 u64;
typedef signed __int64 i64;

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

28

主题

173

帖子

1975

积分

用户组: 版主

UID
1821
精华
6
威望
67 点
宅币
1534 个
贡献
104 次
宅之契约
0 份
在线时间
329 小时
注册时间
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就会重新查找参数最多的函数进行栈分配

1053

主题

2413

帖子

6万

积分

用户组: 管理员

一只技术宅

UID
1
精华
220
威望
327 点
宅币
19229 个
贡献
39830 次
宅之契约
0 份
在线时间
1819 小时
注册时间
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的位置在最左边。

24

主题

256

帖子

1514

积分

用户组: 上·技术宅

UID
3808
精华
6
威望
53 点
宅币
1032 个
贡献
90 次
宅之契约
0 份
在线时间
253 小时
注册时间
2018-5-6
发表于 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
菜鸟一枚,直接指正,不必留情

24

主题

256

帖子

1514

积分

用户组: 上·技术宅

UID
3808
精华
6
威望
53 点
宅币
1032 个
贡献
90 次
宅之契约
0 份
在线时间
253 小时
注册时间
2018-5-6
发表于 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
菜鸟一枚,直接指正,不必留情

本版积分规则

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

GMT+8, 2020-6-3 13:12 , Processed in 0.101634 second(s), 34 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

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