NASM - win64调用ExitProcess不用提供阴影区的原因
概述
通常来说,win64位程序调用API时,必须提供阴影区(shadow space), 这是win64 API调用的约定。
但是发现一个特例 ExitProcess()
.Exit:
sub RSP, 32 ; 加上阴影区再调用API好一些,也没有负面影响
xor ECX, ECX
call ExitProcess ; 退出的函数是API, 为什么不做栈平衡, 是ExitProcess内部不用阴影区么? ExitProcess是特例?
add RSP, 32
调用ExitProcess时,原来的例子是没有阴影区的。
尝试在调用ExitProcess的前后,加上阴影区,如上代码。
但是编译完, 用IDA看exe,调用ExitProcess的后面,原来加的 add RSP, 32 被优化掉了, golink干的?
用IDA看obj, 调用后,也没有恢复栈平衡的代码,原来是被NASM优化掉了。
// 这是obj的反汇编
fn_Start_Exit:
sub rsp, 20h
xor ecx, ecx
call ExitProcess
; // 这里没有自己加的恢复栈的代码了
fn_Start endp
看来,不用加阴影区, ExitProcess就可以正常工作。
那还是恢复原来的实现,如下:
.Exit:
xor ECX, ECX
call ExitProcess ; 退出的函数是API, 为什么不做栈平衡, 是ExitProcess内部不用阴影区么? ExitProcess是特例?
用IDA看反汇编如下
public start
start proc near
sub rsp, 8
sub rsp, 20h
xor ecx, ecx ; lpModuleName
call GetModuleHandleA
mov cs:hInstance, rax
add rsp, 20h
call sub_401026
xor ecx, ecx ; uExitCode
call ExitProcess ; // break here
start endp
在 call ExitProcess处下断点,单步调试一下,看看为啥调用ExitProcess不用给阴影区。
笔记
.text:0000000000401000 ; Attributes: noreturn
.text:0000000000401000
.text:0000000000401000 public start
.text:0000000000401000 start proc near
.text:0000000000401000 sub rsp, 8
.text:0000000000401004 sub rsp, 20h
.text:0000000000401008 xor ecx, ecx ; lpModuleName
.text:000000000040100A call GetModuleHandleA
.text:000000000040100F mov cs:hInstance, rax
.text:0000000000401016 add rsp, 20h
.text:000000000040101A call sub_401026
.text:000000000040101F xor ecx, ecx ; uExitCode
.text:0000000000401021 call ExitProcess ; // 断住了, F7单步
.text:0000000000401021 start endp
.idata:000000000040310E ; void __stdcall __noreturn ExitProcess(UINT uExitCode)
.idata:000000000040310E ExitProcess proc near
.idata:000000000040310E jmp cs:__imp_ExitProcess ; // here
.idata:000000000040310E ExitProcess endp
KERNEL32:000000000042E3E0 kernel32_ExitProcess:
KERNEL32:000000000042E3E0 sub rsp, 28h ; 可以看到 kernel32_ExitProcess 入口处给了32字节的阴影区和一个未知的8字节栈空间
KERNEL32:000000000042E3E4 call cs:off_495DF8 ; F7
KERNEL32:000000000042E3EB nop dword ptr [rax+rax+00h] ; 但是并没有在call 之后平衡栈, 因为已经到不了这里了。
ntdll:00007FFD9280EED0 ntdll_RtlExitUserProcess:
ntdll:00007FFD9280EED0 push rbx ; CODE XREF: KERNEL32:kernel32_ExitProcess+4↑p
ntdll:00007FFD9280EED0 ; DATA XREF: KERNEL32:off_495DF8↑o
ntdll:00007FFD9280EED2 sub rsp, 20h
ntdll:00007FFD9280EED6 mov ebx, ecx
ntdll:00007FFD9280EED8 call near ptr unk_7FFD92831F90
ntdll:00007FFD9280EEDD mov rax, gs:30h
ntdll:00007FFD9280EEE6 movzx ecx, word ptr [rax+17EEh]
ntdll:00007FFD9280EEED shr ecx, 0Ch
ntdll:00007FFD9280EEF0 and ecx, 1
ntdll:00007FFD9280EEF3 call near ptr unk_7FFD9280FEC4
ntdll:00007FFD9280EEF8 call near ptr unk_7FFD927EE6C4
ntdll:00007FFD9280EEFD lea rcx, unk_7FFD9291C0E0
ntdll:00007FFD9280EF04 call near ptr ntdll_RtlEnterCriticalSection
ntdll:00007FFD9280EF09 mov rcx, gs:60h
ntdll:00007FFD9280EF12 mov rcx, [rcx+30h]
ntdll:00007FFD9280EF16 call near ptr ntdll_RtlLockHeap
ntdll:00007FFD9280EF1B mov edx, ebx
ntdll:00007FFD9280EF1D xor ecx, ecx
ntdll:00007FFD9280EF1F call near ptr ntdll_ZwTerminateProcess
ntdll:00007FFD9280EF24 test eax, eax
ntdll:00007FFD9280EF26 js loc_7FFD9286CE08
ntdll:00007FFD9280EF2C call near ptr unk_7FFD9280F5A0
ntdll:00007FFD9280EF31 mov rax, gs:30h
ntdll:00007FFD9280EF3A lea rcx, unk_7FFD9291C0E0
ntdll:00007FFD9280EF41 mov rdx, [rax+48h]
ntdll:00007FFD9280EF45 and cs:qword_7FFD9291C0F8, 0
ntdll:00007FFD9280EF4D mov cs:qword_7FFD9291C0F0, rdx
ntdll:00007FFD9280EF54 mov cs:dword_7FFD9291C0E8, 0FFFFFFFEh
ntdll:00007FFD9280EF5E mov cs:dword_7FFD9291C0EC, 1
ntdll:00007FFD9280EF68 call near ptr ntdll_RtlLeaveCriticalSection
ntdll:00007FFD9280EF6D mov edx, ebx
ntdll:00007FFD9280EF6F or rcx, 0FFFFFFFFFFFFFFFFh
ntdll:00007FFD9280EF73 call near ptr ntdll_RtlReportSilentProcessExit
ntdll:00007FFD9280EF78 call near ptr ntdll_LdrShutdownProcess
ntdll:00007FFD9280EF7D mov edx, ebx
ntdll:00007FFD9280EF7F or rcx, 0FFFFFFFFFFFFFFFFh
ntdll:00007FFD9280EF83 call near ptr ntdll_ZwTerminateProcess ; // 执行完这句, 程序就结束了。
ntdll:00007FFD9280EF88
ntdll:00007FFD9280EF88 loc_7FFD9280EF88: ; CODE XREF: ntdll:ntdll_memset+18841↓j
ntdll:00007FFD9280EF88 add rsp, 20h ; // 这里有函数本身的栈平衡,但是到不了这里了。
ntdll:00007FFD9280EF8C pop rbx
ntdll:00007FFD9280EF8D retn
结论
为API准备阴影区,是方便API可能要往栈上的阴影区中存放4个寄存器(RCX, RDX, R8, R9)的副本用于调试,异常等用途.
但是通过调试,并没有发现ExitProcess向阴影区写入4个寄存器副本的操作。
这就是调用ExitProcess时,可以不用准备阴影区的原因。
在x64程序中,调用其他API, 如果不准备阴影区也不是一定不行(调用完API没有不良影响就行)。其中有一部分API是不用阴影区的,但是谁还有能力记住或探究哪个API调用时是不用准备阴影区的?
因为API数量巨大,如果不是很肯定(e.g. ExitProcess就不用阴影区), 那么还是按照API调用约定,在调用API之前,为函数准备栈上的阴影区。加上也不会错,而且调用API的方式一致。