Ch 19 磁片地圖

在這一章堙A小木偶將撰寫一個可以把三又二分之一英吋軟碟的磁區使用情形,顯示在螢幕上的程式,我稱之為磁片地圖,這個程式的原理前一章已經詳述過了,您可能得隨時參考前一章。先說明這個程式會用到的中斷服務程式。

此程式用到的中斷服務程式

INT 25H/INT 26H

這兩個是 DOS 所提供的服務程式,INT 25H 的功用是讀取絕對磁區,而 INT 26H 是寫入絕對磁區。所謂絕對磁區就是前章所說的邏輯磁區,以 1.44MB 3.5 英吋軟碟片來說,由編號 0 到編號 2879 磁區總共有 2880 個磁區。但下面所提到的 INT 25H/26H 服務程式並不限於軟碟,硬碟也可使用。

當呼叫 INT 25H 中斷之前,AL 代表欲讀取磁碟機,0 表示 A: 磁碟機,1 表示 B: 磁碟機,2 表示 C: 磁碟機其餘依此類推,CX 表示欲讀取之磁區數,DX 表示欲讀取之開始磁區編號,而 DS:BX 是表示由磁片讀出之資料存放的位址。把上述 AL、BX、CX、DX、DS 設定好之後呼叫 INT 25H 執行,假如讀取成功則進位旗標會被清除 (NC),否則進位旗標會被設定 (CY),這時 AH 為錯誤碼,如下表:

80H  回應失敗         10H  資料錯誤         03H  寫入保護
40H  尋找動作失敗     08H  DMA 錯誤         02H  不良位址記號
20H  控制器故障       04H  找不到磁區       01H  不良命令

假如您用 INT 25H 在 Win 9x/Me 中讀取 FAT32 分割,則此時進位旗標也會設定,並且 AX=0207H。

但是以前述之方式讀取磁區有個大缺點,因 DX 最多只能有 16 位元,所以最多只能讀取 0FFFFH 個磁區編號。由每磁區有 512 個位元組,換算成磁碟容量,最多只能處理 32MB 以下的磁碟,但隨著硬碟越來越大,顯然這個限制必須突破,到了 DOS 3.31 之後,INT 25H 重新改寫,它的輸入暫存器稍有不同。假如 CX 之值不為 0FFFFH,則呼叫方式如同前述;假如 CX 之值為 0FFFFH,則 DS:BX 不是指向資料存放處,而是指向一塊 10 個位元組的記憶體空間,這塊記憶體空間才是代表真正的輸入參數。此記憶體空間的前兩個字組代表欲讀取之開始磁區編號,接下來的一個字組為欲讀取之磁區數,再下來的兩個字組為存放位址。如下表:

        dd      ?       ;sector number
        dw      ?       ;number of sectors to read
        dd      ?       ;transfer address

DD 的意思和 DB、DW 類似,是定義 2 個字組的空間,也就是 4 個位元組,中文稱為『雙字組』。如果表示位址,高位址表示區段,但是卻寫在後面。

INT 26H 是用來把資料寫入磁區的,其輸入參數方式和 INT 25H 相同,也是分成兩種,就不贅述了。

AX=7305H/INT 21H

到了 Win 9x/Me 的時代,則是用 AX=7305H/INT 21H 來讀寫 FAT32 的磁區。其輸入參數為 AX 必定是 7305H,CX 必定是 0FFFFH。DL 表示欲讀寫的磁碟機,1 表示 A: 磁碟機,2 表示 B: 磁碟機,3 表示 C: 磁碟機……,此處和 INT 25H/INT 26H 不同。DS:BX 也是指向一塊和磁區編號、磁區數、傳送位址有關的記憶體空間,和上述 INT 25H/INT 26H 相同。SI 是用來表示讀或寫的旗標,其意義如下:

 位 元  描       述
 00     0 表示讀取,1 表示寫入
 12-01  保留,應為 0
 14-13  讀取時應為 00
        寫入時 00 表示未知的資料
               01 表示寫入資料為 FAT
               10 表示寫入資料為目錄資料
               11 表示寫入資料為檔案資料
 15     保留,應為 0

INT 10H/AH=2 與 INT 10H/AH=9 BIOS 中斷服務程式

