附錄三 以對話盒為界面的程式


最近有人問我這麼一道數學題:『100!=100×99×98×97×……×2×1 的乘積末位數有幾個零?而由最末一位往前算,第一個非零的數是什麼?』。在數學上的想法很簡單,只要 2×5 就會得到 10,就會使末位多一個零,而在 100!的乘法堙A2 的因數比 5 多得多,所以只要算出 100、99、98……3、2、1 這些數埵陷X個 5 的因數即可。從一到十有 5、10 含有兩個 5 的因數,11 到 20 也有兩個 5 的因數……故每十個數有兩個五,所以 1 到 100 至少有 20 個 5 的因數,除此之外,像 25、50、75、100 這四個數含有兩個 5 的因數,扣除前面已經算過的,只剩 4 個 5 的因數,最後總加起來,100!有24 個因數 5,所以末位數有 24 個 0。

至於 100! 的末位數第一個非零的數是什麼?想法也是以每 10 個數為一組,每組乘起來的末位數都相同,都是 1×2×3×4×……9×10,末位第一個非零的數是 8,810的個位數是 4,故第二個答案是 4。雖然有了結果,但是總想以程式實際算出來答案是否正確,歷經一段思索,最後終於把這個計算階乘的程式,FACTORIAL.ASM,寫好了,於是有了這個附錄。這個程式執行畫面如下,一開始使用者未輸入數字,故沒有計算結果,『拷貝結果』與『存成…』按鈕是禁用的。當使用者輸入好數字,例如 100,再按下『計算』按鈕,FACTORIAL 會計算 100!並將結果顯示在底下的唯讀編輯框堙C

FACTORIAL.EXE 執行畫面

使用者也可以按下『拷貝結果』按鈕把結果拷貝進『剪貼簿 ( clipboard )』,也可以按下『存成100!.TXT』按鈕,把計算結果存到桌面上。如果使用者還要再算另一數的階乘,把滑鼠移到編輯框並按下滑鼠左鍵或改變編輯框的內容時,『拷貝結果』與『存成…』按鈕會變成禁用的樣子,當輸入好數字再按下『計算』按鈕後,『拷貝結果』與『存成…』按鈕又會再度被啟用。

小木偶在撰寫這個程式時,除了這個程式是『以對話盒為界面的程式』,但是實際上還牽涉到底下幾個主題:『大數的乘法』、『把編輯框的資料拷貝到剪貼簿』、『取得桌面路徑』以及『編輯框的通知碼』等,所以不知道如何訂出這篇目錄的題目:)


以對話盒為界面的程式

FACTORIAL 是一個對話盒當成界面的程式,這一類程式本身就是一個對話盒,其內部有許多控制元件組成,它是藉由 DialogBoxParam 產生的,我們在撰寫程式時,只需要在資源描述檔中的對話盒面板段落婸〝這些控制元件如何安排,以及撰寫對話盒函式 ( 對話盒函式與視窗函式都是屬於『call back』函式,關於什麼是 call back 函式,請參考第二章處理訊息的視窗函式第三段 ) 即可。系統就會為我們建立好各控制元件,然後對話盒管理器會呼叫我們自行撰寫的對話盒函式,此對話盒函式將處理我們感興趣的訊息。當然,這些訊息處理完之後,返回值為 TRUE,表示已處理過了;我們不感興趣而不處理的訊息,則返回值為 FALSE,由系統內部的對話盒管理器去處理。在第十章也提到過這類的對話盒,稱為模式對話盒,所不同的是在第十章的模式對話盒是由另一個視窗所建立,而此處以對話盒為界面的程式則是直接呼叫 DialogBoxParam 建立。其大致的骨架像底下這樣:

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
                OPTION  CASEMAP:NONE
                .586
                .MODEL  FLAT,STDCALL
INCLUDE         WINDOWS.INC
INCLUDE         COMCTL32.INC
INCLUDE         KERNEL32.INC
INCLUDE         USER32.INC
INCLUDELIB      COMCTL32.LIB
INCLUDELIB      KERNEL32.LIB
INCLUDELIB      USER32.LIB
;*************************************************************************************************************
.DATA
hInstance       HANDLE  ?
szDlgName       BYTE    "DIALOG",0      ;對話盒面板名稱
;其他資料
;*************************************************************************************************************
.CODE
;-------------------------------------------------------------------------------------------------------------
;對話盒函式
DlgProc         PROC    hDlg:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
.IF uMsg==WM_INITDIALOG
 
.ELSEIF uMsg==WM_COMMAND
 
.ELSEIF uMsg==WM_CLOSE
                INVOKE  EndDialog,hDlg,NULL
 
.ELSE           ;其他未處理的訊息返回 FALSE
                mov     eax,FALSE
                ret
 
.ENDIF          ;已處理的訊息,返回 TRUE
                mov     eax,TRUE   
                ret
DlgProc         ENDP
;-------------------------------------------------------------------------------------------------------------
start:          INVOKE  GetModuleHandle,NULL
                mov     hInstance,eax
                INVOKE  DialogBoxParam,hInstance,OFFSET szDlgName,NULL,OFFSET DlgProc,NULL
                INVOKE  ExitProcess,eax
                INVOKE  InitCommonControls
;*************************************************************************************************************
END             start

上面的程式,就是以對話盒為主的程式,其中僅處理 WM_INITDIALOG 、WM_COMMAND、WM_CLOSE 這三個訊息,處理過此三個訊息後返回 TRUE,否則返回 FALSE。與上述組合語言原始程式之配合的資源描述檔,一般如下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "c:\masm32\include\resource.h"
 
#define  RT_MANIFEST     24
 
//其他控制元件的識別碼
 
對話盒面板名稱  DIALOG  x, y, cx, cy
STYLE   風格
FONT    大小 ,"字型"
CAPTION "標題名"
BEGIN
  控制元件
END
1       RT_MANIFEST MOVEABLE PURE "程式名.exe.manifest"

很明顯,以對話盒為界面的程式省略了像註冊視窗類別、建立訊息迴圈等程式碼,同時也不需要呼叫 DefWindowProc 處理我們不重視的訊息,可以說是方便又簡潔。

