Ch 00 第一個 Win64 組合語言程式


64 位元的 CPU

中華民國 83 年 3 月,也就是西元 1994 年,英特爾推出具有 64 位元的資料匯流排的奔騰 ( Pentium ) 中央處理器,它與外部線路溝通的資料是以 64 位元的方式傳輸。當時有許多廣告宣稱它是 64 位元的 CPU ( 至今還是有些書仍然如此寫 )。明眼人一看便知這是障眼法!雖然 Pentium 具有 64 位元資料匯流排,但卻只有 32 位元的暫存器,每次運算也只能處理 32 位元的資料。Pentium 每次可以由匯流排接收 64 位元的資料,但得分兩次傳送資料。嚴格來說,Pentium 仍屬 32 位元的 CPU。

對程式設計師而言,Pentium 與 80386/80486 比較,Pentium 除了多出處理多媒體資料的指令集以及較為快速的運算速率之外,關於系統方面的指令,如定址模式、記憶體管理等,都與 80386/80486 相去不遠,一般我們都稱呼它們為 IA-32 ( Intel Architecture 32-bit ) 的 CPU,有時也稱為 i386、x86-32 或者 x86。除了 80386/80486/Pentium,就連後來英特爾出產的 CPU,如 Pentium Pro、Pentium II、Pentium III、Celeron、Pentium 4、Pentium M、Core、Celeron M、Celeron D 都屬於此一架構。美商超微也開發出許多種類的 IA-32 CPU,如 K5、K6、K6-2、K6-III、Duron、Athlon、Athlon XP、Athlon MP、Athlon XP-M 等。

IA-32 架構的 CPU,可以定址到 232 個位元組,亦即 4GB,相當於 4294967296 個位元組,相對於 8086 只能定址 1MB ( 亦即 1048576 ) 位元組,可以說是相當的大,但是面對資料庫、影音處理等的龐大的資料時,仍是力有未逮。於是英特爾與美國惠普公司 ( Hewlett-Packard ) 合作開始研發 64 位元的 CPU,後來在民國 90 年 6 月發表第一代 Itanium ( 中文名稱為『安騰』),後來又在次年發表 Itanium 2,這兩種 CPU 架構稱為 IA-64 ( Intel Architecture 64-bit ),表明是 64 位元架構的 CPU,它能一次處理 64 位元長度的資料,並能提供定址空間為 264(=18446744073709551616≒1.84×1019) 位元組。英特爾本來的計畫是捨棄行之多年的 IA-32 架構,以新的 IA-64 代替,但是 IA-64 與 IA-32 不相容;必須使用各種模擬來執行軟體,以這種模擬方式來執行的效率非常糟糕,市場反應並不好。

民國 91 年 ( 西元 2003 年 ),美商超微 ( AMD ) 在 IA-32 架構上發展了 64 位元的擴充,並命名為 AMD64,同時也先後發表了一系列的 AMD64 架構的 CPU,如 Athlon 64、Athlon 64 FX、Athlon 64 X2、Opteron、Sempron、Turion 64、Phenom。因為 AMD64 是在 IA-32 基礎上擴充為 64 位元,所以對舊有的軟體有很好的相容性,獲得各家廠商支持,包含微軟也為 AMD64 開發 Windows XP 64 Edition 作業系統。後來英特爾眼見競爭對手在 64 位元 CPU 上的成功,也推出了與之幾乎一模一樣架構的處理器,並命名為 IA-32e ( 亦即 IA-32 擴充之意 ),而後更名為 EM64T ( Extended Memory 64 Technology ) 或 Intel 64,現在不管是 AMD64 或 IA-32e、EM64T、Intel 64 都被統稱為 x86-64 或 x64 這種中性的名稱,表示不偏袒任何一家廠商。英特爾出產的 Xeon ( 自 Nocona 起的部分型號 )、Celeron D ( 自 Prescott 起的部分型號 )、Pentium 4 ( 自 Prescott 起的部分型號 )、Pentium D、Pentium Extreme Edition、Xeon ( Woodcrest )、Intel Core 2、Dual-Core、Celeron (自 Core 起的型號 )、Nehalem、Core i7、Intel Core i5 等都屬於 x64 架構。

在這堙A又見到廠商之間的競爭所得的好處。如果不是有超微的競爭,我想英特爾也不會卯足全力發展效率較好的 64 位元 CPU。然而,在作業系統等軟體上面的發展卻不是這樣,微軟一家獨大,即使 CPU 已經進入 64 位元將近十年了,64 位元的作業系統仍然不易使用,有太多的軟體無法順暢執行。假如有一家軟體公司,有足夠的規模能發展可與微軟匹敵的作業系統,我想微軟絕不敢掉以輕心,不重視 64 位元作業系統的發展。

牢騷發完,言歸正傳。到現在 ( 民國 100 年、西元 2011 年 ) 為止,64 位元的電腦系統可說已經完備,其中央處理器為支援 x64 架構的英特爾或超微所出產的主力 CPU;而作業系統可使用 64 位元的 Windows XP/Vista/7 ( 為了方便,這些 64 位於的作業系統稱之為 Win64,有別於 Win32 )。記憶體則可擴充至 4GB 以上,不再像 32 位元的作業系統那樣,即使電腦裝了超過 4GB 的記憶體,但是還是只能使用 4GB。應用軟體也逐漸走向 64 位元原生軟體,而不是像前幾年,得靠 WOW 才能在 64 位元作業系統上執行 32 位元程式。那麼小木偶最鍾愛的組合語言是否能使用 64 位元的 CPU 來撰寫程式呢?答案當然是可以的。底下小木偶就以撰寫 Windows 64 位元的組合語言程式為題,說明如何撰寫簡單的 Windows 64 位元組合語言程式。


x64 CPU 暫存器

x64 架構的 CPU 是屬於 64 位元,包含了 16 個 64 位元的通用暫存器 ( general-purpose registers ),這 16 個暫存器名稱分別是 RAX、RBX、RCX、RDX、RBP、RSP、RSI、RDI、R8、R9、R10、R11、R12、R13、R14、R15。很明顯的,後面的八個暫存器,R8、R9、R10、R11、R12、R13、R14、R15,是新增的;而前面的八個暫存器,RAX、RBX、RCX、RDX、RBP、RSP、RSI、RDI,是把原有的 32 位元加以擴充而成,RAX、RBX…等的「R」是暫存器 ( register )。如下圖所示:

64 位元通用暫存器

雖然是在 64 位元系統中,但是還是可以使用 32、16、8 位元的暫存器。如上圖所示,EAX、EBX、ECX、EDX、ESI、EDI、EBP、ESP 等 32 位元的暫存器仍然可以使用;而新增加的 32 位元暫存器名為 R8D、R9D、R10D、R11D、R12D、R13D、R14D、R15D,暫存器名結尾的『D』是指雙字組 ( DWORD )。16 位元的暫存器也有十六個,分別是舊的 AX、BX、CX、DX、DI、SI、BP、SP 與新增的 R8W、R9W、R10W、R11W、R12W, R13W、R14W、R15W,這堛滿yW』,顯然就是字組 ( WORD ) 之意。可用的 8 位元暫存器也有十六個,分別是舊有的 AL、BL、CL、DL 與新增的 SIL、DIL、BPL、SPL、R8B、R9B、R10B、R11B、R12B、R13B、R14B、R15B,這堛滿yB』是位元組 ( BYTE ) 的意思,而『L』是指低位元組之意。


獲得 64 位元組合語言組譯器、連結器、匯入程式庫以及除錯器

用組合語言撰寫 Win 64 程式必須要有組譯器、連結器、匯入程式庫、除錯器、參考資料等工具或資料才能做到。在網際網路 ( internet ) 上,有許多 64 位元的組譯器可供使用,如 GoASM、Yasm,但是語法上與微軟的巨集組譯器 MASM 有些差異,但是因為小木偶使用微軟組譯器已有相當久的時間,所以還是較鍾情於微軟組譯器。微軟 64 位元的組譯器已更名為 ML64.EXE,在 Visual C++ 2005 安裝完後,可以得到 8.0 版的 ML64.EXE。但是現在已可以由『Windows SDK for Windows Server 2008 and .NET Framework 3.5』得到 9.0 版的 ML64.EXE。

