王爽-汇编语言 万字学习总结

前言

这是我个人在学习王爽老师的《汇编语言》的一些学习笔记,注意,实验很重要,能锻炼我们的逻辑和编程能力,书本上很多实验这里没有记录下来,我自己除了最后一个实验都做了,实验和工具打包到百度网盘了,地址在文章的最后。对文章和代码有任何问题欢迎在评论区留言提问。

基础知识

CPU需要和内存交互,用到的是外部总线

前提须知:我们对总线描述的多少位、多少宽度指的是CPU对其组件交互的一种能力,是一种能力,外部总线由三部分组成:

  • 地址总线

    地址总线有N条地址线,地址总线宽度为N,拥有最多寻找2^N个地址的能力

  • 数据总线

    数据总线的宽度为N,则可以一次性传输 N / 8 个Byte的数据,比如8086数据宽度为16,则一次可最多传输2个字节的数据

  • 控制总线

    CPU对外部器件的控制是通过控制总线进行的,控制总线是一个总称,总线是一些不同控制线的集合。有多少根控制线总线,就意味着CPU提供了对外多少种控制。所以控制总线的宽度决定了CPU对外部器件的控制能力

内存地址空间:假如CPU寻址能力为1024,则1024个内存地址构成了内存地址空间。存储器空间分为ROM,RAM,我们常说的主存即主存地址空间对应硬件内存条,但是地址空间不止这点,还要加上其他硬件的RAM,比如显卡的RAM,这段地址空间会加在主存后面,CPU通过对这段RAM读写数据,显卡就实现相应画面。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y6QNb1Wj-1655735160427)(C:\Users\zhuima\AppData\Roaming\Typora\typora-user-images\image-20211129203706198.png)]

CPU如何控制外设:CPU不能直接控制外设,能直接控制外设的是接口卡,CPU通过控制接口卡,接口卡再控制外设。

寄存器

CPU由运算器、寄存器、控制器组成

  • 运算器进行数据处理
  • 寄存器进行数据存储
  • 控制金控制器件工作
  • 内部总线连接各个器件,在他们之间进行数据的传送

不同CPU寄存器数量不一样,8086有14个寄存器,分别是AX,BX,CX,DX,SI,DI,SP,BP,IP,CS,SS,DS,ES,PSW

常见寄存器是ax,bx,cx,dx,都是16位寄存器,能最大表示2^16 - 1的数字保存一般数据。

常用寄存器分高位和低位,比如ah表示高8位即左边八位,al表示低八位即右边八位(8位存储最大255的数)。

注意:如果对高位或低位单独操作,比如add al,al,如果最后加和得的值为185H,则最后al的值会是85H,前面的1不会进位到ah里,cpu会对al的操作视为一个单独的寄存器。

by the way,对寄存器的操作需要位数匹配,否则会报错,比如:

mov ax,bx

mov al,bh 都可以

mov ax,10101H

mov ax,bl 都会报错

一个16CPU表示什么意思?

  • 运算器一次最多处理16位数据
  • 寄存器最大宽度16位
  • 寄存器和运算器之间的通路为16位

8086是16位CPU,即可以一次处理、运输、暂时存储最大16位数据(地址)。

但是8086地址总线有20位,也就是说可以访问最大1MB大小的地址,而16位8086最多表示64KB大小的数据,是怎么访问1MB的大小的地址呢?

这个问题可以翻译成8086是怎么得出物理地址的

用两个16位地址表示一个20位的地址,即段地址和偏移地址

这里CPU用到了段的概念,物理地址 = 段地址 * 16 + 偏移地址(其中段地址必须是16的倍数)

引用书中原文对这个计算的本质描述: “CPU在访问内存时,用一个基础地址(段地址*16)和一个相对于基础地址的偏移地址相加,给出内存单元的物理地址”。

其中可以得出几个结论:

  • CPU可以用不同的段地址和偏移地址形成同一个物理地址
  • 可以根据需要,将地址连续、起始地址为16位的倍数的一组内存单元定义为一个段。

这种分段的机制是CPU干的,不是内存被分成段了

段寄存器

8086段寄存器有四个段寄存器CS,DS,SS,ES。

