Skip to content

汇编与调试第5周复习笔记(含老师样例代码)

1. 本周学习目标

  • 掌握基于 CF 的二进制输出两种写法(分支法与 adc 法)
  • 掌握十进制输出的经典套路:除 10 取余 + 压栈逆序
  • 理解 push/pop 的执行本质和小端存储
  • 理解 段地址:偏移地址 与 byte/word/dword ptr

2. 知识点整理

2.1 CF 与字符输出

  • shl 会把移出的最高位送入 CF
  • 可以用 jc/jnc 分支输出 0/1
  • 也可以用 adc 直接把 CF 转成字符('0' 或 '1')

2.2 十进制转换套路

  • 每轮 div 10 得到一位余数(低位)
  • 余数先压栈
  • 最后出栈打印,得到高位到低位顺序

2.3 栈与小端

  • push 先减 SP 再写内存
  • pop 先读内存再加 SP
  • 多字节按小端顺序存放:低字节在低地址

2.4 段地址与偏移地址

  • 逻辑地址可写为 ds:[bx]
  • 宽度修饰决定读取字节数:byte/word/dword ptr
  • 给 DS 赋值常用两步:mov ax, imm + mov ds, ax

3. 老师样例代码(第5周)

3.1 1.asm(shl + jc 输出二进制)

asm
;把32位非符号整数转化成二进制输出
.386
code segment use16
assume cs:code
main:
   mov eax, 12345678h
   mov cx, 32
again:   
   shl eax, 1
   mov ebx, eax ; mov指令不影响FL中的标志位
   jc is_1 ; jc表示当CF==1时则跳,而jnc则表示CF==0时则跳
is_0:   
   mov dl, '0'
   jmp output
is_1:
   mov dl, '1'
output:
   mov ah, 2
   int 21h
   mov eax, ebx
   sub cx, 1
   jnz again
   mov ah, 4Ch
   mov al, 0
   int 21h
code ends
end main

3.2 2.asm(shl + adc 无分支输出二进制)

asm
;把32位非符号整数转化成二进制输出
.386
code segment use16
assume cs:code
main:
   mov eax, 12345678h
   mov cx, 32
again:   
   shl eax, 1
   mov ebx, eax ; mov指令不影响FL中的标志位
   mov dl, 0
   adc dl, '0' ; dl = dl + '0' + CF
               ; adc表示add with carry flag
output:
   mov ah, 2
   int 21h
   mov eax, ebx
   sub cx, 1
   jnz again
   mov ah, 4Ch
   mov al, 0
   int 21h
code ends
end main

3.3 3.asm(无符号十进制输出)

asm
; 把eax中的非符号数转化成十进制格式输出
.386
code segment use16
assume cs:code
main:
   mov eax, 2147483647
   mov ebx, 10 ; 除数
   mov ecx, 0  ; 统计除法的次数即十进制位数
div_again:   
   mov edx, 0
   div ebx ; edx、eax / ebx = eax..edx
   add edx, '0'
   push edx ; 把edx压入堆栈
   add ecx, 1
   cmp eax, 0
   jnz div_again
pop_again:
   pop edx
   mov ah, 2
   int 21h
   sub ecx, 1
   jnz pop_again
   mov ah, 4Ch
   mov al, 0
   int 21h
code ends
end main

3.4 pushpop.asm(push/pop 原理讲解)

text
push、pop的语法:
push 一个16位的寄存器或16位的变量
push 一个32位的寄存器或32位的变量
pop  一个16位的寄存器或16位的变量
pop  一个32位的寄存器或32位的变量

push、pop的原理:      
设堆栈指针sp=0x1000 
mov ax, 1234h
mov bx, 5678h
push ax ; CPU在执行此指令时会做以下步骤:
        ;   sp = sp - 2 = 0x0FFE
        ;   *sp = ax
  +0FFE  34h  ;1234h必须按小端规则保存到堆栈中
  +0FFF  12h  ;小端规则(little-endian)是指宽度大于8位的值保存
  +1000       ;到内存中时必须把低8位保存到低地址处,
              ;高8位保存到高地址处

push bx ; CPU在执行此指令时会做以下步骤: 
        ;   sp = sp - 2 = 0x0FFC         
        ;   *sp = bx    
  +0FFC  78h
  +0FFD  56h
  +0FFE  34h  ;1234h必须按小端规则保存到堆栈中
  +0FFF  12h  ;小端规则(little-endian)是指宽度大于8位的值保存
  +1000       ;到内存中时必须把低8位保存到低地址处,
              ;高8位保存到高地址处

设堆栈指针sp=0x1000 
mov eax, 12345678h
mov ebx, 89ABCDEFh 
push eax      ; ①sp=sp-4=0FFCh ②*sp=eax
  +0FFC  78h  ;12345678h必须按小端规则保存到堆栈中               
  +0FFD  56h  ;小端规则(little-endian)是指宽度大于8位的值保存
  +0FFE  34h  ;到内存中时必须把低8位保存到低地址处,         
  +0FFF  12h  ;高8位保存到高地址处                           
  +1000       
              
push ebx       ; ①sp=sp-4=0FF8h ②*sp=ebx
  +0FF8  0EFh  ;89ABCDEFh必须按小端规则保存到堆栈中               
  +0FF9  0CDh  ;小端规则(little-endian)是指宽度大于8位的值保存
  +0FFA  0ABh  ;到内存中时必须把低8位保存到低地址处,         
  +0FFB  89h   ;高8位保存到高地址处                           
  +0FFC  78h
  +0FFD  56h
  +0FFE  34h  
  +0FFF  12h  
  +1000       
              