組譯器、連結器、匯入程式庫

下面說明取得 9.0 版的 ML64.EXE、LINK.EXE 等檔案的過程。首先,到微軟下載中心下載Windows SDK for Windows Server 2008 and .NET Framework 3.5,這是一個檔名為 6.0.6001.18000.367-KRMSDK_EN.iso 的 DVD 光碟影像檔 ( ISO 格式 ),大小為 1394618368 個位元組。在這個光碟影像檔中似乎至少包含著兩種版本號碼相同的 ML64.EXE,這兩種 ML64.EXE 中,一種能在 Win32 或 Win64 作業系統下均能執行;另一種只能在 Win64 系統下執行。前者 ML64.EXE 的格式是 Win32 系統的可執行檔格式,也就是 PE ( Win32 Portable Executable File Format ) 格式,故能在 Win32 或 Win64 環境下組譯連結原始碼。這是因為 Win64 系統為了向下相容,所以當然能執行 Win32 系統的執行檔。後者的 ML64.EXE 是 Win64 可執行檔格式,也就是 PE+ 格式,所以只能在 Win64 系統中執行。

不管是哪一種 ML64.EXE,製造出來的可執行檔都是只能在 64 位元的 Windows 作業系統中執行 ( Win32 應用程式的可執行檔格式為 PE,而 Win64 則為 PE+ )。小木偶以 UltraISO 等虛擬光碟軟體載入此影像檔,自 6.0.6001.18000.367-KRMSDK_EN.iso 壓縮檔中萃取所需檔案:

可在 Win32/Win64 中執行的組譯器 ( PE 格式 )
 所在壓縮檔及壓縮檔內檔名 更改後檔名檔案大小
( bytes )
版本
組譯器在『\Setup\vc_stdx86.cab』壓縮檔內,壓縮檔內的檔名為
FL_ml64_exe_93735_93735_x86_ln.3643236F_FC70_11D3_A536_0090278A1BB8
ML64.EXE305656 9.00.21022.08
連結器在『\Setup\vc_stdx86.cab』壓縮檔內,壓縮檔內的檔名為
FL_link_exe_10395_x86_ln.3643236F_FC70_11D3_A536_0090278A1BB8
註:事實上,vc_stdx86.cab 內有三個檔案:
  FL_link_exe_10395_x86_ln.3643236F_FC70_11D3_A536_0090278A1BB8、
  FL_link_exe_74300_74300_x86_ln.3643236F_FC70_11D3_A536_0090278A1BB8、
  FL_link_exe_74622_74622_x86_ln.3643236F_FC70_11D3_A536_0090278A1BB8,
  這三個檔案內容都相同,所以只需把任何一個解壓縮就可以了。如果你問我為何會這樣,小木偶也不知道。
LINK.EXE790008 9.00.21022.08
匯入程式庫『\Setup\WinSDKBuild-WinSDKBuild_VistaLibs_X64-common.0.cab』壓縮檔內,解壓縮三個檔案:
  Gdi32_Lib.3F64FF45_F00B_4275_8B18_2A475F407315
  Kernel32_Lib.D67E3FC5_0F35_46D3_93B2_574E8F2EB908
  User32_Lib.BEF925DB_25A9_404B_8F26_7FD4969DD408

 GDI32.LIB
 KERNEL32.LIB
 USER32.LIB

140458
237182
137848
9.00.21022.08
其他LINK.EXE 還須 MSPDB80.DLL 才能使用,故還得解壓縮『\Setup\vc_stdx86.cab』壓縮檔內的『FL_mspdb71_dll_2_60032_x86_ln.3643236F_FC70_11D3_A536_0090278A1BB8』。此處看起來應該更改成 MSPDB71.DLL 才對,但是小木偶改成 MSPDB80.DLL 仍可正常執行 LINK.EXE。 MSPDB80.DLL193024 9.00.21022.08
只能在 Win64 中組譯 ( PE+ 格式 )
 所在壓縮檔及壓縮檔內檔名 更改後檔名檔案大小
( bytes )
版本
組譯器在『\Setup\vc_stdamd64.cab』壓縮檔內,壓縮檔內的檔名為
FL_ml64_exe_105147_105147_amd64_ln.3643236F_FC70_11D3_A536_0090278A1BB8
ML64.EXE383480 9.00.21022.08
連結器在『\Setup\vc_stdamd64.cab』壓縮檔內,壓縮檔內的檔名為
FL_link_exe_105145_105145_amd64_ln.3643236F_FC70_11D3_A536_0090278A1BB8
LINK.EXE1045496 9.00.21022.08
匯入程式庫『\Setup\WinSDKBuild-WinSDKBuild_VistaLibs_X64-common.0.cab』壓縮檔內,解壓縮三個檔案:
  Gdi32_Lib.3F64FF45_F00B_4275_8B18_2A475F407315
  Kernel32_Lib.D67E3FC5_0F35_46D3_93B2_574E8F2EB908
  User32_Lib.BEF925DB_25A9_404B_8F26_7FD4969DD408

 GDI32.LIB
 KERNEL32.LIB
 USER32.LIB

140458
237182
137848
9.00.21022.08
其他LINK.EXE 還須 MSPDB80.DLL、MSOBJ80.DLL、MSPDBCORE.DLL、MSPDBSRV.EXE、CVTRES.EXE 五個檔案才能正常使用,這五個檔案都在『\Setup\vc_stdamd64.cab』壓縮檔內,分別是:
FL_mspdb80_dll_107301_107301_amd64_ln.3643236F_FC70_11D3_A536_0090278A1BB8
FL_msobj80_dll_110565_110565_amd64_ln.3643236F_FC70_11D3_A536_0090278A1BB8
FL_mspdbcore_dll_107302_107302_amd64_ln.3643236F_FC70_11D3_A536_0090278A1BB8
FL_mspdbsrv_exe_105148_105148_amd64_ln.3643236F_FC70_11D3_A536_0090278A1BB8
FL_cvtres_exe_105140_105140_amd64_ln.3643236F_FC70_11D3_A536_0090278A1BB8
FL_link_exe_config_105649_105649_amd64_ln.3643236F_FC70_11D3_A536_0090278A1BB8


MSPDB80.DLL
MSOBJ80.DLL
MSPDBCORE.DLL
MSPDBSRV.EXE
CVTRES.EXE
LINK.EXE.CONFIG


235520
97280
396800
132096
38904
268
9.00.21022.08
資源編譯器在『\Setup\WinSDKWin32Tools-WinSDKWin32Tools-amd64.0.cab』壓縮檔內,壓縮檔內的檔名為
  RC_Exe.F3B41A95_99F3_461D_8A54_982E2DAD928A
  RcDll_Dll.F3B41A95_99F3_461D_8A54_982E2DAD928A

RC.EXE
RCDLL.DLL

67944
394600

6.0.5724.0

自 6.0.6001.18000.367-KRMSDK_EN.iso 可以取得兩種不同環境下的組譯器,分別是在 Win32 或 Win64 中組譯。假如您想在 Win32 系統中組譯,那麼您得萃取上表中,上半部的檔案;如果您打算在 Win64 系統中組譯,那麼得萃取上表中,下半部的檔案。小木偶的電腦是 DOS、Win XP 32bits Edition、Win XP 64bits Edition、Win 7 Ultimate 64bits Edition 以及 Unbuntu 9.04 多重啟動,在大部分時間是在 Win64 中組譯,小木偶把上表中上半部的組譯器、連結器 ( ML64.EXE、LINK.EXE、MSPDB80.DLL 等 ) 放在『C:\MASM64\BIN\x32』子目錄堙F把上表中下半部的組譯器、連結器放在『C:\MASM64\BIN\x64』子目錄堙F把三個匯入程式庫 GDI32.LIB、KERNEL32.LIB、USER32.LIB 放在『C:\MASM64\LIB』子目錄 ( 不知為何匯入程式庫不是檔名為 GDI64.LIB、KERNEL64.LIB、USER64.LIB,仍然與 Win32 相同 )。在 Win 7 64bits Edition 系統中,把下面的內容存成『C:\Documents and Settings\使用者名稱\WIN64ASM.BAT』檔案:

