汇编笔记(上)

主要记录一些自己学习中觉得比较重要和容易出错的地方,鸽了好久都没学完的一门武林秘籍。
参考的王爽老师的汇编语言这本书,大部分指令是分散在各个章节中的,在记录中也额外设置了小标题,可以快速索引到。

Debug

Debug的一些配置

配置参考师傅博客:https://blog.csdn.net/lcr_happy/article/details/52491107
下载好DOSBox和debug.exe link.exe masm.exe

将上述EXE文件放到一个名字短路径段的文件夹中,比如F:\tmp
在这里插入图片描述
输入mount c f:\tmp (自己定义的文件夹)
出现非报错语句后输入 c:
接着便可使用debug、masm、link了

debug 可用于调试EXE 看各个寄存器的变化状态
masm  是用于编译汇编源程序
link  是进行连接
连接干了什么:
      连接源程序分开写的目标文件
      源程序中调用了一些外部库
      将OBJ文件中的机器指令转为可执行
简单的进行编译和连接,不考虑中间文件:
      编译: masm 1.asm;
      连接: link 1.OBJ
      调试:debug 1.EXE  可用Tab不全,文件应同在F:\tmp目录下,即存放3个EXE的目录

Debug的一些指令

r 指令 查看或改变寄存器的值
r是查看全部寄存器  
r + 寄存器的名  是查看某一指定寄存器 
r + 寄存器 在 : 后输入即为改变寄存器的值 空格不处理 回车直接不改变

d指令 查看内存中的值  d + 地址(段地址:偏移地址格式) 
d 1000:0 查看10000H处内存值,默认输出128B单元
d 段地址: 偏移地址1 偏移地址2  类似python切片 查看地址1~地址2间的内容

e指令  修改内存值,可用来写入数据或指令的机器码
e + 段地址:偏移地址  a b c d ...用来写入数据
e + 地址 会出现XX. 在.之后可输入自己要把XX改为的值 ,空格不处理 ,回车结束。

a指令 直接向某一地址中写入汇编指令
a + 段地址:偏移地址

t指令 执行当前CS:IP所指向的指令(一般是一条),涉及对ss的修改可能连带对sp的赋值一起修改。类似单步步入

p指令 用来执行int 21H 或 执行loop s (执行到cx=0 循环结束的下一条指令)

g指令 g + 段地址:偏移地址  可以直接将该地址之前的指令执行完  要u指令看仔细了 并不是的连着的 指令长度不一样

q指令 退出debug

源程序注意事项

1、要指明段开头和段结尾 XXsegment 和 XX ends

2、数据不能以字母开头,如 ffffH 必须写成 mov ax,0ffffH,指令不分大小写!!!

3、要指明程序结尾 end

4、源程序会将mov ax,[0] 识别为 mov ax,0 所以 如果是内存赋值的话要加上ds:前缀。
或者mov bx,0 通过mov ax [bx]来间接访问。

5、end指令也可以用来标明程序入口,end start ,start指向第一条汇编指令,start的本质也是地址(偏移地址,相对于代码段)

6、如果只用了ss而没给sp赋值的话,sp默认为0,用法就类似一个ds寄存器。

1、开辟空间

dw为定义字型数据  word  2个字节

db为定义字节型数据  byte 1个字节

dd用来定义双字 dword 4个字节

dup 操作符 结合 dd dw db来开辟空间,多进行数据的复用

可理解为开辟空间,如果跟dw 0123h 就可以理解为开辟一个字的空间,内容为0123h(可能不等价于段的大小)
对dw数据操作 每次add bx 2 对db操作每次inc bx

定义字符串
db 'hello'   没有高级语言的'\0',本质还是这5个字节的ASCII码

dup 举例
如 db 3 dup(0) 即开辟3个字节空间,值都为0

而db 3 dup(0,1,2) 则是开辟9个字节空间,值分别为0,1,2,0,1,2,0,1,2…

总结 :dx(db,dw,dd) 重复的次数 dup(重复的x型数据)

nop指令 的机器指令只占一个字节的空间

6、标号的本质都可理解为段地址

第五章、LOOP和[bx]

1、基础

loop 和 cx结合来控制循环,在loop指令前设计一个标号s,每次执行loop会自动将cx减1,如果cx=0就继续执行loop下面的指令,否则就跳转到s继续执行。
s本质上是个偏移地址,在debug调试中可明显看出loop后跟了一个偏移地址,也算是改变IP的一个方式。

[bx]本质上也是内存中的数据,地址为(DSx16 + bx),还是逃不出基地址+ 偏移地址的圈子

在载入程序时,CX中存放的是程序的长度(多少字节)

2、实验(3)

写一段汇编程序,将"mov ax,4c00H"之前的指令复制到0:200之处。

在写程序之前我们要思考,从哪里开始复制,复制少长度,可能一上来有点懵,所以先通过debug打开一个EXE看一下初始状态。
在这里插入图片描述
在载入程序时,CX中存放的是程序的长度(多少字节),通过u指令也可已看出在0017H到001C前存放的是返回的指令,一共五个字节。
所以mov前的指令一共是CX-5个字节,所以完成拷贝循环CX-5次,所以在程序中直接对CX 进行
SUB CX 5即可,一个一个字节copy

故源程序:

assume cs:code
code segment
	mov ax,cs
	mov ds,ax           //不能mov 段寄存器 和 段寄存器  不能用立即数给段寄存器赋值
	
	mov ax,0020H
	mov es,ax
	mov bx,0
	sub cx,5
	
s:  mov dl,[bx]           //不能mov 内存到内存,在debug中会出错
    mov es:[bx],dl
	inc bx
	loop s
	
	mov ax,4C00H          //源程序可加H来标明16进制 
	int 21H
	
code ends
end

在这里插入图片描述
debug调试,查看功能是否实现。

第六章 包含多个段的程序

理解本章要有一个大局意识,即我们不能随意更改一片内存(可能存放有关程序的重要值),所以为了内存合法,需要在源程序中定义出段来讨内存,这样在加载程序时系统会给我们分配内存,这些内存是合法的,修改不会出现错误。

另一种方法是在程序执行时,向系统申请,不过没深入讨论。

1、 栈、数据、代码公用一个段

本质是使用end start来标明入口点,start应该是表示入口地址的偏移地址部分。
从一个混合实例中学习:

利用栈将程序中定义的数据逆序

assume cs:code
code segment
	dw 0123h,0456h,0678h,0abch,0defh,0fedh,0cbah,0987h
	dw 0,0,0,0,0,0,0,0
logo: mov ax,cs
      mov ds,ax
	  mov ss,ax
	  mov sp,20h
	  
	  mov bx,0
	  mov cx,8
s:    push [bx]
	  add bx,2
	  loop s
	  
	  mov bx,0
	  mov cx,8
s0:   pop [bx]
      add bx,2
	  loop s0
	
	  mov ax,4c00h
	  int 21h
code ends
end logo

上述log即start,也说明这种标号的名称是可变的,但尽量有意义,为了增加代码的可读性。
在这里插入图片描述
可知上述程序代码段,一开始定义了8个字型数据,接着开辟了8个字的空间作为栈来进行逆序操作。
ds和ss中的值都为CS的值,IP的值因为start标号而改变为0020H

在这里插入图片描述
在这个内存分配中0010~001F的内存为栈空间,所以设置sp的值为0020H,要根据实际情况来判断sp的值。

2、多个段编写源程序

1、定义了多个段,代码操作就普通入栈和出栈,主要debug运行后查看cs,ss,ds寄存器的值

assume cs:code,ds:data,ss:stack
data segment 
	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
data ends

stack segment
	dw 0,0,0,0,0,0,0,0
stack ends 

code segment
start:	mov ax,stack
	    mov ss,ax
		mov sp,10h
	
		mov ax,data
		mov ds,ax
	
		push ds:[0]
		push ds:[2]
	
		pop ds:[2]
		pop ds:[0]
	
		mov ax,4c00h
		int 21h
code ends
end start

mov ax,stack,stack是栈段段名,是标号,直接理解为一个地址就行了。

刚进入debug时各个寄存器的值
在这里插入图片描述
运行到返回前各个寄存器的值
在这里插入图片描述
可见最后DS=076A,SS=076B,CS=076C,不难发现DS、SS和CS值在内存中是连续的,DS和SS相差10H个字节,SS和CS相差10H个字节,仔细看源程序便知,ds和ss都是8个字的大小,即16个字节(这是能被16整除下,详见段的大小),如图。
在这里插入图片描述

3、各个段的位置和执行

为什么DS最终是076A开始,而刚刚进入时DS是075A呢?
这与可执行文件的结构有关,可执行文件由描述信息和程序构成,在内存中真正程序开始处之上还有256字节的PSP程序前缀(用于通信,保存一些相关信息),所以DS一开始保存地址 和 真正的程序部分还相差0010H。

同时,按照我们之前的学习,一般是CS和DS(开始时的075A)相差0010H才对,而上图中CS为076C,不符合常规,其实之前我们的源程序,只有代码段,根据程序和机器码位置一 一对应,程序入口处即代码段的汇编指令。而上述多个段的程序刚开始是定义了DS和SS段,程序刚一入口并不是汇编指令,而是开辟了内存空间,这也是为什么修改后的DS和一开始的DS相差了0010H。

而至于CS为什么是076C则是end start指令指明了真正的程序入口,会在加载程序时改变CS:IP的值,使它指向代码段的第一条汇编指令。

当然如果我们改变源程序的顺序,比如:

assume cs:code,ds:data,ss:stack
code segment
		mov ax,stack
	    mov ss,ax
		mov sp,10h
	
		mov ax,data
		mov ds,ax
	
		push ds:[0]
		push ds:[2]
	
		pop ds:[2]
		pop ds:[0]
	
		mov ax,4c00h
		int 21h
code ends

data segment 
	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
data ends

stack segment
	dw 0,0,0,0,0,0,0,0
stack ends 

end 

编译、连接后debug的载入情况
在这里插入图片描述
发现改变了源程序的顺序,可执行文件中的指令顺序也做出了同步的改变,把代码段放在源程序首部(并且代码段第一句话就是有效指令)则不需要end start标明。如图CS:IP指向也是第一条汇编,并且CS和DS的初始差又变为了0010H,之后内存依次是ds和ss。

起初以为stack的定义在mov stack之后会报错,实际发现并没有发生报错并且第一个指令mov ax,076E准确指明了栈段,感觉应该是在加载程序时内存已经同时分配好了,(不像是高级语言的编译器,从上往下摸,在上面找不到定义就报错)。

再参考第四章p79的图4.2,如下
在这里插入图片描述
这也表明了源程序的顺序和内存中机器指令的顺序有个一 一映射的关系,各段在内存中的顺序取决于在远程序中的定义,所以如果源程序中第一个不是代码段则需要end start标明程序入口

如果不指明入口位置,则程序从所分配的空间开始执行

4、段的大小

如果段的数据占N个字节,则程序加载后,段的空间为((N+15)/16) x 16

首先要知道,一个段的大小,最小为16字节最大为(2^16) 64KB,为什么最小为16个字节呢,因为段的起始地址必须为16的倍数,如00010H,段地址为0001H而00011H当然不行,因为00011H的段地址还是0001H,只不过偏移地址是0001H,所以下一个段地址为0002H(最小段的情况下),所以一个段最小为16B,而最大为64KB因为8086的寄存器为16位,最大也就能偏移0~FFFFH为64KB。

所以如果N为16的整数倍,则段的大小就为(N/16) x 16
否则 为 (N/16 + 1) x 16 (N/16)只取整数部分

合成一个式子 变为 [(N+15) /16 ] x 16

例如,在开辟段时,数据段和栈段个占2个字,分配的内存大小仍为16B。

data segment 
	dw 0123h,0456h
data ends

stack segment
	dw 0,0
stack ends 
code segment
start:	mov ax,stack
	    mov ss,ax
		mov sp,10h
	
		mov ax,data
		mov ds,ax
	
		push ds:[0]
		push ds:[2]
	
		pop ds:[2]
		pop ds:[0]
	
		mov ax,4c00h
		int 21h
code ends
end start

在设置栈顶时便能发现是 mov,sp,10h 是按照16字节的大小来的
在这里插入图片描述
debug查看到压栈完成后的内存,栈后两位为0123h 和 0456h

由于栈的中断机制,栈中压入了一些cpu的状态,隐约看到CS的值076CH目前不是太懂,等待后续解决。

5、实验5

上述操作已完成实验的前部分,下面主要是实验(5)(6)
1、 实验(5):
编写程序实现将a段和b段中的数据依次相加,结果保存到c段

assume cs:code 

a segment 
	db 1,2,3,4,5,6,7,8
a ends

b segment
	db 1,2,3,4,5,6,7,8
b ends

c segment
	db 0,0,0,0,0,0,0,0
c ends

code segment
start:    mov ax,c
          mov es,ax   //c段的段地址用es来存放
          mov bx,0
	      mov cx,8
s:		  mov dx,a
          mov ds,dx
          mov al,[bx]
		  mov dx,b
          mov ds,dx
		  add al,[bx]  //用ds寄存器来实现a段和b段内数据相加
		  mov es:[bx],al
		  inc bx
		  loop s
          mov ax,4c00h
		  int 21h
code ends
end start

注意:不能直接mov ds,a等,a,b,c标号即代表着在内存中的段地址。
另:用到三个寄存器,ds,ss,es,如果程序没有其他栈操作ss可直接当做ds来用。

assume cs:code 

a segment 
	db 1,2,3,4,5,6,7,8
a ends

b segment
	db 1,2,3,4,5,6,7,8
b ends

c segment
	db 0,0,0,0,0,0,0,0
c ends

code segment
start:	mov ax,c
		mov es,ax
		mov ax,a
		mov ds,ax
		mov ax,b
		mov ss,ax //不设置sp sp默认为0
		mov bx,0
		mov cx,8
s:		mov dl,[bx]
        add dl,ss:[bx]
		mov es:[bx],dl
		inc bx
		loop s
		mov ax,4c00h
		int 21h
code ends
end start

2、 实验(6)
编写code代码,用push指令将a段中的前八个字型数据逆序存到b段中。
因为是逆序存到b段中,所以考虑把b段当成栈段来用,可以assume ss:b来说明一下

assume cs:code
a segment
	dw 1,2,3,4,5,6,7,8,9,0ah,0bh,0ch,0dh,0eh,0fh,0ffh
a ends

b segment
	dw 0,0,0,0,0,0,0,0
b ends

code segment
start:  mov ax,a
		mov ds,ax
		
		mov ax,b
		mov ss,ax
		mov sp,10h
		
		mov bx,0
		mov cx,8
s:		push [bx]
		add bx,2
		loop s
		
		mov ax,4c00h
		int 21h
code ends
end start

在这里插入图片描述debug查看结果,可知076a到076c之前的32个字节为a段,076c这16个字节为b段,下面接代码段的机器码。

第七章 灵活的寻址方法

1、基础内容

and 和 or指令

and 指令 进行与运算 对应python的 & 和离散的合取
or 指令 进行或运算 对应python的 | 和离散的析取

and 用来讲某一位设置成0 ,or 用来讲某一位设置成 1
例如:
mov al ,10000111B
如果将al的第1位置0(从0开始,第0位,对应第n位十进制为2^n)

and al,11111101B 将第一位(第二个设置为0)  设置后 al = 10000101B

同理,若将第三位设置为1,则

or al,00000100B 即可  设置后 al = 10001111B

tip:有关大小写用位运算的转换

对于大写字母和小写字母,我们知道他们的ASCII码相差32如(A:65)、(a:97),而32正好对应二进制
00100000B,所以大写字母和小写字母的区别主要是二进制第5位(从0开始)是否为1,为1为小写,否则为大写。
如 A 转为 a
A:(01000001B)| (00100000B) = (01100001B):a 通过 or 00100000B来实现
而小写a 转为大写A只需要,把第五位置0即可,通过 & 11011111B来实现

现学现用,大小写转换

源程序: 目的是将data段中第一个字符全转为大写,第二个字符全转为小写

assume cs:code 
data segment
	db 'BaSic'
	db 'iNdOrMaTiOn'
data ends

code segment
start:	mov ax,data
		mov ds,ax
		mov bx,0
		mov cx,5

s:  	mov al,[bx]
		and al,11011111B //转小写 and DFh
		mov [bx],al
		inc bx
		loop s
	
		mov cx,11
s0: 	mov al,[bx]
		or al,00100000B //转大写 or 20H
		mov [bx],al
		inc bx
		loop s0
	
		mov ax,4c00h
		int 21h
code ends
end start

在这里插入图片描述

功能实现后,同时也能看出在原程序中定义的 两个字符串在内存中是连续储存的,与源程序中的换行无关。

各种寻址方式

段地址 ds(默认)
[idata]  用常量来定位一个内存单元 如 [0] [1]等等

[bx]     用一个变量来表示 如 mov bx,0   [bx] 即 dsx16 + 0

[bx+idata]  一个变量和常量的组合
表示  idata[bx]  [bx].idata  [bx+idata]  如200h处的一个内存 bx=0 为 200[bx] [bx].200
200[bx] 就像高级语言的a[i]一样

[bx+si] 两个变量表示   也可写成 [bx][si]

[bx+si+idata]  两个变量 + 一个常量
idata[bx][si] [bx].200[si] [bx][si].200  这里的 . 直接理解成 + 没问题

上述方法灵活使用即可。

TIP:si 和 di这两个寄存器只能当16位来使用,不能分割成两个8位来用,功能和bx差不多,[bx] 、[si]

2、双重循环的使用,及栈缓存

上题目: 将所有字母都改为大写

data segment
	db 'lib             '
	db 'dec             '
	db 'dos             '
	db 'vax             '
data ends

最简单的思路是单层循环,每层循环将0[bx]、1[bx] 、2[bx]的值依次and DFh,最后再bx+10h,反复四次就可以实现。这样可以实现是因为数据太小了,才4x3 加入是100 x 100 或者更大,这就不行了,所以要想着双层循环。

双层循环和单层循环本质上一样,需要标清楚不同循环对应的标号,同时还有一个问题是解决CX的保存问题,因为loop循环只认CX,即内存循环后还能找回外层的CX,故需要一种方式保存外层的CX,下面由两种解决方案,一是通过 再找一个寄存器来保存CX,另外则是通过栈来缓存。

①额外借助寄存器存放CX

用到bx来指向行,si来指向列,dx来存放cx

assume cs:code,ds:data
data segment
	db 'lib             '
	db 'dec             '
	db 'dos             '
	db 'vax             '
data ends

code segment
start:	mov ax,data
		mov ds,ax
		mov bx,0
		mov cx,4

s0:  	mov dx,cx             //先保存cx
		mov si,0
		mov cx,3
		
    s:  mov al,[bx+si]
		and al,0DFH
		mov [bx+si],al
		inc si
		loop s
    
		mov cx,dx             //内层循环结束后复原cx
		add bx,16
		loop s0
		
		mov ax,4c00h
		int 21h
code ends
end start

为了容易区分两层循环,标号特意有了缩进,不过用额外寄存器存cx的方法也有局限,因为8086CPU一共就14个寄存器,并且有的寄存器有特定的功能,如果所有寄存器都在程序用到的话,那么谁还能再存cx呢,或则循环嵌套次数多呢。
在这里插入图片描述

②借助栈来缓存CX

既然寄存器少,但是内存可用啊。其实在单词之前开辟16字节的空间置为0,用来存放cx也可以,但不如栈方便,并且bx的值也要调整不能是0了。 为啥要选16的倍数可以想一下。

一般来说,在需要缓存数据时,都应该使用栈,因为栈的LIOF(后进先出)的特点,使栈能很好的保存当前状态,用上栈,多少层循环都不用怕了。

assume cs:code,ds:data,ss:stack
stack segment
	dw 0,0,0,0,0,0,0,0
stack ends

data segment
	db 'lib             '
	db 'dec             '
	db 'dos             '
	db 'vax             '
data ends

code segment
start:	mov ax,data
		mov ds,ax
		mov ax,stack
		mov ss,ax
		mov sp,10h
		
		mov bx,0
		mov cx,4

s0:  	push cx
		mov si,0
		mov cx,3
		
    s:  mov al,[bx+si]
		and al,0DFH
		mov [bx+si],al
		inc si
		loop s
    
		pop cx
		add bx,16
		loop s0
		
		mov ax,4c00h
		int 21h
code ends
end start

就是添加了一个栈段,对ss盒sp进行初始化,接着把mov dx,cx改成push … 即可。

3、实验题目:

将如下data段中每个单词的前四个字母改为大写字母
几乎和上面的例题一模一样,改成4x4的循环即可

data segment
	db '1. display      '
	db '2. brows        '
	db '3. replace      '
	db '4. lulu         '
data ends

原程序如下:

assume cs:code , ds:data , ss:stack
stack segment
	dw 0,0,0,0,0,0,0,0   //用栈来缓存cx的值,实现双层循环
stack ends

data segment
	db '1. display      '
	db '2. brows        '
	db '3. replace      '
	db '4. lulu         '
data ends

code segment
start:	mov ax,data
        mov ds,ax
		
		mov ax,stack
		mov ss,ax
		mov sp,10h
		mov bx,0
		mov cx,4

s0:     push cx  //压栈 保存当前外层循环的cx
		mov si,3
		mov cx,4

s:      mov al,[bx+si]
		and al,11011111B
		mov [bx+si],al
		inc si
		loop s
        
		pop cx    //内层循环结束,复原外层的cx
		add bx,10h
		loop s0
		
		mov ax,4c00h
		int 21h
code ends
end start

通过bx来控制行,si来定位列实现寻址。

请添加图片描述

第八章 数据处理的两个基本问题

章节内容

围绕两个重要问题进行解决。

1、处理的数据在什么地方?

8086中,只有bx、bp、si、di能用在 […]中进行寻址,可以单独出现即 [si]、 [bx]等,也可以组合出现,但只有四种组合,必须是[bx+si]、[bx+di]、[bp+si]、[bp+di]

注:如果用[…]用到bp,如果段地址没有显示给出(即前缀),段地址默认在 ss
如[bp] 、[bp+si]、[bp+si+idata]都是默认以ss中数据为段地址

汇编语言用三个概念来表示数据的位置

1、立即数  
直接包含在机器指令中的数据(执行前在cpu的指令缓冲区) 可参考第二章中指令执行的过程
如 mov ax,1     mov bx,'a' 等


2、寄存器
数据在寄存器中,在操作时给出相应的寄存器名称
如 mov ax,bx    sub ax,bx


3、数据在内存中
通过 段地址(SA)+ 偏移地址(EA)来访问
可以显示给出如  ds:[0]  ds:[bx]  
也可以默认  如[bx] 默认ds  [bp] 默认ss 

寻址方式,详细可参考上一章
在这里插入图片描述
结合高级语言,理解一些常见的表述方式,怎么直观怎么来用。

2、指令要处理的数据有多长?

1、通过寄存器来标明数据尺寸
这也解释了为什么前边 mov ax,1就是一个字的大小,mov ah,1就是一个字节的大小,并且要求两个对象应该是同一种(大小对应 如 ax 对bx ah对al)

2、用操作符X ptr 指明长度

如 mov word ptr ds:[0],1
word ptr指明访问的内存单元是一个字,注意是指向内存单元。


mov byte ptr ds:[0],1
byte ptr 指明访问内存单元是一个字节大小,没有寄存器参与时,一定要显示指明。

div指令

div是除法指令,除数和被除数存放情况。

除数: 有8位和16位两种,存放在一个寄存器 或 内存单元中(需要用 X ptr指明大小)

被除数:有16位和32位,16为默认存在ax中,如果是32位则低16位在AX中 高16位在dx中

除数为8位则被除数为16位 除数为16位则被除数为32位

如果被除数为16位 则 商存在al中 ah中存余数 ,如果为32位 则ax存商,dx存余数,即低位存商,高位存余数

dd伪指令 和 dup操作符在源程序 开辟空间下解释

实验7:

assume cs:code
data segment

    db '1975','1976','1977','1978','1979','1980','1981','1982','1983'

    db '1984','1985','1986','1987','1988','1989','1990','1991','1992'

    db '1993','1994','1995'

    dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514

    dd 345980,590827,803530,1183000,1843000,2759000,3753000,4649000,5937000

    dw 3,7,9,13,28,38,130,220,476,778,1001,1442,2258,2793,4037,5635,8226

    dw 11542,14430,15257,17800

data ends

table segment

    db 21 dup ('year summ ne ?? ')

table ends

code segment
start:	mov ax,data
		mov ds,ax
		mov ax,table
		mov es,ax
		mov bx,0
		mov si,0
		mov cx,21

s1: 	mov ax,[bx]     //进行1975的数据复制
		mov dx,[bx+2]
		
		mov es:[si],ax
		mov es:[si+2],dx
		
		mov ax,84[bx]  //进行snum数据的复制  注意多了偏移84 即 bx+84开始 84类似数组的首地址 bx表示偏移
		mov dx,84[bx+2]
		
		mov es:5[si],ax
		mov es:5[si+2],dx
				
		add bx,4
		add si,16
		loop s1

        mov cx,21
		mov bx,168
		mov di,0

s2:     mov ax,[bx]
		mov es:[di+10],ax
		add di,16
		add bx,2
		loop s2
    
	    mov cx,21
		mov bx,84
		mov si,0
		mov di,0
		
s3:		mov ax,[bx]
		mov dx,[bx+2]
		div word ptr [si+168]
		mov es:[di+13],ax
		add bx,4
		add si,2
		add di,16
        loop s3

		mov ax,4c00h
		int 21h
code ends
end start

最开始我的实现方式,其实用4次循环会更加的可读,但鉴于第一个是字节型,并且题目中一个’1975’内为双字,并且接下来的21个数也为双字,所以可以放在一起进行处理。
程序运行前table段:
在这里插入图片描述debug后table段:
在这里插入图片描述

用一重循环来实现:

assume cs:code
data segment

    db '1975','1976','1977','1978','1979','1980','1981','1982','1983'

    db '1984','1985','1986','1987','1988','1989','1990','1991','1992'

    db '1993','1994','1995'

    dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514

    dd 345980,590827,803530,1183000,1843000,2759000,3753000,4649000,5937000

    dw 3,7,9,13,28,38,130,220,476,778,1001,1442,2258,2793,4037,5635,8226

    dw 11542,14430,15257,17800

data ends

table segment

    db 21 dup ('year summ ne ?? ')

table ends

code segment
start:	mov ax,data
		mov ds,ax
		mov ax,table
		mov es,ax
		mov bx,0
		mov si,0
		mov di,0
		mov cx,21
s:		mov ax,[si]
		mov dx,[si].2
		mov es:[bx],ax
		mov es:[bx].2,dx
		
		mov ax,84[si]
		mov dx,84[si+2]
		mov es:[bx].5,ax
		mov es:[bx].7,dx
		
		div word ptr ds:168[di]
		mov es:[bx].13,ax
		
		mov ax,168[di]
		mov es:[bx].10,ax
		
		add di,2  //用来进行dw数据的复制 所以+2
		add si,4  //用于复制 dd 数据 所以需要+4
		add bx,16 //用来表示处理的行 一行16字节
		loop s
		mov ax,4c00h
		int 21h
code ends
end start

分析清楚偏移,程序比较好实现。

第九章 转移指令原理

前面提到过了,CS和IP寄存器不能用mov来修改,只能是jmp或者loop指令来修改,本章主要围绕修改CS和IP的不同方法来展开。

1、基础知识

8086CPU的转移指令分为以下几类:

概述
1、无条件转移指令 如 jmp
2、条件转移指令
3、循环指令
4、过程
5、中断
转移条件不同,基本原理相同

操作符offset

offset是汇编语言中由编译器处理的,功能是取得标号的偏移地址。

前面所采用的start、s这种标号,本质上是偏移地址,用offset可以将之提取出来。

如下
code segment 
	start:mov ax,offset start //类似于 mov ax,0
	    s: mov ax,offset s     //类似于 mov ax,3 因为第一条mov指令占3个字节码
code ends

2、jmp指令深入

温习:jmp为无条件的跳转指令,可以单独修改IP或是修改IP和CS
jmp ax 单独修改IP
jmp xxxx(cs):xxxx(ip)

1、依据位移进行转移的jmp指令

jmp short 标号
实现段内短转移,对IP的修改为-128~127,负数是向前移动。
注:汇编指令中,指令中的立即数不论是内存单元还是偏移地址,都会在对应机器指令中出现。

而 jmp short 标号对应的机器码 为EB XX,XX表示位移。

jmp short 标号 
功能为 IP=IP+8位位移   -128~127 补码  
XX位移= 标号处的地址 - jmp下一条指令的地址

jmp near ptr 标号 
功能为IP=IP+16位位移 -2^31 ~ 2^31-1

根据转移范围 把8位改变称为段内短转移,把16位变化称为段内近转移。

以上jmp short和 jmp near ptr都是在段内短跳,
请添加图片描述
如图为,8位数据以补码形式存放蟒蛇。

2、地址在指令中的jmp指令

jmp far ptr 实现段间转移,又称为远转移。

far ptr指明了用标号的段地址和偏移地址修改CS和IP
如 jmp far ptr s
在debug载入时会变为 jmp xxxx:xxxx (s的段地址和偏移地址)

3、转移地址在寄存器中的jmp指令

jmp ax 单纯用于修改IP

4、转移地址在内存中的jmp

分为 jmp word ptr 内存 和 jmp dword ptr 内存

1、jmp word  ptr 内存
用于改变 IP 在内存中取一个字用来修改IP 为什么是一个字呢 正好是一个16位寄存器的大小
段内转移

2、jmp dword ptr 内存
用于修改 IP和CS,并且IP是低2个字节,CS是高两个字节
mov ax,0123h         //低位
mov [bx],ax
mov word ptr [bx+2],0 //高位
jmp dword ptr [bx] 
IP=0123h CS=0

jcxz指令和loop指令

jcxz为有条件跳转,所有的条件指令都是段内短转移,通过EB +位移来实现

jcxz + 标号
如果cx等于0,就跳转到标号处
if(cx==0) jmp short s

loop指令为循环指令,所有的循环指令都是短转移。

实现过程
cx--
if(cx!=0) jmp short s

以上两种指令都是段内短转移,并且都依靠cx来进行判断。

可用多个jmp指令来实现循环,如下,实现在内存2000H的段内查找第一个只为0的字节,找到就把偏移量存在dx中。

1、依靠jcxz实现

code segment
start:	mov ax,2000h  //养成用标号的习惯
		mov ds,ax
		mov bx,0
s:      mov ch,0   //因为是找第一个为0的字节 所以只用cl,故ch高位赋0
		mov cl,[bx]
		jcxz ok
		jmp s
ok:	mov dx,bx
		mov ax,4c00h
		int 21h
code ends

2、通过loop实现

code segment
start:	mov ax,2000h
		mov ds,ax
		mov bx,0
s:		mov ch,0
		mov cl,[bx]
		inc bx
		loop s
ok:	dec bx
		mov ax,4c00h
		int 21h
code ends

inc 和 dec 前面貌似忘记提到
inc 就是值+1 dec就是值-1 increase 和 decrease

位移转移的意义和越界问题

1、位移转移的意义
以loop为例,loop s 的机器指令为 E2 XX(位移),如果字节将地址写在机器指令中的话,就对程序段在内存中的偏移有了严格的限制,假如说程序运行的时候地址发生了改变,那么可能就会出现一些错误,即s所指的指令的偏移发生了变化,如果是位移的话,无论地址变成什么样,位移始终是不变的(就像保守力做功只看始末位置)

2、编译器对位移超界的检测
根据位移进行转移的指令,转移的范围受到转移位移的限制,如果出现转移范围超界的问题,在编译时编译器会报错哦。
如书本上实例:

code segment
start:	jmp short s
			db 128 dup(0)
s:		mov ax,1
		mov ax,4c00h
		int 21h
code ends

可知jmp short 是段内短转移 范围为-128~127 而 db 128 dup(0) 到mov ax第一个字节为128,超范围了编译器会报错。而第二章所说的jmp 2000:1000编译器也不认识,是debug中的汇编指令,编译时也会出错。
所以jmp短跳位移要合法,或者jmp+寄存器,jmp d/word ptr 内存

惊心动魄的实验环节

实验八 分析一个奇怪的程序

assume cs:code
code 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

code ends
end start

debug 调试如图 蓝框内为对应的机器指令
在这里插入图片描述
当调试完赋值语句,即把s处的两个nop处赋值时。
在这里插入图片描述

此时执行到jmp shrot s处,即图中jmp 0008h,当再次t时程序到cs:0008h处,并且再下一次要进行jmp :0000进行返回语句。

为什么是这样呢?按照汇编的理解不是jmp short s1么? 其实这种理解就是比较浅,忽略了本质,应该想,我们拷贝过去的是什么?

是机器码,也就是EB F6 而这明显是根据位移F6来跳转,F6是(-10)的补码,整数在内存中以补码方式储存。而EB F6 则是下一个指令地址000ah - 10就是目的地址为0000h,即跳转到mov ax,4c00 …程序正常的返回。

这或许也能对应上述位移的优势,如果jmp short s里面存放的只是s的偏移地址,那么程序就不能正常的返回了!!!

根据材料编程

主要提供了一个材料关于80x25彩色模式显示缓冲区,在内存中B8000H~BFFFFH共32KB的空间。主体上就是一个25行 每行80个字的一个结构,并且一个字符被分为一个字的大小,一行160个字节,也就是80个字符,其中字的低8位用来存放ASCII码,高8位村粗字符的属性(背景色、字体色、闪烁情况等)
了解完材料后,实验内容是实现在屏幕中间分别显示绿色,绿底红色、白底蓝色的字符串。
参考属性表
第7位对应BL(闪烁),第6 5 4位依次是R G B 背景色接着第3位是 I 高亮 2 1 0位是R G B字体色

所以,绿色是00000010B ,绿底红色是00100100B,白底蓝色是01110001B,也可以加闪烁耍耍。

因为涉及是正中间,所以要计算一下偏移,因为是25行80列,所以中间是12,13,14行 要输出16个字节中间是33~48列,故第一个地址为[(12-1) x 0xa0 + (33-1) x 2 = 0x720

assume cs:code
data segment
	db 'welcome to masm!'
data ends
code segment
start:	mov ax,0b800h
		mov ds,ax
		mov ax,data
		mov es,ax
		mov bx,720h
		mov si,0
		mov cx,16

s:		mov al,es:[si]
		mov [bx],al
		mov ah,2         ;绿色
		mov [bx+1],ah
		
		mov al,es:[si]
		mov [bx+0a0h],al
		mov ah,00100100B ;绿底红色
		mov [bx+1+0a0h],ah
		
		mov al,es:[si]
		mov [bx+0a0h+0a0h],al
		mov ah,01110001B ;白底蓝色
		mov [bx+1+0a0h+0a0h],ah
		
		inc si
		add bx,2
		loop s
		
		mov ax,4c00h
		int 21h
code ends
end start

在简化一点,直接给al赋值,给ah赋值,直接把ax赋给那片内存。
在这里插入图片描述
注意直接 -g cs:xxx到返回处,才能看到完整的显示。

回顾第一章的图1.9,更能理解8086PC机的内存地址分布,从主存到显存再到各类不可改的ROM,显存的地址是A0000~BFFFF,以上操作便是在显存中进行,所以可以直接显示到显示器上。
请添加图片描述

总结

以上便是自己对汇编语言前九章的理解(前四章先鸽着),在学习的同时上机操作也能增添许多乐趣,但是鸽了好久了,不得不说汇编的逻辑真的好清晰,直接对线CPU编程的感觉十分的舒适,后边的部分持续更新,保存笔记方便以后查阅,如果有错误或不足还请指出。

资料链接

最后附上一个大师傅汇编第二版的答案资料和debug等,答案资料的大部题目解释的还是十分清晰的。
链接:https://pan.baidu.com/s/1mOoY4_2-0qBoBAjpKFuAVQ
提取码:cumt
附件非常小,网盘也应该会很快的。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值