雖然如此,但是如果要在視窗中使用圖示,就得在處理 WM_INITDIALOG 訊息時,呼叫 LoadIcon 自資源載入圖示。我們常在處理 WM_INITDIALOG 訊息時,做一些初始化的動作,例如取得控制元件代碼等等,取代一般視窗的 WM_CREATE 訊息。在關閉對話盒時,則是處理 WM_CLOSE 訊息,並在處理此訊息時呼叫 EndDialog 關閉對話盒。此外,以對話盒為界面的程式是由對話盒管理器處理按鍵訊息,因此對於快捷鍵的使用,就得多費工夫了。


大數的乘法

即使是使用 128 位元的乘法指令,也只能算到 39 位數,但是對階乘來講 34!就已經是 39 位數了,34 應該說是很小的數吧?因此以 128 位元的乘法指令,無法滿足需求。如果要算比較大的數的階乘,可以用斯特林公式求得,但是如果要算得一毫不差,就得自行撰寫一個乘法副程式了,而這個乘法副程式能做很大數字的乘法,再利用迴圈就能很快的 ( 也可能是很慢的 ) 求出較大數的階乘。

此一大數乘法副程式稱為 ubcd_mul,其原理很簡單,其實就是小學老師所教的直式乘法。小木偶以 987654×912=900740448 為例觀察:

 被乘數     987654
 乘數     x    912
        ----------
        001975308
 暫存區  009876540
        888888600
        ----------
        900740448

被乘數共有 6 位數,乘數為 3 位數,所得的乘積是以每一位乘數和被乘數相乘所得之結果,先暫時存於暫存區,待三位乘數都輪流與被乘數相乘之後再相加。在暫存區堛漕C一列的位數看起來好像都是被乘數的位數再加一,但實際卻不是如此,考慮之後還要把暫存區中的三列數相加,所以第一列之前應該還有兩個零,同理其他列也都是一樣,因此暫存區的大小應該是『乘積位數×乘數位數』。而乘積位數應該是『被乘數位數+乘數位數』。換句話說,如果要算大數的乘法,暫存區可能需要很大的記憶體,要配置多少記憶體呢?以 987654×912=900740448 為例說明,暫存區要佔用 3×(3+6) 位,乘積要再佔用 (3+6) 位,被乘數與乘數又要再加上 (3+6) 位,故總共要佔用 5×(3+6) 位,如果每個數佔用一個位元組,那麼就得配置 90 個位元組。

FACTORIAL 被設計成可以計算最多 65536 階乘,由『開始』、『所有程式』、『附屬應用程式』『小算盤』可算出 65536!有 287194 位數,亦即 16 進位的 461DAH,但是小木偶打算儲存乘積的記憶體比 461DAH 稍微大一些,是 461E0H,換成十進位就是 287200 個位元組,小木偶在 FACTORIAL 中定義了一個常數,LARGEST,來表示乘積的位數:

        LARGEST EQU     461E0h

FACTORIAL 可以計算五位數 ( 65536 為五位數 ) 以下的階乘,因此暫存區最多僅五列,而每一列需要容納 461E0H 位數,故暫存區需要 461E0H×5 個位元組,再加上乘積得佔用 461E0H 以及乘數佔用 10H 位元組 ( 多一點點也是無妨 ),所以您會在計算階乘的 Factorial 副程式看到呼叫 GlobalAlloc 向系統申請配置記憶體:

        INVOKE  GlobalAlloc,GPTR,LARGEST*6h+10H ;向系統要求LARGEST*6+10H的記憶體大小

FACTORIAL 最後要把結果顯示在一個多行的編輯框控制元件堙A而多行的編輯框控制元件內定可以容納約 32K 位元組 ( 這是指 Win XP 系統而言,精確的數字是 30000 個位元組。有關編輯框控制元件的訊息,請參考第九章的說明 ),不過這個限制是可以更改的,而且作業系統會自動幫應用程式擴大容量,所以並不須擔心多行編輯框容量不足的問題。

由於階乘是連續的乘法,小木偶打算把被乘數放在所申請記憶體的最低位址,乘數放在被乘數之後的較高位址,而所得乘積就放在原來被乘數的地方。如此一來,先使乘積為 1,使乘數為使用者輸入的數,呼叫 ubcd_mul 副程式得到第一次相乘的結果,接著使乘數減一,再呼叫 ubcd_mul 就可得第二次相乘,如此每一次使乘數減一,直到零為止,就可得階乘了。

下圖是以計算 14350 階乘說明記憶體規劃,小木偶向作業系統申請配置了 LARGEST*6h+10H ( 1A4B50H,相當於十進位的 1723216 ) 位元組,如上述所言分成三個區塊:

  1. 被乘數或乘積 ( 由位址 0 到 461DFH,共 287200 位元組 ),以紫色背景表示。
  2. 乘數 ( 由位址 461E0H 到 461EFH,共 16 個位元組 ),以藍色背景表示。
  3. 暫存區 ( 由位址 461F0H 到 1A4B4FH,共 1436000 位元組 ),以黑色背景表示。

在配置記憶體之後,先使乘積為 1,存於此配置記憶體的最低位址,然後把 14350 變成未聚集的 BCD 數,存於所配置記憶體偏移位址的 461E0H 處,此處我假設乘數有 10 位數,接著由乘數高位址開始,尋找第一個非零的數,就可以找到乘數真正位數。然後呼叫 ubcd_mul 副程式,計算其乘積,並存於所配置記憶體的最低位址。換句話說,當 ubcd_mul 返回後,所配置的記憶體最低位址會變成 0、5、3、4、1,其實這就是代表 14350×1 之後的結果,此數存於記憶體位址的 0H 時,是以位數小的數安排在低位址處 ( 像這種安排方式稱為 Little-Endian )。如下圖的上半部所示,是 ubcd_mul 返回後的結果,暫存區中的數值已變得不重要了。

記憶體分配

接著使乘數減一變成 14349,存於記憶體位址 461E0H 處,此時被乘數已存於所配置記憶體位址 0 的地方了,再呼叫 umbc_mul,當返回時,結果如上圖的下半部份。又為使讀者更清楚 ubcd_mul 在記憶體中運作,小木偶列出其直式乘法如下:

       14350
x      14349
-------------
  0000129150
  0000574000
  0004305000
  0057400000
  0143500000
-------------
  0205908150

您可看見,暫存區的最低偏移位址,461F0H,存著的是 14350×9 之後的結果,因 14350×14349 之乘積有 10 位數 ( 假想包含沒有進位的零 ),故 14350×4 存放位址比 14350×9 高 10 個位元組。該圖的下半部藍色的零是大位數部份,橘色的零是小位數的部份,每一個乘數乘被乘數時,有三部份。寫成直式很好分辨,但在記憶體很不容易看得出來。如此每當 ubcd_mul 返回時,使乘數減一,直到為零為止,若乘數為零立即跳出迴圈。

ubcd_mul 副程式是執行兩個未聚集 BCD 數相乘的副程式,這個副程式主要有兩個迴圈組成,第一個迴圈是執行每一位乘數與被乘數相乘後的結果。每一位乘數與被乘數相乘都分成三個部份,請回到上面 987654×912=900740448 的例子,每一位乘數與被乘數相乘時,必須在前面或後面加上若干位數零。因為是使用未聚集的 BCD 數相乘,所以 AAM 指令,再以 AAA 加上進位。

第二個迴圈是使暫存區中的資料相加,詳細情形請參考組合語言的第 13 章第 14 章


把資料拷貝到剪貼簿

要把某些資料拷貝到剪貼簿堙A須進行下面步驟:

  1. 用 OpenClipboard 開啟剪貼簿。
  2. 用 EmptyClipboard 清空剪貼簿。
  3. 用 SetClipboardData 把資料設定進剪貼簿堙C
  4. 用 CloseClipboard 關閉剪貼簿。

OpenClipboard

OpenClipboard 是用來開啟剪貼簿,這個意思並非把剪貼簿的內容顯示在螢幕上,而是表示系統已準備好剪貼簿了。其原型為:

BOOL OpenClipboard(
   HWND   hWndNewOwner  // handle to window opening clipboard  
   );

參數 hWndNewOwner 是某個視窗代碼,這個視窗是指開啟剪貼簿的視窗,亦即此視窗可以存取剪貼簿,假如 hWndNewOwner 是 NULL,表示是呼叫 OpenClipboard 的視窗所開啟。OpenClipboard 如果成功開啟,返回時,EAX 值不等於零;如果失敗,EAX 為 NULL。當應用程式要存取剪貼簿的資料時,必須用 OpenClipboard 開啟剪貼簿;當不需要存取剪貼簿內的資料後,就應該要呼叫 CloseClipboard 關閉剪貼簿。

EmptyClipboard API

第二個步驟是執行 EmptyClipboard,清空剪貼簿,這個過程是把原來在剪貼簿的內容清除。EmptyClipboard 沒有參數。EmptyClipboard 如果成功清除資料,返回時,EAX 值不等於零;如果失敗,EAX 為 NULL。但如果您想使用剪貼簿娷礎釭爾禤ヾA可以不做這一步,這樣的話,如果再把另一份資料移進剪貼簿,那麼舊有的資料並不會消失。

SetClipboardData API

SetClipboardData 是把資料移入剪貼簿堙A其原型為:

HANDLE SetClipboardData(
    UINT    uFormat,    // clipboard format  
    HANDLE  hMem        // data handle 
   );

參數 hMem 是記憶體區塊代碼,它是由 GlobalAlloc API 所配置的記憶體區塊,請參考第 14 章的『配置記憶體』。參數 uFormat 是指移入剪貼簿裡面的資料屬於那一種資料,可以是文字、圖片甚至聲音……,常見的幾種如下:

如果順利的把資料移入剪貼簿,則 SetClipboardData 會傳回非零值;否則傳回 NULL。

CloseClipboard API

CloseClipboard 不需要參數。每當程式改變剪貼簿內的資料,就應該呼叫 CloseClipboard 關閉剪貼簿,使其他應用程式能存取剪貼簿。如果成功的關閉剪貼簿,則 CloseClipboard 會傳回非零值;否則傳回 NULL。


取得桌面路徑

在 Windows 作業系統堙A有一些特殊的資料夾,例如『我的文件』、『啟動』……等等,要取得這些特殊的資料夾在硬碟中的路徑名稱,得先取得一個稱為 item identifier list 的識別碼,然後在以此識別碼取得完整路徑字串。取得 item identifier list 識別碼可用 SHGetSpecialFolderLocation API,這個 API 是包含在 SHELL32.DLL 堶情A因此得包含 SHELL32.INC 及 SHELL32.DLL,其原型如下:

WINSHELLAPI HRESULT WINAPI SHGetSpecialFolderLocation(;
    HWND            hwndOwner,
    int             nFolder,
    LPITEMIDLIST    *ppidl
    );

第一個參數,hwndOwner 指定了所有者視窗,亦即呼叫 SHGetSpecialFolderLocation 的視窗或對話盒。第二個參數是 nFolder,決定要取得那一個子目錄的路徑,nFolder 與其所代表的意義如下表:

nFolder意  義例  子
CSIDL_DESKTOP桌面C:\Documents and Settings\wanker\桌面
CSIDL_DESKTOPDIRECTORY桌面C:\Documents and Settings\wanker\桌面
CSIDL_FONTS字型C:\WINDOWS\Fonts
CSIDL_NETHOOD網路上的芳鄰C:\Documents and Settings\wanker\NetHood
CSIDL_PERSONAL我的文件C:\Documents and Settings\wanker\My Documents
CSIDL_PROGRAMS「開始」功能表的
『所有程式』
C:\Documents and Settings\wanker\「開始」功能表\程式集
CSIDL_RECENT我最近的文件C:\Documents and Settings\wanker\Recent
CSIDL_SENDTO C:\Documents and Settings\wanker\SendTo
CSIDL_STARTMENU「開始」功能表C:\Documents and Settings\wanker\「開始」功能表
CSIDL_STARTUP啟動C:\Documents and Settings\wanker\「開始」功能表\程式集\啟動
CSIDL_SYSTEM C:\WINDOWS\system32
CSIDL_TEMPLATES C:\Documents and Settings\wanker\Templates