SET PATH=C:\MASM64\BIN\x64;%PATH%
SET LIB=C:\MASM64\LIB
SET ML=/link /SUBSYSTEM:WINDOWS

第一行,是當作業系統找不到執行檔時,會到 PATH 所指定的子目錄搜尋。一般而言,小木偶把原始程式 ( 副檔名為 *.ASM ) 存在 E:\HomePage\SOURCE\Win64 或此子目錄底下的孫目錄,而小木偶也希望所組譯、連結後的可執行檔也儲存在與原始程式同一目錄,故得切換到此目錄。此目錄不含有 ML64.EXE、LINK.EXE 等檔案,故以『SET PATH』指定搜尋路徑。在組譯完成後,ML64.EXE 會自動執行 LINK.EXE,也須設定搜尋路徑,否則系統找不到連結器,就無法產生可執行檔。第二行,是指定匯入程式庫所在位置。第三行是設定 ML 環境變數,指定 ML64.EXE 把『/SUBSYSTEM:WINDOWS』參數傳給 LINK.EXE。在 Win XP 32bits Edition 系統堙A則是把上面的 WIN64ASM.BAT 稍作修改:一是把磁碟機代號改成在 Win XP 中所見的磁碟機代號,二是把『SET PATH=C:\MASM64\BIN\x64;%PATH%』改成『SET PATH=H:\MASM64\BIN\x32;%PATH%』。這樣一來,小木偶在 Win 32 環境或 Win 64 環境都可以組譯。

除錯器

目前 ( 民國 100 年、西元 2011 年 ) 能在 64 位元中執行的除錯器並不多,也不太好用。OllyDebug 還未能支援 x64 指令集,Soft-ICE 似乎已停止發展了,其他還有好幾個除錯器,如 fdbg、AutoDebug 等可供選擇。但小木偶選擇微軟的 WinDbg,原因無他,因為有微軟的支援。WinDbg 可以到微軟的 Debugging Tools for Windows 64-bit Version 網頁下載,這個網頁的 WinDbg 有兩種版本,支援 IA64 與 x64,大部分的人應該下載 x64 版本,到目前為止,最新的版本是 6.11.1.404。下載完成後,解壓縮可得 dbg_amd64_6.11.1.404.msi ( 大小為 15953408 個位元組 ),把滑鼠游標移到此檔圖示上,以滑鼠雙擊該圖示即可開始安裝,安裝過程一如大部分軟體,無庸多說。


最簡單的 Win64 組合語言程式

底下,小木偶就示範如何撰寫一個可執行在 Win64 作業系統的原生組合語言程式,先把底下這段程式存成 『E:\HomePage\SOURCE\64_HelloWorld\HELLOW.ASM』檔案:

EXTRN           MessageBoxA:PROC
EXTRN           ExitProcess:PROC

INCLUDELIB      kernel32.lib
INCLUDELIB      user32.lib

MB_OKCANCEL     EQU     1
;*******************************************************************************
.DATA
szTitle BYTE    '最簡單的程式',0
szText  BYTE    '這是在 Windows 64 位元作業系統,',0dh,0ah
        BYTE    '用組合語言寫的程式。',0
;*******************************************************************************
.CODE
;-------------------------------------------------------------------------------
Main    PROC
        sub     rsp,28h
        mov     r9,MB_OKCANCEL
        mov     r8,OFFSET szTitle
        mov     rdx,OFFSET szText
        sub     rcx,rcx
        call    MessageBoxA
        add     rsp,28h
        ret
Main    ENDP
;*******************************************************************************
        END

組譯 64 位元的組合語言原始碼

假如您依照小木偶的方法安排組譯環境,接下來就可以組譯了。首先以滑鼠點選 Windows 系統左下角的『開始』→『所有程式』→『附屬應用程式』→『命令提示字元』,輸入以下指令:( 黃字是您必須輸入的 )

C:\Documents and Settings\使用者>win64asm [Enter]

C:\Documents and Settings\使用者>SET PATH=C:\MASM64\BIN\x64;C:\WINDOWS\system32;C:\W
INDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\system32\WindowsPowerShell\v1.0;C:\Pr
ogram Files\Inventec\Dreye\DreyeSA\DreyeTTs\eTTS\

C:\Documents and Settings\使用者>SET LIB=C:\MASM64\LIB

C:\Documents and Settings\使用者>SET ML=/link /SUBSYSTEM:WINDOWS

C:\Documents and Settings\使用者>e: [Enter]

E:\>cd HomePage\SOURCE\64_HelloWorld [Enter]

E:\HomePage\SOURCE\64_HelloWorld>ml64 hellow.asm /link /entry:Main [Enter]
Microsoft (R) Macro Assembler (x64) Version 9.00.21022.08
Copyright (C) Microsoft Corporation.  All rights reserved.

 Assembling: hellow.asm
Microsoft (R) Incremental Linker Version 9.00.21022.08
Copyright (C) Microsoft Corporation.  All rights reserved.

/OUT:hellow.exe
hellow.obj
/SUBSYSTEM:WINDOWS
/entry:MAIN

E:\HomePage\SOURCE\64_HelloWorld>

如果在 32 位元的 Windows 之下執行 hellow.exe,可得下圖左的視窗;但是如果是在 64 位元的 Windows 下執行,就可以看見下圖右的視窗。

HELLOW in Win32
這是在 Windows XP Pro 32 Edition 執行的結果
HELLOW in Win64
這是在 Windows 7 Ultimate 64 Edition 執行的結果

64 位元組合語言應注意事項

以現階段 ( 中華民國 100 年,西元 2011 年 ) 來說,要以組合語言撰寫 64 位元的程式仍然是很麻煩的,雖然目前有幾個團隊正在努力撰寫可用的包含檔,但尚未完成。除此之外,還有一個隱憂,微軟 64 位元的組譯器,ML64.EXE 無法使用 INVOKE、.IF/.ELSEIF/.ENDIF、.WHILE/.ENDW 等高階的假指令,所以使用 ML64.EXE 得全由手工打造。不像使用 MASM 6.x 組譯 Win32 程式,可以使用這些假指令。這是 Win64 與 Win32 第一個不同之處。

Win64 的呼叫協定

第二個不同之處是『呼叫協定 ( calling convention )』。在 Win32 組合語言中,呼叫 Win API 是以堆疊傳遞參數,且最右邊的參數先推入堆疊,傳回值存於 EAX 暫存器堙A並且由被呼叫的一方 ( 即副程式或 API,像這種被呼叫的程式稱為 callee ) 清除堆疊,此種呼叫協定稱為『STDCALL』。但是在 Win64 卻有底下不同:

  1. 參數的傳遞並非完全靠堆疊,而是靠暫存器以及堆疊;精確的說,前四個參數依序放在 ECX、EDX、R8、R9 暫存器堶情A第五個以後的參數才放在堆疊堙C
  2. 主程式 ( 或稱為呼叫者,caller ) 必須為副程式或 API ( 或稱為被呼叫者,callee )準備好足夠的堆疊空間,以容納這些參數。每個參數佔用四字組 ( QWORD ),亦即 8 個位元組 ( BYTE )。
  3. 即使前四個參數靠暫存器傳遞給副程式或 API,但是主程式仍然得準備 32 個位元組的堆疊空間,以保留給前四個參數存入;甚至連不到四個參數的 API 也得在堆疊準備 32 個位元組的空間!假如您沒有準備好足夠的堆疊空間,程式必定崩潰。
  4. 當程式由副程式返回時,由主程式 ( caller,呼叫者 ) 負責清除堆疊,而不是副程式或 API 清除堆疊。
  5. 堆疊框必須對齊一個節 ( paragraph ),每個節的大小是 16 個位元組,即十六進位的 10H。換句話說,堆疊框必須要在堆疊位址的 WWWWXXXXYYYYZZZ0H 處。或者說,堆疊框所在的堆疊位址必須要能被 10H 整除。

這種呼叫協定稱為『FASTCALL』( 有些文獻是說類似 FASTCALL )。小木偶想,舉個例子來說明,可能會清楚一點。例如要呼叫 MessageBox API 時,在 Win32 底下為:

        INVOKE  MessageBox,hWnd,OFFSET lpText,OFFSET lpCaption,MB_OKCANCEL

