为什么 WriteConsoleW 在使用 ml64 调用 CoInitialize 后中断

Why WriteConsoleW breaks After call CoInitialize using ml64

我正在尝试使用 Visual Studio 2019 年的 ml64.exe 通过 64 位程序集执行一些 Office 自动化。在调用 Office COM 接口之前,我需要调用 CoInitialize。我目前只是测试初始化​​ COM 并写入控制台(我通常不编写汇编代码)如果我注释掉行

call    CoInitialize

WriteConsoleW api 调用按预期工作并向屏幕输出消息 "COM Failed to Initialize" 但是,一旦我添加调用 CoInitialize 回来,控制台屏幕就没有任何输出,并且也崩溃了。

; *************************************************************************
; Proto types for API functions and structures
; *************************************************************************  
EXTRN   GetStdHandle:PROC
EXTRN   WriteConsoleW:PROC
EXTRN   CoCreateInstance:PROC
EXTRN   CoInitialize:PROC
EXTRN   SysFreeString:PROC
EXTRN   SysStringByteLen:PROC
EXTRN   SysAllocStringByteLen:PROC
EXTRN   OleRun:PROC
EXTRN   ExitProcess:PROC

.const

STD_OUTPUT_HANDLE = -11
STD_ERROR_HANDLE = -12

; *************************************************************************
; Object libraries
; *************************************************************************
includelib user32.lib
includelib kernel32.lib
includelib ole32.lib   
includelib oleaut32.lib

; *************************************************************************
; Our data section. 
; *************************************************************************
.data

    strErrComFailed         dw 'C','O','M',' ','F','a','i','l','e','d',' ','t','o',' ','i','n','i','t','i','a','l','i','z','e',0,0 
    strErrOutlookFailed     dw 'F','a','i','l','e','d',' ','t','o',' ','i','n','i','t','i','a','l','i','z','e',' ','O','u','t','l','o','o','k',0,0


    ;  {0006F03A-0000-0000-C000-000000000046}
    CLSID_OutlookApplication    dd 0006f03ah

                                dw 0000h
                                dw 0000h
                                dw 0C000h
                                db 00h
                                db 00h
                                db 00h
                                db 00h
                                db 00h
                                db 46h

    ; {00063001-0000-0000-C000-000000000046}
    IID_OutlookApplication      dd 00063001h
                                dw 0000h
                                dw 0000h
                                dw 0C000h
                                db 00h
                                db 00h
                                db 00h
                                db 00h
                                db 00h
                                db 46h

    ; {00000000-0000-0000-C000-000000000046}
    IID_IUnknown                dd 00000000h
                                dw 0000h    
                                dw 0000h
                                dw 0C000h
                                db 00h
                                db 00h
                                db 00h
                                db 00h
                                db 00h
                                db 46h

; *************************************************************************
; Our executable assembly code starts here in the .code section
; *************************************************************************
.code

wcslen PROC inputString:QWORD
    LOCAL stringLength:QWORD
    mov QWORD PTR inputString, rcx
    mov QWORD PTR stringLength, 0h

continue:

    mov rax, QWORD PTR inputString
    mov rcx, QWORD PTR stringLength
    movzx eax, word ptr [rax+rcx*2]   
    test eax, eax
    je finished              
    mov rax, QWORD PTR stringLength
    inc rax
    mov QWORD PTR stringLength, rax
    jmp continue    

finished:
    mov rax, QWORD PTR stringLength
    ret

wcslen ENDP

main PROC
    LOCAL hStdOutput:QWORD
    LOCAL hErrOutput:QWORD
    LOCAL hResult:DWORD

    xor     ecx,ecx
    call    CoInitialize
    mov     DWORD PTR hResult, eax

    mov     ecx, STD_OUTPUT_HANDLE
    call    GetStdHandle
    mov     QWORD PTR hStdOutput, rax

    mov     ecx, STD_ERROR_HANDLE
    call    GetStdHandle
    mov     QWORD PTR hErrOutput, rax

    lea     rcx,strErrComFailed
    call    wcslen
    mov     QWORD PTR [rsp+32], 0
    xor     r9d, r9d    ; lpNumberOfCharsWritten
    mov     r8d, eax    ; nNumberOfCharsToWrite
    lea     rdx,QWORD PTR strErrComFailed
    mov     rcx,QWORD PTR hStdOutput
    call    WriteConsoleW

    ; When the message box has been closed, exit the app with exit code eax
    mov     ecx, eax
    call    ExitProcess
    ret 


