一、操作符offset
操作符offset
在汇编语言中是由编译器处理的符号,它的功能是取得标号的偏移地址。比如下面的程序
assume cs:codesg
codesg segment
start:mov ax,offset start ; 相当于mov ax,0
s:mov ax,offset s ; 相当于mov ax,3
codesg ends
end start
在上面的程序中,offset
操作符取得了标号start
和s
的偏移地址0和2,所以指令mov ax,offset start
相当于指令mov ax,0
,因为start
是代码段中的标号,他所标记的指令是代码段中的第一条指令,偏移地址位0
mov ax,offset s
相当于指令mov ax,3
,因为s是代码段中的标号,它所标记的指令是代码段中的第二条指令,第一条指令长度为3个字节,则s
的偏移地址为3.
问题:有如下程序段,填写两条指令,使该程序在运行中将s
处的一条指令复制到s0
处
分析:
s
和s0
处的指令所在的内存单元的地址是多少?cs:offset s
和cs:offset s0
- 将
s
处的指令复制到s0
处,就是将cs:offset s
处的数据复制到cs:offset s0
处 - 段地址已知在cs中,偏移地址
offset s
和offset s0
已经送入si
和di
中 - 要复制的数据有多长?
mov ax,bx
指令的长度为两个字节,即1个字
程序如下所示
assume cs:codesg
codesg segment
s: mov ax,bx ; mov ax,bx的机器码占两个字节
mov si,offset s
mov di,offset s0
mov ax,cs:[si] ; 使用ax表示字节的长度为1个字
mov cs:[di],ax
s0:nop ; nop的机器码占一个字节
nop
codesg ends
二、jmp指令
jmp
为无条件转移指令,可以只修改IP,也可以同时修改CS和IP。
jmp
指令要给出两种信息:
- 转移目的地址
- 转移的距离(段间距离、段内短距离、段内近距离)
不同的给出目的地址的方法,和不同的转移位置,对应有不同格式的jmp
指令。下面的几节内容中,我们以给出目的地址的不同方法为主线,讲解jmp
指令的主要应用格式和CPU执行转移指令的基本原理。
三、依据位移进行转移的jmp指令
jmp short 标号
(转到标号处执行指令)
这种格式的jmp
指令实现的是段内段转移,它对IP的修改范围为 ,也就是说,它向前转移时可以最多越过128个字节,向后转移时最多越过127个字节。
jmp
指令中的short
符号,说明指令进行的是段转移。jmp
指令中的标号
是代码段中的标号,指明了指令要转移的目的地,转移指令结束后,CS:IP应该指向标号处的指令。
3.1 程序9.1
比如下面的程序
assume cs:codesg
codesg segment
start:mov ax,0
jmp short s
add ax,1
s:inc ax
codesg ends
end start
上面的程序执行后,ax
中的值为1,因为执行了jmp short s
后,越过了add ax,1
,IP指向了标号s处的inc ax
。也就是或,程序只进行了一次ax
加1操作。
汇编指令jmp short s
对应的机器指令应该是什么样的呢?我们先看一下别的汇编指令和其对应的机器指令。
汇编指令 | 机器指令 |
---|---|
mov ax,0123h | B8 23 01 |
mov ax,ds:[0123h] | Al 23 01 |
push ds:[0123h] | FF 36 23 01 |
可以看到,在一般的汇编指令中,汇编指令中的idata
(立即数),不论它是代表一个数据还是内存单元的偏移地址,都会在对应的机器指令中出现,因为CPU执行的是机器指令,他必须要处理这些数据或地址。
现在将上述程序9.1翻译为机器码,结果如下图所示
对照汇编源程序,我们能够看到,INC AX
对应的偏移地址为8
,而jmp short s
也确实是jmp 0008
跳转到了INC AX
对应的偏移地址处,一切似乎很合理。但是,当我们查看jmp short s
对应的机器码时,却有了一些问题。
jmp 0008
所对应的机器码为EB03。注意,这个机器码不包含转移的目的地址,这就表示CPU在执行EB 03时,不知道转移的目的地址。指令中有地址,机器码没有转移地址❗️ ,而机器码才是CPU工作需要的指令,没有这个指令,CPU根据什么转移呢❓
3.2 程序9.2
把上面的程序9.1修改一下,改写为下面这种
assume cs:codesg
codesg segment
start:mov ax,0
mov bx,0
jmp short s
add ax,1
s:inc ax
codesg ends
end start
比较一下程序9.1和程序9.2的Debug查看的结果。两个程序中的jmp
指令都要使IP指向inc ax
指令,但是程序1的inc ax
指令的偏移地址为8,而程序2的inc ax
的偏移地址为000BH
。
而且两个程序中的jmp
指令所对应的机器码都是EB 03,这就说明了CPU在执行**jmp**
指令的时候是不需要转移目的地址的✔️。否则如果需要的话,转移地址就会出现不同的->对应的机器码就会不一样,但是这里都是EB 03。
CPU不是神仙,它只能处理你提供给它的东西,jmp
指令的机器码中不包含转移目的地址,CPU如何知道IP应该改为多少呢?
所以,在jmp
指令的机器码中,一定包含了某种信息,使得CPU可以将它当作修改IP的依据,现在我们来找一下这个依据㊙️。
9.3 怎么找到的转移地址?
现在,先简单的回忆一下CPU执行指令的过程(需要更多回忆,回到2-10)
- 从CS:IP指向内存单元读取指令,读取的指令进行指令缓冲器
- %3D(IP)%2B%E6%89%80%E8%AF%BB%E5%8F%96%E6%8C%87%E4%BB%A4%E7%9A%84%E9%95%BF%E5%BA%A6%7D#card=math&code=%5Ctext%7B%28IP%29%3D%28IP%29%2B%E6%89%80%E8%AF%BB%E5%8F%96%E6%8C%87%E4%BB%A4%E7%9A%84%E9%95%BF%E5%BA%A6%7D&id=tkCdH),从而读取下一条指令
- 执行指令,转到1,重复这个过程
上面的程序是根据机器码EB 03来进行跳转的,它怎么跳转的呢,CPU在执行EB 03的时候是根据什么修改的IP,使其指向目标指令的呢⁉️
👉答案就是:根据指令码中的03。注意,如果转移的目的地址是程序9.2中的CS:000B,而CPU执行EB 03时,当前的(IP)=000H,如果将当前的,使得(IP)=000BH
,CS:IP就可指向目标指令。指令EB 03并没有告诉CPU转移的目标地址,却告诉了CPU要转移的距离,即向后移动3个字节。
assume cs:codesg
codesg segment
start:mov ax,0
mov bx,0
jmp short s
add ax,1
add ax,1
add ax,1
s:inc ax
codesg ends
end start
现在的指令就变成了EB 09
了,因为它要往后跳转9个字节。
9.4 总结
原来,在jmp short 标号
指令所对应的机器码中,并不包含转移目的地址,而包含的是转移的位移。这个位移,是编译器根据汇编指令中的标号
计算出来的,计算方法如下图所示
9.4.1 jmp short 标号
实际上,jmp short 标号
的功能:(IP)=(IP)+8位位移
。
- 8位位移 = 标号处的地址 -
jmp
指令后的第一个字节的地址 short
指令此处的位移为8位位移- 8位位移的范围为,用补码表示
- 8位位移由编译程序在编译时算出
就像上面的程序找到位置一样
9.4.2 jmp near ptr 标号
jmp near ptr 标号
是和jmp short 标号
功能相近的指令格式,实现的是段内近转移:(IP) = (IP) + 16位位移
。
- 16位位移 = 标号处的地址 -
jmp
指令后的第一个字节的地址 near ptr
指令此处的位移为16位位移- 16位位移的范围为,用补码表示,进行的是段内近转移
- 16位位移由编译程序在编译时算出
四、转移目的地址在指令中的jmp指令
前面的jmp
指令,其对应的机器指令没有转移目的地址,而是相对于当前IP的转移位移。
jmp far ptr 标号
实现的是段间位移,又称远位移📗。功能如下:
(CS) = 标号所在段的段地址
(IP) = 标号所在段的偏移地址
far ptr 指明了指令用标号的段地址和偏移地址修改CS和IP
看下面的程序
assume cs:codesg
codesg segment
start:mov ax,0
mov bx,0
jmp far ptr s
db 256 dup (0) ; 这里使用段间转移
s:add ax,1
inc ax
codesg ends
end start
翻译成机器码以后,可以看到 EA 010B 6A07是直接跳转到s
对应的 段地址:偏移地址
五、转移地址在寄存器中的jmp指令
指令格式:jmp 16位reg
功能:(IP)=(16位reg)
这种格式之前已经看过了,这里就不用再看了
六、转移地址在内存中的jmp指令
转移地址在内存的jmp
指令有两种格式
6.1 jmp word ptr 内存单元地址(段内地址)
功能:从内存单元地址处开始存放着一个字,是偏移的目的偏移地址
内存单元地址可用寻址方式的任意一种格式给出,比如下面的指令
mov ax,0123H
mov ds:[0],ax
jmp word ptr ds:[0]
指行后,%3D0123H%7D#card=math&code=%5Ctext%7B%28IP%29%3D0123H%7D&id=I1CwL)
mov ax,0123H
mov [bx],ax
jmp word ptr [bX]
指行后,%3D0123H%7D#card=math&code=%5Ctext%7B%28IP%29%3D0123H%7D&id=n4U8Q)
6.2 jmp dword ptr 内存单元地址(段间转移)
功能:从内存单元地址开始存放着两个字,高地址处的字是转移的目的段地址,低地址处是转移的目的偏移地址。
- %20%3D%20(%E5%86%85%E5%AD%98%E5%8D%95%E5%85%83%E5%9C%B0%E5%9D%80%2B2)%7D#card=math&code=%5Ctext%7B%28CS%29%20%3D%20%28%E5%86%85%E5%AD%98%E5%8D%95%E5%85%83%E5%9C%B0%E5%9D%80%2B2%29%7D&id=Wjei4)
- %20%3D%20(%E5%86%85%E5%AD%98%E5%8D%95%E5%85%83%E5%9C%B0%E5%9D%80)%7D#card=math&code=%5Ctext%7B%28IP%29%20%3D%20%28%E5%86%85%E5%AD%98%E5%8D%95%E5%85%83%E5%9C%B0%E5%9D%80%29%7D&id=r8kA2)
内存单元地址可用寻址方式的任一格式给出,比如下面的指令
mov ax,0123H
mov ds:[0],ax
mov word ptr ds:[2],0
jmp dword ptr ds:[0]
执行后,%EF%BC%8C(IP)%3D0123H%7D#card=math&code=%5Ctext%7B%28CS%3D0%29%EF%BC%8C%28IP%29%3D0123H%7D&id=w6z2F),CS:IP指向0000:0123
又比如下面的指令
mov ax,0123H
mov [bx],ax
mov word ptr [bx+2],0
jmp dword ptr [bx]
执行后,%EF%BC%8C(IP)%3D0123H%7D#card=math&code=%5Ctext%7B%28CS%3D0%29%EF%BC%8C%28IP%29%3D0123H%7D&id=RIlwq),CS:IP指向0000:0123
七、jcxz指令
jcxz
指令为有条件转移指令,所有的有条件转移指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为:。
指令格式:jcxz 标号
(如果(cx)=0
,转移到标号处执行)
操作:当(cx)=0
时,(IP)=(IP)+8位位移
- 8位位移 = 标号处的地址 -
jcxz
指令后的第一个字节的地址 - 8位位移的范围为,用补码表示
- 8位位移由编译程序在编译时算出
当%7D%5Cne0#card=math&code=%5Ctext%7B%28cx%29%7D%5Cne0&id=PErDn)时,什么也不做(程序向下执行)
从jcxz
的功能中就可以看出,jcxz 标号
的功能相当于
// C语言和汇编语言进行的综合描述,语法当然不对,但是能理解意思
if ((cx)==0)
jmp short 标号;
八、loop指令
loop
指令为循环指令,所有的循环指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为。
指令格式:loop 标号
((cx)=(cx)-1
,如果,转移到标号处执行)
操作:
- %3D(cx)-1%7D#card=math&code=%5Ctext%7B%28cx%29%3D%28cx%29-1%7D&id=lCqOh)
- 如果%7D%5Cne%200%2C%20%5Ctext%7B(IP)%3D(IP)%2B8%7D#card=math&code=%5Ctext%7B%28cx%29%7D%5Cne%200%2C%20%5Ctext%7B%28IP%29%3D%28IP%29%2B8%7D&id=xfdEV)位位移
- 8位位移 = 标号处的地址 -
loop
指令后的第一个字节的地址 - 8位位移的范围为,用补码表示
- 8位位移由编译程序在编译时算出
如果(cx)=0
,什么也不做(程序向下执行)。我们从loop
的功能中可以看出,loop 标号
的功能相当于:
(cx)--;
if ((cx)!=0)
jmp short 标号;
九、根据位移进行转移的意义
前面学习到了
jmp short 标号
jmp near ptr 标号
jcxz 标号
loop 标号
这几种汇编指令,它们对IP的修改是根据转移目的地址和转移起始地址之间的位移来进行的。在他们对应的机器码中不包含转移的目的地址,而包含的的是目的地址的位移。
这种设计,方便了程序段在内存中的浮动装配。例如:
汇编指令 | 机器代码 |
---|---|
mov cx,6 | B9 06 00 |
mov ax,10h | B9 10 00 |
s: add ax,ax | 01 C0 |
loop s | E2 FC |
这段程序装在内存中的不同位置都可以正确执行,因为loop s
在执行时只涉及s
的位移(-4,迁移4个字节,补码表示为FCH),而不是s
的地址。
十、编译器对转移位移超界的检测
注意,根据位移进行转移的指令,它们的转移范围受到转移位移的限制,如果在源程序中出现了转移范围超界的问题,在编译的时候,编译器将报错。
比如,下面的程序将引起编译错误:
assume cs:code
code segment
start:jmp short s
db 128 dup (0)
s:mov ax,0ffffh
code ends
end start
jmp short s
的转移范围是,IP最多向后移动127个字节。
注意:我们在第2章中使用到的jmp 2000:0100
这样的转移指令,是在Debug中使用的汇编指令,汇编编译器并不认识。如果在源程序中使用,编译时也会报错。
检测
检测点9.1
检测1
程序如下
assume cs:code
data segment
db 1 dup (0) ; 开创
data ends
code segment
start:mov ax,data
mov ds,ax
mov bx,0
jmp word ptr [bx+1]
code ends
end start
若要使程序中的jmp
指令执行后,CS:IP指向程序的第一条指令,在data
段中应定义哪些数据?
解答:
如果我们只开辟一个空间
db 1 dup(0)
,编译后发现DS=075A,所以程序的入口在076A处。并且从076A:0 ~ 076A:F都是0,从076A:10处开始才是程序。只需要将
[bx+1]
对应的内存单元里的值为0即可,所以答案可以为db 1 dup(0)
一直到db 16 dup(0)
我在这里迷糊了好久,这个规律为什么在这里不适用了?其实,这个规律在不指定
CS
的地址还是可以使用的,因为cs:code
将cs
指针和code
的段地址联合起来了,所以CS=076B
.但是,我们说的
jmp word ptr [bx+1]
的[bx+1]
是整个段的。
检测2
程序如下
assume cs:code
data segment
dd 12345678H
data ends
code segment
start:mov ax,data
mov ds,ax
mov bx,0
mov [bx],bx
mov [bx+2],cs
jmp dword ptr ds:[0]
code ends
end start
补全程序,使得jmp
指令执行后,CS:IP指向程序的第一条指令
解析:要想使得最后跳转到指令的第一条,就要满足:
所以就将现在的值直接赋给上面开辟的空间的值即可。
- db 1:数据为01H,占1个字节
- dw 1:数据为0001H,占2个字节
- dd 1:数据为00000001H,占4个字节
检测3
用Debug查看内存,结果如下
则此时,CPU执行指令
mov ax,2000H
mov es,ax
jmp dword ptr es:[1000H]
后,%3D00BE%2C%20(IP)%3D0006%7D#card=math&code=%5Ctext%7B%28CS%29%3D00BE%2C%20%28IP%29%3D0006%7D&id=E49Y5)
这个题很简单的,但是涉及到数据的存放问题,例如
dd 12345678H
,它在内存中的存放为78 56 34 12(先低后高)
所以CS对应的是高位置:1234
IP对应的是低位值:5678
检测点9.2
补全指令,利用jcxz
指令,是现在内存2000H段中查找第一个值为0的字节,找到后,将它的偏移地址存储在dx中。
assume cs:code
code segment
start:mov ax,2000H
mov ds,ax
mov bx,0
s:mov cl, [bx]
mov ch,0
jcxz ok ; 将段中的数当作cx作为判断条件,很巧妙的一个思想
inc bx
jmp short s
ok:mov dx,bx
mov ax,4c00h
int 21h
code ends
end start
检测点9.3
补全编程,利用loop
指令,实现在内存2000H 段中查找第一个值为0的字节,找到后,将它的偏移地址存储在dx中
assume cs:code
code segment
start:mov ax,2000H
mov ds,ax
mov bx,0
s: mov cl,[bx]
mov ch,0
inc cx ; 因为loop s有--cx这个动作,所以找到了第一个值为0,--0=-1,此时不会退出loop s
inc bx
loop s
ok: dec bx ; dec指令功能和inc相反,dec bx进行的操作为(bx)=(bx)-1
mov dx,bx
mov ax,4c00h
int 21h
code ends
end start
实验8 分析一个奇怪的程序
分析下面的程序,在运行前思考:这个程序可以正确返回吗?
运行后再思考:为什么会是这种结果?
通过这个程序加深对相关内容的理解
assume cs:codesg
codesg segment
mov ax,4c00h
int 21h
start: mov ax,0
s: nop
nop
mov di,offset s
mov si,offset s2
mov ax,cs:[si]
mov cs:[di],ax
s0: jmp short s
s1: mov ax,0
int 21h
mov ax,0
s2: jmp short s1
nop
codesg ends
end start
这个很奇怪的程序先拿过来编译一下看看它在内存中长什么样子。
之后开始进行单步调试:当调试到s0: jmp short s
这一句的时候异变突起:
原来的jmp 0008
变成了jmp 0000
从而跳转到了程序的最开头完成了整个程序,怎么回事?
分析:现在开始追根溯源看看到底是哪里发生了奇妙的反应
JMP 0000
对应的机器码是EBF6,F6对应的位移正好是8,而IP=0008,意思就是IP向前移动8位从而到达了程序的开头.
跟着程序走一遍,发现程序走到这里的时候会造成一个神奇的效果发生
s段处开辟的两个字节会被jmp short s1
对应的机器码给填充
jmp
指令本质上是往前进行跳转,并且跳转的距离由目的距离和源距离差决定,因为的距离大小正好和程序开头到s
的距离一样,所以就可以直接跳转到开头。
实验9 根据材料编程
这个编程任务必须在进行下面的课程之前独立完成,因为后面的课程中,需要通过这个实验而获得的编程经验。
编程:在屏幕中间分别显示绿色、绿底红色、白底蓝色的字符串welcome to masm!
。
编程所需的知识通过阅读、分析下面的材料获得:
80x25彩色字符模式显示缓冲区(以下简称为显示缓冲区)的结构:
- 内存地址空间中,D8000H~BFFFFH共32KB的空间,为82x25彩色字符模式的显示缓冲区。向这个地址空间写入数据,写入的内容将立即出现在显示器
- 在80x25彩色字符模式下,显示器可以显示25行,每行80个字符,每个字符有256种属性(背景色、前景色、闪烁、高亮等组合信息)
这样,一个字符在显示缓冲区就要占2个字节,分别存放字符的ASCII码和属性。80x25模式下,一屏的内容在显示缓冲区共占4000个字节。
显示缓冲区分8页,每页,显示器可以显示任意一页的内容,一般情况下,显示第0页的内容。也就是说通常情况下, 中的4000个字节的内容将出现在显示器上。
在一页显示缓冲区中:
- 偏移000~09F对应显示器上的第1行(80个字符占160个字节)
- 偏移0A0~13F对应显示器上的第2行
- 偏移140~1DF对应显示器上的第3行
- …….
- 依次类推可知,偏移F00~F9F对应显示器上的第25行
在一行中,一个字符占2个字节的存储空间(一个字),低位字节存储字符的ASCII码,高位字节存储字符的属性。一共有80个字符,占160个字节。
即在一行中:
- 00~01单元对应显示器上的第1列
- 02~03单元对应显示器上的第2列
- 04~05单元对应显示器上的第3列
- ……
- 依此类推,可知,9E~9F单元对应显示器上的第80列。
显示缓冲区里的内容为:
可以看出,在显示缓冲区中,偶地址存放字符,奇地址存放字符的颜色属性。
一个在屏幕上的字符,具有前景(字符色)和背景色(底色)两种颜色,字符还可以以高亮度和闪烁的方式显示。前景色、背景色、闪烁、高亮等信息被记录在属性字节中。
属性字节的格式:
可以按位设置属性字节,从而配出各种不同的前景色和背景色,比如:
- 红底绿字,属性字节为:01000010B
- 红底闪烁绿字,属性字节为:11000010B
- 红底高亮绿字,属性字节为:01001010B
- 黑底白字,属性字节为:00000111B
- 白底蓝字,属性字节为:01110001B
例如:在显示器的0行0列显示红底高亮闪烁绿字的字符串’ABCDEF’(红底高亮闪烁绿字,属性字节为:11001010B,CAH),显示缓冲区里的内容为:
注意:闪烁的效果必须在全屏DOS方式下才能看到
assume cs:code
data segment
db 'welcome to masm!',0
data ends
code segment
start:
;dx存放的是行列信息
mov dh,8
mov dl,3
mov cl,2
mov ax,data
mov ds,ax
mov si,0
call show_str
mov ax,4c00h
int 21h
show_str:
;
;入栈储存数据tt
;
push dx
push cx
push si
;
;第八行定位【460-4ff】
; 【7*a0=460】
mov bl,dh ;行号赋值8
dec bl ;行号自减一
mov al,160 ;A0h【每一行的自增值】
mul bl ;将bl与al相乘结果放到ax中
mov bx,ax ;结果赋值
;
;第三列定位【04-05】
;
add dl,dl ;列号自增
;
;行列号拼接形成偏移地址
;
add bl,dl
;
;设定显示位置
;
mov ax,0b800h
mov es,ax
mov al,cl ;颜色赋值【后面要用cx来结束循环所以这里要把cx的数据先保存起来】
mov di,0 ;数据初始化
s:
mov ch,0
mov cl,ds:[si]
jcxz ok
;将数据赋给cx,当cx为0的时候(到字符串尾)就会自动退出
mov es:[bx+di],cl
mov es:[bx+di+1],al;颜色赋值【颜色设定在高位】
add di,2 ;es段每次跳两个字节
inc si ;ds段每次跳一个
loop s
ok:
;
;数据恢复
;
pop dx
pop cx
pop si
ret
code ends
end start