第三個參數是 item identifier list 識別碼的位址指標,在呼叫 SHGetSpecialFolderLocation 之前,把這個位址以參數方式傳給 SHGetSpecialFolderLocation,SHGetSpecialFolderLocation 返回時,會把 item identifier list 識別碼存入此位址,如果 SHGetSpecialFolderLocation 成功的取得識別碼,則傳回 S_OK。稍後再以這個識別碼呼叫 SHGetPathFromIDList API 就可以得到特殊資料夾完整的路徑名稱。SHGetPathFromIDList 的原型是:

WINSHELLAPI BOOL WINAPI SHGetPathFromIDList(
    LPCITEMIDLIST       pidl, 	
    LPSTR               pszPath	
   );

參數 pidl 是呼叫 SHGetSpecialFolderLocation 後,所得的 item identifier list 識別碼,pszPath 則是指向特殊資料夾完整的路徑位址,一般而言其大小為 MAX_PATH 位元組 ( 如果您開啟 WINDOWS.INC,可以找到 MAX_PATH 等於 260 )。若 SHGetPathFromIDList 執行成功,則返回 TRUE,並且會把完整路徑名稱存到 pszPath 所指的位址堙C


編輯框與按鈕兩控制元件的通知碼

當編輯框的內容被改變時,也就是使用者改變要求的階乘時,就應該使『拷貝結果』按鈕與『存成…』按鈕禁用,否則所得結果是錯誤的,禁用按鈕或啟用按鈕可以用 EnableWindow 來達到目的,要在那一個時機禁用或啟用,則必須監視編輯框的通知碼。

當使用者把滑鼠移到編輯框並按下滑鼠左鍵時,編輯框會發出 WM_COMMAND 訊息給父視窗,並在 lParam 中填入編輯框的代碼,wParam 的低字組填入編輯框的識別碼 ( identifier ),wParam 的高字組填入通知碼 ( notification )。通知碼是告訴父視窗使用者做了那些動作,以使程式做出相對應的動作。底下是常見的通知碼:

同樣的,按鈕控制元件也會把一些通知碼傳遞給父視窗,以讓父視窗做處理,底下是常用通知碼:

當我們把滑鼠游標移到按鈕上後,按下滑鼠左鍵不放時 ( 或者按鈕得到鍵盤輸入焦點,使用者按下空白鍵不放時 ),這時按鈕會顯示被壓下的樣子,此刻按鈕會發出 BN_PUSHED 通知碼。當放開滑鼠左鍵 ( 或放開空白鍵 ),按鈕會顯示彈起來的樣子,此刻按鈕會發出 BN_UNPUSHED。完成一次壓下後再彈起的過程 ( 即所謂點按 ),按鈕發出 BN_CLICKED 通知碼。

不管什麼風格的按鈕,都會發出 BN_CLICKED,但是只有具有 BS_NOTIFY 風格的按鈕,才會發出 BN_DISABLE、BN_PUSHED、BN_KILLFOCUS、BN_PAINT、BN_SETFOCUS 和 BN_UNPUSHED 通知碼。具有 BS_USERBUTTON、BS_RADIOBUTTON 或 BS_OWNERDRAW 風格的按鈕,會自動發出 BN_DBLCLK 通知碼,而其他風格的按鈕則需要具有 BS_NOTIFY 風格,才會發出 BN_DBLCLK 通知碼。

在 FACTORIAL 程式處理 WM_COMMAND 過程堙A小木偶只需處理按鈕發出的 BN_CLICKED 和編輯框發出的 EN_CHANGE 通知碼,其餘的都不處理。先使通知碼存於 EDX,而控制元件的識別碼存於 EAX,然後先檢查通知碼是 BN_CLICKED 還是 EN_CHANGE。如果是 BN_CLICKED,再檢查那一個按鈕發出的,以便作出相對應的處理;如果是 EN_CHANGE,表示是編輯框發出的,使『拷貝結果』與『存成…』兩按鈕禁用。其結構如下:

.ELSEIF uMsg==WM_COMMAND
                mov     edx,wParam
                mov     eax,wParam
                shr     edx,10h         ;EDX=通知碼
                and     eax,0ffffh      ;EAX=控制元件識別碼
   .IF dx==BN_CLICKED
       .IF ax==IDC_CALC
                ;使用者按下『計算』按鈕
       .ELSEIF ax==IDC_COPY
                ;使用者按下『拷貝結果』按鈕
       .ELSEIF ax==IDC_SAVE
                ;使用者按下『存成…』按鈕
       .ENDIF
   .ELSEIF dx==EN_CHANGE
       .IF ax==IDC_OPERAND
                ;使用者改變編輯框內容
       .ENDIF
   .ENDIF

原始碼

底下就是 FACTORIAL.ASM 的原始碼:

;FACTORIAL.ASM可以計算不大於65536的階乘(65536!=5.1629485231E+287193)
        .586
        .model  flat,stdcall
        option  casemap:none

include         windows.inc
include         user32.inc
include         kernel32.inc
include         shell32.inc
includelib      user32.lib
includelib      kernel32.lib
includelib      shell32.lib

IDC_COPY        EQU     2001
IDC_CALC        EQU     2002
IDC_SAVE        EQU     2003
IDC_OPERAND     EQU     2004
IDC_ANSWER      EQU     2005
IDC_SCIENCE     EQU     2006
IDC_SPEND       EQU     2007

