Nasm中文手冊 -------------------------------------------------------------------------------- 第一章: 簡介 ----------------------- 1.1 什麼是NASM NASM是一個爲可移植性與模塊化而設計的一個80x86的彙編器。它支持相當多 的目標文件格式,包括Linux和'NetBSD/FreeBSD','a.out','ELF','COFF',微軟16 位的'OBJ'和'Win32'。它還可以輸出純二進制文件。它的語法設計得相當的簡 潔易懂,和Intel語法相似但更簡單。它支持'Pentium','P6','MMX','3DNow!', 'SSE' and 'SSE2'指令集, 1.1.1 爲什麼還需要一個彙編器? NASM當初被設計出來的想法是'comp.lang.asm.x86'(或者可能是'alt.lang.asm' ,我忘了),從本質上講,是因爲沒有一個好的免費的x86系例的彙編器可以使用, 所以,必須有人來寫一個。 (*)'a86'不錯,但不是免費的,而且你不可能得到32位代碼編寫的功能,除非你 付費,它只使用在dos上。 (*) 'gas'是免費的,而且在dos下和unix下都可以使用,但是它是作爲'gcc'的一 個後臺而設計的,並不是很好,'gcc'一直就提供給它絕對正確的代碼,所以它的 錯誤檢測功能相當弱,還有就是對於任何一個想真正利用它寫點東西的人來講, 它的語法簡直太可怕了,並且你無法在裏面寫正確的16位代碼。 (*) 'as86'是專門爲Minix和Linux設計的,但看上去並沒有很多文檔可以參考。 (*) 'MASM'不是很好,並且相當貴,還且只能運行在DOS下。 (*) 'TASM'好一些,但卻極入與MASM保持兼容,這就意味着無數的僞操作碼和繁瑣 的約定,並且它的語法本質上就是MASM的,伴隨着的就是一些自相矛盾和奇怪的 東西。它也是相當貴的,並且只能運行在DOS下。 所以,只有NASM才能使您愉悅得編程。目前,它仍在原型設計階段-我們不期望它 能夠超越所有的這些彙編器。但請您發給我們bug報告,修正意見,和其他有用的 信息,還有其他任何你手頭有的對我們有用的信息(感謝所有已經這樣在做了的 人們),我們還會不斷地改進它。 1.1.2 許可條件 請閱讀作爲NASM發佈的一部分的文件'Licence',只有在該許可條件下你纔可以使 用NASM。 1.2 聯繫信息 當前版本的NASM(0.98.08)由一個開發小組在維護,你可以從'nasm-devel'郵件列表 中得到(看下面的鏈接),如果你想要報告bug,請先閱讀10.2節 NASM有一個主頁:'http://www.web-sites.co.uk/nasm',更多的信息還可以在 `http://nasm.2y.net/'上獲取。 最初的作者你可以通過email:`[email protected]'和`[email protected]'和他們聯 系,但後來的開發小組並不在其中。 最新的NASM發佈被上傳至官方網站`http://www.web-sites.co.uk/nasm'和`ftp.kernel.org', `ibiblio.org' 公告被髮布至`comp.lang.asm.x86', `alt.lang.asm' 和`comp.os.linux.announce' 如果你想了解NASM beta版的發佈,和當前的開發狀態,請通過在 `http://groups.yahoo.com/group/nasm-devel', `http://www.pairlist.net/mailman/listinfo/nasm-devel' and `http://sourceforge.net/projects/nasm' 註冊來捐助'nasm-devel'郵件列表。 在網站Sourceforge上的列表是較好的一個列表,它也是最新nasm源代碼與發佈的 一個網站,另外的列表也是公開的,但有可能不會被繼續長期支持。 1.3 安裝 1.3.1 在dos和Windows下安裝NASM 如果你拿到了NASM的DOS安裝包,'nasmXXX.zip'(這裏.'XXX'表示該安裝包的NASM版 本號),把它解壓到它自己的目錄下(比如:‘c:/nasm') 該包中會包含有四個可執行文件:NASM可擬行文件'nasm.exe'和'nasmw.exe',還有 NDISASM可執行文件'ndisasm.exe'和'ndisasmw.exe'。文件名以'w'結尾的是'Win32' 可執行格式。是運行在'Windows 95'或'Windows NT'的Intel處理器上的,另外的是 16位的'DOS'可執行文件。 NASM運行時需要的唯一文件就是它自己的可執行文件,所以可以拷貝'nasm.exe' 和'nasmw.exe'的其中一個到你自己的路徑下,或者可以編寫一個'autoexec.bat'把 nasm的路徑加到你的'PATH'環境變量中去。(如果你只安裝了Win32版本的,你可能 希望把文件名改成'nasm.exe'。) 就這樣,NASM裝好了。你不需要爲了運行nasm而讓'nasm'目錄一直存在(除非你把它 加到了你的'PATH'中,所以如果你需要節省空間,你可刪掉它,但是,你可能需要保留 文檔或測試程序。 如果你下載了DOS版的源碼包,'nasmXXXs.zip',那'nasm'目錄還會包含完整的NASM源 代碼,你可以選擇一個Makefiles來重新構造你的NASM版本。 注意源文件`insnsa.c', `insnsd.c', `insnsi.h'和`insnsn.c'是由'standard.mac'中 的指令自動生成的,儘管NASM0.98發佈版中包含了這些產生的文件,你如果改動了 insns.dat,standard.mac或者文件,可能需要重新構造他們,在將來的源碼發佈中有 可能將不再包含這些文件,多平臺兼容的Perl可以從www.cpan.org上得到。 1.3.2 在unix下安裝NASM 如果你得到了Unix下的NASM源碼包'nasm-x.xx.tar.gz'(這裏x.xx表示該源碼包中的 nasm的版本號),把它解壓壓到一個目錄,比如'/usr/local/src'。包被解壓後會創建 自己的子目錄'nasm-x.xx' NASM是一個自動配置的安裝包:一旦你解壓了它,'cd'到它的目錄下,輸入'./configuer', 該腳本會找到最好的C編譯器來構造NASM,並據此建立Makefiles。 一旦NASM被自動配置好後,你可以輸入'make'來構造'nasm'和'ndisasm'二進制文件, 然後輸入'make install'把它們安裝到'/usr/local/bin',並把man頁安裝到 '/usr/local/man/man1'下的'nasm.1和'ndisasm.1'或者你可以給配置腳本一個 '--prefix'選項來指定安裝目錄,或者也可以自己來安裝。 NASM還附帶一套處理'RDOFF'目標文件格式的實用程序,它們在'rdoff'子目錄下, 你可以用'make rdf'來構造它們,並使用'make rdf_install'來安裝。如果你需 要的話。 如果NASM在自動配置的時候失敗了,你還是可以使用文件'Makefile.unx'來編譯它們, 把這個文件改名爲'Makefile',然後輸入'make'。在'rdoff'子目錄下同樣有一個 Makefile.unx文件。 第二章 運行NASM ----------------------- 2.1 NASM命令行語法 要彙編一個文件,你可以以下面的格式執行一個命令: nasm -f <format> <filename> [-o <output>] 比如, nasm -f elf myfile.asm 會把文件'myfile.asm'彙編成'ELF'格式 的文件'myfile.o'.還有: nasm -f bin myfile.asm -o myfile.com 會把文件'myfile.asm'彙編成純二進制格式的文件'myfile.com'。 想要以十六進制代碼的形式產生列表文件輸出,並讓代碼顯示在源代碼的左側, 使用'-l'選項並給出列表文件名,比如: nasm -f coff myfile.asm -l myfile.lst 想要獲取更多的關於NASM的使用信息,請輸入: nasm -h 它同時還會輸出可以使用的輸出文件格式, 如果你使用Linux並且不清楚你的系統是'a.out'還是'ELF',請輸入: file nasm (在nasm二進制文件的安裝目錄下使用),如果系統輸出類似下面的信息: nasm: ELF 32-bit LSB executable i386 (386 and up) Version 1 那麼你的系統就是'ELF'格式的,然後你就應該在產生Linux目標文件時使用選 項'-f elf',如果系統輸入類似下面的信息: nasm: Linux/i386 demand-paged executable (QMAGIC) 或者與此相似的,你的系統是'a.out'的,那你應該使用'-f aout'(Linux的'a.out' 系統很久以前就過時了,現在已非常少見。) 就像其他的Unix編譯器與彙編器,NASM在碰到錯誤以前是不輸出任何信息的,所 以除了出錯信息你看不到任何其他信息。 2.1.1 '-o'選項:指定輸出文件的文件名。 NASM會爲你的輸出文件選擇一個文件名;具體如何做取決於目標文件的格式,對 於微軟的目標文件格式('obj'和'win32'),它會去掉你的源文件名的'.asm'擴展 名(或者其他任何你喜歡使用的擴展名,NASM並不關心具體是什麼),並替換上 'obj'。對於Unix的目標文件格式('aout','coff','elf'和'as86')它會替換成 '.o', 對於'rdf',它會使用'.rdf',還有爲'bin'格式,它會簡單地去掉擴展名,所以 'myfile.asm'會產生的一個輸出文件'myfile'。 如果輸出文件已經存在,NASM會覆蓋它,除非它的文件名與輸入文件同名,在這種 情況下,它會給出一個警告信息,並使用'nasm.out'作爲輸出文件的文件名。 在某些情況下,上述行爲是不能接受的,所以,NASM提供了'-o'選項,它能讓你指定 你的輸出文件的文件名,你使用'-o'後面緊跟你爲輸出文件取的名字,中間可以加 空格也可以不加。比如: nasm -f bin program.asm -o program.com nasm -f bin driver.asm -odriver.sys 請注意這是一個小寫的o,跟大寫字母O是不同的,大寫的是用來指定需要傳遞的選 項的數目,請參閱2.1.15 2.1.2 `-f'選項:指定輸出文件的格式。 如果你沒有對NASM使用'-f'選項,它會自己爲你選擇一個輸出文件格式。在發佈的 NASM版本中,缺省的輸出格式總是'bin';如果你自己編譯你的NASM,你可以在編譯的 時候重定義'OF_DEFAULT'來選擇你需要的缺省格式。 就象'-o','-f'與輸出文件格式之間的空格也是可選的,所以'-f elf'和'-felf'都是 合法的。 所有可使用的輸出文件格式的列表可以通過運行命令'nasm -hf'得到。 2.1.3 `-l' 選項: 產生列表文件 如果你對NASM使用了'-l'選項,後面跟一個文件名,NASM會爲你產生一個源文件的列表 文件,在裏面,地址和產生的代碼列在左邊,實際的源代碼(包括宏擴展,除了那些指定 不需要在列表中擴展的宏,參閱4.3.9)列在右邊,比如: nasm -f elf myfile.asm -l myfile.lst 2.1.4 `-M'選項: 產生Makefile依賴關係. 該選項可以用來向標準輸出產生makefile依賴關係,可以把這些信息重定向到一個文件 中以待進一步處理,比如: NASM -M myfile.asm > myfile.dep 2.1.5 `-F'選項: 選擇一個調試格式 該選項可以用來爲輸出文件選擇一個調試格式,語法跟-f選項相冊,唯一不同的是它產 生的輸出文件是調試格式的。 一個具體文件格式的完整的可使用調試文件格式的列表可通過命令'nasm -f <format> -y' 來得到。 這個選項在缺省狀態下沒有被構建時NASM。如何使用該選項的信息請參閱6.10 2.1.6 `-g' 選項:使調試信息有效。 該選項可用來在指定格式的輸出文件中產生調試信息。 更多的信息請參閱2.1.5 2.1.7 `-E' 選項: 把錯誤信息輸入到文件。 在'MS-DOS'下,儘管有辦法,但要把程序的標準錯誤輸出重定向到一個文件還是非常困 難的。因爲NASM常把它的警告和錯誤信息輸出到標準錯誤設備,這將導致你在文本編 輯器裏面很難捕捉到它們。 因此NASM提供了一個'-E'選項,帶有一個文件名參數,它可以把錯誤信息輸出到指定的 文件而不是標準錯誤設備。所以你可以輸入下面這樣的命令來把錯誤重定向到文件: nasm -E myfile.err -f obj myfile.asm 2.1.8 `-s' 選項: 把錯誤信息輸出到'stdout' '-s'選項可以把錯誤信息重定向到'stdout'而不是'stderr',它可以在'MS-DOS'下進行 重定向。想要在彙編文件'myfile.asm'時把它的輸出用管道輸出給'more'程序,可以這樣: nasm -s -f obj myfile.asm | more 請參考2.1.7的'-E'選項. 2.1.9 `-i'選項: 包含文件搜索路徑 當NASM在源文件中看到'%include'操作符時(參閱4.6),它不僅僅會在當前目錄下搜索給 出的文件,還會搜索'-i'選項在命令行中指定的所有路徑。所以你可以從宏定義庫中 包含進一個文件,比如,輸入: nasm -ic:/macrolib/ -f obj myfile.asm (通常,在 '-i'與路徑名之間的空格是允許的,並且可選的。) NASM更多的關注源代碼級上的完全可移植性,所以並不理解正運行的操作系統對文件的 命名習慣;你提供給'-i'作爲參數的的字符串會被一字不差地加在包含文件的文件名前。 所以,上例中最後面的一個反斜槓是必要的,在Unix下,一個尾部的正斜線也同樣是必要的。 (當然,如果你確實需要,你也可以不正規地使用它,比如,選項'-ifoo'會導致 '%incldue "bar.i'去搜索文件'foobar.i'...) 如果你希望定義一個標準的搜索路徑,比如像Unix系統下的'/usr/include',你可以在環境 變量NASMENV中放置一個或多個'-i'(參閱2.1.19) 爲了與絕大多數C編譯器的Makefile保持兼容,該選項也可以被寫成'-I'。 2.1.10 `-p' 選項: 預包含一個文件 NASM允許你通過'-p'選項來指定一個文件預包含進你的源文件。所以,如果運行: nasm myfile.asm -p myinc.inc 跟在源文件開頭寫上'%include "myinc.inc"然後運行'nasm myfile.asm'是等效的。 爲和'-I','-D','-U'選項操持一致性,該選項也可以被寫成'-P' 2.1.11 `-d'選項: 預定義一個宏。 就像'-p'選項給出了在文件頭放置'%include'的另一種實現,'-d'選項給出了在文 件中寫'%define'的另一種實現,你可以寫: nasm myfile.asm -dFOO=100 作爲在文件中寫下面一行語句的一種替代實現: %define FOO 100 在文件的開始,你可以取消一個宏定義,同樣,選項'-dFOO'等同於代碼'%define FOO'。 這種形式的操作符在選擇編譯時操作中非常有用,它們可以用'%ifdef'來進行測試, 比如'-dDEBUG'。 爲了與絕大多數C編譯器的Makefile保持兼容,該選項也可以被寫成'-D'。 2.1.12 `-u' 選項: 取消一個宏定義。 '-u'選項可以用來取消一個由'-p'或'-d'選項先前在命令行上定義的一個宏定義。 比如,下面的命令語句: nasm myfile.asm -dFOO=100 -uFOO 會導致'FOO'不是一個在程序中預定義的宏。這在Makefile中不同位置重載一個操 作時很有用。 爲了與絕大多數C編譯器的Makefile保持兼容,該選項也可以被寫成'-U'。 2.1.13 `-e'選項: 僅預處理。 NASM允許預處理器獨立運行。使用'-e'選項(不需要參數)會導致NASM預處理輸入 文件,展開所有的宏,去掉所有的註釋和預處理操作符,然後把結果文件打印在標 準輸出上(如果'-o'選項也被指定的話,會被存入一個文件)。 該選項不能被用在那些需要預處理器去計算與符號相關的表達式的程序中,所以 如下面的代碼: %assign tablesize ($-tablestart) 會在僅預處理模式中會出錯。 2.1.14 `-a' 選項: 不需要預處理。 如果NASM被用作編譯器的後臺,那麼假設編譯器已經作完了預處理,並禁止NASM的預 處理功能顯然是可以節約時間,加快編譯速度。'-a'選項(不需要參數),會讓NASM把 它強大的預處理器換成另一個什麼也不做的預處理器。 2.1.15 `-On'選項: 指定多遍優化。 NASM在缺省狀態下是一個兩遍的彙編器。這意味着如果你有一個複雜的源文件需要 多於兩遍的彙編。你必須告訴它。 使用'-O'選項,你可以告訴NASM執行多遍彙編。語法如下: (*)'-O0'嚴格執行兩遍優化,JMP和Jcc的處理和0.98版類似,除了向後跳的JMP是短跳 轉,如果可能,立即數在它們的短格式沒有被指定的情況下使用長格式。 (*)'-O1'嚴格執行兩遍優化,但前向分支被彙編成保證能夠到達的代碼;可能產生比 '-O0'更大的代碼,但在分支中的偏移地址沒有指定的情況下彙編成功的機率更大, (*)'-On' 多編優化,最小化分支的偏移,最小化帶符號的立即數,當'strict'關鍵字 沒有用的時候重載指定的大小(參閱3.7),如果2<=n<=3,會有5*n遍,而不是n遍。 注意這是一個大寫的O,和小寫的o是不同的,小寫的o是指定輸出文件的格式,可參閱 2.1.1 2.1.16 `-t'選項: 使用TASM兼容模式。 NASM有一個與Borlands的TASM之間的受限的兼容格式。如果使用了NASM的'-t'選項, 就會產生下列變化: (*)本地符號的前綴由'.'改爲'@@' (*)TASM風格的以'@'開頭的應答文件可以由命令行指定。這和NASM支持的'-@resp' 風格是不同的。 (*)擴號中的尺寸替換被支持。在TASM兼容模式中,方括號中的尺寸替換改變了操作 數的尺寸大小,方括號不再支持NASM語法的操作數地址。比如,'mov eax,[DWORD VAL]' 在TASM兼容語法中是合法的。但注意你失去了爲指令替換缺省地址類型的能力。 (*)'%arg'預處理操作符被支持,它同TASM的ARG操作符相似。 (*) `%local'預處理操作符。 (*) `%stacksize'預處理操作符。 (*) 某些操作符的無前綴形式被支持。 (`arg', `elif',`else', `endif', `if', `ifdef', `ifdifi', `ifndef', `include',`local') (*) 還有很多... 需要更多的關於操作符的信息,請參閱4.9的TASM兼容預處理操作符指令。 2.1.17 `-w'選項: 使彙編警告信息有效或無效。 NASM可以在彙編過程中監視很多的情況,其中很多是值得反饋給用戶的,但這些情況 還不足以構成嚴重錯誤以使NASM停止產生輸出文件。這些情況被以類似錯誤的形式 報告給用戶,但在報告信息的前面加上'warning'字樣。警告信息不會阻止NASM產生 輸出文件並向操作系統返回成功信息。 有些情況甚至還要寬鬆:他們僅僅是一些值得提供給用戶的信息。所以,NASM支持'-w' 命令行選項。它以使特定類型的彙編警告信息輸出有效或無效。這樣的警告類型是 以命名來描述的,比如,'orphan-labels',你可以以下列的命令行選項讓此類警告信息 得以輸出:'-w+orphan-labels',或者以'-w-orphan-labels'讓此類信息不能輸出。 可禁止的警告信息類型有下列一些: (*)`macro-params'包括以錯誤的參數個數調用多行的宏定義的警告。這類警告信息 缺省情況下是輸出的,至於爲什麼你可能需要禁止它,請參閱4.3.1。 (*)`orphan-labels'包含源文件行中沒有指令卻定義了一個沒有結尾分號的label的 警告。缺省狀況下,NASM不輸出此類警告。如果你需要它,請參閱3.1的例子。 (*) 'number-overflow'包含那些數值常數不符合32位格式警告信息(比如,你很容易打 了很多的F,錯誤產生了'0x7fffffffffff')。這種警告信息缺省狀況下是打開的。 2.1.18 `-v'選項: 打印版本信息。 輸入'NASM -v'會顯示你正使用的NASM的版本號,還有它被編譯的時間。 如果你要提交bug報告,你可能需要版本號。 2.1.19 `NASMENV'環境變量。 如果你定義了一個叫'NASMENV'的環境變量,程序會被把它認作是命令行選項附加的一 部分,它會在真正的命令行之前被處理。你可以通過在'NASMENV'中使用'-i'選項來定 義包含文件的標準搜索路徑。 環境變量的值是通過空格符分隔的,所以值'-s ic:/nasmlib'會被看作兩個單獨的操 作。也正因爲如此,意味着值'-dNAME='my name'不會象你預期的那樣被處理, 因爲它 會在空格符處被分開,NASM的命令行處理會被兩個沒有意義的字符串'-dNAME="my'和 'name"'給弄混。 爲了解決這個問題,NASM爲此提供了一個特性,如果你在'NASMENV'環境變量的第一個 字符處寫上一個非減號字符,NASM就會把這個字符當作是選項的分隔符。所以把環 境變量設成'!-s!-ic:/nasmlib'跟'-s -ic:/nasmlib'沒什麼兩樣,但是 '!-dNAME="my name"就會正常工作了。 這個環境變量以前叫做'NASM',從版本0.98.32以後開始叫這個名字。 2.2 MASM用戶速成。 如果你曾使用MASM寫程序,或者使用在MASM兼容模式下使用TASM, 或者使用'a86', 本節將闡述MASM與NASM語法之間的主要區別。如果你沒有使用過MASM,那最好先 跳過這一節。 2.2.1 NASM是大小寫敏感的。 一個簡單的區別是NASM是大小寫敏感的。當你調用你的符號'foo','Foo',或 'FOO'時,它們是不同的。如果你在彙編'DOS'或'OS/2', '.OBJ'文件,你可以使 用'UPPERCASE'操作符來保證所有的導出到其他代碼模式的符號都是大寫的;但 是,在僅僅一個單獨的模塊中,NASM會區分大小寫符事情。 2.2.2 NASM需要方括號來引用內存地址。 NASM的設計思想是語法儘可能簡潔。它的一個設計目標是,它將在被使用的過程 中,儘可能得讓用戶看到一個單行的NASM代碼時,就可以說出它會產生什麼操作 碼。你可以在NASM中這樣做,比如,如果你聲明瞭: foo equ 1 bar dw 2 然後有兩行的代碼: mov ax,foo mov ax,bar 儘管它們有看上去完全相同的語法,但卻產生了完全不同的操作碼 NASM爲了避免這種令人討厭的情況,擁有一個相當簡單的內存引用語未能。規則 是任何對內存中內容的存取操作必須要在地址上加上方括號。但任何對地址值 的操作不需要。所以,形如'mov ax,foo'的指令總是代表一個編譯時常數,不管它 是一個 'EQU'或一個變量的地址;如果要取變量'bar'的內容,你必須與 'mov ax,[bar]'。 這也意味着NASM不需要MASM的'OFFSET'關鍵字,因爲MASM的代碼'mov ax,offset bar' 同NASM的語法'mov ax,bar'是完全等效的。如果你希望讓大量的MASM代碼能夠被 NASM彙編通過,你可以編寫'%idefine offset'讓預處理器把'OFFSET'處理成一個無 操作符。 這個問題在'a86'中就更混亂了。 NASM因爲關注簡潔性,同樣不支持MASM和它的衍生產品支持的的混合語法,比如像 :'mov ax, table[bx]',這裏,一箇中括號外的部分加上括號內的一個部分引用一個 內存地址,上面代碼的正確語法是:'mov ax,[table+bx] 。同樣,'mov ax,es:[di]' 也是錯誤的,正確的應該是'mov ax,[es:di]'。 2.2.3 NASM不存儲變量的類型。 NASM被設計成不記住你聲明的變量的類型。然而,MASM在看到'var dw 0'時會記住 類型,然後就可以隱式地合用'mov var, 2'給變量賦值。NASM不會記住關於變量 'var'的任何東西,除了它的位置,所以你必須顯式地寫上代碼'mov word [var],2'。 因爲這個原因,NASM不支持'LODS','MOVS','STOS','SCANS','CMPS','INS',或'OUTS' 指令,僅僅支持形如'LODSB','MOVSW',和'SCANSD'之灰的指令。它們都顯式地指定 被處理的字符串的尺寸。 2.2.4 NASM不會 `ASSUME' 作爲NASM簡潔性的一部分,它同樣不支持'ASSUME'操作符。NASM不會記住你往段寄 存器裏放了什麼值,也不會自動產生段替換前綴。 2.2.5 NASM不支持內存模型。 NASM同樣不含有任何操作符來支持不同的16位內存模型。程序員需要自己跟蹤那 些函數需要far call,哪些需要near call。並需要確定放置正確的'RET'指令('RETN' 或'RETF'; NASM接受'RET'作爲'RETN'的另一種形式);另外程序員需要在調用外部函 數時在需要的編寫CALL FAR指令,並必須跟蹤哪些外部變量定義是far,哪些是near。 2.2.6 浮點處理上的不同。 NASM使用跟MASM不同的浮點寄存器名:MASM叫它們'ST(0)','ST(1)'等,而'a86'叫它們 '0','1'等,NASM則叫它們'st0','st1'等。 在版本0.96上,NASM現在以跟MASM兼容彙編器同樣的方式處理'nowait'形式的指令, 0.95以及更早的版本上的不同的處理方式主要是因爲作者的誤解。 2.2.7 其他不同。 由於歷史的原因,NASM把MASM兼容彙編器的'TBYTE'寫成'TWORD'。 NASM以跟MASM不同的一種方式聲明未初始化的內存。MASM的程序員必須使用 'stack db 64 dup (?)', NASM需要這樣寫:'stack resb 64',讀作"保留64字節"。爲了 保持可移植性,NASM把'?'看作是符號名稱中的一個有效的字符,所以你可以編寫這樣 的代碼'? equ 0', 然後寫'dw ?'可以做一些有用的事情。'DUP'還是一個不被支持的語法。 另外,宏與操作符的工作方式也與MASM完全不同,可以到參閱第4,第5章。 第三章 NASM語言 ---------------- 3.1 NASM源程序行的組成。 就像很多其他的彙編器,每一行NASM源代碼包含(除非它是一個宏,一個預處理操作 符,或一個彙編器操作符,參況第4,5章)下面四個部分的全部或某幾個部分: label: instruction operands ; comment 通常,這些域的大部分是可選的;label,instruction,comment存在或不存在都是允 許的。當然,operands域會因爲instruction域的要求而必需存或必須不存在。 NASM使用反斜線(/)作爲續行符;如果一個以一個反斜線結束,那第二行會被認爲 是前面一行的一部分。 NASM對於一行中的空格符並沒有嚴格的限制:labels可以在它們的前面有空格,或 其他任何東西。label後面的冒號同樣也是可選的。(注意到,這意味着如果你想 要寫一行'lodsb',但卻錯誤地寫成了'lodab',這仍將是有效的一行,但這一行不做 任何事情,只是定義了一個label。運行NASM時帶上命令行選項'-w+orphan-labels' 會讓NASM在你定義了一個不以冒號結尾的label時警告你。 labels中的有效的字符是字母,數字,'-','$','#','@','~','.'和'?'。但只有字母 '.',(具有特殊含義,參閱3.9),'_'和'?'可以作爲標識符的開頭。一個標識符還可 以加上一個'$'前綴,以表明它被作爲一個標識符而不是保留字來處理。這樣的話, 如果你想到鏈接進來的其他模塊中定義了一個符號叫'eax',你可以用'$eax'在 NASM代碼中引用它,以和寄存器的符號區分開。 instruction域可以包含任何機器指令:Pentium和P6指令,FPU指令,MMX指令還有甚 至沒有公開的指令也會被支持。這些指令可以加上前綴'LOCK','REP','REPE/REPZ' 或'REPNE'/'REPNZ',通常,支持顯示的地址尺寸和操作數尺寸前綴'A16','A32', 'O16'和'O32'。關於使用它們的一個例子在第九章給出。你也可以使用段寄存器 名作爲指令前綴: 代碼'es mov [bx],ax'等效於代碼'mov [es:bx],ax'。我們推薦 後一種語法。因爲它和語法中的其它語法特性一致。但是對於象'LODSB'這樣的 指令,它沒有操作數,但還是可以有一個段前綴, 對於'es lodsb'沒有清晰地語法 處理方式 在使用一個前綴時,指令不是必須的,像'CS','A32','LOCK'或'REPE'這樣的段前綴 可以單獨出現在一行上,NASM僅僅產生一個前綴字節。 作爲對實際機器指令的擴展,NASM同時提供了一定數量的僞操作指令,這在3.2節 詳細描述。 指令操作數可以使用一定的格式:它們可以是寄存器,僅僅以寄存器名來表示(比 如:'ax','bp','ebx','cr0':NASM不使用'gas'的語法風格,在這種風格中,寄存器名 前必須加上一個'%'符號),或者它們可以是有效的地址(參閱3.3),常數(3.4),或 表達式。 對於浮點指令,NASM接受各種語法:你可以使用MASM支持的雙操作數形式,或者你 可以使用NASM的在大多數情況下全用的單操作數形式。支持的所以指令的語法 細節可以參閱附錄B。比如,你可以寫: fadd st1 ; this sets st0 := st0 + st1 fadd st0,st1 ; so does this fadd st1,st0 ; this sets st1 := st1 + st0 fadd to st1 ; so does this 幾乎所有的浮點指令在引用內存時必須使用以下前綴中的一個'DWORD',QWORD' 或'TWORD'來指明它所引用的內存的尺寸。 3.2 僞指令。 僞指令是一些並不是真正的x86機器指令,但還是被用在了instruction域中的指 令,因爲使用它們可以帶來很大的方便。當前的僞指令有'DB','DW','DD','DQ'和 ‘DT’,它們對應的未初始化指令是'RESB','RESW','RESD','RESQ'和'REST','INCBIN' 命令,'EQU'命令和'TIEMS'前綴。 3.2.1 `DB'一類的僞指令: 聲明已初始化的數據。 在NASM中,`DB', `DW', `DD', `DQ'和`DT'經常被用來在輸出文件中聲明已初始化 的數據,你可以多種方式使用它們: db 0x55 ; just the byte 0x55 db 0x55,0x56,0x57 ; three bytes in succession db 'a',0x55 ; character constants are OK db 'hello',13,10,'$' ; so are string constants dw 0x1234 ; 0x34 0x12 dw 'a' ; 0x41 0x00 (it's just a number) dw 'ab' ; 0x41 0x42 (character constant) dw 'abc' ; 0x41 0x42 0x43 0x00 (string) dd 0x12345678 ; 0x78 0x56 0x34 0x12 dd 1.234567e20 ; floating-point constant dq 1.234567e20 ; double-precision float dt 1.234567e20 ; extended-precision float 'DQ'和'DT'不接受數值常數或字符串常數作爲操作數。 3.2.2 `RESB'類的僞指令: 聲明未初始化的數據。 `RESB', `RESW', `RESD', `RESQ' and `REST'被設計用在模塊的BSS段中:它們聲明 未初始化的存儲空間。每一個帶有單個操作數,用來表明字節數,字數,或雙字數 或其他的需要保留單位。就像在2.2.7中所描述的,NASM不支持MASM/TASM的扣留未 初始化空間的語法'DW ?'或類似的東西:現在我們所描述的正是NASM自己的方式。 'RESB'類僞指令的操作數是有嚴格的語法的,參閱3.8。 比如: buffer: resb 64 ; reserve 64 bytes wordvar: resw 1 ; reserve a word realarray resq 10 ; array of ten reals 3.2.3 `INCBIN':包含其他二進制文件。 'INCBIN'是從老的Amiga彙編器DevPac中借過來的:它將一個二進制文件逐字逐句地 包含到輸出文件中。這能很方便地在一個遊戲可執行文件中包含中圖像或聲音數 據。它可以以下三種形式的任何一種使用: incbin "file.dat" ; include the whole file incbin "file.dat",1024 ; skip the first 1024 bytes incbin "file.dat",1024,512 ; skip the first 1024, and ; actually include at most 512 3.2.4 `EQU': 定義常數。 'EQU'定義一個符號,代表一個常量值:當使用'EQU'時,源文件行上必須包含一個label。 'EQU'的行爲就是把給出的label的名字定義成它的操作數(唯一)的值。定義是不可更 改的,比如: message db 'hello, world' msglen equ $-message 把'msglen'定義成了常量12。'msglen'不能再被重定義。這也不是一個預自理定義: 'msglen'的值只被計算一次,計算中使用到了'$'(參閱3.5)在此時的含義。注意 ‘EQU’的操作數也是一個嚴格語法的表達式。(參閱3.8) 3.2.5 `TIMES': 重複指令或數據。 爲了與絕大多數C編譯器的Makefile保持兼容,該選項也可以被寫成'-U'。 2.1.13 `-e'選項: 僅預處理。 NASM允許預處理器獨立運行。使用'-e'選項(不需要參數)會導致NASM預處理輸入 文件,展開所有的宏,去掉所有的註釋和預處理操作符,然後把結果文件打印在標 準輸出上(如果'-o'選項也被指定的話,會被存入一個文件)。 該選項不能被用在那些需要預處理器去計算與符號相關的表達式的程序中,所以 如下面的代碼: %assign tablesize ($-tablestart) 會在僅預處理模式中會出錯。 2.1.14 `-a' 選項: 不需要預處理。 如果NASM被用作編譯器的後臺,那麼假設編譯器已經作完了預處理,並禁止NASM的預 處理功能顯然是可以節約時間,加快編譯速度。'-a'選項(不需要參數),會讓NASM把 它強大的預處理器換成另一個什麼也不做的預處理器。 2.1.15 `-On'選項: 指定多遍優化。 NASM在缺省狀態下是一個兩遍的彙編器。這意味着如果你有一個複雜的源文件需要 多於兩遍的彙編。你必須告訴它。 使用'-O'選項,你可以告訴NASM執行多遍彙編。語法如下: (*)'-O0'嚴格執行兩遍優化,JMP和Jcc的處理和0.98版類似,除了向後跳的JMP是短跳 轉,如果可能,立即數在它們的短格式沒有被指定的情況下使用長格式。 (*)'-O1'嚴格執行兩遍優化,但前向分支被彙編成保證能夠到達的代碼;可能產生比 '-O0'更大的代碼,但在分支中的偏移地址沒有指定的情況下彙編成功的機率更大, (*)'-On' 多編優化,最小化分支的偏移,最小化帶符號的立即數,當'strict'關鍵字 沒有用的時候重載指定的大小(參閱3.7),如果2<=n<=3,會有5*n遍,而不是n遍。 注意這是一個大寫的O,和小寫的o是不同的,小寫的o是指定輸出文件的格式,可參閱 2.1.1 2.1.16 `-t'選項: 使用TASM兼容模式。 NASM有一個與Borlands的TASM之間的受限的兼容格式。如果使用了NASM的'-t'選項, 就會產生下列變化: (*)本地符號的前綴由'.'改爲'@@' (*)TASM風格的以'@'開頭的應答文件可以由命令行指定。這和NASM支持的'-@resp' 風格是不同的。 (*)擴號中的尺寸替換被支持。在TASM兼容模式中,方括號中的尺寸替換改變了操作 數的尺寸大小,方括號不再支持NASM語法的操作數地址。比如,'mov eax,[DWORD VAL]' 在TASM兼容語法中是合法的。但注意你失去了爲指令替換缺省地址類型的能力。 (*)'%arg'預處理操作符被支持,它同TASM的ARG操作符相似。 (*) `%local'預處理操作符。 (*) `%stacksize'預處理操作符。 (*) 某些操作符的無前綴形式被支持。 (`arg', `elif',`else', `endif', `if', `ifdef', `ifdifi', `ifndef', `include',`local') (*) 還有很多... 需要更多的關於操作符的信息,請參閱4.9的TASM兼容預處理操作符指令。 2.1.17 `-w'選項: 使彙編警告信息有效或無效。 NASM可以在彙編過程中監視很多的情況,其中很多是值得反饋給用戶的,但這些情況 還不足以構成嚴重錯誤以使NASM停止產生輸出文件。這些情況被以類似錯誤的形式 報告給用戶,但在報告信息的前面加上'warning'字樣。警告信息不會阻止NASM產生 輸出文件並向操作系統返回成功信息。 有些情況甚至還要寬鬆:他們僅僅是一些值得提供給用戶的信息。所以,NASM支持'-w' 命令行選項。它以使特定類型的彙編警告信息輸出有效或無效。這樣的警告類型是 以命名來描述的,比如,'orphan-labels',你可以以下列的命令行選項讓此類警告信息 得以輸出:'-w+orphan-labels',或者以'-w-orphan-labels'讓此類信息不能輸出。 可禁止的警告信息類型有下列一些: (*)`macro-params'包括以錯誤的參數個數調用多行的宏定義的警告。這類警告信息 缺省情況下是輸出的,至於爲什麼你可能需要禁止它,請參閱4.3.1。 (*)`orphan-labels'包含源文件行中沒有指令卻定義了一個沒有結尾分號的label的 警告。缺省狀況下,NASM不輸出此類警告。如果你需要它,請參閱3.1的例子。 (*) 'number-overflow'包含那些數值常數不符合32位格式警告信息(比如,你很容易打 了很多的F,錯誤產生了'0x7fffffffffff')。這種警告信息缺省狀況下是打開的。 2.1.18 `-v'選項: 打印版本信息。 輸入'NASM -v'會顯示你正使用的NASM的版本號,還有它被編譯的時間。 如果你要提交bug報告,你可能需要版本號。 2.1.19 `NASMENV'環境變量。 如果你定義了一個叫'NASMENV'的環境變量,程序會被把它認作是命令行選項附加的一 部分,它會在真正的命令行之前被處理。你可以通過在'NASMENV'中使用'-i'選項來定 義包含文件的標準搜索路徑。 環境變量的值是通過空格符分隔的,所以值'-s ic:/nasmlib'會被看作兩個單獨的操 作。也正因爲如此,意味着值'-dNAME='my name'不會象你預期的那樣被處理, 因爲它 會在空格符處被分開,NASM的命令行處理會被兩個沒有意義的字符串'-dNAME="my'和 'name"'給弄混。 爲了解決這個問題,NASM爲此提供了一個特性,如果你在'NASMENV'環境變量的第一個 字符處寫上一個非減號字符,NASM就會把這個字符當作是選項的分隔符。所以把環 境變量設成'!-s!-ic:/nasmlib'跟'-s -ic:/nasmlib'沒什麼兩樣,但是 '!-dNAME="my name"就會正常工作了。 這個環境變量以前叫做'NASM',從版本0.98.32以後開始叫這個名字。 2.2 MASM用戶速成。 如果你曾使用MASM寫程序,或者使用在MASM兼容模式下使用TASM, 或者使用'a86', 本節將闡述MASM與NASM語法之間的主要區別。如果你沒有使用過MASM,那最好先 跳過這一節。 2.2.1 NASM是大小寫敏感的。 一個簡單的區別是NASM是大小寫敏感的。當你調用你的符號'foo','Foo',或 'FOO'時,它們是不同的。如果你在彙編'DOS'或'OS/2', '.OBJ'文件,你可以使 用'UPPERCASE'操作符來保證所有的導出到其他代碼模式的符號都是大寫的;但 是,在僅僅一個單獨的模塊中,NASM會區分大小寫符事情。 2.2.2 NASM需要方括號來引用內存地址。 NASM的設計思想是語法儘可能簡潔。它的一個設計目標是,它將在被使用的過程 中,儘可能得讓用戶看到一個單行的NASM代碼時,就可以說出它會產生什麼操作 碼。你可以在NASM中這樣做,比如,如果你聲明瞭: foo equ 1 bar dw 2 然後有兩行的代碼: mov ax,foo mov ax,bar 儘管它們有看上去完全相同的語法,但卻產生了完全不同的操作碼 NASM爲了避免這種令人討厭的情況,擁有一個相當簡單的內存引用語未能。規則 是任何對內存中內容的存取操作必須要在地址上加上方括號。但任何對地址值 的操作不需要。所以,形如'mov ax,foo'的指令總是代表一個編譯時常數,不管它 是一個 'EQU'或一個變量的地址;如果要取變量'bar'的內容,你必須與 'mov ax,[bar]'。 這也意味着NASM不需要MASM的'OFFSET'關鍵字,因爲MASM的代碼'mov ax,offset bar' 同NASM的語法'mov ax,bar'是完全等效的。如果你希望讓大量的MASM代碼能夠被 NASM彙編通過,你可以編寫'%idefine offset'讓預處理器把'OFFSET'處理成一個無 操作符。 這個問題在'a86'中就更混亂了。 NASM因爲關注簡潔性,同樣不支持MASM和它的衍生產品支持的的混合語法,比如像 :'mov ax, table[bx]',這裏,一箇中括號外的部分加上括號內的一個部分引用一個 內存地址,上面代碼的正確語法是:'mov ax,[table+bx] 。同樣,'mov ax,es:[di]' 也是錯誤的,正確的應該是'mov ax,[es:di]'。 2.2.3 NASM不存儲變量的類型。 NASM被設計成不記住你聲明的變量的類型。然而,MASM在看到'var dw 0'時會記住 類型,然後就可以隱式地合用'mov var, 2'給變量賦值。NASM不會記住關於變量 'var'的任何東西,除了它的位置,所以你必須顯式地寫上代碼'mov word [var],2'。 因爲這個原因,NASM不支持'LODS','MOVS','STOS','SCANS','CMPS','INS',或'OUTS' 指令,僅僅支持形如'LODSB','MOVSW',和'SCANSD'之灰的指令。它們都顯式地指定 被處理的字符串的尺寸。 2.2.4 NASM不會 `ASSUME' 作爲NASM簡潔性的一部分,它同樣不支持'ASSUME'操作符。NASM不會記住你往段寄 存器裏放了什麼值,也不會自動產生段替換前綴。 2.2.5 NASM不支持內存模型。 NASM同樣不含有任何操作符來支持不同的16位內存模型。程序員需要自己跟蹤那 些函數需要far call,哪些需要near call。並需要確定放置正確的'RET'指令('RETN' 或'RETF'; NASM接受'RET'作爲'RETN'的另一種形式);另外程序員需要在調用外部函 數時在需要的編寫CALL FAR指令,並必須跟蹤哪些外部變量定義是far,哪些是near。 2.2.6 浮點處理上的不同。 NASM使用跟MASM不同的浮點寄存器名:MASM叫它們'ST(0)','ST(1)'等,而'a86'叫它們 '0','1'等,NASM則叫它們'st0','st1'等。 在版本0.96上,NASM現在以跟MASM兼容彙編器同樣的方式處理'nowait'形式的指令, 0.95以及更早的版本上的不同的處理方式主要是因爲作者的誤解。 2.2.7 其他不同。 由於歷史的原因,NASM把MASM兼容彙編器的'TBYTE'寫成'TWORD'。 NASM以跟MASM不同的一種方式聲明未初始化的內存。MASM的程序員必須使用 'stack db 64 dup (?)', NASM需要這樣寫:'stack resb 64',讀作"保留64字節"。爲了 保持可移植性,NASM把'?'看作是符號名稱中的一個有效的字符,所以你可以編寫這樣 的代碼'? equ 0', 然後寫'dw ?'可以做一些有用的事情。'DUP'還是一個不被支持的語法。 另外,宏與操作符的工作方式也與MASM完全不同,可以到參閱第4,第5章。 第三章 NASM語言 ---------------- 3.1 NASM源程序行的組成。 就像很多其他的彙編器,每一行NASM源代碼包含(除非它是一個宏,一個預處理操作 符,或一個彙編器操作符,參況第4,5章)下面四個部分的全部或某幾個部分: label: instruction operands ; comment 通常,這些域的大部分是可選的;label,instruction,comment存在或不存在都是允 許的。當然,operands域會因爲instruction域的要求而必需存或必須不存在。 NASM使用反斜線(/)作爲續行符;如果一個以一個反斜線結束,那第二行會被認爲 是前面一行的一部分。 NASM對於一行中的空格符並沒有嚴格的限制:labels可以在它們的前面有空格,或 其他任何東西。label後面的冒號同樣也是可選的。(注意到,這意味着如果你想 要寫一行'lodsb',但卻錯誤地寫成了'lodab',這仍將是有效的一行,但這一行不做 任何事情,只是定義了一個label。運行NASM時帶上命令行選項'-w+orphan-labels' 會讓NASM在你定義了一個不以冒號結尾的label時警告你。 labels中的有效的字符是字母,數字,'-','$','#','@','~','.'和'?'。但只有字母 '.',(具有特殊含義,參閱3.9),'_'和'?'可以作爲標識符的開頭。一個標識符還可 以加上一個'$'前綴,以表明它被作爲一個標識符而不是保留字來處理。這樣的話, 如果你想到鏈接進來的其他模塊中定義了一個符號叫'eax',你可以用'$eax'在 NASM代碼中引用它,以和寄存器的符號區分開。 instruction域可以包含任何機器指令:Pentium和P6指令,FPU指令,MMX指令還有甚 至沒有公開的指令也會被支持。這些指令可以加上前綴'LOCK','REP','REPE/REPZ' 或'REPNE'/'REPNZ',通常,支持顯示的地址尺寸和操作數尺寸前綴'A16','A32', 'O16'和'O32'。關於使用它們的一個例子在第九章給出。你也可以使用段寄存器 名作爲指令前綴: 代碼'es mov [bx],ax'等效於代碼'mov [es:bx],ax'。我們推薦 後一種語法。因爲它和語法中的其它語法特性一致。但是對於象'LODSB'這樣的 指令,它沒有操作數,但還是可以有一個段前綴, 對於'es lodsb'沒有清晰地語法 處理方式 在使用一個前綴時,指令不是必須的,像'CS','A32','LOCK'或'REPE'這樣的段前綴 可以單獨出現在一行上,NASM僅僅產生一個前綴字節。 作爲對實際機器指令的擴展,NASM同時提供了一定數量的僞操作指令,這在3.2節 詳細描述。 指令操作數可以使用一定的格式:它們可以是寄存器,僅僅以寄存器名來表示(比 如:'ax','bp','ebx','cr0':NASM不使用'gas'的語法風格,在這種風格中,寄存器名 前必須加上一個'%'符號),或者它們可以是有效的地址(參閱3.3),常數(3.4),或 表達式。 對於浮點指令,NASM接受各種語法:你可以使用MASM支持的雙操作數形式,或者你 可以使用NASM的在大多數情況下全用的單操作數形式。支持的所以指令的語法 細節可以參閱附錄B。比如,你可以寫: fadd st1 ; this sets st0 := st0 + st1 fadd st0,st1 ; so does this fadd st1,st0 ; this sets st1 := st1 + st0 fadd to st1 ; so does this 幾乎所有的浮點指令在引用內存時必須使用以下前綴中的一個'DWORD',QWORD' 或'TWORD'來指明它所引用的內存的尺寸。 3.2 僞指令。 僞指令是一些並不是真正的x86機器指令,但還是被用在了instruction域中的指 令,因爲使用它們可以帶來很大的方便。當前的僞指令有'DB','DW','DD','DQ'和 ‘DT’,它們對應的未初始化指令是'RESB','RESW','RESD','RESQ'和'REST','INCBIN' 命令,'EQU'命令和'TIEMS'前綴。 3.2.1 `DB'一類的僞指令: 聲明已初始化的數據。 在NASM中,`DB', `DW', `DD', `DQ'和`DT'經常被用來在輸出文件中聲明已初始化 的數據,你可以多種方式使用它們: db 0x55 ; just the byte 0x55 db 0x55,0x56,0x57 ; three bytes in succession db 'a',0x55 ; character constants are OK db 'hello',13,10,'$' ; so are string constants dw 0x1234 ; 0x34 0x12 dw 'a' ; 0x41 0x00 (it's just a number) dw 'ab' ; 0x41 0x42 (character constant) dw 'abc' ; 0x41 0x42 0x43 0x00 (string) dd 0x12345678 ; 0x78 0x56 0x34 0x12 dd 1.234567e20 ; floating-point constant dq 1.234567e20 ; double-precision float dt 1.234567e20 ; extended-precision float 'DQ'和'DT'不接受數值常數或字符串常數作爲操作數。 3.2.2 `RESB'類的僞指令: 聲明未初始化的數據。 `RESB', `RESW', `RESD', `RESQ' and `REST'被設計用在模塊的BSS段中:它們聲明 未初始化的存儲空間。每一個帶有單個操作數,用來表明字節數,字數,或雙字數 或其他的需要保留單位。就像在2.2.7中所描述的,NASM不支持MASM/TASM的扣留未 初始化空間的語法'DW ?'或類似的東西:現在我們所描述的正是NASM自己的方式。 'RESB'類僞指令的操作數是有嚴格的語法的,參閱3.8。 比如: buffer: resb 64 ; reserve 64 bytes wordvar: resw 1 ; reserve a word realarray resq 10 ; array of ten reals 3.2.3 `INCBIN':包含其他二進制文件。 'INCBIN'是從老的Amiga彙編器DevPac中借過來的:它將一個二進制文件逐字逐句地 包含到輸出文件中。這能很方便地在一個遊戲可執行文件中包含中圖像或聲音數 據。它可以以下三種形式的任何一種使用: incbin "file.dat" ; include the whole file incbin "file.dat",1024 ; skip the first 1024 bytes incbin "file.dat",1024,512 ; skip the first 1024, and ; actually include at most 512 3.2.4 `EQU': 定義常數。 'EQU'定義一個符號,代表一個常量值:當使用'EQU'時,源文件行上必須包含一個label。 'EQU'的行爲就是把給出的label的名字定義成它的操作數(唯一)的值。定義是不可更 改的,比如: message db 'hello, world' msglen equ $-message 把'msglen'定義成了常量12。'msglen'不能再被重定義。這也不是一個預自理定義: 'msglen'的值只被計算一次,計算中使用到了'$'(參閱3.5)在此時的含義。注意 ‘EQU’的操作數也是一個嚴格語法的表達式。(參閱3.8) 3.2.5 `TIMES': 重複指令或數據。 前綴'TIMES'導致指令被彙編多次。它在某種程序上是NASM的與MASM兼容彙編器的 'DUP'語法的等價物。你可以這樣寫: zerobuf: times 64 db 0 或類似的東西,但'TEIMES'的能力遠不止於此。'TIMES'的參數不僅僅是一個數值常 數,還有數值表達式,所以你可以這樣做: buffer: db 'hello, world' times 64-$+buffer db ' ' 它可以把'buffer'的長度精確地定義爲64字節,’TIMES‘可以被用在一般地指令上, 所以你可像這要編寫不展開的循環: times 100 movsb 注意在'times 100 resb 1'跟'resb 100'之間並沒有顯著的區別,除了後者在彙編 時會快上一百倍。 就像'EQU','RESB'它們一樣, 'TIMES'的操作數也是嚴格語法的表達式。(見3.8) 注意'TIMES'不可以被用在宏上:原因是'TIMES'在宏被分析後再被處理,它允許 ’TIMES'的參數包含像上面的'64-$+buffer'這樣的表達式。要重複多於一行的代 碼,或者一個宏,使用預處理指令'%rep'。 3.3 有效地址 一個有效地址是一個指令的操作數,它是對內存的一個引用。在NASM中,有效地址 的語法是非常簡單的:它由一個可計算的表達式組成,放在一箇中括號內。比如: wordvar dw 123 mov ax,[wordvar] mov ax,[wordvar+1] mov ax,[es:wordvar+bx] 任何與上例不一致的表達都不是NASM中有效的內存引用,比如:'es:wordvar[bx]'。 更復雜一些的有效地址,比如含有多個寄存器的,也是以同樣的方式工作: mov eax,[ebx*2+ecx+offset] mov ax,[bp+di+8] NASM在這些有效地址上具有進行代數運算的能力,所以看似不合法的一些有效地址 使用上都是沒有問題的: mov eax,[ebx*5] ; assembles as [ebx*4+ebx] mov eax,[label1*2-label2] ; ie [label1+(label1-label2)] 有些形式的有效地址在彙編後具有多種形式;在大多數情況下,NASM會自動產生 最小化的形式。比如,32位的有效地址'[eax*2+0]'和'[eax+eax]'在彙編後具有 完全不同的形式,NASM通常只會生成後者,因爲前者會爲0偏移多開闢4個字節。 NASM具有一種隱含的機制,它會對'[eax+ebx]'和'[ebx+eax]'產生不同的操作碼; 通常,這是很有用的,因爲'[esi+ebp]'和'[ebp+esi]'具有不同的缺省段寄存器。 儘管如此,你也可以使用關鍵字'BYTE','WORD','DWORD'和'NOSPLIT'強制NASM產 生特定形式的有效地址。如果你想讓'[eax+3]'被彙編成具有一個double-word的 偏移域,而不是由NASM缺省產生一個字節的偏移。你可以使用'[dword eax+3]', 同樣,你可以強制NASM爲一個第一遍彙編時沒有看見的小值產生一個一字節的偏 移(像這樣的例子,可以參閱3.8)。比如:'[byte eax+offset]'。有一種特殊情 況,‘[byte eax]'會被彙編成'[eax+0]'。帶有一個字節的0偏移。而'[dword eax]'會帶一個double-word的0偏移。而常用的形式,'[eax]'則不會帶有偏移域。 當你希望在16位的代碼中存取32位段中的數據時,上面所描述的形式是非常有用 的。關於這方面的更多信息,請參閱9.2。實際上,如果你要存取一個在已知偏 移地址處的數據,而這個地址又大於16位值,如果你不指定一個dword偏移, NASM會讓高位上的偏移值丟失。 類似的,NASM會把'[eax*2]'分裂成'[eax+eax]' ,因爲這樣可以讓偏移域不存在 以此節省空間;實際上,它也把'[eax*2+offset]'分成'[eax+eax+offset]',你 可以使用‘NOSPLIT'關鍵字改變這種行爲:`[nosplit eax*2]'會強制 `[eax*2+0]'按字面意思被處理。 3.4 常數 NASM能理解四種不同類型的常數:數值,字符,字符串和浮點數。 3.4.1 數值常數。 一個數值常數就只是一個數值而已。NASM允許你以多種方式指定數值使用的 進制,你可以以後綴'H','Q','B'來指定十六進制數,八進制數和二進制數, 或者你可以用C風格的前綴'0x'表示十六進制數,或者以Borland Pascal風 格的前綴'$'來表示十六進制數,注意,'$'前綴在標識符中具有雙重職責 (參閱3.1),所以一個以'$'作前綴的十六進制數值必須在'$'後緊跟數字,而 不是字符。 請看一些例子: mov ax,100 ; decimal mov ax,0a2h ; hex mov ax,$0a2 ; hex again: the 0 is required mov ax,0xa2 ; hex yet again mov ax,777q ; octal mov ax,10010011b ; binary 3.4.2 字符型常數。 一個字符常數最多由包含在雙引號或單引號中的四個字符組成。引號的類型 與使用跟NASM其它地方沒什麼區別,但有一點,單引號中允許有雙引號出現。 一個具有多個字符的字符常數會被little-endian order,如果你編寫: mov eax,'abcd' 產生的常數不會是`0x61626364',而是`0x64636261',所以你把常數存入內存 的話,它會讀成'abcd'而不是'dcba'。這也是奔騰的'CPUID'指令理解的字符常 數形式(參閱B.4.34) 3.4.3 字符串常數。 字符串常數一般只被一些僞操作指令接受,比如'DB'類,還有'INCBIN'。 一個字符串常數和字符常數看上去很相像,但會長一些。它被處理成最大長 度的字符常數之間的連接。所以,以下兩個語句是等價的: db 'hello' ; string constant db 'h','e','l','l','o' ; equivalent character constants 還有,下面的也是等價的: dd 'ninechars' ; doubleword string constant dd 'nine','char','s' ; becomes three doublewords db 'ninechars',0,0,0 ; and really looks like this 注意,如果作爲'db'的操作數,類似'ab'的常數會被處理成字符串常量,因 爲它作爲字符常數的話,還不夠短,因爲,如果不這樣,那'db 'ab'會跟 'db 'a''具有同樣的效果,那是很愚蠢的。同樣的,三字符或四字符常數會 在作爲'dw'的操作數時被處理成字符串。 3.4.4 浮點常量 浮點常量只在作爲'DD','DQ','DT'的操作數時被接受。它們以傳統的形式表 達:數值,然後一個句點,然後是可選的更多的數值,然後是選項'E'跟上 一個指數。句點是強制必須有的,這樣,NASM就可以把它們跟'dd 1'區分開, 它只是聲明一個整型常數,而'dd 1.0'聲明一個浮點型常數。 一些例子: dd 1.2 ; an easy one dq 1.e10 ; 10,000,000,000 dq 1.e+10 ; synonymous with 1.e10 dq 1.e-10 ; 0.000 000 000 1 dt 3.141592653589793238462 ; pi NASM不能在編譯時求浮點常數的值。這是因爲NASM被設計爲可移植的,儘管它 常產生x86處理器上的代碼,彙編器本身卻可以和ANSI C編譯器一起運行在任 何系統上。所以,彙編器不能保證系統上總存在一個能處理Intel浮點數的浮 點單元。所以,NASM爲了能夠處理浮點運算,它必須含有它自己的一套完整 的浮點處理例程,它大大增加了彙編器的大小,卻獲得了並不多的好處。 3.5 表達式 NASM中的表達式語法跟C裏的是非常相似的。 NASM不能確定編譯時在計算表達式時的整型數尺寸:因爲NASM可以在64位系 統上非常好的編譯和運行,不要假設表達式總是在32位的寄存器中被計算的, 所以要慎重地對待整型數溢出的情況。它並不總能正常的工作。NASM唯一能 夠保證的是:你至少擁有32位長度。 NASM在表達式中支持兩個特殊的記號,即'$'和'$$',它們允許引用當前指令 的地址。'$'計算得到它本身所在源代碼行的開始處的地址;所以你可以簡 單地寫這樣的代碼'jmp $'來表示無限循環。'$$'計算當前段開始處的地址, 所以你可以通過($-$$)找出你當前在段內的偏移。 NASM提供的運算符以運算優先級爲序列舉如下: 3.5.1 `|': 位或運算符。 運算符'|'給出一個位級的或運算,所執行的操作與機器指令'or'是完全相 同的。位或是NASM中優先級最低的運算符。 3.5.2 `^': 位異或運算符。 `^' 提供位異或操作。 3.5.3 `&': 位與運算符。 `&' 提供位與運算。 3.5.4 `<<' and `>>': 位移運算符。 `<<' 提供位左移, 跟C中的實現一樣,所以'5<<3'相當於把5乘上8。'>>'提 供位右移。在NASM中,這樣的位移總是無符號的,所以位移後,左側總是以 零填充,並不會有符號擴展。 3.5.5 `+' and `-': 加與減運算符。 '+'與'-'運算符提供完整的普通加減法功能。 3.5.6 `*', `/', `//', `%'和`%%': 乘除法運算符。 '*'是乘法運算符。'/'和'//'都是除法運算符,'/'是無符號除,'//'是帶 符號除。同樣的,'%'和'%%'提供無符號與帶符號的模運算。 同ANSI C一樣,NASM不保證對帶符號模操作執行的操作的有效性。 因爲'%'符號也被宏預處理器使用,你必須保證不管是帶符號還是無符號的 模操作符都必須跟有空格。 3.5.7 一元運算符: `+', `-', `~'和`SEG' 這些只作用於一個參數的一元運算符是NASM的表達式語法中優先級最高的。 '-'把它的操作數取反,'+'不作任何事情(它只是爲了和'-'保持對稱), '~'對它的操作數取補碼,而'SEG'提供它的操作數的段地址(在3.6中會有 詳細解釋)。 3.6 `SEG'和`WRT' 當寫很大的16位程序時,必須把它分成很多段,這時,引用段內一個符號的 地址的能力是非常有必要的,NASM提供了'SEG'操作符來實現這個功能。 'SEG'操作符返回符號所在的首選段的段基址,即一個段基址,當符號的偏 移地址以它爲參考時,是有效的,所以,代碼: mov ax,seg symbol mov es,ax mov bx,symbol 總是在'ES:BX'中載入一個指向符號'symbol'的有效指針。 而事情往往可能比這還要複雜些:因爲16位的段與組是可以相互重疊的, 你通常可能需要通過不同的段基址,而不是首選的段基址來引用一個符 號,NASM可以讓你這樣做,通過使用'WRT'關鍵字,你可以這樣寫: mov ax,weird_seg ; weird_seg is a segment base mov es,ax mov bx,symbol wrt weird_seg 會在'ES:BX'中載入一個不同的,但功能上卻是相同的指向'symbol'的指 針。 通過使用'call segment:offset',NASM提供fall call(段內)和jump,這裏 'segment'和'offset'都以立即數的形式出現。所以要調用一個遠過程,你 可以如下編寫代碼: call (seg procedure):procedure call weird_seg:(procedure wrt weird_seg) (上面的圓括號只是爲了說明方便,實際使用中並不需要) NASM支持形如'call far procedure'的語法,跟上面第一句是等價的。'jmp' 的工作方式跟'call'在這裏完全相同。 在數據段中要聲明一個指向數據元素的遠指針,可以象下面這樣寫: dw symbol, seg symbol NASM沒有提供更便利的寫法,但你可以用宏自己建造一個。 3.7 `STRICT': 約束優化。 當在彙編時把優化器打開到2或更高級的時候(參閱2.1.15)。NASM會使用 尺寸約束('BYTE','WORD','DWORD','QWORD',或'TWORD'),會給它們儘可 能小的尺寸。關鍵字'STRICT'用來制約這種優化,強制一個特定的操作 數爲一個特定的尺寸。比如,當優化器打開,並在'BITS 16'模式下: push dword 33 會被編碼成 `66 6A 21',而 push strict dword 33 會被編碼成六個字節,帶有一個完整的雙字立即數`66 68 21 00 00 00'. 而當優化器關閉時,不管'STRICT'有沒有使用,都會產生相同的代碼。 3.8 臨界表達式。 NASM的一個限制是它是一個兩遍的彙編器;不像TASM和其它彙編器,它總是 只做兩遍彙編。所以它就不能處理那些非常複雜的需要三遍甚至更多遍彙編 的源代碼。 第一遍彙編是用於確定所有的代碼與數據的尺寸大小,這樣的話,在第二遍 產生代碼的時候,就可以知道代碼引用的所有符號地址。所以,有一件事 NASM不能處理,那就是一段代碼的尺寸依賴於另一個符號值,而這個符號又 在這段代碼的後面被聲明。比如: times (label-$) db 0 label: db 'Where am I?' 'TIMES'的參數本來是可以合法得進行計算的,但NASM中不允許這樣做,因爲 它在第一次看到TIMES時的時候並不知道它的尺寸大小。它會拒絕這樣的代碼。 times (label-$+1) db 0 label: db 'NOW where am I?' 在上面的代碼中,TIMES的參數是錯誤的。 NASM使用一個叫做臨界表達式的概念,以禁止上述的這些例子,臨界表達式 被定義爲一個表達式,它所需要的值在第一遍彙編時都是可計算的,所以, 該表達式所依賴的符號都是之前已經定義了的,'TIMES'前綴的參數就是一個 臨界表達式;同樣的原因,'RESB'類的僞指令的參數也是臨界表達式。 臨界表達式可能會出現下面這樣的情況: mov ax,symbol1 symbol1 equ symbol2 symbol2: 在第一遍的時候,NASM不能確定'symbol1'的值,因爲'symbol1'被定義成等於 'symbols2',而這時,NASM還沒有看到symbol2。所以在第二遍的時候,當它遇 上'mov ax,symbol1',它不能爲它產生正確的代碼,因爲它還沒有知道'symbol1' 的值。當到達下一行的時候,它又看到了'EQU',這時它可以確定symbol1的值 了,但這時已經太晚了。 NASM爲了避免此類問題,把'EQU'右側的表達式也定義爲臨界表達式,所以, 'symbol1'的定義在第一遍的時候就會被拒絕。 這裏還有一個關於前向引用的問題:考慮下面的代碼段: mov eax,[ebx+offset] offset equ 10 NASM在第一遍的時候,必須在不知道'offset'值的情況下計算指令 'mov eax,[ebx+offset]'的尺寸大小。它沒有辦法知道'offset'足夠小,足以 放在一個字節的偏移域中,所以,它以產生一個短形式的有效地址編碼的方 式來解決這個問題;在第一遍中,它所知道的所有關於'offset'的情況是:它 可能是代碼段中的一個符號,而且,它可能需要四字節的形式。所以,它強制 這條指令的長度爲適合四字節地址域的長度。在第二遍的時候,這個決定已經 作出了,它保持使這條指令很長,所以,這種情況下產生的代碼沒有足夠的小, 這個問題可以通過先定義offset的辦法得到解決,或者強制有效地址的尺寸大 小,象這樣寫代碼: [byte ebx+offset] 3.9 本地Labels NASM對於那些以一個句點開始的符號會作特殊處理,一個以單個句點開始的 Label會被處理成本地label, 這意味着它會跟前面一個非本地label相關聯. 比如: label1 ; some code .loop ; some more code jne .loop ret label2 ; some code .loop ; some more code jne .loop ret 上面的代碼片斷中,每一個'JNE'指令跳至離它較近的前面的一行上,因爲'.loop' 的兩個定義通過與它們前面的非本地Label相關聯而被分離開來了。 對於本地Label的處理方式是從老的Amiga彙編器DevPac中借鑑過來的;儘管 如此,NASM提供了進一步的性能,允許從另一段代碼中調用本地labels。這 是通過在本地label的前面加上非本地label前綴實現的:第一個.loop實際上被 定義爲'label1.loop',而第二個符號被記作'label2.loop'。所以你確實需要 的話你可寫: label3 ; some more code ; and some more jmp label1.loop 有時,這是很有用的(比如在使用宏的時候),可以定義一個label,它可以 在任何地方被引用,但它不會對常規的本地label機制產生干擾。這樣的 label不能是非本地label,因爲非本地label會對本地labels的重複定義與 引用產生干擾;也不能是本地的,因爲這樣定義的宏就不能知道label的全 稱了。所以NASM引進了第三類label,它只在宏定義中有用:如果一個label 以一個前綴'..@'開始,它不會對本地label產生干擾,所以,你可以寫: label1: ; a non-local label .local: ; this is really label1.local ..@foo: ; this is a special symbol label2: ; another non-local label .local: ; this is really label2.local jmp ..@foo ; this will jump three lines up NASM還能定義其他的特殊符號,比如以兩個句點開始的符號,比如 '..start'被用來指定'.obj'輸出文件的執行入口。(參閱6.2.6) 第四章 NASM預處理器。 -------------------------------- NASM擁有一個強大的宏處理器,它支持條件彙編,多級文件包含,兩種形式的 宏(單行的與多行的),還有爲更強大的宏能力而設置的‘context stack'機制 預處理指令都是以一個'%'打頭。 預處理器把所有以反斜槓(/)結尾的連續行合併爲一行,比如: %define THIS_VERY_LONG_MACRO_NAME_IS_DEFINED_TO / THIS_value 、 會像是單獨一行那樣正常工作。 4.1 單行的宏。 4.1.1 最常用的方式: `%define' 單行的宏是以預處理指令'%define'定義的。定義工作同C很相似,所以你可 以這樣做: %define ctrl 0x1F & %define param(a,b) ((a)+(a)*(b)) mov byte [param(2,ebx)], ctrl 'D' 會被擴展爲: mov byte [(2)+(2)*(ebx)], 0x1F & 'D' 當單行的宏被擴展開後還含有其它的宏時,展開工作會在執行時進行,而不是 定義時,如下面的代碼: %define a(x) 1+b(x) %define b(x) 2*x mov ax,a(8) 會如預期的那樣被展開成'mov ax, 1+2*8', 儘管宏'b'並不是在定義宏a 的時候定義的。 用'%define'定義的宏是大小寫敏感的:在代碼'%define foo bar'之後,只有 'foo'會被擴展成'bar':'Foo'或者'FOO'都不會。用'%idefine'來代替'%define' (i代表'insensitive'),你可以一次定義所有的大小寫不同的宏。所以 '%idefine foo bar'會導致'foo','FOO','Foo'等都會被擴展成'bar'。 當一個嵌套定義(一個宏定義中含有它本身)的宏被展開時,有一個機制可以 檢測到,並保證不會進入一個無限循環。如果有嵌套定義的宏,預處理器只 會展開第一層,因此,如果你這樣寫: %define a(x) 1+a(x) mov ax,a(3) 宏 `a(3)'會被擴展成'1+a(3)',不會再被進一步擴展。這種行爲是很有用的,有 關這樣的例子請參閱8.1。 你甚至可以重載單行宏:如果你這樣寫: %define foo(x) 1+x %define foo(x,y) 1+x*y 預處理器能夠處理這兩種宏調用,它是通過你傳遞的參數的個數來進行區分的, 所以'foo(3)'會變成'1+3',而'foo(ebx,2)'會變成'1+ebx*2'。儘管如此,但如果 你定義了: %define foo bar 那麼其他的對'foo'的定義都不會被接受了:一個不帶參數的宏定義不允許 對它進行帶有參數進行重定義。 但這並不能阻止單行宏被重定義:你可以像這樣定義,並且工作得很好: %define foo bar 然後在源代碼文件的稍後位置重定義它: %define foo baz 然後,在引用宏'foo'的所有地方,它都會被擴展成最新定義的值。這在用 '%assign'定義宏時非常有用(參閱4.1.5) 你可以在命令行中使用'-d'選項來預定義宏。參閱2.1.11 4.1.2 %define的增強版: `%xdefine' 與在調用宏時展開宏不同,如果想要調用一個嵌入有其他宏的宏時,使用 它在被定義的值,你需要'%define'不能提供的另外一種機制。解決的方案 是使用'%xdefine',或者它的大小寫不敏感的形式'%xidefine'。 假設你有下列的代碼: %define isTrue 1 %define isFalse isTrue %define isTrue 0 val1: db isFalse %define isTrue 1 val2: db isFalse 在這種情況下,'val1'等於0,而'val2'等於1。這是因爲,當一個單行宏用 '%define'定義時,它只在被調用時進行展開。而'isFalse'是被展開成 'isTrue',所以展開的是當前的'isTrue'的值。第一次宏被調用時,'isTrue' 是0,而第二次是1。 如果你希望'isFalse'被展開成在'isFalse'被定義時嵌入的'isTrue'的值, 你必須改寫上面的代碼,使用'%xdefine': %xdefine isTrue 1 %xdefine isFalse isTrue %xdefine isTrue 0 val1: db isFalse %xdefine isTrue 1 val2: db isFalse 現在每次'isFalse'被調用,它都會被展開成1,而這正是嵌入的宏'isTrue' 在'isFalse'被定義時的值。 4.1.3 : 連接單行宏的符號: `%+' 一個單行宏中的單獨的記號可以被連接起來,組成一個更長的記號以 待稍後處理。這在很多處理相似的事情的相似的宏中非常有用。 舉個例子,考慮下面的代碼: %define BDASTART 400h ; Start of BIOS data area struc tBIOSDA ; its structure .COM1addr RESW 1 .COM2addr RESW 1 ; ..and so on endstruc 現在,我們需要存取tBIOSDA中的元素,我們可以這樣: mov ax,BDASTART + tBIOSDA.COM1addr mov bx,BDASTART + tBIOSDA.COM2addr 如果在很多地方都要用到,這會變得非常的繁瑣無趣,但使用下面 的宏會大大減小打字的量: ; Macro to access BIOS variables by their names (from tBDA): %define BDA(x) BDASTART + tBIOSDA. %+ x 現在,我們可以象下面這樣寫代碼: mov ax,BDA(COM1addr) mov bx,BDA(COM2addr) 使用這個特性,我們可以簡單地引用大量的宏。(另外,還可以減少打 字錯誤)。 4.1.4 取消宏定義: `%undef' 單行的宏可以使用'%undef'命令來取消。比如,下面的代碼: %define foo bar %undef foo mov eax, foo 會被展開成指令'mov eax, foo',因爲在'%undef'之後,宏'foo'處於無定義 狀態。 那些被預定義的宏可以通過在命令行上使用'-u'選項來取消定義,參閱 2.1.12。 4.1.5 預處理器變量 : `%assign' 定義單行宏的另一個方式是使用命令'%assign'(它的大小寫不敏感形式 是%iassign,它們之間的區別與'%idefine','%idefine'之間的區別完全相 同)。 '%assign'被用來定義單行宏,它不帶有參數,並有一個數值型的值。它的 值可以以表達式的形式指定,並要在'%assing'指令被處理時可以被一次 計算出來, 就像'%define','%assign'定義的宏可以在後來被重定義,所以你可以這 樣做: %assign i i+1 以此來增加宏的數值 '%assing'在控制'%rep'的預處理器循環的結束條件時非常有用:請參 閱4.5的例子。另外的關於'%assign'的使用在7.4和8.1中的提到。 賦給'%assign'的表達式也是臨界表達式(參閱3.8),而且必須可被計算 成一個純數值型(不能是一個可重定位的指向代碼或數據的地址,或是包 含在寄存器中的一個值。) 4.2 字符串處理宏: `%strlen' and `%substr' 在宏裏可以處理字符串通常是非常有用的。NASM支持兩個簡單的字符 串處理宏,通過它們,可以創建更爲複雜的操作符。 4.2.1 求字符串長度: `%strlen' '%strlen'宏就像'%assign',會爲宏創建一個數值型的值。不同點在於 '%strlen'創建的數值是一個字符串的長度。下面是一個使用的例子: %strlen charcnt 'my string' 在這個例子中,'charcnt'會接受一個值8,就跟使用了'%assign'一樣的 效果。在這個例子中,'my string'是一個字面上的字符串,但它也可以 是一個可以被擴展成字符串的單行宏,就像下面的例子: %define sometext 'my string' %strlen charcnt sometext 就像第一種情況那樣,這也會給'charcnt'賦值8 4.2.2 取子字符串: `%substr' 字符串中的單個字符可以通過使用'%substr'提取出來。關於它使用的 一個例子可能比下面的描述更爲有用: %substr mychar 'xyz' 1 ; equivalent to %define mychar 'x' %substr mychar 'xyz' 2 ; equivalent to %define mychar 'y' %substr mychar 'xyz' 3 ; equivalent to %define mychar 'z' 在這個例子中,mychar得到了值'z'。就像在'%strlen'(參閱4.2.1)中那樣, 第一個參數是一個將要被創建的單行宏,第二個是字符串,第三個參數 指定哪一個字符將被選出。注意,第一個索引值是1而不是0,而最後一 個索引值等同於'%strlen'給出的值。如果索引值超出了範圍,會得到 一個空字符串。 4.3 多行宏: `%macro' 多行宏看上去更象MASM和TASM中的宏:一個NASM中定義的多行宏看上去就 象下面這樣: %macro prologue 1 push ebp mov ebp,esp sub esp,%1 %endmacro 這裏,定義了一個類似C函數的宏prologue:所以你可以通過一個調用來使 用宏: myfunc: prologue 12 這會把三行代碼擴展成如下的樣子: myfunc: push ebp mov ebp,esp sub esp,12 在'%macro'一行上宏名後面的數字'1'定義了宏可以接收的參數的個數。 宏定義裏面的'%1'是用來引用宏調用中的第一個參數。對於一個有多 個參數的宏,參數序列可以這樣寫:'%2','%3'等等。 多行宏就像單行宏一樣,也是大小寫敏感的,除非你使用另一個操作符 ‘%imacro' 如果你必須把一個逗號作爲參數的一部分傳遞給多行宏,你可以把整 個參數放在一個括號中。所以你可以象下面這樣編寫代碼: %macro silly 2 %2: db %1 %endmacro silly 'a', letter_a ; letter_a: db 'a' silly 'ab', string_ab ; string_ab: db 'ab' silly {13,10}, crlf ; crlf: db 13,10 4.3.1 多行宏的重載 就象單行宏,多行宏也可以通過定義不同的參數個數對同一個宏進行多次 重載。而這次,沒有對不帶參數的宏的特殊處理了。所以你可以定義: %macro prologue 0 push ebp mov ebp,esp %endmacro 作爲函數prologue的另一種形式,它沒有開闢本地棧空間。 有時候,你可能需要重載一個機器指令;比如,你可能想定義: %macro push 2 push %1 push %2 %endmacro 這樣,你就可以如下編寫代碼: push ebx ; this line is not a macro call push eax,ecx ; but this one is 通常,NASM會對上面的第一行給出一個警告信息,因爲'push'現在被定義成 了一個宏,而這行給出的參數個數卻不符合宏的定義。但正確的代碼還是 會生成的,僅僅是給出一個警告而已。這個警告信息可以通過 '-w'macro-params’命令行選項來禁止。(參閱2.1.17)。 4.3.2 Macro-Local Labels NASM允許你在多行宏中定義labels.使它們對於每一個宏調用來講是本地的:所 以多次調用同一個宏每次都會使用不同的label.你可以通過在label名稱前面 加上'%%'來實現這種用法.所以,你可以創建一條指令,它可以在'Z'標誌位被 設置時執行'RET'指令,如下: %macro retz 0 jnz %%skip ret %%skip: %endmacro 你可以任意多次的調用這個宏,在你每次調用時的時候,NASM都會爲'%%skip' 建立一個不同的名字來替換它現有的名字.NASM創建的名字可能是這個樣子 的:'[email protected]',這裏的數字2345在每次宏調用的時候都會被修改.而 '..@'前綴防止macro-local labels干擾本地labels機制,就像在3.9中所 描述的那樣.你應該避免在定義你自己的宏時使用這種形式('..@'前綴,然後 是一個數字,然後是一個句點),因爲它們會和macro-local labels相互產生 干擾. 4.3.3 不確定的宏參數個數. 通常,定義一個宏,它可以在接受了前面的幾個參數後, 把後面的所有參數都 作爲一個參數來使用,這可能是非常有用的,一個相關的例子是,一個宏可能 用來寫一個字符串到一個MS-DOS的文本文件中,這裏,你可能希望這樣寫代碼: writefile [filehandle],"hello, world",13,10 NASM允許你把宏的最後一個參數定義成"貪婪參數", 也就是說你調用這個宏時 ,使用了比宏預期得要多得多的參數個數,那所有多出來的參數連同它們之間 的逗號會被作爲一個參數傳遞給宏中定義的最後一個實參,所以,如果你寫: %macro writefile 2+ jmp %%endstr %%str: db %2 %%endstr: mov dx,%%str mov cx,%%endstr-%%str mov bx,%1 mov ah,0x40 int 0x21 %endmacro 那上面使用'writefile'的例子會如預期的那樣工作:第一個逗號以前的文本 [filehandle]會被作爲第一個宏參數使用,會被在'%1'的所有位置上擴展,而 所有剩餘的文本都被合併到'%2'中,放在db後面. 這種宏的貪婪特性在NASM中是通過在宏的'%macro'一行上的參數個數後面加 上'+'來實現的. 如果你定義了一個貪婪宏,你就等於告訴NASM對於那些給出超過實際需要的參 數個數的宏調用該如何擴展; 在這種情況下,比如說,NASM現在知道了當它看到 宏調用'writefile'帶有2,3或4個或更多的參數的時候,該如何做.當重載宏 時,NASM會計算參數的個數,不允許你定義另一個帶有4個參數的'writefile' 宏. 當然,上面的宏也可以作爲一個非貪婪宏執行,在這種情況下,調用語句應該 象下面這樣寫: writefile [filehandle], {"hello, world",13,10} NASM提供兩種機制實現把逗號放到宏參數中,你可以選擇任意一種你喜歡的 形式. 有一個更好的辦法來書寫上面的宏,請參閱5.2.1 4.3.4 缺省宏參數. NASM可以讓你定義一個多行宏帶有一個允許的參數個數範圍.如果你這樣做了, 你可以爲參數指定缺省值.比如: %macro die 0-1 "Painful program death has occurred." writefile 2,%1 mov ax,0x4c01 int 0x21 %endmacro 這個宏(它使用了4.3.3中定義的宏'writefile')在被調用的時候可以有一個 錯誤信息,它會在退出前被顯示在錯誤輸出流上,如果它在被調用時不帶參數 ,它會使用在宏定義中的缺省錯誤信息. 通常,你以這種形式指定宏參數個數的最大值與最小值; 最小個數的參數在 宏調用的時候是必須的,然後你要爲其他的可選參數指定缺省值.所以,當一 個宏定義以下面的行開始時: %macro foobar 1-3 eax,[ebx+2] 它在被調用時可以使用一到三個參數, 而'%1'在宏調用的時候必須指定,'%2' 在沒有被宏調用指定的時候,會被缺省地賦爲'eax','%3'會被缺省地賦爲 '[ebx+2]'. 你可能在宏定義時漏掉了缺省值的賦值, 在這種情況下,參數的缺省值被賦爲 空.這在可帶有可變參數個數的宏中非常有用,因爲記號'%0'可以讓你確定有 多少參數被真正傳給了宏. 這種缺省參數機制可以和'貪婪參數'機制結合起來使用;這樣上面的'die'宏 可以被做得更強大,更有用,只要把第一行定義改爲如下形式即可: %macro die 0-1+ "Painful program death has occurred.",13,10 最大參數個數可以是無限,以'*'表示.在這種情況下,當然就不可能提供所有 的缺省參數值. 關於這種用法的例子參見4.3.6. 4.3.5 `%0': 宏參數個數計數器. 對於一個可帶有可變個數參數的宏, 參數引用'%0'會返回一個數值常量表示 有多少個參數傳給了宏.這可以作爲'%rep'的一個參數(參閱4.5),以用來遍歷 宏的所有參數. 例子在4.3.6中給出. 4.3.6 `%rotate': 循環移動宏參數. Unix的shell程序員對於'shift' shell命令再熟悉不過了,它允許把傳遞給shell 腳本的參數序列(以'$1,'$2'等引用)左移一個,所以, 前一個參數是‘$1'的話 左移之後,就變成’$2'可用了,而在'$1'之前是沒有可用的參數的。 NASM具有相似的機制,使用'%rotate'。就象這個指令的名字所表達的,它跟Unix 的'shift'是不同的,它不會讓任何一個參數丟失,當一個參數被移到最左邊的 時候,再移動它,它就會跳到右邊。 '%rotate'以單個數值作爲參數進行調用(也可以是一個表達式)。宏參數被循環 左移,左移的次數正好是這個數字所指定的。如果'%rotate'的參數是負數,那麼 宏參數就會被循環右移。 所以,一對用來保存和恢復寄存器值的宏可以這樣寫: %macro multipush 1-* %rep %0 push %1 %rotate 1 %endrep %endmacro 這個宏從左到右爲它的每一個參數都依次調用指令'PUSH'。它開始先把它的 第一個參數'%1'壓棧,然後調用'%rotate'把所有參數循環左移一個位置,這樣 一來,原來的第二個參數現在就可以用'%1'來取用了。重複執行這個過程, 直到所有的參數都被執行完(這是通過把'%0'作爲'%rep'的參數來實現的)。 這就實現了把每一個參數都依次壓棧。 注意,'*'也可以作爲最大參數個數的一個計數,表明你在使用宏'multipush'的 時候,參數個數沒有上限。 使用這個宏,確實是非常方便的,執行同等的'POP'操作,我們並不需要把參數 順序倒一下。一個完美的解決方案是,你再寫一個'multipop'宏調用,然後把 上面的調用中的參數複製粘貼過來就行了,這個宏會對所有的寄存器執行相反 順序的pop操作。 這可以通過下面定義來實現: %macro multipop 1-* %rep %0 %rotate -1 pop %1 %endrep %endmacro 這個宏開始先把它的參數循環右移一個位置,這樣一來,原來的最後一個參數 現在可以用'%1'引用了。然後被pop,然後,參數序列再一次右移,倒數第二個 參數變成了'%1',就這樣,所以參數被以相反的順序一一被執行。 4.3.7 連結宏參數。 NASM可以把宏參數連接到其他的文本中。這個特性可以讓你聲明一個系例 的符號,比如,在宏定義中。你希望產生一個關於關鍵代碼的表格,而代碼 跟在表中的偏移值有關。你可以這樣編寫代碼: %macro keytab_entry 2 keypos%1 equ $-keytab db %2 %endmacro keytab: keytab_entry F1,128+1 keytab_entry F2,128+2 keytab_entry Return,13 會被擴展成: keytab: keyposF1 equ $-keytab db 128+1 keyposF2 equ $-keytab db 128+2 keyposReturn equ $-keytab db 13 你可以很輕易地把文本連接到一個宏參數的尾部,這樣寫即可:'%1foo'。 如果你希望給宏參數加上一個數字,比如,通過傳遞參數'foo'來定義符 號'foo1'和'foo2',但你不能寫成'%11',因爲這會被認爲是第11個參數。 你必須寫成'%{1}1',它會把第一個1跟第二個分開 這個連結特性還可以用於其他預處理問題中,比如macro-local labels(4.3.2) 和context-local labels(4.7.2)。在所有的情況中,語法上的含糊不清都可以 通過把'%'之後,文本之前的部分放在一個括號中得到解決:所以'%{%foo}bar 會把文本'bar'連接到一個macro-local label:’%%foo'的真正名字的後面(這 個是不必要的,因爲就NASM處理macro-local labels的機制來講,'%{%foo}bar 和%%foobar都會被擴展成同樣的形式,但不管怎麼樣,這個連結的能力是在的) 4.3.8 條件代碼作爲宏參數。 NASM對於含有條件代碼的宏參數會作出特殊處理。你可以以另一種形式 '%+1'來使用宏參數引用'%1',它告訴NASM這個宏參數含有一個條件代碼, 如果你調用這個宏時,參數中沒有有效的條件代碼,會使預處理器報錯。 爲了讓這個特性更有用,你可以以'%-1'的形式來使用參數,它會讓NASM把 這個條件代碼擴展成它的反面。所以4.3.2中定義的宏'retz'還可以以 下面的方式重寫: %macro retc 1 j%-1 %%skip ret %%skip: %endmacro 這個指令可以使用'retc ne'來進行調用,它會把條件跳轉指令擴展成'JE', 或者'retc po'會把它擴展成'JPE'。 '%+1'的宏參數引用可以很好的把參數'CXZ'和'ECXZ'解釋爲有效的條件 代碼;但是,'%-1'碰上上述的參數就會報錯,因爲這兩個條件代碼沒有相 反的情況存在。 4.3.9 禁止列表擴展。 當NASM爲你的源程序產生列表文件的時候,它會在宏調用的地方爲你展開 多行宏,然後列出展開後的所有行。這可以讓你看到宏中的哪些指令展 開成了哪些代碼;儘管如此,有些不必要的宏展開會把列表弄得很混亂。 NASM爲此提供了一個限定符'.nolist',它可以被包含在一個宏定義中,這 樣,這個宏就不會在列表文件中被展開。限定符'.nolist'直接放到參數 的後面,就像下面這樣: %macro foo 1.nolist 或者這樣: %macro bar 1-5+.nolist a,b,c,d,e,f,g,h 4.4 條件彙編 跟C預處理器相似,NASM允許對一段源代碼只在某特定條件滿足時進行彙編, 關於這個特性的語法就像下面所描述的: %if<condition> ;if <condition>滿足時接下來的代碼被彙編。 %elif<condition2> ; 當if<condition>不滿足,而<condition2>滿足時,該段代碼被彙編。 %else ;當<condition>跟<condition2>都不滿足時,該段代碼被彙編。 %endif '%else'跟'%elif'子句都是可選的,你也可以使用多於一個的'%elif'子句。 4.4.1 `%ifdef': 測試單行宏是否存在。 '%ifdef MACRO'可以用來開始一個條件彙編塊,跟在它後面的代碼當且僅 當一個叫做'MACRO'單行宏被定義時纔會被會彙編。如果沒有定義,那麼 '%elif'和'%else'塊會被處理。 比如,當調試一個程序時,你可能希望這樣寫代碼: ; perform some function %ifdef DEBUG writefile 2,"Function performed successfully",13,10 %endif ; go and do something else 你可以通過使用命令行選項'-dDEBUG'來建立一個處理調試信息的程序,或 不使用該選項來產生最終發佈的程序。 你也可以測試一個宏是否沒有被定義,這可以使用'%ifndef'。你也可以在 '%elif'塊中測試宏定義,使用'%elifdef'和'%elifndef'即可。 4.4.2 `ifmacro': 測試多行宏是否存在。 除了是測試多行宏的存在的,'%idmacro'操作符的工作方式跟'%ifdef'是一 樣的。 比如,你可能在編寫一個很大的工程,而且無法控制存在鏈接庫中的宏。你 可能需要建立一個宏,但必須先確保這個宏沒有被建立過,如果被建立過了, 你需要爲你的宏換一個名字。 如果你定義的一個有特定參數個數與宏名的宏與現有的宏會產生衝突,那麼 '%ifmacro'會返回真。比如: %ifmacro MyMacro 1-3 %error "MyMacro 1-3" causes a conflict with an existing macro. %else %macro MyMacro 1-3 ; insert code to define the macro %endmacro %endif 如果沒有現有的宏會產生衝突,這會建立一個叫'MyMacro 1-3"的宏,如果 會有衝突,那麼就產生一條警告信息。 你可以通過使用'%ifnmacro'來測試是否宏不存在。還可以使用'%elifmacro' 和'%elifnmacro'在'%elif'塊中測試多行宏。 4.4.3 `%ifctx': 測試上下文棧。 當且僅當預處理器的上下文棧中的頂部的上下文的名字是'ctxname'時,條 件彙編指令'%ifctx ctxname'會讓接下來的語句被彙編。跟'%ifdef'一樣, 它也有'%ifnctx','%elifctx','%elifnctx'等形式。 關於上下文棧的更多細節,參閱4.7, 關於'%ifctx'的一個例子,參閱4.7.5. 4.4.4 `%if': 測試任意數值表達式。 當且僅當數值表達式'expr'的值爲非零時,條件彙編指令'%if expr'會讓接 下來的語句被彙編。使用這個特性可以確定何時中斷一個'%rep'預處理器循 環,例子參閱4.5。 '%if'和'%elif'的表達式是一個臨界表達式(參閱3.8) '%if' 擴展了常規的NASM表達式語法,提供了一組在常規表達式中不可用的 相關操作符。操作符'=','<','>','<=','>='和'<>'分別測試相等,小於,大 於,小於等於,大於等於,不等於。跟C相似的形式'=='和'!='作爲'=','<>' 的另一種形式也被支持。另外,低優先級的邏輯操作符'&&','^^',和'||'作 爲邏輯與,邏輯異或,邏輯或也被支持。這些跟C的邏輯操作符類似(但C沒 有提供邏輯異或),這些邏輯操作符總是返回0或1,並且把任何非零輸入看 作1(所以,比如, '^^'它會在它的一個輸入是零,另一個非零的時候,總 返回1)。這些操作符返回1作爲真值,0作爲假值。 4.4.5 `%ifidn' and `%ifidni': 測試文本相同。 當且僅當'text1'和'text2'在作爲單行宏展開後是完全相同的一段文本時, 結構'%ifidn text1,text2'會讓接下來的一段代碼被彙編。兩段文本在空格 個數上的不同會被忽略。 '%ifidni'和'%ifidn'相似,但是大小寫不敏感。 比如,下面的宏把一個寄存器或數字壓棧,並允許你把IP作爲一個真實的寄 存器使用: %macro pushparam 1 %ifidni %1,ip call %%label %%label: %else push %1 %endif %endmacro 就像大多數的'%if'結構,'%ifidn'也有一個'%elifidn',並有它的反面的形 式'%ifnidn','%elifnidn'.相似的,'%ifidni'也有'%elifidni',`%ifnidni' 和`%elifnidni'。 4.4.6 `%ifid', `%ifnum', `%ifstr': 測試記號的類型。 有些宏會根據傳給它們的是一個數字,字符串或標識符而執行不同的動作。 比如,一個輸出字符串的宏可能會希望能夠處理傳給它的字符串常數或一 個指向已存在字符串的指針。 當且僅當在參數列表的第一個記號存在且是一個標識符時,條件彙編指令 '%ifid'會讓接下來的一段代碼被彙編。'%ifnum'相似。但測試記號是否是 數字;'%ifstr'測試是否是字符串。 比如,4.3.3中定義的宏'writefile'可以用'%ifstr'作進一步改進,如下: %macro writefile 2-3+ %ifstr %2 jmp %%endstr %if %0 = 3 %%str: db %2,%3 %else %%str: db %2 %endif %%endstr: mov dx,%%str mov cx,%%endstr-%%str %else mov dx,%2 mov cx,%3 %endif mov bx,%1 mov ah,0x40 int 0x21 %endmacro 這個宏可以處理以下面兩種方式進行的調用: writefile [file], strpointer, length writefile [file], "hello", 13, 10 在第一種方式下,'strpointer'是作爲一個已聲明的字符串的地址,而 'length'作爲它的長度;第二種方式中,一個字符串被傳給了宏,所以 宏就自己聲明它,併爲它分配地址和長度。 注意,'%ifstr'中的'%if'的使用方式:它首先檢測宏是否被傳遞了兩個參 數(如果是這樣,那麼字符串就是一個單個的字符串常量,這樣'db %2'就 足夠了)或者更多(這樣情況下,除了前兩個參數,後面的全部參數都要被 合併到'%3'中,這就需要'db %2,%3'了。) 常見的'%elifXXX','%ifnXXX'和'%elifnXXX'/版本在'%ifid','%ifnum',和 '%ifstr'中都是存在的。 4.4.7 `%error': 報告用戶自定義錯誤。 預處理操作符'%error'會讓NASM報告一個在彙編時產生的錯誤。所以,如果 別的用戶想要彙編你的源代碼,你必須保證他們用下面的代碼定義了正確的 宏: %ifdef SOME_MACRO ; do some setup %elifdef SOME_OTHER_MACRO ; do some different setup %else %error Neither SOME_MACRO nor SOME_OTHER_MACRO was defined. %endif 然後,任何不理解你的代碼的用戶都會被彙編時得到關於他們的錯誤的警告 信息,不必等到程序在運行時再出現錯誤卻不知道錯在哪兒。 4.5 預處理器循環: `%rep' 雖然NASM的'TIMES'前綴非常有用,但是不能用來作用於一個多行宏,因爲 它是在NASM已經展開了宏之後才被處理的。所以,NASM提供了另外一種形式 的循環,這回是在預處理器級別的:'%rep'。 操作符'%rep'和'%endrep'('%rep'帶有一個數值參數,可以是一個表達式; '%endrep'不帶任何參數)可以用來包圍一段代碼,然後這段代碼可以被複制 多次,次數由預處理器指定。 %assign i 0 %rep 64 inc word [table+2*i] %assign i i+1 %endrep 這段代碼會產生連續的64個'INC'指令,從內存地址'[table]'一直增長到 '[table+126]'。 對於一個複雜的終止條件,或者想要從循環中break出來,你可以使用 '%exitrep'操作符來終止循環,就像下面這樣: fibonacci: %assign i 0 %assign j 1 %rep 100 %if j > 65535 %exitrep %endif dw j %assign k j+i %assign i j %assign j k %endrep fib_number equ ($-fibonacci)/2 上面的代碼產生所有16位的Fibonacci數。但要注意,循環的最大次數還是要 作爲一個參數傳給'%rep'。這可以防止NASM預處理器進入一個無限循環。在 多任務或多用戶系統中,無限循環會導致內存被耗光或其他程序崩潰。 4.6 包含其它文件。 又一次使用到一個跟C預處理器語法極其相似的操作符,它可以讓你在你的代 碼中包含其它源文件。這可以通過'%include'來實現: %include "macros.mac" 這會把文件'macros.mac'文件中的內容包含到現在的源文件中。 被包含文件會被在當前目錄下尋找(就是你在運行NASM時所在的目錄,並不是 NASM可執行文件所在的目錄或源程序文件所在的目錄),你可以在NASM的命令行 上使用選項'-i'來增加搜索路徑。 C語言中防止文件被重複包含的習慣做法在NASM中也適用:如果文件 'macros.mac'中有如下形式的代碼: %ifndef MACROS_MAC %define MACROS_MAC ; now define some macros %endif 這樣多次包含該文件就不會引起錯誤,因爲第二次包含該文件時,什麼 也不會發生,因爲宏'MACROS_MAC'已經被定義過了。 在沒用'%include'操作符包含一個文件時,你可以強制讓這個文件被包含 進來,做法是在NASM命令行上使用'-p'選項 4.7 上下文棧。 那些對一個宏定義來講是本地的Labels有時候還不夠強大:有時候,你需 要能夠在多個宏調用之間共享label。比如一個'REPEAT'...'UNTIL'循 環,'REPEAT'宏的展開可能需要能夠去引用'UNTIL'中定義的宏。而且在 使用這樣的宏時,你可能還會嵌套多層循環。 NASM通過上下文棧提供這個層次上的功能。預處理器維護了一個包含上下 文的棧,每一個上下文都有一個名字作爲標識。你可以通過指令'%push' 往上下文棧中加一個新的上下文,或通過'%pop'去掉一個。你可以定義一 些只針對特定上下文來說是本地的labels。 4.7.1 `%push' and `%pop': 創建和刪除上下文。 '%push'操作符用來創建一個新的上下文,然後把它放在上下文棧的頂端。 '%push'需要一個參數,它是這個上下文的名字,例如: %push foobar 這會把一個新的叫做'foobar'的上下文放到棧頂。你可以在一個棧中擁有 多個具有相同名字的上下文:它們之間仍舊是可以區分的。 操作符'%pop'不需要參數,刪除棧頂的上下文,並把它銷燬,同時也刪除 跟它相關的labels。 4.7.2 Context-Local Labels 就像'%%foo'會定義一個對於它所在的那個宏來講是本地的label一樣, '%$foo'會定義一個對於當前棧頂的上下文來講是本地的lable。所以,上 文提到的'REPEAT','UNTIL'的例子可以以下面的方式實現: %macro repeat 0 %push repeat %$begin: %endmacro %macro until 1 j%-1 %$begin %pop %endmacro 然後象下面這樣使用它: mov cx,string repeat add cx,3 scasb until e 它會掃描每個字符串中的第四個字節,以查找在al中的字節。 如果你需要定義,或存取對於不在棧頂的上下文本地的label,你可以使用 '%$$foo',或'%$$$foo'來存取棧下面的上下文。 4.7.3 Context-Local單行宏。 NASM也允許你定義對於一個特定的上下文是本地的單行宏,使用的方式大致 相面: %define %$localmac 3 這會定義一個對於棧頂的上下文本地的單行宏'%$localmax',當然,在又一個 '%push'操作之後,它還是可以通過'%$$localmac'來存取。 4.7.4 `%repl': 對一個上下文改名。 如果你需要改變一個棧頂上下文的名字(比如,爲了響應'%ifctx'),你可以 在'%pop'之後緊接着一個'%push';但它會產生負面效應,會破壞所有的跟棧 頂上下文相關的context-local labels和宏。 NASM提供了一個操作符'%repl',它可以在不影響相關的宏與labels的情況下, 爲一個上下文換一個名字,所以你可以把下面的破壞性代碼替換成另一種形 式: %pop %push newname 換成不具破壞性的版本: `%repl newname'. 4.7.5 使用上下文棧的例子: Block IFs 這個例子幾乎使用了所有的上下文棧的特性,包括條件彙編結構'%ifctx', 它把一個塊IF語句作爲一套宏來執行: %macro if 1 %push if j%-1 %$ifnot %endmacro %macro else 0 %ifctx if %repl else jmp %$ifend %$ifnot: %else %error "expected `if' before `else'" %endif %endmacro %macro endif 0 %ifctx if %$ifnot: %pop %elifctx else %$ifend: %pop %else %error "expected `if' or `else' before `endif'" %endif %endmacro 這段代碼看上去比上面的`REPEAT'和`UNTIL'宏要飽滿多了。因爲它使用了 條件彙編去驗證宏以正確的順序被執行(比如,不能在'if'之間調用'endif' )如果出現錯誤,執行'%error'。 另外,'endif'宏要處理兩種不同的情況,即它可能直接跟在'if'後面,也 可能跟在'else'後面。它也是通過條件彙編,判斷上下文棧的棧頂是'if'還 是'else',並據此來執行不同的動作。 'else'宏必須把上下文保存到棧中,好讓'if'宏跟'endif'宏中定義的 '%$ifnot'引用。但必須改變上下文的名字,這樣'endif'就可以知道這中間 還有一個'else'。這是通過'%repl'來做這件事情的。 下面是一個使用這些宏的例子: cmp ax,bx if ae cmp bx,cx if ae mov ax,cx else mov ax,bx endif else cmp ax,cx if ae mov ax,cx endif endif 通過把在內層'if'中描述的另一個上下文壓棧,放在外層'if'中的上下文 的上面,這樣,'else'和'endif'總能引用到匹配的'if'或'else'。這個 塊-'IF'宏處理嵌套的能力相當好, 4.8 標準宏。 NASM定義了一套標準宏,當開始處理源文件時,這些宏都已經被定義了。 如果你真的希望一個程序在執行前沒有預定義的宏存在,你可以使用 '%clear'操作符清空預處理器的一切。 大多數用戶級的操作符(第五章)是作爲宏來運行的,這些宏進一步調用原 始的操作符;這些在第五章介紹。剩餘的標準宏在這裏進行描述。 4.8.1 `__NASM_MAJOR__', `__NASM_MINOR__', `__NASM_SUBMINOR__'和 `___NASM_PATCHLEVEL__': NASM版本宏。 單行宏`__NASM_MAJOR__', `__NASM_MINOR__',`__NASM_SUBMINOR__'和 `___NASM_PATCHLEVEL__'被展開成當前使用的NASM的主版本號,次版本號, 子次版本號和補丁級。所在,在NASM 0.98.32p1版本中,`__NASM_MAJOR__' 被展開成0,`__NASM_MINOR__'被展開成98,`__NASM_SUBMINOR__'被展開成 32,`___NASM_PATCHLEVEL__'被定義爲1。 4.8.2 `__NASM_VERSION_ID__': NASM版本ID。 單行宏`__NASM_VERSION_ID__'被展開成雙字整型數,代表當前使用的版本 的NASM的全版本數。這個值等於把`__NASM_MAJOR__',`__NASM_MINOR__', `__NASM_SUBMINOR__'和`___NASM_PATCHLEVEL__'連結起來產生一個單個的 雙字長整型數。所以,對於0.98.32p1,返回值會等於 dd 0x00622001 或者 db 1,32,98,0 注意,上面兩行代碼產生的是完全相同的代碼,第二行只是用來指出內存 中存在的各個值之間的順序。 4.8.3 `__NASM_VER__': NASM版本字符串。 單行宏`__NASM_VER__'被展開成一個字符串,它定義了當前使用的NASM的 版本號。所以,在NASM0.98.32下: db __NASM_VER__ 會被展開成: db "0.98.32" 4.8.4 `__FILE__' and `__LINE__': 文件名和行號。 就像C的預處理器,NASM允許用戶找到包含有當前指令的文件的文件名和行 數。宏'__FILE__'展開成一個字符串常量,該常量給出當前輸入文件的文件 名(如果含有'%include'操作符,這個值會在彙編的過程中改變),而 `__LINE__'會被展開成一個數值常量,給出在輸入文件中的當前行的行號。 這些宏可以使用在宏中,以查看調試信息,當在一個宏定義中包含宏 '__LINE__'時(不管 是單行還是多行),會返回宏調用,而不是宏定義處 的行號。這可以用來確定是否在一段代碼中發生了程序崩潰。比如,某人 可以編寫一個子過程'stillhere',它通過'EAX'傳遞一個行號,然後輸出一 些信息,比如:"line 155: still here'。你可以這樣編寫宏: %macro notdeadyet 0 push eax mov eax,__LINE__ call stillhere pop eax %endmacro 然後,在你的代碼中插入宏調用,直到你發現發生錯誤的代碼爲止。 4.8.5 `STRUC' and `ENDSTRUC': 聲明一個結構體數據類型。 在NASM的內部,沒有真正意義上的定義結構體數據類型的機制; 取代它的 是,預處理器的功能相當強大,可以把結構體數據類型以一套宏的形式來 運行。宏 ‘STRUCT’ 和'ENDSTRUC'是用來定義一個結構體數據類型的。 ‘STRUCT’帶有一個參數,它是結構體的名字。這個名字代表結構體本身,它 在結構體內的偏移地址爲零,名字加上一個_size後綴組成的符號,用一個 'EQU'給它賦上結構體的大小。一旦‘STRUC'被執行,你就開始在定義一個 結構體,你可以用'RESB'類僞指令定義結構體的域,然後使用'ENDSTRUC'來 結束定義。 比如,定義一個叫做'mytype'的結構體,包含一個longword,一個word,一個 byte,和一個字符串,你可以這樣寫代碼: struc mytype mt_long: resd 1 mt_word: resw 1 mt_byte: resb 1 mt_str: resb 32 endstruc 上面的代碼定義了六個符號:'m_long'在地置0(從結構體'mytype'開頭開始 到這個longword域的偏移),`mt_word'在地置4, `mt_byte'6, `mt_str'7, `mytype_size'是39,而`mytype' 自己在地置0 之所以要把結構體的名字定義在地址零處,是因爲要讓結構體可以使用本地 labels機制的緣故:如果你想要在多個結構體中使用具有同樣名字的成員, 你可以把上面的結構體定義成這個樣子: struc mytype .long: resd 1 .word: resw 1 .byte: resb 1 .str: resb 32 endstruc 在這個定義中,把結構體域的偏移值定義成了:'mytype.long', `mytype.word', `mytype.byte' and `mytype.str'. NASM因此而沒有內部的結構體支持,也不支持以句點形式引用結構體中的成 員,所以代碼'mov ax, [mystruc.mt_word]’是非法的,'mt_word'是一個常數, 就像其它類型的常數一樣,所以,正確的語法應該是 'mov ax,[mystruc+mt_word]'或者`mov ax,[mystruc+mytype.word]'. 4.8.6 `ISTRUC', `AT' and `IEND': 聲明結構體的一個實例。 定義了一個結構體類型以後,你下一步要做的事情往往就是在你的數據段 中聲明一個結構體的實例。NASM通過使用'ISTRUC'機制提供一種非常簡單 的方式。在程序中聲明一個'mytype'結構體,你可以象下面這樣寫代碼: mystruc: istruc mytype at mt_long, dd 123456 at mt_word, dw 1024 at mt_byte, db 'x' at mt_str, db 'hello, world', 13, 10, 0 iend ‘AT’宏的功能是通過使用'TIMES'前綴把偏移位置定位到正確的結構體域上, 然後,聲明一個特定的數據。所以,結構體域必須以在結構體定義中相同的 順序被聲明。 如果爲結構體的域賦值要多於一行,那接下的內容可直接跟在'AT'行後面,比 如: at mt_str, db 123,134,145,156,167,178,189 db 190,100,0 按個人的喜好不同,你也可以不在'AT'行上寫數據,而直接在第二行開始寫 數據域: at mt_str db 'hello, world' db 13,10,0 4.8.7 `ALIGN' and `ALIGNB': 數據對齊 宏'ALIGN'和'ALIGNB'提供一種便捷的方式來進行數據或代碼的在字,雙字,段 或其他邊界上的對齊(有些彙編器把這兩個宏叫做'EVEN'),有關這兩個宏的語法 是: align 4 ; align on 4-byte boundary align 16 ; align on 16-byte boundary align 8,db 0 ; pad with 0s rather than NOPs align 4,resb 1 ; align to 4 in the BSS alignb 4 ; equivalent to previous line 這兩個個參數都要求它們的第一個參數是2的冪;它們都會計算需要多少字節來 來存儲當前段,當然這個字節數必須向上對齊到一個2的冪值。然後用它們的第 二個參數來執行'TIMES'前綴進行對齊。 如果第二個參數沒有被指定,那'ALIGN'的缺省值就是'NOP',而'ALIGNB'的缺省 值就是'RESB 1'.當第二個參數被指定時,這兩個宏是等效的。通常,你可以在 數據段與代碼段中使用'ALIGN',而在BSS段中使用'ALIGNB',除非有特殊用途,一 般你不需要第二個參數。 作爲兩個簡單的宏,'ALIGN'與'ALIGNB'不執行錯誤檢查:如果它們的第一個參 數不是2的某次方,或它們的第二個參數大於一個字節的代碼,他們都不會有警 告信息,這兩種情況下,它們都會執行錯誤。 'ALIGNB'(或者,'ALIGN'帶上第二個參數'RESB 1')可以用在結構體的定義中: struc mytype2 mt_byte: resb 1 alignb 2 mt_word: resw 1 alignb 4 mt_long: resd 1 mt_str: resb 32 endstruc 這可以保證結構體的成員被對齊到跟結構體的基地址之間有一個正確的偏移值。 最後需要注意的是,'ALIGN'和'ALIGNB'都是以段的開始地址作爲參考的,而 不是整個可執行程序的地址空間。如果你所在的段只能保證對齊到4字節的邊 界,那它會對齊對16字節的邊界,這會造成浪費,另外,NASM不會檢測段的對 齊特性是否可被'ALIGN'和'ALIGNB'使用。 4.9 TASM兼容預處理指令。 接下來的預處理操作符只有在用'-t'命令行開關把TASM兼容模式打開的情況下 纔可以使用(這個開關在2.1.16介紹過) (*) `%arg' (見4.9.1節) (*) `%stacksize' (見4.9.2節) (*) `%local' (見4.9.3節) 4.9.1 `%arg'操作符 '%arg'操作符用來簡化棧上的參數傳遞操作處理。基於棧的參數傳遞在很多高 級語言中被使用,包括C,C++和Pascal。 而NASM企圖通過宏來實現這種功能(參閱7.4.5),它的語法使用上不是很舒服, 而且跟TASM之間是不兼容的。這裏有一個例子,展示了只通過宏'%arg'來處理: some_function: %push mycontext ; save the current context %stacksize large ; tell NASM to use bp %arg i:word, j_ptr:word mov ax,[i] mov bx,[j_ptr] add ax,[bx] ret %pop ; restore original context 這跟在7.4.5中定義的過程很相似,把j_ptr指向的值加到i中,然後把相加的結 果在AX中返回,對於'push'和'pop'的展開請參閱4.7.1關於上下文棧的使用。 4.9.2 `%stacksize'指令。 '%stacksize'指令是跟'%arg'和'%local'指令結合起來使用的。它告訴NASM 爲'%arg'和'%local'使用的缺省大小。'%stacksize'指令帶有一個參數,它 是'flat','large'或'small'。 %stacksize flat 這種形式將使NASM使用相對於'ebp'的基於棧的參數地址。它假設使用一個 近調用來得到這個參數表。(比如,eip被壓棧). %stacksize large 而這種形式會用'bp'來進行基於棧的參數尋址,假設使用了一個遠調用來 獲得這個地址(比如,ip和cs都會被壓棧)。 %stacksize small 這種形式也使用'bp'來進行基於棧的參數尋址,但它跟'large'不同,因爲 他假設bp的舊值已經被壓棧。換句話說,你假設bp,ip和cs正在棧頂,在它們 下面的所有本地空間已經被'ENTER'指令開闢好了。當和'%local'指令結合的 時候,這種形式特別有用。 4.9.3 `%local'指令。 '%local'指令用來簡化在棧框架中進行本地臨時棧變量的分配。C語言中的自 動本地變量是這種類型變量的一個例子。'%local'指令跟'%stacksize'一起 使用的時候特別有用。並和'%arg'指令保持兼容。它也允許簡化對於那些用 'ENTER'指令分配在棧中的變量的引用(關於ENTER指令,請參況B.4.65)。這 裏有一個關於它們的使用的例子: silly_swap: %push mycontext ; save the current context %stacksize small ; tell NASM to use bp %assign %$localsize 0 ; see text for explanation %local old_ax:word, old_dx:word enter %$localsize,0 ; see text for explanation mov [old_ax],ax ; swap ax & bx mov [old_dx],dx ; and swap dx & cx mov ax,bx mov dx,cx mov bx,[old_ax] mov cx,[old_dx] leave ; restore old bp ret ; %pop ; restore original context 變量'%$localsize'是在'%local'的內部使用,而且必須在'%local'指令使用前, 被定義在當前的上下文中。不這樣做,在每一個'%local'變量聲明的地方會引 發一個表達式語法錯誤。它然後可以用在一條適當的ENTER指令中。 4.10 其他的預處理指令。 NASM還有一些預處理指令允許從外部源中獲取信息,現在,他們包括: 下面的預處理指令使NASM能正確地自理C++/C語言預處理器的輸出。 (*) `%line' 使NASM能正確地自理C++/C語言預處理器的輸出。(參閱4.10.1) (*) `%!' 使NASM從一個環境變量中讀取信息,然後這些信息就可以在你的程序 中使用了。(4.10.2) 4.10.1 `%line'操作符。 '%line'操作符被用來通知NASM,輸入行與另一個文件中指定的行號相關。一般 這另一個文件會是一個源程序文件,它作爲現在NASM的輸入,但它是一個預處理 器的輸出。'%line'指令允許NASM輸出關於在這個源程序文件中的指定行號的信 息,而不是被NASM讀進來的整個文件。 這個預處理指令通常不會被程序員用到,但會讓預處理器的作者感興趣,'%line' 的使用方法如下: %line nnn[+mmm] [filename] 在這個指令中,'nnn'指定源程序文件中與之相關的特定行,'mmm'是一個可選的 參數,它指定一個行遞增的值;每一個被讀進來的源文件行被認爲與源程序文件 中的'mmm'行相關。最終,'filename'可選參數指定源程序文件的文件名。 在讀到一條'%line'預處理指令後,NASM會報告與指定的值相關的所有的文件名和 行號 4.10.2 `%!'`<env>': 讀取一個環境變量。 `%!<env>' 操作符可以在彙編時讀取一個環境變量的值,這可以用在一個環境變量 的內容保存到一個字符串中。該字符串可以用在你程序的其他地方。 比如,假設你有一個環境變量'FOO',你希望把'FOO'的值嵌入到你的程序中去。你可 以這樣做: %define FOO %!FOO %define quote ' tmpstr db quote FOO quote 在寫的時候,在定義'quote'時,它會產生一個'沒有結束的字符串'的警告信息, 它會自己在讀進來的字符串的前後加上一個空格。我沒有辦法找到一個簡單的 工作方式(儘管可以通過宏來創建),我認爲,你沒有必要學習創建更爲複雜 的宏,或者如果你用這種方式使用這個特性,你沒有必要使用額外的空間。 第五章: 彙編器指令。 ------------------------------- 儘管NASM極力避免MASN和TASM中的那些庸腫複雜的東西,但還是不得不支持少 量的指令,這些指令在本章進行描述。 NASM的指令有兩種類型:用戶級指令和原始指令。一般地,每一條指令都有一 個用戶級形式和原始形式。在大多數情況下,我們推薦用戶使用有戶級指令, 它們以宏的形式運行,並去調用原始形式的指令。 原始指令被包含在一個方括號中;用戶級指令沒有括號。 除了本章所描述的這些通用的指令,每一種目標文件格式爲了控制文件格式 的一些特性,可以使用一些另外的指令。這些格式相關的指令在第六章中跟 相關的文件格式一起進行介紹。 5.1 `BITS': 指定目標處理器模式。 'BITS'指令指定NASM產生的代碼是被設計運行在16位模式的處理器上還是運行 在32位模式的處理器上。語法是'BITS 16'或'BITS 32' 大多數情況下,你可能不需要顯式地指定'BITS'。'aout','coff','elf'和 'win32'目標文件格式都是被設計用在32位操作系統上的,它們會讓NASM缺 省選擇32位模式。而'obj'目標文件格式允許你爲每一個段指定'USE16'或 'USE32',然後NASM就會按你的指定設定操作模式,所以多次使用'BITS'是 沒有必要的。 最有可能使用'BITS'的場合是在一個純二進制文件中使用32位代碼;這是因 爲'bin'輸出格式在作爲DOS的'.COM'程序,DOS的'.SYS'設備驅動程序,或引 導程序時,默認都是16位模式。 如果你僅僅是爲了在16位的DOS程序中使用32位指令,你不必指定'BITS 32', 如果你這樣做了,彙編器反而會產生錯誤的代碼,因爲這樣它會產生運行在 16位模式下,卻以32位平臺爲目標的代碼。 當NASM在'BITS 16'狀態下時,使用32位數據的指令可以加一個字節的前綴 0x66,要使用32位的地址,可以加上0x67前綴。在'BITS 32'狀態下,相反的 情況成立,32位指令不需要前綴,而使用16位數據的指令需要0x66前綴,使 用16位地址的指令需要0x67前綴。 'BITS'指令擁有一個等效的原始形式:[BITS 16]和[BITS 32]。而用戶級的 形式只是一個僅僅調用原始形式的宏。 5.1.1 `USE16' & `USE32': BITS的別名。 'USE16'和'USE32'指令可以用來取代'BITS 16'和'BITS 32',這是爲了和其他 彙編器保持兼容性。 5.2 `SECTION'或`SEGMENT': 改變和定義段。 'SECTION'指令('SEGMENT'跟它完全等效)改變你正編寫的代碼將被彙編進的段。 在某些目標文件格式中,段的數量與名稱是確定的;而在別一些格式中,用戶 可以建立任意多的段。因此,如果你企圖切換到一個不存在的段,'SECTION'有 時可能會給出錯誤信息,或者定義出一個新段, Unix的目標文件格式和'bin'目標文件格式,都支持標準的段'.text','.data' 和'bss'段,與之不同的的,'obj'格式不能辯識上面的段名,並需要把段名開 頭的句點去掉。 5.2.1 宏 `__SECT__' 'SECTION'指令跟一般指令有所不同,的用戶級形式跟它的原始形式在功能上有 所不同,原始形式[SECTION xyz],簡單地切換到給出的目標段。用戶級形式, 'SECTION xyz'先定義一個單行宏'__SECT__',定義爲原始形式[SECTION],這正 是要執行的指令,然後執行它。所以,用戶級指令: SECTION .text 被展開成兩行: %define __SECT__ [SECTION .text] [SECTION .text] 用戶會發現在他們自己的宏中,這是非常有用的。比如,4.3.3中定義的宏 'writefile'以下面的更爲精緻的寫法會更有用: %macro writefile 2+ [section .data] %%str: db %2 %%endstr: __SECT__ mov dx,%%str mov cx,%%endstr-%%str mov bx,%1 mov ah,0x40 int 0x21 %endmacro 這個形式的宏,一次傳遞一個用出輸出的字符串,先用原始形式的'SECTION'切 換至臨時的數據段,這樣就不會破會宏'__SECT__'。然後它把它的字符串聲明在 數據段中,然後調用'__SECT__'切換加用戶先前所在的段。這樣就可以避免先前 版本的'writefile'宏中的用來跳過數據的'JMP'指令,而且在一個更爲複雜的格 式模型中也不會失敗,用戶可以把這個宏放在任何獨立的代碼段中進行彙編。 5.3 `ABSOLUTE': 定義絕對labels。 'ABSOLUTE'操作符可以被認爲是'SECTION'的另一種形式:它會讓接下來的代碼不 在任何的物理段中,而是在一個從給定地址開始的假想段中。在這種模式中,你 唯一能使用的指令是'RESB'類指令。 `ABSOLUTE'可以象下面這樣使用: absolute 0x1A kbuf_chr resw 1 kbuf_free resw 1 kbuf resw 16 這個例子描述了一個關於在段地址0x40處的PC BIOS數據域的段,上面的代碼把 'kbuf_chr'定義在0x1A處,'kbuf_free'定義在地址0x1C處,'kbuf'定義在地址 0x1E。 就像'SECTION'一樣,用戶級的'ABSOLUTE'在執行時會重定義'__SECT__'宏。 'STRUC'和'ENDSTRUC'被定義成使用'ABSOLUTE'的宏(同時也使用了'__SECT__') 'ABSOLUTE'不一定需要帶有一個絕對常量作爲參數:它也可以帶有一個表達式( 實際上是一個臨界表達式,參閱3.8),表達式的值可以是在一個段中。比如,一 個TSR程序可以在用它重用它的設置代碼所佔的空間: org 100h ; it's a .COM program jmp setup ; setup code comes last ; the resident part of the TSR goes here setup: ; now write the code that installs the TSR here absolute setup runtimevar1 resw 1 runtimevar2 resd 20 tsr_end: 這會在setup段的開始處定義一些變量,所以,在setup運行完後,它所佔用的內存 空間可以被作爲TSR的數據存儲空莘而得到重用。符號'tsr_end'可以用來計算TSR 程序所需佔用空間的大小。 5.4 `EXTERN': 從其他的模塊中導入符中。 'EXTERN'跟MASM的操作符'EXTRN',C的關鍵字'extern'極其相似:它被用來聲明一 個符號,這個符號在當前模塊中沒有被定義,但被認爲是定義在其他的模塊中,但 需要在當前模塊中對它引用。不是所有的目標文件格式都支持外部變量的:'bin'文 件格式就不行。 'EXTERN'操作符可以帶有任意多個參數,每一個都是一個符號名: extern _printf extern _sscanf,_fscanf 有些目標文件格式爲'EXTERN'提供了額外的特性。在所有情況下,要使用這些額外 特性,必須在符號名後面加一個冒號,然後跟上目標文件格式相關的一些文字。比如 'obj'文件格式允許你聲明一個以外部組'dgroup'爲段基址一個變量,可以象下面這樣 寫: extern _variable:wrt dgroup 原始形式的'EXTERN'跟用戶級的形式有所不同,因爲它只能帶有一個參數:對於多個參 數的支持是在預處理器級上的特性。 你可以把同一個變量作爲'EXTERN'聲明多次:NASM會忽略掉第二次和後來聲明的,只採 用第一個。但你不能象聲明其他變量一樣聲明一個'EXTERN'變量。 5.5 `GLOBAL': 把符號導出到其他模塊中。 'GLOBAL'是'EXTERN'的對立面:如果一個模塊聲明一個'EXTERN'的符號,然後引用它, 然後爲了防止鏈接錯誤,另外某一個模塊必須確實定義了該符號,然後把它聲明爲 'GLOBAL',有些彙編器使用名字'PUBLIC'。 'GLOBAL'操作符所作用的符號必須在'GLOBAL'之後進行定義。 'GLOBAL'使用跟'EXTERN'相同的語法,除了它所引用的符號必須在同一樣模塊中已經被 定義過了,比如: global _main _main: ; some code 就像'EXTERN'一樣,'GLOBAL'允許目標格式文件通過冒號定義它們自己的擴展。比如 'elf'目標文件格式可以讓你指定全局數據是函數或數據。 global hashlookup:function, hashtable:data 就象'EXTERN'一樣,原始形式的'GLOBAL'跟用戶級的形式不同,僅能一次帶有一個參 數 5.6 `COMMON': 定義通用數據域。 'COMMON'操作符被用來聲明通用變量。一個通用變量很象一個在非初始化數據段中定義 的全局變量。所以: common intvar 4 功能上跟下面的代碼相似: global intvar section .bss intvar resd 1 不同點是如果多於一個的模塊定義了相同的通用變量,在鏈接時,這些通用變量會被 合併,然後,所有模塊中的所有的對'intvar'的引用會指向同一片內存。 就角'GLOBAL'和'EXTERN','COMMON'支持目標文件特定的擴展。比如,'obj'文件格式 允許通用變量爲NEAR或FAR,而'elf'格式允許你指定通用變量的對齊需要。 common commvar 4:near ; works in OBJ common intarray 100:4 ; works in ELF: 4 byte aligned 它的原始形式也只能帶有一個參數。 5.7 `CPU': 定義CPU相關。 'CPU'指令限制只能運行特定CPU類型上的指令。 選項如下: (*) `CPU 8086' 只彙編8086的指令集。 (*) `CPU 186' 彙編80186及其以下的指令集。 (*) `CPU 286' 彙編80286及其以下的指令集。 (*) `CPU 386' 彙編80386及其以下的指令集。 (*) `CPU 486' 486指令集。 (*) `CPU 586' Pentium指令集。 (*) `CPU PENTIUM' 同586。 (*) `CPU 686' P6指令集。 (*) `CPU PPRO' 同686 (*) `CPU P2' 同686 (*) `CPU P3' Pentium III and Katmai指令集。 (*) `CPU KATMAI' 同P3 (*) `CPU P4' Pentium 4 (Willamette)指令集 (*) `CPU WILLAMETTE' 同P4 (*) `CPU IA64' IA64 CPU (x86模式下)指令集 所有選項都是大小寫不敏感的,在指定CPU或更低一級CPU上的所有指令都會 被選擇。缺省情況下,所有指令都是可用的。 第六章: 輸出文件的格式。 ------------------------- NASM是一個可移植的彙編器,它被設計爲可以在任何ANSI C編譯器支持的平臺 上被編譯,並可以產生在各種intel x86系例的操作系統上運行的代碼。爲了 做到這一點,它擁有大量的可用的輸出文件格式,使用命令行上的選項'-f' 可以選擇。每一種格式對於NASM的語法都有一定的擴展,關於這部分內容, 本章將詳細介紹。 就象在2.1.1中所描述的,NASM基於輸入文件的名字和你選擇的輸出文件的格 式爲你的輸出文件選擇一個缺省的名字。這是通過去掉源文件的擴展名('.asm 或'.s'或者其他你使用的擴展名),然後代之以一個由輸出文件格式決定的擴 展名。這些輸出格式相關的擴展名會在下面一一給出。 6.1 `bin': 純二進制格式輸出。 'bin'格式不產生目標文件:除了你編寫的那些代碼,它不在輸出文件中產生 任何東西。這種純二進制格式的文件可以用在MS-DOS中:'.COM'可執行文件 和'.SYS'設備驅動程序就是純二進制格式的。純二進制格式輸出對於操作系 統和引導程序開發也是很有用的。 'bin'格式支持多個段名。關於NASM處理'bin'格式中的段的細節,請參閱 6.1.3。 使用'bin'格式會讓NASM進入缺省的16位模式 (參閱5.1)。爲了能在'bin'格 式中使用32位代碼,比如在操作系統的內核代碼中。你必須顯式地使用 'BITS 32'操作符。 'bin'沒有缺省的輸出文件擴展名:它只是把輸入文件的擴展名去掉後作爲 輸出文件的名字。這樣,NASM在缺省模式下會把'binprog.asm'彙編成二進 制文件'binprog'。 6.1.1 `ORG': 二進制程序的起點位置。 'bin'格式提供一個額外的操作符,這在第五章已經給出'ORG'.'ORG'的功能 是指定程序被載入內存時,它的起始地址。 比如,下面的代碼會產生longword: `0x00000104': org 0x100 dd label label: 跟MASM兼容彙編器提供的'ORG'操作符不同,它們允許你在目標文件中跳轉, 並覆蓋掉你已經產生的代碼,而NASM的'ORG'就象它的字面意思“起點”所 表示的,它的功能就是爲所有內部的地址引用增加一個段內偏移值;它不允 許MASM版本的'org'的任何其他功能。 6.1.2 `bin'對`SECTION'操作符的擴展。 'bin'輸出格式擴展了'SECTION'(或者'SEGMENT')操作符,允許你指定段的 對齊請求。這是通過在段定義行的後面加上'ALIGN'限定符實現的。比如: section .data align=16 它切換到段'.data',並指定它必須對齊到16字節邊界。 'ALIGN'的參數指定了地址值的低位有多少位必須爲零。這個對齊值必須爲 2的冪。 6.1.3 `Multisection' 支持BIN格式. 'bin'格式允許使用多個段,這些段以一些特定的規則進行排列。 (*) 任何在一個顯式的'SECTION'操作符之前的代碼都被缺省地加到'.text' 段中。 (*) 如果'.text'段中沒有給出'ORG'語句,它被缺省地賦爲'ORG 0'。 (*) 顯式地或隱式地含有'ORG'語句的段會以'ORG'指定的方式存放。代碼 前會填充0,以在輸出文件中滿足org指定的偏移。 (*) 如果一個段內含有多個'ORG'語句,最後一條'ORG'語句會被運用到整 個段中,不會影響段內多個部分以一定的順序放到一起。 (*) 沒有'ORG'的段會被放到有'ORG'的段的後面,然後,它們就以第一次聲 明時的順序被存放。 (*) '.data'段不像'.text'段和'.bss'段那樣,它不遵循任何規則, (*) '.bss'段會被放在所有其他段的後面。 (*) 除非一個更高級別的對齊被指定,所有的段都被對齊到雙字邊界。 (*) 段之間不可以交迭。 6.2 `obj': 微軟OMF目標文件 'obj'文件格式(因爲歷史的原因,NASM叫它'obj'而不是'omf')是MASM和 TASM可以產生的一種格式,它是標準的提供給16位的DOS鏈接器用來產生 '.EXE'文件的格式。它也是OS/2使用的格式。 'obj'提供一個缺省的輸出文件擴展名'.obj'。 'obj'不是一個專門的16位格式,NASM有一個完整的支持,可以有它的32位 擴展。32位obj格式的文件是專門給Borland的Win32編譯器使用的,這個編 譯器不使用微軟的新的'win32'目標文件格式。 'obj'格式沒有定義特定的段名字:你可以把你的段定義成任何你喜歡 的名字。一般的,obj格式的文件中的段名如:`CODE', `DATA'和`BSS'. 如果你的源文件在顯式的'SEGMENT'前包含有代碼,NASM會爲你創建一個叫 做`__NASMDEFSEG'的段以包含這些代碼. 當你在obj文件中定義了一個段,NASM把段名定義爲一個符號,所以你可以 存取這個段的段地址。比如: segment data dvar: dw 1234 segment code function: mov ax,data ; get segment address of data mov ds,ax ; and move it into DS inc word [dvar] ; now this reference will work ret obj格式中也可以使用'SEG'和'WRT'操作符,所以你可以象下面這樣編寫代碼: extern foo mov ax,seg foo ; get preferred segment of foo mov ds,ax mov ax,data ; a different segment mov es,ax mov ax,[ds:foo] ; this accesses `foo' mov [es:foo wrt data],bx ; so does this 6.2.1 `obj' 對`SEGMENT'操作符的擴展。 obj輸出格式擴展了'SEGMENT'(或'SECTION')操作符,允許你指定段的多個 屬性。這是通過在段定義行的末尾添加額外的限定符來實現的,比如: segment code private align=16 這定義了一個段'code',但同時把它聲明爲一個私有段,同時,它所描述的 這個部分必須被對齊到16字節邊界。 可用的限定符如下: (*) `PRIVATE', `PUBLIC', `COMMON'和`STACK' 指定段的聯合特徵。`PRIVATE' 段在連接時不和其他的段進行連接;'PUBLIC'和'STACK'段都會在連接時 連接到一塊兒;而'COMMON‘段都會在同一個地址相互覆蓋,而不會一接一 個連接好。 (*) 就象上面所描述的,'ALIGN'是用來指定段基址的低位有多少位必須爲零, 對齊的值必須以2的乘方的形式給出,從1到4096;實際上,真正被支持的 值只有1,2,4,16,256和4096,所以如果你指定了8,它會自動向上對齊 到16,32,64會對齊到128等等。注意,對齊到4096字節的邊界是這種格式 的PharLap擴展,可能所有的連接器都不支持。 (*) 'CLASS'可以用來指定段的類型;這個特性告訴連接器,具有相同class的 段應該在輸出文件中被放到相近的地址。class的名字可以是任何字。比如 'CLASS=CODE'。 (*) 就象`CLASS', `OVERLAY'通過一個作爲參數的字來指定,爲那些有覆蓋能力 的連接器提供覆蓋信息。 (*) 段可以被聲明爲'USE16'或'USE32',這種選擇會對目標文件產生影響,同時 在段內16位或32位代碼分開的時候,也能保證NASM的缺省彙編模式 (*) 當編寫OS/2目標文件的時候,你應當把32位的段聲明爲'FLAT',它會使缺省 的段基址進入一個特殊的組'FLAT',同時,在這個組不存在的時候,定義這 個組。 (*) obj文件格式也允許段在聲明的時候,前面有一個定義的絕對段地址,儘管沒 有連接器知道這個特性應該怎麼使用;但如果你需要的話,NASM還是允許你 聲明一個段如下面形式:`SEGMENT SCREEN ABSOLUTE=0xB800'`ABSOLUTE'和 `ALIGN'關鍵字是互斥的。 NASM的缺省段屬性是`PUBLIC', `ALIGN=1', 沒有class,沒有覆蓋, 並 `USE16'. 6.2.2 `GROUP': 定義段組。 obj格式也允許段被分組,所以一個單獨的段寄存器可以被用來引用一個組中的 所有段。NASM因此提供了'GROUP'操作符,據此,你可以這樣寫代碼: segment data ; some data segment bss ; some uninitialised data group dgroup data bss 這會定義一個叫做'dgroup'的組,包含有段'data'和'bss'。就象'SEGMENT', 'GROUP'會把組名定義爲一個符號,所以你可以使用'var wrt data'或者'var wrt dgroup'來引用'data'段中的變量'var',具體用哪一個取決於哪一個段 值在你的當前段寄存器中。 如果你只是想引用'var',同時,'var'被聲明在一個段中,段本身是作爲一個 組的一部分,然後,NASM缺省給你的'var'的偏移值是從組的基地址開始的, 而不是段基址。所以,'SEG var'會返回組基址而不是段基址。 NASM也允許一個段同時作爲多個組的一個部分,但如果你真這樣做了,會產生 一個警告信息。段內同時屬於多個組的那些變量在缺省狀況下會屬於第一個被 聲明的包含它的組。 一個組也不一定要包含有段;你還是可以使用'WRT'引用一個不在組中的變量。 比如說,OS/2定義了一個特殊的組'FLAT',它不包含段。 6.2.3 `UPPERCASE': 在輸出文件中使大小寫敏感無效。 儘管NASM自己是大小寫敏感的,有些OMF連接器並不大小寫敏感;所以,如果 NASM能輸出大小寫單一的目標文件會很有用。'UPPERCASE'操作符讓所有的寫 入到目標文件中的組,段,符號名全部強制爲大寫。在一個源文件中,NASM 還是大小寫敏感的;但目標文件可以按要求被整個寫成是大寫的。 'UPPERCASE'寫在單獨一行中,不需要任何參數。 6.2.4 `IMPORT': 導入DLL符號。 如果你正在用NASM寫一個DLL導入庫,'IMPORT'操作符可以定義一個從DLL庫中 導入的符號,你使用'IMPORT'操作符的時候,你仍舊需要把符號聲明爲'EXTERN'. 'IMPORT'操作符需要兩個參數,以空格分隔,它們分別是你希望導入的符號的名 稱和你希望導入的符號所在的庫的名稱,比如: import WSAStartup wsock32.dll 第三個參數是可選的,它是符號在你希望從中導入的鏈接庫中的名字,這樣的話, 你導入到你的代碼中的符號可以和庫中的符號不同名,比如: import asyncsel wsock32.dll WSAAsyncSelect 6.2.5 `EXPORT': 導出DLL符號. 'EXPORT'也是一個目標格式相關的操作符,它定義一個全局符號,這個符號可以被 作爲一個DLL符號被導出,如果你用NASM寫一個DLL庫.你可以使用這個操作符,在 使用中,你仍舊需要把符號定義爲'GLOBAL'. 'EXPORT'帶有一個參數,它是你希望導出的在源文件中定義的符號的名字.第二個 參數是可選的(跟第一個這間以空格分隔),它給出符號的外部名字,即你希望讓使 用這個DLL的應用程序引用這個符號時所用的名字.如果這個名字跟內部名字同名, 可以不使用第二個參數. 還有一些附加的參數,可以用來定義導出符號的一些屬性.就像第二個參數, 這些 參數也是以空格分隔.如果要給出這些參數,那麼外部名字也必須被指定,即使它跟 內部名字相同也不能省略,可用的屬性如下: (*) 'resident'表示某個導出符號在系統引導後一直常駐內存.這對於一些經常使用 的導出符號來說,是很有用的. (*) `nodata'表示導出符號是一個函數,這個函數不使用任何已經初始化過的數據. (*) `parm=NNN', 這裏'NNN'是一個整型數,當符號是一個在32位段與16位段之間的 調用門時,它用來設置參數的尺寸大小(佔用多少個wrod). (*) 還有一個屬性,它僅僅是一個數字,表示符號被導出時帶有一個標識數字. 比如: export myfunc export myfunc TheRealMoreformalLookingFunctionName export myfunc myfunc 1234 ; export by ordinal export myfunc myfunc resident parm=23 nodata 6.2.6 `..start': 定義程序的入口點. 'OMF'鏈接器要求被鏈接進來的所有目標文件中,必須有且只能有一個程序入口點, 當程序被運行時,就從這個入口點開始.如果定義這個入口點的目標文件是用 NASM彙編的,你可以通過在你希望的地方聲明符號'..start'來指定入口點. 6.2.7 `obj'對`EXTERN'操作符的擴展. 如果你以下面的方式聲明瞭一個外部符號: extern foo 然後以這樣的方式引用'mov ax,foo',這樣只會得到一個關於foo的偏移地址,而且 這個偏移地址是以'foo'的首選段基址爲參考的(在'foo'被定義的這個模塊中指定 的段).所以,爲了存取'foo'的內容,你實際上需要這樣做: mov ax,seg foo ; get preferred segment base mov es,ax ; move it into ES mov ax,[es:foo] ; and use offset `foo' from it 這種方式顯得稍稍有點笨拙,實際上如果你知道一個外部符號可以通過給定的段 或組來進行存的話,假定組'dgroup'已經在DS寄存器中,你可以這樣寫代碼: mov ax,[foo wrt dgroup] 但是,如果你每次要存取'foo'的時候,都要打這麼多字是一件很痛苦的事情;所以 NASM允許你聲明'foo'的另一種形式: extern foo:wrt dgroup 這種形式讓NASM假定'foo'的首選段基址是'dgroup';所以,表達式'seg foo'現在會 返回'dgroup',表達式'foo'等同於'foo wrt dgroup'. 缺省的'WRT'機制可以用來讓外部符號跟你程序中的任何段或組相關聯.他也可以被 運用到通用變量上,參閱6.2.8. 6.2.8 `obj'對`COMMON'操作符的擴展. 'obj'格式允許通用變量爲near或far;NASM允許你指定你的變量屬於哪一類,語法如 下: common nearvar 2:near ; `nearvar' is a near common common farvar 10:far ; and `farvar' is far Far通用變量可能會大於64Kb,所以OMF可以把它們聲明爲一定數量的指定的大小的元 素.比如,10byte的far通用變量可以被聲明爲10個1byte的元素,5個2byte的元素,或 2個5byte的元素,或1個10byte的元素. 有些'OMF'鏈接器需要元素的size,同時需要變量的size,當在多個模塊中聲明通用變 量時可以用來進行匹配.所以NASM必須允許你在你的far通用變量中指定元素的size. 這可以通過下面的語法實現: common c_5by2 10:far 5 ; two five-byte elements common c_2by5 10:far 2 ; five two-byte elements 如果元素的size沒有被指定,缺省值是1.還有,如果元素size被指定了,那麼'far'關鍵 字就不需要了,因爲只有far通用變量是有元素size的.所以上面的聲明等同於: common c_5by2 10:5 ; two five-byte elements common c_2by5 10:2 ; five two-byte elements 這種擴展的特性還有,'obj'中的'COMMON'操作符還可以象'EXTERN'那樣支持缺省的 'WRT'指定,你也可以這樣聲明: common foo 10:wrt dgroup common bar 16:far 2:wrt data common baz 24:wrt data:6 6.3 `win32': 微軟Win32目標文件 'win32'輸出格式產生微軟win32目標文件,可以用來給微軟連接器進行連接,比如 Visual C++.注意Borland Win32編譯器不使用這種格式,而是使用'obj'格式(參閱 6.2) 'win32'提供缺省的輸出文件擴展名'.obj'. 注意,儘管微軟聲稱Win32目標文件遵循'COFF'標準(通用目標文件格式),但是微軟 的Win32編譯器產生的目標文件和一些COFF連接器(比如DJGPP)並不兼容,反過來也 一樣.這是由一些PC相關的語義上的差異造成的. 使用NASM的'coff'輸出格式,可以 產生能讓DJGPP使用的COFF文件; 而這種'coff'格式不能產生能讓Win32連接器正確 使用的代碼. 6.3.1 `win32'對`SECTION'的擴展. 就象'obj'格式,'win32'允許你在'SECTION'操作符的行上指定附加的信息,以用來控 制你聲明的段的類型與屬性.對於標準的段名'.text','.data',和'.bss',類型和屬 性是由NASM自動產生的,但是還是可以通過一些限定符來重新指定: 可用的限定符如下: (*) 'code'或者'text',把一個段定義爲一個代碼段,這讓這個段可讀並可執行,但是 不能寫,同時也告訴連接器,段的類型是代碼段. (*) 'data'和'bss'定義一個數據段,類似'code',數據段被標識爲可讀可寫,但不可執 行,'data'定義一個被初始化過的數據段,'bss'定義一個未初始化的數據段. (*) 'rdata'聲明一個初始化的數據段,它可讀,但不能寫.微軟的編譯器把它作爲一 個存放常量的地方. (*) 'info'定義一個信息段,它不會被連接器放到可執行文件中去,但可以傳遞一些 信息給連接器.比如,定義一個叫做'.drectve'信息段會讓連接器把這個段內的 內容解釋爲命令行選項. (*) 'align='跟上一個數字,就象在'obj'格式中一樣,給出段的對齊請求.你最大可 以指定64:Win32目標文件格式沒有更大的段對齊值.如果對齊請求沒有被顯式 指定,缺省情況下,對於代碼段,是16byte對齊,對於只讀數據段,是8byte對齊,對 於數據段,是4byte對齊.而信息段缺省對齊是1byte(即沒有對齊),所以對它來說, 指定的數值沒用. 如果你沒有指定上述的限定符,NASM的缺省假設是: section .text code align=16 section .data data align=4 section .rdata rdata align=8 section .bss bss align=4 任何其的段名都會跟'.text'一樣被對待. 6.4 `coff': 通用目標文件格式. 'coff'輸出類型產生'COFF'目標文件,可以被DJGPP用來連接. 'coff'提供一個缺省的輸出文件擴展名'.o'. 'coff'格式支持跟'win32'同樣的對於'SECTION'的擴展,除了'align'和'info'限 定符不被支持. 6.5 `elf': 可執行可連接格式目標文件. 'elf'輸出格式產生'ELF32'(可執行可連接格式)目標文件,這種格式用在Linux, Unix System V中,包括Solaris x86, UnixWare和SCO Unix. 'elf'提供一個缺 省的輸出文件擴展名'.o'. 6.5.1 `elf'對`SECTION'操作符的擴展. 就象'obj'格式一樣,'elf'允許你在'SECTION'操作符行上指定附加的信息,以控制 你聲明的段的類型與屬性.對於標準的段名'.text','.data','.bss',NASM都會產 生缺省的類型與屬性.但還是可以通過一些限定符與重新指定. 可用的限定符如下: (*) 'alloc'定義一個段,在程序運行時,這個段必須被載入內存中,'noalloc'正好 相反,比如信息段,或註釋段. (*) 'exec'把段定義爲在程序運行的時候必須有執行權限.'noexec'正好相反. (*) `write'把段定義爲在程序運行時必須可寫,'nowrite'正好相反. (*) `progbits'把段定義爲在目標文件中必須有實際的內容,比如象普通的代碼段 與數據段,'nobits'正好相反,比如'bss'段. (*) `align='跟上一個數字,給出段的對齊請求. 如果你沒有指定上述的限定符信息,NASM缺省指定的如下: section .text progbits alloc exec nowrite align=16 section .rodata progbits alloc noexec nowrite align=4 section .data progbits alloc noexec write align=4 section .bss nobits alloc noexec write align=4 section other progbits alloc noexec nowrite align=1 (任何不在上述列舉範圍內的段,在缺省狀況下,都被作爲'other'段看待). 6.5.2 地址無關代碼: `elf'特定的符號和 `WRT' 'ELF'規範含有足夠的特性以允許寫地址無關(PIC)的代碼,這可以讓ELF非常 方便地共享庫.儘管如此,這也意味着NASM如果想要成爲一個能夠寫PIC的匯 編器的話,必須能夠在ELF目標文件中產生各種奇怪的重定位信息, 因爲'ELF'不支持基於段的地址引用,'WRT'操作符不象它的常規方式那樣被 使用,所以,NASM的'elf'輸出格式中,對於'WRT'有特殊的使用目的,叫做: PIC相關的重定位類型. 'elf'定義五個特殊的符號,它們可以被放在'WRT'操作符的右邊用來實現PIC 重定位類型.它們是`..gotpc', `..gotoff', `..got', `..plt' and `..sym'. 它們的功能簡要介紹如下: (*) 使用'wrt ..gotpc'來引用以global offset table爲基址的符號會得到 當前段的起始地址到global offset table的距離.( `_GLOBAL_OFFSET_TABLE_'是引用GOT的標準符號名).所以你需要在返回 結果前面加上'$$'來得到GOT的真實地址. (*) 用'wrt ..gotoff'來得到你的某一個段中的一個地址實際上得到從GOT的 的起始地址到你指定的地址之間的距離,所以這個值再加上GOT的地址爲得 到你需要的那個真實地址. (*) 使用'wrt ..got'來得到一個外部符號或全局符號會讓連接器在含有這個 符號的地址的GOT中建立一個入口,這個引用會給出從GOT的起始地址到這 個入口的一個距離;所以你可以加上GOT的地址,然後從得到的地址處載入, 就會得到這個符號的真實地址. (*) 使用'wrt ..plt'來引用一個過程名會讓連接器建立一個過程連接表入口, 這個引用會給出PLT入口的地址.你可以在上下文使用這個引用,它會產生 PC相關的重定位信息,所以,ELF包含引用PLT入口的非重定位類型 (*) 略 在8.2章中會有一個更詳細的關於如何使用這些重定位類型寫共享庫的介紹 6.5.3 `elf'對`GLOBAL'操作符的擴展. 'ELF'目標文件可以包含關於一個全局符號的很多信息,不僅僅是一個地址:他 可以包含符號的size,和它的類型.這不僅僅是爲了調試的方便,而且在寫共享 庫程序的時候,這確實是非常有用的.所以,NASM支持一些關於'GLOBAL'操作符 的擴展,允許你指定這些特性. 你可以把一個全局符號指定爲一個函數或一個數據對象,這是通過在名字後面 加上一個冒號跟上'function'或'data'實現的.('object'可以用來代替'data') 比如: global hashlookup:function, hashtable:data 把全局符號'hashlookup'指定爲一個函數,把'hashtable'指定爲一個數據對象. 你也可以指定跟這個符號關聯的數據的size,可以一個數值表達式(它可以包含 labels,甚至前向引用)跟在類型後面,比如: global hashtable:data (hashtable.end - hashtable) hashtable: db this,that,theother ; some data here .end: 這讓NASM自動計算表的長度,然後把信息放進'ELF'的符號表中. 聲明全局符號的類型和size在寫共享庫代碼的時候是必須的,關於這方面的更多 信息,參閱8.2.4. 6.5.4 `elf'對`COMMON'操作符的擴展. 'ELF'也允許你指定通用變量的對齊請求.這是通過在通用變量的名字和size的 後面加上一個以冒號分隔的數字來實現的,比如,一個doubleword的數組以 4byte對齊比較好: common dwordarray 128:4 這把array總的size聲明爲128bytes,並確定它對齊到4byte邊界. 6.5.5 16位代碼和ELF 'ELF32'規格不提供關於8位和16位值的重定位,但GNU的連接器'ld'把這作爲 一個擴展加進去了.NASM可以產生GNU兼容的重定位,允許16位代碼被'ld'以 'ELF'格式進行連接.如果NASM使用了選項'-w+gnu-elf-extensions',如果一 個重定位被產生的話,會有一條警告信息. 6.6 `aout': Linux `a.out' 目標文件 'aout'格式產生'a.out'目標文件,這種格式在早期的Linux系統中使用(現在的 Linux系統一般使用ELF格式,參閱6.5),這種格式跟其他的'a.out'目標文件有 所不同,文件的頭四個字節的魔數不一樣;還有,有些版本的'a.out',比如NetBSD 的,支持地址無關代碼,這一點,Linux的不支持. 'a.out'提供的缺省文件擴展名是'.o'. 'a.out'是一種非常簡單的目標文件格式.它不支持任何特殊的操作符,沒有特殊 的符號,不使用'SEG'或'WRT',對於標準的操作符也沒有任何擴展.它只支持三個 標準的段名'.text','.data','.bss'. 6.7 `aoutb': NetBSD/FreeBSD/OpenBSD `a.out'目標文件. 'aoutb'格式產生在BSD unix,NetBSD,FreeBSD,OpenBSD系統上使用的'a.out'目 標文件. 作爲一種簡單的目標文件,這種格式跟'aout'除了開頭四字節的魔數不 一樣,其他完全相同.但是,'aoutb'格式支持跟elf格式一樣的地址無關代碼,所以 你可以使用它來寫'BSD'共享庫. 'aoutb'提供的缺省文件擴展名是'.o'. 'aoutb'不支持特殊的操作符,沒有特殊的符號,只有三個殊殊的段名'.text', '.data'和'.bss'.但是,它象elf一樣支持'WRT'的使用,這是爲了提供地址無關的 代碼重定位類型.關於這部分的完整文檔,請參閱6.5.2 'aoutb'也支持跟'elf'同樣的對於'GLOBAL'的擴展:詳細信息請參閱6.5.3. 6.8 `as86': Minix/Linux `as86'目標文件. Minix/Linux 16位彙編器'as86'有它自己的非標準目標文件格式. 雖然它的鏈 接器'ld86'產生跟普通的'a.out'非常相似的二進制輸出,在'as86'跟'ld86'之 間使用的目標文件格式並不是'a.out'. NASM支持這種格式,因爲它是有用的,'as86'提供的缺省的輸出文件擴展名是'.o' 'as86'是一個非常簡單的目標格式(從NASM用戶的角度來看).它不支持任何特殊 的操作符,符號,不使用'SEG'或'WRT',對所有的標準操作符也沒有任何擴展.它只 支持三個標準的段名:'.text','.data',和'.bss'. 6.9 `rdf': 可重定位的動態目標文件格式. 'rdf'輸出格式產生'RDOFF'目標文件.'RDOFF'(可重定位的動態目標文件格式) `RDOFF'是NASM自產的目標文件格式,是NASM自已設計的,它被反映在彙編器的內 部結構中. 'RDOFF'在所有知名的操作系統中都沒有得到應用.但是,那些正在寫他們自己的 操作系統的人可能非常希望使用'RDOFF'作爲他們自己的目標文件格式,因爲 'RDOFF'被設計得非常簡單,並含有很少的冗餘文件頭信息. NASM的含有源代碼的Unix包和DOS包中都含有一個'rdoff'子目錄,裏面有一套 RDOFF工具:一個RDF連接器,一個RDF靜態庫管理器,一個RDF文件dump工具,還有 一個程序可以用來在Linux下載入和執行RDF程序. 'rdf'只支持標準的段名'.text','.data','.bss'. 6.9.1 需要一個庫: `LIBRARY'操作符. 'RDOFF'擁有一種機制,讓一個目標文件請求一個指定的庫被連接進模塊中,可以 是在載入時,也可以是在運行時連接進來.這是通過'LIBRARY'操作符完成的,它帶 有一個參數,即這個庫的名字: library mylib.rdl 6.9.2 指定一個模塊名稱: `MODULE'操作符. 特定的'RDOFF'頭記錄被用來存儲模塊的名字.它可以被用在運行時載入器作動 態連接.'MODULE'操作符帶有一個參數,即當前模塊的名字: module mymodname 注意,當你靜態連接一個模塊,並告訴連接器從輸出文件中除去符號時,所有的模 塊名字也會被除去.爲了避免這種情況,你應當在模塊的名字前加一個'$',就像: module $kernel.core 6.9.3 `rdf'對`GLOBAL'操作符的擴展. 'RDOFF'全局符號可以包含靜態連接器需要的額外信息.你可以把一個全局符號 標識爲導出的,這就告訴連接器不要把它從目標可執行文件中或庫文件中除去. 就象在'ELF'中一樣,你也可以指定一個導出符號是一個過程或是一個數據對象. 在名字的尾部加上一個冒號和'exporg',你就可以讓一個符號被導出: global sys_open:export 要指定一個導出符號是一個過程(函數),你要在聲明的後南加上'proc'或'function' global sys_open:export proc 相似的,要指定一個導出的數據對象,把'data'或'object'加到操作符的後面: global kernel_ticks:export data 6.10 `dbg': 調試格式. 在缺省配置下,'dbg'輸出格式不會被構建進NASM中.如果你是從源代碼開始構建你 自己的NASM可執行版本,你可以在'outform.h'中定義'OF_DBG'或在編譯器的命令 行上定義,這樣就可以得到'dbg'輸出格式. 'dbg'格式不輸出一個普通的目標文件;它輸出一個文本文件,包含有一個關於到輸 出格式的最終模塊的轉化動作的列表.它主要是用於幫助那些希望寫自己的驅動程 序的用戶,這樣他們就可以得到一個關於主程序的各種請求在輸出中的形式的完整 印象. 對於簡單的文件,可以簡單地象下面這樣使用: nasm -f dbg filename.asm 這會產生一個叫做'filename.dgb'的診斷文件.但是,這在另一些目標文件上可能工 作得並不是很好,因爲每一個目標文件定義了它自己的宏(通常是用戶級形式的操作 符),而這些宏在'dbg'格式中並沒有定義.因此,運行NASM兩遍是非常有用的,這是爲 了對選定的源目標文件作一個預處理: nasm -e -f rdf -o rdfprog.i rdfprog.asm nasm -a -f dbg rdfprog.i 這先把'rdfprog.asm先預處理成'rdfprog.i',讓RDF特定的操作符被正確的轉化成 原始形式.然後,被預處理過的源程序被交給'dbg'格式去產生最終的診斷輸出. 這種方式對於'obj'格式還是不能正確工作的,因爲'obj'的'SEGMENT'和'GROUP'操 作符在把段名與組名定義爲符號的時候會有副作用;所以程序不會被彙編.如果你 確實需要trace一個obj的源文件,你必須自己定義符號(比如使用'EXTERN') 'dbg'接受所有的段名與操作符,並把它們全部記錄在自己的輸出文件中. 第七章: 編寫16位代碼 (DOS, Windows 3/3.1) --------------------------------------------------- 本章將介紹一些在編寫運行在'MS-DOS'和'Windows 3.x'下的16位代碼的時候需要 用到的一些常見的知識.涵獸瞭如果連接程序以生成.exe或.com文件,如果編寫 .sys設備驅動程序,以及16位的彙編語言代碼與C編譯器和Borland Pascal編譯器 之間的編程接口. 7.1 產生'.EXE'文件. DOS下的任何大的程序都必須被構建成'.EXE'文件,因爲只有'.EXE'文件擁有一種 內部結構可以突破64K的段限制.Windows程序也需要被構建成'.EXE'文件,因爲 Windows不支持'.COM'格式. 一般的,你是通過使用一個或多個'obj'格式的'.OBJ'目標文件來產生'.EXE'文件 的,用連接器把它們連接到一起.但是,NASM也支持通過'bin'輸出格式直接產生一 個簡單的DOS '.EXE'文件(通過使用'DB'和'DW'來構建exe文件頭),並提供了一組 宏幫助做到這一點.多謝Yann Guidon貢獻了這一部分代碼. 在NASM的未來版本中,可能會完全支持'.EXE'文件. 7.1.1 使用'obj'格式來產生'.EXE'文件. 本章選描述常見的產生'.EXE'文件的方法:把'.OBJ'文件連接到一起. 大多數16位的程序語言包都附帶有一個配套的連接器,如果你沒有,有一個免費的 叫做VAL的連接器,在`x2ftp.oulu.fi'上可以以'LZH'包的格式得到.也可以在 `ftp.simtel.net'上得到. 另一個免費的LZH包(儘管這個包是沒有源代碼的),叫做 FREELINK,可以在`www.pcorner.com'上得到. 第三個是'djlink',是由DJ Delorie寫 的,可以在`www.delorie.com'上得到. 第四個 'ALINK', 是由Anthony A.J. Williams 寫的,可以在`alink.sourceforge.net'上得到. 當把多個'.OBJ'連接進一個'.EXE'文件中的時候,你需要保證它們當中有且僅有一 個含有程序入口點(使用'obj'格式定義的特殊符號'..start'參閱6.2.6).如果沒有 模塊定義入口點,連接器就不知道在輸出文件的文件頭中爲入口點域賦什麼值,如 果有多個入口被定義,連接器就不知道到底該用哪一個. 一個關於把NASM源文件彙編成'.OBJ'文件,並把它連接成一個'.EXE'文件的例子在 這裏給出.它演示了定義棧,初始化段寄存器,聲明入口點的基本做法.這個文件也 在NASM的'test'子目錄中有提供,名字是'objexe.asm'. segment code ..start: mov ax,data mov ds,ax mov ax,stack mov ss,ax mov sp,stacktop 這是一段初始化代碼,先把DS寄存器設置成指定數據段,然後把‘SS’和‘SP’寄存器 設置成指定提供的棧。注意,這種情況下,在'mov ss,ax'後,有一條指令隱式地把 中斷關閉掉了,這樣抗敵,在載入 'SS'和‘SP’的過程中就不會有中斷髮生,並且沒 有可執行的棧可用。 還有,一個特殊的符號'..start'在這段代碼的開頭被定義,它表示最終可執行代 碼的入口點。 mov dx,hello mov ah,9 int 0x21 上面是主程序:在'DS:DX'中載入一個指向歡迎信息的指針('hello'隱式的跟段 ‘data'相關聯,’data'在設置代碼中已經被載入到‘DS‘寄存器中,所以整個指針是 有效的),然後調用DOS的打印字符串功能調用。 mov ax,0x4c00 int 0x21 這兩句使用另一個DOS功能調用結束程序。 segment data hello: db 'hello, world', 13, 10, '$' 數據段中含有我們想要顯示的字符串。 segment stack stack resb 64 stacktop: 上面的代碼聲明一個含有64bytes的未初始化棧空間的堆棧段,然後把指針 ’stacktop'指向它的頂端。操作符'segment stack stack'定義了一個叫做 ‘stack'的段,同時它的類型也是'STACK'.後者並不一定需要,但是連接串可 能會因爲你的程序中沒有段的類型爲'STACK'而發出警告。 上面的文件在被編譯爲'.OBJ'文件中,會自動連接成爲一個有效的'.EXE'文 件,當運行它時會打印出'hello world',然後退出。 7.1.2 使用’bin'格式來產生`.EXE'文件。 '.EXE'文件是相當簡單的,所以可以通過編寫一個純二進制文件然後在前面 連接上一個32bytes的頭就可以產生一個'.exe'的文件了。這個文件頭也是 相當簡單,它可以通過使用NASM自己的'DB'和'DW'命令來產生,所以你可以使 用'bin'輸出格式直接產生'.EXE'文件。 在NASM的包中,有一個'misc'子目錄,這是一個宏文件'exebin.mac'。它定義 了三個宏`EXE_begin',`EXE_stack'和`EXE_end'. 要通過這種方法產生一個'.EXE'文件,你應當開始的時候先使用'%include'載 入'exebin.mac'宏包到你的源文件中。然後,你應當使用'EXE_begin'宏(不帶 任何參數)來產生文件頭數據。然後像平常一樣寫二進制格式的代碼-你可以 使用三種標準的段'.text','.data','.bss'.在文件的最後,你應當調用 'EXE_end'宏(還是不帶任何參數),它定義了一些標識段size的符號,而這些宏 會由'EXE_begin'產生的文件頭代碼引用。 在這個模塊中,你最後的代碼是寫在'0x100'開始的地址處的,就像是'.COM'文 件-實際上,如果你剝去那個32bytes的文件頭,你就會得到一個有效的'.COM'程 序。所有的段基址是相同的,所以程序的大小被限制在64K的範圍內,這還是跟 一個'.COM'文件相同。'ORG'操作符是被'EXE_begin'宏使用的,所以你不必自己 顯式的使用它 你可以直接使用你的段基址,但不幸的是,因爲這需要在文件頭中有一個重定 位,事情就會變得更復雜。所以你應當從'CS'中拷貝出一個段基址。 進入你的'.EXE'文件後,'SS:SP'已經被正確的指向一個2Kb的棧頂。你可以通過 調用'EXE_stack'宏來調整缺省的2KB的棧大小。比如,把你的棧size改變到 64bytes,你可以調用'EXE_stack 64' 一個關於以這種方式產生一個'.EXE'文件的例子在NASM包的子目錄'test'中, 名字是'binexe.asm' 7.2 產生`.COM'文件 一個大的DOS程序最好是寫成'.EXE'文件,但一個小的程序往往最好寫成'.COM' 文件。'.COM'文件是純二進制的,所以使用'bin'輸出格式可以很容易的地產生。 7.2.1 使用`bin'格式產生`.COM’文件。 '.COM'文件預期被裝載到它們所在段的'100h'偏移處(儘管段可能會變)。然後 從100h處開始執行,所以要寫一個'.COM'程序,你應當象下面這樣寫代碼: org 100h section .text start: ; put your code here section .data ; put data items here section .bss ; put uninitialised data here 'bin'格式會把'.text'段放在文件的最開始處,所以如果你需要,你可以在開始 編寫代碼前先聲明data和bss元素,代碼段最終還是會放到文件的最開始處。 BSS(未初始化過的數據)段本身在'.COM'文件中並不佔據空間:BSS中的元素的地 址是一個指向文件外面的一個空間的一個指針,這樣做的依據是在程序運行中, 這樣可以節省空間。所以你不應當相信當你運行程序時,你的BSS段已經被初始 化爲零了。 爲了彙編上面的程序,你應當象下面這樣使用命令行: nasm myprog.asm -fbin -o myprog.com 如果沒有顯式的指定輸出文件名,這個'bin'格式會產生一個叫做'myprog'的文 件,所以你必須重新給它指定一個文件名。 7.2.2 使用`obj'格式產生`.COM'文件 如果你在寫一個'.COM'文件的時候,產生了多於一個的模塊,你可能希望彙編成 多個'.OBJ'文件,然後把它們連接成一個'.COM'程序。如果你擁有一個能夠輸出 '.COM'文件的連接器,你可以做到這一點。或者擁有一個轉化程序(比如, 'EXE2BIN')把一個'.EXE'輸出文件轉化爲一個'.COM'文件也可。 如果你要這樣做,你必須注意幾件事情: (*) 第一個含有代碼的目標文件在它的代碼段中,第一句必須是:'RESB 100h'。 這是爲了保證代碼在代碼段基址的偏移'100h'處開始,這樣,連接器和轉化 程序在產生.com文件時,就不必調整地址引用了。其他的彙編器是使用'ORG' 操作符來達到此目的的,但是'ORG'在NASM中對於'bin'格式來說是一個格式相 關的操作符,會表達不同的含義。 (*) 你不必定義一個堆棧段。 (*) 你的所有段必須放在一個組中,這樣每次你的代碼或數據引用一個符號偏移 時,所有的偏移值都是相對於同一個段基址的。這是因爲,當一個'.COM'文件 載入時,所有的段寄存器含有同一個值。 7.3 產生`.SYS'文件 MS-DOS設備驅動-'SYS'文件-是一些純二進制文件,跟.com文件相似,但有一點, 它們的起始地址是0,而不是'100h'。因此,如果你用'bin'格式寫一個設備程序 ,你不必使用'ORG'操作符,因爲'bin'的缺省起始地址就是零。相似的,如果你 使用'obj',你不必在代碼段的起始處使用'RESB 100h' '.SYS'文件擁有一個文件頭,包含一些指針,這些指針指向設備中完成實際工 作的不同的子過程。這個結構必須在代碼段的起始處被定義,儘管它並不是 實際的代碼。 要得到關於'.SYS'文件的更多信息,頭結構中必須包含的數據,有一本以FAQ列 表的形式給出的書可以在`comp.os.msdos.programmer'得到。 7.4 與16位C程序之間的接口。 本章介紹編寫調用C程序的彙編過程或被C程序調用的彙編過程的基本方法。要 做到這一點,你必須把彙編模塊寫成'.OBJ'文件,然後把它和你的C模塊一起連接, 產生一個混合語言程序。 7.4.1 外部符號名。 C編譯器對所有的全局符號(函數或數據)的名字有一個轉化,它們被定義爲在名 字前面加上一個下劃線,就象在C程序中出現的那樣。所以,比如,一個C程序的 函數'printf'對彙編語言程序中來說,應該是'_printf'。你意味着在你的匯 編程序中,你可以定義前面不帶下劃線的符號,而不必擔心跟C中的符號名產生 衝突。 如果你覺得下劃線不方便,你可以定義一個宏來替換'GLOBAL'和'EXTERN'操作 符: %macro cglobal 1 global _%1 %define %1 _%1 %endmacro %macro cextern 1 extern _%1 %define %1 _%1 %endmacro (這些形式的宏一次只帶有一個參數;'%rep'結構可以解決這個問題)。 如果你象下面這樣定義一個外部符號: cextern printf 這個宏就會被展開成: extern _printf %define printf _printf 然後,你可用把'printf'作爲一個符號來引用,預處理器會在必要的時候 在前面加上一個下劃線。 'cglobal'宏以相似的方式工作。 7.4.2 內存模式。 NASM沒有提供支持各種C的內存模式的直接機制;你必須自己記住你在何 種模式下工作。這意味着你自己必須跟蹤以下事情: (*) 在使用單個代碼段的模式中(tiny small和compact)函數都是near的, 這表示函數指針在作爲一個函數參數存入數據段或壓棧時,有16位 長並只包含一個偏移域(CS寄存器中的值從來不改變,總是給出函數 地址的段地真正部分),函數調用就使用普通的near'CALL'指令,返回 使用'RETN'(在NASM中,它跟'RET'同義)。這意味着你在編寫你自己的 過程時,應當使用'RETN'返回,你調用外部C過程時,可以使用near的 'CALL'指令。 (*) 在使用多於一個代碼段的模塊中(medium, large和huge)函數是far的, 這表示函數指針是32位長的(包含 16位的偏移值和緊跟着的16位段 地址),這種函數使用'CALL FAR'進行調用(或者'CALL seg:offset') 而返回使用'RETF'。同樣的,你編寫自己的過程時,應當使用'RETF', 調用外部C過程應當使用'CALL FAR'。 (*) 在使用單個數據段的模塊中(tiny, small和medium),數據指針是16位 長的,只包含一個偏移域(’DS‘寄存器的值不改變,總是給出數據元素 的地址的段地址部分)。 (*) 在使用多於一個數據段的模塊中(compact, large和huge),數據指針 是32位長的,包含一個16位的偏移跟上一佧16位的段地址。你還是應 當小心,不要隨便改變了ds的值而沒有恢復它,但是ES可以被隨便用 來存取32位數據指針的內容。 7.4.3 函數定義和函數調用。 16位程序中的C調用轉化如下所示。在下面的描述中,_caller_和_callee_ 分別表示調用者和被調用者。 (*) caller把函數的參數按相反的順序壓棧,(從右到左,所以第一個參數 被最後一個壓棧)。 (*) caller然後執行一個'CALL'指令把控制權交給callee。根據所使用的 內存模式,'CALL'可以是near或far。 (*) callee接收控制權,然後一般會(儘管在沒有帶參數的函數中,這不是 必須的)在開始的時候把’SP‘的值賦給’BP‘,然後就可以把‘BP’ 作爲一個基準指針用以尋找棧中的參數。當然,這個事情也有可能由 caller來做,所以,關於'BP'的部分調用轉化工作必須由C函數來完成 。因此callee如果要把'BP'設爲框架指針,它必須把先前的BP值壓棧。 (*) 然後callee可能會以'BP'相關的方式去存取它的參數。在[BP]中存有BP 在壓棧前的那個值;下一字word,在[BP+2]處,是返回地址的偏移域, 由'CALL'指令隱式壓入。在一個small模式的函數中。在[BP+4]處是參 數開始的地方;在large模式的函數中,返回地址的段基址部分存在 [BP+4]的地方,而參數是從[BP+6]處開始的。最左邊的參數是被後一個被 壓入棧的,所以在'BP'的這點偏移值上就可以被取到;其他參數緊隨其後,偏 移地址是連續的.這樣,在一個象'printf'這樣的帶有一定數量的參數的函 數中,以相反的順序把參數壓棧意味着函數可以知道從哪兒獲得它的第一個 參數,這個參數可以告訴接接下來還有多少參數,和它們的類型分別是什麼. (*) callee可能希望減小'sp'的值,以便在棧中分配本地變量,這些變量可以用 'BP'負偏移來進行存取. (*) callee如果想要返回給caller一個值,應該根據這個值的大小放在'AL','AX' 或'DX:AX'中.如果是浮點類型返回值,有時(看編譯器而定)會放在'ST0'中. (*) 一旦callee結束了處理,它如果分配過了本地空間,就從'BP'中恢復'SP'的 值,然後把原來的'BP'值出棧,然後依據使用的內存模式使用'RETN'或'RETF' 返回值. (*) 如果caller從callee中又重新取回了控制權,函數的參數仍舊在棧中,所以它 需要加一個立即常數到'SP'中去,以移除這些參數(不用執行一系列的pop指令 來達到這個目的).這樣,如果一個函數因爲匹配的問題偶爾被以錯誤的參數個 數來調用,棧還是會返回一個正常的狀態,因爲caller知道有多少個參數被壓 了,它會把它們正確的移除. 這種調用轉化跟Pascal程序的調用轉化是沒有辦法比較的(在7.5.1描述).pascal 擁有一個更簡單的轉化機制,因爲沒有函數擁有可變數目的參數.所以callee知道 傳遞了多少參數,它也就有能力自己來通過傳遞一個立即數給'RET'或'RETF'指令 來移除棧中的參數,所以caller就不必做這個事情了.同樣,參數也是以從左到右 的順序被壓棧的,而不是從右到左,這意味着一個編譯器可以更方便地處理。 這樣,如果你想要以C風格定義一個函數,應該以下面的方式進行:這個例子是 在small模式下的。 global _myfunc _myfunc: push bp mov bp,sp sub sp,0x40 ; 64 bytes of local stack space mov bx,[bp+4] ; first parameter to function ; some more code mov sp,bp ; undo "sub sp,0x40" above pop bp ret 在巨模式下,你應該把'RET'替換成'RETF',然後應該在[BP+6]的位置處尋找第 一個參數,而不是[BP+4].當然,如果某一個參數是一個指針的話,那參數序列 的偏移值會因爲內存模式的改變而改變:far指針作爲一個參數時在棧中佔用 4bytes,而near指針只佔用兩個字節。 另一方面,如果從你的彙編代碼中調用一個C函數,你應該做下面的一些事情: extern _printf ; and then, further down... push word [myint] ; one of my integer variables push word mystring ; pointer into my data segment call _printf add sp,byte 4 ; `byte' saves space ; then those data items... segment _DATA myint dw 1234 mystring db 'This number -> %d <- should be 1234',10,0 這段代碼在small內存模式下等同於下面的C代碼: int myint = 1234; printf("This number -> %d <- should be 1234/n", myint); 在large模式下,函數調用代碼可能更象下面這樣。在這個例子中,假設DS已經 含有段'_DATA'的段基址,你首先必須初始化它: push word [myint] push word seg mystring ; Now push the segment, and... push word mystring ; ... offset of "mystring" call far _printf add sp,byte 6 這個整型值在棧中還是佔用一個字的空間,因爲large模式並不會影響到'int' 數據類型的size.printf的第一個參數(最後一個壓棧),是一個數據指針,所以 含有一個段基址和一個偏移域。在內存中,段基址應該放在偏移域後面,所以, 必須首先被壓棧。(當然,'PUSH DS'是一個取代'PUSH WORD SEG mystring'的更 短的形式,如果DS已經被正確設置的話)。然後,實際的調用變成了一個far調用, 因爲在large模式下,函數都是被far調用的;調用後,'SP'必須被加上6,而不是 4,以釋放壓入棧中的參數。 7.4.4 存取數據元素。 要想獲得一個C變量的內容,或者聲明一個C語言可以存取的變量,你只需要把變 量名聲明爲'GLOBAL'或'EXTERN'即可。(再次提醒,就象在7.4.1中所介紹的,變 量名前需要加上一個下劃線)這樣,一個在C中聲明的變量'ini i'可以在彙編語 中以下述方式存取: extern _i mov ax,[_i] 而要聲明一個你自己的可以被C程序存取的整型變量如:'extern int j',你可 以這樣做(確定你下在'_DATA'段中): global _j _j dw 0 要存取C的數組,你需要知道數組元素的size.比如,'int'變量是2byte長,所以 如果一個C程序聲明瞭一個數組'int a[10]',你可象這樣存取'a[3]':'mov ax, [_a+6]'.(字節偏移6是通過數組下標3乘上數組元素的size2得到的。) 基於C的 16位編譯器的數據size如下:1 for `char', 2 for `short' and `int', 4 for `long' and `float', and 8 for `double'. 爲了存取C的數據結構,你必須知道從結構的基地址到你所感興趣的域的偏移地 址。你可以通過把C結構定義轉化爲NASM的結構定義(使用'STRUC'),或者計算這 個偏移地址然後進行相應操作。 以上述任何一種方法實現,你必須得閱讀你的C編譯器的手冊去找出他是如何組 織數據結構的。NASM在它的宏'STRUC'中不給出任何對結構體成員的對齊操作, 所以你可能會發現結構體類似下面的樣子: struct { char c; int i; } foo; 可能就是4字節長,而不是三個字,因爲'int'域會被對齊到2byte邊界。但是,這 種排布的特性在C編譯器中很可能只是一個配置選項,使用命令行選項或者 '#pragma'行。所以你必須找出你的編譯器是如何實現這個的。 7.4.5 `c16.mac': 與16位C接口的幫助宏。 在NASM包中,在'misc'子目錄下,是一個宏文件'c16.mac'。它定義了三個宏 'proc','arg'和'endproc'。這些被用在C風格的過程定義中,它們自動完成了 很多工作,包括對調用轉化的跟蹤。 (另外一種選擇是,TASM兼容模式的'arg'現在也被編譯進了NASM的預處理器, 詳見4.9) 關於在彙編函數中使用這個宏的一個例子如下: proc _nearproc %$i arg %$j arg mov ax,[bp + %$i] mov bx,[bp + %$j] add ax,[bx] endproc 這把'_nearproc'定義爲一個帶有兩個參數的一個過程,第一個('i')是一個整 型數,第二個('j')是一個指向整型數的指針,它返回'i+*j'。 注意,'arg'宏展開的第一行有一個'EQU',而且因爲在宏調用的前面的那個 label在宏展開後被加在了第一行的前面,所以'EQU'能否工作取決於'%$i'是否 是一個關於'BP'的偏移值。同時一個對於上下文來說是本地的context-local變 量被使用,它被'proc'宏壓棧,然後被'endproc'宏出棧,所以,在後來的過程 中,同樣的參數名還是可以使用,當然,你不一定要這麼做。 宏在缺省狀況下把過程代碼設置爲near函數(tiny,small和compact模式代碼), 你可以通過代碼'%define FARCODE'產生far函數(medium, large和huge模式代 碼)。這會改變'endproc'產生的返回指令的類型,還會改變參數起始位置的偏移 值。這個宏在設置內容時,本質上並依賴數據指針是near或far。 'arg'可以帶有一個可選參數,給出參數的size。如果沒有size給出,缺省設置爲 2,因爲絕大多數函數參數會是'int'類型。 上面函數的large模式看上去應該是這個樣子: %define FARCODE proc _farproc %$i arg %$j arg 4 mov ax,[bp + %$i] mov bx,[bp + %$j] mov es,[bp + %$j + 2] add ax,[bx] endproc 這利用了'arg'宏的參數定義參數的size爲4,因爲'j'現在是一個far指針。當我們 從'j'中載入數據時,我們必須同時載入一個段基址和一個偏移值。 第八章: 編寫32位代碼(Unix, Win32, DJGPP) --------------------------------------------------- 本章主要介紹在編寫運行在Win32或Unix下的32位代碼,或與Unix風格的編譯器, 比如DJGPP連接的代碼時,通常會碰到的一些問題。這裏包括如何編寫與32位C 函數連接的彙編代碼,如何爲共享庫編寫地址無關的代碼。 幾乎所有的32位代碼,即在實際使用中的所有運行在'Win32','DJGPP'和所有PC Unix變體都運行 在_flat_內存模式下。這意味着段寄存器和頁已經被正確設置, 以給你一個統一的32位的4Gb的地址空間,而不管你當前工作在哪個段下,而且 你應當完全忽略所有的段寄存器。當寫一個平坦(flat)模式的程序代碼時,你從 來不必使用段重載或改變段寄存器,而且你傳給'CALL',和'JMP'的代碼段地址, 你存取你的變量時使用的數據段地址,你存取局部變量和函數參數時的堆棧段地 址實際上都在同一個地址空間中。每一個地址都是32位長,只含有一個偏移域 8.1 與32位C代碼之間的接口。 在7.4中有很多關於與16位C代碼之間接口的討論,這些東西有很多在32位代碼中 仍有用。但已經不必擔心內存模式和段的問題了,這把問題簡化了很多。 8.1.1 外部符號名。 大多數32位的C編譯器共享16位編譯器的轉化機制,即它們定義的所有的全局符 號(函數與數據)的名字在C程序中出現時由一個下劃線加上名字組成。但是,並 不是他們中的所有的都這樣做::'ELF'標準指出C符號在彙編語言中不含有一個 前導的下劃線。 老的Linux'a.out'C編譯器,所有的'Win32'編譯器,‘DJGPP'和'NetBSD' 'FreeBSD'都使用前導的下劃線;對於這些編譯器來講,7.4.1中給出的宏 'cextern'和'cglobal'還會正常工作。對於'ELF'來講,下劃線是沒有必要的。 8.1.2 函數定義和函數調用。 32位程序中的C調用轉化如下所述。在下面的描述中,_caller_和_callee_用來o 表示調用函數和被調用函數。 (*) caller把函數的參數按相反的順序(從右到左,這樣的話,第一個參數被最 後一個壓棧)依次壓棧 (*) 然後,caller執行一個near'CALL'指令把控制權傳給callee。 (*) callee接受控制權,然後一般會(但這實際上不是必須的,如果函數不需要 存取它的參數就不用)開始先存儲'ESP'的值到'EBP'中,這樣就可以使用 'EBP'作爲一個基指針去棧中尋找參數。但是,這一點也可以放在caller中 做,所以,調用轉化中'EBP'必須被C函數保存起來。因爲callee要把'EBP' 設置爲一個框架指針來使用,它必須把先前的值給保存起來。 (*) 然後,callee就可以通過與'EBP'相關的方式來存取它的參數了。在[EBP] 處的雙字擁有剛剛被壓棧的'EBP'的前一個值;接下來的雙字,在[EBP+4] TH ,是被'CALL'指令隱式壓入的返回地址。後面纔是參數開始的地方,在 [EBP+8]處。因爲最左邊的參數最後一個被壓棧,在[EBP]的這個偏移地址 上就可以被取得;剩下的參數依次存在後面,以連續增長的偏移值存放。 這樣,在一個如'printf'的帶有一定數量參數的函數中,以相反的順序把 參數壓棧意味着函數可以知道到哪兒去找它的第一個參數,這個參數可以 告訴它總共有多少參數,它們的類型是什麼。 (*) callee可能也希望能夠再次減小'ESP'的值,以爲本地變量開闢本地空間, 這些變量然後就可以通過'EBP'的負偏移來獲取。 (*) callee如果需要返回給caller一個值,需要根據這個值的size把它放在 'AL','AX'或'EAX'中。浮點數在'ST0'中返回。 (*) 一旦callee完成了處理,如果它定義的局部棧變量,它就從'EBP'中恢復 'ESP',然後彈出前一個'EBP'的值,並通過'RET'返回。 (*) 當caller從callee那裏取回了控制權,函數的參數還是放在棧中,所以,它 通常給'ESP'加上一個立即常數以移除參數(而不是執行一系列的'pop'指令 )。這樣,如果一個函數如果因爲意外,使用了錯誤的參數個數,棧還是 會返回到正常狀態,因爲caller知道多少參數被壓棧了,並可以正確的移 除。 對於Win32程序使用的Windows API調用,有另一個可選的調用轉化,對於那些 被Windows API調用的函數(稱爲windows過程)也一樣:他們遵循一個被微軟叫 做'__stdcall'的轉化。這跟Pascal的轉化比較接近,在這裏,callee通過給 'RET'指令傳遞一個參數來清除棧。但是,參數還是以從右到左的順序被壓棧。 這樣,你可以象下面這樣定義一個C風格的函數: global _myfunc _myfunc: push ebp mov ebp,esp sub esp,0x40 ; 64 bytes of local stack space mov ebx,[ebp+8] ; first parameter to function ; some more code leave ; mov esp,ebp / pop ebp ret 另一方面,如果你要從你的彙編代碼中調用一個C函數,你可以象下面這樣寫代 碼: extern _printf ; and then, further down... push dword [myint] ; one of my integer variables push dword mystring ; pointer into my data segment call _printf add esp,byte 8 ; `byte' saves space ; then those data items... segment _DATA myint dd 1234 mystring db 'This number -> %d <- should be 1234',10,0 這段代碼等同於下面的C代碼: int myint = 1234; printf("This number -> %d <- should be 1234/n", myint); 8.1.3 獲取數據元素。 要想獲取一個C變量的內容,或者聲明一個C可以獲取的變量,你必須把這個變 量聲明爲'GLOBAL'或'EXTERN'(再次提醒,變量名前需要加上一個下劃線,就象 8.1.1中所描述的),這樣,一個被聲明爲'int i'的C變量可以從彙編語言中這樣 獲取: extern _i mov eax,[_i] 而要定個一個C程序可以獲取的你自己的變量'extern int j',你可以這樣做(確 定你正在'_DATA'中) global _j _j dd 0 要獲取C數組,你必須知道數組的元素的size。比如,'int'變量是4bytes長,所 以,如果一個C程序聲明瞭一個數組'int a[10]',你可以使用代碼'mov ax, [_a+12]來存取變量'a[3]'。(字節偏移12是通過數組下標3乘上數組元素的size 4得到的)。基於C的32位編譯器上的數據的size如下:1 for `char', 2 for `short', 4 for `int', `long' and `float', and 8for `double'. Pointers, 32位的地址也是4字節長。 要獲取C的數據結構體,你必須知道從結構體的基地址到你所需要的域之間的偏 移值。你可以把C的結構體定義轉化成NASM的結構體定義(使用'STRUC'),或者計 算得到這個偏移值,然後使用它。 以上面任何一種方式實現,你都需要閱讀你的C編譯器的手冊找出它是如何組織 結構體數據的。NASM在它的'STRUC'宏中沒有給出任何特定的對齊規則,所以如 果C編譯器產生結構體,你必須自己指定對齊規則。你可能發現類似下面的結構 體: struct { char c; int i; } foo; 可能是8字節長,而不是5字節,因爲'int'域會被對齊到4bytes邊界。但是,這 種排布特性有時會是C編譯器的一個配置選項,可以使用命令行選項或'#progma' 行來實現,所以你必須找出你自己的編譯器是如何做的。 8.1.4 `c32.mac': 與32位C接口的幫助宏。 在NASM的包中,在'misc'子目錄中,有一個宏文件'c32.mac'。它定義了三個宏: 'proc','arg'和'endproc'。它們被用來定義C風格的過程,它們會自動產生很多 代碼,並跟蹤調用轉化過程。 使用這些宏的一個彙編函數的例子如下: proc _proc32 %$i arg %$j arg mov eax,[ebp + %$i] mov ebx,[ebp + %$j] add eax,[ebx] endproc 它把函數'_proc32'定義成一個帶有兩個參數的過程,第一個('i')是一個整型數, 第二個('j')是一個指向整型數的指針,它返回'i+*j'。 注意,宏'arg'展開後的第一行有個'EQU',因爲在宏調用行的前面的那個label 被加到了第一行上,'EQU'行就可以正常工作了,它把'%Si'定義爲一個以'BP' 爲基址的偏移值。一個context-local變量在這裏被使用,被'proc'宏壓棧,然 後被'endproc'宏出棧,所以,同樣的參數名在後來的過程中還是可以使用,當然 你不一定要那樣做。 'arg'帶有一個可選的參數,給出參數的size。如果沒有size給出,缺省的是4,因 爲很多函數參數都會是'int'類型或者是一個指針。 8.2 編寫NetBSD/FreeBSD/OpenBSD和Linux/ELF共享庫 'ELF'在Linux下取代了老的'a.out'目標文件格式,因爲它包含對於地址無關代碼 (PIC)的支持,這可以讓編寫共享庫變得很容易。NASM支持'ELF'的地址無關代碼 特性,所以你可以用NASM來編寫Linux的'ELF'共享庫。 NetBSD,和它的近親FreeBSD,OpenBSD,採用了一種不同的方法,它們把PIC支持做 進了'a.out'格式。NASM支持這些格式,把它們叫做'aoutb'輸出格式,所以你可 以在NASM下寫BSD的共享庫。 操作系統是通過把一個庫文件內存映射到一個運行進程的地址空間上的某一個點 來實現載入PIC共享庫的。所以,庫的代碼段內容必須不依賴於它被載入到了內 存的什麼地方。 因此,你不能通過下面的代碼得到你的變量: mov eax,[myvar] ; WRONG 而是通過連接器提供一片內存空間,這片空間叫做全局偏移表(GOT);GOT被放到 離你庫代碼的一個常量距離值的地方,所以如果你發現了你的庫被載入到了什麼 地方(這可以通過使用'CALL'和'POP'指令而得到),你可以得到GOT中的地址,然 後你就可以通過這個連接器產生的在GOT中的入口來載入你的變量的地址。 而PIC共享庫的數據段就沒有這些限制了:因爲數據段是可定局的,它必須被拷 貝到內存中,而不是僅僅從庫文件中作一個映射,所以一旦它被拷貝進來,它 就可以被重定位。所以你可以把一些常規的在數據段中重定位的類型用進來,而 不必擔心會有什麼錯誤發生。 8.2.1 取得GOT中的地址。 每個在你的共享庫中的代碼模塊都應當把GOT定義爲一個導出符號: extern _GLOBAL_OFFSET_TABLE_ ; in ELF extern __GLOBAL_OFFSET_TABLE_ ; in BSD a.out 在你的共享庫中,那些需要獲取你的data或BSS段中數據的函數,你必須在它們 的開頭先計算GOT的地址。這一般以如下形式編寫這個函數: func: push ebp mov ebp,esp push ebx call .get_GOT .get_GOT: pop ebx add ebx,_GLOBAL_OFFSET_TABLE_+$$-.get_GOT wrt ..gotpc ; the function body comes here mov ebx,[ebp-4] mov esp,ebp pop ebp ret (對於BSD, 符號`_GLOBAL_OFFSET_TABLE'開頭需要兩個下劃線。) 這個函數的頭兩行只是簡單的標準的C風格的開頭,用於設置棧框架,最後的三 行是標準的C風格的結尾,第三行,和倒數第四行,分別保存和恢復'EBS'寄存器 ,因爲PIC共享庫使用這個寄存器保存GOT的地址。 最關鍵的是'CALL'指令和接下來的兩行代碼。'CALL'和'POP'一起用來獲得 .get_GOT的地址,不用進一步知道程序被載入到什麼地方(因爲call指令是解碼 成跟當前的位置相關)。‘ADD’指令使用了一個特殊的PIC重定位類型:GOTPC 重定位。通過使用限定符'WRT ..gotpc',被引用的符號(這裏是 `_GLOBAL_OFFSET_TABLE_',一個被賦給GOT的特殊符號)被以從段起始地址開始 的偏移的形式給出。(實際上,‘ELF’把它編碼爲從‘ADD’的操作數域開始的 一個偏移,但NASM把它簡化了,所以你在‘ELF’和‘BSD’中可以用同樣的方 式處理。)所以,這條指令然後加上段起始地址,然後得到GOT的真正的地址。 然後減去'.get_GOT'的值,當這條指令執行結束的時候,'EBX'中含有'GOT' 的值。 如果你不理解上面的內容,也不用擔心:因爲沒有必要以第二種方式來獲得 GOT的地址,所以,你可以把這三條指令寫成一個宏,然後就可以安全地忽略 它們: %macro get_GOT 0 call %%getgot %%getgot: pop ebx add ebx,_GLOBAL_OFFSET_TABLE_+$$-%%getgot wrt ..gotpc %endmacro 8.2.2 尋址你的本地數據元素。 得到GOT後,你可以使用它來得到你的數據元素的地址。大多數變量會在你聲明 過的段中;它們可以通過使用'..gotoff'來得到。它工作的方式如下: lea eax,[ebx+myvar wrt ..gotoff] 表達式'myvar wrt ..gotoff'在共享庫被連接進來的時候被計算,得到從GOT 地始地址開始的變量'myvar'的偏移值。所以,把它加到上面的'EBX'中,並把 它放到'EAX'中. 如果你把一些變量聲明爲'GLOBAL',而沒有指定它們的size的話,它們在庫中的 代碼模塊間會被共享,但不會被從庫中導出到載入它們的程序中.但們還會存在 於你的常規data和BSS段中,所以通過上面的'..gotoff'機制,你可以把它們作爲 局部變量那樣存取 注意,因爲BSD的'a.out'格式處理這種重定位類型的一種方式,在你要存取的地 址處的同一個段內必須至少有一個非本地的符號. 8.2.3 尋址外部和通用數據元素. 如果你的庫需要得到一個外部變量(對庫來說是外部的,並不是對它所在的一個 模塊),你必須使用'..got'類型得到它.'..got'類型,並不給你從GOT基地址到 變量的偏移,給你的是從GOT基地址到一個含有這個變量地址的GOT入口的偏移, 連接器會在構建庫時設置這個GOT入口,動態連接器會在載入時在這個入口放上 正確的地址.所以,要得到一個外部變量'extvar'的地址,並放到EAX中,你可以 這樣寫: mov eax,[ebx+extvar wrt ..got] 這會在GOT的一個入口上載入'extvar'的地址.連接器在構建共享庫的時候,會搜 集每一個'..got'類型的重定位信息,然後構建GOT,保證它含有每一個必須的入 口 通用變量也必須以這種方式被存取. 8.2.4 把符號導出給庫用戶. 如果你需要把符號導出給庫用戶,你必須把它們聲明爲函數或數據,如果它們是數 據,你必須給出數據元素的size.這是因爲動態連接器必須爲每一個導出的函數 構建過程連接表入口,還要把導出數據元素從庫的數據段中移出. 所以,導出一個函數給庫用戶,你必須這樣: global func:function ; declare it as a function func: push ebp ; etc. 而導出一個數據元素,比如數組,你必須這樣寫代碼: global array:data array.end-array ; give the size too array: resd 128 .end: 小心:如果你希望通過把變量聲明爲'GLOBAL'並指定一個size,而導出給庫用戶, 這個變量最終會存在於主程序的數據段中,而不是在你的庫的數據段內,所以你 必須通過使用'..got'機制來獲取你自己的全局變量,而不是'..gotogg',就象它 是一個外部變量一樣(實際上,它已經變成了外部變量). 同樣的,如果你需要把一個導出的全局變量的地址存入你的一個數據段中,你不能 通過下面的標準方式實現: dataptr: dd global_data_item ; WRONG NASM會以個普通的重定位解釋這段代碼,在這裏,'global_data_item'僅僅是一個 從'.data'段(或者其他段)開始的一個偏移值;所以這個引用最終會指向你的數據 段,而不是導出全局變量. 對於上面的代碼,你應該這樣寫: dataptr: dd global_data_item wrt ..sym 這時使用了一個特殊的'WRT'類型'..sym'來指示NASM到符號表中去尋找一個在這 個地址的特定符號,而不是通過段基址重定位. 另外一種方式是針對函數的:以下面的方法引用你的一個函數: funcptr: dd my_function 會給用戶一個你的代碼的地址,而: funcptr: dd my_function wrt .sym 會給出過程連接表中的該函數的地址,這是真正的調用程序應該得到的地址.兩 種地址都是可行的. 8.2.5 從庫外調用過程. 從你的共享庫外部調用過程必須通過使用過程連接表(PLT)才能實現,PLT被放在 庫載入處的一個已知的偏移地址處,所以庫代碼可以以一種地址無關的方式去調 用PLT.在PLT中有跳轉到含在GOT中的偏移地址的代碼,所以對共享庫中或主程序 中的函數調用可以被轉化爲直接傳遞它們的真實地址. 要調用一個外部過程,你必須使用另一個特殊的PIC重定位類型,'WRT ..plt'.這 個比基於GOT的要簡單得多:你只需要把調用'CALL printf'替換爲PLT相關的版 本:`CALL printf WRT ..plt'. 8.2.6 產生庫文件. 寫好了一些代碼模塊並把它們彙編成'.o'文件後,你就可以產生你的共享庫了, 使用下面的命令就可以: ld -shared -o library.so module1.o module2.o # for ELF ld -Bshareable -o library.so module1.o module2.o # for BSD 對於ELF,如果你的共享庫要放在系統目錄'/usr/lib'或'/lib'中,那對連接器使 用'-soname'可以把最終的庫文件名和版本號放進庫中: ld -shared -soname library.so.1 -o library.so.1.2 *.o 然後你就可以把'library.so.1.2'拷貝到庫文件目錄下,然後建立一個它的符號 連的妝'library.so.1'. 第九章: 混合16位與32位代碼 ------------------------------------ 本章將介紹一些跟非常用的地址與跳轉指令相關的一些問題, 這些問題當你在 編寫操作系統代碼時會常遇上,比如保護模式初始化過程,它需要代碼操作混合 的段size,比如在16位段中的代碼需要去修改在32位段中的數據,或者在不同的 size的段之間的跳轉. 9.1 混合Size的跳轉. 最常用的混合size指令的形式是在寫32位操作系統時用到的:在16位模式中完成 你的設置,比如載入內核,然後你必須通過切入到保護模式中引導它,然後跳轉到 32位的內核起始地址處.在一個完全32位的操作系統中,這是你唯一需要用到混合 size指令的地方,因爲在它之間的所有事情都可以在純16位代碼中完成,而在它之 後的所在事情都在純32位代碼中. 這種跳轉必須指定一個48位的遠地址,因爲目標段是一個32位段.但是,它必須在 16位段中被彙編,所以,僅僅如下面寫代碼: jmp 0x1234:0x56789ABC ; wrong! 不會正常工作,因爲地址的偏移域部分會被截斷成'0x9ABC',然後,跳轉會是一個 普通的16位遠跳轉. Linux內核的設置代碼使用'as86'通過手工編碼來產生這條指令,使用'DB'指令, NASM可以比它更好些,可以自己產生正確的指令,這裏是正確的做法: jmp dword 0x1234:0x56789ABC ; right 'DWORD'前綴(嚴格地講,它應該放在冒後的後面,因爲它只是把偏移域聲明爲 doubleword;但是NASM接受任何一種形式,因爲兩種寫法都是明確的)強制偏移域 在假設你正從一個16段跳轉到32位段的前提下,被處理爲far. 你可以完成一個相反的操作,從一個32位段中跳轉到一個16位段,使用'word' 前綴: jmp word 0x8765:0x4321 ; 32 to 16 bit 如果'WORD'前綴在16位模式下被指定,或者'DWORD'前綴在32位模式下被指定, 它們都會被忽略,因爲它們每一個都顯式強制NASM進入一個已進進入的模式. 9.2 在不同size的段間尋址. 如果你的操作系統是16位與32位混合的,或者你正在寫一個DOS的擴展,你可能 必須處理一些16位段和一些32位段.在某些地方,你可能最終要在一個16位段中 編寫能獲取32位段中的數據的代碼,或者相反. 如果你要獲取的32位段中的數據正好在段的前64K的範圍內,你可以通過普通的 16位地址操作來達到目的;但是或多或少,你會需要從16位模式中處理32位的尋 址. 最早的解決方案保證你使用了一個寄存器用於保存地址,因爲任何在32位寄存器 中的有效地址都被強制作爲一個32位的地址,所以,你可以: mov eax,offset_into_32_bit_segment_specified_by_fs mov dword [fs:eax],0x11223344 這個不錯,但有些笨拙(因爲它浪費了一條指令和一個寄存器),如果你已經知道 你的目標所在的精確偏移.x86架構允許32位有效地址被指定爲一個4bytes的偏 移,所以,NASM爲什麼不爲些產生一個最佳的指令呢? 它可以,就象在9.1中一樣,你只需要在地址前加上一個'DWORD'前綴,然後,它會 被強制作爲一個32位的地址: mov dword [fs:dword my_offset],0x11223344 同樣跟9.1中一樣,NASM並不關心'DWORD'前綴是在段重載符前,還是這後,所以 可以把代碼改得好看一些: mov dword [dword fs:my_offset],0x11223344 不要把'DWROD'前綴放在方括號外面,它是用來控制存儲在那裏的數據的size的, 而在方括號內的話,它控制地址本身的長度.這兩種方式可以被很容易地區分: mov word [dword 0x12345678],0x9ABC 這把一個16位的數據放到了一個指定爲32位偏移的地址中. 你也可以把'WORD'或'DWROD'前綴跟'FAR'前綴放到一起,來間接跳轉或調用,比 如: call dword far [fs:word 0x4321] 這條指令包含一個指定爲16位偏移的地址,它載入了一個48位的遠指針,(16位 段和32位段偏移),然後調用這個地址. 9.3 其他的混合size指令. 你可能需要用於獲取數據的其它的方式可能就是使用字符串指令('LODSx' 'STOSx',等等)或'XLATB'指令.這些指令因爲不帶有任何參數,看上去好像很難 在它們被彙編進16位段的時候使它們使用32位地址. 而這正是NASM的'a16'和'a32'前綴的目的,如果你正在16位段中編寫'LODSB', 但它是被用來獲取一個32位段中的字符串的,你應當把目標地址載入'ESI',然 後編寫: a32 lodsb 這個前綴強制地址的size爲32位,意思是'LODSB'從[DS:ESI]中載入內容,而不是 從[DS:SI]中.要在編寫32位段的時候,獲取在16位段中的字符串,相應的前綴'a16' 可以被使用. 'a16'和'a32'前綴可以被運用到NASM指令表的任何指令上,但是他們中的大多數 可以在沒有這兩個前綴的情況下產生所有有用的形式.這兩個前綴只有在那些帶 有隱式地址的指令中是有效的: `CMPSx' (section B.4.27), `SCASx' (section B.4.286), `LODSx' (section B.4.141), `STOSx' (section B.4.303), `MOVSx' (section B.4.178), `INSx' (section B.4.121), `OUTSx' (section B.4.195), and `XLATB' (section B.4.334). 還有,就是變量壓棧與出棧指令,(`PUSHA'和`POPF' 和更常用的`PUSH'和`POP') 可以接受'a16'或'a32'前綴在堆棧段用在另一個不同size的代碼段中的時候,強 制一個特定的'SP'或'ESP'被用作棧指針, 'PUSH'和'POP',當在32位模式中被用在段寄存器上時,也會有一個不同的行爲, 它們會一次操作4bytes,而最高處的兩個被忽略,而最底部的兩個給出正被操作的 段寄存器的值.爲了強制push和pop指令的16位行爲,你可以使用操作數前綴'o16' o16 push ss o16 push ds 這段代碼在棧空間中開闢一個doubleword用於存放兩個段寄存器,而在一般情況 下,這一個doubleword只會存放一個寄存器的值. (你也可以使用'o32'前綴在16位模式下強制32位行爲,但這看上去並沒有什麼用 處.) 第十章: 答疑 --------------------------- 本章介紹一些用戶在使用NASM時經常遇到的普遍性問題,並給出解答.同時,如果你 發現了這兒還未列出的BUG,這兒也給出提交bug的方法. 10.1 普遍性的問題. 10.1.1 NASM產生了低效的代碼. 我得到了很多關於NASM產生了低效代碼的BUG報告,甚至是產生錯誤代碼,比如像 指令'ADD ESP,8'產生的代碼.其實這是一個經過深思熟慮設計特性,跟可預測的 輸出相關:NASM看到'ADD ESP,8'時,會產生一個預留32位偏移的指令形式.如果你 希望產生一個節約空間的指令形式,你必須寫上'ADD ESP,BYTE 8'.這不是一個BUG, 至多也只能算是一個不好的特性,各人看法不同而已. 10.1.2 我的jump指令超出範圍. 相似的,人們經常抱怨說在他們使用條件跳轉指令時(這些指令缺省狀況下是'short' 的)經常需要跳轉比較遠,而NASM會報告說'short jump out of range',而不作長 遠轉. 同樣,這也是可預測執行的一個部分,但實際上還有一個更有實際的理由.NASM沒有 辦法知道它產生的代碼運行的處理器的類型;所以它自己不能決定它應該產生' Jcc NEAR'類型的指令,因爲它不知道它正在386或更高一級的處理器上工作.相反, 它把可能超出範圍的短'JNE'指令替換成一個很短的'JE'指令,這個指令僅僅跳過 一個'JMP NEAR'指令;對於低於386的處理器,這是一個可行的解決方案,但是對於 有較好的分支預測功能的處理器很難有較好的效果,所以可代之以'JNE NEAR'.所 以,產生什麼的指令還是取決於用戶,而不是彙編器本身. 10.1.3 `ORG'不正常工作. 那些用'bin'格式寫引導扇區代碼的人們經常抱怨'ORG'沒有按他們所希望的那樣 正常工作:爲了把'0xAA55'放到512字節的引導扇區的末尾,使用NASM的人們會這樣 寫: ORG 0 ; some boot sector code ORG 510 DW 0xAA55 這不是NASM中使用'ORG'的正確方式,不會正常工作.解決這個問題的正確方法是使用 'TIMES'操作符,就象這樣: ORG 0 ; some boot sector code TIMES 510-($-$$) DB 0 DW 0xAA55 'TIME'操作符會在輸出中插入足夠數量的零把彙編點移到510.這種辦法還有一個 好處,如果你意外地在你的引導扇區中放入了太多的內容,以致超出容量,NASM會 在彙編時檢測到這個錯誤,並報告.所以你最終就不必重彙編並去找出錯誤所在. 10.1.4 `TIMES'不正常工作. 關於上面代碼的另一個普遍性的問題是,有人這樣寫'TIMES'這一行: TIMES 510-$ DB 0 因爲'$'是一個純數字,就像510,所以它們相減的值也是一個純數字,可以很好地 被TIMES使用. NASM是一個模塊化的彙編器:不同的組成部分被設計爲可以很容易的單獨重用,所 以它們不會交換一些不必要的信息.結果,'BIN'輸出格式儘管被'ORG'告知'.text' 段應當在0處開始,但是不會把這條信息傳給表達式的求值程序.所以對求值程序 來講,'$'不是一個純數值:它是一個從一個段基址開始的偏移值.因爲'$'和510'之 間的計算結果也不是一個純數,而是含有一個段基址.含有一個段基址的結果是不能 作爲參數傳遞給'TIMES'的. 解決方案就象上一節所描述的,應該如下: TIMES 510-($-$$) DB 0 在這裏,'$'和'$$'是從同一個段基址的偏移,所以它們相減的結果是一個純數,這 句代碼會解決上述問題,併產生正確的代碼. 10.2 Bugs 我們還從來沒有發佈過一個帶有已知BUG的NASM版本.但我們未知的BUG從來就是不 停地出現.你發現了任何BUG,應當首先通過在 `https://sourceforge.net/projects/nasm/'(點擊bug)的'bugtracker'提交給 我們,如果上述方法不行,請通過1.2中的某一個聯繫方式. 請先閱讀2.2,請不要把列在那兒的作爲特性的東西作爲BUG提交給我們.(如果你認 爲這個特性很不好,請告訴我們你認爲它應當被修改的原因,而不是僅僅給我們一 個'這是一個BUG')然後請閱讀10.1,請不要把已經列在那裏的BUG提交給我們. 如果你提交一個bug,請給我們下面的所有信息. (這部分信息一般用戶並不關心,在些省略,原文請參考NASM的英文文檔.) 附錄A: Ndisasm ------------------- 反彙編器, NDISASM A.1 簡介 反彙編器是彙編器NASM的一個很小的附屬品.我們已經擁有一個具有完整的指令 表的x86彙編器,如果不把這個指令表盡最大可能地利用起來,似乎很可惜,所以 我們又加了一個反彙編器,它共享NASM的指令表(並附加上一些代碼) 反彙編器僅僅產生二進制源文件的反彙編.NDISASM不理解任何目標文件格式,就 象'objdump',也不理解'DOS .EXE'文件,就象'debug',它僅僅反彙編. A.2 開始: 安裝. 參閱1.3的安裝指令.NDISASM就象NASM,也有一個幫助頁,如果你在一個UNIX系統下, 你可能希望把它放在一個有用的地方. A.3 運行NDISASM 要反彙編一個文件,你可以象下面這樣使用命令: ndisasm [-b16 | -b32] filename NDISASM可以很容易地反彙編16位或32位代碼,當然,前提是你必須記得給它指定是 哪種方式.如果'-b'開關沒有,NDISASM缺省工作在16位模式下.'-u'開關也包含32位 模式. 還有兩個命令行選項,'-r'打印你正運行的NDISASM的版本號,'-h'給你一個有關命 令行選項的簡短介紹. A.3.1 COM文件: 指定起點地址. 要正確反彙編一個'DOS.COM'文件,反彙編器必須知道文件中的第一條指令是被裝載 到地址'0x100'處的,而不是0,NDISASM缺省地認爲你給它的每一個文件都是裝載到0 處的,所以你必須告訴它這一點. '-o'選項允許你爲你正反彙編的聲明一個不同的起始地址.它的參數可以是任何 NASM數值格式:缺省是十進制,如果它以''$''或''0x''開頭,或以''H'結尾,它是十 六進制的,如果以''Q''結尾,它是8進制的,如果是''B''結尾,它是二進制的. 所以,反彙編一個'.COM'文件: ndisasm -o100h filename.com 能夠正確反彙編. A.3.2 代碼前有數據: 同步. 假設你正反彙編一個含有一些不是機器碼的數據的文件,這個文件當然也含有一些 機器碼.NDISASM會很誠實地去研究數據段,盡它的能力去產生機器指令(儘管它們中 的大多數看上去很奇怪,而且有些還含有不常見的前綴,比如:'FS OR AX, 0x240A'') 然後,它會到達代碼段處. 假設NDISASM剛剛完成從一個數據段中產生一堆奇怪的機器指令,而它現在的位置正 處於代碼段的前面一個字節處.它完全有可能以數據段的最後一個字節爲開始產生另 一個假指令,然後,代碼段中的第一條正確的指令就看不到了,因爲起點已經跳過這條 指令,這確實不是很理想. 爲了避免這一點,你可以指定一個'同步'點,或者可以指定你需要的同步點的數目(但 NDISASM在它的內部只能處理8192個同步點).同步點的定義如下:NDISASM保證會到達 這個同步點.如果它認爲某條指令會跳過一個同步點,它會忽略這條指令,代之以一個 'DB'.所以它會從同步點處開始反彙編,所以你可以看到你的代碼段中的所有指令. 同步點是用'-s'選項來指定的:它們以從程序開始處的距離來衡量,而不是文件位置. 所以如果你要從32bytes後開始同步一個'.COM'文件,你必須這樣做: ndisasm -o100h -s120h file.com 而不是: ndisasm -o100h -s20h file.com 就象上面所描述的,如果你需要,你可以指定多個同步記號,只要重複'-s'選項即可. A.3.3 代碼和數據混合: 自動(智能)同步. 假設你正在反彙編一個'DOS'軟盤引導扇區(可能它含有病毒,而你需要理解病毒,這 樣你就可以知道它可能會對你的系統造成什麼樣的損害).一般的,裏面會含有'JMP' 指令,然後是數據,然後接下來纔是代碼,所以,這很可能會讓NDISASM不能在數據與 代碼交接處找不到正確的點,所以同步點是必須的. 另一方面,你爲什麼要手工指定同步點呢?你要找出來的同步點的地址,當然是可以 從'JMP'中讀取,然後可以用它的目標地址作爲一個同步點,而NDISADM是否可以爲你 做到這一點? 答案當然是可以:使用同步開關'-a'(自動同步)或'-i'(智能同步)會啓用'自動同步 "模式.自動同步模式爲PC相關的前向引用或調用指令自動產生同步點.(因爲NDISASM 是一遍的,如果它遇上一個目標地址已經被處理過的PC相關的跳轉,它不能做什麼.) 只有PC相關的jump纔會被處理,因爲一個絕對跳轉可能通過一個寄存器(在這種情況 下,NDISASM不知道這個寄存器中含有什麼)或含有一個段地址(在這種情況下,目標代 碼不在NDISASM工作的當前段中,所以同步點不能被正確的設置) 對於一些類型的文件,這種機制會自動把同步點放到所有正確的位置,可以讓你不必 手工放置同步點.但是,需要強調的是自動模式並不能保證找出所有的同步點,你可能 還是需要手工放置同步點. 自動同步模式不會禁止你手工聲明同步點:它僅僅只是把自動產生的同步點加上.同 時指定'-i'和'-s'選項是完全可行的. 關於自動同步模式,另一個需要提醒的是,如果因爲一些討厭的意外,你的數據段中的 一些數據被反彙編成了PC相關的調用或跳轉指令,NDISASM可能會很誠實地把同步點放 到所有的這些位置,比如,在你的代碼段中的某條指令的中間位置.同樣,我們不能爲此 做什麼,如果你有問題,你還是必須使用手工同步點,或使用'-k'選項(下面介紹)來禁 止數據域的反彙編. A.3.4 其他選項. '-e'選項通過忽略一個文件開頭的N個bytes來跳過一個文件的文件頭.這表示在反匯 編器中,文件頭不被計偏移域中:如果你給出'-e10 -o10',反彙編器會從文件開始的 10byte處開始,而這會對偏稱域給出10,而不是20. '-k'選項帶有兩個逗號--分隔數值參數,第一個是彙編移量,第二個是跳過的byte數. 這是從彙編偏移量處開始計算的跳過字節數:它的用途是禁止你需要的數據段被反匯 編. A.4 Bug和改進. 現在還沒有已知的bug.但是,如果你發現了,並有了補丁,請發往`[email protected]' 或`[email protected]', 或者在`https://sourceforge.net/projects/nasm/'上的 開發站點,我們會改進它們,請多多給我們改進和新特性的建議. 將來的計劃包括能知道特定的指令運行在那種處理器上,並能標出那些對於某些處理 器來說過於高級的指令(或是'FPU'指令,或是沒有公開的操作符, 或是特權保護模式 指令,或是其它). 感謝所有的人們! 我希望NDISASM對一些人來說是有用的,包括我.:-) 我不推薦把NDISASM單獨出來,以考察一個反彙編器的工作性能,因爲到目前爲止,據 我所知,它不是一個高性能的反彙編器,你應當明確這一點. (完) FROM:http://alickguo.bokee.com/inc/nasm.txt
nasm 中文手冊
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.