前面十章介紹的都是用 MASM64 SDK 來組譯原始程式,也就是使用微軟的 ML64.EXE 組譯器搭配先進 hutch-- 所開發的巨集、包含檔來製作程式。從這一章開始,將使用 UASM 來製作程式。在民國 100 年前後(西元 2011 年前後),小木偶已開始學習在 Win64 系統下撰寫組合語言程式,那時 MASM64 SDK 尚未釋出,小木偶是利用 UASM 的前身,JWASM,組譯器搭配 WinInc 包含檔來撰寫組合語言程式。因此對於撰寫 64 位元的程式而言,UASM 算是「啟蒙」的組譯器,小木偶割捨不下,所以必須要介紹 UASM。
UASM 可說是命運多舛,由於它的原生公司,加拿大的 Watcom 軟體公司被 Powersoft 公司收購,Powersoft 公司又被 Sybase 公司收購,一度停止開發。幸好後來 Sybase 公司將其原始碼開放釋出,再由 Japheth、Habran、Branislav Habus、John Hankinson 等先進持續努力,才有了 UASM。(見第零章)
不論如何,UASM 總算是熬了過來。底下開始介紹 UASM 的用法。
首先,先檢查是否已完成下面的項目:
如果你的安裝目錄與上面不同,也沒有關係,但是底下就要修改相對應的目錄名稱了。因為組譯與連結都要使用到 UASM64.EXE 與 LINK.EXE,而且原始碼要有包含檔及匯入程式庫,這些都在不同的子目錄。每次組譯並連結時,都要先進行底下的設定:
SET INCLUDE=C:\UASM\Include SET LIB=E:\MASM32\lib64 SET PATH=C:\UASM;C:\UASM\bin64;%PATH% SET LINK=/SUBSYSTEM:WINDOWS /DEBUG
如果每次組譯都重新設定搜尋路徑很麻煩,不如製作一個批次檔(batch file)。批次檔副檔名是 BAT,顧名思義,它只須建立一次後,就能一次處理許多指令。
這個批次檔,可以命名為「UASM.BAT」,將底下的文字用「記事本」輸入後,存入「C:\USER\帳號\UASM.BAT」裡面。儲存時的編碼可用「UTF-8」或「ANSI」。UASM.BAT 的內容如下:
1 2 3 4
5 6 7 8
9 10 11 12 |
SET INCLUDE=C:\UASM\Include
SET LIB=C:\UASM\lib64
SET PATH=C:\UASM;C:\UASM\bin64;%PATH%
E:
CD E:\HOMEPAGE\SOURCE\WIN64
@ECHO OFF
IF "%1"=="C" SET LINK=/SUBSYSTEM:CONSOLE /DEBUG
IF "%1"=="c" SET LINK=/SUBSYSTEM:CONSOLE /DEBUG
IF "%1"=="W" SET LINK=/SUBSYSTEM:WINDOWS /DEBUG
IF "%1"=="w" SET LINK=/SUBSYSTEM:WINDOWS /DEBUG
IF "%1"=="" SET LINK=/SUBSYSTEM:WINDOWS /DEBUG
SET LINK |
往後如果要組譯原始程式時,進入「命令提示字元」先執行「UASM.BAT」,就能完成前述設定。除此之外,UASM.BAT 還新增了一項功能。如果在命令提示字元中,輸入「UASM C」(C 的大小寫不拘),可組譯控制臺程式;如果輸入「UASM W」或沒有選項,那麼就會組譯成視窗程式。
批次檔也可以有選項,以「%1」、「%2」、「%3」……代表第一個、第二個、第三個……選項。因此「IF "%1"=="C" SET LINK=/SUBSYSTEM:CONSOLE /DEBUG」的意思就是,如果第一個選項是 C,就設定「LINK=/SUBSYSTEM:CONSOLE /DEBUG」。
UASM 的組譯器是 UASM64.EXE,組譯 Win64 程式時,必須在命令提示字元中,執行 UASM64.EXE,還必須輸入適當的選項。在介紹這些選項之前,要先說說這些選項共通之處:
你可以在「命令提示字元」輸入「UASM64 -h」或「UASM64 -?」,UASM 會顯示出所有選項的簡單描述,也可以在開啟「C:\UASM\JWasm.chm」說明檔閱讀詳細說明。儘管 UASM 功能強大,但是它無法組譯成功後,直接呼叫連結器,因此得自行連結,所以沒有「-link」這個參數。
使用「-Fl」選項(其中的「l」是小寫的「L」),可以使 UASM64.EXE 建立列表檔。其後的「[=file_name]」可以省略,如果省略,列表檔的主檔名就是原始程式的主檔名,副檔名是「LST」。如果「[=file_name]」沒有省略,但只寫主檔名,UASM64.EXE 會自動加上副檔名「LST」;如果「[=file_name]」寫出了完整的檔名,那麼列表檔就用這個名稱。
使用「-Zi」選項,可以在所建立的目的檔中(包含 OMF 以及 COFF 格式的目的檔),加上使用者在原始程式裏所使用的符號資料,這符號資料能讓除錯器在除錯時顯示符號名稱,例如變數名稱或常數名稱,而不是只顯示位址而已,這的確是一大進步。但連結器也必須要配合才行,LINK.EXE 必須下達「/DEBUG」選項,就能達的此目的。
能用原始程式符號除錯的除錯器必須符合 CodeView V4 的標準,有很多除錯程式都可使用,例如像 WinDbg、OllyDbg、x64dbg 等都可以。
「-Zi」後面還可以選擇,0、1、2、3、5、8 六種數字的其中一種。0 表示只有全域變數會被寫入目的檔裏;1 表示全域變數和區域變數都會寫入目的檔;2 表示全域變數、區域變數以及使用者定義的符號都會寫入,這是內定值;3 表示以 EQU 定義的常數也會被寫入目的檔;5 與 8 分別代表支援 CodeView 第 5 版與第 8 版。
「-win64」會使 UASM 製造出 64 位元 COFF 格式的目的檔,進一步連結後就能產生 PE+ 格式的可執行檔,此種可執行檔是 Win64 的原生程式。換句話說,如果要製造出 Win64 系統中的可執行檔,就必須使用這個參數。這個參數會自動啟用
.X64 .MODEL FLAT,FASTCALL OPTION WIN64:0
「.MODEL FLAT,FASTCALL」和「OPTION WIN64:0」都是 UASM64.EXE 的假指令,要寫在原始程式裏面,稍後再解釋。
我們仿照第七章的 SPLWND.ASM 撰寫由 UASM64.EXE 組譯的版本,稱之為「UASM 版的 SPLWND」,其原始程式如下。在文書處理器中將其輸入後,儲存在「E:\HomePage\SOURCE\Win64\SPLWND\SPLWND_U.ASM」。
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
17 18 19 20
21 22 23 24
25 26 27 28
29 30 31 32
33 34 35 36
37 38 39 40
41 42 43 44
45 46 47 48
49 50 51 52
53 54 55 56
57 58 59 60
61 62 63 64
65 66 67 68
69 70 71 72
73 74 75 76
77 78 79 80
81 82 83 84
85 86 87 88
89 |
;組譯與連結:
;SET INCLUDE=C:\UASM\INCLUDE;
;SET LIB=E:\masm32\lib64
;SET PATH=C:\UASM;C:\UASM\bin64;%PATH%
;SET LINK=/SUBSYSTEM:WINDOWS
;uasm64 -win64 splwnd_u.asm
OPTION CASEMAP:NONE
OPTION WIN64:3
INCLUDE WINDOWS.INC
INCLUDELIB GDI32.LIB
INCLUDELIB KERNEL32.LIB
INCLUDELIB USER32.LIB
;***************************************************************************************************
.DATA
hInstance HINSTANCE ? ;模組代碼
hwnd HWND ? ;視窗代碼
CommandLine LPSTR ? ;命令列位址
wc WNDCLASSEX <>
msg MSG <>
rect RECT <>
ps PAINTSTRUCT <>
szClassName DB "SimpleWndClass",0 ;視窗類別名稱
szAppName DB "簡單的視窗",0
sYouPress DB "你按下了 鍵。"
;***************************************************************************************************
.CODE
;---------------------------------------------------------------------------------------------------
WndProc PROC hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.switch uMsg
.case WM_PAINT
invoke BeginPaint,hWnd,ADDR ps
invoke GetClientRect,hWnd,ADDR rect
invoke DrawText,ps.hdc,ADDR sYouPress,SIZEOF sYouPress,ADDR rect,\
DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint,hWnd,ADDR ps
.case WM_CHAR
mov rax,wParam
mov [sYouPress+8],al
invoke InvalidateRect,hWnd,0,1
.case WM_DESTROY
invoke PostQuitMessage,0
.default
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endsw
xor rax,rax
ret
WndProc ENDP
;---------------------------------------------------------------------------------------------------
main PROC
invoke GetModuleHandle,0 ;取得模組代碼
mov hInstance,rax
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style,CS_HREDRAW or CS_VREDRAW
lea rdx,WndProc
mov wc.lpfnWndProc,rdx
mov wc.cbClsExtra,0
mov wc.cbWndExtra,0
mov wc.hInstance,rax
invoke LoadIcon,NULL,IDI_APPLICATION ;取得圖示代碼
mov wc.hIcon,rax ;存入圖示代碼
mov wc.hIconSm,rax ;存入小圖示代碼
invoke LoadCursor,NULL,IDC_ARROW ;取得游標代碼
mov wc.hCursor,rax ;存入游標代碼
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,0
lea rdx,szClassName
mov wc.lpszClassName,rdx
invoke RegisterClassEx,ADDR wc ;註冊視窗類別
invoke CreateWindowEx,0,ADDR szClassName,ADDR szAppName,WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,0,0,hInstance,0
mov hwnd,rax
invoke ShowWindow,hwnd,SW_SHOWNORMAL
invoke UpdateWindow,hwnd
.while TRUE
invoke GetMessage,ADDR msg,0,0,0
.break .if rax==0
invoke TranslateMessage,ADDR msg
invoke DispatchMessage,ADDR msg
.endw
invoke ExitProcess,0 ;程式結束
main ENDP
;***************************************************************************************************
END main |
開啟「命令提示字元」之後,依照底下過程組譯並連結。黃色字是使用者必須自行輸入的指令,每輸入完一行必須按「Enter」鍵,電腦才會執行這道命令。
Microsoft Windows [版本 6.1.7601] Copyright (c) 2009 Microsoft Corporation. All rights reserved. C:\Users\wanker>uasm [Enter] →執行 UASM.BAT 批次檔,省了許多打字輸入 C:\Users\wanker>SET INCLUDE=C:\UASM\Include C:\Users\wanker>SET LIB=C:\UASM\lib64 C:\Users\wanker>SET PATH=C:\UASM;C:\UASM\bin64;C:\Windows\system32;C:\Windows;C: \Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Program Fi les (x86)\UltraEdit-32 C:\Users\wanker>E: E:\>CD E:\HOMEPAGE\SOURCE\WIN64 LINK=/SUBSYSTEM:WINDOWS /DEBUG →到此 UASM.BAT 已執行完畢 E:\HomePage\SOURCE\Win64>cd SPLWND [Enter] →設定 SPLWND_U.ASM 所在子目錄為當前目錄 E:\HomePage\SOURCE\Win64\SPLWND>uasm64 -win64 -Zi SPLWND_U.ASM [Enter] →組譯 UASM v2.56, Oct 27 2022, Masm-compatible assembler. Portions Copyright (c) 1992-2002 Sybase, Inc. All Rights Reserved. Source code is available under the Sybase Open Watcom Public License. SPLWND_U.ASM: 89 lines, 3 passes, 143 ms, 0 warnings, 0 errors →沒有錯誤,成功組譯完成 E:\HomePage\SOURCE\Win64\SPLWND>link SPLWND_U.OBJ [Enter] →連結 Microsoft (R) Incremental Linker Version 14.25.28614.0 Copyright (C) Microsoft Corporation. All rights reserved. /SUBSYSTEM:WINDOWS /DEBUG →沒有訊息,表示 SPLWND_U.EXE 成功製作好了 E:\HomePage\SOURCE\Win64\SPLWND>
上面組譯過程的畫面顏色,是按命令提示字元中的實際顏色。或許有人已經發現,UASM64.EXE 的訊息是彩色的。比較重要的是橙色與紅色的警告與錯誤訊息,如果都是 0,就代表組譯成功。
底下小木偶想談談一些經驗,有關於在命令提示字元所輸入指令的大小寫問題。
Windows 作業系統中,命令提示字元的指令大致可分為兩種:①內部指令與②外部指令。所謂內部指令是指命令提示字元本身提供的,例如「CD」、「SET」等;輸入內部指令時,不區分大小寫,意思就是說輸入「CD」、「Cd」、「cd」都是一樣的。外部指令是指儲存在電腦磁碟機上的可執行檔,例如「UASM64.EXE」、「LINK.EXE」等;執行這些可執行檔,就相當執行外部指令,只需在命令提示字元輸入主檔名即可,不需要把附檔名(就是「EXE」)也寫出來,也不區分大小寫。結論就是,在命令提示字元的指令不區分大小寫。
在命令提示字元中,除了指令不區分大小寫,檔案名稱也不區分大小寫,因此「SPLWND_U.ASM」和「splwnd_u.asm」是一樣的。如果你硬碟 D:\ 中有 SPLWND_U.ASM 檔案,E:\ 中有 splwnd_u.asm 檔案,將「D:\SPLWND_U.ASM」拷貝到「E:\」,命令提示字元就會發出警告訊息,問「確定要複寫嗎?」。由此可見,命令提示字元把相同字母但不同大小寫的檔案,視為同一個檔案。另外,雖然檔名不區分大小寫,但是當建立檔案時,系統會以使用者輸入的大小寫,會如實建立,也會如實顯示。不僅命令提示字元中如此,在 Windows 作業系統的檔案管理員或其他程式也是這樣。
命令提示字元的指令與檔案名稱都不區分大寫小,那麼什麼時候會區分大小寫呢?命令提示字元中的指令,不論是內部指令還是外部指令,如果指令有選項,這些選項中,有些是區分大小寫,例如 UASM64.EXE 的「-win64」、ML64.EXE 的「/Fl」等。也有不區分大小寫的選項,例如 LINK.EXE 的「/SUBSYSTEM:WINDOWS」。事實上,這些選項是依據這些程式最初的設計,而決定是否區分大小寫。
比較適用於 UASM 與 MASM64 SDK 的原始程式:SPLWND_U.ASM 與 SPLWND,你會發現兩者幾乎一樣,差別在於底下的三處,事實上,其他的原始程式用 MASM64 SDK 組譯與用 UASM 組譯,原始程式的寫法不同之處也是這三處:
底下逐項來分析。
①:「OPTION CASEMAP:NONE」是用來設定原始程式中,使用者定義的名稱,例如變數名稱、副程式名稱……等是區別大小寫的。因為 Windows 程式必定會呼叫 Windows API,這些函式名稱都區分大小寫,所以不管用哪個組譯器,都必須要有這一行,差別是 MASM64 SDK 版的原始程式已寫在「masm64rt.inc」裡面了,UASM 版的原始程式必須自行加入。
此外,在進入副程式還必須設定一些特性,這項工作是用「OPTION WIN64」達成。
OPTION 本是 ML64.EXE 的假指令,但 UASM 在原來的「OPTION」基礎上添加了「WIN64:n」選項,所以 UASM64.EXE 可用「OPTION WIN64:n」,但 ML64.EXE 不能使用。它的語法是:
OPTION WIN64:n
其中 n 只有 0~3 位元使用,見下表:
一般而言,撰寫 64 位元的 Windows 程式通常把第零、一位元設定唯一即可,如下:
OPTION WIN64:3
有關影子控間與參數所需的堆疊空間,請分別參閱第二章的「x64 呼叫慣例」與「在堆疊中保留參數空間與對齊節的邊界的策略」。在 64 位元的硬體中,存取的記憶體位址如果以十六進位表示,而且位址的個位數為零,存取速度較快。基於上述原因,假如把區域變數安排在節的邊界上(節是指 16 個位元組,英文是 paragraphs), 便能增進效率。
如果不在原始程式寫上「OPTION WIN64:n」,也可以在組譯時於命令提示字元輸入「uasm64 -win64 原始程式」達成。但在命令提示字元輸入「-win64」選項,只能使用預設值,不如在原始程式中寫上「OPTION WIN64:n」比較有彈性。另外,要組譯 64 位元的 Windows 程式,命令提示字元輸入「uasm64 -win64」中的「-win64」不可省略,否則會發稱錯誤。
②:因為與 UASM 搭配的 WinInc 包含檔中,並沒有自動包含匯入程式庫,因此必須在原始程式中,以「INCLUDELIB」假指令將適當的匯入程式庫引入。
要引入哪一個匯入程式庫,其實不難決定。在原始程式中,如果呼叫某個 Windows API 時,可在網際網路上查閱該 API 的「規格需求」(Requirements)中,需要哪個程式庫(Library),以「INCLUDELIB」假指令引入即可。
例如在程式中呼叫了 BeginPaint,那麼要引入哪個匯入程式庫呢?可以在 Google 中輸入「BeginPaint」,然後找到 Microsoft | Learn 中的說明。然後捲動到「規格需求」或「Requirements」,以紅色框框住的就是要引入的程式庫,如下圖:
③:UASM64.EXE 直接就支援 invoke 與條件控制流程指令這類高階語法,這是 UASM64.EXE 最大的優勢,這些指令都是假指令。
invoke 假指令的語法和 MASM64 SDK 相同,請參閱第六章。但是 UASM 的 invoke 假指令的參數列不能使用字串。
條件控制流程指令包含「.switch/.case/.default/.endsw」、「.if/.elseif/.else/.endif」、「.while/.break .if/.endw」、「.repaet/.break .if/.until」等。這些條件控制流程的語法和 MASM64 SDK 幾乎一樣,請參閱第七章,差別在於判斷式中的邏輯運算子較為直觀。在 UASM 中的邏輯運算子,< 表示小於、> 表示大於、<> 表示不等於。
④:因為 UASM 支援 END 假指令之後寫上程式的進入點,所以通常 UASM 版的原始程式最後一行是
END 進入點
也因為有這一行,所以在連結時就不需指定 /ENTRY 選項。
底下介紹另一個例子,RNDRECT.ASM。這個程式能在工作區中顯示不固定顏色隨機大小的長方形(長方形也叫矩形)。底下先介紹 RNDRECT.ASM 使用到的新指令、新的 Windows API 與概念,再列出完整的原始程式。
RDRAND 指令會讓 CPU 產生一個亂數(也叫隨機數),存入接於其後的目的運算元裏面,其語法如下:
RDRAND 目的運算元
目的運算元可以是 16 位元、32位元、64 位元的暫存器,CPU 會依據不同長度的暫存器,而生成不同大小的亂數。當執行完 RDRAND 指令之後,如果成功產生亂數並存入目的運算元裏面,那麼 CPU 會設定進位旗標;如果失敗,則清除進位旗標。
但並不是所有的 CPU 都支援 RDRAND 指令,因此在使用 RDRAND 之前要先確定 CPU 是否支援該指令。確定的方法很簡單,先把 RAX 設為一,然後執行 CPUID 指令,執行 CPUID 完畢後,如果 RCX 的第 30 位元為一,表示此 CPU 支援 RDRAND 指令;若為零表示不支援。
RDSEED 指令是產生一個亂數種子,然後可以將這個亂數種子啟動另一個偽隨機數生成器(pseudorandom number generator),以得到亂數。RDSEED 的語法是:
RDSEED 目的運算元
目的運算元可以是 16 位元、32位元、64 位元的暫存器,CPU 會依據不同長度的暫存器,而生成不同大小的亂數種子。執行完 RDSEED 指令之後,如果成功產生亂數種子並存入目的運算元裏,那麼 CPU 會設定進位旗標;如果失敗,則清除進位旗標。
並不是所有的 CPU 都支援 RDSEED 指令,因此在使用 RDSEED 之前要先確定 CPU 是否支援該指令。確定的方法是先把 RAX 設為七,接著執行 CPUID 指令,執行完 CPUID 後,如果 RBX 的第 18 位元為一,表示此 CPU 支援 RDSEED 指令;若為零表示不支援。
RDRAND 與 RDSEED 很相像,要選擇使用哪個指令,依據輸出的目的運算元要用於什麼目的。如果希望目的運算元是偽隨機數生成器(PRNG)的初始值,那就用 RDSEED。而其他目的,就使用 RDRAND。
CPUID 是「CPU Identification」的意思,就是獲得 CPU 的名稱及特性。要獲取的名稱或特性,由 EAX 決定,必須查閱 CPU 手冊。依據手冊上的紀載,設定好 EAX 暫存器,再執行 CPUID,CPU 會將名稱或特性放在某些暫存器傳回來。此處僅舉 EAX 為零的例子,如過要詳細資料,請自行查閱手冊。
當 EAX 等於 0 再執行 CPUID 後,如果是英特爾的 CPU,CPUID 會傳回兩筆資料,第一筆資料是存放在 EAX,是 CPUID 所能接受 EAX 的最大值,第二筆資料存放在 EBX、EDX、ECX 共 96 位元,合起來代表「GenuineIntel」字串,表示製造廠商的名稱字串。如果是超微的 CPU,CPUID 會傳回的 EAX 是基本功能的最大值,超微的 CPU 還有擴充功能;另外 EBX、EDX、ECX 也是表示製造廠商的名稱字串,是「AuthenticAMD」字串。
對於 CPU 是否有支援 RDRAND 及 RDSEED 指令,英特爾及超微的檢測方式都相同。早期 CPU 的 CPUID 指令可能不支援檢測 RDRAND 與 RDSEED 指令,但那些 CPU 都是西元 2010 年生產的,現在大概不多了。
先說 BT,BT 是 bit test,可翻譯為「位元測試」,亦即測試某個位元是零還是一。它的語法是:
BT r/m16,r16 BT r/m32,r32 BT r/m64,r64 BT r/m16,imm8 BT r/m32,imm8 BT r/m64,imm8
r/m16、r/m32、r/m64 依序代表 16 位元、32位元、64 位元的暫存器或記憶體變數;r16、r32、r64 依序代表 16 位元、32位元、64 位元的暫存器;imm8 是代表八位元的常數(立即值)。BT 指令會把第一個運算元中的某個位元複製到進位旗標(縮寫是 CF)中,爾後程式能藉由進位旗標判斷該位元是零還是一,至於是第一個運算元的哪一個位元則由其後的立即值(imm8)決定。例如底下的例子:
mov eax,1101b ;1101b=13,其中的 b 代表二進位
bt eax,2
EAX 有 32 個位元,最低位元是最右邊的位元,其名稱是第零個位元,由上面程式碼得知其值為一;最高位元是最左邊的位元,稱為第 31 個位元,上面的程式碼沒表示出來。EAX 的第二個位元是 1,故執行完「bt eax,2」之後,進位旗標為 1。
第二個運算元如果是八個位元長的立即值,其最大是 255,可能會超過第一個運算元的位元組個數。假如超過的話,那麼就除以第一個運算元的位元個數,取其餘數,作為新的立即值,再進行位元測試。例如
mov eax,1110 0001 1111 0011b ;1110 0001 1111 0011b=57843
bt eax,227
EAX 只有 32 個位元,無法測試第 227 個位元是零還是一,所以進行 227 除以 32 的運算,所得餘數是 3。故「BT EAX,227」跟「BT EAX,3」是一樣的結果,執行後,進位旗標為零。
BTR 是「bit test and reset」,BTS 是「bit test and set」,這兩個指令的語法及運算過程幾乎都與 BT 一樣。差別僅在於 BTR 最後會將指定的位元重置(亦即設為零),BTS 最後會設定指定的位元(亦即將該位元設為一)。
BTC 是「bit test and complement」,它的語法及運算過程也幾乎都與 BT 一樣。差別僅在於 BTC 最後會將指定的位元反置。亦即如果原先指定的位元是一,那就改為零;如果原先指定的位元是零,那就改為一。
BSF 是「bit scan forward」的意思,是說往前掃描找到一。先看看 BSF 指令的語法:
BSF 目的運算元,來源運算元
目的運算元只能是 16 位元、32位元、64 位元的暫存器;來源運算元可以是暫存器或記憶體變數,其長度必須和目的運算元一致。BSF 指令會從來源運算元的第零位元開始,往高位元處掃描,找到位元之值為一時停止,將此位元編號(在來源運算元中是第幾個位元)記錄到目的運算元中。如果來源運算元為零,就找不到位元值為一,那麼目的運算元之值無意義,同時零值旗標被設為一;如果有找到一,零值旗標被清除。進位旗標、溢位旗標、符號旗標都無意義。
BSR 是「bit scan reverse」的意思,它的語法、運算方式、旗標設定跟 BSF 幾乎一樣,差別在於 BSR 是由最高位元開始掃描。看底下的例子:
mov ax,0100 0011 1111 0100b bsf bx,ax bsr cx,ax
由 AX 的第零位元開始往高位元掃描,第一次一出現於第二位元,故 BX 為 2。由 AX 的第 15 位元開始往第零位元掃描,第一次一出現於第 14 位元,故 CX 為 14(CX=000DH)。
PeekMessage API 會檢查程式訊息佇列,如果有訊息的話就放入指定的 MSG 結構體中,並回傳非零值;如果沒有訊息回傳零值。PeekMessage 的原型是:
invoke PeekMessage,\ lpMsg,\ ; pointer to structure for message hWnd,\ ; handle to window wMsgFilterMin,\ ; first message wMsgFilterMax,\ ; last message wRemoveMsg ; removal flags
PeekMessage 的用途跟 GetMessage 很相像,就連前面四個參數都一樣:
剛才提過,PeekMessage 跟 GetMessage 很相像,區別在於當訊息佇列裡有訊息的時候,PeekMessage 取回訊息,並在 RAX 中回傳非零值;沒有訊息的時候直接返回,並在 RAX 中回傳零值,不像 GetMessage 會偷偷的將控制權交給 Windows 利用。
所以在 PeekMessage 回傳非零值的時候,程式檢查訊息是否是 WM_QUIT,如果是的話,就要結束訊息迴圈;如果不是的話,就用標準流程處理訊息;回傳零值的時候,表示是空閒時間,程式就可以做其他工作了,但插入做其他工作的時候,執行時間不能過長,不然會影響正常的訊息處理,使視窗的反應變得遲鈍。
如果應用程式想利用空閒時間,也就是說即使訊息佇列裏沒有訊息的時候改作其他事,而不是讓 GetMessage 拱手交出屬於應用程式的 CPU 時間,那麼就不要呼叫 GetMessage,改用 PeekMessage,訊息迴圈可以是像下列的樣子:
.while TRUE
invoke PeekMessage,ADDR msg,0,0,0,PM_REMOVE
.if rax
cmp msg.message,WM_QUIT
je quit
invoke TranslateMessage,ADDR msg
invoke DispatchMessage,ADDR msg
.else
;趁沒有訊息的時候,做其他事的程式碼寫在這裡
.endif
.endw
CreateSolidBrush 是用來建立邏輯畫刷,邏輯畫刷可以用來塗滿矩形、圓形等內部區域。CreateSolidBrush 的語法是
invoke CreateSolidBrush,\
crColor ; brush color value
crColor 是 32 位元長數值,但只有 0~23 位元有用,其中 0~7 位元代表紅色強度,8~15 位元代表綠色強度,16~23 位元代表藍色強度。可以用
00BBGGRR
表示。這三原色的強度,都以 8 個位元表示,每種顏色強度範圍在 0~255 之間,以十六進位表示在 0~FF 之間。事實上,以這樣表示的資料類型,在 Windows 中稱為 COLORREF,有點類似視窗代碼用資料類型 HWND 表示。如果 crColor 是 0FFH 就代表紅色,如果 crColor 是 0FFFFH 就代表黃色……。
如果失敗,回傳值是零;如果成功建立邏輯畫刷,回傳值是邏輯畫刷代碼。一般而言,應用程式會把邏輯畫刷代碼保存起來,如果不用時,就應該刪除。要刪除邏輯畫刷,可以呼叫 DeleteObject。
值得一提的是,畫刷有兩種類型:邏輯畫刷和實體畫刷。邏輯畫刷是應用程式用來塗滿某個形狀內部區域的圖案,包含顏色、紋理、透明度等。實體畫刷則是設備驅動程式根據應用程式的邏輯畫刷定義所建立的位元圖。
DeleteObject 可以用來刪除圖形物件,包含邏輯畫筆、邏輯畫刷、邏輯字形、位元圖、區域以及調色盤,以釋放系統資源。當指定的圖形物件刪除後,其圖形物件代碼就不能再使用了。
invoke DeleteObject,\
hObject ; handle to graphic object
hObject 是要刪除的圖形物件代碼。如果成功刪除圖形物件,回傳值為非零;反之回傳值為零。
FillRect 用指定的畫刷將一塊矩形區域填滿,其原型是:
invoke FillRect,\ hDC,\ ; handle to device context lprc,\ ; pointer to structure with rectangle hbr ; handle to brush
如果成功執行 FillRect,回傳值為非零值,反之回傳值為零。FillRect 在矩形區域塗色時,上邊及左邊的邊線會上色,但是下邊及右邊的邊線不會上色。
RNDRECT.ASM 與 MS-DOS 2.00 作業系統所附的 BASIC 程式,ART.BAS,很相似。兩者都在電腦螢幕上,隨機出現不同的矩形。
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
17 18 19 20
21 22 23 24
25 26 27 28
29 30 31 32
33 34 35 36
37 38 39 40
41 42 43 44
45 46 47 48
49 50 51 52
53 54 55 56
57 58 59 60
61 62 63 64
65 66 67 68
69 70 71 72
73 74 75 76
77 78 79 80
81 82 83 84
85 86 87 88
89 90 91 92
93 94 95 96
97 98 99 100
101 102 103 104
105 106 107 108
109 110 111 112
113 114 115 116
117 118 119 120
121 122 123 124
125 126 127 128
129 130 131 132
133 134 135 136
137 138 139 140
141 142 143 144
145 146 147 148
149 150 151 152
153 154 155 |
;RNDRECT.ASM:在螢幕上顯示一視窗,利用訊息佇列內無訊息的空閒時間,
;於視窗內隨機繪製出矩形。組譯方式如下:
;SET INCLUDE=C:\UASM\Include
;SET LIB=C:\UASM\lib64
;SET PATH=C:\UASM;C:\UASM\bin64;
;SET LINK=/SUBSYSTEM:WINDOWS
;UASM64 -win64 RNDRECT.ASM
;LINK RNDRECT.OBJ
OPTION CASEMAP:NONE
OPTION WIN64:3
INCLUDE WINDOWS.INC
INCLUDELIB GDI32.LIB
INCLUDELIB KERNEL32.LIB
INCLUDELIB USER32.LIB
;*******************************************************************************
.CONST
szClassName DB "RNDRECT",0 ;視窗類別名稱
szAppName DB "矩形",0
szErr DB "錯誤",0
szErrMsg0 DB "CPU不支援。",0
szErrMsg1 DB "註冊視窗類別失敗。",0
;*******************************************************************************
.DATA
hInstance HINSTANCE ? ;模組代碼
hWnd HWND ? ;視窗代碼
iWidth DD ? ;工作區寬度
iHeight DD ? ;工作區高度
;*******************************************************************************
.CODE
;-------------------------------------------------------------------------------
;check_rdrand用來檢查CPU是否支援RDRAND,如果支援回傳值為1;不支援回傳值為0
check_rdrand PROC
;早期的CPU不支援RDRAND指令,所以在使用前要先進行檢測:把EAX暫存器設為1並執行
;CPUID指令,如果支援RDRAND的話,ECX暫存器的第30位元位會被CPU設為1。
mov rax,1
cpuid
bt ecx,30 ;將ECX的第30位元移至CF
mov rax,1
jc support ;如果CF=1,則跳躍至support標記處
xor rax,rax
support: ret
check_rdrand ENDP
;-------------------------------------------------------------------------------
WndProc PROC hwnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.switch uMsg
.case WM_SIZE
mov r10,lParam
mov r11,lParam
and r10,0ffffh
shr r11,10h
mov iWidth,r10d
mov iHeight,r11d
.case WM_DESTROY
invoke PostQuitMessage,0
.default
invoke DefWindowProc,hwnd,uMsg,wParam,lParam
ret
.endsw
xor rax,rax
ret
WndProc ENDP
;-------------------------------------------------------------------------------
;製造一亂數,且小於range,然後存於EAX後返回
get_rnd PROC range:DWORD
mov ecx,30
bsr eax,range ;以二進位表示range,最高位元為1的在第EAX位元
mov r11d,7fffffffh ;使符號位元為0
sub ecx,eax ;ECX=R11D的符號位元要右移多少次
sar r11d,cl ;執行右移CL次
retry: rdrand eax
and eax,r11d
cmp eax,range
ja retry
ret
get_rnd ENDP
;-------------------------------------------------------------------------------
;畫出塗滿顏色的矩形,矩形的大小、顏色、位置都是隨機的
DrawRect PROC hwnd:HWND
LOCAL hBrush:HBRUSH,hdc:HDC,rect:RECT
cmp iWidth,0
jz quit
cmp iHeight,0
jz quit
invoke get_rnd,iWidth ;矩形左上角X座標
mov rect.left,eax
invoke get_rnd,iHeight ;矩形左上角Y座標
mov rect.top,eax
invoke get_rnd,iWidth ;矩形右下角X座標
mov rect.right,eax
invoke get_rnd,iHeight ;矩形右下角Y座標
mov rect.bottom,eax
rdrand rcx
and rcx,0ffffffh ;顏色也是隨機生成的
invoke CreateSolidBrush,ecx
mov hBrush,rax
invoke GetDC,hwnd
mov hdc,rax
invoke FillRect,hdc,ADDR rect,hBrush
invoke ReleaseDC,hwnd,hdc
invoke DeleteObject,hBrush
quit: ret
DrawRect ENDP
;-------------------------------------------------------------------------------
main PROC
LOCAL wc:WNDCLASSEX,msg:MSG
call check_rdrand ;檢查CPU是否支援RDRAND
lea rdx,szErrMsg0
or rax,rax ;若RAX=0,不支援,跳至error:處
jz error
invoke GetModuleHandle,0 ;取得模組代碼
mov hInstance,rax
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style,CS_HREDRAW or CS_VREDRAW
lea rdx,WndProc
mov wc.lpfnWndProc,rdx
mov wc.cbClsExtra,0
mov wc.cbWndExtra,0
mov wc.hInstance,rax
invoke LoadIcon,NULL,IDI_APPLICATION ;取得圖示代碼
mov wc.hIcon,rax ;存入圖示代碼
mov wc.hIconSm,rax ;存入小圖示代碼
invoke LoadCursor,NULL,IDC_ARROW ;取得游標代碼
mov wc.hCursor,rax ;存入游標代碼
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,0
lea rdx,szClassName
mov wc.lpszClassName,rdx
invoke RegisterClassEx,ADDR wc ;註冊視窗類別
lea edx,szErrMsg1
.if rax==0
error: invoke MessageBox,0,rdx,OFFSET szErr,MB_ICONERROR or MB_OK
jmp quit
.endif
invoke CreateWindowEx,0,ADDR szClassName,ADDR szAppName,WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,0,0,hInstance,0
mov hWnd,rax
invoke ShowWindow,hWnd,SW_SHOWNORMAL
invoke UpdateWindow,hWnd
.while TRUE
invoke PeekMessage,ADDR msg,0,0,0,PM_REMOVE
.if rax
cmp msg.message,WM_QUIT
je quit
invoke TranslateMessage,ADDR msg
invoke DispatchMessage,ADDR msg
.else
invoke DrawRect,hWnd
.endif
.endw
quit: invoke ExitProcess,0 ;程式結束
main ENDP
;*******************************************************************************
END main |
前面大致已說明過 RNDRECT.ASM 內容了,底下來說說幾個部分。
RNDRECT 要在工作區內產生一矩形,矩形的位置,也就是左上角與右下角的座標,是隨機產生的,那麼其長與寬也會是隨機的。而 x86 指令中,RDRAND 指令就能產生一個亂數。因此 get_rnd 副程式是藉由 RDRAND 指令產生亂數,然後將此亂數存於 EAX 內傳回給主程式。
但是 EAX 最大可達四十二億,視窗的工作區大小跟螢幕大小有關,而現在的螢幕長或寬的像素(可以把像素看成是點)都在一萬以內,以小木偶的電腦螢幕來說,螢幕大小是 1920×1080,就算再過十年,也不會多出多少。因此我們希望由 get_rnd 所獲得的亂數在一定範圍內。所以讓 get_rnd 添加一個參數,此參數限制存於 EAX 內的亂數要小於或等於此參數。
那該如何辦到呢?小木偶的想法是把參數視為二進位數,在這 32 位元中,由最高位元(也就是第 31 位元)開始向低位元,搜尋在哪一個位元首先出現一,記錄其編號。舉例來說,假設參數是 1424,換算成二進位是「0000 0000 0000 0000 0000 0101 1001 0000」,很明顯,在第 10 位元(以紅色表示)開始出現一。利用 BSR 就能求出哪一個位元編號開始出現一。
接下來就是把比出現一的最高位元還要高的所有位元,都變成零,這樣就能達到目的。例如在這個例子,就是讓 EAX 與「0000 0000 0000 0000 0000 0111 1111 1111」做 AND 運算,就能將第 11~31 位元都變成零。現在的問題變成「怎麼計算出 0000 0000 0000 0000 0000 0111 1111 1111」。
利用 SHR 或 SAR 指令,可以將 7FFF FFFF FFFF FFFFH 位移數次,就變成二進位的「0000 0111 1111 1111」(換算成十六進位是 7FFH)。要位移幾次,應該是要用總共 31 位元減去出現開始出現一的那個位元編號,但因為是將 7FFF FFFF FFFF FFFFH 位移,第 31 位元已經是零了,所以應該減去一次,故要位移的次數是 30 減去出現最高位元是一的那個位元編號。
最後把由 RDRAND 製造出來的亂數與 7FFH 做 AND 運算,其結果仍可能大於 range,因此還需要做比較。如果大於就得重新由 RDRAND 製造新的亂數開始,重新再來一遍,但這種機率已經小了許多。
這段 get_rnd 副程式在原始程式的第 65~77 行。
圖形物件也可稱為 GDI 物件,包含了畫筆、畫刷、字形、位元圖、區域以及調色盤等等。畫筆用來繪製線條、畫刷來塗滿區域、字形來繪製文字。這些圖形物件的使用方法大致步驟如下:
這段過程通常是發生在處理 WM_PAINT 訊息時,在呼叫 BeginPaunt 之後,和呼叫 EndPaint 之前。如果要在其他訊息中繪製圖形,那麼可以在呼叫 GetDC 之後,進行上面的四個步驟繪製圖形,然後呼叫 ReleaseDC 結束繪製。
RNDRECT 利用沒有訊息傳入視窗函式的空閒時間繪製隨機矩形,只要程式還沒結束,就能一直繪製圖形。還有一種方法也可以達到程式沒結束就能一直繪製圖形的效果,那就是建立計時器,但是這是下一章的主題。