本程式用到幾個 INT 10H 的中斷服務程式,此服務程式是 BIOS 所提供的,它處理和螢幕有關的服務,詳細情形見附錄七,此處說明 AH=2、AH=6、AH=9 的用法,參考下表:

目的 AH 輸入輸出
將游標定位於螢幕上 AH=2 BH=顯示頁
DH=列 (Y座標)
DL=行 (X座標)
使一個視窗(某一個指定的矩形範圍)內的文字向上捲動 AH=6 AL=捲動列數(AL=0時表示整個視窗被清除)
BH=捲動後下面填入空白的顏色
CX=該視窗左上角位置
  CH=列 (Y座標),CL=行 (X座標)
DX=該視窗右下角位置
  DH=列 (Y座標),DL=行 (X座標)
在現在游標的位置顯示 ASCII 字元 AH=9 AL=ASCII 字元
BH=顯示頁
CX=欲顯示字元數
BL=字元屬性

上表中的字元屬性,指的是顏色,如果是在單色螢幕是指有沒有底線、閃爍的意思。但是現在已沒有人還在用單色螢幕,所以小木偶僅介紹顏色。顏色分為前景與背景,在 DOS 文字模式底下,只可以顯示十六種顏色,所以用 4 個位元就足夠了。前景是指字的顏色,是用較低的四個位元表示;背景是指『紙』的顏色就是字以外的顏色,以較高的四個位元表示。所以如果字元屬性為 40H,參考下表得知此字元是紅底黑字。

顏色 數值  顏色 數值  顏色 數值  顏色 數值
黑色0   紅色4   深灰色8   淡紅色C
藍色1   紫色5   淡藍色9   淡紫色D
綠色2   棕色6   淡綠色A   黃色E
青色3   灰色7   淡青色B   白色F

在 DOS 文字模式底下,有編號 0 到 3 總共四個顯示頁,一般我們用編號 0 的顯示頁即可。


磁片地圖原始程式

底下是磁片地圖的原始程式,小木偶將它命名為 DISKMAP.ASM:

sector_bytes    equ     512     ;01 每磁區所含位元組數
start_y         equ     1       ;02 由螢幕第一列開始印

packet  struc                   ;04 絕對磁區讀取控制區塊結構
sector_no       dd      ?       ;05 磁區編號
number_sector   dw      ?       ;06 讀取磁區數
tran_offset     dw      ?       ;07 讀取後存放處之偏移位址
tran_segment    dw      ?       ;08 讀取後存放處之區段位址
packet  ends
;***************************************
data    segment                                 ;11 資料區段
driver          db      0                       ;12 A: 磁碟機
fat     packet  <1,9,offset fat_data,seg data>  ;13 讀取 FAT 之控制區塊
        org     20h
fat_data        db      9*sector_bytes dup (0)  ;15 FAT 存放處
mes1    db      ':BOOT sector                       :FAT sector$'
mes2    db      ':ROOT directory                    :bad sector$'
mes3    db      ':uesd sector                       :un-used sector$'
data    ends
;***************************************
code    segment
        assume  cs:code,ds:data
;---------------------------------------
main    proc    far
start:  push    ds
        sub     ax,ax
        push    ax
        mov     ax,data
        mov     ds,ax

        mov     ax,600h         ;31 清除螢幕
        mov     bh,0fh
        sub     cx,cx
        mov     dx,184fh
        int     10h

        mov     al,driver       ;37 讀取FAT
        mov     bx,offset fat
        mov     cx,0ffffh
        int     25h
        popf
        jnc     draw
exit:   ret