一行就解決了,但是在 Win64 堙A卻變成

        mov     r9,MB_OKCANCEL
        mov     r8,OFFSET lpCaption
        mov     rdx,OFFSET lpText
        mov     rcx,hWnd
        call    MessageBoxA

此處的 hWnd、lpText、lpCaption、uType 分別是父程式的視窗代碼、顯示在視窗內文的字串起始位址、視窗標題的字串起始位址、視窗的按鈕形式。在 Win32 中,前輩,Steve Hutchesson,所整理的 MASM32 開發工具,已經把 MB_OKCANCEL 及 MessageBoxA 分別在 WINDOWS.INC 及 USER32.INC 中定義好了,我們只需把它們包含進來就可以了。想想,我們不得不佩服與尊敬 Steve Hutchesson,他給我們造就了這麼大的方便。但是在開發 Win64 程式時,就沒有適合的包含檔了,所以得在原始碼中自行定義 MB_OKCANCEL 及 MessageBoxA。

除此之外,眼尖的讀者應該注意到在程式碼一開始及接近結尾處,有兩行奇怪的指令:

        sub     rsp,28h
        ……
        add     rsp,28h

字面上很容易理解,這是把堆疊指標,RSP 暫存器,減去 28H 個位元組,以及加上 28H,但是為什麼要這樣做呢?原因是我們必須在主程式保留足夠的空間給參數使用,並且使堆疊框對齊 16 位元組,底下說明這個過程。請看下圖一,當系統把控制權交到 hellow.exe 時,也就是進入 Main 主程式時,系統會把返回系統的位址存入堆疊的 12FF58H 處 ( 系統返回位址必存在堆疊位址個位數是 8 的位址上 ),此時 RSP 也指向堆疊的這個位址,而下一個空的堆疊 ( 其位址較低 ) 就是 Main 的堆疊框起始位址,但這時並不知道 Main 會呼叫哪些副程式或 API,因此小木偶在圖一上的堆疊框並沒有畫出終點位址。爾後 Main 會呼叫 MessageBoxA API,這時會把返回 Main 的位址存到堆疊頂端 ( 堆疊 12FF28H 處 ),而 MessageBoxA 有四個參數,每個參數佔 8 個位元組,因此須保留 32 個位元組 ( 32=20H ) 給這四個參數,所以堆疊頂端為返回主程式的位址 ( 堆疊 12FF28H 處 ),接下來比此返回位址高的四個四字組分別給 MessageBoxA 儲存參數用,四個參數加一個返回位址共 28H 個位元組。在 Win64 組合語言中,雖然前四個參數以暫存器傳遞,但是還是得保留堆疊空間,給副程式儲存這四個參數,如圖二、三所示,此時所建立的堆疊框由 12FF50 開始,是對齊節的邊界,並且到 12FF28 為止。若 MessageBoxA API 也呼叫其他 API 的話,MessageBoxA 會把所需的堆疊框建立在 12FF20H 處,也會對齊節的邊界。如圖三所示。


為何 RSP 要減去 28H 而不是減去 20H 或其他數呢?請看上圖四,我們可以想像,如果是減去 20H,那麼 MessageBoxA 內部所建立的堆疊框就會由 12FF28H 開始,這樣就沒有對齊節的邊界,這會引發當機,因此圖四的減去 20H 是錯誤的。故在堆疊上不能只保留 20H 個位元組,必須再多減掉一個四字組才行。這樣一來,雖然堆疊中會有一個四字組的空間沒有使用,但是既能保留足夠的堆疊空間給四個參數,也能對齊節邊界。如果 RSP 減去 38H、48H 或 58H 是否可行呢?您可以試試!

從上面的解說,應該可以知道,假如呼叫的副程式或 API 有四個參數,所保留的堆疊為 28H 個位元組;如果有五個參數,所保留的堆疊也是 28H 個位元組,因為四個參數時會多出一個四字組未使用,恰好就可以給第五個參數使用。如果呼叫的副程式有六個參數,那麼就應該保留 38H;如果有七個參數,也應保留 38H。換句話說,在堆疊上所保留的空間大小為 16n+8,n 與參數個數有關,如下表所示:
表一:參數個數與 RSP 保留大小
參數個數RSP 所應保留位元組大小 參數個數RSP 所應保留位元組大小
4 或小於 428H 1058H
528H 1158H
638H 1268H
738H 1368H
848H 1478H
948H 1578H

Win64 組合語言中,主程式得為副程式準備參數傳遞的空間並且清除堆疊,所以假使一個主程式會呼叫好幾個副程式時,那麼只需考慮所需參數最多的副程式即可。通常只需在主程式的最前面使 RSP 減掉足夠的數,並能對齊節邊界,而結束主程式時再使 RSP 加上該數即可,中間很少再改變 RSP。為了再更清楚的說明『呼叫協定』並且說明參數所需空間,小木偶想再舉一個例子,hellow1.asm。它會呼叫一個計算七個整數和的副程式,addition,再把計算結果用 MessageBoxA 顯示於螢幕上。底下是 hellow1 執行畫面:

底下是 hellow1.asm 的原始碼:

OPTION          CASEMAP:NONE
EXTRN           MessageBoxA:PROC
INCLUDELIB      user32.lib
MB_OK           EQU     0
;*******************************************************************************
.DATA
a1      QWORD   123456789abcdef0h
a2      QWORD   7777555533331111h
a3      QWORD   1444144414441444h
a4      QWORD   0f130f130f130f13h
a5      QWORD   22223333bbbb9999h
a6      QWORD   0000000000000001h
a7      QWORD   0000000000000002h
sum     QWORD   0                       ;0cf250258ad02acf4h
n       QWORD   0
szTitle BYTE    '和',0
szText  BYTE    180 DUP (0)
;*******************************************************************************
.CODE
;-------------------------------------------------------------------------------
Sum     PROC
        mov     rax,rcx         ;存入第一個參數
        add     rax,rdx         ;加上第二個參數
        add     rax,r8          ;加上第三個參數
        add     rax,r9          ;加上第四個參數
        add     rax,[rsp+28h]   ;加上第五個參數
        add     rax,[rsp+30h]   ;加上第六個參數
        add     rax,[rsp+38h]   ;加上第七個參數
        ret
Sum     ENDP
;-------------------------------------------------------------------------------
;把RCX內的十六進位數轉換成字串,存在RDX所指的位址
;輸入:RCX=十六進位數
;   RDX=字串位址
;輸出:RAX=填入最後一個ASCII字元的位址再加一
RCX_To_String   PROC
        mov     [rsp+08h],rcx   ;在堆疊中存入第一個參數
        mov     [rsp+10h],rdx   ;在堆疊中存入第二個參數
        mov     r8,16
nxt:    rol     rcx,4
        mov     al,cl
        and     al,0fh
        add     al,'0'
        cmp     al,'9'
        jbe     ok
        add     al,7
ok:     mov     [rdx],al
        inc     rdx
        dec     r8
        jnz     nxt
        mov     BYTE PTR [rdx],'H'
        mov     rax,rdx
        inc     rax
        ret
RCX_To_String   ENDP
;-------------------------------------------------------------------------------
Start   PROC
        sub     rsp,38h
        mov     rax,a7
        mov     [rsp+30h],rax   ;第七個參數
        mov     rax,a6
        mov     [rsp+28h],rax   ;第六個參數
        mov     rax,a5
        mov     [rsp+20h],rax   ;第五個參數
        mov     r9,a4           ;第四個參數
        mov     r8,a3           ;第三個參數
        mov     rdx,a2          ;第二個參數
        mov     rcx,a1          ;第一個參數
        call    Sum
        mov     sum,rax

        mov     rax,OFFSET szText
nxt:    mov     r8,n            ;n=第幾個數
        shl     r8,3            ;每個四字組佔據8個位元組,2的立方=8
        mov     r9,OFFSET a1    ;每個數的位址在a1位址再加上8*第幾個數
        add     r8,r9
        mov     rdx,rax
        mov     BYTE PTR [rdx],' '
        inc     rdx
        mov     rcx,[r8]
        call    RCX_To_String
        inc     n
        mov     WORD PTR [rax],0a0dh    ;換行
        add     rax,2
        cmp     n,7
        jne     nxt

        mov     BYTE PTR [rax-20],'+'
        mov     rcx,18                  ;印出一條線