main ENDP
End

在调用 CoInitialize 之前,WinDbg 显示以下寄存器状态:

00007ff7`563e1041 e865000000      call    Win64App+0x10ab (00007ff7`563e10ab)
0:000> r
rax=00007ff7563e1037 rbx=0000000000000000 rcx=0000000000000000
rdx=00007ff7563e1037 rsi=0000000000000000 rdi=0000000000000000
rip=00007ff7563e1041 rsp=000000a905affa58 rbp=000000a905affa70
 r8=000000a9058d6000  r9=00007ff7563e1037 r10=0000000000000000
r11=0000000000000000 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0         nv up ei pl zr na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
Win64App+0x1041:
00007ff7`563e1041 e865000000      call    Win64App+0x10ab (00007ff7`563e10ab)
0:000> r ecx
ecx=0

调用 CoInitialize 后有以下寄存器状态:

0:000> r
rax=0000000000000000 rbx=0000000000000000 rcx=8aa77f80a0990000
rdx=0000000000000015 rsi=0000000000000000 rdi=0000000000000000
rip=00007ff7563e1046 rsp=000000a905affa58 rbp=000000a905affa70
 r8=0000029af97e2620  r9=0000029af97e1440 r10=0000000000000005
r11=000000a905aff9d8 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
Win64App+0x1046:
00007ff7`563e1046 8945ec          mov     dword ptr [rbp-14h],eax ss:000000a9`05affa5c=00000000
0:000> r eax
eax=0

调用 GetStdHandle 后:

0:000> r
rax=0000000000000074 rbx=0000000000000000 rcx=0000029af97d2840
rdx=0000000000000015 rsi=0000000000000000 rdi=0000000000000000
rip=00007ff7563e1053 rsp=000000a905affa58 rbp=000000a905affa70
 r8=0000029af97e2620  r9=0000029af97e1440 r10=0000000000000005
r11=000000a905aff9d8 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206

在调用 WriteConsoleW 时,似乎参数仍然正确但没有任何输出到屏幕:

KERNEL32!WriteConsoleW:
00007ffb`a97028f0 ff258a4c0500    jmp     qword ptr [KERNEL32!_imp_WriteConsoleW (00007ffb`a9757580)] ds:00007ffb`a9757580={KERNELBASE!WriteConsoleW (00007ffb`a697b750)}
0:000> du rdx
00007ff7`563e3000  "COM Failed to initialize"
0:000> r
rax=0000000000000018 rbx=0000000000000000 rcx=0000000000000074
rdx=00007ff7563e3000 rsi=0000000000000000 rdi=0000000000000000
rip=00007ffba97028f0 rsp=000000a905affa50 rbp=000000a905affa70
 r8=0000000000000018  r9=0000000000000000 r10=0000000000000005
r11=000000a905aff9d8 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0         nv up ei pl zr na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
KERNEL32!WriteConsoleW:
00007ffb`a97028f0 ff258a4c0500    jmp     qword ptr [KERNEL32!_imp_WriteConsoleW (00007ffb`a9757580)] ds:00007ffb`a9757580={KERNELBASE!WriteConsoleW (00007ffb`a697b750)}

我尝试改用 CoInitializeEx,但遇到了同样的问题:

mov     edx, COINIT_APARTMENTTHREADED   ; dwCoInit (COINIT_APARTMENTTHREADED = 2)
xor     ecx, ecx                        ; pvReserved
call    CoInitializeEx

x64 ABI 要求 执行调用指令时堆栈总是 16 字节对齐 也保留 32 字节 space。所以在每个函数入口点我们都会有:

RSP == 16*N + 8

所以我们通常必须在函数体中做SUB RSP,40 + N*16,如果我们要调用另一个函数。 但是当我们在函数中声明 LOCAL 变量时——编译器 (masm64) 会进行一些堆栈分配,但不会执行堆栈对齐和 32 字节保留 space。所以需要自己做。同样,当您使用 LOCAL 变量时 - masm64 使用 RBP 寄存器来保存旧的 RSP 值并在最后恢复它(使用 leave 指令)。并通过 RBP 访问本地人,因此您不能自己更改 RBP 的功能。

所以代码可以是下一个

STD_OUTPUT_HANDLE = -11
STD_ERROR_HANDLE = -12

EXTRN   __imp_GetStdHandle:QWORD
EXTRN   __imp_WriteConsoleW:QWORD
EXTRN   __imp_CoInitialize:QWORD
EXTRN   __imp_ExitProcess:QWORD
EXTRN   __imp__getch:QWORD
EXTRN   __imp_wcslen:QWORD

WSTRING macro text:VARARG
    FOR arg, <text>
        if @InStr( , arg, @ )
            f = 0
            FORC c,  <arg>
                IF f
                    DW '&c'
                ENDIF
                f = 1
            ENDM
        else
            DW &arg
        endif
    ENDM
    DW 0
ENDM

.const
    strErrComFailed: WSTRING @----, 13, 10, @Press any key:, 13, 10

.code

maina PROC
    LOCAL hStdOutput:QWORD
    LOCAL hErrOutput:QWORD
    LOCAL hResult:DWORD

    sub     rsp,32
    and     rsp,not 15
    xor     ecx,ecx
    call    __imp_CoInitialize
    mov     hResult, eax

    mov     ecx, STD_OUTPUT_HANDLE
    call    __imp_GetStdHandle
    mov     hStdOutput, rax

    mov     ecx, STD_ERROR_HANDLE
    call    __imp_GetStdHandle
    mov     hErrOutput, rax

    lea     rcx,strErrComFailed
    call    __imp_wcslen
    mov     QWORD PTR [rsp+32], 0
    xor     r9d, r9d    ; lpNumberOfCharsWritten
    mov     r8d, eax    ; nNumberOfCharsToWrite
    lea     rdx,strErrComFailed
    mov     rcx,hStdOutput
    call    __imp_WriteConsoleW

    call    __imp__getch

    mov     ecx, eax
    call    __imp_ExitProcess
    ret 

maina ENDP

END

还有一些注意事项:


始终通过指针调用导入函数。所有这些指针名称都以 __imp_ 前缀开头。所以我们需要将导入的 api Xxx 声明为 EXTRN __imp_Xxx:QWORD - 这种更有效的比较 PROC 声明 - 在这种情况下,链接器需要使用单个 jmp 指令构建存根过程:

Xxx proc
    jmp __imp_Xxx
Xxx endp

当然最好直接 call __imp_Xxx 而不是 Xxx 存根,而是 call Xxx - 代码会更小更快


您不需要自己实现 wcslen - 您可以从 ntdllp.lib(总是)或 msvcrt.lib(非常依赖于具体的库实现,但是 msvcrt.dll 当然导出 wcslen - 你可以自己构建 msvcrt.lib 并使用 call __imp_wcslen


使用类似

的声明
strErrComFailed         dw 'C','O','M',' '...

非常不舒服。你可以使用例如这样的宏

    WSTRING macro text:VARARG
        FOR arg, <text>
            if @InStr( , arg, @ )
                f = 0
                FORC c,  <arg>
                    IF f
                        DW '&c'
                    ENDIF
                    f = 1
                ENDM
            else
                DW &arg
            endif
        ENDM
        DW 0
    ENDM


  ;strErrComFailed: WSTRING @----, 13, 10, @Press any key:, 13, 10

最后,函数 WriteConsoleW 中的 lpNumberOfCharsWritten 参数是可选的。如果您寻找声明是 sdk - 它是用 __out_opt_Out_opt_ 声明的。所以如果不需要这样的信息,你可以在这里传递 0