draw:   sub     bx,bx           ;45 將由標定位於第一列第零行
        mov     ah,2
        mov     dh,start_y
        mov     dl,bl
        int     10h
        mov     ax,942h         ;50 於此處印出一個藍底紅字的 B
        mov     bx,01ch
        mov     cx,1
        int     10h

        inc     dx              ;55 將由標定位於第一列第一行
        mov     ah,2
        sub     bx,bx
        int     10h
        mov     ax,946h         ;59 於此處印出九個白底紅字的 F
        mov     bx,0fch
        mov     cx,9
        int     10h

        mov     ah,2            ;64 將由標定位於第一列第十行
        mov     bh,0
        mov     dl,0ah
        mov     dh,start_y
        int     10h
        mov     ax,952h         ;69 於此處印出九個深紅底白字的 R
        mov     bx,4fh
        mov     cx,7
        int     10h

        call    draw_fat        ;74 印出磁區分布圖

        mov     ah,2
        mov     bh,0
        mov     dh,19+start_y
        mov     dl,bh
        int     10h
        mov     ax,942h         ;81 於此處印出一個藍底紅字的 B
        mov     bx,01ch         ;82 表示啟動磁區
        mov     cx,1
        int     10h
        mov     ah,2
        mov     bh,0
        mov     dh,19+start_y
        mov     dl,1
        int     10h
        mov     ah,9
        mov     dx,offset mes1
        int     21h
        mov     ah,2            ;93 於此處印出一個白底紅字的 F
        mov     bh,0            ;94 表示 FAT 磁區
        mov     dh,19+start_y
        mov     dl,23h
        int     10h
        mov     ax,946h
        mov     bx,0fch
        mov     cx,1
        int     10h
        
        mov     ah,2
        mov     bh,0
        mov     dh,20+start_y
        mov     dl,bh
        int     10h
        mov     ax,952h         ;108 於此處印出一個深紅底白字的 R
        mov     bx,04fh         ;109 表示根目錄磁區
        mov     cx,1
        int     10h
        mov     ah,2
        mov     bh,0
        mov     dh,20+start_y
        mov     dl,1
        int     10h
        mov     ah,9
        mov     dx,offset mes2
        int     21h
        mov     ah,2            ;120 於此處印出一個紅底黑字的 B
        mov     bh,0            ;121 表示損壞的磁區
        mov     dh,20+start_y
        mov     dl,23h
        int     10h
        mov     ax,942h
        mov     bx,0c0h
        mov     cx,1
        int     10h
        
        mov     ah,2
        mov     bh,0
        mov     dh,21+start_y
        mov     dl,bh
        int     10h
        mov     ax,941h         ;135 於此處印出一個綠底黃字的 A
        mov     bx,02eh         ;136 表示已使用的磁區
        mov     cx,1
        int     10h
        mov     ah,2
        mov     bh,0
        mov     dh,21+start_y
        mov     dl,1
        int     10h
        mov     ah,9
        mov     dx,offset mes3
        int     21h
        mov     ah,2            ;147 於此處印出一個綠底黑字的 .
        mov     bh,0            ;148 表示未使用磁區
        mov     dh,21+start_y
        mov     dl,23h
        int     10h
        mov     ax,92eh
        mov     bx,020h
        mov     cx,1
        int     10h
        
        mov     ah,2
        mov     bh,0
        mov     dh,21+start_y
        mov     dl,0
        int     10h
        ret
main    endp
;---------------------------------------
;將編號為偶數的邏輯磁區印在螢幕上適當位置(x,y),
;適當位置僅和邏輯磁區編號(AX)有關,把 AX 除以 160
;所得的商存於 AL,餘數存於 AH,則 (x,y)=( AH/2, AL+start_y )
;輸入:AX:邏輯磁區編號
;      DX:該邏輯磁區編號所對應之FAT值
;輸出:若 DX=0FF7H,印出紅底黑字的『B』
;      若 DX=0000H,印出藍底灰字的『.』
;      DX非以上兩者,印出綠底黃字的『A』
print_even_sector       proc    near
        push    dx
        mov     bl,160          ;175 依上八行數學式求出 (x,y)
        div     bl
        mov     dl,ah
        mov     dh,al
        shr     dl,1
        add     dh,start_y
        sub     bx,bx
        mov     ah,2
        int     10h             ;183 把由標定位於適當位置
        pop     dx              ;184 由 DX 值印出 B、A 或 .
        mov     ax,941h
        mov     bx,02eh
        mov     cx,1
        cmp     dx,0ff7h
        jne     psm0
        mov     al,'B'
        mov     bx,0c0h
        jmp     short psm1
psm0:   cmp     dx,0
        jne     short psm1
        mov     al,'.'
        mov     bx,020h
psm1:   int     10h
        ret
print_even_sector       endp
;---------------------------------------
;以 FAT 資料印出整張磁片的檔案分布圖案
;輸入:DS:SI:FAT 存放處
;      AX:邏輯磁區編號
draw_fat        proc    near
        mov     si,offset fat_data+4
        mov     ax,34           ;206 資料由第 34 個磁區開始