line:   mov     BYTE PTR [rax],'-'
        inc     rax
        loop    line
        mov     WORD PTR [rax],0a0dh    ;換行
        add     rax,2
        mov     rdx,rax
        mov     rcx,sum
        call    RCX_To_String           ;印出和

        mov     r9,MB_OK
        mov     r8,OFFSET szTitle
        mov     rdx,OFFSET szText
        sub     rcx,rcx
        call    MessageBoxA
        add     rsp,38h
        sub     rax,rax
        ret
Start   ENDP
;*******************************************************************************
        END

HELLOW1.ASM 的主程式,Start,會呼叫兩個副程式及一個 API,這兩個副程式是 SumOf 和 RCX_To_String,前者是計算七個整數的和,需要輸入七個參數;後者是把 RCX 暫存器之數值變成 ASCII 字串存在 RDX 所指定的位址,需要兩個參數。此外,Start 還會呼叫 MessageBoxA API,它需要四個參數。綜合所呼叫的副程式或 API,得知 SumOf 所需參數最多,所以 Start 只需針對七個參數準備所需堆疊即可,因此您可以在 Start 副程式的開始與結束看到

        sub     rsp,38h
        …      ……
        add     rsp,38h

為什麼要保留 38H 個位元組呢?下圖五是剛進入 Start 主程式時的堆疊情形,在堆疊 12FF58H 位址的返回位址是 hellow1 結束後,返回到系統的位址,而 RSP 也是指向這個位址。接下來是保留 38H 的堆疊空間給參數使用,12FF58H 減去 38H 是 12FF20H,故 RSP 變為 12FF20H。在接下來的幾行:

        mov     rax,a7
        mov     [rsp+30h],rax   ;第七個參數
        mov     rax,a6
        mov     [rsp+28h],rax   ;第六個參數
        mov     rax,a5
        mov     [rsp+20h],rax   ;第五個參數

是把第五、六、七個參數移到堆疊堙C由圖六來看,第五、六、七個參數分別放在位址 RSP+20H、RSP+28H、RSP+30H,因此小木偶利用 MOV 指令把參數存入適當堆疊位址。接下來的四行是把第四、三、二、一個參數分別存在 R9、R8、RDX、RCX 暫存器中。下一個指令是『call SumOf』,CALL 會使 CPU 的 RSP 減去 8 個位元組,再把返回位址 ( 圖七以黃色標明的返回位址 ),即『mov sum,rax』所在位址,存入堆疊,並且使 RIP 指向 SumOf 副程式所在位址,於是便進入 SumOf 副程式了。SumOf 副程式的堆疊框會建立在 12FF10H 處,對齊節邊界。

在 SumOf 副程式堶n讀取七個參數並使它們相加,前四個參數在暫存器中,但是後三個參數必須到堆疊中取出。由於 RSP 所指位址在進入 SumOf 的前後不同,因此您可以看到第五個參數是在 RSP+28H ( 在主程式,Start,把第五個參數存入堆疊時,是用「mov [rsp+20h],rax」在副程式加上第五個參數是用「add rax,[rsp+28h]」),如上圖七與下面程式片段:

        add     rax,[rsp+28h]   ;加上第五個參數
        add     rax,[rsp+30h]   ;加上第六個參數
        add     rax,[rsp+38h]   ;加上第七個參數

一般而言,如果副程式功能很多,那麼很可能會用到許多暫存器,這時候就需要把前四個參數存放到堆疊堣F,這大概也就是要在堆疊堳O存前四個參數的原因了。不過 SumOf 副程式很簡單,並不需要把這四個參數移到堆疊堙C即使這樣,小木偶還是遵循 Win64 程式的寫法。接下來執行 ret 退出 SumOf 副程式,返回到主程式中。這時候 RSP 便加上 8,指到堆疊位址 12FF20 處,這時堆疊的使用情形就如同前面還沒有執行 SumOf 前的情形一樣,唯一不同的是堆疊已有足夠的空間可容納 RCX_To_String 副程式的參數,如上圖八。

RCX_To_String 要執行八次,前七次是把要做加法的七個數變成 ASCII 字元,存於 RDX 所指的位址;最後一次則是把和變成 ASCII 字串。每次在呼叫 RCX_To_String 前,堆疊情形都如上圖八,堆疊堛 RCX、RDX 只是預留存入參數的空間。進入此副程式時,RSP 會減去 8,就如上圖七,指向 12FF18H,只是堆疊位址的 12FF30∼12FF50H 都沒用到,當退出此副程式時又恢復變成圖八的樣子。在 RCX_To_String 副程式中,小木偶展示了把參數存在副程式的方法,即第一個參數存在 RSP+8H 處,第二個參數存在 RSP+10H 處,雖然在 hellow1 程式中並沒有用,但是在其他複雜的副程式,如果要儲存參數,應該就是這樣做了。

由上面的說明,在 Win64 組合語言堙A堆疊的使用情形大致可以說是這樣的:呼叫前,把前四個參數存入 RCX、RDX、R8、R9 暫存器堙A第五個參數存於 RSP+20H、第六個參數存於 RSP+28H……堙A如下圖左。執行 CALL 指令後,就進入了副程式,如有必要應先把第一個參數存到 RSP+8h,第二個參數存到 RSP+10h……。這時可分為兩種情形,第一種情形比較單純,這個副程式為 leaf function ( leaf function 是指不呼叫其他副程式或 API 的副程式,就如同 hellow1 堛 SumOf 或 RCX_To_String ),RSP 不需要再調整,所以假如要讀取第五個參數,得讀取 RSP+28h,要讀取第六個參數,得讀取 RSP+30h;此外也可以設定區域變數,第一個區域變數在 RSP-8h,第二個區域變數在 RSP-10h,如下圖右:


第二種情形是此副程式為 non leaf function ( 表示這種副程式會呼叫其他副程式或 API,就如同 hellow1 中的 Start 主程式 ),RSP 必須再減去 16n+8 個位元組,以容納參數。在這種情形之下,如果要在堆疊中保存某些暫存器,或是有區域變數時,RSP 又必須再減去若干位元組,但無論如何,總是要設法使堆疊框在節邊界的位址上對齊。

撰寫 Win64 組合語言其他注意事項

我想組合語言撰寫 Win64 程式,最不易了解的應該就是上面所描述的呼叫協定,除此之外還有一些末節需要遵守:

  1. 副程式內可以改變 RAX、RCX、RDX、R8、R9、R10、R11 等七個暫存器之值,稱這些暫存器為 volatile;而暫存器 RBX、RBP、RSI、RDI、R12、R13、R14、R15 則不可更改,稱為 non-volatile,如果一定要使用 non-volatile 暫存器,應該進入副程式後,先存於堆疊。
  2. 通常參數長度應為一個四字組 ( QWORD,即 64 位元 ),如果不到 64 位元,較高的雙字組不用,一般設為零。

以 WinDbg 除錯

WinDbg 可以使用原始碼的變數名稱、副程式名稱,如果要這樣做的話,必需在組譯時把這些資料寫進 PDB 檔內,若以組譯 hellow.asm 得用下面的方法組譯:

ml64 /Zi hellow.asm /link /entry:Main

這樣的話,就會製造出 HELLOW.PDB 檔案。開啟 WinDbg 後,以滑鼠選擇彈出選單『File』、『Open Executable』( 也可以直接按快捷鍵,Ctrl-E ),如下圖所示,接下來會跳出一個對話盒,選擇 HELLOW.EXE 檔即可:

當選好 HELLOW.EXE 後,就會產生兩個子視窗『Command』視窗以及原始碼視窗,如下圖。Command 視窗可分為三部份,右下角的紅框部份是往後的除錯工作時輸入命令所在,例如:指定中斷點、單步追蹤、執行……等都在這個紅框輸入命令。您可以看到這個視窗的左下角用綠框框住,有『0:000』這樣的字樣,最左邊的『0』代表『程式』(process) 編號,右邊的『000』代表『執行緒』編號。『Command』視窗的最大部分,用藍框框住的是輸出的結果。一般來說,Win64 程式的程式碼由位址 140001000H 處開始,因此我們可以在欄框部份輸入『u 140001000』指令,並按下『Enter』鍵,就能夠觀察我們所寫的程式碼。WinDbg 的指令與以前 DOS 的 Debug 指令有部份相同,對小木偶而言,可說是駕輕就熟。這個『u』指令是『unassembly』的意思,亦即反組譯。
我們也可以以滑鼠拖曳 Command 視窗到 WinDbg 視窗上沿,這樣的話,Command 視窗就會與原始碼視窗分割 WinDbg 視窗,而不會互相遮掩。在除錯過程,我們也很希望能見到其他資料,如暫存器的變化,可以到『View』『Registers』叫出暫存器視窗,如下圖:
我們可以如法炮製,把滑鼠游標移到這些子視窗的標題欄,然後按住滑鼠左鍵不放,拖曳這些子視窗到 WinDbg 的上沿或左、右沿來調整這些子視窗位置;也可以把滑鼠游標移到子視窗邊線上,如下圖紫色線上,按住左鍵不放,調整他們的大小,如下圖:
上圖的 Command 視窗中,以藍框框住的部份,顯示了剛剛我們反組譯的結果,現在讓我們在紅框的地方輸入指令『u 140001010』,按下『Enter』鍵,就可以在下圖藍框框住的部份看到真正的程式碼。照理來說,程式碼應該是在 140001000H 處,但是因為加上了除錯資料,所以程式碼位址在 140001010H,而藍框的部份是組譯器自行加上去的。我們可以由上圖中,綠框的部份知道,真正的程式碼是在 140001010H 處,所以我們接著輸入『g 140001010』,表示使 WinDbg 執行到位址 140001010H 處停止,如下圖
輸入完『g 140001010』後別忘記按下『Enter』鍵,就會看到下圖的結果,注意到原始碼子視窗的『Main PROC』變成藍底白字,表示 WinDbg 執行到這兒了;而暫存器子視窗的某些暫存器變紅字,表示這些暫存器的數值更動了,如下圖:
接下來,按下快捷鍵『F8』,『F8』鍵表示單步追蹤,亦即每執行一個指令就停下來,或者也可以在 Command 子視窗輸入『t』( trace ),WinDbg 執行『sub rsp,28h』這道指令,在下圖,可以見到 RSP 暫存器變成 12FF30H 了:
其他的部分,聰明的您可一一試探!


區域變數

在 Win64 組合語言副程式中,是否可以用 LOCAL 宣告區域變數呢?答案是可以的。使用時 LOCAL 必定要跟在 PROC 假指令之後,其語法與 MASM 6.x 一樣:( 可以參考 Win32 組合語言第三章有關區域變數的部份 )

LOCAL   變數名[重複次數]:資料型態

小木偶把上面的 HELLOW1.ASM 的主程式,Start,中的兩個全域變數,n 與 sum,改成區域變數,原始碼變成 HELLOW2.ASM。底下以白色字標示的部份就是宣告區域變數的地方:

OPTION          CASEMAP:NONE
EXTRN           MessageBoxA:PROC
INCLUDELIB      user32.lib
MB_OK           EQU     0

;*******************************************************************************
.DATA
a1      QWORD   123456789abcdef0h	;第一個參數
a2      QWORD   7777555533331111h	;第二個參數
a3      QWORD   1444144414441444h	;第三個參數
a4      QWORD   0f130f130f130f13h	;第四個參數
a5      QWORD   22223333bbbb9999h	;第五個參數
a6      QWORD   0000000000000001h	;第六個參數
a7      QWORD   0000000000000002h	;第七個參數
szTitle BYTE    '和',0
szText  BYTE    180 DUP (0)
;*******************************************************************************
.CODE
;-------------------------------------------------------------------------------
Sum     PROC
        mov     rax,rcx         ;存入第一個參數
        add     rax,rdx         ;加上第二個參數
        add     rax,r8          ;加上第三個參數
        add     rax,r9          ;加上第四個參數
        add     rax,[rsp+28h]   ;加上第五個參數
        add     rax,[rsp+30h]   ;加上第六個參數
        add     rax,[rsp+38h]   ;加上第七個參數
        ret
Sum     ENDP
;-------------------------------------------------------------------------------
;把RCX內的十六進位數轉換成字串,存在RDX所指的位址
RCX_To_String   PROC
        mov     [rsp+08h],rcx   ;在堆疊中存入第一個參數
        mov     [rsp+10h],rdx   ;在堆疊中存入第二個參數
        mov     r8,16
nxt:    rol     rcx,4
        mov     al,cl
        and     al,0fh
        add     al,'0'
        cmp     al,'9'
        jbe     ok
        add     al,7
ok:     mov     [rdx],al
        inc     rdx
        dec     r8
        jnz     nxt
        mov     BYTE PTR [rdx],'H'
        mov     rax,rdx
        inc     rax
        ret
RCX_To_String   ENDP
;-------------------------------------------------------------------------------
Start   PROC    USES rbx rsi            ;保存RBX、RSI暫存器
        LOCAL   sum:QWORD,n:QWORD       ;設定兩個區域變數sum、n
        sub     rsp,40h
        mov     rax,a7
        mov     [rsp+30h],rax   ;第七個參數
        mov     rax,a6
        mov     [rsp+28h],rax   ;第六個參數
        mov     rax,a5
        mov     [rsp+20h],rax   ;第五個參數
        mov     r9,a4           ;第四個參數
        mov     r8,a3           ;第三個參數
        mov     rdx,a2          ;第二個參數
        mov     rcx,a1          ;第一個參數
        call    Sum
        mov     sum,rax

        mov     n,0
        mov     rax,OFFSET szText
nxt:    mov     r8,n
        shl     r8,3
        mov     r9,OFFSET a1
        add     r8,r9
        mov     rdx,rax
        mov     BYTE PTR [rdx],' '
        inc     rdx
        mov     rcx,[r8]
        call    RCX_To_String
        inc     n
        mov     WORD PTR [rax],0a0dh
        add     rax,2
        cmp     n,7
        jne     nxt
        mov     BYTE PTR [rax-20],'+'
        mov     rcx,18
line:   mov     BYTE PTR [rax],'-'
        inc     rax
        loop    line
        mov     WORD PTR [rax],0a0dh
        add     rax,2
        mov     rdx,rax
        mov     rcx,sum
        call    RCX_To_String

        mov     r9,MB_OK
        mov     r8,OFFSET szTitle
        mov     rdx,OFFSET szText
        sub     rcx,rcx
        call    MessageBoxA
        add     rsp,40h
        sub     rax,rax
        ret
Start   ENDP
;*******************************************************************************
        END

組譯時,不加『/Zi』參數,以『ml64 hellow2.asm /link /entry:Start』組譯,再用 WinDbg 載入。先觀察組譯後變成什麼樣子,輸入『u 140001000 14000114a』( 底下黃字的部分是必須打字的部分,且要記得加上 Enter 鍵 ):

