由於我們大多不是硬體晶片設計師,所以無法天生就搞懂「為何處理器要用這種方式運作」。所以想理解「處理器原理」最簡單的策略,就是多寫點該處理器的組合語言程式。
印出hello, world的程式碼分析
寫8086組合語言之前,要先記住,你的角色比較像是「空間規畫師」而不只是程式碼撰寫員。
我講過兩百遍的說法是:處理器就像是磨坊,記憶體就像是穀倉,你得適當地分配穀物從穀倉進入磨坊的流程與空間的配置,這系統才會正確且有效的運作。
所以,當我們寫一個8086小小組合語言時,要先知道的就是,我們的程式得把「資料」放在某個區段,把「程式」放在另一個區段。
請參考,「印出hello, world程式」:
ORG 100h
MOV DX, OFFSET MESSAGE
MOV AH, 09h
INT 21h
RET
MESSAGE DB 'hello, world!$'
ORG 100h
這是所謂的.COM可執行檔案,組合語言程式執行開頭的「假指令」,目的只是告訴組譯器,「前面留下100h的空間」。100h是「16進位的100」的意思,若用10進位表示,100h(16進位)
= 256(10進位)。所有的.COM可執行檔都用這行字開頭。
MOV DX, OFFSET MESSAGE
執行INT 21的第09h號呼叫,你得把DS:DX這兩個暫存器指向「字串存在」的位址。由於.COM檔案很小,所以CS、DS、ES和SS四個節區都設在同一個位址,故DS暫存器的內容已經指向正確位址了,但DX暫存器得在印字前偏移到正確的「存放要印的字」的位址。其中,MESSAGE只是一個標籤,是最後一行,用來存放字串(hello,world!)的位址標籤。
這行程式碼翻譯成中文,就是「請把MESSAGE所指向的資料位址(也就是存放『hello, world!』字串的記憶體位址),移到DX暫存器裡面。」
MOV AH, 09h
這行程式碼,是要處理器「把09h移到AH暫存器」。
INT 21h
請處理器執行「第21h號中斷」。
請注意,執行中斷前,前面幾行程式(MOV DX, OFFSET MESSAGE、MOV AH,09h)都是準備工作。所有的21h號中斷,都用AH暫存器作為編號設定。比方說,AH = 2,執行的是印字元服務;AH = 9,是印字串服務。
RET
RET就是告訴處理器,請處理器把程式的控制權返回給呼叫者。在這個程式裡面,呼叫者就是作業系統(或者說,這個組譯器),所以程式執行到RET就結束了。
MESSAGE DB 'hello, world!$'
這行程式碼放在最後,只是用來儲存要印的字串。這字串是要給前面的程式碼列印的,MESSAGE只是標籤,用來指示字串的位址。字串的內容是「hello,world!」,但是最後放個錢符號(dollar sign)是用來指示「這是字串的結尾」,這是INT 21h中,第09h號呼叫的慣例。傳說中,除了CP/M的發明人,吉納德博士(據說CP/M是DOS的抄襲對象)以外,沒有人知道第09h號呼叫為何使用$作為字串結尾的符號。
至於「DB」,則是「Define Byte」的縮寫,意指後面的資料都是一個byte寬度。
組譯器和編譯器
程式碼寫好後,你得交給「組譯器」把程式碼變成「可執行檔」。上期請你安裝的emu8086就是組譯器。
嚴格說來,emu8086包含了組譯器、程式編輯器、除錯器和模擬器等等,是一個具體而微的組合語言開發環境。組譯器的工作,就是把你寫的程式(一般稱為原始碼)轉換成處理器可懂的2進位程式(一般稱為可執行檔)。
同樣的程式,如果你用C語言寫,要印個hello, world,其實只要輸入「printf("hello,world");」就可以了。如果用C++寫,則輸入「cout << "hello,world";」就可以了。但是,再講一遍,我們學組合語言,要了解的是「處理器的行為」,不是為了要容易開發程式。
在前面的組合語言範例中,要印出hello, world,好,我們得知道:
● 要用DOS的第09h號呼叫,且DS:DX要指向字串所在的記憶體位址。也就是說,處理器的運作過程中,你得不斷的控制暫存器內容(像是這裡的DS:DX的內容),讓它符合規範上的需求。
● 你得熟記各種呼叫的功能(像是這裡的09h號呼叫),以執行各種系統服務(像是印字、讀取鍵盤……)
● 你得規畫記憶體內容,以存放各種程式運作過程中的資料(像是這裡的DB 'hello, world$')
● 你得熟知各種處理器的指令集有哪些(像是這裡的MOV、RET),用法為何,以便熟練的運用在程式裡面。
● 你還得知道組譯器的使用方法,才能得到你要的程式執行結果
然後,你把程式寫好了,把原始碼饋入組譯器內,組譯器會把沒有問題的原始碼轉譯成可執行檔。
組譯結果的分析
再看看你執行程式的結果,會出現一個「模擬器」視窗(emulator),右邊是程式組譯的結果,左邊是「程式執行碼」。比方說,「MOV DX, OFFSET MESSAGE」已經被組譯成16進位的「BA0801」。
請問,「BA0801」誰能看懂這什麼意思?我想沒人能懂吧!
但是,處理器能懂的,就只是這些數字(事實上也只是這些數字化成的電位高低而已)。這整個印字程式,已經被組譯器組譯成「BA0801B409CD21C868……」這樣的程式碼。這類程式碼一般稱為「二進位程式碼」,正是因為「它們這樣的2進位形式,只有處理器才看得懂」。程式設計師用組譯器工作,是因為我們最多能看懂原始程式而已,2進位程式碼未免太難以記憶和理解了。但有時候,有些人會想要修改程式的行為(有時這被稱為「hack」或是「crack」),那就得借助一些工具的幫助,直接用2進位(或是16進位)修改可執行檔本身。對處理器而言,它只是傻傻地根據程式碼的樣子去執行,程式跑出來的結果是由程式設計師來控制,若是程式寫出來有問題,或是程式碼有問題,甚或是組譯器有問題(原始碼沒問題,但是組譯出來的結果有問題),這都可能會造成程式當掉。
這就是俗稱的bug。
比方說,如果在「第09h號呼叫」的印字串呼叫中,要列印的資料忘了加$,會發生什麼事?
你可以自己玩玩看。我只能說,結果是不可預期的。「只是一個$而已嘛!」不,對寫程式而言,一個$的差別,往往就是「正常」和「死當」的重大分野,不可不慎!
印出暫存器的2進位內容
好,我們再寫一個程式,這是我的組合語言教科書裡面的一個範例。「把BL暫存器裡的內容,以2進位形式印出來」。
如果你都沒學過組合語言,這樣的題目應該就要想破頭了,因為毫無頭緒。
本人經過作弊以後,倒是寫出來了,給各位解釋一下。
8086有個指令,稱為RCL,這表示「向左旋轉一個位元,且把旋轉的位元先放到Carry裡面」(Carry是進位旗標)。假設資料的2進位是10110100,那每RCL一次,Carry裡面就會依序出現1、0、1、1、0、1、0、0。
這個程式的想法是,假設我們要把放在BL暫存器的內容,每次先旋轉到Carry旗標,那Carry旗標就會依序是0或1。此時立刻把Carry用ADC指令加上30h,並把結果加到DL暫存器裡面,就可以把「數字0或1」變成「ASCII文字的0或1」。此時呼叫INT 21h的第2號呼叫,就可以把DL的內容印出。
只要重複上述方法8次,就可以把BL裡面的數字內容,用2進位印出。
程式碼有幾個地方要說明。
程式的第3行,「MOV, BL,0B4h」,就只是把BL放一個數值,準備要印出來的。你可以自己改變這裡的數值,觀察程式印出來的2進位對不對(16進位的B4 =2進位的10110100)。更簡單的做法是,你就直接把程式碼寫成「MOV BL,10110100b」,這樣更直接(最後面的b表示這是2進位數字,通知組譯器辨認用的)。同樣的,你可以多改幾次MOV到BL裡面的數值
第6行的「MOV CX, 0008」,就只是把CX的數值填入8,因為我們要印出8個字元,所以要旋轉8次。而第7行的「Print_Binary」只是一個文字標籤,這標籤和第12行的「LOOP Print_Binary」中間夾的4行程式碼,就是用來印出2進位數字的。每LOOP一次,CX值就會少1。所以我們把CX填入8,LOOP就會循環執行區塊中的程式碼8次。
本期結語:學透指令集,你就熟這處理器了
8086有各種指令碼,用的都是所謂的「助憶格式」。比方說,RCL就是Rotate Carry Left(透過Carry旗標,向左旋轉1個bit)的意思。所以,我們也就知道,應該也有RCR這樣的指令,也應該有不經過Carry的旋轉指令(ROR和ROL就是了)。在emu8086程式裡面,有一個8086全部指令的參考列表,各位可以研究研究。
如果,各位有興趣,可以動手改改看,「要怎麼改,可以讓這個程式印出BX裡面16個位元的2進位數值呢?」這就當作有興趣讀者的習題吧!下期,將把處理器的原理做個總結介紹。
熱門新聞
2024-11-12
2024-11-10
2024-11-13
2024-11-10
2024-11-14
2024-11-11