df0:    push    ax              ;207 SI=第幾個 FAT 位元組
        mov     dx,[si]         ;208 由 SI 取得 FAT 內容
        mov     cl,4            ;209 並存於 DX
        shr     dx,cl
        call    print_even_sector
        pop     ax
        add     si,3
        inc     ax
        inc     ax
        cmp     ax,2880
        jnz     df0
        ret
draw_fat        endp
;---------------------------------------
code    ends
;***************************************
stack   segment stack
        db      20h dup ('STACK   ')
stack   ends
;***************************************
        end     start

如果您用 MASM 5.0 來組譯這個程式,照正常方法組譯完全不用修改就可以了,如下:

H:\HomePage\SOURCE>..\masm50\masm diskmap; [Enter]
Microsoft (R) Macro Assembler Version 5.00
Copyright (C) Microsoft Corp 1981-1985, 1987.  All rights reserved.


  51170 + 349374 Bytes symbol space free

      0 Warning Errors
      0 Severe  Errors

H:\HomePage\SOURCE>..\masm50\link diskmap; [Enter]

Microsoft (R) Personal Computer Linker  Version 2.40
Copyright (C) Microsoft Corp 1983, 1984, 1985.  All rights reserved.

但如果您用 MASM 6.xx 組譯的話,請您用相容於 5.0 版的方式組譯,就是要加上『/Zm』參數:

H:\HomePage\SOURCE>path h:\homepage\masm611d;%path% [Enter]

H:\HomePage\SOURCE>ml /Zm diskmap.asm [Enter]
Microsoft (R) Macro Assembler Version 6.11d
Copyright (C) Microsoft Corp 1981-1995.  All rights reserved.

 Assembling: diskmap.asm

Microsoft (R) Segmented Executable Linker  Version 5.60.339 Dec  5 1994
Copyright (C) Microsoft Corp 1984-1993.  All rights reserved.

LINK : warning L4017: /r : unrecognized option name; option ignored
Object Modules [.obj]: /r diskmap.obj
Run File [diskmap.exe]: "diskmap.exe"
List File [nul.map]: NUL
Libraries [.lib]:
Definitions File [nul.def]:

得到 DISKMAP.EXE 就可以試試看執行它了,在執行這個程式之前,要先把三又二分之一英吋的軟碟片放在 A: 磁碟機,下圖是小木偶的一張軟碟片使用情形:

小木偶的一張軟碟片使用情形

由上圖可以看出,這張磁片中間有一大段是沒使用的空間,後面又有一塊是已經使用的空間,前面還有已損壞的磁區。


說明

這個程式有很大一部份是處理在螢幕上的某一個位置印出帶有顏色的文字來,這些參考上面的中斷呼叫程式應該並不難瞭解。所以小木偶不詳細介紹這些過程,僅就觀念上及結構體介紹。

STRUC/ENDS ( STRUCT/ENDS,結構體 )

STRUC 是一個假指令,它使組譯器保留一塊記憶體空間給一些資料使用。MASM 組譯器將它包裝成一個新的資料型態,這種結構體可以看成是一塊連續的記憶體空間,這個空間內的記憶體可以是各種已經定義好的資料型態,如 DB、DW 等等。在某些時候,結構體特別好用,例如處理 CX=0FFFFH 的絕對磁區的讀取就是一個很好的例子。當我們用此中斷程式,DS:BX 是指向一塊十個位元組的記憶體空間,這個記憶體空間代表真正的輸入參數,因此小木偶就把這十個位元組包裝成一個結構體,至於如何定義結構體,請參考程式第 4 到第 9 行,

packet  struc                   ;04 絕對磁區讀取控制區塊結構
sector_no       dd      ?       ;05 磁區編號
number_sector   dw      ?       ;06 讀取磁區數
tran_offset     dw      ?       ;07 讀取後存放處之偏移位址
tran_segment    dw      ?       ;08 讀取後存放處之區段位址
packet  ends

packet 的名稱可以任意取,後面接著 struc 這個保留字,表示這裡有一個結構體定義開始了,而幾行之後必定有一個 ends,這個 ends 您應該猜得出來就是結構體結束,雖然和區段結束相同,但是 MASM 會自動配合,不用使我們擔心。所以結構體的形式是:

名稱    struc
欄位1   d*      ?
欄位2   d*      ?
………………………
名稱    ends