pop ebx       ; ①ebx = *sp; ②sp=sp+4=0FFCh
  +0FF8  0EFh ; pop ebx前sp=0FF8h
  +0FF9  0CDh
  +0FFA  0ABh
  +0FFB  89h
  +0FFC  78h  ; pop ebx后sp=0FFCh
  +0FFD  56h
  +0FFE  34h  
  +0FFF  12h  
  +1000       
              
pop eax       ; ①eax=*sp; sp=sp+4=1000h
  +0FFC  78h  ; pop eax前sp=0FFCh
  +0FFD  56h
  +0FFE  34h  
  +0FFF  12h  
  +1000       ; pop eax后sp=1000h

3.5 2.c(与 2.asm 对照)

c
//把32位非符号整数转化成二进制输出
#include <stdio.h>
main()
{
   int a = 0x87654321, mask = 0x80000000;
   int i;
   for(i=0; i<32; i++)
   {
      /*
      if((a & mask) != 0) // if(a & mask)
         putchar('1');
      else
         putchar('0');
       */
      // putchar(((a & mask) != 0) + '0');
      // putchar(!!(a & mask) + '0');
      putchar((a<0) + '0');
      a = a << 1; // a <<= 1;
   }
}

3.6 p.c(指针与下标等价)

c
#include <stdio.h>
main()
{
   char a[]="ABCD", *p;
   p = a;
   putchar(p[0]);
   putchar(0[p]);  // 0[p] ==> *(0+p) 
   putchar(*p);
}

3.7 段地址偏移地址.txt(已按正常中文还原)

text
1. 段地址、偏移地址

char c;
char *p = (char *)0x12345;
c = *p; // c = 0x56;
物理地址  值
10000h    12h ; 10000h是段起始地址, 其十六进制的个位必须=0
...
12344h    34h ; 12344h的偏移地址=2344h,段地址=1000h
12345h    56h ; 12345h的16位偏移地址=12345h-10000h=2345h
...           ; 12345h的16位段地址=段首地址去掉个位=1000h
1FFFFh    78h ; 1FFFFh是段的末地址, 段长度=
              ; 1FFFFh-10000h+1 = 10000h字节 = 64K
              ; 64*2的10次方 = 2的6次方*2的10次方
              ; = 2的16次方即10000h
如何把物理地址12345h处的字节56h保存到寄存器al中?
mov ax, 1000h
mov ds, ax    ; ds是数据段寄存器(data segment简称)
              ; 注意ds不能接受常数给它赋值, 它只能接受
              ; 一个16位的寄存器或16位的变量给它赋值;
              ; 故不能写成mov ds, 1000h
mov bx, 2345h ; bx=偏移地址=2345h
mov al, ds:[bx]; 相当于C语言的 al = *(ds:bx);
              ; 其中ds:bx是逻辑地址, 它起到了C语言的指针的作用
              ; al = 56h

2. 汇编语言中变量的一般表示形式:
    宽度修饰 用段寄存器表示的段地址:[偏移地址]
其中偏移地址既可以用常数表示,也可以用寄存器+常数表示
宽度修饰包括以下几种:
(1) byte ptr  表示变量的宽度是8位
(2) word ptr  表示变量的宽度是16位
(3) dword ptr 表示变量的宽度是32位

10000h    12h ; 10000h是段起始地址, 其十六进制的个位必须=0
...
12344h    34h ; 12344h的偏移地址=2344h,段地址=1000h
12345h    56h ; 12345h的16位偏移地址=12345h-10000h=2345h
              ; 12345h的16位段地址=段首地址去掉个位=1000h
12346h    78h
12347h    9Ah
...
1FFFFh    78h ; 1FFFFh是段的末地址, 段长度=
              ; 1FFFFh-10000h+1 = 10000h字节 = 64K
              ; 64*2的10次方 = 2的6次方*2的10次方
              ; = 2的16次方即10000h
设ds=1000h, bx=2344h, 现在要把12344h处的字节赋值给al
mov al, byte ptr ds:[bx] ; al = *(char *)(ds:bx)
                         ; byte=char, ptr=第2颗*号

mov al, ds:[bx] ; 跟上面的语句等价, 因为根据al的宽度为8位
                ; 编译器可以推断ds:[bx]也是8位,故byte ptr可以省略
                ; al = 34h
                
设ds=1000h, bx=2344h, 现在要把12344h处的short int赋值给ax
mov ax, word ptr ds:[bx]; word=short int
                        ; ax=5634h

设ds=1000h, bx=2344h, 现在要把12344h处的long int赋值给eax
mov eax, dword ptr ds:[bx]; dword=long int
                          ; eax=9A785634h

4. 本周易错点清单

  • shl 后 CF 只反映“移出的位”,不是任意位
  • adc 用法不熟,忘记先把 dl 清零
  • 十进制输出忘记入栈,导致输出反序
  • push/pop 宽度与寄存器不匹配
  • 给 DS 直接赋立即数
  • 忽略小端顺序,word/dword 读取结果判断错

5. 复习建议

  1. 把 1.asm 和 2.asm 对照着背,理解有分支与无分支两种写法。
  2. 手写 3.asm,先写除法循环,再写出栈循环。
  3. 对照 段地址偏移地址 一节,自己画一次内存地址与字节分布图。