汇编程序设计与计算机体系结构软件工程师教程笔记:指令
《匯編程序設計與計算機體系結構: 軟件工程師教程》這本書是由Brain R.Hall和Kevin J.Slonka著,由愛飛翔譯。中文版是2019年出版的。個人感覺這本書真不錯,書中介紹了三種匯編器GAS、NASM、MASM異同,全部示例代碼都放在了GitHub上,包括x86和x86_64,并且給出了較多的網絡參考資料鏈接。這里只摘記了NASM和MASM,測試代碼僅支持Windows和Linux的x86_64。
4. 基本指令
4.1 簡介:
在很多情況下,MASM會根據上下文來推測指令中的操作數是什么類型,有的時候NASM也是這樣。在撰寫指令的目標操作數時,如果要對變量解引用(dereference),NASM要求你必須指出大小,也就是必須在變量名的前面寫上一個表示尺寸的命令,例如用BYTE表示字節、WORD表示字等,比方說像這樣:”mov DWORD [test], eax”
4.2 數據的移動與算術運算:
MOV指令有幾條具體的要求:
(1). 兩個操作數的大小必須相同;
(2). 兩個操作數不能全是內存操作數(也就是說要想在兩個內存操作數之間移動數據,必須用寄存器做中介);
(3). 指令指針寄存器(ip/eip/rip)不能用作目標操作數。
使用XCHG指令可令兩個位置上的數據彼此交換。和MOV指令類似,XCHG指令的兩個操作數也不能全都是內存操作數。
加法與減法:INC、DEC、ADD、SUB
INC與DEC指令可以操作內存操作數(變量)或寄存器,這兩個指令的目標很簡單,就是給操作數加1或減1。
NASM對待變量的方式有點像C++對待指針的方式:如果直接寫出變量名本身,那么它會將其視為內存地址。要想引用該地址中的內容(也就是由變量名所指代的那份數據),必須用一對方括號給變量解引用,如:”INC DWORD [sum]”
ADD與SUB指令可以用字面量來做加減法,也可以用內存或寄存器中的值來計算。
NEG指令可以切換操作數的正負號,也就是對操作數的值求補。
乘法與除法:MUL、IMUL、DIV、IDIV
MUL指令用來給無符號的整數執行乘法。該指令只接受一個操作數即乘數(multiplier)。被乘數保存在與乘數尺寸相對應的累加寄存器中。與被乘數一樣,積的保存位置也無須手工指定,因為這是由MUL指令自動指定的,如下表所示:M指內存或變量,R指寄存器,L指字面值,被乘數與積的存放地點是根據乘數的尺寸默認指定的。
帶符號的整數相乘要通過IMUL指令執行。與MUL一樣,該指令也有單操作數的版本,在這種情況下,它所支持的操作數與上表中所列的相同。這條指令與MUL的主要區別在于它還有雙操作數及三操作數的版本。雙操作數的版本與ADD及SUB指令類似,也就是會把這兩個操作數都當成運算的源數據,并且要將其中一個操作數用作運算的目標,以便存放計算結果。因此對于保存運算結果的目標操作數來說,IMUL與ADD及SUB一樣都會將其中原有的值覆蓋掉。如果不想讓IMUL指令把這個值覆蓋掉,那么可以使用三操作數的版本。該版本要求開發者指出乘數、被乘數與積的位置。
與MUL類似,除法也分為無符號與帶符號兩種。DIV指令用來執行無符號整數的除法運算,它會將結果以商與余數的形式分別保存,如下表所示:
使用DIV指令的時候只需要指定除數就可以了。被除數需要提前加載到與除數尺寸相對應的寄存器里。
帶符號的整數需要用IDIV指令來相除。與IMUL指令不同,該指令沒有雙操作數或三操作數的版本,還是只提供一個操作數,即除數。與乘法指令不同,這兩種除法指令都不會通過設定狀態標志來反映與運算結果有關的信息,也就是說,相關標志的值是未定義(undefined)的。
移位:左移位(shift left)指令SHL與右移位(shift right)指令SHR都可以把內存操作數或寄存器中的值移動一定的位數,這個位數用字面量來指定。SHL和SHR是邏輯移位(logical bit shift),這會令某些二進制位出現在存儲范圍之外,同時會令空出來的那些數位都填上0.無符號的數據通常是可以做邏輯移位的。如果想通過左移位或右移位的方式給帶符號的整數做乘除法,必須使用算術移位(arithmetic bit shift)指令以便保留其符號位。SAL(Shift Arithmetic Left)是算術左移,SAR(Shift Arithmetic Right)是算術右移。SAL與SAR的語法跟對應的邏輯移位指令類似,都是將內存操作數或寄存器中的值移動一定的位數,這個位數也用字面量來指定。
處理負值:4種能夠執行符號擴展(sign extension)的指令,也就是CBW(將BYTE轉成WORD)、CWD(將WORD轉成DWORD)、CDQ(將DWORD轉成QWORD),以及CQO(將QWORD轉成OCTA),如下表所示:
4.3 數據的尋址與傳輸:
數據對齊:CPU訪問存放在偶數地址上的數據要比訪問存放在奇數地址上的數據更快。每種匯編器都有1條或多條命令用來修改位置計數器。MASM用的是ALIGN命令,NASM則依據不同的程序段分別使用ALIGN或ALIGNB命令。這幾種命令的參數都必須是整數,而且應該是2的冪。這些命令會推進位置計數器,直到它的值變為該整數的倍數為止。這可以用來確保數據出現在偶數的內存地址上。
數據尋址:直接尋址(direct addressing)是直接訪問某個值,而間接尋址(indirect addressing)則是通過值所代表的內存地址來訪問另一個值。NASM的變量表示的就是其內存地址,而不是該地址中的值(如果想使用這個值,要用方括號括起來)。在MASM代碼中,操作數的地址可以結合MOV指令及OFFSET命令來獲取。LEA(Load Effective Address, 加載有效地址)指令在32位與64位模式之下都可以把操作數的地址加載到目標中。由于實際地址要在程序運行的時候才能夠知道,因此,涉及這些地址的操作應該用LEA來完成。就獲取內存地址而言,MOV與LEA指令之間的重要區別在于,后者的目標操作數必須是寄存器。
數組:匯編語言里的數組也是由類型相同的一系列數據構成的,這些數據在內存中以相等的間隔存放。數組的名稱實際上指的是該數組中的首個元素。訪問數組元素時,一定要正確算出該元素距離數組開頭有多少個字節。匯編器不會自動檢查你所做的訪問是否與元素之間的邊界相合。字符串(也就是由字符所構成的數組)的長度可以通過當前位置計數器(current location counter)來計算。其實不只是字符串,其它數組所占據的字節數也可以用這個辦法求出。
用MASM代碼來操作數組時的專用命令:TYPE命令返回變量所占據的字節數;LENGTHOF命令返回數組中的元素個數;SIZEOF命令則返回整個數組所占據的總字節數(相當于把TYPE命令與LENGTHOF命令所返回的結果乘起來)。
改變數據的大小及類型:MASM版本的代碼需要使用一種新的命令即PTR命令,它會把早前定義的變量尺寸忽略掉,轉而將該變量視為一個指向某份數據的指針,這份數據的大小由PTR前面的詞決定。
我們在將數值復制到大寄存器的低半區時,應該使用MOVZ/MOVZX(ZX表示Zero eXtend,用0來擴展)與MOVS/MOVSX(SX表示Sign eXtend, 用符合位來擴展)兩種指令,因為它們能夠在復制的同時,用0位或符合位來填充高半區里的每一個二進制位。
5. 中級指令
5.2 按位執行的布爾運算:
NOT(非):它會分別反轉操作數的每一個二進制位。這實際上就是在計算操作數的反碼(或對操作數求反)。用來做NOT運算的那條指令也叫做NOT,它帶有一個操作數。該指令將會對此數求反并把結果寫入原位置。
AND(與):有一種常見的用途是通過AND來判斷某值是偶數還是奇數。判斷的原理是:檢查受測數字的最低有效位(LSB)是0還是1。還有一個任務也可以通過AND操作迅速解決,即將ASCII字符從小寫變成大寫。大寫與小寫字母所對應的二進制值,大寫與小寫是由第5個二進制位(假設最右側的叫做第0位)來控制的,除此之外,其它那些二進制位是完全相同的。此外,OR或XOR操作也可以用來轉換大小寫。前者用來將大寫轉成小寫,后者可以實現雙向切換:如果原字母是小寫就將其改為大寫,如果原字母是大寫就講其改寫小寫。AND操作所對應的指令也叫做AND。
OR(或):如果想實現跟AND運算相反的效果(也就是置1而不是清零),可以用OR指令,對兩個操作數的每個二進制位分別執行OR運算。OR還有一個用法,是判斷某數為正、為負,還是為0。只要把受測的數與它自身做OR運算,就可以根據結果判明這三種情況了。之所以能夠如此,是因為OR與AND指令都會設置處理器中的許多標志位,例如CF、OF、PF、SF及ZF等。
XOR(異或):與OR類似,用來在兩個操作數的每一對二進制位之間執行XOR運算。不過,它與OR有個關鍵的區別在于:只有當兩個操作數一個是True一個是False時結果才是True,如果兩個操作數均為True或均為False,那么結果是False。由于XOR運算是可逆的(reversible),因此成為很多對稱加密(symmetric encryption)算法與數據存儲算法中的重要環節。將原值與另一個值(此值稱作鍵,key)連取兩次XOR,就可以令運算結果回到原值。
5.3 分支:branch,意思是說程序可以根據開發者所實現的邏輯進入不同的執行路徑,甚至可以直接跳過某些指令。分支有兩種形式,一種是無條件地進入某路徑,另一種是根據條件來做測試,從而依照測試結果進入相應的路徑。
無條件跳轉:標簽只是用來在代碼中標明某個位置而已,它本身并不占據內存空間。在匯編代碼中使用標簽的其中一個原因是要給JMP指令提供跳轉目標。這條指令能夠直接令程序跳轉到某個地方。
有條件跳轉:在匯編語言中執行條件測試有多種辦法,其中之一是使用TEST指令。該指令會對兩個操作數執行AND運算,但并不修改二者的值,而且也不保存計算結果。不過,它會根據計算結果設置處理器的PF、SF及ZF等標志位。在結果中,如果值為1的二進制位是偶數個,PF標志就是1,否則為0,SF標志反映結果的最高有效位,ZF標志反映結果是否為0.CF與OF也會為TEST指令所修改,無論計算結果如何,這兩個標志位總是會清零。
還有一種方法也可以做條件測試,就是用CMP指令來比較(compare)兩個操作數。這能夠實現出與高級語言類似的條件判斷邏輯(例如等于、大于、小于等于)。匯編語言的條件跳轉至少要用兩條指令實現,首先,用一條指令比較兩個操作數,然后,用一條或多條指令來處理比較結果。基本的條件測試邏輯可以由CMP指令起頭。該指令會用目標操作數減去來源操作數,并根據計算結果修改CPU的相關標志位。與TEST指令類似,CMP指令不會修改這兩個操作數,而且也不保存計算結果。比較了兩個操作數之后可以用很多種辦法來跳轉,下表列出了常用的跳轉指令以及每條指令所判斷的CPU標志位:選擇跳轉指令的時候一定要注意操作數是無符號數還是帶符號數。
5.4 重復執行:重復(repetition)也叫做循環(looping),意思是多次執行預先寫好的某一組指令,具體的執行次數通常由某個計數器變量或條件來控制。
用CX/ECX/RCX計數器實現循環:由計數器所控制的循環很容易就能用匯編語言的LOOP指令實現出來。該指令會把某個”C系列的寄存器”(也就是cx/ecx/rcx)當作遞減計數器(decrementing counter)來用,每循環一次就將該計數器的值減1.值降為0的時候,結束循環并繼續執行LOOP下方的指令。
下面列出了各匯編器的規則:
(1).MASM與NASM代碼用$表示當前位置計數器(current location counter)。
(2).MASM及NASM代碼要先寫目標操作數,后寫來源操作數。
(3).NASM的標識符區分大小寫。
(4).MASM的標識符默認不區分大小寫,但是可以添加”option casemap:none”命令來區分(這一般添加在.MODEL命令之后)。
(5).NASM代碼用不加括號的ST0、ST1等寫法來表示FPU棧寄存器。
(6).MASM通常用”%st(1)/ST(1)”、”%st(2)/ST(2)”這樣帶括號的寫法來表示FPU棧寄存器。
(7).NASM用EQU命令將表達式的值設定給某個符號;MASM可以用=號,也可以用EQU命令。
(8).MASM和NASM都用單引號或雙引號把字符串括起來。
(9).MASM的指令會自動根據某些因素(例如數據的大小)做出相應的處理,因此,有的時候很難看清楚這樣一條指令究竟會如何執行。
(10).NASM通常不要求開發者用表示數據大小的命令來修飾源操作數,但如果用了也是可以的,而目標操作數的大小則必須明確,不然就要通過命令來指明。
GitHub:https://github.com/fengbingchun/CUDA_Test
總結
以上是生活随笔為你收集整理的汇编程序设计与计算机体系结构软件工程师教程笔记:指令的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 汇编程序设计与计算机体系结构软件工程师教
- 下一篇: 汇编程序设计与计算机体系结构软件工程师教