在 struc 和 ends 這兩行之間的資料型態定義可以視程式設計師需要調整,當程式設計師像上面定義好結構體後,MASM 會記住有一個新的資料型態產生 (以這個程式為例,packet 就是新的資料型態),但是這時候 MASM 並不會分出一塊記憶體給它使用,換句話說這時僅僅是『宣告』新的資料型態而已。到程式第 13 行,MASM 才劃分出一塊記憶體給 fat 使用,而 fat 的資料型態就是上述的 packet,請參考下一行,當我們定義結構體的數值時需用一對『<』、『>』括起來:

fat     packet  <1,9,offset fat_data,seg data>

fat 變數事實上包含十個位元組,前四個位元組表示磁區編號,稱為 sector_no;接下來的兩個位元組表示取磁區數,稱為 sector_number……。像程式第 13 行的敘述就會產生

        dd      1
        dw      9
        dw      offset fat_data
        dw      seg data

的資料。當我們要在程式中改變其數值時,可以用

        mov     cx,fat.number_sector
        mov     fat.tran_segment,dx

的方式改變,上面就是把 fat 結構體內的磁區數存入 CX 暫存器,跟把 DX 暫存器的數值存入 fat 結構體內的緩衝區區段位址。

值得一提的還有在 MASM 6.0 版以後也可以用 STRUCT 來代替 STRUC,除此之外,在 MASM 6.0 版以後,STRUC 之後可以接上一個可以省略的 alignment 選項與 NONUNIQUE 選項,如下面:。

名稱    STRUC     [alignment][,NONUNIQUE]
欄位1   d*      ?
欄位2   d*      ?
………………………
名稱    ENDS

alignment 可以是 1、2 或 4,這些值是表示組譯器為 STRUC 內的各欄位在記憶體安排的位址。在 32 位元的 CPU 存取記憶體資料時,如果記憶體位址能為四整除時,存取速度較快;而 16 位元的 CPU 則是以所讀取的記憶體位址能為 2 整除較快。因此有時為了速率,可以強迫每個欄位的位址由偏移位址 2、4 的倍數開始,此處的偏移位址是相對於結構體起始,並非記憶體起始處。例如 alignment 為 2 時,表示每個欄位以偶數位址開始,若不是偶數時,則組譯器會自動填上一個空格。若 alignment 為 4 時,則每個欄位由四的倍數開始,但是如果有兩個欄位加起來的長度小於四,則可能會把這些欄位以偶數開始,而合起來成為四的倍數,亦即使下一欄位由四的倍數開始。例如有一結構體,student,宣告為:

student STRUCT  4
lname   DW      ?
fname   DD      ?
year    DW      ?
id      DB      ?
student ENDS

lname 佔去 2 個位元組,因此組譯器會補上 2 個位元組的空位。再接下來的 fname 佔去 4 個位元組,不補空位。再接下去的 year 和後面的 id 共佔 3 個位元組,因此補 1 個位元組的空位。如下圖分布:

說明 alignment

有時我們會需要取得結構體某個欄位的位址,以下面的程式片段為例。首先我們先定義一個名為 packet 的結構體,然後在資料段中定義 fat 變數,此變數的資料形態即為 packet 結構體。

packet          struc   ;絕對磁區讀取控制區塊結構
sector_no       dd      ?
number_sector   dw      ?
tran_offset     dw      ?
tran_segment    dw      ?
packet          ends
;***************************************
data    segment
driver          db      0
fat     packet  <1,9,offset fat_data,seg data>
;***************************************
…………………………
        mov     si,offset fat.sector_no
        mov     dx,packet.sector_no

最後兩行程式中有關位址的意義並不相同,第一行會被組譯器組譯成取得 fat 變數中的 sector_no 欄位位址,這個位址是從資料段開始算起,因此執行完第一行後,SI 為 1;如果採用最後一行的方式,則組譯器會組譯成取得 packet 結構體的 sector_no 位址,而此位址是由結構體的起始位址算起,因此 DX 為零。在 Win32 環境下,使用相當多的結構體,若您想繼續進階 Win32 組合語言程式設計,就得瞭解結構體。

程式說明

