Ch 16 檔案處理(2):在螢幕上印出檔案內容

程式使用方法

在這一章裡,小木偶將示範如何在螢幕上印出檔案內容,其實這個功能和 DOS 的 TYPE 指令很像,但是 TYPE 如果沒有加上『管線』的話,前面的內容很快就會捲動到螢幕上端而消逝,而且 TYPE 指令無法回到上一頁觀察。小木偶打算將這些缺點改善,可以用 PageUp 或 PageDown 按鍵觀看上一頁或下一頁,按 Esc 鍵跳回 DOS。這個程式,小木偶取名為 DISPLAY.COM,用法是:

DISPLAY 欲觀看之路徑檔名

原始程式

程式如下:

block   equ     25*80           ;01 每次顯示一頁,一頁有25行,每行80個字
;***************************************
code    segment
        assume  cs:code,ds:code
        org     100h
;---------------------------------------
start:  mov     si,80h          ;07 指向 PSP 的參數區
        lodsb                   ;08 取出要顯示之檔案名
        or      al,al           ;09 檢查是否有輸入檔案名
        jnz     g_fn0
        mov     dx,offset mes1
error:  mov     ah,9
        int     21h
        mov     al,01h
exit:   mov     ah,4ch          ;15 結束 DISPLAY 程式
        int     21h

g_fn0:  cbw                     ;18 有輸入檔案名
        add     si,ax
        mov     byte ptr [si],0 ;20 在檔名後加上0,使成ASCIIZ字串
        mov     dx,82h
        mov     ax,3d00h
        int     21h             ;23 開啟檔案
        mov     dx,offset mes2
        jc      error
        mov     handle,ax       ;26 存入檔案代碼

        sub     bx,bx           ;28 將 0 存於頁數
        mov     pt_pg,bx
        mov     top_pg[bx],bx   ;30 因為檔案的第零個字必定顯示在
;讀取檔案內容                   ;31 必定顯示在第零頁第零行第零字
read:   mov     cx,block
        mov     dx,offset content
        mov     bx,handle
        mov     si,dx
        mov     ah,3fh
        int     21h             ;37 讀取2000個字元
        mov     di,si           ;38 設定每次讀取之最後一個字之位址
        add     di,ax           ;39 ,並存於 DI暫存器
;印出一頁文字
print:  mov     al,0            ;41 設定螢幕之x座標,column,及y座
        mov     row,al          ;42 標,row,讓二者均為0,使其指向
        mov     column,al       ;43 第零行第零列
next:   mov     dl,[si]
        mov     ah,2
        int     21h             ;46 印出文字
        inc     si
        cmp     dl,0dh          ;48 若遇到換行字元或
        je      inc_line
        inc     column
        cmp     column,80       ;51 該行已經到達80個字元時
        je      inc_line        ;52 就該換行了
        cmp     si,di           ;53 是否已經到讀取的最後一個字了
        jne     next
        mov     dx,pt_pg        ;55 設定最大頁數及檔案結束
        mov     eof,1
        mov     max_pt,dx       ;57
        jmp     key_input

inc_line:
        mov     ah,0
        inc     row             ;62 增加一行
        mov     column,ah
        cmp     row,25
        jne     next
key_input:                      ;66 等待鍵盤輸入
        int     16h
        cmp     ah,1
        je      esc_key
        cmp     ah,49h
        je      pgup_key
        cmp     ah,51h
        je      pgdn_key
        mov     ah,0
        jmp     key_input       ;75
esc_key:
        mov     al,0            ;77 按下Esc鍵,結束程式
        jmp     exit

pgup_key:
        cmp     pt_pg,0         ;81 若已經在第零頁就不處理
        jz      key_input
        dec     pt_pg           ;83 頁數減一
        mov     bx,pt_pg
        shl     bx,1
        mov     dx,top_pg[bx]
        jmp     short move_file_pointer

pgdn_key:
        cmp     eof,1
        jne     pgdn0
        mov     dx,max_pt       ;92 檢查是否已經是最後一頁
        cmp     dx,pt_pg
        jz      key_input
pgdn0:  mov     bx,pt_pg        ;95 取得這一頁第零個字在檔案的第幾個位址
        shl     bx,1            ;96 並存於 AX 暫存器
        inc     pt_pg           ;97 指向下一頁
        mov     ax,top_pg[bx]
        sub     si,offset content
        mov     bx,pt_pg
        shl     bx,1
        add     si,ax
        mov     top_pg[bx],si   ;103 設定下一頁第零個字在檔案的第幾個位址
        mov     dx,si
move_file_pointer:
        mov     ax,4200h        ;106 移動檔案指標
        mov     bx,handle
        mov     cx,0
        int     21h
        jmp     read

;---------------------------------------
mes1    db      'No file.$'
mes2    db      'Open file error.$'
handle  dw      ?               ;115 檔案代碼
eof     db      0               ;116 End of file,若為一,表示檔案結束
column  db      0               ;117 螢幕之行數,由0到79共80行,由上而下
row     db      0               ;118 螢幕之列數,由0到24共25列,由左而右
pt_pg   dw      0               ;119 第幾頁,pointer of page,由零開始
max_pt  dw      ?               ;120 最大頁數
content db      block dup (?)   ;121 讀取檔案時的資料存放處
top_pg  dw      512 dup (?)     ;122 每一頁的第零行第零列的那個字在檔
                                ;123 案的第幾個字
;---------------------------------------
code    ends
;***************************************
        end     start

程式結構及邏輯

這個程式裡,按下 PageUp 鍵或 PageDown,程式就會顯示 25 行的內容,這 25 行小木偶可以稱之為『一頁』,當使用者按下這兩個鍵的其中任何一個,程式必須馬上找到要顯示的內容,然後顯示在螢幕上。假如能夠找到每一頁要顯示內容的第一個(在此程式稱為第零個,以下也稱第零個)字在檔案的那一個地方,問題就解決了大半。小木偶使用一個變數 top_pg 來儲存每一頁第零個字在檔案中的『位址』。

事實上,top_pg 不只一個變數,他是由許多相同性質的未知數構成的,什麼叫『相同性質的』呢?top_pg 內所儲存的數都是代表著每一頁的第零個字在檔案的那一個地方(所謂在那一個地方是指,由檔案起始處算起的第幾個字,起始處是第零個,下一個是第一個,再下一個是第二個……),這些表示相同意義的變數,集合起來稱為『陣列』。top_pg 就是一個陣列,在此 top_pg 是由 512 個字組( 一個字組大小是 16 位元 )所構成的一維陣列,第零個字組表示第零頁,第一個字組表示第一頁,第二個字組表示第二頁……。而現在在螢幕上所顯示的是第幾頁,存於 pt_pg 變數中,這個變數也可以說是 top_pg 的指標。如下圖所示:

top_pg 說明圖

你可以想像,任何一個檔案顯示在螢幕上,第零頁的第零個字一定是在螢幕上的第零頁第零字,而下一頁 (也就是第一頁) 的第零字就會隨每一行的長度不同而有變化了。假如有一個檔案其內容為『琵琶行』:

潯陽江頭夜送客 0
楓葉荻花秋瑟瑟 1
主人下馬客在船 2
舉酒欲飲無管絃 3
最不成歡慘將別 4
…………………
嘈嘈切切錯雜彈 24
大珠小珠落玉盤 25
間關鶯語花底滑 26
幽頁泉流水下灘 27
…………………
血色羅裙翻酒污 49
今年歡笑復明年 50
…………………

因為每個中文字含兩個位元組,所以每一行包含歸位字元(CR)及換行字元(LF)共 16 個字, 那第零頁的第零字就是檔案的第 0 字,第一頁的第零字就是檔案的第 400 個字,第二頁就是第 800 個字。

而一開始,程式並不曉得檔案有多大,無法估算有幾行幾頁,所以假設 512 dup (?) 來定義 top_pg (原始碼的第 122 行),因此這個程式只能開啟 512 頁,若超過了程式並沒有檢查措施可能會造成錯誤,但也可能不會。至於使用 dw 則限制了這個程式最多只能顯示開啟檔案的前 65536 個字,若超過了,顯示會不正確。當然你也可以針對這兩個缺點加以改善。

假如解決了每一頁的第零個字是在檔案的何處,程式很快地就可以使用上一章所談過的 DOS 中斷服務程式,AH=42H/INT 21H (請參考第十五章註二),來移動檔案指標到該處,並且讀取該處的內容至記憶體中。至於要讀取多少長度程式事先也無法預知,所以小木偶假設讀取 2000 個字,因為螢幕共 25 行(row),80 個(column,也可說 80 列)字,所以每一頁最多有 2000 個字。

整個程式大致結構就如上所言,底下是程式的流程圖:

流程圖

當檔案開啟之後,display 程式立刻讀取檔案指標 (剛開啟時檔案指標為零,指向檔案最前面) 所指位址後的 2000 個位元組,而後用 AH=02/INT 21h 將字元印在螢幕上),當遇到歸位字元或是超過一行 80 個字時,就要換行了 (原始程式的第 48 行到第 52 行檢查);當達到 25 行時,表示已經印完一頁了(原始程式的第 64 行檢查),並等待使用者按鍵(原始程式的第 66 行到第 68 行)。在這段印出一頁的程式裡,我用兩個變數,column 和 row,來記錄現在印到螢幕的第幾行第幾列,小木偶只要檢查這兩個變數是否超過 80 或 25 就可以了。

當螢幕顯示一頁後,就用 AH=0/INT 16H 呼叫 BIOS 中斷服務程式,等待使用者輸入(程式第 66、67 行),然後判斷按下那一個鍵,跳到適當地方執行。Y是按下 Esc 鍵,就結束程式 (第 76 到 78 行)。

若是按下 PageUp 鍵,先檢查現在在螢幕上顯示的是否第零頁(也就是檔案的最前面),也就是檢查 pt_pg 是否為零的意思,如果是的話,就不處理跳回等待鍵盤輸入的地方。如果不是的話,將 pt_pg 減一,並以 pt_pg 之值取得該頁的第零字在檔案的位置(程式第 84 行到 86 行),然後跳到移動檔案指標的程式(程式第 87 行),移動檔案指標(程式第 105 行到 109 行),再跳到讀取檔案內容(程式第 110 行),讀取(程式第 32 行到 37 行)。

若是按下 PageDown 鍵,處理方式和 PageUp 不太相同,因為當程式開啟時並不知道一共有幾頁,所以也無法知道最大頁數是多少,這個表示最大頁數的變數,小木偶定義成 max_pt,只有當讀取檔案到最尾端時,程式才能得知,小木偶在原始程式的第 53 行檢查是否已讀到最後一個字元,如果是的話將 eof 變數設為一,而這一頁也就是最大頁數了(程式第 55 到 57 行)。到了按下 PageDown 鍵時,就可以判斷是否已經讀過檔案最尾端,pt_pg 是否已經是最大頁數,如果這兩個條件都成立(程式第 90 到 94 行),那就不處理跳到等待鍵盤輸入的地方。只要有一個條件不成立,就得在螢幕顯示下一頁。在顯示下一頁之前,得先取得下一頁的第零字在檔案何處,而這個位置可以由這一頁的第零字的位置再加上這一頁的最後一字的位置加一(存於 SI,詳見後面說明)得到,見程式第 95 行到第 103 行。其餘和 PageUp 的情形類似就不贅述。


系統知識及觀察

取得參數

這個程式的使用方法是直接在命令列輸入

display 要顯示之檔名

那程式是如何取得這個檔名呢?或是說假如有個程式還有其他的參數,系統又是如何傳遞給程式,程式要如何取得呢?

原來其中的祕密在程式前的 100H 位元組內,還記得每次我們寫 COM 檔都要從 100H 開始嗎?這 100H 之前的區域稱之為『程式前置區』(program segment prefix,簡稱 PSP ),其中有許多是存放系統和程式溝通的資訊,其中在第 80H 到 0FFH 之內的區域稱之為參數區,其內所存的資料就是存放 DOS 命令提示下所輸入之程式後面,使用者所輸入的參數。而 80H 位址所存放的就是參數的總長度。

這樣說,你也許還不太明白,讓我舉個例子吧。當小木偶在 DOS 模式下指令:

H:\HomePage\SOURCE\>display protect2.txt

時,『display』是程式名稱,『 protect2.txt』就是所謂的參數。但是我們為了方便觀察,必須用 DEBUG 來觀察記憶體內的情形 (請參考第二章),而不是像上面那樣下指令。小木偶的做法是,在 DOS 下指令

H:\HomePage\SOURCE>..\masm50\debug display.com protect2.txt

然後我們看看參數區有什麼內容:

-d 80 L30
1E18:0080  0D 20 70 72 6F 74 65 63-74 32 2E 74 78 74 0D 00   . protect2.txt..
1E18:0090  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
1E18:00A0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................
-

是不是在 81H 到 8DH 內的資料就是我們剛剛輸入的參數部份,共 0DH 個位元組,而 80H 的內容就是參數長度。在輸入參數外,後面還有一個 0DH 這顯然就是使用者按下鍵盤的 Enter 鍵。

觀察 LODSB/LODSW 指令執行情形

LODSB 指令是把 DS:SI 所指的記憶體一個位元組的大小搬到 AL 暫存器中。同時使 SI 暫存器增加一,或減少一,指向下一個位址以便下次讀取記憶體的內容,至於是增加一或減少一,要看『方向旗標』( 請參考附錄二旗標暫存器 )而定,假如方向旗標為零(清除)的話,則 SI 加一﹔反之減一。

例如在 DEBUG 內,

-d 70 L20 [Enter]
1E18:0070  20 20 20 20 20 20 20 20-00 00 00 00 00 00 00 00          .........
1E18:0080  0D 20 70 72 6F 74 65 63-74 32 2E 74 78 74 0D 00   . protect2.txt..
-r [Enter]
AX=0000  BX=0000  CX=0CF0  DX=0000  SP=FFFE  BP=0000  SI=0080  DI=0000
DS=1E18  ES=1E18  SS=1E18  CS=1E18  IP=0103   NV UP EI PL NZ NA PO NC
1E18:0103 AC             LODSB
-t [Enter]
AX=000D  BX=0000  CX=0CF0  DX=0000  SP=FFFE  BP=0000  SI=0081  DI=0000
DS=1E18  ES=1E18  SS=1E18  CS=1E18  IP=0104   NV UP EI PL NZ NA PO NC
1E18:0104 0AC0           OR     AL,AL

執行前,SI 為 80H,AL 為 00H,方向旗標為 UP,表示加一。執行後 SI 變為 81H,AL 為 0DH。

要清除方向旗標要用 CLD 指令( clear direction flag ),若要設定方向旗標要用 STD 指令( set direction flag )。

而 LODSW 則是每次搬一個字組(16個位元)到 AX 暫存器,同時使 SI 之值加 2 或減 2。這兩個指令常常用在處理字串上,其原文是『Load string by byte』和『Load string by word』,所謂 Load,中文是載入之意,可以想成是把資料由記憶體載入到 CPU 的暫存器內。


回到首頁到第十五章到第十七章