LARGEST         EQU     461E0h  ;461d9h=287193
BIGGEST_NUMBER  EQU     65536
EFFECT          EQU     40
;**********************************************************
.DATA
hInstance       HANDLE  ?
lpTemp          LPSTR   ?
nAnswer         DWORD   ?               ;乘積字串位元組數
hCopy           HANDLE  ?               ;『拷貝結果』按鈕的代碼
hSave           HANDLE  ?               ;『存成…』按鈕代碼
hMemProduct     HANDLE  ?
nBytesWritten   DWORD   ?
dwDesktop       DWORD   ?
lpEndPath       LPSTR   ?
lpEndFileName   LPSTR   ?
szDlgName       BYTE    'Factorial',0
szIcon          BYTE    'Factorial',0
szFormat        BYTE    'E%d',0
szSciNotation   BYTE    EFFECT DUP (0)  ;科學記號(Scientific notation)
szSaveAs        BYTE    '存成',0dh,0ah
szFileName      BYTE    11 DUP (0)
szOverMsg       BYTE    '太大了,無法顯示',0
szPathName      BYTE    MAX_PATH dup (0)
szErrorFile     BYTE    '檔案'
szFileExisted   BYTE    '已存在!',0
szFmtSpend      BYTE    '共花費%d毫秒',0
szSpendTime     BYTE    30 DUP (0)
;***********************************************************
.CODE
;-----------------------------------------------------------
ubcd_add        PROC    USES ebx edx esi edi lpLow1,lpLow2,lpSum,n1,n2
;計算兩非聚集BCD數之和
;輸入:lpLow1:被加數最低位址,以未聚集BCD方式儲存,大位數在高位址
;      lpLow2:加數最低位址,以未聚集BCD方式儲存,大位數在高位址
;      lpSum:和最低位址
;      n1:被加數位數
;      n2:加數位數
;輸出:EAX:為和所儲存的位址,以未聚集BCD方式儲存,大位數在高位址
;      ECX:和的長度,即位數
;      EBX、EDX、ESI、EDI均被保存起來
        mov     esi,lpLow1      ;ESI=被加數最低位址
        mov     ebx,lpLow2      ;EBX=加數最低位址
        mov     eax,n1
        cmp     eax,n2  ;比較被加數與加數那一個位數大
        jae     u_a0
        xchg    eax,n2  ;若加數位數大,則使被加數與加數交換最低位址
        xchg    esi,ebx ;(ESI與EBX),同時也交換位數(n1、n2)
        mov     n1,eax
        mov     lpLow2,esi
        mov     lpLow1,ebx

;開始相加時,ESI指向兩數位數較大者的位址,n1為其位數
u_a0:   sub     eax,n2
        mov     n1,eax  ;n1=重疊部份的位數
        mov     edi,lpSum

;計算重疊部份,例如
;  987654
;+    912
;---------
;  988566
;被加數、加數都有個位、十位、百位數,所以是重疊的部份。
        clc
        sub     eax,eax
u_a1:   lodsb           ;取得加數中的一位
        add     al,ah   ;加上進位的數
        cbw
        add     al,[ebx]
        aaa             ;加法調整,若有進位,會存在AH
        stosb           ;存入和內
        inc     ebx
        dec     n2
        jnz     u_a1
        cmp     n1,0
        jz      u_a3    ;假如被加數與加數位數相同時,n1會等於零

;計算只有較大位數部份,以上面為例,即計算
;  987654
;+ 000912
;---------
;  988566
;987+000的部份
u_a2:   lodsb
        add     al,ah    ;AH=計算重疊部份後的進位
        cbw
        aaa
        stosb
        dec     n1
        jnz     u_a2

;處理最大位數進位
u_a3:   sub     al,al   ;使AL等於零
        add     al,ah   ;AH=進位,加上進位
        or      al,al
        jz      u_a4
        stosb
u_a4:   mov     ecx,edi
        mov     eax,lpSum
        sub     ecx,eax
        ret
ubcd_add        ENDP
;-----------------------------------------------------------
ubcd_mul        PROC    USES ebx esi edi,lpLow1,lpLow2,Product,n1,n2
;計算兩非聚集BCD整數之乘積
;輸入:lpLow1:被乘數最低位址,以未聚集BCD方式儲存,大位數在高位址
;      lpLow2:乘數(multiplier)最低位址,以未聚集BCD方式儲存,大位數在高位址
;      Product:乘積最低位址
;      n1:被乘數位數
;      n2:乘數位數
;輸出:EAX:為乘積所儲存位址,以未聚集BCD方式儲存,大位數在高位址
;      ECX:乘積的位數
;      EDX被破壞
;原理:以987654*912=900740448為例:
;         987654 被乘數,存於所配置記憶體位址的0∼(LARGEST-1)處,也是乘積所存放處,共有n1位數
;       x    912 乘數,存於所配置記憶體位址的LARGEST∼(LARGEST+3FH)處,共有n2位數,此例n2=3
;      ----------
;      zz1975308 暫存區,由所配置記憶體位址的(LARFEST+40H)處開始,對每一位乘數而言
;      z0987654z ,均佔據n1+n2位數,此例n1+n2共9位數,乘積也是9位數
;      8888886zz
;      ----------
;      900740448
;被乘數會被三位乘數乘三次,每一次都可分成三部份:最右邊要填入0,相乘,做左邊也要填入0
;底下的區域變數,lpHi1,lpHi2,nProduct分別表示被乘數最高位址、乘數最高位址、乘積位數
        LOCAL   lpHi1,lpHi2,nProduct:DWORD
        mov     eax,lpLow1      ;計算被乘數、乘數最大位數所在位址,
        mov     ebx,lpLow2      ;並存於lpHi1、lpHi2
        add     eax,n1
        add     ebx,n2
        mov     lpHi1,eax
        mov     lpHi2,ebx
        mov     ecx,n1          ;計算乘積位數,並存於 nProduct
        add     ecx,n2
        mov     nProduct,ecx

;第一個大迴圈,處理每一位乘數去乘被乘數
        mov     edi,lpTemp      ;EDI指向暫存區(存放每一位乘數相乘後的結果)
        mov     esi,lpLow2      ;每次計算乘數一位數乘被乘數結果之迴圈開始
m1:     sub     eax,eax         ;清除EAX、EDX使往後的AAM、AAA指令能正確運算
        mov     edx,eax

        mov     ecx,esi         ;小位數的填零部分,ECX為填零的個數
        sub     ecx,lpLow2
        mov     ebx,lpLow1      ;計算乘數每位數乘被乘數部分
        push    ecx
        jcxz    m3