这里讨论一下CS寄存器。

CS是代码寄存器,IP是指针寄存器

CS段寄存器和IP偏移地址配合,CS:IP对应物理地址CS*16+IP。

CPU通过访问CS*16+IP地址读取指令运行

可以通过jmp改变IP的值:jmp bx,但CS段寄存器不变

实验一

使用DEBUG命令查看CPU寄存器的相关信息

什么是DEBUG

Debug是DOS、Windows都提供的实模式(8086方式)程序的调试工具,使用它以查看CPU各种寄存器中的内容、内存的情况和在机器码级跟踪程序的运行。

不过现在windows7及以上都不支持debug的命令了,做这个实现需要下载额外的软件配置,具体步骤看下面的链接

https://blog.csdn.net/weixin_34216107/article/details/86259971

常用功能

Debug有很多功能,这里只说常用的命令

  • r 查看或修改寄存器的内容

    查看寄存器中的内容
    在这里插入图片描述

    修改寄存器中的内容

    r后面跟寄存器的名字,中间有没有空格都无所谓,后面介绍的所以命令开始的参数都可以不跟空格,不过后面的参数都需要用空格分割,rcs后输入修改后的地址回车即可
    在这里插入图片描述

  • d 查看内存中的内容

    d后面直接跟段地址:偏移地址,此命令会列出这个地址开始到后面128个字节的内容,即默认查看给出地址后面128个字节内容

在这里插入图片描述

也可以只查看一小段内容,在后面加个偏移量即可

在这里插入图片描述

继续使用d命令不跟参数则默认看后面的内容

在这里插入图片描述

  • e 改写内存中的内容

    e后面直接键入地址,默认从该地址逐字节修改,空格跳到下一个,回车结束修改

在这里插入图片描述

也可修改为字符或字符串

在这里插入图片描述

  • u 将机器指令翻译成汇编代码

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3ygTCVsB-1655735160439)(C:\Users\zhuima\AppData\Roaming\Typora\typora-user-images\image-20211130200642879.png)]

  • t 命令执行一行机器指令

    注意观察寄存器值的变化

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-giufII4D-1655735160440)(C:\Users\zhuima\AppData\Roaming\Typora\typora-user-images\image-20211130200826276.png)]

  • a 命令以汇编代码格式在内存写入机器指令

    a后面跟地址,可以不断键入汇编指令,以回车结束

在这里插入图片描述


寄存器(内存访问)

字的概念

一个字==在内存中连续的两个字节,即16位

8086在高八位存储高位字节,第八位存储低位字节。

在内存中前面的是低位字节,后面的是高位字节,故如果存储9630H在一个字中的话,在内存中是30 96,反过来也是一样,如果内存里是56 78 ,对这两个连续字节类型看成一个字的话,读出来的数据应该是7856H

为什么在这里要说字的概念?

因为8086是16位的,对16位寄存器操作都相当于在对字进行操作

用汇编获取地址里的内容

如标题所说,用下面代码获取地址中的值

mov al,[0]

方括号表示包着的是内存地址,里面内容是偏移地址。

也可以这么写 mov [0],al 意思是将al的值放入ds:0000地址里面

默认的数据段寄存器是DS

故这段语句相当于 mov al,[ds:0],当然这是伪码

对了,CPU硬件设计不支持直接像操作通用寄存器一样直接赋值给段寄存器,只能通过寄存器到寄存器

mov ds,1000 # 非法
mov ax,1000
mov ds,ax # 合法

栈是一段连续的内存地址,编程时默认保存数据,汇编通过push、pop对栈进行操作,比如

push ax # 向栈中填ax的值
pop ax # 弹出栈顶的值,放入ax中

8086使用SS作为栈的段寄存器,SP作为偏移寄存器

SS:SP始终指向栈顶

如果我们把2000:1 ~ 2000:F 这段地址空间作为栈空间的话,当其位空我们执行pop就会越界,同时,栈为空时,sp等于0010,也就是指向底部的下一个内存单元,当其为满时执行push栈也会越界,需要注意的是8086没有防止越界的机制,只能我们自己注意进行判断

栈的操作都是以字为单位,可以有

  • push 通用寄存器 | 段寄存器 | 地址空间 ([xxx])
  • pop 通用寄存器 | 段寄存器 | 地址空间 ([xxx])

需要注意的是,地址空间是向下增加的,也就是上面地址小,下面地址大,同样,栈底在下面,栈顶在上面,入栈时sp减小,出栈时sp增大

d 额外支持的用法

d 段寄存器:offset offset_2
d 通用寄存器:offset offset_2

注意:当出现对ss寄存器进行操作时,比如 mov ss,ax; mov ss,[0];pop ss,其下面的一条语句也会同时运行,这涉及到中断。

第一个程序

用记事本编写1.asm,功能是计算2^3,内容如下

assume cs:abc

abc segment
	mov ax,2
	add ax,ax
	add ax,ax
	
	mov ax,4C00H
	int 12H
abc ends
end

源程序由伪指令汇编代码构成

汇编代码解释:

前面三句不用说,主要是最后两句,这两句是固定的,而且必须要有,相当于return。

伪指令功能介绍:

  • assume

    将有特殊用途的段和相关的寄存器关联起来,这里将代码段和cs寄存器关联在一起,不太重要,这里甚至可以不用写这句伪指令

  • xxx segment

    xxx是自定义的标签,表示定义一个段

    xxx ends 这里对应上面的xxx segment,表示这个段的结束,ends可以理解为end segment

  • end

    表示程序的结束,给编译器说,必须要有

伪指令不会转换为机器码,主要编译器进行识别,转换为机器码的只有里面的汇编代码部分。

将源程序转换为可执行程序分两步:

  1. 编译

    将源码转换为目标文件 x.obj

    MASM 1.asm

  2. 链接

    将目标文件链接成可执行文件,下面引用原书对链接描述:

    1、当源程序很大时,可以将它分为多个源程序文件来编译,每个源程序编译成目标文件后,再用链接程序将他们连接到一起,生成一个可执行文件

    2、程序中调用了某个库文件中的子程序,需要将这个库文件和该程序生成的目标文件连接到一起,生成一个可执行文件

    3、一个源程序编译后,得到了存有机器码的目标文件,目标文件中的有些内容还不能直接用来生成可执行文件,连接程序将这些内容处理为最终的可执行信息。所以,在只有一个源程序文件,而又不需要调用某个库的子程序的情况下,也必须用连接程序对目标文件进行处理,生成可执行文件。

    注意:对于连接的过程,可执行文件是我们要得到的最终结果

    LINK 1.obj

可执行文件的加载过程

首先操作系统会分配给程序一段内容空间,起始地址是 SA:0000,这段内存前面256个字节创建程序段前缀(PSP)的数据区,DOS要利用PSP来和加载程序进行通信。程序加载后ds保存所在内存去的段地址,即ds保存SA的值

然后在256个字节紧跟着后面是程序的入口地址 SA+10H:0000,即设置CS为SA+10,IP为0000,

SA + 10H的来由:SA x 16+256=(SA+16)x 16 = SA+10:0000

