現(xiàn)今,完全用匯編語言寫的獨(dú)立的程序是不經(jīng)常的。匯編一般用在某些至關(guān)重要的程序。為什么?用高級(jí)語言來編程比用匯編要簡單得多。同樣,使用匯編將使得程序移植到另一個(gè)平臺(tái)非常困難。事實(shí)上,根本很少使用匯編程序。
那么,為什么任何人都需要學(xué)習(xí)匯編程序呢?
1. 有時(shí),用編程寫的代碼比起編譯器產(chǎn)生的代碼要少而且運(yùn)行得更快。
2. 匯編允許直接訪問系統(tǒng)硬件信息,而這個(gè)在高級(jí)語言中很難或根本不可能實(shí)現(xiàn)。
3. 學(xué)習(xí)匯編編程將幫助一個(gè)人深刻地理解系統(tǒng)如何運(yùn)行。
4. 學(xué)習(xí)匯編編程將幫助一個(gè)人更好地理解編譯器和高級(jí)語言像C如何工作。
這兩點(diǎn)表明學(xué)習(xí)匯編是非常有用的,即使在以后的日子里不在編程里用到它。事實(shí)上,作者很少用匯編編程,但是他每天都使用來自它的想法。
第一個(gè)程序
在這一節(jié)里的初期的程序?qū)⑷繌膱D 1.6 里的簡單 C 驅(qū)動(dòng)程序開始。它簡單地調(diào)用另一個(gè)稱為 asm main 的函數(shù)。這個(gè)是真正意義將用匯編編寫的程序。使用C驅(qū)動(dòng)程序有幾個(gè)優(yōu)點(diǎn)。首先,這樣使C系統(tǒng)正確配置程序在保護(hù)模式下運(yùn)行。所有的段和它們相關(guān)的段寄存器將由 C 初始化。匯編代碼不需
要為這個(gè)擔(dān)心。其次,C 庫同樣提供給匯編代碼使用。作者的 I/O 程序利用了這個(gè)優(yōu)點(diǎn)。他們使用了 C 的 I/O 函數(shù)( printf,等)。下面顯示了一個(gè)簡單的匯編程序。
___________________________________________________________first.asm___________________________________________________________
1 ; 文件: first.asm
2 ; 第一個(gè)匯編程序。這個(gè)程序總共需要兩個(gè)整形變量作為輸入然后輸出它們的和。
3 ;
4 ;
5 ; 利用djgpp 創(chuàng)建執(zhí)行文件:
6 ; nasm -f coff first.asm
7 ; gcc -o first first.o driver.c asm_io.o
8
9 %include "asm_io.inc"
10 ;
11 ; 初始化放入到數(shù)據(jù)段里的數(shù)據(jù)
12 ;
13 segment .data
14 ;
15 ; 這些變量指向用來輸出的字符串
16 ;
17 prompt1 db "Enter a number: ", 0 ; 不要忘記空結(jié)束符
18 prompt2 db "Enter another number: ", 0
19 outmsg1 db "You entered ", 0
20 outmsg2 db " and ", 0
21 outmsg3 db ", the sum of these is ", 0
22
23 ;
24 ; 初始化放入到.bss段里的數(shù)據(jù)
25 ;
26 segment .bss
27 ;
28 ; 這個(gè)變量指向用來儲(chǔ)存輸入的雙字
29 ;
30 input1 resd 1
31 input2 resd 1
32
33 ;
34 ; 代碼放入到.text段
35 ;
36 segment .text
37 global _asm_main
38 _asm_main:
39 enter 0,0 ; 開始運(yùn)行
40 pusha
41
42 mov eax, prompt1 ; 輸出提示
43 call print_string
44
45 call read_int ; 讀整形變量儲(chǔ)存到input1
46 mov [input1], eax ;
47
48 mov eax, prompt2 ; 輸出提示
49 call print_string
50
51 call read_int ; 讀整形變量儲(chǔ)存到input2
52 mov [input2], eax ;
53
54 mov eax, [input1] ; eax = 在input1里的雙字
55 add eax, [input2] ; eax = eax + 在input2里的雙字
56 mov ebx, eax ; ebx = eax
57
58 dump_regs 1 ; 輸出寄存器值
59 dump_mem 2, outmsg1, 1 ; 輸出內(nèi)存
60 ;
61 ; 下面分幾步輸出結(jié)果信息
62 ;
63 mov eax, outmsg1
64 call print_string ; 輸出第一條信息
65 mov eax, [input1]
66 call print_int ; 輸出input1
67 mov eax, outmsg2
68 call print_string ; 輸出第二條信息
69 mov eax, [input2]
70 call print_int ; 輸出input2
71 mov eax, outmsg3
72 call print_string ; 輸出第三條信息
73 mov eax, ebx
74 call print_int ; 輸出總數(shù)(ebx)
75 call print_nl ; 換行
76
77 popa
78 mov eax, 0 ; 回到C中
79 leave
80 ret
___________________________________________________________first.asm___________________________________________________________
這個(gè)程序的第13行定義了指定儲(chǔ)存數(shù)據(jù)的內(nèi)存段的部分代碼(名稱為 .data )。只有是初始化的數(shù)據(jù)才需定義在這個(gè)段中。行 17 到 20,聲明了幾個(gè)字符串。它們將通過 C 庫輸出,所以必須以 null 字符(ASCII 值為 0)結(jié)束。記住 0 和 '0' 有很大的區(qū)別。
不初始化的數(shù)據(jù)需聲明在 bss 段(名為 .bss,在 26 行)。這個(gè)段的名字來自于早期的基于 UNIX 匯編運(yùn)算符,意思是\由符號(hào)開始的塊。"這同樣會(huì)有一個(gè)堆棧段。它將在以后討論。
代碼段根據(jù)慣例被命名為.text。它是放置指令的地方。注意主程序(38行)的代碼標(biāo)號(hào)有一個(gè)下劃線前綴。這個(gè)是在 C 中稱為約定的一部分。這個(gè)約定指定了編譯代碼時(shí) C 使用的規(guī)則。C 和匯編交互使用時(shí),知道這個(gè)約定是非常重要的。以后將會(huì)將全部約定呈現(xiàn);但是,現(xiàn)在你只需要知道所有的 C 編譯器里的 C 符號(hào)(也就是: 函數(shù)和全局變量)有一個(gè)附加的下劃線前綴。(這個(gè)規(guī)定是為 DOS/Windows 指定的,在 linux 下C 編譯器并不為 C 符號(hào)名上加任何東西。)
在 37 行的全局變量(global)指示符告訴匯編定義 asm main 為全局變量。與 C 不同的是,變量在缺省情況下只能使用在內(nèi)部范圍中。這就意味著只有在同一模塊的代碼才能使用這個(gè)變量。global 指示符使指定的變量可以使用在外部范圍中。這種類型的變量可以被程序里的任意模塊訪問。asm io 模塊聲明了全局變量 print int 和 et.al.。這就是為什么在 first.asm 模塊里能使用它們的緣故。
編譯器依賴
上面的匯編代碼指定為基于 GNU (GNU 是一個(gè)以免費(fèi)軟件為基礎(chǔ)的計(jì)劃 http://www.fsf.org )的 DJGPP C/C++ 編譯器 ( http://www.delorie.com/djgpp )。 這個(gè)編譯器可以從 Internet 上免費(fèi)下載。它要求一個(gè)基于386或更好的PC而且需在DOS,Windows 95/98 或NT下運(yùn)行。這個(gè)編譯器使用COFF (CommonObject File Format,普通目標(biāo)文件格式)格式的目標(biāo)文件。為了符合這個(gè)格
式,nasm 命令使用 -f coff 選項(xiàng)(就像上面代碼注釋展示的一樣)。最終目標(biāo)文件的擴(kuò)展名為 o。
Linux C 編譯器同樣是一個(gè) GNU 編譯器。為了轉(zhuǎn)變上面的代碼使它能在 Linux 下運(yùn)行,只需簡單將37到38行里的下劃線前綴移除。Linux使用ELF(Executable and Linkable Format,可執(zhí)行和可連接格式)格式的目標(biāo)文件。Linux 下使用 -f elf 選項(xiàng)。它同樣產(chǎn)生一個(gè)擴(kuò)展名為 o 的目標(biāo)文
件。
Borland C/C++ 是另一個(gè)流行的編譯器。它使用微軟 OMF 格式的目標(biāo)文件。Borland 編譯器使用 -f obj 選項(xiàng)。目標(biāo)文件的擴(kuò)展名將會(huì)是 obj。OMF 與其它目標(biāo)文件格式相比使用了不同的段指示符。數(shù)據(jù)段(13行)必須改成:
segment DATA public align=4 class=DATA use32
bss 段(26)必須改成:
segment BSS public align=4 class=BSS use32
text 段(36)必須改成:
segment TEXT public align=1 class=CODE use32
必須在 36 行之前加上一新行:
group DGROUP BSS DATA
微軟 C/C++ 編譯器可以使用 OMF 或 Win32 格式的目標(biāo)文件。(如果給出的是 OMF 格式,它將從內(nèi)部把信息轉(zhuǎn)變成 Win32 格式。)Win32 允許像 DJGPP 和 Linux 一樣來定義段。在這個(gè)模式下使用 -f win32 選項(xiàng)來輸出。目標(biāo)文件的擴(kuò)展名將會(huì)是 obj。
匯編代碼
第一步是匯編代碼。在命令行,鍵入:
nasm -f object-format first.asm
object-format要么是coff ,elf ,obj,要么是win32,它由使用的C編譯器決定。(記住在Linux和Borland下,資源文件同樣必須改變。)
編譯C代碼
使用C編譯器編譯 driver.c 文件。對(duì)于 DJGPP ,使用:
gcc -c driver.c
-c 選項(xiàng)意味著編譯,而不是試圖現(xiàn)在連接。同樣的選項(xiàng)能使用在 Linux,Borland 和 Microsoft 編譯器上。
連接目標(biāo)文件
連接是一個(gè)將在目標(biāo)文件和庫文件里的機(jī)器代碼和數(shù)據(jù)結(jié)合到一起產(chǎn)生一個(gè)可執(zhí)行文件的過程。就像下面將展示的,這個(gè)過程是非常復(fù)雜的。C 代碼要求運(yùn)行標(biāo)準(zhǔn) C 庫和特殊的啟動(dòng)代碼。與直接調(diào)用連接程序相比,C 編譯器更容易調(diào)用帶幾個(gè)正確的參數(shù)的連接程序。
例如:使用 DJGPP 來連接第一個(gè)程序的代碼,使用:
gcc -o first driver.o first.o asm_io.o
這樣產(chǎn)生一個(gè) first.exe(或在 Linux 下只是 first)可執(zhí)行文件。
對(duì)于 Borland,你需要使用:
bcc32 first.obj driver.obj asm_io.obj
Borland 使用列出的第一個(gè)文件名來確定可執(zhí)行文件名。所以在上面的例子里,程序?qū)⒈幻麨?first.exe。
將編譯和連接兩個(gè)步驟結(jié)合起來是可能的。例如:
gcc -o first driver.c first.o asm_io.o
現(xiàn)在 gcc 將編譯 driver.c 然后連接。
理解一個(gè)匯編列表文件
-l listing-file 選項(xiàng)可以用來告訴 nasm 創(chuàng)建一個(gè)指定名字的列表文件。這個(gè)文件將顯示代碼如何被匯編。這兒顯示了 17 和 18 行(在數(shù)據(jù)段)在列表文件中如何顯示。(行號(hào)顯示在列表文件中;但是注意在源代碼文件中顯示的行號(hào)可能不同于在列表文件中顯示的行號(hào)。)
48 00000000 456E7465722061206E- prompt1 db "Enter a number: ", 0
49 00000009 756D6265723A2000
50 00000011 456E74657220616E6F- prompt2 db "Enter another number: ", 0
51 0000001A 74686572206E756D62-
52 00000023 65723A2000
每一行的頭一列是行號(hào),第二列是數(shù)據(jù)在段里的偏移地址(十六進(jìn)制顯示)。第三列顯示將要儲(chǔ)存的十六進(jìn)制值。這種情況下,十六進(jìn)制數(shù)據(jù)符合 ASCII 編碼。最終,顯示來自資源文件的正文。列在第二行的偏移地址非??赡懿皇菙?shù)據(jù)存放在完成后的程序中的真實(shí)偏移地址。每個(gè)模塊可能在數(shù)據(jù)段(或其它段)定義它自己的變量。在連接這一步時(shí),所有這些數(shù)據(jù)段的變量定義結(jié)合形成一個(gè)數(shù)據(jù)段。最終的偏移由連接程序計(jì)算得到。
這兒有一小部分 text 段代碼(資源文件中 54 到 56 行)在列表文件中如何顯示:
94 0000002C A1[00000000] mov eax, [input1]
95 00000031 0305[04000000] add eax, [input2]
96 00000037 89C3 mov ebx, eax
第三列顯示了由匯編程序產(chǎn)生的機(jī)器代碼。通常一個(gè)指令的完整代碼不能完全計(jì)算出來。例如:在 94 行,input1 的偏移(地址)要直到代碼連接后才能知道。匯編程序可以算出 mov 指令(在列表中為 A1)的操作碼,但是它把偏移寫在方括號(hào)中,因?yàn)闇?zhǔn)確的值還不能算出來。這種情況下,0 作為一個(gè)暫時(shí)偏移被使用,因?yàn)?input1 在這個(gè)文件中,被定義在 bss 段的開始。記住這不意味著它會(huì)在程序的最終 bss 段的開始。當(dāng)代碼連接后,連接程序?qū)⒃谖恢蒙喜迦胝_的偏移。其它指令,如 96 行,并不涉及任何變量。這兒匯編程序可以算出完整的機(jī)器代碼。
Big和Little Endian 表示法
如果有人仔細(xì)看過 95 行,將會(huì)發(fā)現(xiàn)機(jī)器代碼中的方括號(hào)里的偏移地址非常奇怪。input2 變量的偏移地址為4(像文件定義的一樣);但是顯示在內(nèi)存里偏移不是00000004,而是04000000。
為什么?不同的處理器在內(nèi)存里以不同的順序儲(chǔ)存多字節(jié)整形:big endian 和 little endian。Big endian 是一種看起來更自然的方法。最大(也就是: 最高有效位)的字節(jié)首先被儲(chǔ)存,然后才是第二大的,依此類推。
例如:雙字 00000004 將被儲(chǔ)存為四個(gè)字節(jié) 00 00 00 04。IBM 主機(jī),許多 RISC 處理器和 Motorola 處理器都使用這種 big endian 方法。然而,基于Intel的處理器使用 little endian 方法!首先被
儲(chǔ)存是最小的有效字節(jié)。所以 00000004 在內(nèi)存中儲(chǔ)存為 04 00 00 00。這種格式強(qiáng)制連入 CPU 而且不可能更改。通常情況下,程序員并不需要擔(dān)心使用的是哪種格式。但是,在下面的情況下,它們是非常重要的。
1. 當(dāng)二進(jìn)制數(shù)據(jù)在不同的電腦上傳輸時(shí)(不管來自文件還是網(wǎng)絡(luò))。
2. 當(dāng)二進(jìn)制數(shù)據(jù)作為一個(gè)多字節(jié)整形寫入到內(nèi)存中然后當(dāng)作單個(gè)單個(gè)字節(jié)讀出,反之亦然。
Endian 格式并不應(yīng)用于數(shù)組的排序。數(shù)組的第一個(gè)元素通常在最低的地址里。這個(gè)應(yīng)用在字符串里(字符數(shù)組)。Endian 格式依然用在數(shù)組的單個(gè)元素中。
骨架文件
圖1.7顯示了一個(gè)可以用來書寫匯編程序的開始部分的骨架文件。
更多建議: