Ch 09 顯示記憶體的內容

真實模式的記憶體

IBM PC/XT/AT 及其相容電腦的歷史簡述

Intel 公司出品的 80386 等級以上的 CPU 可在真實模式(real mode)、保護模式(protect mode)及虛擬 86 三種模式下操作,但是在這裡只探討真實模式。在真實模式下 CPU 最多只能使用 1MB(1048576 個位元組) 的記憶體。假如你的電腦配有超過 1MB 的記憶體,在 DOS 真實模式下,也只能使用 1MB 的記憶體,多餘的記憶體也不能使用。

為什麼會有這樣的限制呢?這要從西元 1980 年 4 月說起。在那年之前,最流行個人電腦機種是蘋果公司出品的 Apple ][ 電腦,它配有 8 位元的 6502 CPU,最多可以使用 64KB(65536 個位元組) 的記憶體。那一年 IBM 打算推出新一代的 16 位元個人電腦,稱為IBM PC,就是 personal computer 之意,之後一兩年之內 IBM PC 大賣,幾乎佔了 Apple ][ 的市場。IBM 所用的策略是公開 PC 的內部架構,所以許多相容廠商只要有意願都可以販賣製造 PC 的相容機種。

後來,IBM 另外又推出了型號 XT 的電腦,它與 PC 最大的差別在 XT 配備了 10MB 的硬碟機,後來再推出以 80286 CPU 為基礎的 AT (advance technique) 個人電腦。之後各家相容廠商推出的相容電腦銷售比 IBM 還好,以致演變到 IBM 無法再也掌握 PC 的規格,所以變成現在的 PC 是百家爭鳴,蓬勃發展。當然現在的 PC 內部架構和當初 IBM 生產的 PC 架構幾乎全不同了,但是 PC 這個名詞已經變成指專為一個人所使用的電腦,而不是以前 IBM 推出的 PC。

當初 IBM PC 可以使用 1MB 的記憶體,在當時可以說是相當的大,比當時的 Apple ][ 大了 16 倍,在當時沒有人說不夠,就像現在一般電腦約 128MB 到 256MB 的記憶體,假如有一台電腦使用 2048MB 的記憶體,一定沒人會說太小的。所以這 1MB 的限制也是歷史演進的結果,非是設計者的錯誤。

8088 的位址表示法

當初 IBM 採用 Intel(英特爾) 生產的 8088 為 CPU。8088 有 20 條位址線,可以定址到 1MB(220=1048576),1MB 的大小是 1048576 個位元組,換算成十六進位是 100000H,這 100000H 個位元組的記憶體從 00000 到 FFFFFH,所以如果要存取某一個記憶體的內容必須要有暫存器可以容納五個十六進位的位數(就是要有 20 位元的暫存器),然後才能存取。

但是這樣會產生許多更大的困擾,例如當 20 位元的暫存器和 16 位元的暫存器要交換資料時該如何處理?而且電腦工業都是以位元組(就是 8 個位元)的整數倍來作為計數單位,如 16、32、64 等來作為單位,所以 8088 的暫存器都是 16 位元,但是這樣最多只能存取 64KB 記憶體,並不能存取 1MB 的記憶體。

當初設計 8088 的工程師便想到一個很好的方法。就是採用分段處理,將 64KB 定義為一個區段(segment),64KB 的大小剛好是從 0000H 到 FFFFH,可以用 16 位元表示﹔然後 1MB 的記憶體可以分成十六個區段,這些區段可以用 16 位元的暫存器表示。我們實際上在程式中使用時,就用 Segment:Offset 來定出這 1MB 記憶體中的任何一個位元組,Segment 所代表的就是區段位址,Offset 所代表的就是偏移位址。

Segment 和 Offset 都是用 16 位元來表示,Segment:Offset 所表示的實際位址是 16D*Segment+Offset,例如記憶體 01AD:0021 其實是表示 01ADH 乘以 10H 再加上 0021H 的記憶體,等於 1AF1H 的位址。用 Segment:Offset 這種表示位址的方法就叫『分段位址』,若是直接用 20 位元的十六進位數來表示位址的方法就叫『絕對位址』或『線性位址』。請看下圖:

任何一個位址用絕對位址來表示,只有一種表示法;但是用分段位址,會有許多種表示法。例如上述 01AD:0021 和 01AE:0011、01AF:0001 都是同一絕對位址 1AF1H。同時您可以發現,區段位址每減一,偏移位址得加 10H。

雖然以現在眼光來看,現在電腦至少也有 64MB 的記憶體,128MB 的也不在少數,1MB 的記憶體非常小。但是在那個年代,1MB 的記憶體已經很大了,大多數的電腦只裝 640KB,Lotus 1-2-3 只要 256KB 就能跑得很順。


顯示 1MB 內的記憶體內容原始程式

底下小木偶撰寫一個程式 MEMDUMP.ASM,可以顯示任何在 1MB 以內記憶體內的內容。這個程式會顯示某一區段的記憶體內容,可以按鍵盤上的 S 鍵改變區段,按 O 鍵改變偏移位址,按 Esc 鍵跳出程式﹔用 PageUp 鍵或 PageDown 鍵來顯示上一頁或下一頁的 140H 個位元組。這個程式無法在 Windows XP/2K/Vista/7 堸鶡獢A只能在 DOS 或 Windows 9x/Me 中的 DOS 模式執行,最好是將工具列的關閉,才不會有亂碼﹔在 MS DOS 下的倚天中文裡,請按住 Ctrl、Alt 兩個鍵不放,再按下英文字母 A 鍵,在中、英文間切換避免亂碼。( 如果您想試試看這個程式的話,得在 Windows XP/2K/Vista/7 埵w裝類似 Virtual PC 之類的 DOS 模擬器,請參閱附錄 11。)
;***************************************
memdump segment
        assume  cs:memdump,ds:memdump
        org     100h
;---------------------------------------
start:  jmp     begin
seg_adr dw      0           ;007
off_adr dw      0           ;008
row     db      ?           ;009
msg     db      ' Seg:Off  00 01 02 03 04 05 06 07-08 09 0A '
        db      '0B 0C 0D 0E 0F  01234567-89ABCDEF',0dh,0ah,'$'
ipt_of  db      ' Input offset address : $'
ipt_sg  db      'Input segment address : $'
begin:  call    print_msg   ;014

        mov     row,3       ;016 由第四列開始
nxt_rw: mov     ah,2        ;017 設定AH為游標定位
        mov     dl,0
        mov     dh,row
        mov     bh,0
        int     10h         ;021 呼叫INT 10H中斷
        mov     bx,seg_adr  ;022 將要印的區段放入BX
        mov     es,bx       ;023 設定區段,以利往後將記憶體內容取出
        call    print_bx_hex;024 印出
        mov     dl,':'
        call    print
        mov     bx,off_adr
        call    print_bx_hex
        mov     dl,' '
        call    print       ;030 印出空格

        mov     si,off_adr
        mov     ch,10h      ;033 每列印出16個位元組
nxt_col_0:
        mov     bl,es:[si]  ;035 取出記憶體內容存於 BL
        call    print_bl_hex    ;036 印出記憶體內容
        mov     dl,' '      ;037 印出空白
        mov     ax,si
        and     al,0fh      ;038 檢查是要印空白或『-』
        cmp     al,7
        jne     pnt_sp      ;041 若是AL=7要印出『-』
        mov     dl,'-'      ;042 印出『-』
pnt_sp: call    print       ;043 印出
        inc     si          ;044 指向下一個位址
        dec     ch          ;045 檢查是否已印完16個位元組了?
        jnz     nxt_col_0

        mov     dl,' '      ;048 記憶體的內容已印完,印出ASCII字元
        call    print
        mov     si,off_adr  ;050 要印出ASCII字元,故重新取回偏移位址
        mov     ch,10h
nxt_col_1:                  ;052 判斷要不要印『-』
        mov     ax,si
        and     al,0fh
        cmp     al,8
        jne     nt_pnt
        mov     dl,'-'
        call    print
nt_pnt: mov     dl,es:[si]  ;059 取出記憶體
        cmp     dl,' '      ;060 若ASCII碼小於1Fh,則印出空白
        jae     ok_pnt
        mov     dl,' '
ok_pnt: call    print
        inc     si
        dec     ch          ;065 檢查是否已印完16個ASCII字元了?
        jnz     nxt_col_1

        inc     row
        mov     off_adr,si  ;069 指向下一個16位元組記憶體的前端
        cmp     row,23      ;070 是否已印完一頁?
        jnz     nxt_rw
press_key:                  ;072 已印完一頁,等使用者按鍵
        mov     ah,0
        int     16h
        cmp     ah,01h
        je      esc_key     ;076 按下Esc鍵
        cmp     ah,1fh
        je      s_key       ;078 按下S鍵
        cmp     ah,18h
        je      o_key       ;080 按下O鍵
        cmp     ah,49h
        je      pgup        ;082 按下PgUp鍵
        cmp     ah,51h
        jne     press_key

pgdn:   jmp     begin       ;086 按下PgDn鍵

pgup:   sub     off_adr,140h*2
        jmp     begin

s_key:  mov     cx,offset ipt_sg
        call    input
        mov     seg_adr,bx
        sub     off_adr,140h
        jmp     begin

o_key:  mov     cx,offset ipt_of
        call    input
        and     bx,0fff0h
        mov     off_adr,bx
        jmp     begin

esc_key:
        mov     ax,4c00h
        int     21h         ;105 結束程式返回DOS
;---------------------------------------
print_msg       proc    near
        mov     ah,2
        mov     dx,100h
        mov     bh,0
        int     10h
        mov     dx,offset msg
        mov     ah,9
        int     21h
        mov     cx,76       ;115 115到116的目的是
next_c: mov     dl,0cdh     ;116 印出第2列的雙橫線
        mov     ah,2
        int     21h
        loop    next_c      ;119
        ret
print_msg       endp
;---------------------------------------
;在銀幕上印出 BL 的十六進位數
;輸入:BL
print_bl_hex    proc    near
        mov     cl,4
        rol     bl,cl
        call    print_4_bits
        rol     bl,cl
        call    print_4_bits
        ret
print_bl_hex    endp
;---------------------------------------
;目的:在銀幕上印出 BX 之十六進位值
;輸入:BX
print_bx_hex    proc    near  ;136
        mov     cl,4
        rol     bx,cl
        call    print_4_bits
        rol     bx,cl
        call    print_4_bits
        rol     bx,cl
        call    print_4_bits
        rol     bx,cl
print_4_bits:
        mov     dx,bx
        and     dl,0fh
        add     dl,30h
        cmp     dl,3ah
        jb      print
        add     dl,7
print:  mov     ah,2
        int     21h
        ret
print_bx_hex    endp
;---------------------------------------
;目的:輸入一個十六進位數
;輸入:DX-提示字串
;輸出:BX-輸入的十六進位數
input   proc    near
        mov     ah,2
        mov     dx,1700h
        mov     bh,0
        int     10h
        mov     ah,9
        mov     dx,cx
        int     21h

        mov     cx,404h ;169 CH=4位16進位數
        sub     bx,bx
next:   mov     ah,0
        int     16h     ;172 呼叫BIOS鍵盤服務程式
        cmp     al,0dh  ;173 若輸入Enter鍵表示要輸入完成
        je      ok1
        mov     dl,al   ;175 將輸入的ASCII碼存入DL以利印在螢幕上
        cmp     al,'0'  ;176 檢查輸入的ASCII碼是否為阿拉伯數字
        jb      next
        cmp     al,'9'
        ja      alpha   ;179 若否,則檢查是否為A到F的英文字
        sub     al,'0'  ;180 減去30h,以得到十六進位數
        jmp     short ok0
alpha:  and     al,0dfh ;182
        mov     dl,al
        sub     al,37h  ;184 檢查是否為A到F的英文字
        cmp     al,0ah
        jb      next
        cmp     al,0fh
        ja      next    ;188

ok0:    push    ax      ;190
        call    print   ;191 在螢幕上印出輸入的字
        pop     ax
        shl     bx,cl   ;193 將BX乘以16d
        cbw
        add     bx,ax   ;196 使BX加上輸入的16進位數
        dec     ch      ;197 檢查是否超過4位十六進位數
        jz      ok1
        mov     dh,bh   ;199 檢查是否超過BX容量
        and     dh,0f0h
        jz      next    ;201 沒超過

ok1:    push    bx      ;203
        mov     ah,2
        mov     dx,1700h
        mov     bh,0
        int     10h
        pop     bx
        mov     cx,57   ;209 209到213的目的是當輸入完
ok2:    mov     ah,2    ;210 成後,清除螢幕上第23行的字
        mov     dl,' '
        int     21h
        loop    ok2     ;213
        ret
input   endp
;---------------------------------------
memdump ends
;***************************************
        end     start

程式規劃

這個程式算是比較大的程式了,在寫比較大的程式之前,應先規劃好,才不會給人凌亂的感覺,以致讓日後維護程式時花費許多時間。

如何規劃

要規劃程式當然要先知道程式有什麼功能,執行時有什麼畫面?所以我們先看看執行的情形:

在上圖中,和 DEBUG 中 D 指令的畫面一樣,大致可分為三部份,最左邊是位址位址是以分段定址方式表示,區段位址與偏移位址都是十六位元。中間是記憶體內容,以每列十六個位元組列出,每個位元組都是 8 位元。最右邊是每個位元組對應的 ASCII 字元。

很明顯每一列都是列出 16 個位元組的資料,而每列的下面一列都比上面一列的記憶體位址多 16,而且顯示方式不管是位址、記憶體內容或 ASCII 字元每一列都相同。所以如果能寫出顯示一列的程式,而其他列就依樣畫葫蘆,很快就可以完成此程式了。而每一列的差別只是下面一列的位址比上面一列的位址多 16D。所以小木偶設一個變數 off_adr(offset address 之意),代表每列最低的偏移位址。

每一列最左邊必須要印出兩個 16 位元的內容,小木偶就將要印出的內容存放在 BX,然後呼叫 print_bx_hex 副程式即可。中間必須印出 16 個 8 位元的內容,小木偶將它存放在 BL 中呼叫 print_bl_hex 副程式印出,而且要重複 16 次,每次偏移位址都增加一,為防止改變 off_adr 之值,所以用 SI 作為指標代替 off_adr。

其他細節

如果使用者按下 PgDn 鍵的話,程式就得顯示下一頁 140h 個位元組,這 140h 個位元組的第一列比前一頁的最後一頁多 10h,因此只需將 off_adr 再增加 10h ,但是在處理完了前一頁時就已經加 10h 了,所以不用再加 10h。

但是如果使用者按下 PgUp 鍵時,就得顯示上一頁,而此時 off_adr 卻指向下一頁的第一個位元組,所以 off_adr 要減去 140h 的兩倍。

如果使用者按下 S 鍵或 O 鍵,程式得讓使用者輸入一個十六位元的數字,分別改變區段位址和偏移位址,而區段位址和偏移位址分別存入 seg_adr 和 off_adr。至於如何輸入十六位元的數字,請參考上一章


新的指令與服務中斷

INT 10H/AH=2 中斷

INT 10H 這個中斷服務程式是主機板上的 BIOS 提供的,主要用途是提供螢幕的服務程式,所要的服務放在 AH 暫存器中。這些服務包含設定顯示模式、設定游標大小、設定游標位置等等。

在這個程式裡,小木偶使用的是將游標固定在在螢幕的某個位置,這時 AH 必須設為 2,表示我要 BIOS 服務程式幫我設定游標位置,而 BH 暫存器必須放螢幕頁,一般都設為零,DL、DH 分別存放螢幕的行與列。

在 DOS 模式下,由左而右稱為列,每一列由最左邊開始編號為零,一直到最右邊編號 79,每一列最多總共可以顯示 80 個英文字。由上而下稱為行,每一行也是從編號零開始,一直到 24,最多可以顯示 25 個字。這個程式的

        mov     row,3       ;016 由第四列開始
nxt_rw: mov     ah,2        ;017 設定AH為游標定位
        mov     dl,0
        mov     dh,row
        mov     bh,0
        int     10h         ;021 呼叫INT 10H中斷
片段就是設定游標在螢幕的第零行第三列。請參考下圖說明:
說明螢幕行列關係

直接定址與暫存器間接定址

接下來還有一個大問題要解決,就是如何取得記憶體的內容。你可以想像:如果你要到一個你不熟悉的地方找你朋友,而你得知道你朋友住在那裡,或是說你得知道你朋友的地址,你才能找到。

在電腦中也是一樣,如果要由記憶體取得其內容,必須要告訴電腦要取得那一個記憶體的內容才行,這『那一個記憶體』用術語來說就是記憶體的『位址』。

事實上,我們在前面幾章已經用過這個觀念了,當我們定義一個變數時,就是告訴組譯器要保留一個位址給這個變數,然後組譯器會用這個位址代替原始檔中的變數名稱。所以當程式中要存取這個變數時,事實上是到該位址去存取。這種方法稱為『直接定址』。

但是在這個程式裡,因為我們要顯示任何一個記憶體的內容,所以存取的位址會一直改變,我們就無法用上面的方法,幸好組合語言提供了另一種存取記憶體的方法,稱為『暫存器間接定址』,例如程式中的

        mov     bl,es:[si]  ;035 取出記憶體內容存於 BL
		................................
nt_pnt: mov     dl,es:[si]  ;059 取出記憶體
就是屬於這類定址方式。

以第三十五行為例,這一行是把 ES:SI 所指位址的內容取出,存放於 BL,有點複雜是吧?沒關係,每個學習組合語言的人都要花一些時間來熟悉它。先假設,如果在 DEBUG 裡輸入下面黃色的指令:

-d 0:0 [Enter]
0000:0000  9E 01 00 00 00 04 70 00-16 00 FC 07 65 04 70 00  ......p...|.e.p.
若此時 ES 為零,而 SI 為 0,則執行第 35 行指令時,CPU 會到 0000:0000(就是 ES:SI 所指的位址) 去找,結果該位址的內容是 9EH,所以就會把 9EH 存入 BL 中。

如果現在 SI 變成 1 時,BL 就會變成 01H。若 SI 為 2 時,BL 就會變成 00H。可以用下圖說明:

所以只要使 SI 由 0000 遞增到 0FFFFH,就可以存取整個區段的內容。如果要存取其他區段,只需改變 ES 值即可。

總言而之,暫存器間接定址是把記憶體位址放入某個暫存器裡,並用中括弧將暫存器括弧起來,就能取得暫存器內容所指的記憶體數值,而非暫存器的內容。可以用來作為暫存器間接定址的暫存器有 BX、SI、DI、SP 四個,一般而言如果寫成像下面的程式片段:

        mov     dh,[bx]
        mov     dl,[si]
CPU 會到 DS 所指的區段找 BX、SI 內的記憶體位址所含的內容;如果用 DI 作為暫存器間接定址的暫存器,則 CPU 會到 ES 所指的區段去找;若是用 SP 的話,CPU 會到 SS 去找,這些是內定的(或是稱隱含的)。當然也可以改變,就是在中括弧前面加上要尋找的區段。於是就變成程式中的第 35、59 行了。

新的指令 DEC

第45行指令 DEC 是使後面的變數或暫存器之值減一,變數或暫存器可以是8位元或16位元。如果經過減一後,暫存器或變數之值變為零時,零其標會被設為1,所以下一行可以直接檢查運算結果是否為零,來決定要不要跳躍。用法如下:
dec     暫存器
dec     變數

新的指令 ROL/ROR

第127行指令 ROL 是旋轉指令,它和 SHL 指令很相像,差別是在 SHL 是向左移位指令,原來最左邊的位元會被移出暫存器而消失不見,最右邊會填進0﹔而 ROL 最左邊的位元會被移到最右邊,重新填進暫存器內。如下圖:

ROR 是向右旋轉。ROL、ROR 用法如下:

ROL     暫存器
ROL     變數
ROR     暫存器
ROR     變數


註一:一般科學上,千、百萬的數量分別是 103、106 的意思,常常用 K、M 分別表示千、百萬。但是在電腦上 K、M 常常是代表 210、220 的意思,210=1024 而 220=1048576 都很接近一千、一百萬。


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