Ch 15 檔案處理(1):代碼式的檔案處理簡介

在接下來的幾章裡,小木偶想跟各位談談如何用組合語言處理檔案,所謂檔案處理的意思是指建立檔案、修改檔案等等的工作。

觀念

檔案代碼

DOS 檔案處理的方法有兩種,一是採用 FCB 式(file control block,檔案控制區),另一種是 handle 式(file handle,檔案代碼)。FCB 式的檔案處理方式已經落伍了,到第十七章再向各位報告,因此現在讓我集中精力,向各位說明 handle 式的檔案處理方式。

所謂 handle 式的檔案管理有點類似去醫院堿搵f。當我們去醫院看病時首先到掛號處領取號碼牌,此後醫生就依據號碼牌叫病人醫病。Handle 式的檔案處理也是類似,當我們開啟檔案或建立檔案時,DOS 就給予此檔案一個號碼,這個號碼代表此檔案,當程式想處理檔案時,就以這代碼來表示這個檔案,而不再直接用檔名了。上述所提到的號碼就稱為 file handle,或叫檔案代碼。

再說得更詳細一點,當我們開啟檔案或建立檔案時就可以取得一個檔案代碼,做法是要先把檔案名稱 ( 也可包含路徑 ) 放在記憶體某處,並在檔名最後加上 00H 即可,然後交給指定的 DOS 服務程式去執行就可以了。像這種 ASCII 字串再加上 00H 的字串稱為 ASCIIZ 字串。一切就是這麼簡單。

當我們呼叫關閉檔案的 DOS 服務程式時,DOS 就收回這個檔案代碼,讓其他新的檔案使用。DOS 規定最大開啟檔案的數目是放在 CONFIG.SYS 中,用

files=xx

來指定,此處的 xx 就是 DOS 所能開啟最大檔案數目,當然 xx 也可以修改成我們所需要的檔案數目。值得注意的是,DOS 在一開機時,就已經指定 5 個檔案代碼給系統使用了,所以如果你在 CONFIG.SYS 內指定 files=20,事實上你只能開啟 15 個檔案,因此如果要使用同使開啟多檔案的程式,例如 Windows 3.1,最好能在 CONFIG.SYS 指定大一點的開檔數目。這 5 個檔案是:

0000H  標準輸入設備 ( stdin )
0001H  標準輸出設備 ( stdout )
0002H  標準錯誤設備 ( stderr )
0003H  標準輔助設備 ( stdaux )
0004H  標準列印設備 ( stdprn )

這五個『檔案代碼』看起來似乎不是檔案,而是週邊設備。譬如,標準輸入設備、標準輸出設備指的是鍵盤與螢幕,標準列印設備指的是印表機,怎麼會這樣呢?原來 DOS 把這些設備視為檔案,對它們輸出資料時,就像是對『檔案』寫入。這也就是我們可以用『>』、『<』來表輸出到螢幕上的文字變成檔案的緣故。

檔案指位器

開啟或建立檔案之後,取得了檔案代碼,如果要讀取某些檔案內容或者寫入某些內容到檔案中,又該如何做呢?

考慮全部的讀取或寫入的時機,並不是所有情況下都必須從檔案最前端讀取或寫入,例如在一個全校數千筆的學生資料堙A只要讀取某一班資料時,如果每次都得從檔案頭開始讀取,然後再尋找到該筆資料,這樣就非常的耗時耗力。

為了避免這個問題,DOS 內部會為每個開啟的檔案建立一個 32 位元長的檔案指位器,每當剛開啟檔案時,檔案指位器便指向檔案最前面,每讀取或寫入一次時,檔案指位器就自動地指向讀取或寫入的尾端。假如程式不是依順序讀取讀取或寫入時,DOS 服務程式也有一個功能,可以僅移動檔案指位器,而不讀取或寫入。這樣程式只需計算要使檔案指位器移到要讀取或寫入的地方,然後再加以處理即可。

有關檔案處理常用的 DOS 服務中斷

好吧!先看看下表中常用的檔案代碼 DOS 服務程式。下表分兩部份,上半部是在 DOS 中的服務程式,下半部是 Windows 9x/Me 中 DOS 模式中的服務程式,兩者的差別在 Windows 9x/Me 可使用長檔名,而 DOS 中的服務程式無法使用長檔名。

在 DOS 系統中有關檔案代碼的服務程式
用途 呼叫 輸入 輸出
建立新檔案AH=3CH DS:DX=新檔名之ASCIIZ字串位址
CX=檔案屬性
  • 0 一般檔案
  • 1 唯讀檔案
  • 2 隱藏檔案
  • 4 系統檔案
  • AX=檔案代碼
    詳情參考錯誤碼
    開啟檔案 AH=3DH DS:DX=欲開啟檔名之ASCIIZ字串位址
    AL=設定開啟的檔案屬性
  • 0表示唯讀
  • 1表示唯寫
  • 2表示可讀可寫
  • AX=檔案代碼
    詳情參考錯誤碼
    關閉檔案 AH=3EH BX=欲關閉之檔案代碼 詳情參考錯誤碼
    讀取檔案內容 AH=3FH BX=欲讀取之檔案代碼
    CX=欲讀出之長度,以位元組為單位
    DS:DX=讀出檔案內容之存放位址
    AX=實際讀出之位元組數
    詳情參考錯誤碼
    將記憶體內容寫入檔案 AH=40H BX=欲寫入之檔案代碼
    CX=欲寫入之位元組數
    DS:DX=欲寫入之記憶體位址
    AX=實際寫入之位元組數
    詳情參考錯誤碼
    刪除檔案 AH=41H DS:DX=欲刪除路徑檔名之ASCIIZ字串位址 詳情參考錯誤碼
    移動檔案指位器 AH=42H BX=檔案代碼
    CX:DX=移動位元組數
    AL=由何處開始移動(移動方式)(註二)
  • 0 由檔案起始處開始
  • 1 目前指標開始
  • 2 由檔案末端開始
  • DX:AX=移動後距檔案起始處多遠
    建立或開啟檔案
    此功能是3CH與3DH的總和
    AX=6CHDS:SI=欲開啟或建立之ASCIIZ字串
    BX=存取模式
    CX=屬性
    DX=動作 ( 詳細情形請參考註三 )
     
    在 Windows 9x/Me 有關長檔名的檔案代碼服務程式 INT 21H/AH=71H
    用途呼叫 輸入輸出
    建立或開啟檔案 AX=716CH DS:SI=欲開啟或建立之ASCIIZ字串
    BX=存取模式
    CX=屬性
    DX=動作 ( 詳細情形請參考註三 )
    NC 成功,AX=檔案代碼
         CX= 1表示被開啟
  • 2 表示被建立
  • 3 表示被取代
  • CY 失敗,AX=錯誤碼
  • 刪除檔案 AX=7141H DS:DX=欲刪除檔名之 ASCIIZ 字串位址
    CH=屬性
    SI=0000 不允許使用萬用字元刪除檔案
  • 0001 允許使用萬用字元刪除檔案且只刪除符合指定屬性的檔案
  • NC 成功
    CY 失敗,AX=錯誤碼
    建立子目錄 AX=7139H DS:DX=指向欲建立子目錄名之 ASCIIZ 位址 NC 成功
    CY 失敗,AX=錯誤碼
    尋找第一個符合的檔案 AX=714EH DS:DX=指向欲尋找的檔名 ASCIIZ 字串之位址,可包含萬用字元
    CL=允許的屬性
    CH=必需的屬性
    SI=FindData 的日期時間格式,0 表示 Win 9x 格式;1 表示 DOS 格式
    ES:DI=指向 FindData 結構體
    NC-成功
       AX=檔案尋找代碼
       CX=Unicode 轉換旗標
         位元 0 為 1,表示返回的長檔名含無法轉換
               的萬國碼字元
         位元 1 為 1,表示返回的短檔名含無法轉換
               的萬國碼字元
    CY-失敗
    尋找下一個符合的檔案 AX=714FH BX=檔案尋找代碼,亦即呼叫 AX=714EH 之返回值
    SI=FindData 的日期時間格式,0 表示 Win 9x 格式;1 表示 DOS 格式
    ES:DI=指向 FindData 結構體

    一般而言,尋找檔案會先以 714EH 找到第一個檔案及檔案尋找代碼,此時第一個檔案的資料已在 FindData 堣F,再以 714FH 尋找第二、第三……個符合的檔案,如果不須再尋找的話,以 71A1H 關閉檔案尋找代碼,並將資源還給系統。
    NC-成功
       AH=4FH
       CX=Unicode 轉換旗標
         位元 0 為 1,表示返回的長檔名含無法轉換
               的萬國碼字元
         位元 1 為 1,表示返回的短檔名含無法轉換
               的萬國碼字元
    CY-失敗

    一般而言,代碼式的檔案處理方式,執行完後如果進位旗標被設定(在 DEBUG 顯示 CY),表示錯誤產生,此時 AX 暫存器的內容表示錯誤碼。如果進位旗標被清除(NC),則表示沒有錯誤發生。因此我們應該在執行完中斷服務程式後,檢查是否有錯誤。


    一個範例:CRTTCT.ASM

    原始碼

    底下我來示範一個程式,CRTTCT.COM,這個程式會在 C:\MYDOCU~1 子目錄內建立一個名為 TEST.TXT 的檔案,然後 DOS 等待我們輸入一些字,如果按下 Esc 鍵就會將我們輸入的字存入 TEST.TXT 檔並跳回 DOS。這個程式並沒有編輯功能,因此如果輸入錯誤,是沒法到回去修改的。程式如下:

    ;***************************************
    code    segment
            assume  cs:code,ds:code
            org     100h
    ;---------------------------------------
    start:  jmp     begin
    file_name       db      'C:\MYDOCU~1\TEXT.TXT',0
    file_handle     dw      ?
    message_1       db      '無法建立 TEXT.TXT 檔。$'
    message_2       db      '無法將資料存入 TEXT.TXT 檔。$'
    message_3       db      '寫入成功!$'
    buffer          db      200 dup (?)  ;12 輸入緩衝區
    begin:  mov     ah,3ch
            mov     cx,0
            mov     dx,offset file_name  ;15 取得ASCIIZ字串位址
            int     21h
            mov     dx,offset message_1
            jc      error
            mov     file_handle,ax       ;19 將handle存入變數
            mov     si,offset buffer     ;20 將SI指向輸入緩衝區
    next:   mov     ah,0                 ;21 字元輸入
            int     16h
            cmp     al,1bh               ;23 檢查是否按下Esc鍵
            je      exit
            mov     [si],al              ;25 若不是,則將該字元存於緩衝區
            mov     ah,2                 ;26 及印在螢幕上
            mov     dl,al
            int     21h
            cmp     al,0dh               ;29 若使用者按下Enter鍵
            jne     not_cr
            mov     dl,0ah               ;31 則還要再存入換行字元
            inc     si
            mov     ah,2
            int     21h
            mov     [si],dl
    not_cr: inc     si
            cmp     si,offset begin      ;37 檢查緩衝區是否填滿了
            jne     next
    exit:   mov     cx,si
            mov     dx,offset buffer
            mov     ah,40h
            sub     cx,dx
            jz      close
            mov     bx,file_handle
            int     21h                  ;45 存入檔案
            mov     dx,offset message_2
            jc      error
    close:  mov     ah,3eh
            mov     bx,file_handle
            int     21h                  ;50 關閉檔案
            mov     dx,offset message_3
    error:  mov     ah,9
            int     21h
            mov     ax,4c00h
            int     21h
    ;---------------------------------------
    code    ends
    ;***************************************
            end     start

    將這個原始檔編譯、連結並轉換成 CRTTCT.COM 檔,執行看看

    小木偶先執行 CRTTXT.COM 程式,然後輸入

    I learn assembly language hard,
    beacuse it is very fun.

    這一段文字(綠色圈起來的地方),在逗點後按下 Enter 鍵跳到下一行在繼續輸入直到句點,輸入完後再按下 Enter 鍵,再按下 Esc 鍵,程式立即把我所輸入的文字存入 C:\MYDOCU~1\TEXT.TXT 檔,並返回 DOS。

    然後我在 DOS 命令提示下,檢查 TEXT.TXT 檔(紅色圈起來的地方),你也可以筆記本開啟他,甚至繼續編輯。

    說明

    底下仔細說明這個程式。

    第十三行到第十五行是設定建立檔案所需要的暫存器值,參考上表,必須設定AH、DS、DX、CX等暫存器。因為這個程式是 COM 檔,其所有區段都相同,故 DS 暫存器不必設定就是正確值,其他三個暫存器都必須在呼叫中斷前設好。

    若無錯誤,第十九行將 DOS 傳回來的檔案代碼存入一個變數中,以備所需。第二十行設定使用者輸入的資料存放處,存於 buffer 這個位址所指的地方開始。SI 指向這個位址,以後每當使用者輸入一個字元,就會存於 SI 所指位址處,然後再使 SI 增加一,使下一個輸入的字能存於正確位址,不會覆蓋前面的資料。而 buffer 這個位址在第十二行已經先定義了,

    buffer          db      200 dup (?)

    這個定義出現了 dup 指令,它的用法是:

    變數名          db      n dup (value)

    表示在『變數名』所指的位址設定一個長 n 個位元組的區域,在這個區域內的每個位元組的值都是 value。假如不想設定這塊區域的起始值,可以用『?』代替 value。因為程式設計師不曉得使用者會輸入多少個字,故先假設一個長200 個位元組的區域給使用者輸入,因此這時 dup 恰好符合所需。

    接下來是鍵盤輸入字元,及檢查使用者是否按下 Esc 鍵,如果不是按下 Esc 鍵,則將該 ASCII 字元存於 buffer 所指的那塊區域(第25行),並印出來(第26到28行)。

    如果使用者按下 Enter 鍵時,這時應該將游標移到螢幕下一行的最左邊,但是不幸的是,在 IBM PC 裡按下 Enter 鍵只能產生 ASCII 碼的 0dh,它只能使游標移到最左邊,不能移到下一行,故程式得自行產生『換行』效果。移到下一行(換行)的 ASCII 碼控制符號是 0ah (參考附錄四),所以在第31行加入一個換行符號,並存於 buffer 所指的那一塊區域,同時使 SI 指向下一個位址,以便存入下一個輸入的字元。

    第37行則是檢查 buffer 所指的位址是否已經填滿了,若填滿或是使用者提前按下 Esc 鍵則存檔(39到45行)、關閉檔案(48到50行)並返回 DOS (54到55行)。其中還參雜著檢查是否有錯誤的檢查程式片段及印出訊息的程式片段。


    註一:

    請參考附錄五

    註二:

    AH=42H/INT 21H移動檔案指標這個服務常式只能由參考點向後移,不能向前移,所能使用的參考點有三:檔案開始處、檔案末端及被移動後所指的檔案指標處。為了方便,寫入或讀取檔案時,也會向後更動檔案指標。當檔案被開啟或建立時,檔案指標為零,也就是在檔案起始位置。如果讀取或寫入或移動檔案指標 1000 個位元組,檔案指標便移到離檔案開始處 1000 個位元組的地方,如果再讀取或寫入或移動檔案指標 500 個位元組,那檔案指標就會在往後移 500 個位元組,也就是檔案開始處後 1500 個位元組的地方,這時以第 1000 個位元組為參考點向後移 500 個位元組。

    好,現在第一個問題來了,假如現在檔案指標已經在檔案開始後 1500 個位元組的地方了,但我想讀取檔案開始的第 1024 位元組以後的 100 個位元組,該怎麼辦呢?這時就得用 AH=42H/INT 21H 這個服務程式了,首先得移動檔案指標,由檔案開始處向後移 1024 個位元組,再讀取 100 個位元組。

    底下的程式片段,就是先將檔案指標移到從檔案開始處算(AL=0)的第 1024 位元組(CX:DX=0000:0400H),再讀取。

    mov     ah,42h
    mov     al,0        ;把參考點設為檔案開始處
    mov     bx,handle
    mov     dx,1024     ;向後移 1024 個位元組
    sub     cx,cx
    int     21h
    
    mov     ah,3fh      ;再讀取 100 個位元組
    mov     bx,handle
    mov     cx,100
    mov     dx,buffer
    int     21h

    第二個問題,如果檔案指標已經到檔案末端,再移動檔案指標會發生什麼事呢?根據小木偶在 MS-DOS 6.20 版上實驗結果,這時檔案指標還是會繼續向後移而超過檔案長度,且不會報錯,不過,此時如果再讀取,只能讀到 0 位元組。

    最後,不論移動方式為何,傳回值 DX:AX 所代表之檔案指標都是由檔案起始處算起。

    此外這個服務程式也可以用來取得檔案長度,方法就是當檔案指標在起始處時,輸入 AL=0、CX:DX=0,再呼叫此服務程式,如此傳回的 DX:AX 就是檔案長度。


    註三:

    在 Win 9X/Me 堛 DOS 模式中,可以用長檔名開啟檔案,首先小木偶簡單介紹檔名。在 Win 9X/Me 作業系統下,假如檔名符合 DOS 系統中的規定,也就是主檔名 8 個英文字,副檔名 3 個英文字這種規則的檔名,Win 9X/Me 僅用一個 FDB ( 請參考第 18 章 ) 記錄。假如檔名超過上述規則,Win 9X/Me 用兩個或兩個以上的 FDB 記錄檔名,其中一個是 Win 9X/Me 把原來較長的檔名濃縮後的結果,緊接著的數個 FDB 用來存放真正的長檔名。所以您在 Win 9X/Me 的 DOS 模式下看見的檔名像下圖:

    說明長檔名與濃縮檔名
    例如上圖中『PROGRA~1』是系統處理濃縮後的檔名,真正的檔名是後面的『Program Files』。在 Win 9X/Me 系統堛 INT 21H/AH=71H 是專門用來處理長檔名的,其子功能用 AL 表示。

    以開啟檔案為例,在 Win 9X/Me 堨 AX=716CH/INT 21H 服務常式提供程式設計師使用。要開啟的檔案名稱由 DS:SI 所指位址的 ASCIIZ 字串表示,即此字串必須是檔名後再加上一個 0。BX 決定開啟後的存取模式、共享模式、繼承模式等。底下說明 BX 各位元的意義:

    位元意  義
    0∼3 表示存取模式:
       0000:唯讀,只能讀取不可寫入
       0001:唯寫,只可寫入不可讀取
       0010:可讀可寫
       0100:唯讀,且不改變檔案最後存取時間
    4∼6 表示共享模式,只有在 SHARE.EXE 常駐時才需要用到這三個位元,底下是它們的說明:
       000:可被其他程式再開啟
       001:禁止被其他程式再開啟
       010:可被其他程式開啟,但僅可讀取,禁止寫入
       011:可被其他程式開啟,但僅可寫入,不可讀取
       100:可被其他程式開啟,可讀可寫
       111:可在網路中被其他機器使用。
    7 表示繼承模式,此位元設為 1,表示子行程不能繼承此檔案,此位元為 0 表示子行程可繼承此檔案。
    8 此位元設為一時,不使用緩衝區資料直接到磁碟上存取;設為零時則使用緩衝區的資料,若緩衝區沒有才到磁碟上存取。
    9 此位元設為一時表示此檔案不用壓縮格式,即使該檔案所在磁碟機為壓縮硬碟;設為零時則使用壓縮格式,但是只有在該檔案所在硬碟為壓縮硬碟才有效。
    A  
    B∼C 沒有使用。
    D 此位元設為一時,發生嚴重錯誤 ( 例如開啟軟碟片上的檔案,但軟碟片沒放入磁碟機中 ) 時,不執行 INT 24H;反之設為零時,發生嚴重錯誤則執行 INT 24H。
    E 此位元設為一時,資料直接寫入檔案;設為零時等緩衝區滿了再寫入檔案。
    F 沒有使用。

    CX 表示屬性,各位元所代表的意義如下表:

    位元 意  義位元 意  義
    0 此位元為一時,為唯讀檔 4 保留無用,必須為零
    1 此位元為一時,為隱藏檔 5 此位元為一時,為普通檔案
    2 此位元為一時,為系統檔 6 保留無用,必須為零
    3 此位元為一時,為磁碟機卷名 7 此位元為一時,為 Novell NetWare 共享

    DX 是依據檔案是否存在來決定所採取的動作,這些動作包含開啟、重新產生或出錯,其意義如下表:

    數值動作名稱 如果檔案已存在如果檔案不存在
    0001H OPEN 開啟出錯
    0002H TRUNC 重新產生出錯
    0010H CREATE 出錯重新產生
    0011H OPEN+CREATE 開啟重新產生
    0012H TRUNC+CREATE 重新產生重新產生


    回到首頁到第十四章到第十六章