文章目录
- 前言
- 1. Debug和编译器masm的指令处理差异
- 1.1 示例引入
- 1.2 例题分析
- 1.2.1 在Debug中编程实现
- 1.2.2 汇编源程序实现
- 1.2.3 Debug的实际实施情况
- 1.2.4 汇编源程序的实际实施情况
- 1.2.5 两种情况的对比分析
- 1.3 问题与解决
- 1.3.1 问题
- 1.3.2 方法一
- 1.3.3 方法二
- 1.4 比较与总结
- 1.4.1 比较
- 1.4.2 总结
- 2. loop和[bx]的联合应用
- 2.1 问题引入
- 2.2 提问与分析
- 2.3 程序实现
- 2.4 程序改进
- 2.4.1 分析
- 2.4.2 程序实现
- 2.5 总结
- 结语
前言
????
汇编语言是很多相关课程(如数据结构、操作系统、微机原理)的重要基础。但仅仅从课程的角度出发就太片面了,其实学习汇编语言可以深入理解计算机底层工作原理,提升代码效率,尤其在嵌入式系统和性能优化方面有重要作用。此外,它在逆向工程和安全领域不可或缺,帮助分析软件运行机制并增强漏洞修复能力。
本专栏的汇编语言学习章节主要是依据王爽老师的《汇编语言》来写的,和书中一样为了使学习的过程容易展开,我们采用以8086CPU为*处理器的PC机来进行学习。
1. Debug和编译器masm的指令处理差异
1.1 示例引入
我们在 Debug 中写过类似的指令:
mov ax, [0]
表示将 ds:0处的数据送入 ax 中。
但是在汇编源程序中,指令“mov ax,[0]
”被编译器当作指令“mov ax,0
”处理。
下面通过具体的例子来看一下 Debug 和汇编编译器 masm 对形如“mov ax,[0]
”这类指令的不同处理。
1.2 例题分析
任务:将内存 2000:0、2000:1、2000:2、2000:3单元中的数据送入al,bl,cl,dl中。
1.2.1 在Debug中编程实现
程序如下:
mov ax,2000
mov ds,ax
mov al,[0]
mov bl,[1]
mov cl,[2]
mov dl,[3]
1.2.2 汇编源程序实现
程序如下:
assume cs:code
code segment
mov ax,2000h
mov ds,ax
mov al,[0]
mov bl,[1]
mov cl,[2]
mov dl,[3]
mov ax,4c00h
int 21h
code ends
end
1.2.3 Debug的实际实施情况
Debug 中的情况如下图所示。
1.2.4 汇编源程序的实际实施情况
将汇编源程序存储为compare.asm,用masm、link生成compare.exe,用 Debug加载compare.exe,如下图所示。
1.2.5 两种情况的对比分析
从上面的两个图中我们可以明显地看出,Debug 和编译器masm 对形如“mov ax,[0]
”这类指令在解释上的不同。
我们在 Debug 中和源程序中写入同样形式的指令:“mov al,[0]
”、“mov bl,[1]
”、“mov cl,[2]
”、“mov dl,[3]
”,但 Debug 和编译器对这些指令中的“[idata]”却有不同的解释。Debug 将它解释为“[idata]”是一个内存单元,“idata”是内存单元的偏移地址;而编译器将“[idata]”解释为“idata”。
1.3 问题与解决
1.3.1 问题
那么我们如何在源程序中实现将内存2000:0、2000:1、2000:2、2000:3单元中的数据送入 al,bl,cl,dl 中呢?
1.3.2 方法一
目前的方法是,可将偏移地址送入bx寄存器中,用[bx]的方式来访问内存单元。比如我们可以这样访问2000:0单元:
mov ax,2000h
mov ds,ax ;段地址2000h送入ds
mov bx,0 ;偏移地址0送入bx
mov al,[bx] ;ds:bx单元中的数据送入a1
1.3.3 方法二
上面的这种方法是可以,可是比较麻烦,我们要用bx来间接地给出内存单元的偏移地址。我们还是希望能够像在 Debug 中那样,在“[]”中直接给出内存单元的偏移地址。这样做,在汇编源程序中也是可以的,只不过,要在“[]”的前面显式地给出段地址所在的段寄存器。比如我们可以这样访问2000:0单元:
mov ax,2000h
mov ds ax
mov al,ds:[0]
1.4 比较与总结
1.4.1 比较
比较一下汇编源程序中以下指令的含义。
-
“
mov al, [0]
”,含义:(al)=0,将常量0送入al 中(与 mov al,0 含义相同); -
“
mov al, ds:[0]
”,含义:(al)=((ds)*16+0),将内存单元中的数据送入al中; -
“
mov al, [bx]
”,含义:(al)=((ds)*16+(bx)),将内存单元中的数据送入al中; -
“
mov al, ds:[bx]
”,含义:与“mov al, [bx]
”相同。
1.4.2 总结
从上面的比较中可以看出:
(1)在汇编源程序中,如果用指令访问一个内存单元,则在指令中必须用“[…]”来表示内存单元,如果在“[]”里用一个常量idata直接给出内存单元的偏移地址,就要在“[]”的前面显式地给出段地址所在的段寄存器。比如
mov al, ds:[0]
如果没有在“[]”的前面显式地给出段地址所在的段寄存器,比如
mov al, [0]
那么,编译器masm 将把指令中的“[idata]”解释为“idata”
(2)如果在“[]”里用寄存器,比如 bx,间接给出内存单元的偏移地址,则段地址默认在ds中。当然,也可以显式地给出段地址所在的段寄存器。
2. loop和[bx]的联合应用
2.1 问题引入
考虑这样一个问题,计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中。
我们还是先分析一下。
2.2 提问与分析
(1)运算后的结果是否会超出dx所能存储的范围?
ffff:0~ffff:b内存单元中的数据是字节型数据,范围在0-255之间,12个这样的数据相加,结果不会大于65535,可以在dx中存放下。
(2)我们是否将 ffff:0~ffff:b中的数据直接累加到dx中?
当然不行,因为ffff:0~ffff:b中的数据是8位的,不能直接加到16位寄存器dx中。
(3)我们能否将ffff:0~ffff:b中的数据累加到dl中,并设置(dh)=0,从而实现累加到dx中的目标?
这也不行,因为dl是8位寄存器,能容纳的数据的范围在0~255之间,ffff:0~ffff:b中的数据也都是 8 位,如果仅向dl中累加12个 8 位数据,很有可能造成进位丢失。
(4)我们到底怎样将ffff:0~ffff:b中的8位数据,累加到16位寄存器dx中?
从上面的分析中,我们可以看到,这里面有两个问题:类型的匹配和结果的不超界。
具体的说,就是在做加法的时候,我们有两种方法:
-
(dx)=(dx)+内存中的8位数据;
-
(dl)=(dl)+内存中的8位数据;
第一种方法中的问题是两个运算对象的类型不匹配,第二种方法中的问题是结果有可能超界。
2.3 程序实现
怎样解决这两个看似矛盾的问题?
目前的方法(在后面的内容中我们还有别的方法)就是我们得用一个16位寄存器来做中介。
我们将内存单元中的8位数据赋值到一个16位寄存器ax中,再将ax中的数据加到dx上,从而使两个运算对象的类型匹配并且结果不会超界。
想清楚以上的问题之后,编写程序代码如下。
assume cs:code
code segment
mov ax,0ffffh
mov ds,ax ;设置(ds)=ffffh
mov dx,0 ;初始化累加寄存器,(dx)=0
mov al,ds:[0]
mov ah,0 ;(ax)=((ds)*16+0)=(ffff0h)
add dx,ax ;向dx中加上ffff:0单元的数值
mov al,ds:[1]
mov ah,0 ;(ax)=((ds)*16+1)=(ffff1h)
add dx,ax ;向dx中加上 ffff:1单元的数值
mov al,ds:[2]
mov ah,0 ;(ax)=((ds)*16+2)=(ffff2h)
add dx,ax ;向dx中加上ffff:2单元的数值
mov al,ds:[3]
mov ah,0 ;(ax)=((ds)*16+3)=(ffff3h)
add dx,ax ;向dx中加上ffff:3单元的数值
mov al,ds:[4]
mov ah,0 ;(ax)=((ds)*16+4)=(ffff4h)
add dx,ax ;向dx中加上ffff:4单元的数值
mov al,ds:[5]
mov ah,0 ;(ax)=((ds)*16+5)=(ffff5h)
add dx,ax ;向dx中加上ffff:5单元的数值
mov al,ds:[6]
mov ah,0 ;(ax)=((ds)*16+6)=(ffff6h)
add dx,ax ;向dx中加上ffff:6单元的数值
mov al,ds:[7]
mov ah,0 ;(ax)=((ds)*16+7)=(ffff7h)
add dx,ax ;向dx中加上ffff:7单元的数值
mov al,ds:[8]
mov ah,0 ;(ax)=((ds)*16+8)=(ffff8h)
add dx,ax ;向dx中加上ffff:8单元的数值
mov al,ds:[9]
mov ah,0 ;(ax)=((ds)*16+9)=(ffff9h)
add dx,ax ;向dx中加上ffff:9单元的数值
mov al,ds:[0ah]
mov ah,0 ;(ax)=((ds)*16+0ah)=(ffffah)
add dx,ax ;向dx中加上ffff:a单元的数值
mov al,ds:[0bh]
mov ah,0 ;(ax)=((ds)*16+0bh)=(ffffbh)
add dx,ax ;向dx中加上ffff:b单元的数值
mov ax,4c00h ;程序返回
int 21h
code ends
end
上面的程序很简单,不用解释,大家一看就懂。不过,在看懂了之后,你是否觉得这个程序编得有些问题?
它似乎没有必要写这么长。这是累加ffff:0~ffff:b中的12个数据,如果要累加 0000:0~0000:7fff中的32KB个数据,按照这个程序的思路,将要写将近10万行程序(写一个简单的操作系统也就这个长度了????????????)。
2.4 程序改进
应用 loop 指令,改进上面的程序,使它的指令行数让人能够接受。
思考后看分析。
2.4.1 分析
可以看出,在程序中,有12个相似的程序段,我们将它们一般化地描述为:
mov al,ds:[X] ;ds:X指向ffff:X单元
mov ah,0 ;(ax)=((ds)*16+(X))=(ffffXh)
add dx,ax ;向dx中加上ffff:X单元的数值
我们可以看到,12个相似的程序段中,只有mov al ds:[X]
指令中的内存单元的偏移地址是不同的,其他都一样。而这些不同的偏移地址是在0≤X≤bH的范围内递增变化的。
我们可以用数学语言来描述这个累加的运算: s u m = ∑ X = 0 0 b H ( f f f f h ∗ 10 h + X ) sum=\sum_{X=0}^{0bH}(ffffh*10h+X) sum=∑X=00bH(ffffh∗10h+X)
从程序实现上,我们将循环做:
(al)=((ds)*16+X)
(ah)=0
(dx)=(dx)+(ax)
一共循环12次,在循环开始前(ds)=ffffh,X=0。ds:X 指向第一个内存单元。每次循环后,X递增,ds:X指向下一个内存单元。
完整的算法描述如下。
初始化:
(ds)=ffffh
X=0
(dx)=0
循环12次:
(al)=((ds)*16+X)
(ah)=0
(dx)=(dx)+(ax)
X=X+1
可见,表示内存单元偏移地址的X应该是一个变量,因为在循环的过程中,偏移地址必须能够递增。这样,在指令中,我们就不能用常量来表示偏移地址。我们可以将偏移地址放到 bx中,用[bx]的方式访问内存单元。在循环开始前设(bx)=0,每次循环,将bx中的内容加1即可。
最后一个问题是,如何实现循环12次?我们的loop指令该发挥作用了。
更详细的算法描述如下。
初始化:
(ds)=ffffh
(bx)=0
(dx)=0
(cx)=12
循环 12 次:
s: (al)=((ds)*16+(bx)) (ah)=0
(dx)=(dx)+(ax)
(bx)=(bx)+1
loop s
2.4.2 程序实现
最后,我们写出程序。
assume cs:code
code segment
mov ax,0ffffh
mov ds,ax
mov bx,0 ;初始化 ds:bx指向 ffff:0
mov dx,0 ;初始化累加寄存器dx,(dx)=0
mov cx,12 ;初始化循环计数寄存器cx,(cx)=12
S: mov al,[bx]
mov ah,0
add dx,ax ;间接向dx中加上((ds)*16+(bx))单元的数值
inc bx ;bx自增1,ds:bx指向下一个单元
loop s
mov ax,4c00h
int 21h
code ends
end
2.5 总结
在实际编程中,经常会遇到,用同一种方法处理地址连续的内存单元中的数据的问题。我们需要用循环来解决这类问题,同时我们必须能够在每次循环的时候按照同一种方法来改变要访问的内存单元的地址。这时,就不能用常量来给出内存单元的地址(比如,[0]、[1]、[2]中,0、1、2是常量),而应该用变量。“mov al,[bx]
”中的 bx 就可以看作一个代表内存单元地址的变量,我们可以不写新的指令,仅通过改变bx中的数值,改变指令访问的内存单元。
结语
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下。
也可以点点关注,避免以后找不到我哦!
Crossoads主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是作者前进的动力!