m2:     mov     [edi],al
        inc     edi
        loop    m2

m3:     mov     al,[ebx]        ;BX指向被乘數
        mul     BYTE PTR [esi]
        aam
        add     al,dh           ;加上前一次的進位
        aaa
        mov     dh,ah           ;進位存於DH
        mov     [edi],al
        inc     ebx
        inc     edi
        cmp     ebx,lpHi1
        jne     m3

        mov     [edi],dh        ;處理進位部分
        inc     edi

        pop     eax             ;處理大位數填零部分
        mov     ecx,nProduct
        sub     ebx,lpLow1
        sub     ecx,eax
        sub     ecx,ebx
        dec     ecx             ;ECX為填零的個數
        jcxz    m5
m4:     mov     BYTE PTR [edi],0
        inc     edi
        loop    m4

m5:     inc     esi             ;指向乘數的下一位
        cmp     esi,lpHi2       ;檢查乘數是否都已算完
        jne     m1

;第二個大迴圈,處理在暫存區中每一位乘數乘積之和
        mov     edi,Product
        mov     ecx,nProduct
        xor     eax,eax
        inc     ecx
        rep     stosb           ;清除乘積的垃圾資料

        mov     ecx,lpLow2
        mov     esi,Product     ;ESI=乘積位址
        sub     lpHi2,ecx       ;lpHi2=乘數位數,即要相加次數
        mov     ebx,lpTemp      ;EBX為暫存區位址
        mov     edx,nProduct
m6:     INVOKE  ubcd_add,esi,ebx,esi,edx,edx    ;ESI=ESI+EBX
        add     ebx,edx
        dec     lpHi2
        jnz     m6
        mov     eax,Product
        mov     ecx,nProduct
        cmp     BYTE PTR [eax+ecx-1],0  ;檢查最高位數是否為零
        jnz     m7
        dec     ecx             ;若沒有進位,則乘積位數減一
m7:     ret
ubcd_mul        ENDP
;-----------------------------------------------------------
;此副程式是計算eax的階乘
;輸入:EAX-要計算階乘的運算元
;      EDX-指定所得結果位址
;輸出:EAX-所得結果位址,此結果以ASCII字串儲存,以0結尾
;      ECX-乘積有幾位數,22!=『1124000727 7776076800 00』有22位數
;      EDX-乘積字串位元組數,22!=『1124000727 7776076800 00』含空白、換行字元佔有24個位元組
;原理:若使用者輸入不是0!,先要求配置的記憶體,記憶體分配如下:
;偏移位址          意義
;0∼LARGEEST-1       被乘數、乘積存放位址
;LARGEST∼LARGEST+10H   乘數存放位址
;LARGEST+0FH∼LARGEST*8H  暫存區
Factorial       PROC    USES esi edi
        LOCAL   factorial:DWORD
        LOCAL   hMemory:HANDLE  ;向系統要求配置的記憶體位址
        LOCAL   lpMul2:LPSTR    ;乘數以未聚集BCD數存於lpMul2所指位址,高位數存於高位址
        LOCAL   lpProduct:LPSTR ;乘積位址
        LOCAL   dwDigMul1,dwDigMul2:DWORD       ;被乘數、乘數位數
        LOCAL   dwTick:DWORD
        mov     factorial,eax   ;要計算階乘的數存於factorial
        mov     lpProduct,edx   ;乘積位址存於lpProduct

        INVOKE  GetTickCount    ;取得時間,自開機到現在經過幾個毫秒
        mov     dwTick,eax

        mov     edx,lpProduct
        mov     ecx,1
        mov     [edx],cx
        cmp     factorial,0     ;若不是求0!,則跳至not_0:處
        jnz     not_0

;若是計算0!,則結果為0!=1
        add     BYTE PTR [edx],'0'
        mov     DWORD PTR [szSciNotation],45302e31h
        push    edx
        mov     WORD PTR [szSciNotation+4],30h
        INVOKE  GetTickCount
        sub     eax,dwTick
        INVOKE  wsprintf,OFFSET szSpendTime,OFFSET szFmtSpend,eax
        sub     ecx,ecx
        pop     eax
        inc     ecx
        mov     edx,ecx
        inc     edx
        jmp     finish

;若不是計算0!
not_0:  mov     dwDigMul1,ecx                   ;乘積位數一開始只有一位,其實乘積就是1
        INVOKE  GlobalAlloc,GPTR,LARGEST*6h+10H ;向系統要求LARGEST*6+10H的記憶體大小
        mov     hMemory,eax
        mov     BYTE PTR [eax],1;先使乘積為1
        add     eax,LARGEST     ;所配置的記憶體中,最低的LARGEST位元組為被乘數存放處
        mov     lpMul2,eax
        add     eax,10h         ;比被乘數高位址的10H位元組為乘數的存放處
        mov     lpTemp,eax      ;暫存區位址

;把乘數以未聚集BCD數存入lpMul2所指位址,高位址存放高位數
next:   mov     eax,factorial
        mov     edi,lpMul2
        mov     ebx,10          ;最多只能求10位數的階乘
        mov     ecx,ebx
@@:     sub     edx,edx
        div     ebx
        mov     [edi],dl
        inc     edi
        loop    @b
        dec     edi
        std
        mov     ecx,ebx         ;由乘數高位址開始,尋找第一個非零的數
        repe    scasb
        inc     ecx
        cld
        mov     dwDigMul2,ecx   ;ECX=乘數位數

;大數乘法,hMemory=hMomory*lpMul2
        INVOKE  ubcd_mul,hMemory,lpMul2,hMemory,dwDigMul1,ecx
        mov     dwDigMul1,ecx   ;下次的被乘數位數就是這次的乘積位數
        dec     factorial
        jnz     next

;把位址hMemory所存的乘積,以Big-Endian、ASCII字串存入lpProduct所指位址
;位址hMemory的乘積是以Little-Endian、未聚集BCD方式儲存
        mov     esi,hMemory
        mov     edi,lpProduct
        add     esi,ecx
        xor     edx,edx
        push    esi
