IA-32彙編語言筆記(9)—— 循環程序設計

  • 記錄彙編語言課筆記,可能有不正確的地方,歡迎指出
  • 教材《新概念彙編語言》—— 楊季文
  • 這篇文章對應書第二章 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
  • 簡單分析

    1. 堆棧示例
      在這裏插入圖片描述
    2. 32位數除法,先把被除數擴展到64位,這裏是無符號數,所以直接0擴展就行。使用64位無符號數除法div OPDR,被除數放在edx:eax中,除數OPDR這裏是ecx,商存在eax,餘數在edx
    3. 沒優化,改一個數的值要三步:從堆棧取到寄存器,改寄存器值,存回堆棧。幾乎所有數據計算之後都要先在堆棧更新,要用時再從堆棧取
  • 現在再打開優化反彙編一次

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
  • 簡單分析
    1. 堆棧示例
      在這裏插入圖片描述
    2. 仍使用64位無符號數除法div OPDR,但除數OPDR用了源變址寄存器esi,可能因爲本質是指針寄存器,所以這裏利用堆棧給它賦值,esi=0x0A
    3. 優化
      1. 每輪循環一開始就把n取到寄存器,循環結束時才存回,減少堆棧操作。
      2. 使用test指令判斷等於0

(2)循環指令

  1. 循環指令的說明

    1. 類似於條件轉移指令,段內轉移,相對轉移方式。
    2. 通過在指令指針寄存器EIP上加一個地址差的方式實現轉移
    3. 用一個字節(8位)表示地址差,轉移範圍僅在-128至+127之間
    4. 在保護方式(32位代碼段)下,以ECX作爲循環計數器。在實方式下,以CX作爲循環計數器
    5. 不影響各標誌。
  2. 計數循環指令LOOP

名稱 LOOP(計數循環指令)
格式 LOOP LABEL
動作 令使寄存器ECX的值減1,如果結果不等於0,則轉移到標號LABEL處,否則順序執行LOOP指令後的指令
注意 用於循環次數已知的循環,如for循環
計數器必須用ecx先設置計數器ECX初值,即循環次數。
由於首先進行ECX減1操作,再判結果是否爲0,所以最多可循環2322^{32}遍。
;統計寄存器EAX中位是1的個數

	  XOR   EDX, EDX        ;清EDX
      MOV   ECX, 32         ;設置循環計數
LAB1: SHR   EAX, 1          ;右移1位(最低位進入進位標誌CF)
      ADC   DL, 0           ;統計(實際是加CF)
      LOOP    LAB1         ;循環
  1. 等於/全零循環指令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			
  1. 不等於/非零循環指令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                        //返回
    }
}
  
  • 說明:
    1. 堆棧示意
      在這裏插入圖片描述
    2. 32位有符號數除法,先用CDQEAX符號擴展到EDX:EAX

二、綜合示例

  1. 把二進制數轉換爲十進制數的ASCII碼串
    1. 方法:

      1. 把一個整數除以10,所得的餘數就是個位數。
      2. 把所得的商再除以10,所得的餘數就是十位數。
      3. 繼續把所得的商除以10,所得的餘數就是百位數。
      4. 依次類推,就可以得到一個整數的各位十進制數字了。

      32位二進制數能表示的最大十進制數只有10位,循環地除上10次,就可以得到各位十進制數,注意這樣得到的結果最前面有若干個0

    2. 把一位十進制數轉換爲對應的ASCII碼,只要加上數字符‘0’的ASCII碼。

    3. 存放順序:
      由於先得到個位數,然後得到十位數,再得到百位數,所以在把所得的各位十進制數的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. 改進上面的程序,(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;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章