- 記錄彙編語言課筆記,可能有不正確的地方,歡迎指出
- 教材《新概念彙編語言》—— 楊季文
- 這篇文章對應書第二章 IA32處理器基本功能 3.4部分
一、循環程序設計
(1)循環程序設計示例
1. 兩種循環結構
2. 簡單循環示例
- 有簡單循環程序
//統計無符號整數n作爲十進制數時的位數
int cf320(unsigned int n)
{
int len = 0;
do {
len++;
n = n/10;
} while (n != 0);
return len ;
}
- 現在把它反彙編(關閉優化)
//堆棧傳參數,eax傳返回值
push ebp
mov ebp, esp
push ecx ;在堆棧,安排局部變量len
mov DWORD PTR [ebp-4], 0 ; len=0;
LN3cf320: ; do {
; len++;
mov eax, DWORD PTR [ebp-4]
add eax, 1
mov DWORD PTR [ebp-4], eax
; n = n/10;
mov eax, DWORD PTR [ebp+8]
xor edx, edx ;因n是無符號數,用XOR指令清0
mov ecx, 10
div ecx
mov DWORD PTR [ebp+8], eax
cmp DWORD PTR [ebp+8], 0
jne SHORT LN3cf320
; return len ;
mov eax, DWORD PTR [ebp-4] ;準備返回值
;}
mov esp, ebp ;撤銷局部變量len
pop ebp ;撤銷堆棧框架
ret
-
簡單分析
- 堆棧示例
- 32位數除法,先把被除數擴展到64位,這裏是無符號數,所以直接0擴展就行。使用64位無符號數除法
div OPDR
,被除數放在edx:eax
中,除數OPDR這裏是ecx,商存在eax
,餘數在edx
- 沒優化,改一個數的值要三步:從堆棧取到寄存器,改寄存器值,存回堆棧。幾乎所有數據計算之後都要先在堆棧更新,要用時再從堆棧取
- 堆棧示例
-
現在再打開優化反彙編一次
push ebp
mov ebp, esp
;ECX作爲len
xor ecx, ecx ;len=0;
push esi ;在使用ESI之前,保護之
LL3cf320: ;do {
; len++;
; n = n/10;
mov eax, DWORD PTR [ebp+8]
push 10 ;準備藉助堆棧送到ESI
xor edx, edx ;使得EDX=0
pop esi ;使得ESI=10
div esi
inc ecx
mov DWORD PTR [ebp+8], eax
test eax, eax ;測試n是否爲0?
jne SHORT LL3cf320
; return len ;
mov eax, ecx ;準備返回值
pop esi ;恢復ESI
;}
pop ebp
ret
- 簡單分析
- 堆棧示例
- 仍使用64位無符號數除法
div OPDR
,但除數OPDR用了源變址寄存器esi
,可能因爲本質是指針寄存器,所以這裏利用堆棧給它賦值,esi=0x0A - 優化
- 每輪循環一開始就把n取到寄存器,循環結束時才存回,減少堆棧操作。
- 使用test指令判斷等於0
- 堆棧示例
(2)循環指令
-
循環指令的說明
- 類似於條件轉移指令,段內轉移,相對轉移方式。
- 通過在指令指針寄存器EIP上加一個地址差的方式實現轉移。
- 只用一個字節(8位)表示地址差,轉移範圍僅在-128至+127之間。
- 在保護方式(32位代碼段)下,以ECX作爲循環計數器。在實方式下,以CX作爲循環計數器。
- 不影響各標誌。
-
計數循環指令LOOP
名稱 | LOOP(計數循環指令) |
---|---|
格式 | LOOP LABEL |
動作 | 令使寄存器ECX的值減1,如果結果不等於0,則轉移到標號LABEL處,否則順序執行LOOP指令後的指令 |
注意 | 用於循環次數已知的循環,如for循環 |
計數器必須用ecx先設置計數器ECX初值,即循環次數。 | |
由於首先進行ECX減1操作,再判結果是否爲0,所以最多可循環遍。 |
;統計寄存器EAX中位是1的個數
XOR EDX, EDX ;清EDX
MOV ECX, 32 ;設置循環計數
LAB1: SHR EAX, 1 ;右移1位(最低位進入進位標誌CF)
ADC DL, 0 ;統計(實際是加CF)
LOOP LAB1 ;循環
- 等於/全零循環指令LOOPE/LOOPZ
名稱 | LOOPE/LOOPZ(等於/全零循環指令) |
---|---|
格式 | LOOPE(LOOPZ) LABEL |
動作 | 指令使寄存器ECX的值減1,如果結果不等於0,並且零標誌ZF等於1(表示相等),則轉移到標號LABEL處,否則順序執行。 |
注意 | 適用於循環比較直到找到相等字符的情況 |
同一條指令,有兩個助記符 | |
指令本身實施的ECX減1操作不影響標誌 | |
可以在循環開始前把ecx設爲-1,相當於最大循環FFFFFFFFH-1次,退出循環後用not ecx 把ecx按位取反,即可得LOOPE執行次數 |
//在一個字符數組中查找第一個非空格字符,假設字符數組buff的長度爲100:
LEA EDX, buff ;指向字符數組首
MOV ECX, 100 ;
MOV AL, 20H ;空格字符
DEC EDX ;爲了簡化循環,先減1
LAB2:
INC EDX ;調整到指向當前字符
CMP AL, [EDX] ;比較
LOOPE LAB2
- 不等於/非零循環指令LOOPNE/LOOPNZ
名稱 | LOOPNE/LOOPNZ(等於/全零循環指令) |
---|---|
格式 | LOOPNE(LOOPNZ) LABEL |
動作 | 指令使寄存器ECX的值減1,如果結果不等於0,並且零標誌ZF等於0(表示不相等),則轉移到標號LABEL處,否則順序執行。 |
注意 | 適用於循環比較直到找到不相等字符的情況 |
同一條指令,有兩個助記符 | |
指令本身實施的ECX減1操作不影響標誌 | |
可以在循環開始前把ecx設爲-1,相當於最大循環FFFFFFFFH-1次,退出循環後用not ecx 把ecx按位取反,即可得LOOPNE執行次數 |
//演示LOOPNE指令的使用:嵌入彙編代碼形式,測量由用戶輸入的字符串之長度
#include <stdio.h>
int main( )
{ char string[100]; //用於存放字符串
int len; //用於存放字符串長度
printf("Input string:"); //由用戶輸入一個字符串
scanf("%s",string);
_asm
{
LEA EDI, str //使得EDI指向字符串
XOR ECX, ECX //假設字符串“無限長”
XOR AL, AL //使AL=0(字符串結束標記)
DEC EDI //爲了簡化循環,先減1
LAB3: INC EDI //指向待判斷字符
CMP AL, [EDI] //是否爲結束標記
LOOPNE LAB3 //如果不是結束標記,繼續循環
NOT ECX //據ECX,推得字符串長度
MOV len, ECX
}
printf("len=%d\n",len); //顯示爲len=12
return 0;
}
(3)計數器轉移指令
- 上面的第一條
LOOP
指令,提供了一種指定循環次數的方法,但它有一個問題:由於是先將ecx減一再判斷,當設定循環次數爲0時,實際上會循環FFFFFFFFH次。爲了解決這個問題,IA32專門提供了一條用ECX是否爲0作爲判斷條件的條件轉移指令JECXZ/JCXZ
名稱 | JECXZ/JCXZ(計數器轉移指令) |
---|---|
格式 | JECXZ(JCXZ) LABEL |
動作 | 指令實現當寄存器ECX(CX)的值等於0時轉移到標號LABEL處,否則順序執行。 |
注意 | 通常在上面幾條循環指令之前使用,這樣當循環次數爲0時,就可以跳過循環體 |
JECXZ對應判斷ECX值;JCXZ對應判斷CX值 |
//計算由用戶輸入的若干成績的平均值
//演示堆棧傳遞參數調用子程序和JECXZ指令的使用:
//注意JECXZ和LOOP配合
#include <stdio.h>
#define COUNT 5 //假設成績項數
int main()
{
int score[COUNT]; //用於存放由用戶輸入的成績
int i, average;
for (i=0; i < COUNT; i++)
{ //由用戶從鍵盤輸入成績
printf("score[%d]=", i);
scanf("%d", &score[i]);
}
//調用子程序計算成績平均值
_asm {
LEA EAX, score
PUSH COUNT //把數組長度壓入堆棧
PUSH EAX //把數組起始地址壓入堆棧
CALL AVER //調用子程序
ADD ESP, 8 //平衡堆棧
MOV average, EAX
}
printf("average=%d\n",average);
return 0;
_asm {
AVER: //子程序入口
PUSH EBP
MOV EBP, ESP
MOV ECX, [EBP+12] //取得數組長度
MOV EDX, [EBP+8] //取得數組起始地址
XOR EAX, EAX //將EAX作爲和sum
XOR EBX, EBX //將EBX作爲下標i
JECXZ OVER //如數組長度爲0,不循環累加
NEXT:
ADD EAX, [EDX+EBX*4] //累加
INC EBX //調整下標i
LOOP NEXT
CDQ //被除數符號擴展到64位,準備做除法
IDIV DWORD PTR [EBP+12]
OVER:
POP EBP //撤銷堆棧框架
RET //返回
}
}
- 說明:
- 堆棧示意
- 32位有符號數除法,先用
CDQ
把EAX
符號擴展到EDX:EAX
- 堆棧示意
二、綜合示例
- 把二進制數轉換爲十進制數的ASCII碼串
-
方法:
- 把一個整數除以10,所得的餘數就是個位數。
- 把所得的商再除以10,所得的餘數就是十位數。
- 繼續把所得的商除以10,所得的餘數就是百位數。
- 依次類推,就可以得到一個整數的各位十進制數字了。
32位二進制數能表示的最大十進制數只有10位,循環地除上10次,就可以得到各位十進制數,注意這樣得到的結果最前面有若干個0
-
把一位十進制數轉換爲對應的ASCII碼,只要加上數字符‘0’的ASCII碼。
-
存放順序:
由於先得到個位數,然後得到十位數,再得到百位數,所以在把所得的各位十進制數的ASCII碼存放到字符串中去時,要從字符串的尾部開始。
-
int main( )
{
unsigned uintx = 56789123; //無符號整型變量
char buffer[11]; //用於存放ASCII碼串的緩衝區
_asm
{
LEA ESI, buffer ;獲存放字符串的緩衝區首地址
MOV EAX, uintx ;取得待轉換的數據
MOV ECX, 10 ;循環次數(十進制數的位數)
MOV EBX, 10 ;十進制的基數是10
NEXT:
XOR EDX, EDX ;形成64位的被除數(無符號數除)
DIV EBX ;除以10,EAX含商,EDX含餘數
ADD DL, '0' ;把析出十進制位轉成對應的ASCII碼
MOV [ESI+ECX-1], DL ;保存到緩衝區
LOOP NEXT ;計數循環
;
MOV BYTE PTR [ESI+10],0 ;設置字符串結束標誌
}
printf("%s\n", buffer); //輸出字符串
return 0;
}
- 改進上面的程序,(1)設二進制數是有符號的。如果負數,則所得字符串的第一個字符應該是負號。
(2)不需要前端可能出現的字符‘0’
int main( )
{
int intx = -57312;
char buffer[16]; //足夠長
//printf(“%d\n”,intx);
_asm
{
LEA ESI, buffer ;置指針初值
MOV EAX, intx ;取得待轉換的數據
CMP EAX, 0 ;判斷待轉換數據是否爲負數
JGE LAB1 ;非負數,轉
MOV BYTE PTR [ESI], '-' ;先保存一個負號
INC ESI ;調整指針
NEG EAX ;取相反數,得正數
LAB1:
MOV ECX, 10 ;最多循環10次
MOV EBX, 10 ;每次除以10
MOV EDI, 0 ;置有效位數的計數器初值
NEXT1:
XOR EDX, EDX
DIV EBX ;獲得1位十進制數
;
PUSH EDX ;把所得1位十進制數壓入堆棧
INC EDI ;有效位數增加1
;
OR EAX, EAX ;測試結果(商)
LOOPNE NEXT1 ;如結果不爲0,考慮繼續循環
MOV ECX, EDI ;置下一個循環的計數
NEXT2:
POP EDX ;從堆棧彈出餘數
ADD DL, '0' ;轉成對應的ASCII碼
MOV [ESI], DL ;依次存放到緩衝區
INC ESI
LOOP NEXT2 ;循環處理下一位
;
MOV BYTE PTR [ESI], 0 ;設置字符串結束標誌
}
printf("%s\n", buffer); //輸出字符串
return 0;
}