@@:     dec     esi
        mov     al,[esi]
        or      al,'0'
        inc     edx
        stosb
  .IF (dl==10)||(dl==20)||(dl==30)||(dl==40)
        mov     al,20h          ;每10位數字以空白隔開
        stosb
  .ELSEIF dl==50
        xor     edx,edx
        mov     ax,0a0dh        ;每50位數字換行
        stosw
  .ENDIF
        loop    @b
        mov     al,cl           ;AL=NULL字元
        stosb
        sub     edi,lpProduct
        mov     dwDigMul2,edi   ;EDI=乘積字串位元組數,暫存於dwDigMul2

;把位址hMemory所存的乘積,以科學記號方式(ASCII字串)存於szSciNotation所指位址
        pop     esi
        mov     ecx,dwDigMul1
        mov     edi,OFFSET szSciNotation
        dec     esi
        mov     al,[esi]
        dec     ecx
        add     al,'0'
        dec     esi
        mov     ah,'.'
        stosw
        mov     edx,ecx
  .IF ecx>=EFFECT-10            ;若乘積位數大於(EFFECT-10)位,
        mov     ecx,EFFECT-10   ;僅印出前(EFFECT-10)位(包含一位整數)
  .ELSEIF ecx==0
        mov     al,'0'
        stosb
        jmp     ok
  .ENDIF
@@:     mov     al,[esi]
        add     al,'0'
        stosb
        dec     esi
        loop    @b
ok:     INVOKE  wsprintf,edi,OFFSET szFormat,edx
        INVOKE  GetTickCount
        sub     eax,dwTick
        INVOKE  wsprintf,OFFSET szSpendTime,OFFSET szFmtSpend,eax
        INVOKE  GlobalFree,hMemory
        mov     eax,lpProduct
        mov     ecx,dwDigMul1   ;ECX=乘積位數
        mov     edx,dwDigMul2   ;EDX=乘積字串位元組數
finish: ret
Factorial       ENDP
;-----------------------------------------------------------
DlgProc PROC    hDlg:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
                LOCAL   hMem:HANDLE,hFile:HANDLE
.IF uMsg==WM_INITDIALOG
        ;載入圖示
                INVOKE  LoadIcon,hInstance,OFFSET szIcon
                INVOKE  SendMessage,hDlg,WM_SETICON,ICON_SMALL,eax
        ;取得『拷貝結果』、『存成…』按鈕代碼,並使其禁用
                INVOKE  GetDlgItem,hDlg,IDC_COPY
                mov     hCopy,eax
                INVOKE  EnableWindow,eax,FALSE
                INVOKE  GetDlgItem,hDlg,IDC_SAVE
                mov     hSave,eax
                INVOKE  EnableWindow,eax,FALSE
        ;配置321658個位元組的記憶體空間,以存放乘積,乘積最多為287194位數,而每10位數
        ;多填一個空格,每50位數多一個換行字元(兩個位元組),故每50位數多6個位元組
        ;287194÷50=5743……44   50位數一行,共有5744行
        ;287194+5744×6=321658   每行多6個位元組,故多了5744×6個位元組
                INVOKE  GlobalAlloc,GMEM_SHARE or GPTR,321658
                mov     hMemProduct,eax
        ;取得使用者桌面路徑
                INVOKE  SHGetSpecialFolderLocation,hDlg,CSIDL_DESKTOP,OFFSET dwDesktop
                INVOKE  SHGetPathFromIDList,dwDesktop,OFFSET szPathName
                mov     edx,OFFSET szPathName
@@:             cmp     BYTE PTR [edx],0
                je      found_zero
                inc     edx
                jmp     @b
found_zero:     mov     BYTE PTR [edx],'\'
                inc     edx
                mov     lpEndPath,edx

.ELSEIF uMsg==WM_COMMAND
                mov     edx,wParam
                mov     eax,wParam
                shr     edx,10h         ;EDX=通知碼
                and     eax,0ffffh      ;EAX=控制元件識別碼
   .IF dx==BN_CLICKED
       .IF ax==IDC_CALC
                INVOKE  GetDlgItemInt,hDlg,IDC_OPERAND,NULL,FALSE
           .IF eax<=BIGGEST_NUMBER
                mov     edx,hMemProduct
                call    Factorial
                mov     nAnswer,edx             ;nAnswer=乘積字串位元組數
                push    eax
                INVOKE  EnableWindow,hCopy,TRUE
                INVOKE  EnableWindow,hSave,TRUE
                pop     eax
                mov     edx,OFFSET szSciNotation
           .ELSE
                mov     eax,OFFSET szOverMsg
                mov     nAnswer,0               ;nAnswer=0
                mov     edx,eax                 ;EDX=科學記號字串位址
           .ENDIF
                push    edx
                INVOKE  SetDlgItemText,hDlg,IDC_ANSWER,eax
                push    IDC_SCIENCE
                push    hDlg
                call    SetDlgItemText
                INVOKE  GetDlgItemText,hDlg,IDC_OPERAND,OFFSET szFileName,8
                mov     edx,OFFSET szFileName
                mov     ecx,lpEndPath
@@:             mov     ah,[edx]
                mov     [ecx],ah
                inc     edx
                inc     ecx
                dec     al
                jnz     @b
                mov     DWORD PTR[edx],58542e21h        ;填入檔名的其餘部份
                mov     WORD PTR [edx+4],54h            ;,『!.TXT』,0
                mov     DWORD PTR[ecx],58542e21h
                add     ecx,4
                mov     WORD PTR [ecx],54h
                inc     ecx
                mov     lpEndFileName,ecx
                INVOKE  SetDlgItemText,hDlg,IDC_SAVE,OFFSET szSaveAs
                INVOKE  SetDlgItemText,hDlg,IDC_SPEND,OFFSET szSpendTime
       .ELSEIF ax==IDC_COPY
                INVOKE  OpenClipboard,NULL      ;開啟剪貼簿
                call    EmptyClipboard          ;清空剪貼簿
                INVOKE  GlobalAlloc,GMEM_SHARE or GPTR,nAnswer
                mov     esi,hMemProduct
                mov     edi,eax
                mov     hMem,eax
                mov     ecx,nAnswer
                rep     movsb
                INVOKE  SetClipboardData,CF_TEXT,eax
                INVOKE  GlobalFree,hMem
                call    CloseClipboard
       .ELSEIF ax==IDC_SAVE
                INVOKE  CreateFile,OFFSET szPathName,GENERIC_WRITE,0,0,CREATE_NEW,\
                        FILE_ATTRIBUTE_NORMAL or FILE_FLAG_WRITE_THROUGH,NULL
           .IF eax==INVALID_HANDLE_VALUE
                mov     ah,LENGTHOF szFileExisted
                mov     ecx,OFFSET szFileExisted
                mov     edx,lpEndFileName
