8086汇编精讲2

学习学习

Posted by lll-yz on October 23, 2021

源程序到可执行程序

1、编写源程序,开各种编辑器写代码,产生一个文本文件。

2、编译、链接,编译产生目标文件obj,再链接生成可执行文件。

​ 可执行文件包含”程序”和”数据”以及相关的描述信息(程序多大,占用内存空间大小等)

3、操作系统执行可执行文件中的程序。

源程序:

assume cs:codesg

codesg segment
	
	mov ax,0123H
	mov bx,0456H
	add ax,bx
	add ax,ax
	
	mov ax,4C00H
	int 21H
	
codesg ends

end

说明:

1、伪指令

​ xxx segment end

​ xxx ends

​ assume

2、指令

3、标号

​ codesg

图解:

5RC59A.png

程序的基本框架

框架:各种段构成,存放数据、代码、栈空间等等。

eg. 编程运算2^3。

(1)我们要定义一个段,名称为 abc。

abc segment
    ..
abc ends

(2)在这个段中写入汇编指令,来实现我们的任务。

abc segment
	
	mov ax,2
	add ax,ax
	add ax,ax
	
abc ends

(2)然后,要指出程序在何处结束。

abc segment

	mov ax,2
	add ax,ax
	add ax,ax
	
abc ends

end

(4)abc 被当作代码段来用,所以,应该将 abc 和 cs 联系起来。(当然,对于这个程序,也不是非这样做不可。)

assume cs:abc

abc segment

	mov ax,2
	add ax,ax
	add ax,ax
	
abc ends

end

(5)还有一个要加:

assume cs:codesg

codesg segment
	
	mov ax,0123H
	mov bx,0456H
	add ax,bx
	add ax,ax
	
	mov ax,4C00H
	int 21H
	
codesg ends

end

DOSBox

使用暂时省略…

两个问题:

1、之前说过有一个正在运行的程序将1.exe装入内存,这个程序是什么?载入内存后,如何执行?

2、程序结束后,返回到哪里?

操作系统都有内核和外壳,操作员通过shell与系统交互DOS中有一个command.com,叫命令解释器,是DOS的shell,DOS初始化后,会运行command.com,然后你懂的。

答:DOS中执行1.exe的,是command.com,command.com设置CPU的CS:IP指向程序入口,得以执行,结束后返回到command.com。

5RkRKI.png

[]:当bx破门而入

提示:对于[bx]来说,默认段寄存器也是ds。

mov ax,[bx]

mov al,[bx]

潜规则——()

因为某些原因,我们使用”()”来表示一个寄存器或内存单元中的内容,如(ax)表示ax的内容,(2000H)表示内存2000H单元中的内容。

5fwUyQ.png

潜规则——idata

idata表示常量,比如mov ax,[0]其中的0就是常量,以后统一写出mov ax,[idata]

而对于mov bx,idata 就相当于mov bx,1 或 mov bx2 等等。

Loop指令

loop:名字听起来,像是循环的样子,没错,他就是循环指令。

loop指令格式:loop 标号

CPU执行loop时,要进行两步操作:

1、(cx)=(cx-1)

2、判断cx中的值,不为0则跳到标号处执行,为0则向下执行。

总而言之,loop做循环指令,cx放循环次数。

eg.编程计算2^n,这里我们取n=12:

assume cs:code
code segment
	mov ax,2
	
	mov cx,11
s:  add ax,ax
	loop s
	
	mov ax,4c00h
	int 21h
	
code ends
end

测试

inc dec

在在这之前要先了解:

inc 加1指令
dec 减1指令

一、加一指令inc
inc a 相当于 add a,1 //i++
优点 速度比sub指令快,占用空间小
这条指令执行结果影响AF、OF、PF、SF、ZF标志位,但不影响CF进位标志位.

二、减一指令dec
dec a 相当于 sub a,1 
004012D7      83E8 01        SUB EAX,1
004012DA      836D FC 01     SUB DWORD PTR SS:[EBP-4],1
004012DE      41             INC ECX
004012DE      FF41 FC       INC DWORD PTR DS:[ECX-4]


优点 速度比sub指令快,占用空间小
这条指令执行结果影响AF、OF、PF、SF、ZF标志位,但不影响CF进位标志位.

5f0xu4.png

内存中的情况 地址
BE 21000H
00 21001H
BE 21002H
00 21003H
BE 21004H
BE 21005H
BE 21006H
  21007H

巨蠢MASM

大家可以试试在debug中写mov ax,[0] 发现没有问题。再试试用masm编译mov ax,[0],你会神奇的发现竟然被当成mov ax,0 处理!!!

怎么解决呢?我们可以先将0送入bx,再 mov ax,[bx] 这样就可以被识别。或是在[0]的时候加上 ds:[0] 也可以,如 mov ax,ds:[0]。

程序与段

db 定义字节类型变量,一个字节数据占1个字节单元,读完一个,偏移量加1。 dw 定义字类型变量,一个字数据占2个字节单元,读完一个,偏移量加2。 dd 定义双字类型变量,一个双字数据占4个字节单元,读完一个,偏移量加4。

一个程序,需要很多空间,用于存放代码,数据和一些复杂的结构。

编程计算以下8个数据的和,结果放在ax中,0123h, 0456h, 0789h, 0abch, 0edfh, 0fedh, 0cbah, 0987h。

assume cs:code
code segment
	0123h,0456h,0789h,0abch,0edfh,0fedh,0cbah,0987h

start:
	mov bx,0
	mov ax,0
	
	mov cx,8
s:  add ax,cs:[bx]
	add bx,2
	loop s
	
	mov ax,4c00h
	int 21h
	
code ends

end start

54TbPx.png

assume cs:codesg

codesg segment

	dw 0123h,0456h,0789h,0abch,0edfh,0fedh,0cbah,0987h
	dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
	
start:
	mov ax,cs
	mov ss,ax
	mov sp,30h
	
	mov bx,0
	mov cx,8
s:  push cs:[bx]
	add bx,2
	loop s
    
	mov bx,0
	mov cx,8
s0: pop cs:[bx]
	add bx,2
	loop s0
	
	mov ax,4c00h
	int 21h
	
codesg ends
end start

修改一下格式,更方便,将程序段,栈段,数据段分开放:

assume cs:codesg,ds:datasg,ss:stacksg

datasg segment

	dw 0123h,0456h,0789h,0abch,0edfh,0fedh,0cbah,0987h
	
datasg ends

stacksg segment

	dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
	
stacksg ends

codesg segment

	mov ax,stacksg
	mov ss,ax
	mov sp,20h
	
	mov ax,datasg
	mov ds,ax
	
	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
	
codesg ends
end

测试

1、下面的程序实现依次用内存0:0~0:15单元中的内容改写程序中的数据,完成程序:

注意:这里括号内为我们要填的内容,不是代表值

assume cs:codesg

codesg segment

	dw 0123h,0456h,0789h,0abch,0edfh,0fedh,0cbah,0987h
	
start: mov ax,0
	   mov ds,ax
	   mov bx,0
	   
	   mov cx,8
s:	   mov ax,[bx]
	   (mov cs:[bx],ax)
	   add bx,2
	   loop s
	   
	   mov ax,4c00h
	   int 21h
	  
codesg ends

end start

2、下面的程序实现依次用内存0:0~0:15单元中的内容改写程序中的数据,数据的传送用栈来进行,栈空间设置在程序内,完成程序: 注意:这里括号内为我们要填的内容,不是代表值

assume cs:codesg

codesg segment

	dw 0123h,0456h,0789h,0abch,0edfh,0fedh,0cbah,0987h
	
	dw 0,0,0,0,0,0,0,0,0,0		;10个字单元用作栈空间

start: mov ax,(cs)
	   mov ss,ax
	   mov sp,(24h)
	   
	   mov ax,0
	   mov ds,ax
	   mov bx,0
	   mov cx,8
s:     push [bx]

	   (pop ss:[bx])
	   add bx,2
	   loop s
	   
	   mov ax,4c00h
	   int 21h
	  
codesg ends

end start

与或非

and:与指令,按位进行与运算。

mov al,01100011B
and al,00111011B    ;al=00100011B

or:或指令,按位进行或运算。

mov al,01100011B
or  al,00111011B	;al=01111011

not:非指令,按位进行求反运算。

格式:NOT OPRD (OPRD可为任意通用寄存器或存储器操作数)

卷土重来的字符(串)

字符的定义以及ds和cs在debug下微妙的关系….

assume cs:code,ds:data

data segment

	db "unIX"
	db "foRK"
	
data ends

code segment

start:
	mov al,'a'
	mov bl,'b'
	
	mov ax,4c00h
	int 21h
	
code ends

end start

字母以ASCII的形式存放。

大小写转换程序

将dataseg中第一个字符串改为大写,第二个字符串改为小写。

因为是ASCII形式存放,所以 小写=大写+20H。

assume cs:codesg,ds:datasg

datasg segment

	db 'BaSic'
	ab 'iNfOrMaTion'
	
datasg ends

codesg segment

start:
	mov ax,datasg
	mov ds,ax
	mov bx,0
	mov cx,5
s:
	mov al,[bx]
	and al,11011111B
	mov [bx],al
	inc bx
	loop s
	
	mov bx,5
	mov cx,11
s0: mov al,[bx]
	or al,00100000B
	mov [bx],al
	inc bx
	loop s0
	
	mov ax,
	int 21h
	
codesg ends

end start
codesg ends

end start

[bx+idata]

mov ax,[bx+200]

含义:将一个内存单元的内容送入ax,长度为2个字节,偏移地址bx的值+200,段地址是ds。

潜规则描述法:(ax)=((ds)*16+(bx)+200)

其它皮肤:mov ax,[200+bx]

​ mov ax,200[bx]

​ mov ax,[bx].200

SI和DI

SI和DI也是寄存器,和bx功能相近。

5Iv7FS.png

SI和DI的使用:

5IxZex.png

codesg segment
start:
	mov ax,datasg
	mov ds,ax
	mov si,0
	mov di,16
	
	mov c,8
s:  mov ax,[si]
	mov [di],ax
	add si,2
	add di,2
	loop s
	
	mov ax,4c00h
	int 21h
codesg ends

end start

bx, si, di 联合使用

mov ax,[bx+si]

mov ax,[bx][si]

mov ax,[bx+200+si]

mov ax,[200+bx+si]

mov ax,200[bx][si]

mov ax,[bx].200[si]

mov ax,[bx][si].200

寻址

所谓寻址方式,就是定位内存地址的方法。

5o9QEt.png

bx si di bp

bp:是基址指针,段地址默认在SS中.可以定位物理地址,比如:”mov ax,[bp+si+6]或mov ax,[bp+di+6]。

寻址方式:

5oF8ds.png

汇编中的数据位置

1、立即数(idata) 对于直接包含在机器指令中的数据(执行前在CPU的指令缓冲器中),在汇编中成为:立即数,在汇编指令中直接给出。

2、寄存器 指令要处理的数据在寄存器中,在汇编指令中给出相应的寄存器名。

3、段地址(SA)和偏移地址(EA) 指令要处理的数据在内存中,在汇编指令中可用[X]的格式给出EA,SA在某个段寄存器中。存放段地址的寄存器可用使用默认的。

寻址方式

5okqE9.png

X ptr

x ptr 操作符指明内存长度,如8086里可以为byte和word:

mov word ptr ds:[0],1

inc byte ptr [bx]

add word ptr [bp],2

push和pop永远都是对word(16位)进行操作。

div

div 是除法指令,使用div做除法时要注意以下问题:

  • 除数:有8位和16位两种,在一个reg或内存单元中。
  • 被除数:默认放在ax或dx中,dx存放高16位,ax存放低16位。
  • 结果:如果除数为8位,则al存储除法操作的商,ah存储余数;如果除数为16位,则ax存储除法操作的商,dx存储除法操作的余数。

例子:

5oZNKf.png

转移指令

能够修改CS和IP的指令都是转移指令。

8086CPU的转移行为有如下几类:

  • 段内转移:只修改IP,比如 jmp ax (近转移、短转移)
  • 段间转移:同时修改CS:IP,比如 jmp 1000:0

8086CPU转移指令分类如下:

1、无条件跳转

2、条件跳转

3、循环指令

4、过程

5、中断

jmp:简单&不简单

assume cs:codesg

codesg segment

start: mov ax,0
	   jmp short s
	   add ax,1
s:	   inc ax

codesg ends

end start

jmp short s ;跳转到标号s:处

CPU三部曲:

(1)从CS:IP指向内存单元读取指令,读取的指令进入指令缓冲器;

(2)(IP)=(IP)+所读取指令的长度,从而指向下一条指令;

(3)执行指令。转到 1,重复这个过程。

jmp流程:

5o5EFI.png

段内短转移和近转移

jmp short 标号 (IP) = (IP) +8位位移

jmp near ptr 标号 (IP) = (IP) +16位位移

段间转移(远转移)

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

jcxz指令

条件转移指令之一。鉴于所有的条件转移指令都是短转移,所以机器码中包含的是位移,而不是目的地址。IP修改范围:-128~127

指令格式:jcxz 标号(如果(cx)=0,转移到标号处执行。)

操作:当(cx)=0 时,(IP)=(IP)+8 位位移;

用高级语言(C语言)来描述,就是:

if((cx)==0) jmp short 标号;

检测点

1、(1)程序如下:

assume cs:code
 
data segment
    ?
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段中应该定义哪些数据?

本题中,执行jmp word ptr [b+1]指令后,会将data段[bx+1]中的数据赋值给IP,题目要求指向程序中的第一条指令,因此IP需等于0,所以只需data[1]中的数据为0即可。所以答案:

db x dup() ;x是重复的次数,()里是要重复的数逗号分隔。它可以按照给定的次数来复制某个操作数,可以避免多次输入同样一个数据。

db 8 dup(0) 	;这里定义八个字节的0  

(2)补全程序,使jmp指令执行后,CS:IP指向程序的第一条指令:

assume cs:code
 
data segment
    dd 12345678H
data ends
 
code segment
    start:
        mov ax, data
        mov ds, ax
        mov bx, 0
        mov [bx], __
        mov [bx+2], __
        jmp dword ptr ds:[0]
 
code ends
end start

执行完jmp指令后,跳到第一条指令(将IP置为0),jmp dword ptr ds:[0]指令执行完,会从ds:[0]开始,取出两个字的数据,把前面两个字节的数据赋值给IP,后面两个字节的数据赋值给CS,所以要将data[0]存放0,data[2]存放cs。

0
cs

(3)用Debug查看内存,结果如下:

2000:1000 BE 00 06 00 00 00 ……
 
此时CPU执行指令
 
mov ax, 2000H
mov es, ax
jmp dword ptr es:[1000H]
 
后,(CS) = ? (IP) = ?

因 jmp dword ptr es:[1000H] 的功能是将从es:10000H开始的两个字(四个字节),前面的两个字节赋值给IP,后面两个赋值给CS。所以答案为:

CS=0006 	IP=00BE

2、补全编程,利用jcxz指令,实现在内存2000H段中查找第一个值为0的字节,找到后,将它的偏移地址存储在dx中。

assume cs:code
code segment
start: mov ax,2000H
       mov ds,ax
       mov bx,0

    s: (mov cl,ds:[bx])
       (mov ch,0) 
       (jcxz ok)
       (inc bx)
       jmp short s

   ok: mov dx,bx
       mov ax,4c00h
       int 21h
code ends
end start

程序分析:

​ 标号s前面是将ds:bx指向了段地址为2000H的内存段。s标号到jmp short s是一个循环(死循环,除非有跳出语句)。

​ 这里我们需要jcxz有条件转移语句来实现循环的跳出,jcxz的逻辑表达式只有一个就是(cx)=0,想法将cx的内容赋值为ds:bx,也就是说从ds:[0]开始,逐个字节的将单元内容赋值给cx,然后执行jcxz语句。

​ 由于是逐个字节的比较,bx的偏移量应该是以字节为单元。我们使用的cx寄存器是16位的,我们只需要低8位的cl寄存器就可以了。为了保证ch为0,首先必须置零。它们组合在一起就是cx的整体值(dh+dl)

​ 需填充的 第一行:mov cl,[bx] ;将2000段内存逐个字节赋值给cl

​ 第二行:mov ch, 0 ;保证高8位为0

​ 第三行:jcxz ok ;判断cx值,如果cx=0,跳转到ok标号

​ 第四行:inc bx ;cx!=0,继续执行jcxz后面的语句。递增bx,

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 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

程序分析:

​ 1)保证这个loop循环的动力是:cx!=0,首先搞清这点。

​ 2)如果ds:[0]=0情况下。如果我们使用jcxz指令,只有这种情况满足条件,第一个字节为0.故不能使用jcxz指令。

​ 3)理解loop指令的动作,首先是(cx)=(cx)-1.然后才是跳转到标号执行。

​ 4)添加inc cx指令

​ 如果第一个字节是0情况,inc cx导致(cx)=1,执行到loop指令时,首先(cx)=(cx)-1,导致(cx)=0,loop顺序执行下面的指令(也就是标号ok的指令)。

​ 如果第一个字节非0情况,inc cx导致(cx)>1,执行到loop指令是,首先(cx)=(cx)-1,导致(cx)> 0(不为0),loop跳转到s标号处执行代码。

​ 5)答案:inc cx

ret和retf

ret指令用栈中的数据,修改IP的内容,从而实现近转移。

ret指令本质:

(IP)=((SS)*16+(SP))

(SP)=(SP)+2

retf指令用栈中的数据,修改CS和IP内容,实现远转移。

retf指令本质:

(IP)=((SS)*16+(SP))

(SP)=(SP)+2

(CS)=((SS)*16+(SP))

(SP)=(SP)+2

call

call指令做两件事:

1、将call的下一段指令的IP或CS和IP压入栈中。

2、转移。

显然,call指令不能用于短转移,其他的和jmp一致。

call使用一:

call 标号

(SP)=(SP)-2

((SS)*16+(SP))=(IP)

(IP)=(IP)+16位位移

相当于:

push ip

jmp near ptr 标号

16位位移=标号处地址-call指令后的第一个字节的地址

16位位移范围 -32768~32767,补码表示

16位位移由编译器算出

call使用二:

call far ptr 标号

(SP)=(SP)-2

((SS)*16+(SP))=(CS)

(SP)=(SP)-2

((SS)*16+(SP))=(IP)

(CS)=标号所在段的段地址

(IP)=标号所在段中的偏移地址

相当于:

push cs(call指令的下一条指令的cs地址)

push ip(call指令的下一条指令的ip地址)

jmp far ptr 标号

call使用三:

call 16位reg

(sp)=(sp)-2

((ss)*16+(sp))=(ip)

(ip)=(16位reg)

相当于:

push ip

jmp 16位寄存器

call使用四:

call word ptr 内存单元地址 或 call dword ptr 内存单元地址

相当于:

push ip push cs

jmp word ptr 内存单元地址 push ip

​ jmp dword ptr 内存单元地址

检测

(1)下面程序执行后,ax中的数值为多少?

assume cs:codesg
 
stack segment 
     dw 8 dup (0)
stack ends 
 
code segment
start: 
	mov ax, stack
	mov ss, ax        ;设置ss指向stack数据段
	mov sp, 16        ;设置sp栈指针指向栈底
	mov ds, ax        ;ds也指向stack段
	mov ax, 0
    call word ptr ds:[0EH]    ;执行这条语句时, ip已经指向下一条语句, 即“inc ax”. 那么设此时IP = x; 
							  ;call 相当于push IP; IP= x; 然后jump 到 ds:[0EH], 又因为ds:[0EH] = x; 
							  ;故结果IP依然等于原值, 程序顺序执行 

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

ax=3。

(2)下面的程序执行后 , ax 和 bx 的值是什么 ?

assume cs:code,ds:data
data segment
    dw 8 dup(0)
data ends
code segment
    start:
        mov ax, data
        mov ss, ax
        mov sp, 16
        mov word ptr ss:[0], offset second
        mov ss:[2], cs
        call dword ptr ss:[0]
        nop
    second:
        mov ax, offset second
        sub ax, ss:[0CH]
        mov bx, cs
        sub bx, ss:[0EH]
code ends
end start

这个程序定义了数据段 , 其实也是栈段和数据段使用了同一段内存 首先也是初始化栈 : mov ax, data mov ss, ax mov sp, 16 然后执行 : mov word ptr ss:[0], offset s 将 s 标号处两个字节的地址移动到 ss:[0] 处 mov ss:[2], cs 将代码段地址的值保存在到 ss:[2] 处 , 同样两个字节 call dword ptr ss:[0] CPU首先读取该指令 然后将 ip 增加该指令的长度 然后准备开始执行 (由于是 dword 因此 cs 和 ip 都要压栈) 执行首先要将当前的 cs 压栈 (保存在 ss:[0EH]) 然后 ip (也就是 call 指令下一个指令的地址 , 也就是 nop 的地址) 压栈 (保存在 ss:[0CH]) 处 然后读取 ss:[0] 中的四个字节的数据 , 相当于 : 高地址为段地址 , 低地址为偏移地址 也就说 cs 为 (ss:[2]) , ip 为 (ss:[0]) 这个 cs 和 ip 组成的地址就是 second 的地址 现在开始执行 second mov ax, offset second 读指令 , ip自增 , 然后执行 , 因此执行完后 , ax 为该指令的偏移地址 然后 sub ax, ss:[0CH] 通过之前的分析 : ss:[0CH] 处保存的是 : call dword ptr ss:[0] 这条指令的下一条指令 nop 的相对于代码段偏移地址 ax = (mov ax, offset second 的偏移地址) - ( nop 的偏移地址) 也就是 nop 指令的长度 , 也就是 1 接下来 : mov bx, cs 将代码段的基址赋值给 bx bx = (bx) - (ss:[0EH]) (ss:[0EH]) = (cs) 因此 bx = 0

乘法指令:mul

1、两个相乘的数:要么都是8位,要么都是16位。如果是8位,一个默认放在al中,另一个放在8位reg或内存字节单元中;如果是16位,一个在AX中,另一个在16位reg或内存字单元中。

2、结果,如果是8位乘法,结果默认放在ax中;如果是16位乘法,结果高位默认放在dx中存放,低位放在ax中。

格式 :

mul reg

mul 内存单元

内存单元可以用不同的寻址方式给出,比如:

mul byte ptr ds:[0]

eg. 100*10, 100*10000 (10的十六进制为:aH,100的十六进制为:64H,10000的十六进制为:2170H)

mov al,64H mov ax,64H

mov bl,0aH mov bx,2710H

mul bl mul bx

eg.编程,计算data段中第一组数据的3次方,结果保存在后面的一组dword单元中。

assume cs:code

data segment

	dw 1,2,3,4,5,6,7,8
	dd 0,0,0,0,0,0,0,0

data ends

程序:

code segment
start:
	mov ax,data
	mov ds,ax
	mov si,0
	mov di,16
	
	mov cx,8
s:	
    mov bx,[si]
	call cube
	mov [di],ax
	mov [di+2],dx
	add si,2
	add di,4
	loop s
	
	mov ax,4cooh
	int 21h
	
cube:
	mov ax,bx
	mul bx
	mul bx
	ret
code ends
end start