程式第 31 行到第 35 行是清除螢幕,程式第 37 到第 41 行是讀取軟碟片 FAT 的資料,要讀取的磁區編號、讀取的磁區數、存放 FAT 資料位址均在 fat 結構體內定義,分別在程式的第 4 行到第 9行,及第 13 行。第 45 行到第 72 行是把啟動磁區、FAT 磁區、根目錄磁區畫在螢幕上,因為這些都是固定的,所以小木偶直接畫上去,並沒有做檢查。

程式第 74 行呼叫 draw_fat 副程式,這個副程式是根據剛才由軟碟上讀入的 FAT 資料,把磁區畫在螢幕上。螢幕上總共可以顯示 80*25 個字 ( 即2000個字 ),而軟碟片上有 2880 個磁區,無法全部顯示在螢幕上,所以小木偶只好把編號是偶數的磁區顯示在螢幕上,這樣就只要顯示 1440 個磁區就可以了,如果每行顯示 80 個字,那只需要 18 行就可以顯示完畢,其餘剩下的空間可以用來顯示像『使用的磁區如何表示』、『損壞的磁區如何表示』等資訊。

至於如何安排標號偶數的磁區在螢幕上的位置呢?小木偶用一個名為 print_even_sector 的副程式來處理。請先參考下圖:

安排磁區顯示於螢幕上

圖中每一個粉紅色小長方形框框代表在螢幕上的一個字,照剛才所說,應該顯示 18 列才對,但是為節省空間,所以小木偶僅畫兩列代表。每一列都有 80 個字,每一個字表示兩個磁區,這兩個磁區的編號小木偶用紅色字寫在上方,下方的黃色數字來表示它在螢幕上的 X 座標 (由 0 到 79 共 80 個字,Y 座標並沒有在圖上表示出來)。

因為每個字表示兩個磁區,所以小木偶想,就看看偶數編號的磁區即可,您應該可以發現,每一列的第零個磁區分別是 0、160、320……,雖然 320 極其以後並沒有顯示在圖上,但是小木偶想,您應該可以由『每列 80 字,每字兩磁區』推算出來,所以字的 Y 座標可以推算的出來,應該等於磁區編號除以 160 所得的商,但是如果不是由螢幕第一列開始顯示,應該再加上一個起始顯示的列數,這也就是 Y=AL+start_y 的原因。

再觀察第零列的每一個字,小木偶想您也應該可以看到,X 座標恰好是磁區編號的一半,而第一列的 X 座標則是磁區編號減去 160 後的一半,所以 X 座標就是磁區編號除以 160 後的餘數之半,這就是 X=AH/2 的原因了。所以程式第 175 行到第 183 行就可依上述規則定出游標位置。

程式 189 行到第 198 行是根據 FAT 內的數值印出字元,如果是『0FF7H』則表示壞磁區印出『B』;如果是『000H』,表示未使用,印出『.』;如果是其他數則表示是被使用的磁區,印出『A』。所以 print_even_sector 副程式有兩個重要的參數,分別是磁區編號及該磁區編號的 FAT 數值,磁區編號用來計算字元位置,FAT 內的數值用來顯示該以什麼樣的字元顯示。這兩個參數分別用 AX,DX 表示,並且由 print_even_sector 的『父』程式,draw_fat,傳給它。

draw_fat 副程式

這個副程式是畫出整個磁片使用分布,小木偶用 SI 來表示讀到第幾個 FAT 位元組,用 AX 表示磁區編號,事實上這兩者是相關的,可以用 SI 來求出 AX,但小木偶為簡化程式,所以採用分別計算。

由於三又二分之一英吋的軟碟片是由第 33 個磁區開始存放資料,這不是小木偶要顯示的磁區,小木偶從第 34 個磁區開始顯示 (參考第十八章),第 34 個磁區是 FAT 的第 3 個欄位,也就是第 4 個位元組開始,所以程式第 205 行會加上 4 的原因在此,至於由 SI 取得 FAT 位元組後,再得到 FAT 數值和 FAT 的編排方式有關,請參考第十八章,經過運算 (程式第 208 到第 210 行) 才能得到 FAT 數值,DX。而磁區編號就由 34 開始,這應該毫無疑問。

每次顯示完一個字,磁區編號應增加 2,這是因為每個字表示兩個磁區,而 SI 應該增加 3,直到磁區編號到最後面 2880 為止。


回到首頁到第十八章到第二十章