使用debug 1.exe可以跟踪程序的运行,需要用``p`命令运行int 21指令

注:PSP的头两个字节是 CD 20

[ BX ] 和 loop指令

要完整描述一个内存单元,需要两种信息:1、内存单元的地址;2、内存单元难度长度(类型)

[ bx ] 和 [0]类似,段地址都是ds,只不过前者偏移地址变成bx,后者偏移地址是0,这里得到了内存地址,那么大小由寄存器来判断,比如mov ax,[bx]就是一个字大小;mov al,[bx]就是一个字节大小

loop 指令

先看一段loop演示代码,功能是计算2^3

assume code

code segment
	mov ax,2
	mov cx,3
s:	add ax,ax
	loop s
	
	mov ax,4c00h
	int 12h
code ends
end

loop功能描述:1、先cx寄存器自减一;2、如果不为0就跳转到标记点(在这里是s),如果为0则向下运行

所以遇到loop的功能框架如下

	mov cx,循环次数
s: ;这里是s,自己编写可以取别的标签
	循环执行的程序段
	loop s

问题:在跟踪loop的程序时,单步t命令跟踪太慢了,如果循环次数多会很麻烦。

解答:这时可以使用g 偏移地址,这里的偏移地址就是u命令查看汇编代码前面的xxxx:偏移地址这个地址,命令会一次性直接运行到这里来,严谨点说是让设置这个偏移地址处的指令为下一个运行的语句。还可以使用p命令一次性运行完loop,不过只限在下一个指令是loop的时候使用。

tips:编写源程序时数据不能以字母开头,也就是说

mov ax,ffffH ;报错
mov ax,0ffffH ;安全

编译器的一个特性

在debug里可以用mov ax,[0]得到偏移地址0里的内容,但是如果源文件里写这句会被转换为mov ax,0,所以源程序要有同样的效果只能:

mov ax,ds:[0] # 显式的在前面加ds
mov ax,[bx] # 用其他寄存器代替

段前缀:

mov ax,ds:[bx]
mov ax,cs:[bx]
mov ax,ss:[bx]
mov ax,es:[bx]

这些出现在访问内存单元的指令中,用于显式地指明内存单元的段地址的ds: \ cs: \ ss: \ es: ,在汇编语言中称为段前缀。

注意:不能随意对某个地址的值进行覆盖,因为其可能保存其他程序的重要数据,在DOS模式下,一般0:200~0:2ff这256个字节没有被操作系统或其他程序使用,意思是以后实验可以安全(任意)使用这段地址。

注意:debug x.exe 程序加载程序后cx保存的是程序的大小,单位是字节

包含多个段的程序

直接上代码

assume cs:code,ss:stack,ds:data
data segment
	dw 1234h,5678h,1231h,0235h
data ends
stack segment
	dw 0,0,0,0,0,0,0,0
stack ends
code segment
start:
	mov ax,stack ;这里不能直接mov,ss,stack
	mov ss,ax	; 因为编译器认为段名是一个数值类型
	...			; 而段寄存器不能直接赋予数值类型
	mov ax,data
	mov ds,ax
	...
	mov ax,4c00h
	int 21h
code ends
end start
	

为什么需要多个段?

  1. 解决数据、栈、代码堆积在一起混乱的问题
  2. 解决数据、栈、代码放一个段导致空间不够用的问题,一个段大小只有64KB

知识点:

  1. assume多出来的ss:stack,ds:data给程序员看的,增加可读性,为了让段寄存器和相关段关联起来还是要在代码段里手动设置
  2. 新关键字dw(define word)定义一个字型数据
  3. 程序开头不再是代码段了,所以需要手动指定程序的入口地址是说明,这就要设置start标签(标签名可以随便取,这里设置start为了增加可读性),编译器通过end start识别到程序的入口地址,从而编译的时候设置cs:ip

更灵活的定位内存地址的方法

定义字符串

db 'hello' ;编译器会自动转换为对应的ASCII编码

一个位运算技巧

and al,11011111B ; 将一个字母变为大写'a' > 'A'; 'A' > 'A'
or al,00100000B ; 将一个字母变为小写 'A' > 'a'; 'a' > 'a'

[bx + idata]

idata表示一个数值

以前用[bx]表示一个内存单元,现在可以用更灵活的方式表示

  • [bx + 200] 表示偏移地址是:bx的内容加上200,下面一样的功能
  • [200+ bx]
  • 200[bx]
  • [bx].200

SI 和 DI 寄存器

si 和 di是8086CPU中和bx功能相近的寄存器,si和di都不能分成两个八位寄存器来使用(ax可以分为ah、al两个八位寄存器,而si和di这两个不能)。

为什么说和bx功能相近?

因为汇编中规定ax作为段地址赋值的中间地址

bx、si、di 作为取地址空间值的偏移地址(或作为偏移地址的基地址),放在[]里来使用,或者ds:si,ds:di


si、di还有一种用法就是:[bx+si]

还有一种同样的写法:[bx][si] (常用)


此时还有一种表现形式

[bx+si+idata],以下和这句一样的效果

[bx+idata+si]

[200+bx+si]

200[bx][si]

[bx].200[si]

[bx][si].200

不禁感叹一句,内存地址的表示显示真tm多 !

一种编程技巧

如果要使用双重循环时,可以开辟一段空间作为栈使用,然后将栈里的内容弹出来给cx

pop cx

数据处理的两个基本问题

数据指令处理前数据在哪

  1. 数据寄存器里mov ax,bx
  2. 数据在指令缓冲区里(立即数)mov ax,0
  3. 数据在内存里 mov ax,[0]

寻址寄存器:bx、si、di、bp

8086CPU规定用上面的四个寄存器寻址(也就是用在"[…]"里)

不过使用也有规定(按一定的组合使用,但四个寄存器单独使用都是可以的),下面是cpu的一些规定:

;下面是正确的
mov ax,[bx+si]
mov ax,[bx+di]
mov ax,[bp+si]
mov ax,[bp+di]
;下面是错误的
mov ax,[bx+bp]
mov ax,[si+di]

;也就是说bx和bp是大哥,不能同时有两个大哥(一山不容二虎)
;si和di是大哥的老婆,两个女人也不能碰面(碰到要掐架)

注意:当"[…]"里出现bp寄存器的时候,默认的段寄存器变成ss

在这里插入图片描述

指令要处理的数据有多长

  1. 通过要操作的寄存器自动判断,如:

    mov ax,0 ;2个字节
    mov al,0 ;1一个字节
    
  2. 如果出现mov [bx],2这个语句就无法判断是多少个字节了

    这种情况要用到X ptr指定数据类型,X在汇编指令种可以为 word 或 byte,如:

    mov byte ptr [0],1	;覆盖一个字节
    mov word ptr [0],2	;覆盖一个字
    

新的指令:

  • div 除法指令

    用法:div reg 或 内存单元(div 除数)

    注意点:

    1、被除数:默认在ax或dx和ax中,如果除数为8位,那么被除数为16位,默认在ax中;如果除数为16位,那么被除数为32位,默认在dx和ax中,dx存高16位,ax存低16位。

    2、结果:如果除数为8位,则AL存储除法操作的商,AH存储除法操作的余数;如果除数为16位,则AX存储除法操作的商,DX存储除法操作的余数。

  • 伪指令dd

    dd即define dword(double word)定义双字

    总结前面提到的定义数据的伪指令:

    db 定义字节

    dw 定义字

    dd 定义双字

  • 伪指令dup

    说明:结合上面定义数据指令使用,初始化大量空间很有用

    用法:db 重复次数 dup(重复内容)

    举例:

    db 3 dup(0)
    ;相当于db 0,0,0
    db 3 dup('123')
    ;相当于db '123123123'
    

转移指令的原理

转移指令:可以修改IP,或同时修改CS和IP的指令。概括的讲,转移指令就是可以控制CPU执行内存中某处代码的指令。

废话不多说,直接上新指令:

  1. 操作符offset

    可以获取标签的偏移地址,如

    code segment
    start:
    	mov ax,offset s (相当于mov ax,3 ;因为这条指令长度是3,所以偏移为3)
    	mov ax,offset satrt (相当于mov ax,0)
    
  2. jmp short 标号(转移到标号处执行指令)

    段内短转移指令,根据标号出的地址和这句地址相减得出位移大小,位移范围 -128 ~ 127 (八位补码的范围)

    注意,机器码EB后面的那个字节是转移的位移数,编译器会自己算位移大小放进去,这里是00,即IP = IP + 00,如果远一点会加一个别的数

在这里插入图片描述

  1. jmp near 标号(转移到标号处执行指令)

    段内近转移指令,原理同上,位移范围是 -32768 ~ 32767(16位补码范围)

  2. jmp far ptr 标号

    断间转移,又称为远转移,可以跳转到别的段运行指令,far ptr指明了指令用标号的段地址和偏移地址修改CS和 IP。如

    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
    

在这里插入图片描述

注意jmp那段指令的机器码,段地址在后面,偏移地址在前面

  1. jmp 16位reg

    只不过用寄存器作为偏移地址

    功能:IP = 16位reg

  2. jmp word ptr 内存单元地址(段内转移)

    功能:从内存单元地址开始处拿一个字当偏移地址,比如

    mov ax,1234h
    mov ds:[0],ax
    mov word ptr ds:[0]
    ; IP = 1234h
    
  3. jmp dword ptr 内存单元地址(段间转移)

    功能:从内存单元地址处开始存放着两个字,高地址处的字是转移的目的段地址,低地址处是转移的目的偏移地址。如:

    mov ax,0123h
    mov ds:[0],ax
    mov word ptr ds:[2],0
    jmp dword ptr ds:[0]
    ; CS=0000,IP=0123h
    
  4. jcxz 标号

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

    功能:判断cx是否为0,为0则跳转到标号去,和下面的C类似:

    if(cx==0)jmp short 标号
    

    说明一下,loop指令是循环指令,也是短转移

    短转移的好处就是跳转的距离是相对的位移,所以换一块内存空也能使用。

注意:如果位移的范围越界了的话,编译器在编译的时候会报错。

CALL和RET指令

RET指令

ret指令相对简单,执行下面两步,变换IP

1、IP = SS*16+SP

2、SP = SP + 2

相当于 pop IP

retf指令则变换IP和CS,执行下面四部

1、IP = SS*16+SP

2、SP = SP + 2

3、CS = SS*16+SP

4、SP = SP + 2

相当于 pop ip,pop cs

CALL指令

call指令就相对多了

  • CALL 标号(段内转移)

    执行下面两步

    1、SP = SP - 2

    2、SS*16+SP = IP

    3、IP = IP + 16位位移

    相当于:

    push ip
    jmp near 标号
    
  • CALL far ptr 标号(段间转移)

    执行下面几步

    1、SP = SP - 2

    2、SS*16+SP = CS

    3、SP = SP - 2

    4、SS*16+SP = IP

    相当于:

    push cs
    push ip
    jmp far ptr 标号
    
  • CALL 16位reg

    执行下面几步

    1、SP = SP - 2

    2、SS*16+SP = IP

    3、IP = 16位reg

    相当于:

    push ip
    jmp 16位reg
    
  • call word ptr 内存单元地址

    相当于:

    push ip
    jmp word ptr 内存单元地址
    
  • call dword ptr 内存单元地址

    相当于:

    push cs
    push ip
    jmp dword ptr 内存单元地址
    

mul指令

乘法指令,和div指令类似。

用法:mul 寄存器或内存单元地址

另一个乘数默认是AL或AX,取决于另一个乘数的位数。如果是八位结果保存在AX中;如果是16位,结果高位保存在DX中,低位保存在AX中。


利用RET和CALL可以实现模块化编程,如

主程序:
	??
	call 子程序

子程序:
	??
	ret

不过这样就涉及到多参数传递寄存器冲突的问题,解决方法:

  • 多参数传递

    用栈来传递多参数

  • 寄存器冲突解决

    在子程序里把要用到的寄存器先入栈(保存现场),在返回之前出栈(还原现场),如:

    主程序:
    	??
    	call 子程序
    
    ; 这里用到两个寄存器
    子程序:
    	;保存现场
    	push cx
    	push ax
    	
    	mov cx,0
    	mov ax,0
    	
    	;还原现场
    	pop ax
    	pop cx
    	ret
    

标志寄存器

一种特殊的寄存器,不同处理机,个数和结构都可能不同,具有以下3种作用:

  1. 用来存储相关指令的某些执行结果
  2. 用来为CPU执行相关指令提供行为根据
  3. 用来控制CPU的相关工作方式。

8086中标志寄存器有16位,叫flag寄存器,存储的信息通常称为程序状态字(PSW)。flag寄存器按位起作用,但不是每一个位都用。

话不多说,直接介绍:

  1. ZF标志

    flag的第6位,零标志位,记录相关指令执行后,结果是否为0,为0则zf=1;结果不为0,zf=0

    mov ax,1
    sub ax,1 ;zf=1
    mov ax,1
    add ax,1 ;zf=0
    

    注意:主要针对的运算执行如add,or,mul等,mov,push等传送指令对标志寄存器没影响。

  2. PF标志

    flag的第2位,奇偶标志位。记录相关指令执行后,计算结果bit位1的个数,如果个数为偶数pf=1,如果为奇数pf=0

    mov ax,1
    sub ax,1 ;pf=1
    mov ax,2
    sub ax,1 ;pf=0
    
  3. SF标志

    flag的第七位,符号标志位,记录相关指令执行后,结果是否为负,为负sf=1,非负sf=0

    mov ax,0
    sub ax,1 ;sf=1
    mov ax,1
    sub ax,1 ;sf=0
    

    注意:将数据当成有符号数使用时,sf才有意义,虽然当成无符号数也会影响sf的值

  4. CF标志

    flag第0位,进位标志位。对无符号数来说

    一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值。

    ;相加的溢出位
    mov al,98h
    add al,al	;cf=1,al=30h
    ;al+al=130
    
    ;相减的借位
    mov al,97h
    sub al,98h	;cf=1
    ;197-98=ff
    
  5. OF标志

    flag第11位,进位标志位。对有符号数来说

    操作结果溢出了of=1,没溢出of=0

    mov al,0F0h
    add al,88h ;of=1
    
  6. adc指令

    带位加法指令,利用CF位上记录的进位值

    指令格式:adc 操作对象1,操作对象2

    功能:操作对象1=操作对象1+操作对象2+CF

    mov ax,2
    mov bx,1
    sub bx,ax
    adc ax,1 ;ax=2+1+cf=4
    
  7. sbb指令

    带位减法指令,同样利用cf值,不过换成了减.

    指令格式:sbb 操作对象1,操作对象2

    功能:操作对象1=操作对象1-操作对象2-CF

    计算003E1000H-00202000H

    mov bx,1000h
    mov ax,003eh
    sub bx,2000h
    sbb ax,0020h
    
    
  8. cmp指令

    格式:cmp ax,bx

    功能:计算ax - bx,但不保存结果,结果影响标志位变化,通过jejl等指令跳转,类似if判断

    无符号数判断总结(书上截图)

在这里插入图片描述

有符号数判断总结

zf=1,说明ax=bx
sf=1,of=0,说明ax<bx
sf=1,of=1,说明ax>bx
sf=0,of=1,说明ax<bx
sf=0,of=0,说明ax>=bx

条件跳转指令

无符号数跳转总结

在这里插入图片描述

有符号数跳转跟无符号数跳转指令相同,只是检测的标志位不同

指令一般配合cmp使用,由上图可知指令跳转是根据标志位跳转的。所以理论上指令add改变了标志位,紧跟着je也能跳转。

DF 标志位

flag的第十位是DF,一般用作串处理。

命令:movsb

作用:

mov es:[di], ptr byte ds:[si]		;8086不支持这种写法,只是演示
如果DF=0,inc si,inc di
如果DF=1,dec si,dec di

同样有:movsw

作用:

mov es:[di], ptr word ds:[si]		;8086不支持这种写法,只是演示
如果DF=0,add si,2,add di,2
如果DF=1,add si,2,add di,2

一般上面两个指令结合指令rep来用

用法:rep movsb / movsw

作用:

s:movsb
loop s

同样,也可以通过两个命令更改DF的值

cld	;df=0
std	;df=1

pushf 和 popf

两个指令分别将标志寄存器入栈和出栈

标准寄存器在DEBUG中的显示

如图

在这里插入图片描述

在这里插入图片描述

中断

中断:发生中断的话无论做什么都会保留现场去执行发生中断的处理程序。

内中断:当CPU内部发生这4个就是内中断

1、除法错误,比如div除法溢出

; 执行下面三条语句就会发生除法错误中断
mov ax,1000h
mov bh,1
div bh

2、单步执行

3、执行into指令

4、执行int指令

中断会发生的事

中断发生会转去执行中断处理程序,这时就需要一个地址,即CS:IP,这些地址的集合叫做中断向量表。

8086的中断向量表存在0:0-0:400出,共1KB的大小。每个中断都有CS:IP,各占一个字,高字保存CS,低字保存IP。

但是0:200后面的512字节的空间是空闲的,所以我们写程序可以利用。

提一点:根据书中的实验11做完,发现0号中断程序被彻底改变(除法中断=0号中断),也就是说除法中断彻底被改变。

单步执行

发生中断后会:

pushf
push cs
push ip

一般搭配指令 iret使用

iret指令做的事:

pop ip
pop cs
popf

可以看到DEBUG程序是可以单步执行的,每执行一下指令t就显示寄存器的状态,然后暂停等待输入。

单步执行是CPU提供的一种供程序单步调试的功能。

一个新知识点:

当执行 mov ss,xxxxh 时,为什么会连带下面一条语句一起运行。

在前面看到,中断会入栈保存现场,所以当一个程序运行到对栈顶寄存器赋值时,还需要对sp赋值栈才完整,随意CPU会自动多运行一条语句,所以我们一般将mov ss 和mov sp写在一起,即当程序运行到mov ss时发生中断也不会跳转到中断处理程序,还会再运行一条语句再跳过去,目的是将栈搞完整。

int 指令

功能

CPU执行int n指令,引发一个n号中断,执行下面4部。

1、取中断类型码

2、标志寄存器入栈,IF=0,TF=0

3、CS、IP入栈

4、IP=n*4,CS=n*4+2

安装一个中断

安装一个7c号中断,安装在内存0:200处

功能是实现一个字型的平方,参数是ax,结果高位在dx,低位在ax

assume cs:code

code segment
start:
	mov ax,cs
	mov ds,ax
	mov si,offset sqr		;ds:si指向源地址
	mov ax,0
	mov es,ax
	mov di,200h				;设置es:di指向目的地址
	mov cx,offset sqrend-offset sqr	;设置cx为传输长度
	cld						;设置传输方向为正
	rep movsb
	
	mov ax,0
	mov es,ax
	mov word ptr es:[7ch*4],200h
	mov word ptr es:[7ch*4+2],0
	
	mov ax,4c00h
	int 21h
sqr:
	mul ax
	iret	; 注意中断最后要写iret,而不是mov ax,4c00h和int 21h
sqrend:nop

code ends
end start

BIOS 中断例程应用

一个中断往往提供多个子程序调用,BIOS和DOS提供的中断例程都是用ah来传递内部子程序的编号。

下面看一下int 10h中断例程的设置光标位置功能。

mov ah,2	;置光标
mov bh,0	;第0页
mov dh,5	;dh放行号
mov dl,12	;dl放列号
int 10h

下面看一下int 10h中断例程的在光标位置显示字符功能。

mov ah,9	;在光标处显示字符
mov al,'a'	;字符
mov bl,7	;颜色
mov bh,0	;第0页
mov cx,3	;字符重复个数
int 10h

在这里插入图片描述

DOS 中断例程应用

int 21h是DOS提供的中断,其中包含了很多子程序

前面使用的int 21h使用的4ch号中断,即程序返回功能,如下:

mov ah,4ch	;程序返回
mov al,0	;返回值
int 21h

下面调用int 21h的在光标处显示字符串的功能。

mov ah,2	;置光标
mov bh,0	;第0页
mov dh,5	;行号
mov dl,12	;列号
int 10h

;ds:dx指向字符串首地址,字符串末尾用$标识
mov ah,9
int 21h

端口

在PC机系统中,和CPU通过总线相连的芯片除各种存储器外,还有已下3种芯片:

1、各种接口卡(比如网卡、显卡)上的接口芯片,它们控制接口卡进行工作;

2、主板上的接口芯片,CPU通过它们对部分外设进行访问;

3、其他芯片,用来存储相关的系统信息,或进行相关的输入输出处理。

CPU对以上3种芯片数据的读写就是通过端口实现的。

汇编对端口的读写只能用in和out,且读入数据只能保存在ax寄存器,16位在ax,8位在al中。

例如:

in al,20h	;从20h端口读入一个字节
out 20h,ah	;向20h端口写入一个字节

shl 和 shr 指令

shl 是逻辑左移指令

shr 是逻辑右移指令

例子:

shl al,1	;al左移一位
shr al,1	;al右移一位

mov cl,2	;移位大于1必须保存在cl中
shl al,cl	;al左移2位

移位的最后一位移出位保存在CF中

例如:AL=0100 0000,左移一位CF=0,左移两位CF=1

下载地址

链接:https://pan.baidu.com/s/1Vay7PFF2WadtIDgC8rLgbQ
提取码:ddmj

  • 1
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值