彙編總結(3)——程式設計初步
堆疊的作用
組合語言中的堆疊就是高階語言中的棧。
堆疊主在彙編程式設計中主要有三個作用:
- 過程呼叫&返回指令
- 引數傳遞
- 區域性變數
過程呼叫&返回指令
過程呼叫中的過程指什麼?
組合語言中的過程就是高階語言裡面說的子程式,呼叫子程式(過程、函式)的本質就是控制轉移,它與無條件轉移的區別是呼叫子程式需要考慮返回。
過程呼叫指令用於由主程式轉移到子程式;
過程返回指令用於由子程式返回到主程式。
指令 | 中文名 | 格式 | 解釋 | 備註 | location |
---|---|---|---|---|---|
CALL | 過程呼叫指令 | CALL LABEL | 段內直接呼叫LABEL | 與jmp的區別在於call指令會在呼叫label之前儲存返回地址(call 中return之後主程式還可以繼續執行,jmp 當label執行完畢後不能返回主程式繼續執行) | p61 |
RET | 段內過程返回指令 | RET | 使子程式結束,繼續執行主程式 | P61 |
call指令的背後
段內直接呼叫的背後操作其實是兩步:
(1)把返回地址(EIP內容)壓入堆疊
(2)使得EIP內容為目標地址偏移,從而實現轉移
返回地址:緊隨過程呼叫指令的下一條指令的地址(有效地址)
目標地址:子程式開始處的地址(有效地址)
與無條件轉移相比,過程呼叫指令call只是多了第一步(保護現場)。
ret指令的背後
過程返回指令的執行其實進行的是如下操作:
從堆疊中彈出地址偏移,送到指令指標暫存器EIP中,這個返回地址通常就是在執行相應的呼叫指令時所壓入堆疊的返回地址。
引數傳遞
入口引數:主程式傳給子程式的引數
出口引數:子程式傳給主程式的引數
引數傳遞的方法主要有:暫存器傳遞法、堆疊傳遞法、約定記憶體單元傳遞法、call後續區傳遞法等。具體情況需要事先約定好。
一般c語言的習慣是使用堆疊傳遞入口引數,使用暫存器傳遞出口引數,因為一般入口引數比較多,出口引數比較少。
區域性變數
這裡就一個結論,堆疊可以用於安排動態區域性變數。
算術邏輯運算指令
乘除指令
無符號數乘法指令(MUL)
指令格式:
MUL OPRD
該指令實現兩個無符號數的乘法運算,乘數是OPRD,被乘數位於AL、AX或EAX中(由OPRD的尺寸決定)。
需要注意的是乘積之後尺寸翻倍,兩個8位的數乘積為16位,結果存放在AX中,類似的,兩個16位數的乘積結果為32位,放在DX:AX中,最後,64位的乘積放在EDX:EAX中。
運算元OPRD可以是通用暫存器、儲存單元,但是不能是立即數。
有符號數乘法指令(IMUL)
有符號乘法指令有三種使用形式:
IMUL OPRD;
IMUL DEST,SRC;
IMUL DEST,SRC1,SEC2;
具體解釋如下:
-
IMUL OPRD
單運算元乘法指令和無符號數乘法的規則差不多,只是在乘的時候把乘數和被乘數都當成有符號數。
-
IMUL DEST,SRC
資料流方向是DEST<=DEST*SRC
要求目的運算元DEST只能是16位或32位通用暫存器,源運算元SRC可以是通用暫存器或儲存單元,需與目的運算元尺寸一致,可以是一個立即數(尺寸不能超過目的運算元)。
乘數和被乘數均作為有符號數。
-
IMUL DEST,SRC1,SEC2
資料流方向為DEST<=SRC1*SRC2
目的運算元DEST只能是16位或32位通用暫存器。
SRC1可以是通用暫存器或者儲存單元,須與目的運算元尺寸一致,但不能是立即數。
SRC2只能是一個立即數,尺寸不能超過目的運算元。
被乘數和乘數均為有符號數。
無符號數除法指令(DIV)
一般格式:
DIV OPRD
OPRD是除數,被除數位於AX、DX:AX或EDX:EAX中,由OPRD的尺寸決定,被除數的尺寸翻倍,商在AL、AX或者EAX中,餘數在AH、DX或者EDX中,商和餘數的尺寸和OPRD相同。
運算元OPRD可以是通用暫存器,可以是儲存單元,但不能是立即數。
注意使用DIV指令時要防止除溢位,比如:
比如上圖,除完之後應該商300餘0,可是300超出了AL的表示範圍,這時候就產生了溢位情況,在實際使用中要注意防範類似情況。
有符號數除法指令(IDIV)
指令格式:
IDIV OPRD
基本原則和DIV一樣,不同之處在於:
- 除法時有符號的
- 如果不能整除,餘數的符號與被除數符號一致,而且餘數的絕對值小於除數的絕對值。
總結
指令 | 中文名 |
---|---|
MUL | 無符號數乘法指令 |
IMUL | 有符號數乘法指令 |
IMUL DEST,SRC | 有符號數乘法指令 |
IMUL DEST,SRC1,SRC2 | 有符號數乘法指令 |
DIV | 無符號數除法指令 |
IDIV OPRD | 有符號數除法指令 |
符號拓展指令
符號拓展指令的實質是用被拓展暫存器的符號位佔據目標拓展暫存器。
指令 | 中文名 | 格式 | 解釋 |
---|---|---|---|
CBW | 位元組轉化為字指令 | CBW | 把暫存器AL中的符號拓展到暫存器AH;如果AL最高有效位、為0,則AH=0,如果AL最高位為1,則AH=FFH |
CWD | 字轉化為雙字指令 | CWD | 把暫存器AX中的符號拓展到暫存器DX;AX最高位0和1不同情況的拓展策略同CBW |
CDQ | 雙字轉化為四字指令 | CDQ | 把暫存器EAX中的符號拓展到EDX;AX最高位0和1不同情況的拓展策略同CBW |
CWDE | 字轉化為雙字指令 | CWDE | 把AX中的符號拓展到EAX的高16位;AX最高位0和1不同情況的拓展策略同CBW |
使用舉例:
符號拓展傳送指令(MOVSX)
一般格式:
MOVSX DEST,SRC
把SRC符號拓展後送到DEST。
目的運算元的尺寸必須大於源運算元的尺寸。源運算元的尺寸可以是8位或16位,目的運算元的尺寸可以是8位或16位。
使用舉例:
零拓展傳送指令(MOVZX)
一般格式:
MOVZX DEST,SRC
把SRC零拓展後送到DEST。
源運算元可以是8位或16位,目的運算元可以是16位或32位。
使用舉例:
邏輯運算指令
需要注意:
只有通用暫存器或者儲存單元可作為目的運算元,用於存放運算結果。
指令 | 中文名 | 格式 | 解釋 | 備註 |
---|---|---|---|---|
NOT | 否運算指令 | NOT OPRD | 把運算元OPRD按位取反,然後送回OPRD | |
AND | 與運算指令 | AND DEST,SRC | 把兩個運算元進行與運算之後結果送回DEST | 同1得1,否則得0 |
OR | 或運算指令 | OR DEST,SRC | 把兩個運算元進行或運算之後結果送回DEST | 同0得0,否則得1 |
XOR | 異或運算 | XOR DEST,SRC | 把兩個運算元進行異或運算之後結果送回DEST | 相同得0不同得1 |
TEST | 測試指令 | TEST DEST,SRC | 與AND指令類似,將各位相與,但是結果不送回DEST,僅影響狀態位標誌,指令執行後,ZF、PF、SF反映運算結果,CF和OF被清零 | 通常用於檢測某些位是否為1,但又不希望改變運算元的值 |
test使用舉例:
判斷AL中的位6和位2是否有一位為1:
test al,01000100B;
隨後,判斷標誌位ZF,如果ZF為0,說明al第6位和第2位都為0,否則說明二者有一個為1.
移位指令
一般移位指令
指令 | 中文名 | 格式 | 解釋 | 備註 |
---|---|---|---|---|
SAL | 算術左移 | SAL OPRD,count | 把運算元oprd左移count位,右邊補0 | 與shl指令一樣 通過擷取count的低5位,實際的移位數被限於0到31之間。 |
SHL | 邏輯左移 | SHL OPRD,count | 把運算元oprd左移count位,右邊補0 | 與sal指令一樣 通過擷取count的低5位,實際的移位數被限於0到31之間。 |
SAR | 算術右移 | SAR OPRD,count | 把運算元oprd右移count位,同時每右移一位,左邊補符號位,移出的最低位進入標誌位CF | 通過擷取count的低5位,實際的移位數被限於0到31之間。 |
SHR | 邏輯右移 | SHR OPRD,count | 把運算元oprd右移count位,左邊補0,移出的最低位進入標誌位CF | 通過擷取count的低5位,實際的移位數被限於0到31之間。 |
迴圈移位指令
指令 | 中文名 | 格式 | 解釋 | 備註 |
---|---|---|---|---|
ROL | 左迴圈移位指令 | ROL OPRD,count | 左迴圈移一位之後最高位移到最低位的同時也進入CF | 通過擷取count的低5位,實際的移位數被限於0到31之間。 |
ROR | 右迴圈移位指令 | ROR OPRD,count | 右迴圈移一位之後最低位移到最高位的同時也進入CF | 通過擷取count的低5位,實際的移位數被限於0到31之間。 |
RCL | 帶進位左迴圈移位 | RCL OPRD,count | 相當於CF在最高位直接參與迴圈移位 | 大迴圈左移 通過擷取count的低5位,實際的移位數被限於0到31之間。 |
RCR | 帶進位右迴圈移位 | RCR OPRD,count | 相當於CF在最高位直接參與迴圈移位 | 大迴圈右移 通過擷取count的低5位,實際的移位數被限於0到31之間。 |
使用例項:
實現把al的最低位送到bl的最低位,仍保持al不變。
ror bl,1;//bl迴圈右移一位
ror al,1;//al迴圈右移一位,最低位進入cf
rcl bl,1;//bl帶進位左移,帶進了來自al的最低位(cf)
rol al,1;//恢復al
雙精度移位指令
雙精度移位指令是為了方便地把一個運算元的部分內容通過移位複製到另一個運算元。
格式:
- 雙精度左移:SHLD OPRD1,OPRD2,count
- 雙精度右移:SHRD OPRD1,OPRD2,count
解釋:
-
SHLD OPRD1,OPRD2,count
將OPRD1左移指定的count位,在低端空出的位用運算元OPRD2高階的count位填補,但是OPRD2內容保持不變,運算元OPRD1中最後移出的位保留在進位標誌CF中。
-
SHRD OPRD1,OPRD2,count
將OPRD1右移指定的count位,空出的位用OPRD2低端的count位填補,但是OPRD2內容保持不變,運算元OPRD1中最後移出的位保留在進位標誌CF中。
分支程式設計
無條件和條件轉移指令
段內轉移和段間轉移
- 段內轉移(近轉移):僅僅重新設定指令指標暫存器EIP的轉移,由於沒有調整CS,所以轉移後繼續執行的指令仍在同一程式碼段中。
- 段間轉移(遠轉移):不僅重新設定EIP,而且重新設定程式碼段暫存器CS的轉移,由於重置了CS,轉移後繼續執行的指令在另一程式碼段中。
對於段內轉移和段間轉移需要注意:
- 條件轉移指令和迴圈指令只能實現段內轉移;
- 無條件轉移指令和過程呼叫指令以及返回指令,既可以是段內轉移,也可以是段間轉移;
- 軟中斷指令和中斷返回指令一定是段間轉移;
直接轉移和間接轉移
- 直接轉移:轉移指令中直接給出轉移目標地址的轉移;
- 間接轉移:轉移指令中給出包含轉移目標地址的暫存器或者儲存單元的轉移;
需要注意無條件轉移指令和過程呼叫指令集可以是直接轉移也可以是間接轉移。
無條件轉移指令
無條件轉移指令分為4種:
- 段內直接轉移
- 段內間接轉移
- 段間直接轉移
- 段間間接轉移
需要說明的是無條件轉移指令均不影響標誌暫存器的狀態標誌。
無條件段內直接轉移
JMP LABEL;
標號LABEL表示要轉移的目標位置(轉移目的地)。
無條件段內直接轉移的機器碼構成如下:
操作碼OP 地址差rel
地址差rel實際上是LABEL所指定的指令的地址偏移與緊跟JMP指令的下一條指令的地址偏移之間的差值,rel可正可負,這樣才可以實現前後的跳轉。地址差rel可以用一個位元組表示,也可用4個位元組或2位元組表示,如果只用一個位元組表示,就稱之為短(short)轉移,否則稱為近(near)轉移。一般如果當彙編器彙編到某條轉移指令時可以計算出地址差rel,彙編器會自動判斷出應該用1位元組表示rel還是4位元組或2位元組,否則彙編器會使用較多的位數來表示地址差。所以,當程式設計師在寫程式時能顧及出用8位就可以表示出地址差,那麼可以在標號前加一個彙編器操作符**“SHORT”**來指定用一個位元組表示地址差,表示轉移的目的地就在附近。
無條件段內間接轉移
JMP OPRD
OPRD是32位通用暫存器或者雙字儲存單元,比如:
JMP ECX;
JMP DWORD PTR [EBX];
無條件段間轉移指令JMP
段間轉移指令和段內轉移指令差不多,只是涉及到改變程式碼段暫存器CS的內容,情況較為複雜,在之後的文章中介紹。
條件轉移指令
條件轉移指令在前一篇文章已經介紹,這裡不再贅述,只是需要特別明確一下rel偏移量的概念。
多路分支的實現
多路分支實際上指的就是switch-case的彙編實現,這裡的實現原理主要是通過無條件間接轉移指令和目標地址表來實現多路分支,舉個例子:
考慮一下多路分支程式:
int cf319(int x, int operation)
{
int y;
//多路分支
switch ( operation ) {
case 1:
y = 3*x;
break;
case 2:
y = 5*x+6;
break;
case 4:
case 5:
y = x*x ;
break;
case 8:
y = x*x+4*x;
break;
default:
y = x ;
}
if ( y > 1000 )
y = 1000;
return y;
}
上面的程式為了更加具有一般性,刻意沒有安排連續的case值,其彙編的實現如下:
push ebp
mov ebp, esp
; switch ( operation ) {
mov eax, DWORD PTR [ebp+12] ;取得引數operation(case值)
dec eax ;從0開始計算,所以先減去1
cmp eax, 7 ;從0開始計算,最多就是7
ja SHORT LN2cf319 ;超過,則轉default(LN2cf319對應defalut處理語句)
;
jmp DWORD PTR LN12cf319[ eax*4 ] ;這句是關鍵,這裡實現了多路分支
;
LN12cf319: ;多向分支目標地址表
DD LN6cf319 ; case 1
DD LN5cf319 ; case 2
DD LN2cf319 ; default
DD LN4cf319 ; case 4
DD LN4cf319 ; case 5
DD LN2cf319 ; default
DD LN2cf319 ; default
DD LN3cf319 ; case 8
看上面的彙編實現,其核心思想是巧妙地運用了無條件段內間接轉移和目標地址表,因為目標地址表每項佔4個位元組,所以跳轉的地址是目標地址表的起始地址加case物件(這裡是operation)乘以4,如果operation為0,跳到目標地址表的首地址,即就是LN6c,如果operation為1,跳到LN5c,以此類推就實現了多路分支的巧妙跳轉。
使用該方法的一般建議是:當多路分支數超過5時,考慮無條件間接轉移方式和目標地址表結合實現多路分支會更高效。
迴圈程式設計
迴圈指令
迴圈指令類似於條件轉移指令,其採用的是段內相對轉移的方式,是通過在指令指標暫存器EIP上加一個地址差的方式實現的轉移,需要注意的是迴圈指令中的這個地址差只用了一個位元組(8位)來表示,所以轉移範圍僅在-128-127之間。在保護方式(32位程式碼段)下,ECX作為迴圈計數器,實方式下,以CX為迴圈計數器,迴圈指令不影響各標誌位。
指令 | 中文名 | 格式 | 解釋 | 備註 |
---|---|---|---|---|
LOOP | 計數迴圈指令 | LOOP LABEL | 使ECX的值減1,當ECX的值不為0的時候跳轉至LABEL,否則執行LOOP之後的語句 | |
LOOPE | 等於迴圈指令 | LOOPE LABEL | 使ECX的值減1,如果結果不等於0並且零標誌ZF等於1(表示相等),那麼就轉移到LABEL,否則執行LOOPE之後的語句 | ECX的減1並不影響標誌位,ZF是否為1取決於迴圈指令之前指令對其的影響。 |
LOOPZ | 零迴圈指令 | LOOPZ LABEL | 使ECX的值減1,如果結果不等於0並且零標誌ZF等於1(表示相等),那麼就轉移到LABEL,否則執行LOOPZ之後的語句 | ECX的減1並不影響標誌位,ZF是否為1取決於迴圈指令之前指令對其的影響。 |
LOOPNE | 不等於迴圈指令 | LOOPE LABEL | 使ECX的值減1,如果結果不等於0並且零標誌ZF等於0(表示不相等),那麼就轉移到LABEL,否則執行LOOPNE之後的語句 | ECX的減1並不影響標誌位,ZF是否為1取決於迴圈指令之前指令對其的影響。 |
LOOPNZ | 非零迴圈指令 | LOOPNZ LABEL | 使ECX的值減1,如果結果不等於0並且零標誌ZF等於0(表示不相等),那麼9就轉移到LABEL,否則執行LOOPNZ之後的語句 | ECX的減1並不影響標誌位,ZF是否為1取決於迴圈指令之前指令對其的影響。 |
JECXZ | 計數轉移指令 | JECXZ LABEL | 當暫存器ECX的值為0時轉移到LABEL,否則順序執行 | 注意與LOOP的關係是JECXZ是直接判斷ECX,沒有先減ECX通常在迴圈開始之前使用該指令,所以迴圈次數為0時,就可以跳過迴圈體 |
子程式設計
呼叫約定
-
-_cdecl被稱為 C 呼叫約定。預設呼叫約定。引數按照從右至左的順序入堆疊,函式本身不清理堆疊。
-
_stdcall被稱為 pascal 呼叫約定。引數按照從右至左的順序入堆疊,函式自身清理堆疊。
-
_fastcall 是快速呼叫約定。通過 暫存器傳遞引數。前兩個引數由 ECX 和 EDX 傳送,其他引數按照從右至左的順序入堆疊,函式自身清理堆疊。