0:000> u 140001000 14000114a [Enter]
image00000001_40000000+0x1000:
00000001`40001000 488bc1               mov     rax,rcx
00000001`40001003 4803c2               add     rax,rdx
00000001`40001006 4903c0               add     rax,r8
00000001`40001009 4903c1               add     rax,r9
00000001`4000100c 4803442428           add     rax,qword ptr [rsp+28h]
00000001`40001011 4803442430           add     rax,qword ptr [rsp+30h]
00000001`40001016 4803442438           add     rax,qword ptr [rsp+38h]
00000001`4000101b c3                   ret
00000001`4000101c 48894c2408           mov     qword ptr [rsp+8],rcx
00000001`40001021 4889542410           mov     qword ptr [rsp+10h],rdx
00000001`40001026 49c7c010000000       mov     r8,10h
00000001`4000102d 48c1c104             rol     rcx,4
00000001`40001031 8ac1                 mov     al,cl
00000001`40001033 240f                 and     al,0Fh
00000001`40001035 0430                 add     al,30h
00000001`40001037 3c39                 cmp     al,39h
00000001`40001039 7602                 jbe     image00000001_40000000+0x103d (00000001`4000103d)
00000001`4000103b 0407                 add     al,7
00000001`4000103d 8802                 mov     byte ptr [rdx],al
00000001`4000103f 48ffc2               inc     rdx
00000001`40001042 49ffc8               dec     r8
00000001`40001045 75e6                 jne     image00000001_40000000+0x102d (00000001`4000102d)
00000001`40001047 c60248               mov     byte ptr [rdx],48h
00000001`4000104a 488bc2               mov     rax,rdx
00000001`4000104d 48ffc0               inc     rax
00000001`40001050 c3                   ret
00000001`40001051 55                   push    rbp
00000001`40001052 488bec               mov     rbp,rsp
00000001`40001055 4883c4f0             add     rsp,0FFFFFFFFFFFFFFF0h
00000001`40001059 53                   push    rbx
00000001`4000105a 56                   push    rsi
00000001`4000105b 4883ec40             sub     rsp,40h
00000001`4000105f 488b05ca1f0000       mov     rax,qword ptr [image00000001_40000000+0x3030 (00000001`40003030)]
00000001`40001066 4889442430           mov     qword ptr [rsp+30h],rax
00000001`4000106b 488b05b61f0000       mov     rax,qword ptr [image00000001_40000000+0x3028 (00000001`40003028)]
00000001`40001072 4889442428           mov     qword ptr [rsp+28h],rax
00000001`40001077 488b05a21f0000       mov     rax,qword ptr [image00000001_40000000+0x3020 (00000001`40003020)]
00000001`4000107e 4889442420           mov     qword ptr [rsp+20h],rax
00000001`40001083 4c8b0d8e1f0000       mov     r9,qword ptr [image00000001_40000000+0x3018 (00000001`40003018)]
00000001`4000108a 4c8b057f1f0000       mov     r8,qword ptr [image00000001_40000000+0x3010 (00000001`40003010)]
00000001`40001091 488b15701f0000       mov     rdx,qword ptr [image00000001_40000000+0x3008 (00000001`40003008)]
00000001`40001098 488b0d611f0000       mov     rcx,qword ptr [image00000001_40000000+0x3000 (00000001`40003000)]
00000001`4000109f e85cffffff           call    image00000001_40000000+0x1000 (00000001`40001000)
00000001`400010a4 488945f8             mov     qword ptr [rbp-8],rax
00000001`400010a8 48c745f000000000     mov     qword ptr [rbp-10h],0
00000001`400010b0 48b83b30004001000000 mov     rax,offset image00000001_40000000+0x303b (00000001`4000303b)
00000001`400010ba 4c8b45f0             mov     r8,qword ptr [rbp-10h]
00000001`400010be 49c1e003             shl     r8,3
00000001`400010c2 49b90030004001000000 mov     r9,offset image00000001_40000000+0x3000 (00000001`40003000)
00000001`400010cc 4d03c1               add     r8,r9
00000001`400010cf 488bd0               mov     rdx,rax
00000001`400010d2 c60220               mov     byte ptr [rdx],20h
00000001`400010d5 48ffc2               inc     rdx
00000001`400010d8 498b08               mov     rcx,qword ptr [r8]
00000001`400010db e83cffffff           call    image00000001_40000000+0x101c (00000001`4000101c)
00000001`400010e0 48ff45f0             inc     qword ptr [rbp-10h]
00000001`400010e4 66c7000d0a           mov     word ptr [rax],0A0Dh
00000001`400010e9 4883c002             add     rax,2
00000001`400010ed 48837df007           cmp     qword ptr [rbp-10h],7
00000001`400010f2 75c6                 jne     image00000001_40000000+0x10ba (00000001`400010ba)
00000001`400010f4 c640ec2b             mov     byte ptr [rax-14h],2Bh
00000001`400010f8 48c7c112000000       mov     rcx,12h
00000001`400010ff c6002d               mov     byte ptr [rax],2Dh
00000001`40001102 48ffc0               inc     rax
00000001`40001105 e2f8                 loop    image00000001_40000000+0x10ff (00000001`400010ff)
00000001`40001107 66c7000d0a           mov     word ptr [rax],0A0Dh
00000001`4000110c 4883c002             add     rax,2
00000001`40001110 488bd0               mov     rdx,rax
00000001`40001113 488b4df8             mov     rcx,qword ptr [rbp-8]
00000001`40001117 e800ffffff           call    image00000001_40000000+0x101c (00000001`4000101c)
00000001`4000111c 49c7c100000000       mov     r9,0
00000001`40001123 49b83830004001000000 mov     r8,offset image00000001_40000000+0x3038 (00000001`40003038)
00000001`4000112d 48ba3b30004001000000 mov     rdx,offset image00000001_40000000+0x303b (00000001`4000303b)
00000001`40001137 482bc9               sub     rcx,rcx
00000001`4000113a e80b000000           call    image00000001_40000000+0x114a (00000001`4000114a)
00000001`4000113f 4883c440             add     rsp,40h
00000001`40001143 482bc0               sub     rax,rax
00000001`40001146 5e                   pop     rsi
00000001`40001147 5b                   pop     rbx
00000001`40001148 c9                   leave
00000001`40001149 c3                   ret
00000001`4000114a ff25b00e0000         jmp     qword ptr [image00000001_40000000+0x2000 (00000001`40002000)]

很明顯的看到,主程式由 140001051 處開始,也就是小木偶以白色字標示出來的那三行開始。這三行指令也是 LOCAL 假指令被組譯器組譯的結果,接著的兩行用灰色字表示的是 PROC 加上 USES 組譯後的結果。由 WinDbg 反組譯結果來看,可知當 ML64.EXE 遇到 LOCAL 假指令時,會先把 RBP 推入堆疊保存起來,然後再把 RBP 設成 RSP,這些動作和以前在 Win32 時一樣,都是以 RBP 當成堆疊的指標存取區域變數,在位址 1400010A4、1400010A8 用淡藍色標示出來的兩行指令,就是存取區域變數 sum、n,下圖十三、十四有更詳細的說明。這三行指令的第三行是在堆疊中預留區域變數的空間,因為區域變數 sum、n 共佔據 16 個位元組,所以在堆疊上保留 16 個位元組的空間,因此有

        add     rsp,0FFFFFFFFFFFFFFF0h

這個程式碼,加上 -10H 亦即減去 10H。除此之外,當使用 LOCAL 宣告區域變數後,在副程式結束的地方,組譯器還會加上一道 LEAVE 指令,LEAVE 指令設定 RSP 之值變為 RBP,接著會從堆疊彈出一個四字組到 RBP 暫存器中,於是堆疊、RSP 便恢復原狀了,也就是在位址 140001148 處。換句話說,使用 LOCAL 假指令時,組譯器會增加

        PUSH    RBP
        MOV     RBP,RSP
        ADD     RSP,FFFFFFFFFFFFXXXXH
        ……    …………
        LEAVE

這幾道指令。雖然說 LOCAL 假指令必須接在 PROC 假指令之後,但是如果 PROC 之後使用 USES 保存某些暫存器,這些暫存器會用 PUSH 指令,保留在區域變數之後,才推入堆疊 ( 亦即被保存的暫存器在堆疊較低位址 )。因此,第一個區域變數一定是在『RBP-8H』處、第二個區域變數是在『RBP-10H』處。如果 PROC 之後,用 USES 保存某些暫存器,那麼在副程式結束之前,也就是執行 RET 指令之前,會先由堆疊彈出被保存的數值到相對應的暫存器。解說完 LOCAL、PROC USES 假指令後,接下來小木偶想先觀察堆疊變化的情形,先使 WinDbg 執行到程式入口處,輸入『g 140001051』並按下『Enter』鍵,這時候電腦尚未執行位址 140001051 處的指令;接著觀察暫存器及堆疊內容,我們先後輸入『r』、『d 12fef0』指令。這三個過程如下:

0:000> g 140001051 [Enter]
ModLoad: 000007fe`ff590000 000007fe`ff5be000   C:\Windows\system32\IMM32.DLL
ModLoad: 000007fe`fef00000 000007fe`ff009000   C:\Windows\system32\MSCTF.dll
image00000001_40000000+0x1051:
00000001`40001051 55              push    rbp
0:000> r [Enter]
rax=000000007767f560 rbx=0000000000000000 rcx=000007fffffdf000
rdx=0000000140001051 rsi=0000000000000000 rdi=0000000000000000
rip=0000000140001051 rsp=000000000012ff58 rbp=0000000000000000
 r8=000007fffffdf000  r9=0000000140001051 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
image00000001_40000000+0x1051:
00000001`40001051 55              push    rbp
0:000> d 12fef0 [Enter] →由上面 RSP 暫存器之值,可知現在堆疊底在 12FF58H 處,但我們要觀察的是將來要使用到的堆疊,也就是比它位址更低的地方
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Windows\system32\kernel32.dll - 
00000000`0012fef0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`0012ff00  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`0012ff10  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`0012ff20  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`0012ff30  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`0012ff40  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`0012ff50  00 00 00 00 00 00 00 00-6d f5 67 77 00 00 00 00  ........m.gw....
00000000`0012ff60  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

這時候 RSP 指向堆疊 12FF58 處,這埵s放返回系統的位址,此位址是 7767F56D,如下圖九及上面橙色字。下邊這些圖中的每個方格表示一個四字組,最上面的紫色指令是即將要執行的指令,但尚未執行。接著按下六次『F8』鍵,單步追蹤,以及一個『g』指令,執行到呼叫 Sum 副程式之前:

0:000> t [Enter] →把 RBP 推入堆疊,如下圖九
image00000001_40000000+0x1052:
00000001`40001052 488bec          mov     rbp,rsp
0:000> t [Enter] →使 RBP 設為 RSP,如下圖十
image00000001_40000000+0x1055:
00000001`40001055 4883c4f0        add     rsp,0FFFFFFFFFFFFFFF0h
0:000> t [Enter] →使 RSP 減 10H,保留給區域變數使用,如下圖十一
image00000001_40000000+0x1059:
00000001`40001059 53              push    rbx
0:000> t [Enter] →在堆疊中保存 RBX、RSI,如下圖十二
image00000001_40000000+0x105a:
00000001`4000105a 56              push    rsi
0:000> t [Enter]
image00000001_40000000+0x105b:
00000001`4000105b 4883ec40        sub     rsp,40h
0:000> t [Enter] →在堆疊中預留七個參數的空間,如下圖十三
image00000001_40000000+0x105f:
00000001`4000105f 488b05ca1f0000  mov     rax,qword ptr [image00000001_40000000+0x3030 (00000001`40003030)]
                                          ds:00000001`40003030=0000000000000002

在上圖十三將執行『sub rsp,40h』指令 ( 在程式碼位址 14000105B 處 ),減掉 40H 的原因除了為呼叫副程式 Sum 的參數外,也要注意到是否能使下個堆疊框對齊節邊界。此處先來看看堆疊的情況,輸入『d 12fee0』,如下面 WinDbg 傾印的結果,深紫色是 RSI 被推入堆疊所在,但紫色則是 RBX,接著是兩個四字組,分別是區域變數 n、sum 所在,接著洋紅色的是保存在堆疊的 RBP:

0:000> d 12fee0 [Enter]
00000000`0012fee0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`0012fef0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`0012ff00  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`0012ff10  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`0012ff20  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`0012ff30  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`0012ff40  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`0012ff50  00 00 00 00 00 00 00 00-6d f5 67 77 00 00 00 00  ........m.gw....
0:000> g 14000109f [Enter] →由 14000105F 到 14000109F 的指令是把參數填入堆疊或暫存器,如上圖十四
image00000001_40000000+0x109f:
00000001`4000109f e85cffffff      call    image00000001_40000000+0x1000 (00000001`40001000)
0:000> t [Enter] →呼叫 Sum 副程式,把下一指令的返回位址 1400010A4 推入堆疊,如上圖十五
image00000001_40000000+0x1000:
00000001`40001000 488bc1          mov     rax,rcx
0:000> d 12fee0 [Enter] →先觀察堆疊情形,底下淡藍色的是參數,白的是呼叫 Sum 後的返回位址
00000000`0012fee0  00 00 00 00 00 00 00 00-a4 10 00 40 01 00 00 00  ...........@....
00000000`0012fef0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`0012ff00  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`0012ff10  99 99 bb bb 33 33 22 22-01 00 00 00 00 00 00 00  ....33""........
00000000`0012ff20  02 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`0012ff30  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`0012ff40  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`0012ff50  00 00 00 00 00 00 00 00-6d f5 67 77 00 00 00 00  ........m.gw....
0:000> r [Enter]
rax=22223333bbbb9999 rbx=0000000000000000 rcx=123456789abcdef0
rdx=7777555533331111 rsi=0000000000000000 rdi=0000000000000000
rip=0000000140001000 rsp=000000000012fee8 rbp=000000000012ff50
 r8=1444144414441444  r9=0f130f130f130f13 r10=0000000000000000
r11=0000000000000000 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
image00000001_40000000+0x1000:
00000001`40001000 488bc1          mov     rax,rcx

進入 Sum 副程式後,如要存取第五個參數,就要到 RSP+28H 去取得,若要存取第六個參數,就要到 RSP+30H 去取得……。這部分在 HELLOW1 已提過了,就不再重複了。


結語

Win64 堆疊框在 Win64 系統中,以組合語言撰寫程式還是一件麻煩的事,什麼煩瑣的細節,都得由程式設計的人親自安排;但是也因為這樣,你將獲得更多的知識與經驗。小木偶在這段摸索的過程中花費了許多精神,因此寫下這些經過使想學習 Win64 組合語言的人不必再浪費時間。

在 Win64 組合語言中,與 Win32 組合語言最不同的地方應該是呼叫協定,尤其是設置堆疊框時,必須保留多少位元組給副程式的參數、保存在堆疊的暫存器以及區域變數來使用,是個很大的問題。請參考右圖,這是一個副程式 A 呼叫副程式 B 時,所建立的堆疊框,假設在副程式 A 堜狻I叫的副程式中,以副程式 B 所需參數最多,那麼所需保留的空間就以副程式 B 所需參數個數計算即可。

程式設計師應在副程式 A 一開始就設定好 RSP ( 稍後再詳細說明 ),使 RSP 恰好指到節的邊界上,如右圖的棕底白色字所指的地方,也就是位址以棕底淡藍字顯示的地方;如此一來,當呼叫副程式 B 時會把返回位址放入右圖中標示『返回 A 的位址』處,副程式 B 的堆疊框就會對齊節邊界。而呼叫副程式 B 所需的參數前四個當然是放在 RCX、RDX、R8、R9 暫存器堙A第五個參數則放在 [RSP+20H] ( 見右圖棕底藍字的 RSP+20H ),其餘依右圖可得。進入副程式 B 之後,假如副程式 B 設有區域變數,那麼組譯器會自動把 RBP 推入堆疊,這時候就可以以 RBP 為指標存取副程式 B 的參數,參考右圖,第五個參數在 [RBP+30H] 處,第六個參數在 [RBP+38H] 處……;假如要把前四個參數存在堆疊中,也可以依 RBP 為基準,例如 RCX 應存在 [RBX+10H] 處;假如要存取區域變數,則不須擔心,因組譯器會自動依區域變數名稱存取。

假如堆疊框媔僅只有副程式的參數,那麼就參考表一即可,如同上面所說的,這時候在堆疊框的大小是 16n+8 位元組;但是如果還包含區域變數或保存的暫存器時,那該怎麼辦呢?難道每次都得用圖畫畫出堆疊使用情形來計算得到嗎?我想大概不需這麼麻煩吧!請看圖右最上面以綠字標示的 RSP 是副程式 B 最後 RSP 所指位址;再看副程式 A 的堆疊框,也就是以棕底白色字所標示的 RSP 最後所指位址。請仔細觀察,不管是副程式 A 或 B 的 RSP 最後也都指在 XXXXX0 處,也是對齊節的邊界。換句話說,RSP 減掉參數所佔的空間,還要再調整 RSP 使 RSP 能對齊節的邊界,可以用下面程式碼達成:

副程式名    PROC    USES r15 其他要保存的暫存器列表
            LOCAL   變數名:QWORD,……
            mov     r15,rsp
            sub     rsp,n*8
            and     rsp,0FFFFFFFFFFFFFFF0h
            ……
            mov     rsp,r15
            ret
副程式名    ENDP

上面程式碼中的 n 代表這個副程式呼叫的 API 或副程式中最多的參數個數,這樣就不再需要去費心的計算 RSP 到底要減去多少了。


回到首頁到下一章