@@:             mov     al,[ecx]
                mov     [edx],al
                inc     ecx
                inc     edx
                dec     ah
                jnz     @b
                INVOKE  MessageBox,hDlg,OFFSET szPathName,OFFSET szErrorFile,MB_OK
           .ELSE
                mov     hFile,eax
                INVOKE  WriteFile,eax,hMemProduct,nAnswer,OFFSET nBytesWritten,NULL
                INVOKE  CloseHandle,hFile
           .ENDIF
                INVOKE  EnableWindow,hSave,FALSE
       .ENDIF
   .ELSEIF dx==EN_CHANGE
       .IF ax==IDC_OPERAND
                INVOKE  EnableWindow,hCopy,FALSE
                INVOKE  EnableWindow,hSave,FALSE
       .ENDIF
   .ENDIF

.ELSEIF uMsg==WM_CLOSE
                INVOKE  GlobalFree,hMemProduct
                INVOKE  EndDialog,hDlg,NULL

.ELSE
        mov     eax,FALSE
        ret
.ENDIF
        mov     eax,TRUE
        ret
DlgProc	ENDP
;-----------------------------------------------------------
start:  INVOKE  GetModuleHandle,NULL
        mov     hInstance,eax
        INVOKE  DialogBoxParam,hInstance,OFFSET szDlgName,NULL,\
                OFFSET DlgProc,NULL
        INVOKE  ExitProcess,eax
;***********************************************************
        END     start

底下是 FACTORIAL.RC 檔的內容:

#include "c:\masm32\include\resource.h"

#define IDC_COPY    2001
#define IDC_CALC    2002
#define IDC_SAVE    2003
#define IDC_OPERAND 2004
#define IDC_ANSWER  2005
#define IDC_SCIENCE 2006
#define IDC_SPEND   2007

Factorial       ICON    exclamation01.ico

Factorial       DIALOG  6, 15, 280, 132
STYLE           DS_MODALFRAME|WS_POPUP|WS_VISIBLE|WS_CAPTION|WS_SYSMENU
CAPTION         "計算某整數階乘"
FONT            8,"MS Sans Serif"
{
 CONTROL        "! =",         -1,"static",SS_LEFT|WS_CHILD|WS_VISIBLE|WS_GROUP, 63, 12, 30, 14
 CONTROL        ""   ,IDC_SCIENCE,"static",SS_LEFT|WS_CHILD|WS_VISIBLE|WS_GROUP, 77, 12,160, 14
 RTEXT          "=",     -1, 6, 27, 15, 12
 EDITTEXT       IDC_OPERAND,23, 10, 38, 12, WS_TABSTOP|ES_NUMBER|ES_RIGHT
 EDITTEXT       IDC_ANSWER, 23, 27,238, 55, WS_BORDER|WS_TABSTOP|ES_MULTILINE|ES_AUTOVSCROLL|
                                            ES_READONLY|WS_VSCROLL
 CONTROL        "",IDC_SPEND,"static",SS_LEFT|WS_CHILD|WS_VISIBLE|WS_GROUP,23,87,100,14
 DEFPUSHBUTTON  "計算"       ,IDC_CALC, 10,103, 80, 22
 PUSHBUTTON     "拷貝結果"   ,IDC_COPY,100,103, 80, 22
 PUSHBUTTON     "存成\n!.TXT",IDC_SAVE,190,103, 80, 22,BS_MULTILINE
}

圖示檔,exclamation01.ico,可以按下下載,有了 FACTORIAL.ASM、FACTORIAL.RC、以及 exclamation01.ico 三個檔案,把它們都放在同一個子目錄,例如都放在『E:\Homepage\SOURCE\FACTORIAL\』堙A就可以依底下步驟組譯而得到 FACTORIAL.EXE 可執行檔:

E:\>cd homepage\source\factorial [Enter]

E:\HomePage\SOURCE\FACTORIAL>rc factorial.rc [Enter]

E:\HomePage\SOURCE\FACTORIAL>ml factorial.asm /link factorial.res [Enter]
Microsoft (R) Macro Assembler Version 6.14.8444
Copyright (C) Microsoft Corp 1981-1997.  All rights reserved.

 Assembling: FACTORIAL.ASM
Microsoft (R) Incremental Linker Version 5.12.8078
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

/SUBSYSTEM:WINDOWS
"FACTORIAL.obj"
"/OUT:FACTORIAL.exe"
"FACTORIAL.res"

E:\HomePage\SOURCE\FACTORIAL>

GetTickCount

這個 API 是用來計算電腦從系統開始運作到目前為止經過了多少毫秒,並把這個時間存在 EAX 媔レ^來。因為結果是以 32 位元大小記錄,所以最多可以記錄到 49.7 天,如果超過則歸零開始計時。此外這個 API 沒有參數。

小木偶利用這個 API 測量計算某數的階乘花了多少時間。小木偶所使用的 CPU 是 AMD Athlon 64X2 3600+,計算 100!似乎不須花什麼時間,各位讀者可看本章一開始的圖就知道;計算 1000!只花了 0.14 秒;計算 10000!花了 11.9 秒;但是計算 65536!竟花了 13 分 4 秒 (用另一台 CPU 是 Core(TM) Due T7250 2.00GHz 的筆記型電腦費時 6 分 57 秒 )。可見所花的時間並非成線性等比關係,而是花更多倍的時間。