小木偶在前一章講了有關以檔案代碼方式的處理檔案時,常用的 DOS 服務程式;當然不能光說不練,所以小木偶在這一章寫了一個範例,LISTFILE.ASM。此程式可以把目前目錄底下的所有檔案列出來,包含具有唯讀、隱藏、系統屬性的檔案。先看看下圖組譯、連結及執行後的畫面,最下面兩個黃色框框是 LISTFILE.EXE 執行後的結果,分成兩欄: 上圖最後一個指令,將螢幕切換成 40×25 彩色文字模式,這時 DOS 會清除螢幕,然後我們再次執行 LISTFILE.EXE,如下圖只有一欄:
要列出一個目錄內的所有檔案,必須先呼叫 AH=4EH/INT 21H(此 DOS 服務程式也稱為「find first matching file」),如果成功,DOS 會清除進位旗標(carry flag,簡稱 CF),並把第一個符合的檔案名稱連同檔案屬性、大小、最後修改的日期與時間,填入磁碟傳輸區(DTA)內。如果失敗,DOS 會設定進位旗標,當然在 DTA 內就不會有資料。
這裡所謂的「符合的檔案名稱」是指符合 DS:DX 位址上的字串,此處的字串是檔案的名稱,可以包含「?」與「*」兩個萬用字元,因此可能有很多檔案符合。設定進位旗標的意思是使進位旗標變為一;清除進位旗標的意思是使進位旗標變為零。進位旗標是旗標暫存器的第零位元,旗標暫存器是在 x86 CPU 裡面的一個暫存器。
接著要找第二個以及更多的檔案,要在呼叫 AH=4EH/INT 21H 且呼叫成功之後,再呼叫 AH=4FH/INT 21H(此 DOS 服務程式也稱為「find next matching file」)。AH=4FH/INT 21H 如果失敗,DOS 就會設定進位旗標,可以檢查錯誤碼 AX 是否為 12H,表示已沒有符合的檔案,到此整個搜尋過程結束;如果成功找到下個符合的檔案,也會清除進位旗標,同時更新 DTA 變為新找到符合檔案名稱的該檔案資料,等處理完這些資料須再呼叫 AH=4FH/INT 21H,檢查進位旗標,並重複這個步驟直到失敗為止。這些流程,可以參考右圖:
根據上面的思路,小木偶寫出以下的程式:
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 156
157 158 159 160
161 162 163 164
165 166 167 168
169 170 171 172 |
;List File能列出當前目錄的檔案資訊,如檔名、檔案大小、最後修改日期與時間
;組譯:ml listfile.asm
.MODEL SMALL,C
.386
.STACK
;*******************************************************************************
.DATA
files DB "*.*",0 ;尋找符合檔案,即所有檔案
buffer DB 40 DUP (?),"$" ;包含檔名、檔案大小、最後修改日期與時間的字串
dta DB 43 DUP (?) ;DISK TRANSFER AREA
pause DB "Press any key to continue . . .$"
cr_lf DB 0dh,0ah,"$"
n_files DB 0 ;已印出多少個檔案了
max_f DB 46 ;假設為80x25模式,螢幕畫面最多顯示46個檔案
;*******************************************************************************
.CODE
;-------------------------------------------------------------------------------
;改良式除法,division,能儘量避免除法溢位
;輸入:DX:AX-被除數
; CX-除數
;輸出:DX:AX-商
; CX-餘數
division PROC
LOCAL temp:WORD ;暫時存放原先被除數的低字組
LOCAL quotient:WORD ;暫時存放商的高字組
mov temp,ax ;保存原先被除數的低字組
mov ax,dx
sub dx,dx
div cx ;第一次除法的餘數為第二次除法的高字組
mov quotient,ax ;第一次除法後的商為最後商的高字組,先保存起來
mov ax,temp ;取回原先被除數的低字組當做第二次除法的低字組
div cx
mov cx,dx ;第二次除法的餘數就是最後的餘數
mov dx,quotient
ret
division ENDP
;-------------------------------------------------------------------------------
;把AX內的十六進位數變成兩位數的十進位數字串,BX所指的位址是存放個位數,[BX-1]存
;放十位數。用於處理月份、日、時、分。
two_digits PROC
xor dx,dx
mov cx,10
call division
add cl,"0"
add al,"0"
mov [bx],cl
mov [bx-1],al
sub bx,2 ;BX-2預備存放「.」或「:」
ret
two_digits ENDP
;-------------------------------------------------------------------------------
;製作像「CHTMSDNX.ISO2537519104 2020.02.21 15:10」的字串,分別是
; 檔名 檔案大小 年份 月 日 時 分
;輸入:buffer-用於儲存此字串
; dta-檔案資料來源
print PROC
LOCAL digits:WORD ;位數,例如198是三位數,用於換算成十進位時的位數
cld
;初始buffer字串,全部填入空白字元
mov al," "
mov cx,SIZEOF buffer-1 ;扣去「$」,故要減去一
lea edi,buffer
rep stosb
;把檔名存入buffer的第零個∼第11個位元組內,長度12個位元組
mov di,OFFSET buffer
mov si,OFFSET dta+30
mov cx,13 ;ASCIZ檔名共13位元組
f_name: lodsb
or al,al
jz f_size
stosb
loop f_name
;把檔案大小存入buffer的第12個∼第21個位元組,長度10個位元組
f_size: mov si,OFFSET dta+26
mov bx,OFFSET buffer+21
mov digits,10
lodsw
mov dx,[si] ;DX:AX=檔案大小,單位是位元組
.WHILE digits
mov cx,10 ;除數=CX
call division
add cl,"0" ;餘數=CX
mov [bx],cl
.BREAK .IF ax==0
dec bx
dec digits
.ENDW
;把檔案最後修改日期存入buffer的第23個∼第32個位元組
mov si,OFFSET dta+22
mov ax,[si+2]
and ax,11111b
mov bx,OFFSET buffer+32
call two_digits ;處理日
mov BYTE PTR [bx],"."
dec bx
mov ax,[si+2]
shr ax,5
and ax,1111b
call two_digits ;處理月份
mov BYTE PTR [bx],"."
dec bx
mov ax,[si+2]
shr ax,9
add ax,1980
mov digits,4 ;西元年份有四位數
xor dx,dx
.WHILE digits
mov cx,10
call division
add cl,"0"
mov [bx],cl
dec bx
dec digits
.ENDW
;把檔案最後修改時間存入buffer的第34個∼第38個位元組
mov ax,[si]
shr ax,5
and ax,111111b
mov bx,OFFSET buffer+38
call two_digits
mov BYTE PTR [bx],":"
mov ax,[si]
dec bx
shr ax,11
call two_digits
ret
print ENDP
;-------------------------------------------------------------------------------
.STARTUP
mov ax,ds
mov es,ax
;呼叫AH=1Ah/INT 21h,設定DTA
mov ah,1ah
mov dx,OFFSET dta
int 21h
;呼叫AH=0FH/INT 10H,檢查螢幕是在80x25模式還是40x25模式
mov ah,0fh
int 10h
cmp ah,40
jnz search
mov max_f,23
;呼叫AH=4Eh/INT 21h,尋找第一個符合的檔案
search: mov ah,4eh
mov dx,OFFSET files
mov cx,27h ;尋找具有唯讀、隱藏、系統、保存屬性的檔案
int 21h ;尋找第一個符合「*.*」的檔案,其資料存入dta
jc ok
nxt: call print ;把要印在螢幕上的字串內容計算出來
mov dx,OFFSET buffer
mov ah,9
int 21h
inc n_files
mov cl,max_f
cmp n_files,cl ;檢查是否已經填滿個螢幕了
jb go_on ;若沒有,跳至go_on:處繼續;若已填滿,繼續下個指令
mov dx,OFFSET pause ;印出「Press any key to continue . . .」
mov ah,9
int 21h
mov ah,0
mov n_files,ah
int 16h ;等使用者按下任意鍵
mov dx,OFFSET cr_lf
mov ah,9
int 21h ;換行
go_on: mov ah,4fh ;尋找下一個符合「*.*」的檔案
int 21h
jnc nxt ;若有找到,跳至nxt:處;若找不到,結束程式
ok: mov ax,4c00h
int 21h
.EXIT 0
;*******************************************************************************
END |
不管彩色螢幕或單色螢幕都有許多種顯示模式,但最常用的是底下兩種文字模式:①80×25 與②40×25,因為每個 ASCII 字元佔用一個位元組,所以小木偶打算每個檔案的資料,包含檔名、檔案大小、最後修改的日期與時間,都限制在 40 個字元,也就是說每個檔案的資料有 40 個位元組的空間,不多也不少,其分配方式如下圖,而執行結果就在本章一開始的圖片:
這樣安排還有個好處。小木偶用 AH=09H/INT 21H 顯示檔案資料,這個 DOS 服務程式有兩個特性:①如果要顯示的字串太長,在顯示完一列之後,其餘的部分會在下一列顯示,相當於換行;②假如上次已經在螢幕上顯示到最右側的那一行,那麼下次顯示時,也會到下一列顯示。因為此利用這兩個特性,只要每次顯示 40 個字元,不需要額外程式碼就能排版得整整齊齊的。差別只在於,80×25 模式會顯示兩欄,40×25 模式的只會顯示一欄。
雖然排版方式並不會隨 80 行還是 40 行顯示模式改變,但是如果一個目錄裡的檔案太多,必須在顯示 23 列時讓它暫停顯示,等使用者觀察完再繼續。
如果是 80 行模式,23 列會顯示 46 個檔案,而 40 行模式就只能顯示 23 個檔案。小木偶在 LISTFILE.ASM 的第 14 行定義變數 max_f,記錄每個畫面最多顯示幾個檔案,此處先假設使用者是在 80×25 模式之中執行,故先假設為 46。到了程式第 137∼142 行檢查顯示模式之後,再確定 max_f 是 46 還是 23,前者是 80×25 模式,後者是 40×25 模式。
此外,小木偶在程式第 13 行定義一個變數 n_files,記錄已經處理過多少的檔案了。當然 n_files 必須從零開始,每處理過一個檔案,就會使 n_files 增加一 ( 程式第 152 行 ),然後與 max_f 比較,如果 n_files 比較小,就繼續呼叫 AH=4FH/INT 21H 尋找下一個符合的檔案;如果相等,就執行第 156∼164 行之間的程式,包含①印出「Press any key to continue . . .」字串,②等使用者按鍵,③使 n_files 歸零,④換行四件事。
一般獲得顯示模式的方法是呼叫 AH=0FH/INT 10H,它的用法是
獲得現在的顯示模式 ( get current video mode ) 輸入:AH=0FH 執行:INT 10H 返回:AH=每一列的字元個數 AL=顯示模式 ( 常見的有七種,與第十六章的「設定顯示模式」中的顯示模式相同 ) BH=目前的顯示頁
呼叫 AH=0FH/INT 10H 之後,AH 暫存器內便會傳回來每一列有多少字元,只有兩種結果 40 或 80。
BH 為目前的顯示頁。視訊記憶體可劃分為幾塊區域,這幾塊區域可視為許多顯示頁,每次顯示在螢幕上的只是其中一頁。利用這個特性,我們可以在 CPU 空閒之餘事先計算待會要顯示在螢幕上的資料,存入下一個顯示頁,等到要顯示時進行顯示頁切換即可,非常快速。例如 IBP PC 配置的是 CGA 顯示卡,上面的視訊記憶體有 16KB,而在 80×25 文字模式下,一個螢幕的畫面需用去 80×25×2=4KB,故可分為四個顯示頁,平常顯示第 0 頁,但程式可以先計算好第 1 頁,等要顯示時切換即可。
但在 LISTFILE.ASM 用不到顯示頁,只要檢查 AH 中每一列的字元個數,在第 139 行檢查 AH 是否為 40,如果是的話,就把 max_f 變為 23;如果不是,就維持原來在第 14 行的預設值,46。
小木偶利用 DOS 服務程式 AH=4EH/INT 21H 或 AH=4FH/INT 21H 取得檔案名稱、檔案大小等資料,這些資料會存放在磁碟傳輸區 ( disk transfer area,縮寫為 DTA ) 中。在程式一開始執行時,DOS 預設的磁碟傳輸區在程式前置區 ( program segment prefix,縮寫為 PSP ) 中,PSP 是一塊 128 個位元組的記憶體,程式一開始執行時,DS、ES 暫存起都指向 PSP。磁碟傳輸區就在 PSP 的偏移位址 0080H 處,這裡同時也是在 DOS 的提示字元後,使用者輸入指令之後,參數存放的地方。可以做底下的實驗,以驗證這個說法。
在 DOS 提示字元下 ( 提示字元就是底下的「E:\DOS\FINDFILE>」) 輸入 ( 本來每輸入一道指令都要按「Enter」鍵,但為了怕誤會,底下都沒寫出「Enter」鍵來 )
E:\DOS\FINDFILE>c:\tools\symdeb listfile.exe /"I love assembly!" Microsoft (R) Symbolic Debug Utility Version 4.00 Copyright (C) Microsoft Corp 1984, 1985. All rights reserved. Processor is [80286] -R [Enter] AX=0000 BX=0000 CX=0186 DX=0000 SP=0400 BP=0000 SI=0000 DI=0000 DS=20D6 ES=20D6 SS=20FF CS=20E6 IP=00EC NV UP EI PL NZ NA PO NC 20E6:00EC B8F820 MOV AX,20F8 -D ES:80 L20 [Enter] 20D6:0080 14 20 2F 22 49 20 6C 6F-76 65 20 61 73 73 65 6D . /"I love assem 20D6:0090 62 6C 79 21 22 0D 00 00-00 00 00 00 00 00 00 00 bly!"...........
上面的例子是用 SYMDEB 載入 LISTFILE.EXE 除錯,雖然 LISTFILE.EXE 不需要參數,但是輸入「 /"I love assembly!"」作為參數未嘗不可。你會看到,DOS 把這些參數原封不動的被 DOS 搬到 PSP:0080,也就是白色字所標示的地方。要說明的是 PSP:0080 這個位址上的位元組是使用者輸入的參數長度,不包含最後的「Enter」( 即 0DH ),單位是位元組。
由以上觀察,可以知道 PSP:0080 處既是指令的參數存放的空間,也是 DTA 所佔用的空間,兩者相衝突,再者為了方便可以直接以 DS 存取 AH=4EH/INT 21H 或 AH=4FH/INT 21H 傳回來的資料,因此小木偶呼叫 AH=1AH/INT 21H 重新設置 DTA,見 LISTFILE.ASM 的第 130∼135 行。
print 副程式是把 DTA 內的資料「印」在 buffer 字串內,這些資料包含三項:①檔名、②檔案大小、③檔案最後修改的日期與時間。
由於檔名長度並不固定,因此如果前一個檔名太長而後一個檔名太短,就會在 buffer 內留下上個檔案的殘存資料。要避免發生這種情況,可以在填入檔名之前先填入空白,見第 59∼63 行。接下來的第 64∼72 行,把檔名由 DTA 的第 30 個位元組開始搬移到 buffer 字串的起頭。檔名的長度,包含主、副檔名、「.」及最後一個 0,最多 13 個位元組,但是只要檢測到 0,就表示檔名已經結束了。
第 73∼87 行把檔案大小存入 buffer 的第 12∼21 個位元組,共十個位元組。DOS 僅支援 FAT12 與 FAT16 檔案系統,單一個檔案最大可達 4GB,有十位數。這段程式的原理可參考第七章,這些數值是由個位數、十位數……往大數填,但是在記憶體位址卻是往低位址填,所以你會看到第 75 行,BX 先指向 buffer 的第 21 個位元組。
第 88∼114 行是計算檔案最後修改的日期,此日期會被填入 buffer 的第23個∼第32個位元組,與檔案大小中間隔一個空白。日期格式是「YYYY.MM.DD」,西元年四位、月份、日各兩位,中間以「.」隔開。跟檔案大小一樣,也是先指向高位址,先處理日的個位數、十位數,再計算月份、西元年份。
第 115∼125 行是計算檔案最後修改的時間,其格式是「HH:MM」,跟上面處理檔案最後修改的日期差不多,小木偶就不再贅述。綜觀這兩項,你會發現月份、日、時、分都是兩位數,因此處理方式類似,差別在於分隔符號。所以小木偶特地為了這四個數值撰寫處理兩位數的副程式,two_digits。