assembly 我既不理解Microsoft x64调用约定,也不理解FASM对它的实现

nr7wwzry  于 2023-03-02  发布在  其他
关注(0)|答案(1)|浏览(228)

我用FASM编写了一个简短的PE 32+程序,它向stdout写入“Hello World!”并退出。

format PE64 console
include 'win64wx.inc'
.code
  start:
    invoke WriteFile,<invoke GetStdHandle,STD_OUTPUT_HANDLE>,hello,hello.length,dummy,0
    invoke ExitProcess,0
  .end start
.data
  dummy rd 1  
  hello db "Hello world!",13,10,0
  hello.length = $ - hello

我已经看过生成的机器码,但是我不明白为什么要这样操作RSP。

sub rsp,byte +0x08         ;Allocate 8 bytes on the stack. 
sub rsp,byte +0x30         ;Allocate shadow space for WriteFile (48 bytes)
sub rsp,byte +0x20         ;Allocate shadow space for GetStdHandle
mov rcx,0xfffffffffffffff5 ;Set the constant for stdout
call [rel 0x1060]          ;Call GetStdHandle. The handle for stdout is now in RAX
add rsp,byte +0x20         ;Deallocate shadow space for GetStdHandle
mov rcx,rax                ;Set stdout handle: hFile
mov rdx,0x403004           ;Set the pointer to string "Hello World!\r\n": lpBuffer
mov r8,0xf                 ;Set the length of the string: nNumberOfBytesToWrite
mov r9,0x403000            ;Set the pointer for lpNumberOfBytesWritten
mov qword [rsp+0x20],0x0   ;Push a 64 bit NULL pointer onto the stack: lpOverlapped
call [rel 0x1068]          ;Call WriteFile
add rsp,byte +0x30         ;Deallocate shadow space for WriteFile
sub rsp,byte +0x20         ;Allocate shadow space for ExitProcess
mov rcx,0x0                ;Set the return value
call [rel 0x1058]          ;Call ExitProcess
add rsp,byte +0x20         ;Deallocate shadow space for ExitProcess

我知道提前为WriteFile分配空间并不重要,但为什么是sub rsp,byte +0x30而不是sub rsp,byte +0x28?为什么第一个sub rsp,byte +0x08在那里?是FASM的特性还是我从根本上误解了Microsoft x64堆栈管理规则?

db2dz4w8

db2dz4w81#

注解已经解决了16字节的堆栈对齐问题,这会导致在调用指令中压入返回地址后堆栈变得不对齐,这意味着您必须在函数prolog中将其与sub rsp, 8重新对齐,请参见https://learn.microsoft.com/en-us/cpp/build/stack-usage。(0x20)字节,可用于溢出前4个参数,这些参数在寄存器中传递,参见https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention。虽然调用方负责分配影子堆栈,当然,这可以组合起来用于多个函数调用。FASM实际上可以使用frame/endf宏来完成这一操作,请参见https://flatassembler.net/docs.php?article=win32#1.4。现在,如果您显式调用GetStdHandle,将删除嵌套(不确定为什么需要这样做),您可以合并分配:

format PE64 console
include 'win64wx.inc'
.code
  start:
   frame
    invoke GetStdHandle,STD_OUTPUT_HANDLE
    invoke WriteFile,rax,hello,hello.length,dummy,0
    invoke ExitProcess,0
   endf
  .end start
.data
  dummy rd 1  
  hello db "Hello world!",13,10,0
  hello.length = $ - hello

其组装成:

sub rsp,0x8
sub rsp,0x30
mov rcx,0xFFFFFFFFFFFFFFF5
call qword ptr ds:[<&GetStdHandle>]
mov rcx,rax
mov rdx,test.403004
mov r8,0xF
mov r9,test.403000
mov qword ptr ss:[rsp+0x20],0x0
call qword ptr ds:[<&WriteFile>]
mov rcx,0x0
call qword ptr ds:[<&FatalExit>]
add rsp,0x30

sub rsp, 0x20现在没有了。不幸的是,sub rsp, 8仍然在这里(它是由.code宏插入的),但它要干净得多。如果你使用proc宏,那么push rbp将已经对齐堆栈,所以你不需要额外的sub rsp, 8
当然,这是FASM,因此,如果不使用proc,您可以添加/重新定义一些宏来合并所有分配,甚至可以使用影子堆栈来处理您使用的非易失性寄存器的溢出,并在不调用函数时保持堆栈不对齐。这是我在https://github.com/stevenwdv/asmsequent/blob/main/proc_mod.inc中所做的(使用frame/save/rest(ore)/...宏),它可能有点难看,我不知道它的大部分工作原理,但它完成了任务。

mov qword ptr ss:[rsp+0x8],r12
mov qword ptr ss:[rsp+0x10],r13
mov qword ptr ss:[rsp+0x18],r14
mov qword ptr ss:[rsp+0x20],r15
push rsi
push rbx
sub rsp,0x28
...
call ...
...
call ...
...
add rsp,0x28
pop rbx
pop rsi
mov r15,qword ptr ss:[rsp+0x20]
mov r14,qword ptr ss:[rsp+0x18]
mov r13,qword ptr ss:[rsp+0x10]
mov r12,qword ptr ss:[rsp+0x8]
ret

注意组合分配(以及可能的情况下影子堆栈的使用)。

相关问题