【我所認知的BIOS】->彙編語言之宏彙編1
By LightSeed
2010-2-2
其實早就想寫點關於彙編語言的文章了,但是最近感覺比較累,自己也比較懶今天才動手寫。哎。。。真是身心俱疲,房價漲了,小菜也漲了,妹兒的要求也漲了,但是哥的工資還是不漲。這社會我快混不下去了。但是想想做男人嘛,有什麼大不了的。做人,最重要的是心態要好,要穩。
1、我體會到的C和彙編
目前我還做的legacy的BIOS,裏面全是彙編語言。老實說,最近在study C語言的東西,也在研究用C生成ASM的文件,細細想來C確實很強大,彙編語言的效率確實也高。從C生成的彙編語言來看的話,如果是組織的好,實際的彙編語言的效率應該是比C高的。關於彙編語言的基礎性的東西我就不多贅述了。在這裏我想談談我在仿寫ADU那個debug tool的時候用的一些彙編語言的裏的高級語句。(當然先說清楚,這也是參考了BIOS code的東西,讓我自己寫我想我是寫不出來的啦。),話有說回來,對於程序員來說,如果讓我在C和彙編語言之間選擇,當然還是比較喜歡用C了。(最近我在study ACPI的東西的時候,就試圖用C來寫一個ACPI dump的工具,發覺用C寫真的好方便哦,兩天就能搞出來一個功能了,但是如果用匯編的話,就算知道算法,估計寫起來也比較費神。)
在組織數據段中的數據的時候,按照結構體的形式來存儲有關聯的數據是一種明智的選擇。但是由於這個結構體需要輸入的東西比較多,那麼每次都手動去輸入相關數據的話,估計煩也煩死了。所以宏彙編在這裏就其了很大的作用。
2、宏彙編
(以下這段話是引用自網上的,對於這些定義放在這裏純粹是爲了這篇文章的完整性。)
;----------------------------引用開始----------------------
在程序設計中,爲了簡化程序的設計,將多次重複使用的程序段用宏指令代替。宏指令是指程序員事先定義的特定的“指令”,這種“指令”是一組重複出現的程序指令塊的縮寫和替代。宏指令定義後,凡在宏指令出現的地方,宏彙編程序總是自動地把它們替換成對應的程序指令塊。宏指令有時也稱爲宏,包含宏定義和宏調用。
使用宏指令的好處是:簡化源程序的編寫,傳遞參數靈活,功能更強。
2.1宏指令的定義
宏是源程序中的一段具有獨立功能的程序代碼。它只要在源程序中定義一次,就可以多次調用,調用時只要使用一個宏指令語句就可以了。宏指令定義由開始僞指令MACRO、宏指令體、宏指令定義結束僞指令ENDM組成。格式如下:
宏指令名 MACRO
[形式參數1,形式參數2,…, 形式參數N]
;宏指令體(宏體)
ENDM
其中,宏指令名是宏定義爲宏體程序指令塊規定的名稱,可以是任一合法的名字,也可以是系統保留字(如指令助記符、僞指令操作符等),當宏指令名是某個系統保留字時,則該系統保留字就被賦予新的含義,從而失去原有的意義。MACRO語句到ENDM語句之間的所有彙編語句構成宏指令體,簡稱宏體,宏體中使用的形式參數必須在MACRO語句中列出。
形式參數是出現在宏體內某些位置上可以變化的符號,也可以是任一合法的名字,甚至是寄存器名(筆者:我不建議你這樣使用)。如果形式參數中使用某些寄存器名,那麼在宏彙編展開時,將不認爲這些寄存器名是寄存器本身,而是形式參數,並被實際參數所代替。形式參數可以缺省,也可以有一個或多個。當形式參數多於一個時,形式參數之間用逗號隔開。定義宏指令時,每行的字符數不能多於132個。宏指令定義一般放在源程序的開頭,以避免產生不應發生的錯誤。
宏指令必須先定義後調用(引用)。宏指令可以重新定義,也可以嵌套定義。嵌套定義是指在宏指令體內還可以再定義宏指令或調用另一宏指令。(筆者:其實這和C中的方式也比較相似。擴展一下,彙編語言中的結構體也是這也的。)
2.2宏指令的調用
宏指令一旦定義後,就可以用宏指令名字(宏名)來引用(或調用),這個引用過程即爲宏調用。宏調用的格式爲
宏指令名 實際參數1,實際參數2,…,實際參數N
其中,“實際參數”的類型和順序要與形式參數的類型和順序保持一致,宏調用時將一一對應地替換宏指令體中的形式參數。當有兩個以上參數時,中間用逗號、空格或製表符隔開。宏指令調用時,實際參數的數目並不一定要和形式參數的數目一致。當實參個數多於形參的個數時,忽略多餘的實參;當實參個數少於形參個數時,多餘的形參用空串代替。
;---------------------------------引用結束------------------------
哈哈,我們應該向前輩看齊的嘛,多學習前輩的東西,總是不會錯的。
3、條件彙編
IF/IFE僞指令中的表達式可以採用第3章學習的關係運算符EQ(相等)、NE(不相等)、GT(大於)、LT(小於)、GE(大於等於)、LE(小於等於)。關係表達式用0ffffh(非0)表示真,用0表示假。
條件彙編僞指令
對於他們的定義我就不詳細例舉了,只是在後面的敘述中要用到這些語句。所以先列在這裏,如果需要您可以去查更過的資料。 |
4、實例
上面對文字性的東西定義了太多了,我們來直接看實例吧,這樣比較直觀。(本想在這裏先引用一個簡單的宏彙編的例子的,可是後來又想想算了,如果有人對這篇文章敢興趣的話,我想簡單的宏彙編也應該難不到你。)下面的例子主要是當時在study 宏彙編的時候見到網上的例子,在這裏就直接引用過來,(雖說是從CSDN上的一個老兄blog裏面看到的,但是我感覺是小木偶前輩的例子,所以我一不知道究竟哪位是原作者,您可以看看這裏:
http://blog.csdn.net/benny5609/archive/2008/06/12/2541246.aspx
)
範例:通用的推入堆棧宏
8086 指令的 PUSH 只能把十六位的寄存器或十六位變量推入堆棧,不能把十六位立即值 (常數) 或八位的寄存器推入堆棧,而底下這個宏範例,push_op,也能使立即值或八位的寄存器推入堆棧。底下是 push_op 源代碼:
page ,132 ;01
push_op MACRO arg ;;03
reg16 = 0 ;;05
reg08 = 0 ;;06
addr = 0 ;;07
;;09 檢查輸入參數是否爲 16 位的寄存器
IRP reg,<AX,BX,CX,DX,CS,DS,ES,SS,SI,DI,BP,SP,ax,bx,cx,dx,cs,ds,es,ss,si,di,bp,sp>
IFIDN <reg>,<arg>
push arg ;;12 如果相等的話,推入堆棧
reg16 = 0ffffh ;;13 數定虛擬變量爲真
exitm ;;14 跳出 IRP 塊
ENDIF
ENDM
IF reg16 ;;17 若 reg16 爲真
exitm ;;18 則跳出 push_op 宏
ENDIF
;;21 檢查輸入參數是否爲 16 位的寄存器
IRP reg,<aX,bX,cX,dX,cS,dS,eS,sS,sI,dI,bP,sP,Ax,Bx,Cx,Dx,Cs,Ds,Es,Ss,Si,Di,Bp,Sp>
IFIDN <reg>,<arg>
push arg
reg16 = 0ffffh
exitm
ENDIF
ENDM
IF reg16
exitm
ENDIF
;;33 檢查輸入參數是否爲 8 位的寄存器
IRP reg,<al,bl,cl,dl,ah,bh,ch,dh,AH,BH,CH,DH,AL,BL,CL,DL>
IFIDN <reg>,<arg>
reg08 = 0ffffh
exitm
ENDIF
ENDM
IF reg08
IRPC char,arg ;;41 取得寄存器名的第一個字母
push char&&x ;;42 推入堆棧
exitm ;;43 跳出 IRPC 塊
ENDM
exitm ;;45 跳出 push_op 宏
ENDIF
;;48 檢查輸入參數是否爲 8 位的寄存器
IRP reg,<Al,Bl,Cl,Dl,Ah,Bh,Ch,Dh,aL,bL,cL,dL,aH,bH,cH,dH>
IFIDN <reg>,<arg>
reg08 = 0ffffh
ENDIF
ENDM
IF reg08
IRPC char,arg
push char&&x
exitm
ENDM
exitm
ENDIF
;;62 檢查輸入參數是否爲含有寄存器間接尋址模式,即 [BX]、[SI]……等等
IRPC char,arg
IF ('&char' eq '[')
addr=0ffffh
exitm
ENDIF
ENDM
IF addr
push arg
exitm
ENDIF
arg_size=((type arg) 1)/2 ;;74 輸入參數之長度
arg_type=(.type arg) and 3 ;;75 輸入參數之類型
;;77 檢查輸入參數是否爲變量
IF arg_type eq 2
arg_offset =0
REPT arg_size
arg_address=word ptr arg arg_offset
push arg_address
arg_offset=arg_offset 2
ENDM
exitm
ENDIF
;;88 檢查輸入參數是否爲標號
IF arg_type eq 1
push bp
mov bp,sp
push ax
mov ax,offset arg
xchg ax,[bp]
mov bp,ax
pop ax
exitm
;;98 若不是寄存器、尋址模式、變量、標號的話,應爲立即值
ELSE
push bp
mov bp,sp
push ax
mov ax,arg
xchg ax,[bp]
mov bp,ax
pop ax
ENDIF
ENDM這個宏結構很明顯,先檢查要推入堆棧的參數是否爲 16 位寄存器( 第 9 行到第 31 行 ),如果不是再檢查是否爲 8 位寄存器 ( 第 33 行到第 60 行 ),如果不是寄存器的話,再檢查是否爲寄存器間接尋址 ( 第 62 行到第 72 行 ),如果不是以上這幾種的話,再檢查是否推入變量到堆棧 ( 第 77 行到第 87 行 ),接下來檢查是否推入標號到堆棧 ( 第 88 行到第 97 行 ),假如都不是上述情形的話,就是推入立即值到堆棧了 ( 第 98 行到第 107 行 )。
檢查是否爲寄存器的方法是用不定重複塊 ( IRP ) 來指定要比較的範圍,故引數列 ( 即第 10 行角括號內的引數 ) 包含所有 16 位寄存器名稱,但是因爲參數與引數都被視爲字串,所以大小寫是有差別的,必須在引數列裏包含不同的大小寫排列方式。指定好比較範圍後,再用 IFIDN 比較輸入參數是否爲引數列中的一個,假如是 16 位寄存器的話,則直接把該參數推入堆棧即可,並設定一個虛擬變量,reg16,爲 0ffffh,0ffffh 表示真的意思 ( 第 12、13 行 )。然後再跳出 push_op 堆棧。
小木偶再把 IRP 重複塊的執行方式說明一遍。第 10 行到第 16 行程序碼爲:
IRP reg,<AX,BX,CX,DX,CS,DS,ES,SS,SI,DI,BP,SP,ax,bx,cx,dx,cs,ds,es,ss,si,di,bp,sp>
IFIDN <reg>,<arg>
push arg ;;12 如果相等的話,推入堆棧
reg16 = 0ffffh ;;13 數定虛擬變量爲真
exitm ;;14 跳出 IRP 塊
ENDIF
ENDM
IF reg16 ;;17 若 reg16 爲真
exitm ;;18 則跳出 push_op 宏
ENDIF表示在第 10 行到第 16 行程序碼會重複彙編。第一次時,reg 會以 AX 代入彙編,第 11 行是比較 arg 與 reg 這兩字串是否相等,如果相等則彙編第 12 行到第 14 行之間的程序碼,不相等則結束 IFIDN,然後遇到 ENDM,故重複第二次,使 reg 以 BX 代入彙編……一直到 sp 所有引數結束。
第 14 行,是因爲假如已經找到相符合的寄存器,就沒必要再比較了,這樣可以加快彙編速度。( 雖然也沒快多少。) 第 17 行到第 19 行,也是這樣的道理,既已找到是把寄存器推入堆棧,也就沒必要彙編以下的程序了,故直接跳出 push_op 堆棧。
您可能會問,第 14 行就已有了 exitm,爲何第 18 行還要有個 exitm 呢?這是因爲 RPT、IRP、IRPC 這三個重複塊,類似宏結構,若要在中間停止彙編都可以用 exitm 來跳出宏或塊,所以第 14 行是跳出 IRP 塊,第 17 行是跳出 push_op 宏。
第 33 行到第 61 行,是檢查參數是否爲 8 位寄存器,方法和上述幾乎相同,差別在於 8 位寄存器 ( 例如 ah ) 無法推入堆棧必須改成 16 位寄存器 ( 例如 ax )。所以第 41 行到第 44 行多了個 IRPC 重複塊,此重複塊是爲了取得寄存器的第一個字母,當取得第一個字母就把該字母加上『x』再推入堆棧,然後跳出 IRPC 塊及 push_op 宏。至於該 IRPC 重複塊的運作方式如下:該 IRPC 塊重複次數只有兩次,分別以 8 位寄存器名稱的兩個字母代入彙編,當第一次時即以 8 位寄存器名稱的第一個字母代入,然後加上『x』再推入堆棧,然後立刻跳出 IRPC 塊,故事實上這個重複塊只彙編一次而已。
第 42 行的『&&x』爲何要有兩個『&』呢?這是因爲根據 MASM 手冊上說每層塊要使用『&』,故第二層要用兩個『&』。
第 62 行到第 73 行是用來檢查推入堆棧的參數是否爲寄存器間接尋址,寄存器間接尋址模式是像底下的樣子:
mov ax,[bx]
push [si]
sub ax,[bx 200h]
觀察以上幾個例子,您會發現,這種尋址模式含有兩個中括號,因此檢驗方法就是以 IRPC 檢查參數中是否有『[』( 第 64 行到第 67 行的 IF 條件彙編 ),假如有的話,會使虛擬變量,addr,設爲真。然後接下來的就直接使該參數推入堆棧,因爲 PUSH 指令就可以直接推入寄存器間接尋址模式。
上面這些東西,其實大多是參考前輩的,希望能給正在尋找宏彙編資料的童鞋一點啓發。