汇编 订阅
汇编程序是指把汇编语言书写的程序翻译成与之等价的机器语言程序的翻译程序。汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序。汇编语言是为特定计算机或计算机系列设计的一种面向机器的语言,由汇编执行指令和汇编伪指令组成。采用汇编语言编写程序虽不如高级程序设计语言简便、直观,但是汇编出的目标程序占用内存较少、运行效率较高,且能直接引用计算机的各种设备资源。它通常用于编写系统的核心部分程序,或编写需要耗费大量运行时间和实时性要求较高的程序段。 展开全文
汇编程序是指把汇编语言书写的程序翻译成与之等价的机器语言程序的翻译程序。汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序。汇编语言是为特定计算机或计算机系列设计的一种面向机器的语言,由汇编执行指令和汇编伪指令组成。采用汇编语言编写程序虽不如高级程序设计语言简便、直观,但是汇编出的目标程序占用内存较少、运行效率较高,且能直接引用计算机的各种设备资源。它通常用于编写系统的核心部分程序,或编写需要耗费大量运行时间和实时性要求较高的程序段。
信息
外文名
assembler
汉语拼音
huibian chengxu
中文名
汇编程序
学    科
计算机
汇编程序背景信息
汇编执行指令是机器指令的符号化表示,其操作码用记忆符表示,地址码直接用标号、变量名字、常数等表示。汇编执行指令经汇编程序翻译为机器指令,二者之间基本上保持一一对应的关系。汇编伪指令又称作汇编指示,用于向汇编程序提供用户自定义的符号、数据的类型、数据空间的长度,以及目标程序的格式、存放位置等提示性信息,其作用是指示汇编程序如何进行汇编。使用汇编语言编写的源代码,需要通过使用相应的汇编程序将它们转换成可执行的机器代码。这一过程被称为汇编过程。许多汇编程序可以识别代表地址和常量的标签(label)和符号(symbols),这样就可以用字符来代表操作数而无需采取写死的方式。普遍地说,特定的汇编语言和特定的机器语言指令集是一一对应的。许多汇编程序为程序开发、汇编控制、辅助调试提供了额外的支持机制。有的汇编语言编程工具经常会提供宏,它们也被称为宏汇编器。汇编语言不像其他大多数的程序设计语言一样被广泛用于程序设计;在实际应用中,它通常被应用在底层硬件操作和高要求的程序优化的场合。驱动程序、嵌入式操作系统和实时运行程序都需要汇编语言 [1]  。
收起全文
精华内容
参与话题
问答
  • 汇编入门(长文多图,流量慎入!!!)

    万次阅读 多人点赞 2017-05-16 12:43:59
    8086汇编 本笔记是笔者观看小甲鱼老师(鱼C论坛)《零基础入门学习汇编语言》系列视频的笔记,在此感谢他和像他一样共享资源、帮助他人的筒子们==本文比较长,由于笔者个人能力有限,错漏在所难免,欢迎读者们批评...

    8086汇编


    本笔记是笔者观看小甲鱼老师(鱼C论坛)《零基础入门学习汇编语言》系列视频的笔记,是王爽所著的《汇编语言》的简单版,感谢ttps://my.csdn.net/baidu_36313748的,在建议此感谢小甲鱼和像他一样共享资源、帮助他人的筒子们==本文比较长,由于笔者个人能力有限,错漏在所难免,欢迎读者们批评指正。

    图文无关

    一、基础知识

    引言

    • 基本了解硬件系统的结构;
    • 利用硬件系统的编程结构和指令集,有效灵活地控制系统进行工作。

    1.1 机器语言

    • 机器语言是机器指令的集合。电子计算机的机器指令是一系列二进制数字。计算机将之转换为一系列高低电平脉冲信号来驱动硬件工作的。

    1.2 汇编语言的产生

    • 由于机器语言指令都是由01组成,难以编写,记忆和维护程序.所以汇编语言为了解决这一问题产生。汇编语言的主体是汇编指令,汇编指令是机器指令的助记符。
    • 寄存器: CPU中存储数据的器件,一个CPU中有多个寄存器。

    1.3 汇编语言的组成

    • 1、汇编指令(机器码的助记符,有对应的机器码);
    • 2、伪指令(由编译器执行)和其他符号(由编译器识别)。

    1.4 存储器

    • CPU工作需要指令和数据,指令和数据存储在存储器中。

    1.5 指令和数据

    • 在内存或者磁盘中存储的都是为二进制信息,指令和数据由我们设定(走的总线)

    1.6 存储单元

    • 存储器被划分为若干个存储单元,每个存储单元从0开始顺序编号。
    • B、KB、MB、GB、TB等单位。

    1.7 CPU对存储器的读写

    • CPU要对数据进行读写,必须和外部器件进行以下三类信息的交互:

      • 1、存储单元的地址(地址信息);
      • 2、器件的选择、读或写命令(控制信息);
      • 3、读或写的数据(数据信息) 。
    • 总线是连接CPU和其他芯片的导线,逻辑上分为地址总线数据总线控制总线

      逻辑上总线的分类

    • CPU从内存单元中读写数据的过程:

      • 1、CPU通过地址线将地址信息发出;
      • 2、CPU通过控制线发出内存读命令,选中存储器芯片,并通知它将要从中读或写数据;
      • 3、存储器将相应的地址单元中的数据通过数据线送入CPU或CPU通过数据线将数据送入相应的内存单元。

    1.8 地址总线

    • CPU是通过地址总线指定存储单元,地址总线传送的能力决定了CPU对存储单元的寻址能力。(一般32位CPU,寻址能力为2^32=4G)

    1.9 数据总线

    • CPU通过数据总线来与内存等器件进行数据传送,数据总线的宽度决定了CPU和外界的数据传送速度。

    1.10 控制总线

    • 控制总线是一些不同控制的集合,CPU通过控制总线对外部器件的控制。控制总线的宽度决定了CPU对外部器件的控制能力。

    小结

    • 1、汇编指令时机器指令的助记符,与机器指令一一对应。
    • 2、每一种CPU都有自己的汇编指令集。
    • 3、CPU可以直接使用的信息在存储器中存放。
    • 4、在存储器中指令和数据都是二进制信息。
    • 5、存储单元从0开始顺序编号。
    • 6、一个存储单元可以存储8个bit。
    • 7、B、KB、MB、GB等单位之间的转换。
    • 8、CPU管脚和总线相连。总线的宽度表示CPU不同方面的性能:
      • 地址总线的宽度决定了CPU的寻址能力;
      • 数据总线的宽度决定了CPU与其他器件进行一次数据传送的量;
      • 控制总线宽度决定了CPU对系统中其他器件的控制。

    检测点 1.1

    检测点1.1

    1.11 内存地址空间(概述)

    • CPU可寻的内存单元构成这个CPU的内存地址空间。例如一个CPU的地址总线宽度为10,那么可以寻址的1024个内存单元构成了这个CPU的内存空间。

    1.12 主板

    • 主板主板,主要的电路板 :laughing:

    1.13 接口卡

    • CPU通过接口卡间接控制外部设备。

    1.14 各类存储器

    • 随机存储器RAM(主板上的RAM、拓展插槽上的RAM和接口卡上的RAM)和只读存储器器ROM(装有BIOS的ROM)。
      PC集中各类存储器的逻辑连接

    1.15 内存地址空间

    • 各类存储器在物理上是独立的,但是:

      • 1、都和CPU的总线相连;
      • 2、 CPU对他们进行读或写的时候都通过控制线发出的内存读写命令。

      将各类存储器看作一个逻辑存储器

    • 不同的计算机系统的内存地址空间分配情况是不同的。

    二、寄存器(CPU的工作原理)

    引言

    • CPU由运算器、控制器、寄存器 等器件组成,靠内部总线相连。
    • 内部总线实现CPU内部各器件之间的联系;外部总线实现CPU和主板上其他器件的联系。
    • 在CPU中:
      • 运算器进行信息处理;
      • 寄存器进行信息存储;
      • 控制器控制各种器件进行工作;
      • 内部总线连接各种器件在它们之间进行数据的传送。

    2.1 通用寄存器

    • 8086有14个寄存器:
      • AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、CS、ES、PSW
    • AX、BX、CX、DX通常用来存放一般性数据,被称为通用寄存器。
    • 16位寄存器所能存储的数据最大值为2^16^-1 。
    • 为保证兼容性,8086 CPU的通用寄存器可以分为两个独立的8位寄存器使用。例: AX可分为AH和AL。

    2.2 字在寄存器中的存储

    • 8086 CPU所有的寄存器是16位,可以存放2个字节(一个字)。

    • 一字节由8 bit 组成,可以存在8位寄存器中。

    • 字(word)是两字节,16位。

      一个字由两个字节组成

    2.3 几条汇编指令

    • 汇编指令对大小写不敏感

      汇编指令举例

    汇编指令 控制CPU完成的操作 用高级语言的语法描述
    mov ax,18 将8送入AX AX=18
    mov ah,78 将78送入AH AH=78
    add ax,8 将寄存器AX中的数值加上8结果存入AX中 AX=AX+8
    mov ax,bx 将寄存器BX中的数据送入寄存器AX AX=BX
    add ax,bx 将AX,BX中的内容相加结果存入AX中 AX=AX+BX

    检测点 2.1

    检测点2.1

    2.4 物理地址

    • 所有的内存单元构成一个一维的线性存储空间。
    • CPU访问内存单元时要给出内存单元的唯一地址就是物理地址。

    2.5 16位结构的CPU

    • 1、运算器一次最多可以处理16位数据。
    • 2、 寄存器的最大宽度为16位。
    • 3、寄存器和运算器之间的通路是16位。

    2.6 8086 CPU给出物理地址的方法

    • 8086有20位的地址总线,可以传送20位地址,寻址能力为1M;但8086内部为16位结构,只能传送16位的地址。
    • 8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址。

    8086CPU相关部件的逻辑结构

    • 8086CPU读写内存的步骤:
      • 1、CPU中的相关部件提供段子和偏移地址这两个16位的地址;
      • 2、段地址和偏移地址通过内部总线送入到一个称为地址加法器的部件;
      • 3、地址加法器将两个16位地址合并成一个20位的地址;
      • 4、地址加法器通过内部总线将20位物理地址送送入输入输出地址;
      • 5、输入输出控制电路将20位物理地址送上地址总线;
      • 6、20位物理地址被地址总线传送到存储器。
    • 地址加法器工作原理:物理地址=段地址*16+偏移地址。

    地址加法器的过程
    - 段地址*16就是数据左移4位(二进制)

    移位位数 二进制 十六进制 十进制
    0 10B 2H 2
    1 100B 4H 4
    2 1000B 8H 8
    3 10000B 10H 16
    4 100000B 20H 32
    • 一个数据的二进制形式左移N位,相当于该数据乘以2的N次方。一个数据X进制形式左移N位,相当乘以NX。

    2.7 段地址*16+偏移地址=物理地址

    • CPU可以通过不同的段地址和偏移地址形成一个相同的物理地址。

    CPU可以通过不同的段地址和偏移地址形成相同的物理地址

    段地址*16是移位

    2.8 段的概念

    • 人为定义的,将若干地址连续的内存单元看作一个段。用段地址*16定位段的起始地址(基址),用偏移地址定位段中的内存单元。
      段的概念

    • 一个段的起始地址是16的倍数。偏移地址为16位,寻址能力为64K,所以段的最大长度也是64K。

    检测点 2.2

    检测点2.2

    2.9 段寄存器

    • 8086 CPU有4个段寄存器:CS(代码段)、DS(数据段)、SS(堆栈段)、ES(附加段),这4个段提供给8086CPU内存单元的段地址。

    2.10 CS和IP

    • CS(代码段寄存器)IP(指令指针寄存器) 是8086CPU中最关键的寄存器,它们指示了CPU当前要读取指令的地址。在任意时刻CPU将CS:IP指向的内容当作指令执行。
    • 8086CPU工作过程的简要概述:

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

        8086PC机刚开始启动时,CPU从内存FFFF0h单元中读取指令执行,FFFF0h单元中的指令时8086PC机开机后执行的第一条指令。

      • 2、 IP=IP+所读取指令的长度,从而正确的指向下一条指令;

      • 3、执行指令。转到步骤1,周而复始。

    2.11 修改CS、IP的指令

    • mov指令(传送指令) 可以改变8086CPU大部分寄存器的值,但不能用于设置CS、IP的值。
    • jmp指令(转移指令) 可以用来同时修改CS和IP的值,格式为
        jmp 段地址:偏移地址;同时修改CS和IP
        jmp 某一合法寄存器;则是仅修改IP

    2.12 代码段

    • 对于8086PC机,在编程时可以将长度为N(N小于等于64KB)的一组代码存在一组地址连续、起始地址为16的倍数的内存单元中,这段内存是用来存放代码的,从而定义了一个代码段。
    • 利用CS:IP来指向内存单元从而让CPU执行其中的内容。

    检测点 2.3

    检测点2.3

    使用Debug

    windows xp系统自带debug,请使用xp以上系统的读者执行自行下载debug.exe和dosbox,使用方法笔者不再赘述,在dosbox中可以使用debug。
    debug in dosbox

    • 可以使用汇编金手指查阅指令。
    • R命令查看、改变CPU寄存器的内容;
    • D命令查看内存中的内容;
    • E命令改写内存中的内容;
    • U命令将内存中的机器指令翻译成汇编指令;
    • T命令执行一条机器指令;
    • G命令跳转到偏移地址;
    • P命令结束循环或者是int 21H时是退出程序;
    • A命令是以汇编指令的格式在内存中写入一条机器指令。

    三、寄存器(内存访问)

    3.1 内存中字的存储

    • 字是两个字节,要用两个地址连续的内存来存放,字的低位字节存在低地址中,高位字节存放在高地址单元中。

    3.2 DS和[address]

    • DS通常存放要访问的数据的段地址。
    • 8086 CPU由于硬件的设计不支持将数据直接送入段寄存器的操作。

      数据 -> 通用寄存器 -> 段寄存器

    • [ ]里边的数据代表偏移地址值

    • mov指令:
      • 将数据直接送入寄存器;
      • 将一个寄存器或内存单元中的内容送入另一个寄存器;
    • mov指令格式:
    mov 寄存器名,内存单元

    3.3 字型的传送

    • 高地址单元和高8位寄存器,低地址单元和低8位寄存器相对应。

    3.4 mov、add、sub指令

    • 有两个操作对象,jmp只有一个操作对象。
    • 使用汇编金手指查阅指令
    • mov指令的几种形式
    mov 寄存器,数据;mov ax,8
    mov 寄存器,寄存器;mov ax,bx
    mov 寄存器,内存单元;mov ax,[0]
    mov 内存单元,寄存器;mov [0],ax
    mov 段寄存器,寄存器;mov ds,ax
    mov 寄存器,段寄存器;mov ax,ds
         ……
    • add指令的几种形式
    add 通用寄存器,数据
    add 通用寄存器,通用寄存器
    add 通用寄存器,内存单元
    add 内存单元,寄存器
    • sub指令的几种形式
    sub 通用寄存器,数据
    sub 通用寄存器,通用寄存器
    sub 通用寄存器,内存单元
    sub 内存单元,通用寄存器  

    3.5 数据段

    • 对于8086PC机,在编程时可以将长度为N(N小于等于64KB)的一组代码存在一组地址连续、起始地址为16的倍数的内存单元中,这段内存是用来存放数据的,从而定义了一个数据段。
    • 可以通过在DS中存放数据段的段地址,用相关的指令访问数据段中的具体单元来访问数据段中的数据。

    检测点 3.1

    检测点3.1

    3.6 栈

    • 具有特殊的访问方式的存储空间,也是内存空间的一部分,数据先进后出。
    • 有两个基本操作:
      • 入栈:将一个新的元素放到栈顶;
      • 出栈:从栈顶取出一个元素。
    • 栈顶元素最后入栈最先出栈。

    3.7 8086 CPU提供的栈机制

    • 现今的CPU都有栈的设计,基于8086CPU编程可以将一段内存当作栈来使用。
    • 8086CPU的入栈(PUSH)POP(出栈),以字为单位。
      • push ax 将寄存器ax中的数据送入栈
      • pop ax 从栈顶取出数据送入ax
    • 段寄存器SS存放栈顶的段地址,寄存器SP存放栈顶的偏移地址。任意时刻SS:SP指向栈顶元素。push时SP先自减法后写内存,pop先读内存sp后自加。
    • pop之后数据还是存在内存中,push时覆盖。
      CS和IP存放当前指令的段地址和偏移地址。

    3.8 栈顶越界的问题

    • 栈是空的,则SP指向栈底+1的内存。
    • 8086 CPU只纪录栈顶,栈空间由自己控制。栈顶越界问题导致溢出漏洞。
    • 8086CPU只考虑当前的情况:
      • 当前栈顶在何处;
      • 当前要执行的指令时哪一条。

    3.9 push、pop指令

    • 可以直接对段寄存器使用。
    ;pushpop格式
    push 寄存器
    pop 寄存器
    push 段寄存器
    pop 段寄存器
    push 内存单元
    pop 内存单元
    • 通用寄存器命名是x结尾的,段寄存器是以s结尾。
    • CPU在执行指令时,数据的段地址是从DS中获得,代码是在CS中获得,栈地址是从SS获得。

    3.10 栈段

    • 对于8086PC机,在编程时可以将长度为N(N小于等于64KB)的一组代码存在一组地址连续、起始地址为16的倍数的内存单元中,这段内存是当作栈来用,从而定义了一个栈段。
    • 寄存器清零可用sub ax,ax或者直接赋值0,常见的也有使用xor
    • 当栈空间定义为最大时,栈为空时SP=0。

    检测点 3.2

    检测点3.2


    四、第一个程序

    引言

    编写完成的汇编语言程序,用编译器编译成可执行文件并在操作系统中运行。

    4.1 一个源程序从写出到执行的过程

    • 编写
      • 用编辑器(Sublime Text、Nodepad++、UltraEdit)编写,文件后缀为.asm。
    • 编译链接

      • 使用MASM.EXE编译生产obj(目标文件)。masm也请读者自行搜索下载。
      • LINKE.EXE对目标文件进行连接生产可在操作系统中直接运行的可执行文件。

      可执行文件包含程序(机器码)、数据(源程序中定义的数据)和相关的描述信息。

  • 执行

    • 操作系统中依照可执行文件中的描述信息将可执行文件中的机器码和数据加载入内存并进行相关的初始化,然后CPU执行。
  • 4.2 源程序

    • 汇编指令:有对应的机器码的指令,编译为机器码被CPU执行
    • 伪指令:没有对应的机器码,不被CPU所执行,由编译器执行来进行相关的编译工作。
      • segment和ends是用来定义一个段的,是成对使用的伪指令,再写可被编译器编译的汇编程序是必须要用的。
    assume cs:codesg ;假设代码段的名称为codesg
    codesg segment ;定义一个codesg段
    mov ax,0123H
    mov bx,0456H
    add ax,bx
    add ax,ax
    mov ax,4c00h
    int 21h
    codesg ends ;codesg段结束
    end ;是个伪指令,程序的结束标记

    assume用来加上某一段寄存器和程序中的某一用segment……ends定义的段相关联。通过assume说明这种关联,在需要的情况下编译程序可以将段寄存器和某一个具体的段相联系。
    - 一个汇编程序是由多个段组成。一个有意义的汇编程序中至少要用一个段来存放代码。
    - 程序与源程序
    程序与源程序

    • 标号:指代地址
    • 程序的结构

    • 小练习:

    ;编程运算2^3
    assume cs:abc ;段与寄存器关联
    
    abc segment ;定义一个段,名称为abc
    mov ax,2;写入汇编指令
    add ax,ax
    add ax,ax
    
    abd ends      
    end ;程序结束处
    • 程序的返回:一个程序结束后将CPU的控制权交还给使它得以运行的程序的过程。应该在程序的末尾添加返回的程序段。
      codesg:放在segment前面,作为一个段的名称,这个段的名称最终将被编译、连接程序,称为一个段的段地址 。
    mov ax,4c00H
    int 21H ;第21号中断
    ;这两条指令说实现的功能就是程序返回。
    • 语法错误和逻辑错误

    4.3 编辑源程序

    • 使用编辑器编辑,扩展名为.asm
        assume cs:ABC
        ABC segment
            mov ax,2
            add ax,ax
            add ax,ax
            mov ax,4c00H
            int 21h
        ABC ends
        end

    4.4 编译

    • masn和 1.asm在同一目录中,dos下使用masm 1.asm命令即可生产1.obj文件。
      源程序文件和masm文件在同一目录下

      编译源程序

    4.5 连接

    • link 1.obj,生成exe文件,摁enter忽略编译程序提示输入的信息。

      连接程序

      连接生成exe

    • 当源程序很大时,可以将它分成多个源程序文件编译,每个源程序编译成目标文件后再用连接程序将他们连接到一起,生成一个可执行文件。或者程序中调用了某个库文件中的子程序,需要将这个库文件和该目标文件连接到一起,生成一个可执行文件。或者一个源程序编译后得到存有机器码的目标文件,目标文件中的有些内容还不能直接生成可执行文件,连接程序将此内容处理为最终的可执行文件信息。

    4.6 简化编译和连接

    • 使用ml命令,ml 1.asm

    4.7 exe的执行

    • 为兼容16位的程序,使用dosbox运行。

    4.8 可执行文件中的程序转入内存并运行的原理

    • 在dos中可执行文件中的程序p1若要运行吗必须有一个正在运行的程序p2将p1从可执行文件中加载如内存,将CPU的控制权交给它,p1才能得以运行;当p1运行完毕后,应该将CPU的控制权交还给使它de’yi 运行的程序p2。
    • 汇编程序从写出到执行的过程:编程 -> 编译 -> 连接 -> 加载 -> 内存中的程序 -> 运行
    • 在dos系统中.exe文件中的加载过程

      exe文件中程序的加载过程

      psp的内容

    4.9 程序执行过程的跟踪

    • 使用debug(xp以上的系统在dosbox中使用)来跟踪一个程序的运行过程。

    使用debug来跟踪程序运行


    五、[BX]和loop指令

    引言

    • 约定符号()来表示一个寄存器或者一个内存单元中的内容。例如(ax)=0010H表示ax中的内容为0010H;(21000H)=0010H,表示2000:1000处的内容为0010H。
    • 约定符号idata表示常量。

    5.1 [BX]

    • inc指令是自增1的意思
    • 和[0]有些类似,[0]表示内存单元,它的偏移地址是0。[bx]也是表示一个内存单元,它的内存偏移地址在bx中。
    mov bx,0
    mov ax,[bx]
    mov al,[bx]
    • 用以下两种信息描述一个内存单元:
      • 1、内存单元的地址;
      • 2、内训单元的长度(类型)。
        我们用[0]表示一个内训单元时,0表示单元的偏移地址,段地址默认在DS中,单元的长度(类型)可以由具体指令中的其他的操作对象(比如说寄存器)指出。
    mov ax,[0];0对应的字单元,主要单位要看操作对象(寄存器)
    mov al,[0];字节

    5.2 loop指令

    • 指令的格式是loop 标号。CUP执行loop指令时要进两步操作:
      • CX中存放循环的次数,执行时CX中的内容自减1。相当于C的do while
      • 判断CX中的值,不为0则转至标号处执行程序,为0则向下执行。
    • 通常loop指令来实现循坏功能CX中存放循环的次数。
    assume cs:code
           code segment
           mov ax,2
           add ax,ax
    
           mov ax,4c00H
           int 21H
    code ends
    end
    ;计算2^3
    assume cs:code
    code segment
         mov ax,2
         add ax,ax
         add,ax,ax
    
         mov ax,4c00H
         int 21h
    code ends
    end
    ;计算2^12
    assume cs:code
    code segment
    start:  mov ax,2
            mov cx,11
          p:add,ax,ax       
            loop p;p是标号
    
           mov ax,4c00H;masm默认数字是十进制
           int 21H
    code ends
    end start
    ;编程计算123*236,结果放在ax中
    assume cs:code
    code segment
    start:mov ax,0
          mov cx,236
       an:add ax,123
         loop an
          mov ax,4c00H
          int 21H
    code ends
    end start
    assume cs:code
    code segment
    start:mov ax,0
          mov cx,123
      pa:add ax,236
          loop pa
    
          mov ax,4c00H
          int 21H
    
    code ends
    end start

    5.3 在Debug中跟踪用loop指令实现的循环程序

    • 注意:在汇编源程序中数据不能以字母开头,有字母的在前面加0处理。
    • t命令单步执行、G命令和P命令。
    • 使用汇编金手指查阅指令。

    5.4 Debug和汇编编译器Masm对指令的不同处理

    • Degug中mov ax,[0],表示将ds:0处的数据存入al中。ah=0,因为一个内存单元是8位的,ax是16位的,同位存储。而编译器[0]会被当作0处理
    • 将内存2000:0、2000:1、2000:2、2000:3单元中的数据(字节)送入阿al、bl、cl、dl中。

      • debug中:

      debug 1

      debug 3

      • 在MASM中:

      1masmtest

      masmtest2

      • 要在编译器中实现用偏移地址[]中的内容传送先bx来代替,mov 偏移地址,bx 再 mov al,[bx]。如要直接使用[ ]则要加上段地址ds:[偏移地址]
    • 在MASM中:
    mov al,[0] ;将al赋值0
    mov al,ds[0] ;将al赋值段地址为ds,偏移地址为0的内存单元中的内容
    mov al,[bx] ;默认段地址为ds,将al赋值偏移地址为bx
    mov al,ds:[bx] ;将al赋值段地址为ds,偏移地址为bx

    5.5 loop和[BX]的联合应用

    • 可以用循环来解决处理地址连续的内存单元中的数据的问题,用变量来给出内存单元的地址。

    5.6 段前缀

    • 出现在访问内存单元的指令中用显式地指明内存单元的段地址的ds、cs、ss、es称为段前缀。没有显式地给出内存单元的段地址则默认在ds中。

    5.7 一段安全的空间

    在8086模式中,随意向一段内存空间写入数据是危险的,因为这段空间中可能存放着重要的系统数据或代码。

    assume cs:code
    code segment
        mov ax,0
        mov ds,ax
        mov ds:[26H],ax
        mov ax,4c00H
        int 21H
    code ends
    end
    • 但笔者在练习的时候出现dosbox下debug卡死

      dangeroustest

    • dos下0:200H~0:2FFH的256个字节的空间是安全的,dos和其他合法程序一般都不会使用这段空间。内存0000:0000~0000:03FF大小为1kb的空间是系统存放中断处理程序入口地址的中断向量表。一般情况下0:200H~0:2FFH的256个字节的空间所对应的中断向量表都是空的,操作系统和其他应用程序都不占用。
      dos安全空间

    5.8 段前缀的使用

    • 将内存ffff:0~ffff:b段单元中的数据拷贝到0:200 ~ 0:20b单元中
    assume cs:code
    code segment
              mov bx,0 ;(bx)=0,偏移地址从0开始
              mov cx,12 ;(cx)=12,循环12次
          s:  mov ax,offffh
              mov ds,ax ;(ds)=0ffffh
              mov dl,[bx] ;(ds)=((ds)*16+(bx)),将ffff:bx中的数据送入dl
              mov ax,0020h
              mov ds,ax ;(ds)=0020h
              mov [bx],dl ;((ds)*16+(bx))=dl,将数据送入0020:bx
              inc bx ;(bx)=(bx)+1
              loop s
    
              mov ax,4c00h
              int 21h
    code ends
    end
    • 两个内存单元相差64KB则不再同一个段里,需要设置ds的值两次,效率不高。
    • 使用 es(附加段)
    ;优化后的代码,优化了两次设置ds
    assume cs:code
    code segment
               mov ax,offffh
               mov ds,ax ;(ds)=0ffffh
               mov ax,0020h
               mov es,ax ;(es)=0020H
               mov bx,0 ;(bx)=0,此时ds:bx指向ffff:0,es:bx指向0020:0
               mov cx,12 ;(cx)=12,循环12次
           s:  mov dl,[bx] ;(ds)=((ds)*16+(bx)),将ffff:bx中的数据送入dl
               mov es:[bx],dl ;((es)*16+(bx))=dl,将数据送入0020:bx
               inc bx ;(bx)=(bx)+1
               loop s
    
               mov ax,4c00h
               int 21h
    code ends
    end

    六、包含多个段的程序

    6.1在代码段中使用数据

    • 编程计算0123H、0456H,0abxH、0defH、0fesH、0cbaH、0987H这8个数据的和,结果存放在ax中:
    assume cs:codesg
    codesg segment
            dw 0123H,0564H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
            ;dw,define word,定义字型数据,db定义字节型数据
            ;由于数据在代码段中,所以段地址是CS
            ;dw定义的数据在最开始的地方,所以偏移地址是0开始
        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
    codesg ends
    end start ;入口找end

    定义字型数据

    • end的作用除了通知编译器结束之外还有告诉编译器程序的入口在什么地方。
    • 可执行文件中的程序执行过程

    6.2 在代码段中使用栈

    • 利用栈编程将定义的数据逆序(联想栈的特性)存放:dw 0123H,0564H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
    assume cs:codesg
    codesg segment
            dw 0123H,0564H,0789H,0abcH,0defH,0fedH,0cbaH,0987H;地址0~15
            dw 0,0,0,0,0,0,0,0;定义8个字型空数据,后面当作栈来使用,地址是16~31
    
        start:  
                mov ax,cs
                mov ss,ax
                mov sp,32;设置栈底ss:sp指向cs:32,十进制的32
                mov bx,0
                mov cx,8
               s:push cs:[bx]
                add bx,2
                loop s; 以上代码段0~15个单元中的8个字型数据一次入栈
    
                mov bx,0
                mov cx,8
              s0:pop cs:[bx]
                add bx,2
                loop s0;依次出栈8个执行数据到代码段0~15单元中
    
                mov ax,4c00h
                int 21h
    codesg ends
    end start;指明程序入口在start处
    • 如果对此程序的栈有疑惑,跳转到 3.6 栈和3.10 栈段

    6.3 将数据、代码、栈放入不同的段

    • 在8086CPU中数据、栈和代码存储空间不能大于64KB。可以用像定义代码段一样的方法来定义多个段并在其中定义需要的数据,或者通过定义数据来取得栈空间。
    assume cs:codesg,ds:data,ss:stack;在源程序中为三个段进行有意义的名称
    data segment
            dw 0123H,0564H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
    data ends
    
    stack segment
            dw 0,0,0,0,0,0,0,0;定义8个字型空数据,后面当作栈来使用
    stack ends
    
    code segment
        start:      
                mov ax,stack
                mov ss,ax
                mov sp,16;设置栈底ss:sp指向stack:16,
                mov ax,data
                mov ds,ax;ds指向data段
                mov bx,0;ds:bx指向data段中的第一个单元
               s:push cs:[bx]
                add bx,2
                loop s; 以上代码段0~16个单元中的8个字型数据一次入栈
    
                mov bx,0
                mov cx,8
              s0:pop cs:[bx]
                add bx,2
                loop s0;依次出栈8个执行数据到代码段0~16单元中
    
                mov ax,4c00h
                int 21h
    codesg ends
    end start;指明程序入口在start处
    • 程序中指令决定了断中的内容是作为数据处理还是作为指令执行还是作为栈空间使用。

    检测点 6.1

    检测点6.1

    实验五

    assume cs:codesg,ds:data,ss:stack
    data segment
            dw 0123H,0564H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
    data ends
    
    stack segment
            dw 0,0,0,0,0,0,0,0
    stack ends
    
    codesg segment
        start:  mov ax,stack
                mov ss,ax
                mov sp,16
                mov ax,data
                mov ds,ax
                push ds:[0]
                push ds:[2]
                pop ds:[2]
                pop ds:[0]
    
                mov ax,4c00h
                int 21h
    codesg ends
    end start

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

    7.1 and和or指令

    • and指令:逻辑与指令,按位进行与运算。
      and两个同时为真的结果才为真。
    mov al,01100011B
    and al,00111011B
    ;执行后 al=00100011B
    • 可用and指令将操作对象的相应位设为0,其他位不变
    and al,10111111B;将al第六位设为0
    and al,01111111B;将al第七位设为0
    and al,11111110B;将al第0位设为0
    • or指令:逻辑或指令,按位进行或运算。
    mov al,01100011B
    and al,00111011B
    ;执行后 al=01111011B
    • 可用or指令将操作对象的相应位设为1,其他位不变
    and al,01000000B;将al第六位设为1
    and al,10000000B;将al第七位设为1
    and al,00000001B;将al第0位设为1

    7.2 关于ASCII码

    ASCII码表

    • 将字符的ascii码写入显存屏幕就显示出相关的字符。

    7.3 以字符形式给出数据

    • 用‘’的方式指明数据是以字符的形式给出的。例如’A’
    assume cs:code,ds:data
    
    data segment
            db 'unIx'
            db 'foRK'
    data ends
    
        code segment
        start:  mov al,'a'
                mov bx,'b'
    
                mov ax,4c00h
                int 21h
    code ends
    end start

    73 ascii

    以字符形式给出数据

    7.4 大小写转换的问题

    • 大写字母比小写字母ASCII大32(20H)。
    大写 二进制 小写 二进制
    A 01000001 a 01100001
    B 01000010 b 01100010
    C 01000011 c 01100011
    D 01000100 d 01100100
    • 从第0位开始计算,大写字母ASCII码第五位为0,小写字母ASCII码第五位为1。
    ;大小写转换
    assume cs:codesg,ds:datasg
    datasg segment
    db'BaSiC'
    db'iNfOfMaTiOn'
    datasg ends
    
    codesg segment
        start:  mov ax,datasg
                mov ds,ax;设置ds执行datasg段
                mov bx,0;设置(bx)=0,ds:bx指向'BaSiC'的第一个字母
                mov cx,5;设置循环次数,因为BaSiC有5个字母
                  s:mov al,[bx];将ASCII码从ds:bx所指向的单元中取出
                and al,11011111B;口岸al中ASCII码的第5个位置变为0,变为大写字母
                mov [bx],al;转变后将ASCII码写回单元
                inc bx;(bx)加1,ds:bx指向下一个字母
                loop x
                mov bx,5;设置(bx)=5,ds:bx指向'iNfOfMaTiOn'的第一个字母
                mov cx,11
                 s0:mov al,[bx]
                or al,00100000B
                mov [bx],al
                inc bx
                loop s0
    
                mov ax,4c00H
                int 21H
    codesg ends
    end start
    

    7.5 [bx+idata]

    • [bx+idata]表示的是一个内存单元,它的偏移地址为bx+idata
    ;[bx+idata]可以写成以下格式
    mov ax,[200+bx]
    mov ax,200[bx]
    mov ax,[bx].200
    ;使用debug查看内存
    mov ax,2000H
    mov ds:ax
    mov bx,1000H
    mov ax,[bx]
    mov cx,[bx+1]
    add cx,[bx+2]

    7.6 用[bx+idata]的方式进行数组的处理

    • 用[bx+idata]的方式进行数组处理
    ;改进大小写转换程序
    assume cs:codesg,ds:datasg
    datasg segment
    db'BaSiC'
    db'iNfOfMaTiOn'
    datasg ends
    
    codesg segment
        start:  mov ax,datasg
                mov ds,ax;设置ds执行datasg段
                mov bx,0;设置(bx)=0,ds:bx指向'BaSiC'的第一个字母
                mov cx,5;设置循环次数,因为BaSiC有5个字母
              s:mov al,[bx+0];将ASCII码从ds:bx所指向的单元中取出
                and al,11011111B;口岸al中ASCII码的第5个位置变为0,变为大写字母
                mov [bx],al;转变后将ASCII码写回单元
                mov [bx+5];定位第二个字符串的字符
                or al,00100000B
                mov [bx+5],al
                inc bx
                loop s
    
                mov ax,4c00H
                int 21H
    codesg ends
    end start
    • C语言的形式
        include<stdio.h>
    
        char a[5]="BaSiC";
        char b[11]="iNfOfMaTiOn";
        main()
        {
            int i;
            i=0;
            do
            {
                a[i]=a[i]&0xDF;
                b[i]=b[i]|0x20;
                i++;
            }while(i<5);
        }

    7.7 SI和DI

    • SI和DI在8086CPU中和bx功能相近,充当BX的扩充,但是不能分成两个8位寄存器来使用。[SI]段地址默认也是在DS中。
    • 下面的指令实现了相同的功能
    mov bx,0
    mov ax,[bx]
    
    mov si,0
    mov ax,[si]
    
    mov di,0
    mov ax,[di]
    ;-------------
    ;下面的三组指令也实现了另一个组相同的功能
    ;-------------
    mov bx,0
    mov ax,[bx+123]
    
    mov si,0
    mov ax,[si+123]
    
    mov di,0
    mov ax,[di+123]
    • 一般ds:si指向要复制的原始空间,ds:di指向复制的目的空间。
    ;用DI和SI实现复制到它后面的数据区中
    assume cs:codesg,ds:datasg
    datasg segment
    db'welcome to asm!'
    db'................'
    datasg ends
    
    codesg segment
        start  :mov ax,datasg
                mov ds,ax
                mov si,0
                mov di,16
                mov cx,8
              s:mov ax,[si]
                mov [di],ax
                add si,2
                add di,2
                loop s
    
                mov ax,4c00h
                int 21H
    ;------
    ;用数组的思维[bx(si或di)+idata]的方式优化程序
    ;------
    assume cs:codesg,ds:datasg
    datasg segment
    db'welcome to asm!'
    db'................'
    datasg ends
    
    codesg segment
        start  :mov ax,datasg
                mov ds,ax
                mov si,0
                mov cx,8
              s:mov ax,[si];第一个字符串的的第一个元素
                mov [si+16],ax;目标字符串的第二个元素
                add si,2
                loop s
    
                mov ax,4c00h
                int 21H
    codesg ends
    end start
    

    7.8 [bx+si]和[bx+di]

        mov ax,2000h
        mov ds,ax
        mov bx,1000h
        mov si,0
        mov ax,[bx+si]
        inc si
        mov cx,[bx+si]
        inc si
        mov di,si
        mov ax,[bx+di]

    7.9 [bx+si+idata]和[bx+di+idata]

    • 常数后要加.例如[bx+si].idata或者[bx].idata[si]
        mov ax,2000h
        mov ds,ax
        mov bx,1000h
        mov si,0
        mov ax,[bx+2+si]
        inc si
        mov cx,[bx+si+2]
        inc si
        mov di,si
        mov ax,[bx+di+2]

    7.10 不同的寻址方式的灵活应用

    • 编程将数据段中每一个单词的头一个字母改为大写字母。

    数据段中的数据存储结构_2

    assume cs:codesg,ds:datasg
    datasg segment
        db'1. file          ';长度刚好都是16个字节
        db'2. edit          '
        db'3. search        '
        db'4. view          '
        db'5. options       '
        db'6. help          '
    datasg ends
    
    codesg segment
          start:
                mov ax,datasg
                mov ds,ax
                mov bx,0
                mov cx,6
              s:    
                mov al,[bx+3]
                and al,11011111B
                mov [bx+3],al
                add bx,16
                loop s
    
                mov ax,4c00h
                int 21h
    codesg ends
    end start
    • 编程将数据段中每个单词改为大写字母

    数据段中的数据存储结构2

    ;有bug,问题在于cx的使用,进行二重循环,只用一个循环计数器,造成在进行内层的时候覆盖了外层循环的循环计数值。
    assume cs:codesg,ds:datasg
    datasg segment
        db 'ibm             '
        db 'dec             '
        db 'dos             '
        db 'vax             '
    datasg ends
    
    codesg segment
          start:mov ax,datasg
                mov ds,ax
                mov bx,0;用bx来定位行
                mov cx,4
             s0:mov si,0;用si来定位列
                mov cx,3
              s:mov al,[bx+si]
                and al,11011111B
                mov [bx+si],al
                inc si
                loop s
                add bx,16
                loop s0
    
                mov ax,4c00h
                int 21h
    codesg ends
    end start
    • 程序没有返回到cmd

    712bug

    712bug1

    712bug2

    loop s;三次循环后cx等于0add bx,16
    loop s0;先是cx=cx-1再判断时候等于0,此时cx=FFFF不为0再循环,变成死循环了
    • 因为loop是和cx一起使用的,不能多用个寄存器来解决loop循环次数的问题。解决的方法是在每次开始内层循环时用dx将外层循环cx的值保存起来,在执行外层循环的loop指令前再回复外层循环的cx的数值。
    • 改进后程序
    assume cs:codesg,ds:datasg
        datasg segment
        db 'ibm             '
        db 'dec             '
        db 'dos             '
        db 'vax             '
    datasg ends
    
    codesg segment
          start:
                mov ax,datasg
                mov ds,ax
                mov bx,0;用bx来定会行
                mov cx,4
             s0:
                mov dx,cx;用dx寄存器来临时存放外层cx的值
                mov si,0;用si来定位列
                mov cx,3
              s:
                mov al,[bx+si]
                and al,11011111B
                mov [bx+si],al
                inc si
                loop s
                add bx,16
                mov cx,dx;在进行外层循环的时候回复cx的值
                loop s0
                mov ax,4c00h
                int 21h
    codesg ends
    end start
    • 在上面的程序中,8086 CPU si、cx、ax、bx这些寄存器经常要使用到;cs、ip、ds也不能用,因为cs:ip时刻指向当前指令,ds指向datasg段;那么可用的寄存器就只用dx、di、es、ss、sp、bp等寄存器了。内存可以解决经常性的数据暂存问题。为了使程序结构清晰便于阅读,应该使用栈
    • 再次被改进的程序
    assume cs:codesg,ds:datasg
        datasg segment
        db 'ibm             '
        db 'dec             '
        db 'dos             '
        db 'vax             '
        dw 0;定义一个字用来保存cx的值
    datasg ends
    
    codesg segment
          start:mov ax,datasg
                mov ds,ax
                mov bx,0;用bx来定位行
                mov cx,4
             s0:mov ds:[40h],cx;datasg:40h单元存放外层cx的值
                mov si,0;用si来定位列
                mov cx,3
              s:mov al,[bx+si]
                and al,11011111B
                mov [bx+si],al
                inc si
                loop s
                add bx,16
                mov cx,ds:[40h];在进行外层循环的时候回复cx的值
                loop s0
                mov ax,4c00h
                int 21h
    codesg ends
    end start
    • 再次使用栈改进程序
    assume cs:codesg,ds:datasg,ss:stacksg
    datasg segment
        db 'ibm             '
        db 'dec             '
        db 'dos             '
        db 'vax             '
    datasg ends
    
    stacksg segment
        dw 0,0,0,0,0,0,0,0;定义一个段,用作栈段,容量为16个字节
    stacksg ends
    
    codesg segment
          start:mov ax,stacksg
                mov ss,ax
                mov sp,16
                mov ax,datasg
                mov ds,ax
                mov bx,0;用bx来定位行
                mov cx,4
             s0:push cx;datasg:40h单元存放外层cx的值
                mov si,0;用si来定位列
                mov cx,3
              s:mov al,[bx+si]
                and al,11011111B
                mov [bx+si],al
                inc si
                loop s
                add bx,16
                pop cx;在进行外层循环的时候回复cx的值
                loop s0
                mov ax,4c00h
                int 21h
    codesg ends
    end start
    • 编程将数据段中的每个单词的前四个字母改为大写字母

    数据段中的数据存储结构3

    assume cs:codesg,ds:datasg,ss:stacksg
    
    stacksg segment
    stacksg ends
    
    datasg segment
        db '1. display      '
        db '2. brows        '
        db '3. replace      '
        db '4. modify       '
    datasg ends
    codesg segment
          start:mov ax,stacksg
                mov ss,ax
                mov sp,16
                mov ax,datasg
                mov ds,ax
                mov bx,0
                mov cx,4
             s0:push cx
                mov si,0
                mov cx,4
              s:mov al,[bx+si+3]
                and al,11011111B
                mov [bx+si+3],al
                inc si
                loop s
                add bx,16
                pop cx
                loop s0
    
                mov ax,4c00h
                int 21h
    codesg ends
    end start

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

    引言

    • 本章是总结性的内容,数据处理的两个基本问题是
      • 处理的数据在哪?
      • 要处理的数据有多长?
    • 自定义得描述符:
      • reg寄存器
        • ax、bx、cx、dx、ah、al、bh、bl、ch、cl、dh、dl、sp、bp、si、di;
      • sreg段寄存器
        • ds、ss、cs、es。
        • -

    8.1 bx、si、di、bp

    • 在8086 CPU中只有bx、si、di、bp这四个寄存器用在[ ]中进行内存单元寻址。在[]中,组合只能以这四种形式:bx和si、bx和di、bp和si、bp和di
        ;以下指令是错误的
        mov ax,[ax]
        mov ax,[cx]
        mov ax,[dx]
        mov ax,[ds]
        mov ax,[bx+bp]
        mov ax,[si+di]
    • 正确的指令
        mov ax,[bx]
        mov ax,[si]
        mov ax,[di]
        mov ax,[bp]
        mov ax,[bx+si]
        mov ax,[bx+di]
        mov ax,[bp+si]
        mov ax,[bp+di]
        mov ax,[bx+si+idata]
        mov ax,[bx+di+idata]
        mov ax,[bp+si+idata]
        mov ax,[bp+di+idata]
    • [bp]的段地址默认在ss中。

    8.2 机器指令处理的数据所在的位置

    • 绝大部分机器指令时进行数据处理的,大致可以分为3类:读、写、运算。指令在处理前可以在三个地方:CPU内部、内存、端口。
    机器码 汇编指令 指令执行前数据的位置
    89C3 mov bx,[0] 内存,ds:0单元
    89C3 mov bx,ax CPU内部,ax寄存器
    BB0100 mov bx,1 CPU内部,指令缓冲器

    8.3 汇编语言中数据位置的表达

    • 汇编语言中用三个概念来表达数据的位置。
      • 1、立即数(idata)
      • 2、寄存器
      • 3、段地址(SA)和偏移地址(EA)

    8.4 寻址方式总小结

    寻址方式总结_1

    8.5 指令要处理的数据有多长

    • 8086 CPU可以处理byte和word两种数据尺寸。
    • 通过寄存器指明要处理的数据尺寸;push指令只进行字操作,若没有寄存器名存在的情况下,用操作符word ptr或者byte ptr指明内存单元的长度。 例如
        mov word ptr ds:[0],1
        inc word ptr [bx]
        inc word ptr ds:[0]
        add byte ptr [bx],2
        ;假设内存2000:1000 FF FF FF FF FF FF ……
        ;如果用以下指令
        mov ax,2000H
        mov ds,ax
        mov byte ptr [1000H],1
        ;那么内存中的内容变为
        ;2000:1000 01 FF FF FF FF FF ……
        如果是用以下指令
        mov ax,2000H
        mov ds,ax
        mov word ptr [1000H],1
        ;那么内存中的内容变为
        ;2000:1000 01 00 FF FF FF ……

    8.6 寻址方式的综合应用

    86题目_1

    • 初步汇编代码
        mov ax,seg
        mov ds,ax
        mov bx,60h;确定记录物理地址:ds:bx
        mov word ptr [bx+0ch],38;寄存器相对寻址     排名字段改为38
        add word ptr [bx+0eh],70;收入字段增加70
    
        mov si,0;用si来定位产品字符串中的字符
        mov byte ptr [bx+10h+si],'V';相对基址变址寻址
        inc si
        mov byte ptr [bx+10h+si],'A'
        inc si
        mov byte ptr [bx+10h+si],'X'
    • c语言描述
        struct company  /*定义一个公司记录的结构体*/
        {
            char cn[3]; /*公司名称*/
            char hn[9]; /*总裁姓名*/
            int pm;     /*排名*/
            int sr;     /*收入*/
            char cp[3]; /*著名产品*/
        };
    
        struct compant dec={"DEC","Ken Olsen",137,40,"PDF"};
        /*定义一个公司记录的变量,内存中将存有一条公司的记录*/
        mian()
        {
            int i;
            dec.pm=38;
            dec.sr=dec.sr+70;
            i=0;
            dec.cp[i]='V';
            i++;
            dec.cp[i]='A';
            i++;
            dec.cp[i]='X';
            return 0;
        }
    • 按照c语言的风格用汇编写
        mov ax,seg
        mov ds,ax
        mov bx,60h;记录首地址送入bx
    
        mov word ptr [bx].och,38;排名字段改为38
        add word ptr [bx].0eh,70;收入字段增加70
    
        ;产品名字段改为字符串'VAX'
        mov si,0
        mov byte ptr [bx].10h[si],'V'
        inc si
        mov byte ptr [bx].10h[si],'A'
        inc si
        mov byte ptr [bx].10h[si],'X'
    • 多种寻址方式为结构化数据的处理提供了方便。
    • 一般用[bx+idata+si]的方式来访问结构体,用idata定位结构体中的某一数据项,用si定位数组项中的每个元素。 例如:[bx].idata、[bx].idata[si]。

    8.7 div指令

    • div(divide)是除法指令,可用乘法模拟,格式为:
    div reg(寄存器)
    div 内存单元。
    • 除数:8位或16位,在寄存器或内存单元中;被除数:默认放在AX或DX和AX中。
            div byte ptr ds:[0]
            div byte ptr [bx+si+idata]
            ;al放商,ah放余数
    
            div word ptr es:[0]
            div word ptr [bx+si+idata]
            ;ax放商,dx放余数
    除数 被除数
    8位 16为(AX)
    16位 32位(DX高16位+AX低16位)

    - 8位或16位看的是除数。

    运算 8位 16位
    AL AX
    余数 AH DX

    - 利用除法指令计算10001/100编程

        ;被除数1001可用ax寄存器存放,除数100可用8位寄存器存放,要进行8位除法。
        mov ax,1001
        mov bl,100
        div bl
        ;执行后al的值等于0AH(10),ah的值等于1(余数为1)。
    • 利用除法指令计算100001/100编程
        ;被除数100001大于2^16=65535(FFFF),不能用ax来存放,要用dx和ax两个寄存器联合存放。除数小于255,可用一个8位寄存器存放,但是被除数是32位的,除数应为16位,所以要用一个16位寄存器来存放除数。
        ;100001的十六进制为186A1H,100001的高16位(1)存放在dx,低16位(86AH)存放在ax中。
        mov dx,1
        mov ax,86A1H
        mov bx,100
        div bx
        ;执行后ax内容等于03E8H(即1000),dx的值等于1(余数)。

    8.8 伪指令dd

    • db定义字节型数据,dw定于字型数据,dd 定于 dword(double word双字型数据)
    data segment
            db 1;第一个数据为01h,在data:0处,占1个字节
            dw 1;第二个数据为0001h,在data:1处,占1个字
            dd 1;第三个数据为00000001h,在data:3处,占2个字
    data ends
    • 利用除法指令计算 dd 100001H 除以 dw 100,商放在 dw 0中
    data segment
        dd 100001H;低16位存储在ax中,高16位存储在dx中
        dw 100
        dw 0
    data ends
    
        mov ax,data
        mov ds,ax
        mov ax,ds:[0];低16位存储在ax中
        mov dx,ds:[2];高16位存储在dx中
        div word ptr ds:[4]
        mov ds:[6],ax

    8.9 伪指令dup

    • 和db、dw、dd等数据定义伪指令配合使用,用来进行数据的重复。格式 db或者dw或者dd 重复的次数 dup (重复的数据)
    • 例如:
    db 3 dup(0)
    ;定义了3个字节,它们的值都是0,等同于db 0,0,0。
    db 3 dup(0,1,2)
    ;定义了9个直接,它们是012012012,相当于db 012012012
    db 3 dup('abc','ABC')
    ;定义了18个直接,它们是'abcABCabcABCabcABC'

    实验七 寻址方式在结构化数据访问中的应用

    实验7
    实验7数据

    • ds已经和data段联系了,数据段不够用时用扩展段ES
        ;初始化阶段
        mov ax,data
        mov ds,ax
        mov ax,table;data已经被占用
        mov es,ax
    
        mov bx,0
        mov si,0
        mov di,0
        mov cx,21
    
        ;存放年份,每一个bx就是一个字节
        mov al,[bx]
        mov es:[di],al
        mov al,[bx+1]
        mov es:[di+1],al
        mov al,[bx+2]
        mov es:[di+2],al
        mov al,[bx+3]
        mov es:[di+3],al
    
        ;存放公司的总收入
        mov ax,[bx+54H];第一个年收入是dd数据类型,段地址为54H
        mov dx,[bx+54H]
        mov es:[di+5H],ax
        mov es:[di+7H],dx
    
        ;存放公司的人数
        mov ax,[si+0A8H];第一个人数的数据段地址为0A8H
        mov es:[di+0A8H],ax
    
        ;计算人均收入并存放
        mov ax,[bx+54H]
        mov dx,[bx+56H];这两句诗初始化被除数
        div word ptr,ds:[si+0A8H];除以人数
        mov es:[di+0dH],ax;将商放入指定位置
    
        ;为下一次循环时存放数据做准备
        add bx,4;bx确定年份和收入
        add si,2;si确定人数
        add di,16;di确定的是每行的列数
    • 完整的程序
    assume cs:codesg,ds:data,es:table
    
    data segment
            db '1975','1976' '1977' ……
            dd 1622382 ……
            dw 3,7,9 ……
            ;数据在题目中
    data ends
    
    table segment
            db 21 dup('year summ ne ?? ')
    table ends
    
          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 al,[bx]
                mov es:[di],al
                mov al,[bx+1]
                mov es:[di+1],al
                mov al,[bx+2]
                mov es:[di+2],al
                mov al,[bx+3]
                mov es:[di+3],al
    
                mov ax,[bx+54H]
                mov dx,[bx+56H]
                mov es:[di+5H],ax
                mov es:[di+7H],dx
    
                mov ax,[si+0A8H]
                mov es:[di+0AH],ax
    
                mov ax,[bx+54H]
                div word ptr ds:[si+0A8H]
                mov es:[di+0dH],ax
    
                add bx,4
                add si,2
            loop s
    mov ax,4c00h
    int 21h
    codesg ends
    end start

    九、转移指令的原理

    引言

    • 可以修改IP,或者同时修改CS和IP的指令统称为转移指令。 简单的来说可以控制CPU执行内存中某处代码的指令就是转移指令。
    • 8086
    • CPU的转移行为有只修改的段内转移(如jmp ax) 和同时修改该CS和IP的段间转移(如jmp 1000:0)。其中段内转移分为短转移(IP的修改范围为-128~127)和近转移 (IP的修改范围为-32768~32767)。
    • 8086 CPU的转移指令分为以下几类:
      • 无条件转移指令(如:jmp)
      • 条件转移指令
      • 循环指令(如:loop)
      • 过程
      • 中断

    9.1 操作符offset

    • offset是伪指令,由编译器处理,它的功能是取得标号的偏移地址。
        assume cs:codesg
        codesg segment
    
          start:mov ax,offset start;相当于 mov ax,偏移地址0,段地址是从0开始
              s:mov ax,offset s;相当于 mov ax,3,标记的是代码段中的第二条指令,第一条指令长度为3个字节,则s的偏移地址为3
    
        codesg ends
        end start

    9.2 jmp指令

    • jmp为无条件转移,可以只修改IP,也可以同时修改CS和IP。
    • jmp需要两种信息
      • 1、转移的目的地址;
      • 2、转移的距离(段间转移、段内转移、段内近转移)。

    9.3 依据位移进行转移的jmp指令

    • 段内短转移,jmp short 标号 ,对IP的修改范围是-128~127,一个字节的空间,即向前转移最多128字节,向后最多127字节。short 表明指令进行的是短转移,标号指明了指令要转移的目的地,转移指令结束后CS:IP指向标号处的指令
        assume cs:codesg
        codesg segment
    
          start:mov ax,0
              jmp short s
              add ax,1
              s:inc ax
    
        codesg ends
        end start
    • 一般汇编指令中的立即数(idata)会出现在对应的机器指令中。而jmp指令的机器指令并不包含目的地址,包含的是相对于当前IP的转移位移,CPU并不需要目的地址就可以实现对IP的修改。
      92debug_1

    • CPU执行指令的过程 在 2.10 CS和IP

    • jmp short s 指令的读取和执行过程:
      • 1、CS:IP指向jmp short s 的机器码;
      • 2、读取指令码进入指令缓冲器
      • 3、 改变IP,(IP)=(IP)+所读取指令的长度,IP指向下一个指令;
      • 4、CPU执行指令缓冲器中的指令;
      • 5、执行后CS:IP继续指向下一个指令
    • jmp short 标号的功能为(IP)=(IP)+8位位移。
      • 1、8位为=标号处的地址-jmp指令后的第一个字节的地址;
      • 2、short 指明此处的位移为8位;
      • 3、8位位移的范围为-128~127,用补码表示。
      • 4、8位位移由编译程序在编译时算出的。
    • jmp near ptr 标号 指令实现段内近转移,功能为(IP)=(IP)+16位位移。
      • 1、16位为=标号处的地址-jmp指令后的第一个字节的地址;
      • 2、nearptr 指明此处的位移为16位;
      • 3、16位位移的范围为-32769~32767,用补码表示。
      • 4、16位位移由编译程序在编译时算出的。

    转移位移的计算方法

    9.4 转移的目的地址在指令中的jmp指令

    • jmp far ptr 段间转移,又称为远转移
    • 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
    • 机器码中包含了转移的目的地址。

    94debug

    附注3 汇编编译器(masm.exe)对jmp的相关处理

    这里写图片描述

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

    • jmp 16位寄存器,功能是16位寄存器赋值给IP,实现段内的近(短)转移。
    • 参考 2.11 修改CS、IP的指令

    9.6 转移地址在内存中的jmp指令

    • 转移地址在内存中的jmp指令有两种格式:

      • 1、jmp word ptr内存单元地址(16位只能实现段内转移)。 功能是从内存单元地址处开始存放一个字(转移的目的偏移地址),内存单元地址可用寻址方式的格式给出。

            mov ax,0123H
            mov ds:[0],ax
            jmp word ptr ds:[0]
            ;相当于 jmp ax,执行后(IP)=0123h
        
            mov ax,0123H
            mov [bx],ax
            jmp word ptr [bx]
            ;执行后(IP)=0123h
      • 2、jmp dword ptr 内存单元地址(段间转移)。 功能:从内存单元地址处开始存放两个字型数据,高地址是转移的目的段地址,低地址处是转移的目的偏移地址。(CS)=(内存单元地址+2),(IP)=(内存单元地址),内存单元地址可用寻址方式的任一格式给出。

            mov ax,0123H
            mov ds:[0],ax
            mov word ptr ds:[2],0
            jmp dword ptr ds:[0]
        
            mov ax,0123H
            mov [dx],ax
            mov word ptr [bx+2],0
            jmp dword ptr [bx]
        
            ;执行后 (CS)=0,(IP)=0123H CS:IP指向0000:0123

    检测点 9.1

    检测点 9.1

    9.7 jcxz指令

    • 指令格式为jcxz 标号,如果cx的值为0,则转移到标号处执行,不为0则向下执行。
      • 当cx的值为0时,(IP)=(IP)+8位位移,8位位移=标号处的地址-jcxz指令后的第一个字节的地址。
      • 8位位移的范围是-128~127,用补码表。
      • 8位位移由编译器在编译时算出。
    • jcxz指令是有条件转移指令,所有的条件转移指令都是短指令,在对应的机器码中包含转移的位移而不包含目的地址,对IP的修改范围都为-128-127。

    检测点 9.2

    检测点9.2_1

    9.8 loop指令

    • loop指令为循环指令,所有的循环指令都是短转移,在对应的机器码中包含转移的位移而不包含目的地址。操作i:
      • cx先自减1;
      • 当cx的值不为0时,(IP)=(IP)+8位位移,8位位移=标号处的地址-loop指令后的第一个字节的地址。
      • 8位位移的范围是-128~127,用补码表。
      • 8位位移由编译器在编译时算出。

    检测点 9.3

    检测点 9.3

    9.9 根据位移进行转移的意义

        jmp short 标号
        jmp near ptr 标号
        jcxz 标号
        loop 标号
    • 它们对IP的修改时根据转移目的地址和转移起始地址自检的位移来进行的。在它们对应的机器码中不包含转移的目的地址,而包含的是目的地址的位移距离。方便了程序段在内存中的浮动分配,没有固定目的地址的限制,更灵活。

    9.10 编译器对转移位移超界的检测

    • 根据位移进行转移的指令,它们的转移范围受到了转移位移的限制,如果在源程序中出现了转移范围超界的问题,在编译时编译器会报错。
    assume cs:code
    
        code segment
        start:  jmp short s
              db 128 dup(0)
              s:mov ax,0FFFFH
        code ends
    end start

    910err

    910err2

    实验8

    • 实验八可以正常退出
      test8
    assume cs:codesg
    codesg segment
                mov ax,4c00h
                int 21h
    
          start:mov ax,0
              s:nop
                nop;nop占用两个字节,不执行任何操作
    
                mov di,offset s
                mov si,offset s2
                mov ax,cs:[si];jmp short s1的机器码给了ax
                mov cs:[di],ax;覆盖到指令 s:nop nop那
    
             s0:jmp short s;s那已经被jmp short s1机器码覆盖
             s1:mov ax,0
                int 21h
                mov ax,0
             s2:jmp short s1;jmp -8h,向上跳到s1,s1又向上跳-10字节
                nop
    codesg ends
    end start   

    test8debug

    实验9

    实验9

    assume cs:code,ds:data,ss:stack
    
    data segment
                db'welcome to masm!';定义要显示的字符串(共16字节)
                db 02H,24H,71H;定义字符的属性
    data ends
    
    stack segment
                dw 8 dup(0)
    stack ends
    
    code segment
    
        start:
                mov ax,data
                mov ds,ax
                mov ax,stack
                mov ss,ax
                mov sp,10H
    
                xor bx,bx;bx清零,用来索引颜色
                mov ax,0b872H;算出屏幕第12行中间的显存的段起始位置放入ax中
    
                mov cx,3;s3循环控制行数,要显示三个字符串外循环为3次
            s3: push cx;三个进栈操作为外循环s3保存相关寄存器的值
                push ax;以防止它们的值在内循环中被破坏
                push bx
    
                mov es,ax;此时es为屏幕第12行中间的显存的段起始位置
    
                mov si,0;si用来索引代码列的字符
                mov di,0;di用来定位目标列
    
                mov cx,10H
                ;s1循环控制存放的字符,一个字符串中含有10H个字节内循环为10H次
            s1: mov al,ds:[si]
                mov es:[di],al
                inc si
                add id,2
                loop s1;吃循环实现偶地址中存放字符
    
                mov di,1;设置di的值为1,为在显存奇数地址中存放字符的颜色属性做准备
                pop bx
                mov al.ds:[bx+10H];取消颜色属性
                inc bx
    
                mov cx,10H;第二个内循环也为10H
            s2: mov es:[di],al
                add di 2
                loop s2;此循环实现奇数地址存放字符的颜色属性
    
                ;以下4句为下一趟外循环做准备
                pop ax
                add ax,0AH;将显存的段地址起始地址设置为当前行的下一行
                          ;[在段地址中甲0aH,相当于在偏移地址中加了0a0h(=160d)]
                pop cx
                loop s3
    
                mov ax,4C00H
                int 21H
    
    code ends
    end start
    • welcome to masm

    welcome to masm

    十、CALL和RET指令

    引言

    • 回想程序之间的加载返回过程。
    • call和ret指令都是转移指令,它们都修改IP或者同时修改CS和IP,经常被共用来实现程序的设计。
    • 这一章讲解call和ret指令的原理。

    10.1 ret和retf指令

    • ret指令用栈中的数据来修改IP的内容,从而实现近转移。
    • CPU执行ret指令时:
      • 1、(IP)=((SS)*16+(SP)),指向栈顶
      • 2、(SP)=(SP)+2
    • retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移。
    • CPU执行retf指令时,进行下面两步操作:
      • 1、(IP)=((SS)*16+(SP))
      • 2、(SP)=(SP)+2
      • 3、(CS)=((SS)*16+(SP))
      • 4、(SP)=(SP)+2
    • 用汇编的语法来解释ret和retf指令:
      • CPU执行ret指令相当于进行 POP IP
      • CPU执行retf指令相当于进行 POP IP和POP CS
    assume cs:codesg
    
     stack segment
                  db 16 dup(0)
    stack ends
    
              codesg segment
                  mov ax,4c00h
                  int 21h
    
              start:
                  mov ax,stack
                  mov ss,ax
                  mov sp,16
                  mov ax,0
    
                  push ax
    
                  mov bx,0
                  ret
    codesg ends
    
    end start

    1012debug_1

    assume cs:codesg
    
    stack segment
            db 16 dup(0)
    stack ends
    
    codesg segment
            mov ax,4c00h
            int 21h
    
        start:
            mov ax,stack
            mov ss,ax
            mov sp,16
            mov ax,0
            push cs
            push ax
    
    
            mov bx,0
            retf
    codesg ends
    
    end start

    检测点 10.1

    检测点10.1

    10.2 call指令

    • call指令经常跟ret指令配合使用,CPU执行call指令时:
      • 1、将当前的IP或者CS和IP压入栈;
      • 2、转移(jmp)。
    • call指令除了不能实现短转移之外,call指令实现转移的方法和jmp指令的原理相同。call指令实现段间的转移(远转移)或近转移。

    10.3 依据位移进行转移的call指令

    • call标号(将当前的IP压入栈后转到目标处执行指令),执行时进行以下操作:
      • 1、(SP)=(SP)-2
        ((SS)*16+(SP))=(IP)
      • 2、(IP)=(IP)+16位位移;
      • 3、16位位移=标号处的地址减去call指令后的第一个字节的地址。16位位移的范围是-32768~32767,用补码表示。16位位移由编译器编译时算出。
    • 用汇编语法解释call指令:
    push IP
    jmp near 标号

    检测点 10.2

    检测点10.2

    10.4 转移的目的地址在指令中的call指令

    • call far ptr 标号 实现的是段间转移,执行时:
      • 1、CS先自减2;
      • 2、CS的值等于SS的值乘以16加上SP的值,SP自减2,IP的值等于SS的值*16加上SP的值;
      • 3、CS的值等于标号所在的段地址,IP的值等于标号所在的偏移地址.
    • 用汇编语法解释call指令:
    push CS
    push IP
    jmp far ptr 标号

    检测点 10.3

    检测点 10.3

    10.5 转移地址在寄存器中的call指令

    • 指令格式是:call 16位寄存器,功能是:
      • 1、SP的值先自减2;
      • 2、IP的值SS的值乘以16再加上SP的值;
      • 3、 IP的值等于16位寄存器的内容。
    • 用汇编语法解释此种call指令,CPU执行call 16位reg时,相当于:
    push IP
    jmp 16位寄存器

    检测点 10.4

    检测点 10.4

    10.6 转移地址在内存中的call指令

    • 转移地址在内存中的call指令有两种格式
           call word ptr 内存单元地址;段内跳转
           call dword ptr 内存单元地址;段间跳转
    • 用汇编语法解释call word ptr 内存单元地址
           push IP
           jmp word ptr 内存单元地址
    • 例子:
           mov sp,10h
           mov ax,0123H
           mov ds:[0],ax
           call word ptr ds:[0]
           ;执行后IP的值等于0123H,SP的值等于0EH
    • 用汇编语法解释call dword ptr 内存单元地址
           push CS
           push IP
           jmp word ptr 内存单元地址
    • 例子:
           mov sp,10h
           mov ax,0123H
           mov ds:[0],ax
           mov word ptr ds:[0],0
           call dword ptr ds:[0]
           ;执行后IP的值等于0123H,SP的值等于0CH,CS的值等于0

    检测点 10.5

    检测点10.5

    10.7 call和ret的配合使用

    • 下面的程序返回前,bx中的值是多少?
    assume cs:code
    code segment
          start:
                mov ax,1
                mov cx,3
                call s
                mov bx,ax
                mov ax,4c00h
                int 21h
              s:
                add ax,ax
                loop s
                ret
    code ends
    end start
    • 具有一定功能的程序段称为子程序,用call转去执行,在子程序后面使用ret实现返回。
    • 具有子程序的源程序的框架如下

    107call

    10.8 mull指令

    • mull指令时乘法指令,相乘的两个数要么都是8位的,要么都是16位的
      • 8位:在AL中和8位寄存器中或内存字节单元中;
      • 16位:在AX中和16位寄存器或内存字单元中。
      • 结果
        • 8位的存放在AX中;
        • 16位:DX(高位)和AX(低位)中。
    mull reg
    
    mull 内存单元
    mull byte ptr ds:[0]
    
    mull word ptr [bx+si+idata]
    ;(ax)=(ax)*((ds)*16+(bx)+(si)+idata)
    ;(dx)=(ax)*((ds)*16+(bx)+(si)+idata)
    ;计算100*10,两个数都小于255,可以做8位乘法
    mov ax,100
    mov bx,10
    mull bl
    ;结果(ax)=1000(03E8H)
    
    ;计算100*1000,1000都大于255,要做16位乘法
    mov ax,100;高位自动补零
    mov bx,10000
    mull bx
    ;结果(ax)=4240H,(dx)=000FH,F4240H=1000000

    10.9 模块化程序设计

    • cal和ret指令共同支持汇编语言编程中的模块化设计。

    10.10 参数和结果传递的问题

    • 用寄存器来存储参数和结果是最常用的方法。对于存放参数的寄存器和存放结果的寄存器,调用者和子程序的读写操作恰恰相反:

      • 调用者将参数送入参数寄存器,从结果寄存器中取到返回值;
      • 子程序 从参数寄存器中取到参数,将返回值送入结果寄存器。
    • 编程:根据提供的N来计算N^3

    cube:mov ax,bx
                   mul bx
                   mul bx
                   ret
    • 编程:计算data段中第一组数据的3次方,结果保存在后面一组dword单元中
    assume cs:code
    
    data segment
              dw 1,2,3,4,5,6,7,8
              dd 8 dup (0)
    data ends
    
    code segment
              start:
                  mov ax,data
                  mov ds,ax
                  mov si,0;ds:si指向第一组word单元
                  mov di,16;ds:di指向第二组dword单元
    
                  mov cx,8
              s:  mov bx,[si]
                  call cube
                  mov [di],ax
                  mov [di+2],dx
                  add si,2;ds:di指向下一个word单元
                  add di,4;ds:di指向下一个dword单元
                  loop s
    
                  mov ax,4c00h
                  int 21h
    
                 cube:mov ax,bx
                  mul bx
                  mul bx
                  ret
    code ends
    end start

    10.11 批量数据的传递

    • 将批量数据放在内存中,然后将他们呢所在内存空间的首地址放在寄存器中,传递给需要的子程序,批量数据的返回结果也是采用同样的方法。除此之外还可以用栈来传递参数。
    assume cs:code
    
    data segment
        db'conversation'
    data ends
    
        start:
            mov ax,data
            mov ds,ax
            mov si,0;ds:si指向字符串(批量数据)所在空间的首地址
    
            mov cx,12;cx存放字符串的长度
            call capital
    
            mov ax,4c00h
            int 21h
    
        capital:
            add byte ptr [si],11011111B
            inc si
            loop capital
            ret
    code ends

    10.12 寄存器冲突的问题

    • 编程:将一个全是字母,以0结尾的字符串转化为大写
        capital:
            mov cl,[si];低8位
            mov ch,0;高8位设置为0
            jcxz ok;如果(cx)=0则结束,如果不是0则处理
            and byte ptr [si],11011111B
            inc si
            jmp short capital
        ok:
            ret
    • 编程将data段中的字符串全部转化为大写
        assume cs:code
        data segment
        db'word',0
        db'unix',0
        db'wind',0
        db'good',0
        data ends
        ;此程序有bug,cx有问题
        assume cs:code
    
        data segment
        db'word',0
        db'unix',0
        db'wind',0
        db'good',0
        data ends
    
        code segment
        start:
                mov ax,data
                mov ds,ax
                mov bx,0
    
                mov cx,4
            s:
                mov si,bx
                call capital
                add bx,5
                loop s
    
                mov ax,4c00h
                int 21h
    
        capital:
                mov cl,[si]
                mov ch,0
                jcxz ok
                and byte ptr [si],11011111b
                inc si
                jmp short capital
            ok:
                ret
    
        code ends
        end start

    实验十

    test10_1_1
    test10_1_2
    test10_1_3

    • 实验10.1 显示字符串
    assume cs:code
    
    data segment
        db 'welcome to masm!',0
    data ends
    
    code segment
        start:
                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:;子程序
                push cx
                push si
    
                mov al,0A0h;每行有80*2=160个字节=0a0h
                dec dh;行号在显存中下标从0开始,所以减1
                mul dh;相当于从第(n-1)*0a0h个byte单元开始
    
                mov bx,ax;定位好的位置偏移地址存放在bx里(行)
    
                mov al,2;每个字符占2个字节
                mul dl;定位列,结果ax存放的是定位好的列的位置
                sub ax,2;列号在显存中下标从0开始,又因为是偶字节存放字符,所以减2
    
                add bx,ax;此时bx中存放的是行与列的偏移地址
    
                mov ax,0B800h;显存开始的地方
                mov es,ax;es中存放的是显存的第0页的起始地段地址
    
                mov di,0;di指向显存的偏移地址,确定指向下一个要处理的字符的位置
                mov al,cl;cl存放颜色参数,下边cl要用来临时存放要处理的字符
                mov ch,0;下边cx存放的是每次准备处理的字符
            s:
                mov cl,ds:[si];指向'welcome to masm ',0
    
                jcxz ok;cl为0时跳转
                mov es:[bx+di],cl;偶地址存放字符
                mov es:[bx+di+1],al;奇地址存放字符的颜色属性
                inc si
                add di,2;指向了下个字符
                jmp short s ;无条件跳转,jcxz是离开的关键跳
    
            ok:
                pop si
                pop cx
                ret;定义结束
    code ends
    end start
    
    

    test101

    • 实验10.2
        assume cs:code,ss:stack
    
        stack segment
        dw 8 dup(0)
        stack ends
    
        code segment
    
        start:
                mov ax,stack
                mov ss,ax
                mov sp,10h
                mov ax,4240h
                mov dx,0fh
                mov xx,0ah
    
                call divdw
    
                mov ax,4c00h
                int 21h
    
            divdw:
                push ax;低16位先保存
                mov ax,dx;ax这时是高16位了
                mov dx,0;为了不影响余数位和高位数
                div cx
                mov bx,ax
                pop ax
                div cx
                mov cx,dx
                mov dx,dx
                ret
        code ends
        end start
    • 实验10.3
    assume cs:code,ds:data
    
    data segment
        db 10 dup(0)
    data ends
    
    code segment
        start:
                mov ax,12666
                mov bx,data;指向字符串的首地址
                mov ds,bx
                mov si,0
    
                call dtoc;实现将word型整数转化为字符串并存储
    
                mov dh,8;打印初始化
                mov dl,3
                mov cl,0cah
    
                call show_str;开始打印字符串
    
                mov ax,4c00h
                int 21h
            dtoc:
                push dx
                push cx
                push ax
                push si
    
                mov bx,0;bx在子程序中用来存放位数,用栈来临时存放修改后的字符
            s1:
                mov cx,10d;d表示十进制,cx准备被除,用取余法来取出数字
                mov dx,0
    
                div cx;除以十
                mov cx,ax;得到的商复制给cx,要利用jcxz
                jcxz s2;当商为0则跳到s2
                add dx,30h;余数加上30h得到相应的ascii码
                push dx
                inc bx
    
                jmp short s1
            s2:
                add ax,30h;当商为0的时候,余数为个位
                push dx
    
                inc bx;再进行一次栈操作(补充当商为零而余数不为零时的情况)
                mov cx,bx;总共有bx位进栈,所以循环次数为bx
                mov si,0
            s3:
                pop ax;s3实现将栈中的数据依次出栈放到指定的内存中
                mov [si],al
                inc si
                loop s3
    
            okay:
                pop bx
                pop si
                pop ax
                pop dx
    
                ret
            show_str:;子程序
                push bx
                push cx
                push si
    
                mov al,0A0h;每行有80*2=160个字节=0a0h
                dec dh;行号在显存中下标从0开始,所以减1
                mul dh;相当于从第(n-1)*0a0h个byte单元开始
    
                mov bx,ax;定位好的位置偏移地址存放在bx里(行)
                mov al,2;每个字符占2个字节
                mul dl;定位列,结果ax存放的是定位好的列的位置
    
                sub ax,2;列号在显存中下标从0开始,又因为是偶字节存放字符,所以减2
                add bx,ax;此时bx中存放的是行与列的偏移地址
    
                mov ax,0B800h;显存开始的地方
                mov es,ax;es中存放的是显存的第0页的起始地段地址
    
                mov di,0;di指向显存的偏移地址,确定指向下一个要处理的字符的位置
                mov al,cl;cl存放颜色参数,下边cl要用来临时存放要处理的字符
                mov ch,0;下边cx存放的是每次准备处理的字符
            S:
                mov cl,ds:[si]
    
                jcxz ok
    
                mov es:[bx+di],cl
                mov es:[bx+di+i],al
    
                inc si
                add di,2
    
                jmp short s
    
            ok:
                pop si
                pop cx
                pop bx
                ret
    code ends
    end start

    十一、标志寄存器

    引言

    • CPU内部的寄存器中有一种特殊的寄存器:
      • 1、用来存储相关指令的某些执行结果;
      • 2、用来为CPU执行相关指令提供行为依据;
      • 3、用来控制CPU的相关工作方式。
    • 8086 CPU的标志寄存器只有16位,其中存储的信息通常被称为程序状态字(PSW)。
    • 本章中的标志寄存器(以下简称为flag)。某些指令将影响标志寄存器中的多个标志位,这些被影响的标记位比较全面地记录ill指令的执行结果,为相关的处理提供了所需的依据。
    • flag寄存器是按位起作用的,每一位都有专门的含义,记录特定的信息,与其他寄存器不一样。
    • 8086 CPU的flag寄存器的结构:

    flag

    • flag的1、3、5、12、13、14、15位在8086 CPU中没有使用,而0、2、4、6、7、8、9、10、11位都具有特殊的含义。

    11.1 ZF(zero flag)标志

    • flag的第6位是ZF,零标志位,它记录相关指令执行后,结果为0,ZF=1(记录下是0这样的肯定信息),结果不为0,ZF=0(表示结果非0)。
        mov ax,1
        sub ax,1
    
        mov ax,1
        and ax,0
        ;指令执行后,结果为0,则ZF=1
    
        mov ax,2
        sub ax,1
    
        mov ax,1
        or ax,0
        ;指令执行后,结果为1,则ZF=0
    • 在8086CPU中,add、sub、mul、div、inc、or、and等它们大多都是运算(逻辑运算或是算术运算)指令,是影响标志寄存器的,而mov、push、pop等传送指令对标志寄存器一般没有影响,因为不会产生结果。

    11.2 PF标志

    • flag的第2位是PF,奇偶标志位,记录指令执行后结果所有的二进制位中1的个数。为偶数,PF=1,为奇数PF=0
        mov al,1
        add al,10
        ;执行结果为00001011B,有3个1,则PF=0
    
        mov al,1
        or al,10
        ;执行后结果为00000011B,有2个1,则PF=1

    11.3 SF(sign flag)标志

    • flag的第7位是SF符号标志位,记录指令执行后结果为负则SF=1,结果为正,SF=0。弱国我们将数据当作无符号数来运算,SF的值没有意义,虽然相关的指令影响了它的值。
    • 有符号数与补码
      • 计算机默认把负数用补码记录。
      • 00000001B,可以看作无符号数1,也可以看作符号数+1;
      • 10000001B,可以看作无符号数129,也可以看作有符号数-127。
    • 补码
    mov al,10000001B
    add al,1
    ;执行指令后al的值是10000010B,无符号数130,有符号数-126

    检测点 11.1

    检测点11.1

    11.4 CF(carry flag)标志

    • flag的第0位是CF,进位标志位。一般情况下,在进行无符号运算的时候,它记录了运算结果的最高有效位向更高位的进位值或从更高位的借位值。对于位数为N的无符号数,其对应的二进制信息的最高位为N-1位的最高有效位,假想存在第N位。
      更高位

    • 两个8位的数据运算可能产生进位或者借位,由于这个进位值在8位数中无法保存,8086CPU就用flag的CF位来记录这个进位值。

        mov al.98h
        add al,al;执行后(al)=30h,cf=1,cf记录了从最高有效位向更高位的进位值
        add al,al;执行后(al)=60h,cf=0,cf记录了从更高有效位向更高位的进位值
    
        mov al,97h
        sub al,98h;执行后(al)=ffh,cf=1,cf记录了向更高位的借位值
        sub al,al;执行后(al)=0,cf=0,cf记录了向更高位的借位值

    11.5 OF(overflow flag)标志

    • 如果运算结果超出了机器所能表达的范围(对于8位有符号数,机器所能表达的范围是-128~127)将产生溢出,对有符号数而言。
    assume cs:code
    
        code segment
    
        start:
                mov al,01100010b
                add al,01100011b
    
                mov ax,4c00h
                int 21h
        code ends
    end start

    1150f_1

        assume cs:code
    
        code segment
    
        start:
                mov al,10001000b
                add al,11110000b
    
                mov ax,4c00h
                int 21h
        code ends
        end start

    115of2

    assume cs:code
    
        code segment
        start:
                mov al,98h
                add al,al
                add al,al
    
                mov ax,4c00h
                int 21h
        code ends
    end start
    

    CFdebug

    assume cs:code
    
        code segment
        start:
                mov al,97h
                sub al,98h
                add al,al
    
                mov ax,4c00h
                int 21h
        code ends
    end start
    

    114cf2

    • CF是对无符号数运算有意义的标志位,而OF是对有符号数运算有意义的标志位; CPU用CF位来记录无符号数运算是否产生了进位,用OF位来记录有符号数是否产生了溢出。用SF位来记录结果的符号
        mov al,98d
        add al,99d
        ;对于无符号数运算,98+99没有进位,CF=0
        ;对于有符号数运算,98+99发生溢出,OF=1

    检测点 11.2

    检测点11.2

    11.6 adc指令

    • adc是带有进位加法指令,利用了CF位上记录的进位值。格式:adc操作对象1,操作对象2,功能:操作对象1=操作对象1+操作对象2+CF。
        mov ax,2
        mov bx,1
        sub bx,ax
        adx ax,1
        ;执行后 (ax)=4,相当于计算(ax)+1+CF=2+1+1+4
    
        mov ax,1
        add ax,ax
        adc ax,3
        ;执行后(ax)=5,相当于执行(ax)+3+CF=2+3+0=5
    
        mov al,98H
        add al,al
        adx al,3
        ;执行后 (ax)=34H,相当于执行(ax)+3+CF=30H+3+1=34H
    • 由adc指令前面的指令决定在执行adc指令的时候加上的CF的值的含义,关键在于所加上的CF的值是被什么指令设置的。如果CF的值是被sub指令设置的,那么它的含义就是借位值;如果是被add指令设置的,那么它的含义就是进位值。加法运算先是低位相加,再高位相加加上低位相加产生的进位值。
    • 编程:计算1EF000H+201000H,结果存放在AX(高16位)和BX(低16位)中。
        mov ax,001EH
        mov bx,0F000H
        add bx,1000H
        adc ax,0020H
    • 编程:1EF0001000H+2010001EF0H,结果存放在AX(高16位)、BX(次16位)中和cx(低16位)。
        mov ax,001EH
        mov bx,0F000H
        mov cx,1000H
        add cx,1EF0H
        add bx,1000H
        adc ax,0020H
    • 编程:对两个128位数据进行相加
    assume cs:code,ds:data
        data segment
                db 16 dup(88H)
                db 16 dup(11H)
        data ends
    
        code segment
        start:
                mov ax,data
                mov ds,ax
                mov si,0
                mov di,16
    
                mov cx,8
                call add128
    
                mov ax,4C00H
                int 21H
        add128:
                push ax
                push cx
                push si
                push di
    
                sub ax,ax;将CF设置为0
            s:
                mov ax,[si]
                adc ax,[di]
                mov [si],ax
                inc si;不能用add si,2代替
                inc si;因为会影响cf位
                inc di;而loop和inc不会影响
                inc di
    
                loop s
    
                 pop di
                 pop si
                 pop cx
                 pop ax
                 ret
        code ends
    end start

    11.7 sbb指令

    • sbb是带借位减法指令,利用了CF位上记录的借位值。格式:sbb 操作对象1,操作对象2,功能是:操作对象1=操作对象1-操作对象2-CF。
    • 利用sbb指令我们可以对任意大的数据进行减法运算。sbb和adc是基于同样的思想设计的两条指令,在应用思路上sbb和adc类似。
    • 编程:计算003E1000H-00202000H,结果放在ax、bx中
        mov bx,1000H
        mov ax,003EH
        sbb bx,2000H
        sbb ax,0020H

    11.8 cmp指令

    • cmp是比较指令,功能上相当于减法指令,只是不保存结果。格式:cmp 操作对象1,操作对象2.功能:计算操作对象1-操作对象2但不保存结果,仅仅是根据计算结果对标志寄存器进行设置。
    • cmp指令运算执行后通过做减法将对标志寄存器产生影响,其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果。
        cmp ax,ax
        ;执行后结果为0,ZF=1,PF=1,SF=0,CF=0,OF=0
    
        mov ax,8
        mov bx,3
        cmp ax,bx
        ;执行后ax、bx的值不变,ZF=0,PF=1,SF=0,CF=0,OF=0
        cmp ax,bx

    118cmp_1

    118cmp2

    • CPU在执行cmp指令时也包含了对无符号数运算和进行有符号数运算,所以利用cmp指令可以对无符号数进行比较也可以对有符号数进行比较。
    • 单纯地考察SF的值不可能知道结果的正负。因为SF记录的只是可以在计算机中存放的相应位数的结果的正负(例如:add ah, al执行后,SF记录的是ah中的8位二进制信息所表示的数据的正负)。如果没有溢出发生的话,实际结果的正负和逻辑上真正结果的正负就一致了。。例如:22H(34)-0A0H(-96)=130=82H(是-126的补码),SF=1。
      • 1、如果SF=1或SF=0,OF=0,逻辑上真正结果的正负=实际结果的正负。
      • 2、如果SF=1或SF=0,OF=1,逻辑上真正结果的负正=实际结果的正负。

    11.9 检测比较结果的条件转移指令

    • 与cmp相配使用,根据cmp指令的比较结果(cmp指令执行后相关标志位的值)进行工作的指令。
    • cmp指令可以同时进行两种比较,无符号数比较和有符号数比较,所以根据cmp指令的比较结果进行转移的指令也分为两种:
      • 根据无符号数的比较结果进行转移的条件转移指令,它们检测ZF、CF的值;
      • 根据有符号数的比较结果进行转移的条件转移指令,它们检测SF、OF、ZF的值。
    • 它们所检测的标志位都是cmp指令进行无符号数比较时候记录比较结果的标志位。
    指令 含义 检测的相关标志位
    je 等于则转移 ZF=1
    jne 不等于则转移 ZF=0
    jb 低于则转移 CF=1
    jnb 不低于则转移 CF=0
    ja 高于则转移 CF=0 and ZF=0
    jna 不高于则转移 CF=1 or ZF=1


    j e ne b nb a na
    jump equal not equal below not below above not above
    • 编程:如果ah的值等于bh则ah的值等于ah的值加ah的值,否则ah的值等于ah的值加上bh的值。
            cmp ah,bh
            je s;ZF=1则跳转
            add ah,bh
            jmp short ok
        s:
            add ah,bh
        ok:ret
    • je检测的是ZF的位置,不管je前面是什么指令,只要CPU执行je指令时,ZF=1那么就发生转移。
            mov ax,0
            mov ax,0
            je s
            inc ax
        s:
            inc ax
        ;执行后ax的值等于1,add ax,0使得ZF=1,所以je指令将进行转移。
    • 课堂练习

    • 编程:统计data段中数值为8的字节的个数,用ax保存统计结果。

        ;方案一
    assume cs:code
    
        data segment
                db 8,11,8,1,8,5,63,38
        data ends
    
        code segment
        start:
                mov ax,data
                mov ds,ax
                mov bx,0;ds:bx指向第一个字节
                mov ax,0;初始化累加器
                mov cx,0
    
            s:
                cmp byte ptr [bx],8;和8进行比较
                jne next;如果不相等转到next,继续循环
                inc ax;如果相等就计数值加1
    
            next:
                inc bx
                loop s;执行后:(ax)=3
    
                mov ax,4c00h
                int 21h
        code ends
    end segment 
    ;方案二
    assume cs:code
    
        data segment
                db 8,11,8,1,8,5,63,38
        data ends
    
        code segment
        start:
                mov ax,data
                mov ds,ax
                mov bx,0;ds:bx指向第一个字节
                mov ax,0;初始化累加器
                mov cx,0
    
            s:
                cmp byte ptr [bx],8;和8进行比较
                je ok;如果不相等转到ok,继续循环
                jmp short next;如果不想等就转到next,继续循环
    
            ok:
                inc ax;如果相等就计数值加1
    
            next:
                inc bx
                loop s;执行后:(ax)=3
    
                mov ax,4c00h
                int 21h
        code ends
    end segment
    • 编程:统计data段中数值大于8的字节的个数,用ax保存统计结果。
    assume cs:code
    
        data segment
                db 8,11,8,1,8,5,63,38
        data ends
    
        code segment
        start:
                mov ax,data
                mov ds,ax
                mov bx,0;ds:bx指向第一个字节
                mov ax,0;初始化累加器
                mov cx,0
    
            s:
                cmp byte ptr [bx],8;和8进行比较
                jna next;如果大于8转到next,继续循环
                inc ax;如果大于就计数值加1
    
            next:
                inc bx
                loop s;执行后:(ax)=3
    
                mov ax,4c00h
                int 21h
        code ends
    end segment 

    检测点 11.3

    检测点11.3

    11.10 DF(direction flag)标志和串传送指令

    • flag的第10位是DF,方向标志位,在串处理指令中,控制每次操作后si(一般指向原始偏移地址)、di(一般指向目标偏移地址)的增减。
      • DF=0:每次操作后si、di递增;
      • DF=1,每次操作后so、di递减。
    • movsb(mov string byte)串传送指令,以字节为单位传送。将ds:si指向的内存单元中的字节送入es:di中,然后根据标志寄存器DF位的值将si和di递增1或递减1。movsw,以字为单位传送。将ds:si指向的内存单元中的字送入es:di中,然后根据标志寄存器DF位的值将si和di递增2或递减2。
    • movsb和movsw进行的是串传送操作中的一个步骤,一般和rep配合使用,格式:rep movsb,rep的作用是根据cx 的值,重复执行后面的串传送指令。由于每次执行一次movsb指令si和di都会递增或递减指向后一个单元或前个单元,则rep movsb就可以循环实现(cx)个字符的传送。
      • 1、传送的原始位置;
      • 2、传送的目的位置;
      • 3、传送的长度;
      • 4、传送的方向。
      • movsb功能:((es)*16+(di))=((ds)*16+(si)),如果DF=0,则(si)=(si)+1,(di)=(di)+1;如果DF=1,则(si)=(si)-1,(di)=(di)-1。
    • 由于flag的DF位决定着串传送指令执行后,si和di改变的方向,8086CPU提供两条指令对DF位进行设置:
      • cld指令:将标志寄存器的DF位设置为0;
      • std指令:将标志寄存器的DF位设置为1。

    11.11 pushf和popf

    • pushf的功能 是件标志寄存器的值压栈,popf是从栈中弹出数据m,送入标志寄存器中。pushf和popf为直接访问标志寄存器提供了一种方法。
        ;下面的程序执行后ax的值是多少?
        mov ax,0
        push ax
        popf
        mov ax,0fff0h
        add ax,0010h
        pushf
        pop ax 
        and al,11000101b
        and ah 00001000b
    • 编程:用串传送指令将data段总的第一个字符串复制到它后面的空间中。
    assume cs:code
    
        data segment
                db'welcome to masm!'
                db 16 dup(0)
        data ends
    
        code segment
        start:
                mov ax,data
                mov ds,ax
                mov si,0;指向data:0
                mov es,ax
                mov di,16;指向data:16
                mov cx,16;rep循环16次
    
                cld;设置DF=0,正向传送
                rep movsb
    
                mov ax,4c00h
                int 21h
        code ends
    end start

    1110

    • 用串传送指令将F00H段中的最后16个字符复制到data段中
    assume cs:code
    
        data segment
                db 16 dup(0)
        data ends
    
        code segment
        start:
                mov ax,0f00h
                mov ds,ax
                mov si,0ffffh;指向f0000:ffff
                mov ax,data
                mov es,ax
                mov di,16;指向data:15
                mov cx,16;rep循环16次
    
                std;设置DF=1,逆向传送
                rep movsb
    
                mov ax,4c00h
                int 21h
        code ends
    end start

    检测点 11.4

    检测点11.4

    11.12 标志寄存器在Debug中的表示

    debugflag

    标志 值为1的标记 值为0的标记
    OF OV NV
    SF NG PL
    ZF ZR NZ
    PF PE PO
    CF CY NC
    DF DN UP

    十二、内中断

    引言

    • 中断时CPU处理外部突发事件的一个重要技术。它能使CPU在运行过程中对外部事件发出的中断请求几时进行处理,处理完成后又立即返回断电,基础进行CPU原来的工作。引起中断的原因或是说发出中断请求的来源叫做中断源。根据中断源的不同,可以把中断分为硬件中断和软件中断两大类,而硬件中断又可以分为外部中断和内部中断两类。
    • 外部中断一般是指由计算器外部设备发出的中断请求。如:键盘中断、打印机中断、定时器中断等。外部中断时可以屏蔽的中断,业绩是说利用中断控制器可以屏蔽这些外部设备的中断请求。
    • 内部中断是指因硬件出错(如突然掉电)或运算出错(如除数为0、单步中断)所引起的中断。内部中断是不可屏蔽的。
    • 软件中断其实并不是真正的中断,它们只是可被调用执行的一般程序以及DOS的系统功能调用(int 21)等都是软件中断。
    • 中断的优先权:
      • 1、除法出错、溢出中断、软件中断;
      • 2、不可屏蔽中断;
      • 3、可屏蔽中断;
      • 4、单步中断。
    • 中断信息中包含有标识中断源的类型码。根据CPU的设计,中断源类型码的作用就是用来定位中断处理程序。

    12.1 内中断的产生

    • 8086CPU内部有以下情况发生时将产生相应的中断信息:
      • 1、除法错误;
      • 2、单步执行;
      • 3、执行into指令;
      • 4、执行int指令。
    • 8086CPU中的中断类型码如下:
      • 1、除法错误:0
      • 2、单步执行:1
      • 3、执行into指令:4
      • 4、执行int指令,该指令格式为int n,n为立即数是提供给CPU的中断类型码。

    12.2 中断处理程序

    • CPU在收到中断信息后立即去执行该中断信息的处理程序。

    12.3 中断向量表

    • 中断向量列表就是中断向量(中断处理程序的入口地址)的列表,其在内存中保存,存放着256个中断源说对应的中断处理程序的入口。8086PC机中断向量表放在内存地址0处。从内存0000:0000到0000:03FF的1024(一个物理地址是由段地址和偏移地址构成,要用4个字节来存放)个单元中存放着中断向量表。
    • CPU用8位的中断类型码通过中断向量表找到相应的中断处理程序的入口地址。中断向量表中存放的就是各个类型的处理程序的地址,8位的类型码是个索引。

    12.4 中断过程

    • 用中断码在中断向量表中找到中断处理程序的入口地址,用它来设置CS和IP,使CPU执行中断程序。用中断类型码找到中断向量并用它设置CS和IP,这个工作室由CPU的硬件自动完成的,这个工作的过程被称为中断过程。
    • 8086CPU的中断过程:
      • 1、从中断信息中取得中断类型码;
      • 2、标志寄存器的值入栈,以保护标志位;
      • 3、设置标志寄存器的第8位TF和第9位IF的值为0;
      • 4、CS的内容入栈,IP的内容入栈;
      • 5、从内存地址为中断类型码* 4和中断类型码 *4+2的两个字单元中读取中断处理程序的入口地址设置IP和CS。
      • 在最后一步完成后,CPU开始执行由程序员编写的中断处理程序。

    12.5 中断处理程序和iret指令

    • 常规的步骤
      • 1、保存用到的寄存器;
      • 2、处理中断;
      • 3、 恢复用到的寄存器;
      • 4、 用iret指令返回。
    • iret指令的功能为相应的按顺序恢复之前保存起来的IP、CS地址和标志位寄存器。用汇编语法描述为:
    pop IP
    pop CS
    popf

    12.6 除法错误中断的处理

    • 当CPU执行dvi等处罚指令的时候,如果发生了除法溢出错误,将产生中断类型码为0的中断信息,CPU将检测到这个信息然后引发中断过程,转去执行0号中断所对应的中断处理程序。
    assume cs:codesg
    
        codesg segment
        start:
                mov ax,1000h
                mov bh,1    
                div bh
        codesg ends
    
    end start

    126
    debug 126

    12.7 编程处理0号中断

    • 改变0号中断处理程序的功能,在屏幕中间显示字然后再返回操作系统。
      • 当发生除法溢出时产生0号中断信息,引发中断过程。
        • 此时CPU将进行以下工作(中断过程)
        • 当中断0发生时,CPU将转去执行中断处理程序。
        • 先进行相关处理,然后向显示缓冲区送字符串,最后返回。
      • 改变后的中断处理程序应该放在内存中,因为除法溢出随时可能发生,CPU随时都可能将CS:IP指向改变后的中断处理程序的入口执行程序。
      • 把程序存入内存,修改向量表(即将内存地址登记在中断向量表的对应表项中),中断时调用这个内存。
        除法溢出对应的中断类型码为0,它的中断处理程序的入口地址应该从0* 4+2地址单元开始存放,段地址存放在0* 4+2字单元中,偏移地址存放在0*4字单元中。也就是改变后的中断处理程序的段地址0存放在0000:0002字单元中,偏移地址200H存放在0000:0000字单元中。如果要显示的字符串在程序的data段中,那么程序执行完成后返回,它所占用的内存空间被系统释放,在其中存放的信息也可能被别的信息覆盖。
    assume cs:code
    
              code segment
              start:
                      mov ax,cs
                      mov ds,ax
                      mov si,offset do0;设置ds:di指向源地址
                      mov ax,0
                      mov es,ax
                      mov di,200h;设置es:si指向目的地址
                      mov cx,offset do0end - offset do0;设置cx为传输长度,编译器可以识别加减乘除运算符
                      cld;设置传输方向为正
                      rep movsb
    
                      mov ax,0;设置中断向量表
                      mov es,ax
                      mov word ptr es:[0*4],200h
                      mov word ptr es:[0*4+2],0
    
                      mov ax,4c00h
                      int 21h
                  do0:
                      jmp short do0start
                      db"welcome to masm!";在代码段中存储数据
                  do0start:
                      mov ax,cs
                      mov ds,ax
                      mov si,202h;jmp short do0start这条指令栈两个字节
                      ;显示字符串,设置es:di指向字符串
                      mov ax,0b800h;显存空间,直接显示在显示器上
                      mov es,ax
                      mov di,12*160+36*2;这只es:di指向显存空间的中间位置
                      mov cx,16;设置cx为字符串(welcome to masm!)长度
                  s:
                      mov al,[si]
                      mov es:[di],al
                      inc si
                      add di,1
                      mov al,02h
                      mov es:[di],al
                      add di,1
                      loop s
    
                      mov ax,4c00h
                      int 21h
                  do0end:
                      nop
    
              code ends
    end start
    ![do0](https://img-blog.csdn.net/20170522081857031?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvR2liYnNfcA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) ![do02](https://img-blog.csdn.net/20170522081929203?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvR2liYnNfcA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)

    12.8 单步中断

    • CPU执行完一条指令之后,如果检测到标志寄存器的TF位为1,则产生单步中断引发中断过程。单步中断的中断类型码为1,它所引发的中断过程如下:
      • 1、取得中断类型码;
      • 2、标志寄存器入栈,TF、IF设置为0;
      • 3、CS、IP入栈;
      • 4、指向指定类型码的中断向量表。

    12.9 响应中断的特殊情况

    • 在有些情况下CPU在执行完当前指令后,即便是发生了中断也不会响应。

      在执行完向ss寄存器传送数据的指令后,即便检测到了中断信号CPU也不会响应。因为ss:sp指向栈顶,对他们的设置应该连续完成。如果在执行完设置ss指令后mCPU响应中断引发中断过程,要在栈中压入标志寄存器、CS和IP的值。而ss改变,sp并未改变则ss:sp指向不是正确的栈顶将引发错误。

    • 我们要将栈顶设置为1000:0,不应该隔开

    应该 不应该
    mov ax,1000h mov ax,1000h
    mov ss,ax mov ss,ax
    mov sp,0 mov ax,0
    mov ax,0 mov sp,0

    十三、int 指令

    引言

    • 在第12章中了解中断过程和除法错误中断和单步中断的处理,这章了解int指令。

    13.1 int 指令

    • int格式:int n,n为中断类型码,它的功能是引发中断过程。CPU执行int n之力量能够,相当引发一个n号的中断过程,可以在程序中使用int指令调用任何一个中断的中断处理程序。执行过程如下:

      • 中断过程从,此处去执行n号中断的中断处理程序。

        assume cs:code
        
            code segment
        
            start:
                    mov ax,0b800h
                    mov es,ax
                    mov byte ptr es:[12*160+40*2],'!'
        
                    int 0;执行int 0指令,引发中断过程,执行0号中断处理程序
            code ends
         end start

        131dosbox

    • int指令的最终功能和call类似,都是调用一段程序。一般情况下系统将一些具有一定功能的子程序以中断处理程序的方式提供给应用程序调用,也可以自己编写一些中断处理程序供别人使用。

    13.2 编写供应用程序调用的中断例程

    • 中断处理程序简称为中断例程。
    • 实例1:编写、安装中断7ch的中断例程实现求一word型数据的平方。
      • 1、编程实现求平方功能的程序;
      • 2、安装程序在0:200处;
      • 3、设置中断向量表将程序的入口地址保存在7ch表项中,使其成为中断7ch的中断例程。
                 ;计算
    ssume cs:code
                 code segment
                 start:
                         mov ax,3456
                         int 7ch
                         add ax,ax
                         adc ax,dx
                         mov ax,4c00h
                         int 21h
                 code ends
                 end start
    
    
    
                 ;安装程序
                 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 ws:[7ch*4+2],0
                         mov ax,4c00h
                         int 21h
                     sqr:
                         mul ax
                         iret
                     sqrend:
                         nop
                 code ends
    end start
    • CPU执行int 7ch指令进入中断例程之前,标志寄存器、当前的CS和IP都被压入栈中,在执行完中断例程后,用iret指令恢复int 7ch执行前的标志寄存器和CS和IP的值,从而接着执行应用程序。
    • int指令和iret指令配合使用与call指令和ret指令配合使用具有相似的思路。
    • 实例2:编写、安装中断7ch的中断例程,实现将一个全是字母,以0为结尾的字符串转化为大写。
    assume cs:code
                 data segment
                         db'conversation',0
                 data ends
    
                 code segment
                 start:
                         mov ax,data
                         mov ds,ax
                         mov si,0
                         int 7ch
                         mov ax,4c00h
                         int 21h
                 code ends 
                 end start
    
    
    
    
    
    
                 assume cs:code
                 code segment
    
                 start:
                         mov ax,cs
                         mov ds,ax
                         mov si,offset capital
                         mov ax,0
                         mov es,ax
                         mov di 200h
                         mov cx,offset capitalend - offset capital
                         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
    
                     capital:
                         push cx
                         push si
                     change:
                         mov cl,[si]
                         mov ch,0
                         jcxz ok
                         and byte ptr [si],11011111b
                         inc si
                         jmp short change
                     ok:
                         pop si
                         pop cx
                         iret
    
                     capitalend:
                         nop
                 code ends
    end start
    • 要注意用到的寄存器冲突。

    13.3 对int、iret和栈的深入理解

    • 中断处理程序和iret指令
    • 编程:用7ch中断例程完成loop指令的功能,在屏幕中间显示80个”!”.

      loop指令需要循环次数和到标号的位移。为了模拟loop指令7ch中断例程应具备下面dec cx和如果cx的值不等于0则转移到标号s处。

      • int 7ch引发中断过程后,进入7ch中断例程在中断过程中当前的标志寄存器、CS和IP都要压栈。此时压入的CS和IP中的内容分别是调用程序的段地址(可以认为是标号s的段地址)和int 7ch后一条指令的偏移地址(即标号se的偏移地址)。使用iret指令用栈中的内容设置CS、IP,从而实现转移到标号s处。
      assume cs:code
      
          code segment
          start:
                  mov ax,0b800h;显存地址
                  mov es,ax
                  mov di,160*12
                  mov bx,offset s- offset se;设置从标号s的转移位移
                  mov cx,80
              s:
                  mov byte ptr es:[di],'!'
                  add di,2
                  int 7ch;如果cx的值不等于0则转移到标号s处
              se:
                  nop
      
                  mov ax,4c00h
                  int 21h
      code ends
      end start
      
      
      
      
          ;7ch中断例程
              lp:
                  push bp
                  mov bp,sp;
                  dec cx
                  jcxz lpret
                  add [bp+2],bx
              lpret:
                  pop bp
                  iret

    13.4 BIOD和DOS所提供的中断例程

    • bios中主要包含以下几部分内容:
      • 1、硬件操作系统的检测和初始化程序;
      • 2、外部中断和内部中断的中断例程;
      • 3、用于对硬件设备进行I\O操作的中断例程;
      • 4、其他和硬件系统相关的中断例程。
    • bios和dos在所提供的中断例程中包含了许多子程序,可以用int指令直接调用。和硬件设备相关的dos中断例程中一般都调用了bios的中断例程

    13.5 bios和dos中断例程的安装过程

    • 1、开机后8086CPU一加电初始化CS和IP,自动执行FFFF:0处指令,转去执行bios中的硬件系统检测和初始化程序。
    • 2、初始化程序将建立bios所支持的中断向量,即将biso提供的中断例程的入口地址登记在中断向量表中。
    • 3、硬件系统检测和初始化完成后,调用int 19h进行操作系统的引导。从此将计算机交由操作系统控制。
    • 4、dos启动后除完成其他工作外,还将它所提供的中断例程装入内存并建立相应的中断向量。

    13.6 bios中断例程应用

    • bios和dos提供的中断例程douyongah来传递内部子程序的编号。
    • int 10h中断例程是bios提供的中断例程,其中包含了多个和屏幕输出相关的子程序。int 10h中断例程的设置光标位置功能:
        mov ah,2;表示调用10h号中断例程的2号子程序,功能为设置光标位置
        mov bh,0;页号
        mov dh,5;行号
        mov dl 12;列号
        int 10h;
    ;功能为在光标位置显示字符功能
    mov ah,9;置光标,调用9号子程序
    mov al,'a';字符
    mov bl,7;颜色属性,和在显存中的属性字节的格式相同
    mov bh,0;第0页
    mov cx,3;字符重复个数
    int 10h
    • 编程:在屏幕的第5行12列显示3个红底高亮闪烁绿色的’a’
        assume cs:code
    
        code segment
                mov ah,2;设置光标
                mov bh,0;第0页
                mov dh,5;dh中放行号
                mov dl,12;dl中放列号
                int 10
    
                mov ah,9;设置光标
                mov al,'a';字符
                mov bl,11001010b;颜色属性
                mov bh,0;第0页
                mov cx,3;字符重复个数
                int 10h
    
                mov ax,4c00h
                int 21h
        code ends
        end

    136

    13.7 dos中断例程应用

    • int 21h 中断例程
    mov ah,4ch;程序返回
    mov al,0;返回值0是正常返回
    ;合起来写就是 mov ax,4c00h
    int 21h
    • int 2h中断例程还具有在光标位置显示字符串的功能

      ds:dx;要显示的字符串需要用 $ 作为结束符
      mov ah,9;功能号9,表示在光标位置显示字符串
      int 21h

    十四、端口

    引言

    • CPU可以直接读写3个地方的数据:
      • 1、CPU内部的寄存器;
      • 2、内存单元;
      • 3、 端口。

    14.1 端口的读写

    • mov、push、pop等死内存读写指令。in和out是端口读指令写指令时in是从端口读取数据,out是往端口写入数据。in和out指令只能用ax或al来存放从端口中读入的数据或要发送到端口中的数据,访问8位短空时用al,访问16位端口时用ax。

      • 访问内存:
        • mov ax,ds:[8+0];假设(ds)=0
        • 执行时,与总线相关的操作:
          • 1、CPU通过地址线信息8发出;
          • 2、CPU通过控制线发出内存读命令,选中存储器芯片并通知它将要从中读取数据;
          • 3、 存储器将8号单元中的数据通过数据线送入CPU。
      • 访问端口:
        • in al,60h;从60h号端口读入一个字节。
        • 执行时与总线相关的操作:
          • 1、CPU通过地址线将地址信息60h发出;
          • 2、CPU通过控制线发出端口读命令,选中端口所在的芯片,并通知它,将要从中读取数据;
          • 3、端口所在的芯片将60h端口中的数据通过数据线送入CPU。
                      ;对0~255以内的端口进行读写
                      in al,20h;从20h端口读入一个字节
                      out 20h,al;往20h端口写入一个字节
            
            
                      ;对256~65535的端口进行读写时,端口放在dx中
                      mov dx,3f8h;将端口号3f8h送入dx
                      in al,dx;从3f8端口读入一个字节
                      out 3f8h,al;往3f8h端口写入一个字节

    14.2 CMOS RAM芯片

    • CMOA RAM特征:

      • 1、包含一个实时钟和一个有128个存储单元的RAM存储器。(早期的计算机位64个字节)。
      • 2、该芯片靠电池供电。因此关机后其内部的实时钟仍可正常工作,RAM中的信息不会丢失。
      • 3、128个字节的RAM,内部实时钟占用0~0dh单元来保存时间信息默契与大部分单元用于保存系统配置信息,供系统启动时bios程序读取。
      • bios也提供了相关的程序使用户在开机时配置CMOS RAM中的系统信息。

      • 4、该芯片内部有两个端口,端口地址为70h和71
        h。CPU通过这两个端口读写CMOS RAM。

      • 5、70h为地址端口,存放要访问的CMOS RAM单元的地址;71h为数据端口,存放从选定的CMOS RAM单元中读取的数据,或要写入到其中的数据。
    • CPU对CMOS RAM的读写分两步进行,以读2号单元为例:
      • 1、将2送入端口70h;
      • 2、从71h读出2号单元的内容。

    14.3 shl和shr指令

    • shl为逻辑左移指令功能为:
      • 1、将一个寄存器或内存单元中的数据向左移位;
      • 2、将最后移出的一位写入CF中;
      • 3、最低位用0补充。
              mov al,01001000b
              shl al,1;将al中的数据左移一位
              ;执行后al的值是10010000b,CF=0
    • 如果移动位数大于1时,必须将移动位数放在cl中。
              mov al,01010001b
              mov cl,3
              shl al,cl
              ;执行后al的值为10001000b,cf=0
    • 二进制逻辑左移一位,相当于执行x=x*2(2是进制位)
    mov al,00000001b 执行后al的值等于00000001b=1
    shl al,1 执行后al的值等于00000010b=2
    shl al,1 执行后al的值等于00000100b=4
    shl al,1 执行后al的值等于00001000b=8
    mov cl,3
    shl al,cl 执行后al的值等于01000000b=64
    • shr为逻辑左移指令功能为:
      • 1、将一个寄存器或内存单元中的数据向右移位;
      • 2、将最后移出的一位写入CF中;
      • 3、最高位用0补充。
    • 二进制逻辑右移一位,相当于执行x=x/2(2是进制位)

    14.4 CMOS RAM中存储的时间信息

    • 在CMOS RAM中以每个信息一字节存放着当前的时间信息:年09h,月08h,日07h,时04h,分02h,秒00h。这些数据以BCD码的方式存放,BCD码以4位为一位。

    • 数值26BCD码表示为0010 0110,用两个BCD码表示两位十进制,高4位表示十位,低4位表示各位。

    • 编程:在屏幕中间显示当前的月份。
        assume cs:code
    
        code segment
        start:
                ;向地址端口70h写入要访问的单元地址,读取CMOS RAM的信息
                mov al,8
                out 70h,al
                in al,71h;从数据端口中取得指定单元中的数据
                mov ah,al;al中为从CMOS RAM的8号端口读出数据
                mov cl,4
                shr ah,cl;ah中为月份的十位数码值
                and al,00001111b;ah中为月份的个位数值码
                add ah,30h;BCD码值+30h(字符'0')=十进制对应的ASCII码
                add al,30h
                ;用BCD码表示的月份以十进制的形式显示到屏幕上。
                mov bx,0b800h;显存
                mov es,bx
                mov byte ptr es:[160*12+40*2],ah;显示月份的十位数码
                mov byte ptr es:[160*12+40*2+2],al;显示月份的个位数码
    
                mov ax,4c00h
                int 21h
    
        code ends
        end start

    144_1

    十五、外中断

    15.1接口芯片和端口

    • CPU通过端口和外设进行联系。在PC系统的接口卡和主板上,装有各种接口芯片。这些外设接口芯片的内部有若干寄存器,CCPU将这些寄存器当作端口来访问。外设的输入不直接送入内存和CPU,而是送入相关的接口芯片的端口中;CPU向外设的输出也不是直接送入到外设而是先送入端口再由相关的芯片送到外设。

    15.2外中断信息

    • 外中断源有两类:
      • 1、可屏蔽中断;
        • 可屏蔽中断时CPU可以不响应的外中断。CPU是否响应可屏蔽中断要看标志寄存器的IF位的设置。
      • 2、不可屏蔽中断
    • 当CPU检测到可屏蔽中断信息时:
      • 如果IF=1,则CPU在执行完当前指令后响应中断引发中断过程。
      • 如果IF=0,着不响应可屏蔽中断。
      • 内中断过程
    • > 可屏蔽中断所引发的中断过程,除在第一步的实现上有所不同外,基本上和内中断的中断过程相同。因为可屏蔽中断信息来自于CPU外部,中断类型码是通过数据总线送入CPU的;而内中断的中断类型码是在CPU内部产生的。在中断过程中将IF置0的原因是在进入中断处理程序后禁止其他的可屏蔽中断。
    • 8086CPU提供的设置IF的指令如下:
      • sti,设置IF=1;
      • cli,设置if=0.
    • 不可屏蔽中断是CPU必须响应的外中断。当CPU检测到不可屏蔽中断信息时,则在执行完当前指令后立即响应引发中断过程。对于8086CPU不可屏蔽的中断类型码固定为2。所以中断过程中不需要取中断类型码。几乎所有外设引发的外中断都是可屏蔽中断。
    • 不可屏蔽中断过程:
      • 1、标志寄存器入栈,IF=0,TF=0’
      • 2、CS和IP入栈;
      • 3、(IP)=(8),(CS)=(0AH)

    15.3PC机及键盘的处理过程

    • 键盘输入的处理过程:
      • 1、键盘输入产生扫描码;
      • 2、扫描码送入60h端口;
      • 3、引发9号中断;
      • 4、执行int 9中断例程。
      • > 前三步由硬件系统自动完成,第四步用户可以修改int 9中断程序。
    • 按下一个键产生的扫描码称为通码,松开一个键产生的扫描码称为断码。扫描码被送入主板上的相关接口芯片端口地址为60h的寄存器中。
    • 扫描码长度为一个字节,通码的第7位为0,断码的第7位为1。即断码=通码+80h。

    键盘上部分键的扫描码

    • bios提供了int 9中断例程,用来进行基本键盘输入处理,主要的工作如下:
      • 1、读出60h端口中的扫描码;
      • 2、如果是字符键的扫描码就将它和它所对应的字符码(ASCII码)送入内存中的bios键盘缓冲区;
        • 键盘的输入到达60h端口时相关的芯片就会向CPU发出中断类型码为9的可屏蔽中断信息。
        • CPU检测到该中断信息后,如果IF=1,则相应中断,引发中断过程,转去执行int 9中断例程。
        • 如果是控制键(如ctrl)和切换键(如capslock)的扫描码,则将其转变为状态字节(用为进制位记录控制键和切换键状态的字节)写入内存中存储状态字节的单元
      • 3、键盘系统进行相关的控制。如向相关芯片发出应答信息。
    • bios键盘缓冲区是系统启动后mbios用于存放int 9中断例程所接收的键盘输入的内存区。该内存可以存储15个键盘输入,在bios键盘缓冲区中一个键盘输入用一个字单元存放,高位字节存放扫描码,低位字节存放字符码。0040:17单元存储键盘状态字节该字节记录了控制键和切换键的状态
      • 0:置1表表示按下右shift键
      • 1:置1表表示按下左shift键
      • 2:置1表表示按下ctrl
      • 3:置1表表示按下alt
      • 4:置1表表示按下scroll指示灯亮
      • 5:置1表表示按下numlock,小键盘输入的是数字
      • 6:置1表表示按下capslock,输入大写字母
      • 7:置1表表示按下insert。处于删除状态

    15.4编写int 9中断

    • 键盘输入的处理过程
    • 编程:在屏幕中间依次显示让人看清的a~z,按下esc键后改变显示的颜色。
        ;显示字符
    code segment
        start:
                mov ax,0b800h
                mov es,ax
                mov ah,'a'
            s:
                mov es:[160*12+40*2],ah
                inc ax
                cmp ah,'z'
                jna s
                mov ax,4c00h
                int 21h
        code ends
    end start
        ;延迟显示字符
    
    assume cs:code
        stack segment
                db 128 dup(0)
        stack ends
    
        code segment
        start:
                mov ax,stack
                mov ss,ax
                mov sp,128
                mov ax,0b800h
                mov es,ax
                mov ah,'a'
            s:
                mov es:[160*12+40*2],ah
                call delay
                inc ah
                cmp ah,'z'
                jna s
    
                mov ax,4c00h
                int 21h
    
            delay:
                push ax
                push dx
                mov dx,10h;循环100次,延迟的时间和CPU的计算能力成反比
                mov ax,0
            s1:
                sub ax,1
                sbb dx,0
                cmp ax,0
                jne s1
                cmp dx,0
                jne s1
                pop dx
                pop ax
                ret
        code ends
    end start
        ;实现IF=0,TF=0步骤
        pushf
        pop ax
        and ah,11111100b
        push ax
        popf
    • int指令在执行时CPU进行的工作
    • 完整程序
    assume cs:code
    
        stack segment
                db 128 dup(0)
        stack ends
    
        data segment
                dw 0,0
        data ends
    
        code segment
        start:
                mov ax,stack
                mov ss,ax
                mov sp,128
                mov ax,data
                mov ds,ax
                mov ax,0
                mov es,ax
    
                push es:[9*4]
                pop ds:[0]
                push es:[9*4+2]
                pop ds:[2];将原来的int9中断例程的入口地址保存
    
                mov word ptr es:[9*4+2],offset int9
                mov es:[9*4+2],cs;在中断向量表中设置新的int 9中断例程的入口地址
    
                mov ax,0b800h
                mov es,ax
                mov ah,'a'
    
            s:
                mov es:[160*12+40*2],ah
                call delay
                inc ah
                cmp ah,'z'
                jna s
                mov ax,0
                mov es,ax
    
                push ds:[0]
                pop es:[9*4]
                push ds:[2]
                pop es:[9*4+2];将中断向量表中int9中断例程的入口恢复为原来的地址
    
                mov ax,4c00h
                int 21h
    
            delay:
                push ax
                push dx
                mov dx,10h;循环100次,延迟的时间和CPU的计算能力成反比
                mov ax,0
            s1:
                sub ax,1
                sbb dx,0
                cmp ax,0
                jne s1
                cmp dx,0
                jne s1
                pop dx
                pop ax
                ret
    
                ;新的int 9中断例程
    
            int9:
                push ax
                push bx
                push es
    
                in al,60h
    
                pushf
                pushf
                pop bx
                and bh,11111100b
                push bx
                popf
                call dword ptr ds:[0];对int指令进行模拟,调用原来的int9中断例程
    
                cmp al,1;esc键盘扫描码
                jne int9ret
    
                mov ax,0b800h
                mov es,ax
                inc byte ptr es:[160*12+40*2+1];改变颜色
    
            int9ret:
                pop es
                pop bx
                pop ax
                iret
    
        code ends
    end start
    

    15.5安装新的int 9中断例程

    • 小甲鱼版(笔者未成功运行)
    assume cs:code
    
        stack segment
                db 128 dup(0)
        stack ends
    
        data segment
                dw 0,0
        data ends
    
        code segment
        start:
                mov ax,stack
                mov ss,ax
                mov sp,128
                mov ax,data
                mov ds,ax
                mov ax,0
                mov es,ax
    
                push es:[9*4]
                pop ds:[0]
                push es:[9*4+2]
                pop ds:[2];将原来的int9中断例程的入口地址保存
    
                mov word ptr es:[9*4+2],offset int9
                mov es:[9*4+2],cs;在中断向量表中设置新的int 9中断例程的入口地址
    
                mov ax,0b800h
                mov es,ax
                mov ah,'a'
    
            s:
                mov es:[160*12+40*2],ah
                call delay
                inc ah
                cmp ah,'z'
                jna s
                mov ax,0
                mov es,ax
    
                push ds:[0]
                pop es:[9*4]
                push ds:[2]
                pop es:[9*4+2];将中断向量表中int9中断例程的入口恢复为原来的地址
    
                mov ax,4c00h
                int 21h
    
            delay:
                push ax
                push dx
                mov dx,10000h;循环100次,延迟的时间和CPU的计算能力成反比
                mov ax,0
            s1:
                sub ax,1
                sbb dx,0
                cmp ax,0
                jne s1
                cmp dx,0
                jne s1
                pop dx
                pop ax
                ret
    
                ;新的int 9中断例程
    
            int9:
                push ax
                push bx
                push es
    
                in al,60h
    
                pushf
                pushf
                pop bx
                and bh,11111100b
                push bx
                popf
                call dword ptr ds:[0];对int指令进行模拟,调用原来的int9中断例程
    
                cmp al,1;esc键盘扫描码
                jne int9ret
    
                mov ax,0b800h
                mov es,ax
                inc byte ptr es:[160*12+40*2+1];改变颜色
    
            int9ret:
                pop es
                pop bx
                pop ax
                iret
    
        code ends
    end start
    • 王爽原版(笔者未成功运行)
    assume cs:code
    
        stack segment
                db 128 dup(0)
        stack ends
    
        code segment
        start:
                mov ax,stack
                mov ss,ax
                mov sp,128
    
                push cs
                pop ds
    
                mov ax,0
                mov es,ax
    
                mov si,offset int9;设置ds:si指向源地址
                mov di,204h;设置es:di指向目的地址
                mov cx,offset int9end - offset int9;设置cx为传输长度
                cld;设置传输方向
                rep movsb
    
                push es:[9*4]
                pop es:[200h]
                push es:[9*4+2]
                pop es:[202h]
    
                cli
                mov word ptr es:[9*4],204h
                mov word ptr es:[9*4+2],0
                sti
    
                mov ax,4c00h
                int 21h
    
            int9:
                push ax
                push bx
                push cx
                push es
    
                in al,60h
    
                pushf
                call dword ptr cs:[200h];当此中断例程执行时(CS)=0
    
                cmp al,3bh;f1的扫描码
                jne int9ret
    
                mov ax,0b800h
                mov es,ax
                mov bx,1
                mov cx,2000
    
            s:
                inc byte ptr es:[bx]
                add bx,2
                loop s
    
            int9ret:
                pop es
                pop cx
                pop bx
                pop ax
                iret
    
            int9end:
                nop
    
        code ends
    end start

    第16章 直接定址表

    16.1 描述单元长度的标号

    assume cs:code
    cod segment
        a:db 1,2,3,4,5,6,7,8
        b:dw 0
    
    start:
            mov si,offset a
            mov bx,offset b
            mov cx,8
        s:
            mov al,cs:[si]
            mov ah,0
            add cs:[bx],ax
            inc si
    
        loop s
            mov ax,4c00h
            int 21h
    code ends
    end start
    ;代码中的 s、start等都是标号,表示了内存的地址

    在code段中使用的标号a,b后面没有:,因此他们可以同时描述内存地址和单元长度的标号

    assume cs:code
    cod segment
        a db 1,2,3,4,5,6,7,8 ;描述了地址code:0,和从这个地址开始以后的内存单元都是直接单元
        b dw 0 ;则b是code[8]
    
    start:
            mov si,0
            mov cx,8
        s:
            mov al,a[si] ;相当于mov al,cs:0[si]
            mov ah,0
            add b,ax
            inc si
    
        loop s
            mov ax,4c00h
            int 21h
    code ends
    end start
    

    检测点 16.1

    检测点16.1

    16.2 在其他段中使用数据标号

    • 注意:在后面加有:的地址标号只能在代码段中使用,不能在其他段中使用。

      assume cs:code,ds:data
      cod segment
      a:db 1,2,3,4,5,6,7,8
      b:dw 0
      data ends
      
      start:
          mov ax,data
          mov ds,ax
          mov si,0
      s:
          mov al,a[si]
          mov ah,0
          add b,ax
          inc si
      
      loop s
          mov ax,4c00h
          int 21h
      code ends
      end start
    • 如果现在代码段中直接用数据标号访问数据,则需要用伪指令assume将标号所在的段和一个段寄存器联系起来。 我们可以将标号当作数据来定义,此时编译器将标号所表示的地址当作数据的值。

    data segment
        a db 1,2,3,4,5,6,7,8
        b dw 0
        c dw a,b
        ;相当于 c dw offset a,offset b
    data ends
    
    data segment
        a db 1,2,3,4,5,6,7,8
        b dw 0
        c dd a,b
        ;相当于 c dw offset a,seg a,offset b,seg b
        ;seg操作符,功能是取得某一标号的段地址
    data ends
    

    16.3 直接定址表

    • 利用表,在两个数据集合之间建立一种映射关系,使我们可以利用查表的方法根据给出的数据得到其在另一集合中对应数据

      • 目的:
        1. 为了算法的清晰和简洁
        2. 为了加快运算速度
        3. 为了使程序易于扩充
    • 小练习,编写子程序,以十六进制的形式在屏幕中间显示给定的byte型数据。小技巧,利用映射关系,0-9数值+30h=对应字符的ascii值,10-15和A到F之间的银色关系是:数值+37h=对应字符的ascii的值

      assume cs:code
      code:segment
          mov al,0eh
      
          call showbyte
      
          mov ax,4c00h
          int 21
          ;子程序,用al传送要显示的数据
      
      showbyte:
          jmp short show
      
          table db '1023456789ABCDEF';字符表
      
      show:
          push bx
          push es
      
          mov ah,al
          shr ah,1
          shr ah,1
          shr ah,1
          shr ah,1;右移4位,ah中得到高4位的值
          and al,00001111b;al中为低4位
      
          mov bl,ah
          mov bh,0
          mov ah,table[bx];用高4位的值作为相对于table的便宜,取得对应的字符
      
          mov bx,0b800h
          mov es,bx
          mov es:[160*12+40*2],ah
      
          mov bl,al
          mov bh,0
          mov al,table[bx];用低4位的值作为相对于table的偏移,取得对应的字符
      
          mov es:[160*12+40*2+2],al
      
          pop es
          pop bx
          ret
      
      code ends
      end start 

    16.4 程序入口地址的直接定址表

    • 小练习
      1. 清屏:将显存中当前屏幕中的支付设为空格;
      2. 设置前景色:设置显存中当前屏幕中处于奇地址的属性字节的第0、1、2位;
      3. 设置背景色:设置显存中当前屏幕中处于奇地址的属性字节的第4、5、6位;
      4. 向上滚动一行:依次将第n+行的内容复制到第n行处,最后一行为空。
    ;================================入口函数1=====================================
    ;入口函数说明;
    ;用ah传递功能号,0是清屏,1是设置前景色,2是设置背景色,3是向上滚动一行
    
    setscreen:
            jmp short set
            table dw sub1,sub2,sub3,sub4
    
    set:
            push bx
            cmp ah,3;判断传递的功能号是否大于3
            ja sret
            mov bl,ah
            mov bh,0
            add bx,bx;根据ah中的功能号计算对应子程序的地址在table表中的偏移
    
            call word ptr table[bx];调用对应的子程序
    
    sret;
            pop bx
            iret
    
    ;================================入口函数2=====================================
    ;入口函数说明;
    ;用ah传递功能号,0是清屏,1是设置前景色,2是设置背景色,3是向上滚动一行
    
    setscreen:
            cmp ah,0
            je do1
            cmp ah,1
            je do2
            cmp ah,2
            je do3
            cmp ah,3
            je do4
    
            jmp short sret
    
    do1:
            call sub1
            jmp short sret
    
    do2:
            call sub2
            jmp short sret
    
    do3:
            call sub3
            jmp short sret
    
    do4:
            call sub4
            jmp short sret
    
    ;子功能==========================================================================
    
    ;清屏
    sub1:
            push bx
            push cx
            push es
            mov bx,0b800h
            mov es,bx
            mov bx,0
            mov cx,2000
    
    sub1s:
            mov byte ptr es:[bx],''
            add bx,2
        loop sub1s
    
            pop es
            pop cx
            pop bx
            ret
    
    ;设置前景色
    sub2:
            push bx
            push cx
            push es
            mov bx,0b800h
            mov es,bx
            mov bx,1
            mov cx,2000
    
    sub2s:
            mov byte ptr es:[bx],11111000b
            or es:[bx],al
            add bx,2
        loop sub2s
    
            pop es
            pop cx
            pop bx
            ret
    
    ;设置背景色
    sub3:
            push bx
            push cx
            push es
            mov cl,4
            shl al,cl
            mov bx,0b800h
            mov es,bx
            mov bx,1
            mov cx,2000
    
    sub3s:
            mov byte ptr es:[bx],10001111b
            or es:[bx],al
            add bx,2
        loop sub3s
    
            pop es
            pop cx
            pop bx
            ret
    
    ;向上滚动一行
    sub4:
            push cx
            push si
            push di
            push es
            push ds
    
            mov si,0b800h
            mov es,si
            mov ds,si
            mov si,160;ds:si指向第n+行
            mov di,0;es:di指向第n行
            cld
            mov cx,24;共复制24行
    
    sub4s:
            push cx
            mov cx,160
            rep movsb;复制
            pop cx
        loop sub4s
    
            mov cx,80
            mov si,0
    
    sub4s1:
            mov byte ptr es:[160*24+si],'';最后一行清空
            add si,2
        loop sub4s1
    
            pop ds
            pop es
            pop di
            pop si
            pop cx
            ret;结束

    第十七章 使用BIOS进行键盘输入和磁盘读写

    略。。。。。。

    笔者看不下去了。。。。有兴趣的读者可以继续找相关的资料看。。。

    END

    展开全文
  • 王爽《汇编语言》笔记(详细)

    万次阅读 多人点赞 2019-03-24 19:53:32
    汇编指令:是机器指令便于记忆的书写格式。汇编指令是机器指令的助记符,同机器指令一一对应。 指令:指令通常由操作码和地址码(操作数)两部分组成 指令集:每种CPU都有自己的汇编指令集。 汇编语言发展至今,有...

    文章目录

    一、基础知识


    1、指令

    机器指令:CPU能直接识别并执行的二进制编码

    汇编指令:汇编指令是机器指令的助记符,同机器指令一一对应。

    指令:指令通常由操作码和地址码(操作数)两部分组成

    指令集:每种CPU都有自己的汇编指令集。

    汇编语言由3类指令组成。

    • 汇编指令
    • 伪指令:没有对应的机器码,由编译器执行,计算机并不执行
    • 其他符号:如+、-、*、/等,由编译器识别,没有对应的机器码。

    编译器:够将汇编指令转换成机器指令的翻译程序每一种CPU都有自己的汇编指令集。
    在这里插入图片描述
    在内存或磁盘上,指令和数据没有任何区别,都是二进制信息

    2、存储器

    随机存储器(RAM)在程序的执行过程中可读可写,必须带电存储

    只读存储器(ROM)在程序的执行过程中只读,关机数据不丢失
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    (以上3张图片来自王道考研 - 计算机组成原理课件)

    3、总线


    1、总线

    总线是连接各个部件的信息传输线,是各个部件共享的传输介质

    主板上有核心器件和一些主要器件,这些器件通过总线(地址总线、数据总线、控制总线)相连。这些器件有CPU、存储器、外围芯片组、扩展插槽等。扩展插槽上一般插有RAM内存条和各类接口卡。
    来自唐朔飞·计算机组成原理经典课件

    总线根据位置分类:

    • 片内总线(芯片内部总线)

    • 系统总线(计算机各部件之间的信息传输线)

      根据传送信息的不同,系统总线从逻辑上又分为3类,地址总线、控制总线和数据总线。

    CPU要想进行数据的读写,必须和外部器件(标准的说法是芯片)进行以下3类信息的交互。

    1. 地址总线:CPU通过地址总线来指定存储单元
      在这里插入图片描述
      1根导线可以传送的稳定状态只有两种,高电平或是低电平。用二进制表示就是1或0

    图示有10根地址线即一次可以传输10位,访问存储单元地址为1011,寻址范围为0 ~ (210 - 1)

    1. 数据总线:CPU与内存或其他器件之间的数据传送是通过数据总线来进行的
      在这里插入图片描述
      8根数据线一次可传送一个8位二进制数据(即一个字节),传送2个字节需要两次;16根数据线一次可传送2个字节(内存对齐核心原理)

    2. 控制总线:CPU对外部器件的控制是通过控制总线来进行的。

    有多少根控制总线,就意味着CPU提供了对外部器件的多少种控制。
    所以,控制总线的宽度决定了CPU对外部器件的控制能力。

    2、CPU对存储器的读写

    在这里插入图片描述
    1、 CPU通过地址线将地址信息3发出。
    2、 CPU通过控制线发出内存读命令,选中存储器芯片,并通知它,将要从中读取数据。
    3、 存储器将3号单元中的数据8通过数据线送入CPU。写操作与读操作的步骤相似。
    联想:在组成原理中用微操作表示:(PC) → MAR; 1 → R; M(MAR) → MDR; …

    3、CPU对外设的控制

    CPU对外设都不能直接控制,如显示器、音箱、打印机等。

    直接控制这些设备进行工作的是插在扩展插槽上的接口卡。

    扩展插槽通过总线和CPU相连,所以接口卡也通过总线同CPU相连。CPU可以直接控制这些接口卡,从而实现CPU对外设的间接控制。

    如:CPU无法直接控制显示器,但CPU可以直接控制显卡,从而实现对显示器的间接控制

    4、内存地址空间

    CPU将系统中各类存储器看作一个逻辑存储器,这个逻辑存储器就是我们所说的内存地址空间。
    对于CPU,所有存储器中的存储单元都处于一个统一的逻辑存储器中,它的容量受CPU寻址能力限制。(或许就是计组中学的统一编址吧)
    在这里插入图片描述
    每个物理存储器在这个逻辑存储器中占有一个地址段,即一段地址空间。CPU在这段地址空间中读写数据,实际上就是在相对应的物理存储器中读写数据(对ROM写无效)。
    在这里插入图片描述

    二、寄存器


    1、寄存器

    CPU由运算器、控制器、寄存器等器件构成,这些器件靠片内总线相连。

    运算器进行信息处理;控制器控制各种器件进行工作;寄存器进行信息存储;

    8086CPU有14个寄存器:AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW都是16位
    在这里插入图片描述

    16位结构CPU具有下面几方面的结构特性。

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

    8086CPU可以一次性处理以下两种尺寸的数据。

    • 字节:记为byte,一个字节由8个bit组成,可以存在8位寄存器中。
    • 字:记为word,一个字由两个字节组成,可以存在一个16位寄存器中(16位CPU)
      在这里插入图片描述
      8086采用小端模式:高地址存放高位字节,低地址存放低位字节。

    2、通用寄存器

    通用寄存器:通常用来存放一般性的数据,有AX、BX、CX、DX,它们可分为两个可独立使用的8位寄存器,

    16位 8高位 8低位
    AX AH AL
    BX BH BL
    CX CH CL
    DX DH DL

    在进行数据传送或运算时,要注意指令的两个操作对象的位数应当是一致的

    一个8位寄存器所能存储的数据范围是0 ~ 28-1。

    3、8086CPU给出物理地址的方法

    8086CPU有20位地址总线,可以传送20位地址,达到1MB寻址能力。
    8086CPU又是16位结构,在内部一次性处理、传输、暂时存储的地址为16位。
    从8086CPU的内部结构来看,如果将地址从内部简单地发出,那么它只能送出16位的地址,表现出的寻址能力只有64KB。
    8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址。
    在这里插入图片描述
    当8086CPU要读写内存时:

    1. CPU中的相关部件提供两个16位的地址,一个称为段地址,另一个称为偏移地址;
    2. 地址加法器将两个16位地址合成为一个20位的物理地址

    地址加法器采用物理地址 = 段地址×16 + 偏移地址的方法用段地址和偏移地址合成物理地址。

    例如,8086CPU要访问地址为123C8H的内存单元,1230H左移一位(空出4位)加上00C8H合成123C8H

    4、段寄存器

    我们可以将一段内存定义为一个段,用一个段地址指示段,用偏移地址访问段内的单元,可以用分段的方式来管理内存。

    用一个段存放数据,将它定义为“数据段”;

    用一个段存放代码,将它定义为“代码段”;

    用一个段当作栈,将它定义为“栈段”。

    注意:

    • 一个段的起始地址一定是16的倍数;
    • 偏移地址为16位,变化范围为0-FFFFH,所以一个段的长度最大为64KB。
    • CPU可以用不同的段地址和偏移地址形成同一个物理地址。

    段寄存器:8086CPU有4个段寄存器:CS、DS、SS、ES,提供内存单元的段地址。

    1、CS和IP

    CS为代码段寄存器,IP为指令指针寄存器,

    CPU将CS、IP中的内容当作指令的段地址和偏移地址,用它们合成指令的物理地址,

    CPU将CS:IP指向的内容当作指令执行。(即PC)
    在这里插入图片描述
    8086CPU的工作过程简要描述

    1. 从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器
    2. IP=IP+所读取指令的长度,从而指向下一条指令
    3. 执行指令。转到步骤1,重复这个过程。

    在8086CPU加电启动或复位后(即CPU刚开始工作时)CS和IP被设置为CS=FFFFH,IP=0000H,即在8086PC机刚启动时,FFFF0H单元中的指令是8086PC机开机后执行的第一条指令。

    8086CPU提供转移指令修改CS、IP的内容。

    • jmp 段地址:偏移地址:用指令中给出的段地址修改CS,偏移地址修改IP。如:jmp 2AE3:3

    • jmp 某一合法寄存器:仅修改IP的内容。如:jmp ax。在含义上好似:mov IP,ax

    8086CPU不支持将数据直接送入段寄存器的操作,这属于8086CPU硬件设计

    2、DS 和 [address]

    DS寄存器:通常用来存放要访问数据的段地址

    [address]表示一个偏移地址为address的内存单元,段地址默认放在ds中

    通过数据段段地址和偏移地址即可定位内存单元。

    mov bx, 1000H ;8086CPU不支持将数据直接送入段寄存器的操作
    
    mov ds, bx ;ds存放数据段地址
    
    mov [0], al ;将al数据(1字节)存到1000H段的0偏移地址处,即10000H
    
    mov ax, [2] ;将数据段偏移地址2处的一个字(8086为2字节)存放到ax寄存器
    
    add cx, [4] ;将偏移地址4处的一个字数据加上cx寄存器数据放到cx寄存器
    
    sub dx, [6] ;dx寄存器数据减去数据段偏移地址6处的字数据存到dx
    

    3、SS 和 SP

    在基于8086CPU编程的时候,可以将一段内存当作栈来使用。

    栈段寄存器SS,存放段地址,SP寄存器存放偏移地址,任意时刻,SS:SP指向栈顶元素

    8086CPU中,入栈时,栈顶从高地址向低地址方向增长。

    push ax表示将寄存器ax中的数据送入栈中,由两步完成。

    1. SP=SP-2,SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶;
    2. 将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶。
      在这里插入图片描述

    pop ax表示从栈顶取出数据送入ax,由以下两步完成。

    1. 将SS:SP指向的内存单元处的数据送入ax中;
    2. SP=SP+2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。

    实验

    1. 将10000H~1000FH这段空间当作栈,初始状态栈是空的;
    2. 设置AX=001AH,BX=001BH;
    3. 将AX、BX中的数据入栈;
    4. 然后将AX、BX清零;
    5. 从栈中恢复AX、BX原来的内容。
    mov ax, 1000H 
    mov ss, ax 
    mov sp, 0010H    ;初始化栈顶
    mov ax, 001AH
    mov bx, 001BH 
    
    push ax 
    push bx    ;ax、bx入栈
    
    sub ax, ax   ;将ax清零,也可以用mov ax,0,
                 ;sub ax,ax的机器码为2个字节,
                 ;mov ax,0的机器码为3个字节。
            
    sub bx, bx 
    
    pop bx  ;从栈中恢复ax、bx原来的数据
    pop ax  ;
    

    三、第一个程序

    1、汇编程序从写出到执行的过程

    在这里插入图片描述
    加载后,CPU的CS:IP指向程序的第一条指令(即程序的入口)

    ;1.asm
    assume cs:codesg ;将用作代码段的段codesg和段寄存器cs联系起来。
    
    codesg segment ;定义一个段,段的名称为“codesg”,这个段从此开始
    			   ;codesg是一个标号,作为一个段的名称,最终被编译连接成一个段的段地址
    
    	mov ax, 0123H
    	mov bx, 0456H 
    	add ax, bx
    	add ax, ax 
    	
    	mov ax, 4c00H 
    	int 21H ;这两条指令实现程序的返回
    	
    codesg ends ;名称为“codesg”的段到此结束
    
    end ;编译器在编译汇编程序的过程中,碰到了伪指令end,结束对源程序的编译
    

    在这里插入图片描述
    在这里插入图片描述

    2、程序执行过程跟踪

    DOS系统中.EXE文件中的程序的加载过程
    在这里插入图片描述

    在这里插入图片描述


    四、[bx] 和 loop指令


    1、[bx] 和 loop指令

    [bx] 的含义:[bx]同样表示一个内存单元,它的偏移地址在bx中,段地址默认在ds中

    loop指令的格式是:loop 标号,CPU执行loop指令的时候,要进行两步操作,

    1. (cx) = (cx) - 1;

    2. 判断 cx 中的值,不为零则转至标号处执行程序,如果为零则向下执行。

    例如:计算212

    assume cs:code 
    
    code segment 
    	mov ax, 2
    	
    	mov cx, 11 ;循环次数
    s:  add ax, ax 
    	loop s     ;在汇编语言中,标号代表一个地址,标号s实际上标识了一个地址,
                   ;这个地址处有一条指令:add ax,ax。
                   ;执行loop s时,首先要将(cx)减1,然后若(cx)不为0,则向前
                   ;转至s处执行add ax,ax。所以,可以利用cx来控制add ax,ax的执行次数。
    	
    	mov ax,4c00h 
    	int 21h 
    code ends 
    end
    

    loop 和 [bx] 的联合应用

    计算ffff:0 ~ ffff:b单元中的数据的和,结果存储在dx中

    问题分析:

    1. 这些内存单元都是字节型数据范围0 ~ 255 ,12个字节数据和不会超过65535,dx可以存下
    2. 对于8位数据不能直接加到 dx

    解决方案:

    用一个16位寄存器来做中介。将内存单元中的8位数据赋值到一个16位寄存器a中,再将ax中的数据加到dx

    assume cs:code 
    
    code segment 
    	mov ax, 0ffffh ;在汇编源程序中,数据不能以字母开头,所以要在前面加0。
    	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      ;ds:bx指向下一个单元
    	loop s 
    	
    	mov ax, 4c00h 
    	int 21h 
    code ends 
    end
    

    2、段前缀

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

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

    段前缀的使用

    将内存ffff:0 ~ ffff:b单元中的数据复制到0:200 ~ 0:20b单元中。

    assume cs:code 
    
    code segment 
    	mov ax, 0ffffh 
    	mov ds, ax   ;(ds)= 0ffffh 
    	mov ax, 0020h
        mov es, ax   ;(es)= 0020h     0:200 等效于 0020:0
        mov bx, 0    ;(bx)= 0,此时ds:bx指向ffff:0,es:bx指向0020:0
        
    	mov cx,12   ;(cx)=12,循环12次
    s:  mov dl,[bx] ;(d1)=((ds)* 16+(bx)),将ffff:bx中的字节数据送入dl 
    	mov es:[bx],dl ;((es)*16+(bx))=(d1),将dl中的数据送入0020:bx 
    	inc bx  ;(bx)=(bx)+1
    	loop s 
    	
    	mov ax,4c00h 
    	int 21h 
    code ends 
    end
    

    五、包含多个段的程序

    程序中对段名的引用,将被编译器处理为一个表示段地址的数值。

    mov ax, data 
    
    mov ds, ax 
    
    mov bx, ds:[6]
    

    在代码段中使用数据

    ;计算 8 个数据的和存到 ax 寄存器
    assume cs:code 
    
    code segment 
    
    	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ;define word 定义8个字形数据
    
    	start:	mov bx, 0  ;标号start
    			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    ;end除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方
    	     	 ;用end指令指明了程序的入口在标号start处,也就是说,“mov bx,0”是程序的第一条指令。
    

    在代码段中使用栈

    ;利用栈,将程序中定义的数据逆序存放。
    assume cs:codesg 
    
    codesg segment 
    	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ; 0-15单元
    	dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ; 16-47单元作为栈使用
    			
    	start:	mov ax, cs 
    			mov ss, ax 
    			mov sp, 30h ;将设置栈顶ss:sp指向栈底cs:30。   30h = 48d
    			mov bx, 0
    			
    			mov cx, 8
    	s:		push cs:[bx]
    			add bx, 2
    			loop s    ;以上将代码段0~15单元中的8个字型数据依次入栈
    			
    			mov bx, 0
    			
    			mov cx, 8
    	s0:		pop cs:[bx]		
    			add bx,2
    			loop s0   ;以上依次出栈8个字型数据到代码段0~15单元中
    			
    			mov ax,4c00h 
    			int 21h 
    codesg ends 
    end start	;指明程序的入口在start处
    

    将数据、代码、栈放入不同的段

    assume cs:code,ds:data,ss:stack 
    
    data segment 
    	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ;0-15单元
    data ends 
    
    stack segment 
    	dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ;0-31单元
    stack ends 
    
    code segment 
    	start:	mov ax, stack;将名称为“stack”的段的段地址送入ax
    			mov ss, ax
    			mov sp, 20h  ;设置栈顶ss:sp指向stack:20。 20h = 32d
    			
    			mov ax, data ;将名称为“data”的段的段地址送入ax
    			mov ds, ax   ;ds指向data段
    			
    			mov bx, 0    ;ds:bx指向data段中的第一个单元
    			
    			mov cx, 8
    	s:	    push [bx]
    			add bx, 2
    			loop s       ;以上将data段中的0~15单元中的8个字型数据依次入栈
    			
    			mov bx, 0
    			
    			mov cx, 8
    	s0:		pop [bx]
    			add bx, 2
    			loop s0      ;以上依次出栈8个字型数据到data段的0~15单元中
    			
    			mov ax, 4c00h 
    			int 21h 
    code ends
    end start
    ;“end start”说明了程序的入口,这个入口将被写入可执行文件的描述信息,
    ;可执行文件中的程序被加载入内存后,CPU的CS:IP被设置指向这个入口,从而开始执行程序中的第一条指令
    

    关于可执行文件结构与程序入口的详细描述参考:PE文件结构

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


    1、and 和 or

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

    mov al, 01100011B
    and al, 00111011B

    执行后:al=00100011B即都为1才为1

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

    mov al, 01100011B
    or al, 00111011B
    执行后:al=01111011B 即只要有一个为1就为1

    关于ASCII码
    世界上有很多编码方案,有一种方案叫做ASCII编码,是在计算机系统中通常被采用的。简单地说,所谓编码方案,就是一套规则,它约定了用什么样的信息来表示现实对象。比如说,在ASCII编码方案中,用61H表示“a”,62H表示“b”。一种规则需要人们遵守才有意义。

    在文本编辑过程中,我们按一下键盘的a键,就会在屏幕上看到“a”。我们按下键盘的a键,这个按键的信息被送入计算机,计算机用ASCII码的规则对其进行编码,将其转化为61H存储在内存的指定空间中;文本编辑软件从内存中取出61H,将其送到显卡上的显存中;工作在文本模式下的显卡,用ASCII码的规则解释显存中的内容
    61H被当作字符“a”,显卡驱动显示器,将字符“a”的图像画在屏幕上。我们可以看到,显卡在处理文本信息的时候,是按照ASCII码的规则进行的。这也就是说,如果我们要想在显示器上看到“a”,就要给显卡提供“a”的ASCIⅡ码,61H。如何提供?当然是写入显存中。

    以字符形式给出的数据

    assume cs:code,ds:data 
    
    data segment 
    	db 'unIx'   ;相当于“db 75H,6EH,49H,58H”
    	db 'foRK'
    data ends 
    
    code segment
    start:	mov al, 'a'  ;相当于“mov al, 61H”,“a”的ASCI码为61H;
    		mov b1, 'b'
    		
    		mov ax, 4c00h 
    		int 21h 
    code ends
    end start
    

    大小写转换的问题
    在这里插入图片描述
    小写字母的ASCII码值比大写字母的ASCII码值大20H

    大写字母ASCII码的第5位为0,小写字母的第5位为1(其他一致)

    assume cs:codesg,ds:datasg 
    
    datasg segment 
    	db 'BaSiC'
    	db 'iNfOrMaTion'
    datasg end
    
    codesg segment 
    	start:	mov ax, datasg 
    			mov ds, ax	;设置ds 指向 datasg段
    		
    			mov bx, 0	;设置(bx)=0,ds:bx指向’BaSic’的第一个字母
    			
    			mov cx, 5     	 ;设置循环次数5,因为’Basic'有5个字母
    	s:		mov al, [bx]     ;将ASCII码从ds:bx所指向的单元中取出
    			and al, 11011111B;将al中的ASCII码的第5位置为0,变为大写字母
    			mov [bx], al	 ;将转变后的ASCII码写回原单元
    			inc bx		     ;(bx)加1,ds:bx指向下一个字母
    			loop s 
    			
    			mov bx, 5	;设置(bx)=5,ds:bx指向,iNfOrMaTion'的第一个字母
    			
    			mov cx, 11	;设置循环次数11,因为‘iNfOrMaTion'有11个字母
    	s0:		mov al, [bx]
    			or al, 00100000B;将a1中的ASCII码的第5位置为1,变为小写字母
    			mov [bx], al 
    			inc bx
    			loop s0
    			
    			mov ax, 4c00h 
    			int 21h 
    codesg ends
    

    2、[bx+idata]

    [bx+idata]表示一个内存单元, 例如:mov ax, [bx+200]
    该指令也可以写成如下格式:

    mov ax, [200+bx]
    
    mov ax, 200[bx]
    
    mov ax, [bx].200
    

    用[bx+idata]的方式进行数组的处理

    assume cs:codesg,ds:datasg 
    
    datasg segment 
    	db 'BaSiC';转为大写
    	db 'MinIx';转为小写
    datasg ends
    
    codesg segment
    	start:
    		mov ax, datasg 
    		mov ds, ax 
    		mov bx, 0  ;初始ds:bx
    	
    		mov cx, 5
    	s:	mov al, 0[bx]  
    		and al, 11011111b ;转为大写字母
    		mov 0[bx], al ;写回
    		mov al, 5[bx]  ;[5 + bx]
    		or al, 00100000b ;转为小写字母
    		mov 5[bx], al 
    		inc bx
    		loop s
    		
    		mov ax, 4c00h 
    		int 21h
    codesg ends
    end start
    

    C语言描述

    int main()
    {
    	char a[] = "BaSic";
    	char b[] = "MinIX";
    	
    	int i = 0;
    	
    	do
    	{
    		a[i] = a[i] & 0xDF;
    		b[i] = b[i] | 0x20;
    		i++;
    	} while(i < 5);
    
    	return 0;
     } 
    

    3、SI 、DI 与 寻址方式的灵活应用

    1、si 、di

    si和di是8086CPU中和bx功能相近的寄存器,si和di不能够分成两个8位寄存器来使用。

    assume cs: codesg, ds: datasg 
    
    datasg segment 
    	db 'welcome to masm!';用si和di实现将字符串‘welcome to masm!"复制到它后面的数据区中。
    	db '................'
    datasg ends
    
    codesg segment 
    	start:	mov ax, datasg 
    			mov ds, ax 
    			mov si, 0
    			
    			mov cx, 8
    	s:		mov ax, 0[si] ;[0 + si]
    			mov 16[si], ax ;[16 + si] 使用[bx +idata]方式代替di,使程序更简洁
    			add si, 2 
    			loop s 
    			
    			mov ax, 4c00h 
    			int 21h 
    codesg ends 
    end start
    

    2、[bx + si] 和 [bx + di]

    [bx+si]和[bx+di]的含义相似

    [bx+si]表示一个内存单元,它的偏移地址为(bx)+(si)

    指令mov ax, [bx + si]的含义:将一个内存单元字数据的内容送入ax,段地址在ds中

    该指令也可以写成如下格式:mov ax, [bx][si]

    3、[bx+si+idata]和[bx+di+idata]
    [bx+si+idata]表示一个内存单元,它的偏移地址为(bx)+(si)+idata

    指令mov ax,[bx+si+idata]的含义:将一个内存单元字数据的内容送入ax,段地址在ds中

    4、不同的寻址方式的灵活应用
    [idata]用一个常量来表示地址,可用于直接定位一个内存单元;
    [bx]用一个变量来表示内存地址,可用于间接定位一个内存单元;
    [bx+idata]用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元;
    [bx+si]用两个变量表示地址;
    [bx+si+idata]用两个变量和一个常量表示地址。

    
    ;将datasg段中每个单词改为大写字母
    assume cs:codesg,ds:datasg,ss:stacksg 
    
    datasg segment
    	db 'ibm            ' ;16
    	db 'dec            ' 
    	db 'dos            '
    	db 'vax            '  ;看成二维数组
    datasg ends 
    
    stacksg segment ;定义一个段,用来做栈段,容量为16个字节
    	dw 0, 0, 0, 0, 0, 0, 0, 0
    stacksg ends 
    
    codesg segment 
    	start:	mov ax, stacksg 
    			mov ss, ax
    			mov sp, 16 
    			mov ax, datasg 
    			mov ds, ax 
    			mov bx, 0 ;初始ds:bx
    			
    			;cx为默认循环计数器,二重循环只有一个计数器,所以外层循环先保存cx值,再恢复,我们采用栈保存
    			mov cx, 4
    	s0:		push cx	;将外层循环的cx值入栈
    			mov si, 0
    			mov cx, 3	;cx设置为内层循环的次数
    	s:		mov al, [bx+si]
    			and al, 11011111b ;每个字符转为大写字母
    			mov [bx+si], al 
    			inc si
    			loop s 
    			
    			add bx, 16 ;下一行
    			pop cx	;恢复cx值
    			loop s0 ;外层循环的loop指令将cx中的计数值减1
    			
    			mov ax,4c00H 
    			int 21H 
    codesg ends
    end start
    

    七、数据处理的两个基本问题

    1、 bx、si、di和bp

    在8086CPU中,只有这4个寄存器可以用在“[…]”中来进行内存单元的寻址。

    在[ ]中,这4个寄存器可以单个出现,或只能以4种组合出现:bx和si、bx和dibp和si、bp和di

    只要在[……]中使用寄存器bp,而指令中没有显性地给出段地址, 段地址就默认在ss中

    2、机器指令处理的数据在什么地方

    数据处理大致可分为3类:读取、写入、运算。

    在机器指令这一层来讲,并不关心数据的值是多少,而关心指令执行前一刻,它将要处理的数据所在的位置。指令在执行前,所要处理的数据可以在3个地方:CPU内部、内存、端口
    在这里插入图片描述

    3、汇编语言中数据位置的表达

    汇编语言中用3个概念来表达数据的位置

    • 立即数(idata)
    mov ax, 1                 ;对于直接包含在机器指令中的数据(执行前在CPU的指令缓冲器中)
    add bx, 2000h             ;在汇编语言中称为:立即数(idata)
    or bx, 00010000b
    mov al, 'a'
    
    • 寄存器
    mov ax, bx     ;指令要处理的数据在寄存器中,在汇编指令中给出相应的寄存器名。
    mov ds, ax 
    push bx 
    mov ds:[0], bx 
    push ds 
    mov ss, ax
    mov sp, ax
    
    • 段地址(SA)和偏移地址(EA)
    ;指令要处理的数据在内存中,在汇编指令中可用[X]的格式给出EA,SA在某个段寄存器中。
    mov ax, [0]
    mov ax, [di]
    mov ax, [bx+8]
    mov ax, [bx+si]
    mov ax, [bx+si+8]   ;以上段地址默认在ds中
    
    mov ax, [bp]
    mov ax, [bp+8]
    mov ax, [bp+si]
    mov ax, [bp+si+8]   ;以上段地址默认在ss中
    
    mov ax, ds:[bp]
    mov ax, es:[bx]
    mov ax, ss:[bx+si]
    mov ax, cs:[bx+si+8] ;显式给出存放段地址的寄存器
    

    寻址方式
    在这里插入图片描述

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

    8086CPU的指令,可以处理两种尺寸的数据,byte和word

    1. 通过寄存器名指明要处理的数据的尺寸。
      例如: mov al, ds:[0] 寄存器al指明了数据为1字节

    2. 在没有寄存器名存在的情况下,用操作符X ptr指明内存单元的长度,X在汇编指令中可以为wordbyte
      例如:mov byte ptr ds:[0], 1 byte ptr 指明了指令访问的内存单元是一个字节单元

    3. 有些指令默认了访问的是字单元还是字节单元
      例如,push [1000H],push 指令只进行字操作。

    5、寻址方式的综合应用

    在这里插入图片描述

    mov ax, seg 
    mov ds, ax 
    mov bx, 60h   ;确定记录地址,ds:bx 
    
    mov word ptr [bx+0ch], 38   ;排名字段改为38  [bx].0ch
    add word ptr [bx+0eh], 70   ;收入字段增加70  [bx].0eh
    mov si, 0   ;用si来定位产品字符串中的字符
    mov byte ptr [bx+10h+si], 'V'   ;[bx].10h[si]
    inc si 
    mov byte ptr [bx+10h+si], 'A'
    inc si 
    mov byte ptr [bx+10h+si], 'X'
    

    C语言描述

    /*定义一个公司记录的结构体*/
    struct company
    {
        char cn[3];/*公司名称*/
        char hn[9];/*总裁姓名*/
        int pm;/*排名*/
        int sr;/*收入*/
        char cp[3];/*著名产品*/
    };
    //sizeof (struct company) == 24
    
    int main()
    {
        /*定义一个公司记录的变量,内存中将存有一条公司的记录*/
        struct company dec = {"DEC", "Ken Olsen", 137, 40, "PDP"};
    
        int i;
    
        dec.pm = 38;
        dec.sr = dec.sr + 70;
    
        i = 0;
        dec.cp[i] = 'V'; //mov byte ptr [bx].10h[si], 'V'
        i++;
        dec.cp[i] = 'A';
        i++;
        dec.cp[i] = 'X';
    
        return 0;
    }
    
    

    6、div指令、dd、dup、mul指令

    div是除法指令

    1. 除数:有8位和16位两种,在一个寄存器内存单元中。

    2. 被除数:默认放在AXDX和AX中,
      如果除数为8位,被除数则为16位,默认在AX中存放;
      如果除数为16位,被除数则为32位,在DX和AX中存放,DX存放高16位,AX存放低16位

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

    ;利用除法指令计算100001/100。
    ;100001D = 186A1H
    mov dx, 1
    mov ax, 86A1H ;(dx)*10000H+(ax)=100001
    mov bx, 100
    div bx
    
    ;利用除法指令计算1001/100
    mov ax, 1001
    mov bl, 100
    div b1
    

    伪指令dd

    db和dw定义字节型数据和字型数据。

    dd是用来定义dword(double word,双字)型数据的伪指令

    操作符dup

    dup在汇编语言中同db、dw、dd等一样,也是由编译器识别处理的符号。
    它和db、dw、dd等数据定义伪指令配合使用,用来进行数据的重复

    db 3 dup (0)       ;定义了3个字节,它们的值都是0,相当于db 0,0,0。
    db 3 dup (0, 1, 2) ;定义了9个字节,它们是0、1、2、0、1、2、0、1、2,相当于db 0,1,2,0,1,2,0,1,2。
    db 3 dup ('abc', 'ABC') ;定义了18个字节,它们是abcABCabcABCabcABCC,相当于db 'abc', 'ABC' ,'abc' , 'ABC, 'abc', 'ABC'。
    

    mul 指令

    mul是乘法指令,使用 mul 做乘法的时候:相乘的两个数:要么都是8位,要么都是16位。

    • 8 位: AL中和 8位寄存器内存字节单元中;

    • 16 位: AX中和 16 位寄存器内存字单元中。

    结果

    • 8位:AX中;

    • 16位:DX(高位)和 AX(低位)中。

    格式:mul 寄存器mul 内存单元

    ;计算100*10
    ;100和10小于255,可以做8位乘法
    mov al,100
    mov bl,10
    mul bl
    
    ;结果: (ax)=1000(03E8H) 
    
    
    ;计算100*10000
    ;100小于255,可10000大于255,所以必须做16位乘法,程序如下:
    mov ax,100
    mov bx,10000
    mul bx
    
    ;结果: (ax)=4240H,(dx)=000FH     (F4240H=1000000)
    
    

    八、转移指令的原理


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

    8086CPU的转移行为有以下几类。

    • 只修改IP时,称为段内转移,比如:jmp ax
    • 同时修改CS和IP时,称为段间转移,比如:jmp 1000:0

    由于转移指令对IP的修改范围不同,段内转移又分为:短转移和近转移

    • 短转移IP的修改范围为-128 ~ 127
    • 近转移IP的修改范围为-32768 ~ 32767

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

    • 无条件转移指令(如:jmp)
    • 条件转移指令
    • 循环指令(如:loop)
    • 过程
    • 中断

    1、操作符offset

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

    ;将s处的一条指令复制到s0处
    assume cs:codesg
    codesg segment
     s:   mov ax, bx           ;(mov ax,bx 的机器码占两个字节)
          mov si, offset s     ;获得标号s的偏移地址
          mov di, offset s0    ;获得标号s0的偏移地址
          
          mov ax, cs:[si]
          mov cs:[di], ax
     s0:  nop                     ;(nop的机器码占一个字节)
          nop
     codesg ends
     ends
    

    2、jmp指令

    jmp为无条件转移,转到标号处执行指令可以只修改IP,也可以同时修改CS和IP;

    jmp指令要给出两种信息:

    • 转移的目的地址
    • 转移的距离(段间转移、段内短转移,段内近转移)

    jmp short 标号 jmp near ptr 标号 jcxz 标号 loop 标号 等几种汇编指令,它们对 IP的修改

    是根据转移目的地址和转移起始地址之间的位移来进行的。在它们对应的机器码中不包含转移的目的地址,而包含的是到目的地址的位移距离。

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

    jmp short 标号(段内短转移)

    指令“jmp short 标号”的功能为(IP)=(IP)+8位位移,转到标号处执行指令

    (1)8位位移 = “标号”处的地址 - jmp指令后的第一个字节的地址;

    (2)short指明此处的位移为8位位移;

    (3)8位位移的范围为-128~127,用补码表示

    (4)8位位移由编译程序在编译时算出。

    assume cs:codesg
    codesg segment
      start:mov ax,0
            jmp short s ;s不是被翻译成目的地址
            add ax, 1
          s:inc ax ;程序执行后, ax中的值为 1 
    codesg ends
    end start
    

    CPU不需要这个目的地址就可以实现对IP的修改。这里是依据位移进行转移

    jmp short s指令的读取和执行过程:

    1. (CS)=0BBDH,(IP)=0006,上一条指令执行结束后CS:IP指向EB 03(jmp short s的机器码);
    2. 读取指令码EB 03进入指令缓冲器;
    3. (IP) = (IP) + 所读取指令的长度 = (IP) + 2 = 0008,CS:IP指向add ax,1;
    4. CPU指行指令缓冲器中的指令EB 03;
    5. 指令EB 03执行后,(IP)=000BH,CS:IP指向inc ax

    jmp near ptr 标号 (段内近转移)

    指令“jmp near ptr 标号”的功能为:(IP) = (IP) + 16位位移

    2、转移的目的地址在指令中的jmp指令

    jmp far ptr 标号(段间转移或远转移)

    指令 “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 ;s被翻译成转移的目的地址0B01 BD0B
              db 256 dup (0) ;转移的段地址:0BBDH,偏移地址:010BH
        s:    add ax,1
              inc ax
    codesg ends
    end start
    

    在这里插入图片描述

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

    jmp 16位寄存器 功能:IP =(16位寄存器)

    转移地址在内存中的jmp指令有两种格式:

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

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

    mov ax, 0123H
    mov ds:[0], ax
    jmp word ptr ds:[0]
    ;执行后,(IP)=0123H
    
    • jmp dword ptr 内存单元地址(段间转移)

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

    1. (CS)=(内存单元地址+2)
    2. (IP)=(内存单元地址)
    mov ax, 0123H
    mov ds:[0], ax;偏移地址
    mov word ptr ds:[2], 0;段地址
    jmp dword ptr ds:[0]
    ;执行后,
    ;(CS)=0
    ;(IP)=0123H
    ;CS:IP 指向 0000:0123。
    

    4、jcxz指令和loop指令

    jcxz指令

    jcxz指令为有条件转移指令,所有的有条件转移指令都是短转移,

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

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

    当(cx) = 0时,(IP) = (IP) + 8位位移

    • 8位位移 = “标号”处的地址 - jcxz指令后的第一个字节的地址;
    • 8位位移的范围为-128~127,用补码表示;
    • 8位位移由编译程序在编译时算出。

    当(cx)!=0时,什么也不做(程序向下执行)

    loop指令

    loop指令为循环指令,所有的循环指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。

    对IP的修改范围都为-128~127。

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

    (cx) = (cx) - 1;如果 (cx) != 0,(IP) = (IP) + 8位位移。

    • 8位位移 = 标号处的地址 - loop指令后的第一个字节的地址;
    • 8位位移的范围为-128~127,用补码表示;
    • 8位位移由编译程序在编译时算出。

    如果(cx)= 0,什么也不做(程序向下执行)。

    九、call和ret指令


    call和ret指令都是转移指令,它们都修改IP,或同时修改CS和IP。

    1、ret 和 retf

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

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

    CPU执行ret指令时,相当于进行: pop IP

    (1)(IP) = ( (ss) * 16 + (sp) )

    (2)(sp) = (sp) + 2

    CPU执行retf指令时,相当于进行:pop IP, pop CS

    (1)(IP) = ( (ss) * 16 + (sp) )

    (2)(sp) = (sp) + 2

    (3)(CS) = ( (ss) * 16 + (sp) )

    (4)(sp) = (sp) + 2

    assume cs:code 
    stack seqment
    	db 16 dup (0)
    stack ends 
    
    code segment
    		mov ax, 4c00h
    		int 21h 
     start:	mov ax, stack 
     		mov ss, ax
     		mov sp, 16
    		mov ax, 0
    		push ax ;ax入栈
    		mov bx, 0
    		ret ;ret指令执行后,(IP)=0,CS:IP指向代码段的第一条指令。可以push cs  push ax  retf
    code ends
    end start
    

    2、call 指令

    call指令经常跟ret指令配合使用,因此CPU执行call指令,进行两步操作:

    (1)将当前的 IP 或 CS和IP 压入栈中;

    (2)转移(jmp)。

    call指令不能实现短转移,除此之外,call指令实现转移的方法和 jmp 指令的原理相同

    call 标号(近转移)

    CPU执行此种格式的call指令时,相当于进行 push IP jmp near ptr 标号

    call far ptr 标号(段间转移)

    CPU执行此种格式的call指令时,相当于进行:push CS,push IP jmp far ptr 标号

    call 16位寄存器

    CPU执行此种格式的call指令时,相当于进行: push IP jmp 16位寄存器

    call word ptr 内存单元地址

    CPU执行此种格式的call指令时,相当于进行:push IP jmp word ptr 内存单元地址

    mov sp, 10h
    mov ax, 0123h
    mov ds:[0], ax
    call word ptr ds:[0]
    ;执行后,(IP)=0123H,(sp)=0EH
    

    call dword ptr 内存单元地址

    CPU执行此种格式的call指令时,相当于进行:push CS push IP jmp dword ptr 内存单元地址

    mov sp, 10h
    mov ax, 0123h
    mov ds:[0], ax
    mov word ptr ds:[2], 0
    call dword ptr ds:[0]
    ;执行后,(CS)=0,(IP)=0123H,(sp)=0CH
    

    3、call 和 ret 的配合使用

    分析下面程序

    assume cs:code
    code segment
    start:	mov ax,1
    	    mov cx,3
         	call s ;(1)CPU指令缓冲器存放call指令,IP指向下一条指令(mov bx, ax),执行call指令,IP入栈,jmp
         	
    	    mov bx,ax	;(4)IP重新指向这里  bx = 8
         	mov ax,4c00h
         	int 21h
         s: add ax,ax
         	loop s;(2)循环3次ax = 8
    	    ret;(3)return : pop IP
    code ends
    end start
    

    call 与 ret 指令共同支持了汇编语言编程中的模块化设计

    编写子程序

    十、标志寄存器


    1、标志寄存器

    CPU内部的寄存器中,有一种特殊的寄存器(对于不同的处理机,个数和结构都可能不同)具有以下3种作用。

    (1)用来存储相关指令的某些执行结果;

    (2)用来为CPU执行相关指令提供行为依据;

    (3)用来控制CPU的相关工作方式。

    这种特殊的寄存器在8086CPU中,被称为标志寄存器(flag)。

    8086CPU的标志寄存器有16位,其中存储的信息通常被称为程序状态字(PSW-Program Status Word)

    flag寄存器是按位起作用的,它的每一位都有专门的含义,记录特定的信息。

    在这里插入图片描述
    在8086CPU的指令集中,有的指令的执行是影响标志寄存器的,比如,add、sub、mul、div、inc、or、and等,它们大都是运算指令(进行逻辑或算术运算);有的指令的执行对标志寄存器没有影响,比如,mov、push、pop等,它们大都是传送指令

    1、零标志位 (ZF)

    零标志位(Zero Flag)。它记录相关指令执行后,其结果是否为0。

    如果结果为0,那么zf = 1(表示结果是0);如果结果不为0,那么zf = 0。

    mov ax, 1
    sub ax, 1 ;执行后,结果为0,则zf = 1
    
    mov ax, 2
    sub ax, 1 ;执行后,结果不为0,则zf = 0
    

    2、奇偶标志位 (PF)

    奇偶标志位(Parity Flag)。它记录相关指令执行后,其结果的所有bit位中1的个数是否为偶数。

    如果1的个数为偶数,pf = 1,如果为奇数,那么pf = 0。

    mov al, 1
    add al, 10 ;执行后,结果为00001011B,其中有3(奇数)个1,则pf = 0;
    
    mov al, 1
    or al, 2  ;执行后,结果为00000011B,其中有2(偶数)个1,则pf = 1;
    

    3、符号标志位(SF)

    符号标志位(Symbol Flag)。它记录相关指令执行后,其结果是否为负。

    如果结果为负,sf = 1;如果非负,sf = 0。

    计算机中通常用补码来表示有符号数据。计算机中的一个数据可以看作是有符号数,也可以看成是无符号数。

    00000001B,可以看作为无符号数1,或有符号数+1;
    10000001B,可以看作为无符号数129,也可以看作有符号数-127。

    对于同一个二进制数据,计算机可以将它当作无符号数据来运算,也可以当作有符号数据来运算

    CPU在执行add等指令的时候,就包含了两种含义:可以将add指令进行的运算当作无符号数的运算,也可以将add指令进行的运算当作有符号数的运算

    SF标志,就是CPU对有符号数运算结果的一种记录,它记录数据的正负。在我们将数据当作有符号数来运算的时候,可以通过它来得知结果的正负。如果我们将数据当作无符号数来运算,SF的值则没有意义,虽然相关的指令影响了它的值

    mov al, 10000001B 
    add al, 1   ;执行后,结果为10000010B,sf = 1,表示:如果指令进行的是有符号数运算,那么结果为负;
    
    mov al, 10000001B
    add al, 01111111B   ;执行后,结果为0,sf = 0,表示:如果指令进行的是有符号数运算,那么结果为非负
    

    3、进位标志位(CF)

    进位标志位(Carry Flag)。一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值
    在这里插入图片描述
    97H - 98H 产生借位CF = 1 ==》 (al) = 197H - 98H = FFH

    4、溢出标志位(OF)

    溢出标志位(Overflow Flag)。一般情况下,OF记录了有符号数运算的结果是否发生了溢出。

    如果发生溢出,OF = 1;如果没有,OF = 0。

    CF和OF的区别:CF是对无符号数运算有意义的标志位,而OF是对有符号数运算有意义的标志位

    CPU在执行add等指令的时候,就包含了两种含义:无符号数运算和有符号数运算。

    • 对于无符号数运算,CPU用CF位来记录是否产生了进位;
    • 对于有符号数运算,CPU用OF位来记录是否产生了溢出,当然,还要用SF位来记录结果的符号。
    mov al, 98
    add al, 99   ;执行后将产生溢出。因为进行的"有符号数"运算是:(al)=(al)+ 99 = 98 + 99=197 = C5H 为-59的补码
                 ;而结果197超出了机器所能表示的8位有符号数的范围:-128-127。
                 ;add 指令执行后:无符号运算没有进位CF=0,有符号运算溢出OF=1
                 ;当取出的数据C5H按无符号解析C5H = 197, 当按有符号解析通过SP得知数据为负,即C5H为-59补码存储,
                 
    mov al,0F0H  ;F0H,为有符号数-16的补码   -Not(F0 - 1)
    add al,088H  ;88H,为有符号数-120的补码   -Not(88- 1)
                  ;执行后,将产生溢出。因为add al, 088H进行的有符号数运算结果是:(al)= -136 
                  ;而结果-136超出了机器所能表示的8位有符号数的范围:-128-127。
                  ;add 指令执行后:无符号运算有进位CF=1,有符号运算溢出OF=1
    

    2、adc指令和sbb指令

    adc是带进位加法指令,它利用了CF位上记录的进位值。

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

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

    mov ax, 2
    mov bx, 1
    sub bx, ax  ;无符号运算借位CF=1,有符号运算OF = 0
    adc ax, 1   ;执行后,(ax)= 4。adc执行时,相当于计算:(ax)+1+CF = 2+1+1 = 4。
    

    在这里插入图片描述

    ;计算1EF000H+201000H,结果放在ax(高16位)和bx(低16位)中。
    ;将计算分两步进行,先将低16位相加,然后将高16位和进位值相加。
    mov ax, 001EH 
    mov bx, 0F000H 
    add bx, 1000H
    adc ax, 0020H
    

    sbb指令

    sbb是带借位减法指令,它利用了CF位上记录的借位值。

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

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

    ;计算 003E1000H - 00202000H,结果放在ax,bx中,程序如下:
    mov bx, 1000H
    mov ax, 003EH
    sub bx, 2000H
    sbb ax, 0020H
    

    3、cmp指令

    cmp是比较指令,cmp的功能相当于减法指令,只是不保存结果。cmp指令执行后,将对标志寄存器产生影响。

    其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果。

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

    例如:
    指令cmp ax, ax,做(ax)-(ax)的运算,结果为0,但并不在ax中保存,仅影响flag的相关各位。
    指令执行后:zf=1,pf=1,sf=0,cf=0,of=0。

    CPU在执行cmp指令的时候,也包含两种含义:进行无符号数运算和进行有符号数运算。

    cmp ax, bx 无符号比较时
    (ax) = (bx) zf = 1
    (ax) ≠ (bx) zf = 0
    (ax) < (bx) cf = 1
    (ax) ≥ (bx) cf = 0
    (ax) > (bx) cf = 0 且 zf = 0
    (ax) ≤ (bx) cf = 1 且 zf = 1

    上面的表格可以正推也可以逆推

    如果用cmp来进行有符号数比较时
    SF只能记录实际结果的正负,发生溢出的时候,实际结果的正负不能说明逻辑上真正结果的正负
    但是逻辑上的结果的正负,才是cmp指令所求的真正结果,所以我们在考察SF的同时考察OF,就可以得知逻辑上真正结果的正负,同时就知道比较的结果。

    mov ah, 08AH  ; -Not(8A-1) = -118  即当成有符号数时为-118
    mov bh, 070H  ; 有符号数时最高位为0为正数, 70H = 112
    cmp ah, bh    ;(ah)-(bh)实际得到的结果是1AH 
    		      ; 在逻辑上,运算所应该得到的结果是:(-118)- 112 = -230
    		      ; sf记录实际结果的正负,所以sf=0
    

    cmp ah, bh
    (1)如果sf=1,而of=0 。 of=0说明没有溢出,逻辑上真正结果的正负=实际结果的正负; sf=1,实际结果为负,所以逻辑上真正的结果为负,所以(ah)<(bh)

    (2)如果sf=1,而of=1: of=1,说明有溢出,逻辑上真正结果的正负≠实际结果的正负; sf=1,实际结果为负。
    实际结果为负,而又有溢出,这说明是由于溢出导致了实际结果为负,,如果因为溢出导致了实际结果为负,那么逻辑上真正的结果必然为正。 这样,sf=1,of=1,说明了(ah)>(bh)。

    (3)如果sf=0,而of=1。of=1,说明有溢出,逻辑上真正结果的正负≠实际结果的正负;sf=0,实际结果非负。而of=1说明有溢出,则结果非0,所以,实际结果为正。
    实际结果为正,而又有溢出,这说明是由于溢出导致了实际结果非负,如果因为溢出导致了实际结果为正,那么逻辑上真正的结果必然为负。这样,sf=0,of=1,说明了(ah)<(bh)。
    (4)如果sf=0,而of=0
    of=0,说明没有溢出,逻辑上真正结果的正负=实际结果的正负;sf=0,实际结果非负,所以逻辑上真正的结果非负,所以(ah)≥(bh)。

    4、检测比较结果的条件转移指令

    可以根据某种条件,决定是否修改IP的指令

    jcxz它可以检测cx中的数值,如果(cx)=0,就修改IP,否则什么也不做。

    所有条件转移指令的转移位移都是[-128,127]。

    多数条件转移指令都检测标志寄存器的相关标志位,根据检测的结果来决定是否修改IP

    这些条件转移指令通常都和cmp相配合使用,它们所检测的标志位,都是cmp指令进行无符号数比较的时记录比较结果的标志位

    根据无符号数的比较结果进行转移的条件转移指令(它们检测zf、cf的值)

    指令 含义 检测的相关标志位
    je 等于则转移 zf = 1
    jne 不等于则转移 zf = 0
    jb 低于则转移 cf = 1
    jnb 不低于则转移 cf = 0
    ja 高于则转移 cf = 0 且 zf = 0
    jna 不高于则转移 cf = 1 且 zf = 1

    j:jump,e:equal,b:below,a:above,n:not

    ;编程,统计data段中数值为8的字节的个数,用ax保存统计结果。
    mov ax, data 
    mov ds, ax 
    mov bx, 0   ;ds:bx指向第一个字节
    mov ax, 0   ;初始化累加器mov cx,8
    
    s:
    	cmp byte ptr [bx], 8   ;和8进行比较
    	jne next  ;如果不相等转到next,继续循环
    	inc ax  ;如果相等就将计数值加1
    next:
    	inc bx
    	loop s ;程序执行后:(ax)=3
    

    5、DF标志和串传送指令

    方向标志位。在串处理指令中,控制每次操作后si、di的增减。

    • df = 0每次操作后si、di递增;
    • df = 1每次操作后si、di递减。

    格式:movsb
    功能:将ds:si指向的内存单元中的字节送入es:di中,然后根据标志寄存器df位的值,将si和di递增或递减

    格式:movsw
    功能:将ds:si指向的内存字单元中的字送入es:di中,然后根据标志寄存器df位的值,将si和di递增2或递减2。

    格式:rep movsb
    movsb和movsw进行的是串传送操作中的一个步骤,一般来说,movsb和movsw都和rep配合使用,
    功能:rep的作用是根据cx的值,重复执行后面的串传送指令

    8086CPU提供下面两条指令对df位进行设置。

    • cld指令:将标志寄存器的df位置0
    • std指令:将标志寄存器的df位置1
    ;将data段中的第一个字符串复制到它后面的空间中。
    data segment 
    	db 'Welcome to masm!'
    	db 16 dup (0)
    data ends
    
    mov ax, data 
    mov ds, ax 
    mov si, 0   ;ds:si 指向data:0
    mov es, ax 
    mov di, 16  ;es:di指向data:0010
    
    mov cx, 16  ;(cx)=16,rep循环16次
    cld  ;设置df=0,正向传送
    rep movsb
    

    6、pushf和popf

    pushf的功能是将标志寄存器的值压栈,而popf是从栈中弹出数据,送入标志寄存器中

    pushf和popf,为直接访问标志寄存器提供了一种方法。

    十一、内中断

    1、内中断的产生

    任何一个通用的CPU,都具备一种能力,可以在执行完当前正在执行的指令之后,检测到从CPU外部发送过来的或内部产生的一种特殊信息,并且可以立即对所接收到的信息进行处理。这种特殊的信息,我们可以称其为:中断信息。中断的意思是指,CPU不再接着(刚执行完的指令)向下执行,而是转去处理这个特殊信息。

    中断信息可以来自CPU的内部和外部(内中断,外中断)

    内中断:当CPU的内部有需要处理的事情发生的时候,将产生中断信息,引发中断过程。这种中断信息来自CPU的内部

    8086CPU的内中断(下面四种情况将产生中断信息)

    • 除法错误,比如,执行div指令产生的除法溢出;
    • 单步执行;
    • 执行 into指令;
    • 执行 int指令。

    中断信息中包含中断类型码,中断类型码为一个字节型数据,可以表示256种中断信息的来源(中断源

    上述的4种中断源,在8086CPU中的中断类型码如下。

    • 除法错误:0
    • 单步执行:1
    • 执行into指令:4
    • 执行int指令,该指令的格式为int n,指令中的n为字节型立即数,是提供给CPU的中断类型码。

    2、中断处理程序、中断向量表、中断过程

    中断处理程序

    用来处理中断信息的程序被称为中断处理程序。

    根据CPU的设计,中断类型码的作用就是用来定位中断处理程序。比如CPU根据中断类型码4,就可以找到4号中断的处理程序

    中断向量表

    中断向量就是中断处理程序的入口地址。中断向量表就是中断处理程序入口地址的列表

    CPU用8位的中断类型码通过中断向量表找到相应的中断处理程序的入口地址
    在这里插入图片描述

    中断过程

    中断过程的主要任务就是用中断类型码在中断向量表中找到中断处理程序的入口地址,设置CS和IP

    简要描述如下

    1. 取得中断类型码N;
    2. pushf
    3. TF=0,IF=0 (为什么这样参考单步中断)
    4. push CS , push IP
    5. (IP)=(N * 4),(CS)=(N * 4 + 2)

    硬件在完成中断过程后,CS:IP将指向中断处理程序的入口,CPU开始执行中断处理程序。

    3、iret指令

    CPU随时都可能执行中断处理程序,中断处理程序必须一直存储在内存某段空间之中
    而中断处理程序的入口地址,即中断向量,必须存储在对应的中断向量表表项中。

    中断处理程序的常规编写步骤:

    1. 保存用到的寄存器;
    2. 处理中断;
    3. 恢复用到的寄存器;
    4. iret指令返回。

    iret 指令描述为:pop IP pop CS popf

    iret指令执行后,CPU回到执行中断处理程序前的执行点继续执行程序

    4、除法错误中断的处理

    mov ax, 1000h 
    mov bh, 1
    div bh ;除法溢出错误
    

    1、当CPU执行div bh时,发生了除法溢出错误,产生0号中断信息,从而引发中断过程,

    2、CPU执行0号中断处理程序

    3、系统中的0号中断处理程序的功能:显示提示信息“Divide overflow”后,返回到操作系统中。

    编程实验

    编程:编写0号中断处理程序do0,当发生除法溢出时,在屏幕中间显示“overflow!”,返回DOS。

    1、0000:0200至0000:02FF的256个字节的空间所对应的中断向量表项都是空的,可以将中断处理程序do0传送到内存0000:0200处。

    2、中断处理程序do0放到0000:0200,再将其地址登记在中断向量表对应表项

    • 0号表项的地址0:00:0字单元存放偏移地址,0:2字单元存放段地址
    • 将do0的段地址0存放在0000:0002字单元中,将偏移地址200H存放在0000:0000字单元
    assume cs:code
    
    code segment
    start:	
    		mov ax, cs
    		mov ds, ax
    		mov si, offset do0		;设置ds:si指向源地址
    		mov ax, 0
    		mov es, ax
    		mov di, 200h			;设置es:di指向目的地址0000:0200
    		mov cx, offset do0end - offset do0		;设置cx为传输长度 编译时给出do0部分代码长度
    		cld				        ;设置传输方向为正
    		rep movsb ;将do0的代码送入0:200处
    		
    		mov ax, 0               ;设置中断向量表
    		mov es, ax
    		mov word ptr es:[0*4], 200h
    		mov word ptr es:[0*4+2], 0
    
          	mov ax,4c00h
          	int 21h
    
    ;do0程序的主要任务是显示字符串
    do0:	jmp short do0 start 
          	db "overflow!"
    
    do0start:
          	mov ax, cs
          	mov ds, ax
          	mov si, 202h			;设置ds:si指向字符串
    
          	mov ax, 0b800h
          	mov es, ax
    		mov di, 12*160+36*2		;设置es:di指向显存空间的中间位置
    
            mov cx, 9				;设置cx为字符串长度
    	s:	mov al, [si]
          	mov es:[di], al
          	inc si
          	add di, 1
    		mov al, 02h             ;设置颜色
    		mov es:[di], al        
    		add di, 1
          	loop s
    
          	mov ax, 4c00h
          	int 21h
    do0end:	nop
    
    code ends
    end start
    
    

    5、单步中断

    CPU在执行完一条指令之后,如果检测到标志寄存器的TF位为1,则产生单步中断,引发中断过程。单步中断的中断类型码为1

    Debug是如何利用CPU所提供的单步中断的功能进行调试?如使用t命令查看寄存器状态

    Debug提供了单步中断的中断处理程序,功能为显示所有寄存器中的内容后等待输入命令

    在使用t命令执行指令时,Debug将TF设置为1,在CPU执行完这条指令后就引发单步中断,执行单步中断的中断处理程序,所有寄存器中的内容被显示在屏幕上,并且等待输入命令。

    在进入中断处理程序之前,设置TF=0。从而避免CPU在执行中断处理程序的时候发生单步中断

    6、int指令

    int指令的格式为:int n ,n为中断类型码,它的功能是引发中断过程。

    CPU执行int n指令,相当于引发一个n号中断的中断过程

    在程序中使用int指令调用任何一个中断的中断处理程序(中断例程)

    编写供应用程序调用的中断例程

    实验1

    ;求2 * 3456^2
    assume cs:code
    
    code segment
    
    start: 
         mov ax, 3456 ;(ax)=3456
    ​     int 7ch  ; 调用中断7ch的中断例程,计算ax中的数据的平方
    ​     add ax, ax  
    ​     adc dx, dx  ;存放结果,将结果乘以2
    
    ​     mov ax,4c00h
    ​     int 21h
    code ends
    end start 
    
    ;编程:安装中断7ch的中断例程
    ;功能:求一word型数据的平方。
    ;参数:(ax) = 要计算的数据。
    ;返回值:dx、ax中存放结果的高16位和低16位。
    
    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  ;CPU执行int 7ch指令进入中断例程之前,标志寄存器、当前的CS和IP被压入栈
    		      ;在执行完中断例程后,应该用iret 指令恢复int 7ch执行前的标志寄存器和CS、IP的
    sqrend:	nop
    
    code ends
    end start
    

    实验2

    ;功能:将一个全是字母,以0结尾的字符串,转化为大写。
    ;参数:ds:si指向字符串的首地址。
    ;应用举例:将data段中的字符串转化为大写。
    assume cs:code
    
    data segment
    	db 'conversation',0
    data ends
    
    code segment
    start:  mov ax, data
    		mov ds, ax
    		mov si, 0
    		int 7ch
    		
    		mov ax,4c00h
    		int 21h
    code ends
    end start   
    
    
    assume cs:code
    code segment
    
    start:
    		mov ax,cs
    		mov ds,ax
    		mov si,offset capital
    		mov ax,0
    		mov es,ax
    		mov di,200h
    		mov cx,offset capitalend - offset capital
    		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
    
    capital:
    		push cx
    		push si
    		
    change: 
    		mov cl,[si]
    		mov ch,0
    		jcxz ok
    		and byte ptr [si],11011111b
    		inc si
    		jmp short change
    ok:	
    		pop si
    		pop cx
    		iret
    		
    capitalend:nop
    
    code ends
    
    end start
    
    

    7、BIOS和DOS所提供的中断例程

    在系统板的ROM中存放着一套程序,称为BIOS(基本输入输出系统)

    BIOS中主要包含以下几部分内容

    • 硬件系统的检测和初始化程序;
    • 外部中断和内部中断的中断例程;
    • 用于对硬件设备进行I/O操作的中断例程;
    • 其他和硬件系统相关的中断例程。

    程序员在编程的时候,可以用int 指令直接调用BIOS和DOS系统提供的中断例程,来完成某些工作。
    和硬件设备相关的DOS中断例程中,一般都调用了BIOS的中断例程。

    BIOS和DOS中断例程的安装过程

    BIOS和DOS提供的中断例程是如何安装到内存中的呢?

    1、开机后,CPU一加电,初始化(CS)= 0FFFFH,(IP)= 0,自动从FFFF:0单元开始执行程序。FFFF:0处有一条转跳指令,CPU执行该指令后,转去执行BIOS中的硬件系统检测和初始化程序。

    2、初始化程序将建立BIOS所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表中。
    注意,对于BIOS所提供的中断例程,只需将入口地址登记在中断向量表中即可,因为它们是固化到ROM中的程序,一直在内存中存在。

    3、硬件系统检测和初始化完成后,调用int 19h进行操作系统的引导。从此将计算机交由操作系统控制。

    4、DOS启动后,除完成其他工作外,还将它所提供的中断例程装入内存,并建立相应的中断向量。

    BIOS中断例程应用

    一般来说,一个供程序员调用的中断例程中往往包括多个子程序,中断例程内部用传递进来的参数来决定执行哪一个子程序。

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

    编程:在屏幕的5行12列显示3个红底高亮闪烁绿色的“al。

    assume cs:code 
    
    code segment
    ;int 10h中断例程的"设置光标位置"功能
    mov ah, 2;设置光标调用第10h号中断例程的2号子程序,功能为设置光标位置(可以提供光标所在的行号、列号和页号作为参数)
    
    ;设置光标到第0页,第5行,第12列
    mov bh, 0;第0页
    mov dh, 5;dh中放行号
    mov dl, 12;dl中放列号
    int 10h
    
    ;int10h中断例程的"在光标位置显示字符"功能。
    mov ah,9 ;调用第10h号中断例程的9号子程序,功能为在光标位置显示字符
    ;提供要显示的字符、颜色属性、页号、字符重复个数作为参数
    mov al,'a'  ;字符
    mov b1,11001010b  ;颜色属性
    mov bh,0  ;第0页
    mov cx,3  ;字符重复个数
    int 10h
    
    code ends 
    end
    

    bh中页号的含义:内存地址空间中,B8000H~BFFFFH共32kB的空间,为80*25彩色字符模式的显示缓冲区。
    一屏的内容在显示缓冲区中共占4000个字节。显示缓冲区分为8页,每页4KB(约4000B),显示器可以显示任意一页的内容。一般情况下,显示第0页的内容。也就是说,通常情况下,B8000H~B8F9FH中的4000个字节的内容将出现在显示器上。

    DOS中断例程应用
    int 21h中断例程是DOS提供的中断例程,4ch号功能,即程序返回功能

    mov ah, 4ch ;调用第21h号中断例程的4ch号子程序,功能为程序返回,可以提供返回值作为参数
    mov al, 0 ;返回值
    int 21h
    

    编程:在屏幕的5行12列显示字符串“Welcome to masm!”。

    assume cs:code 
     
    data segment 
    	db	'Welcome to masm',  '$'     ;“$”本身并不显示,只起到边界的作用
    data ends 
    
    code segment
    start:	mov ah, 2 ;10号中断设置光标位置功能
    		mov bh, 0 ;第0页
    		mov dh, 5;dh中放行号
    		mov dl, 12 ;dl中放列号
    		int 10h 
    		
    		mov ax, data 
    		mov ds, ax 
    		mov dx, 0 ;ds:dx指向字符串的首地址data:0  (参数)
    		mov ah, 9 ;调用第21h号中断例程的9号子程序,功能为在光标位置显示字符串,可以提供要显示字符串的地址作为参数
    		int 21h 
    		
    		mov ax, 4c00h ;21号中断程序返回功能
    		int 21h 
    code ends
    end start
    

    十二、端口

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

    • 各种接口卡(比如,网卡、显卡)上的接口芯片,它们控制接口卡进行工作;
    • 主板上的接口芯片,CPU通过它们对部分外设进行访问;
    • 其他芯片,用来存储相关的系统信息,或进行相关的输入输出处理。

    在这些芯片中,都有一组可以由CPU读写的寄存器。这些寄存器,它们在物理上可能处于不同的芯片中,
    但是它们在以下两点上相同。

    • 都和CPU的总线相连,这种连接是通过它们所在的芯片进行的;
    • CPU对它们进行读或写的时候都通过控制线向它们所在的芯片发出端口读写命令。

    从CPU的角度,将这些寄存器都当作端口,对它们进行统一编址,从而建立了一个统一的端口地址空间。
    每一个端口在地址空间中都有一个地址。在访问端口的时候,CPU通过端口地址来定位端口。因为端口所在的芯片和CPU通过总线相连,

    CPU可以直接读写以下3个地方的数据。

    • CPU内部的寄存器;
    • 内存单元;
    • 端口。

    1、端口的读写

    端口地址和内存地址一样,通过地址总线来传送。在PC系统中,CPU最多可以定位64KB个不同的端口。则端口地址的范围为0-65535

    端口的读写指令只有两条:inout,分别用于从端口读取数据和往端口写入数据。

    在in和out指令中,只能使用ax或al来存放从端口中读入的数据或要发送到端口中的数据。

    ;对0~255以内的端口进行读写时:
    in al, 20h  ;从20h端口读入一个字节
    out 20h, al  ;往20h端口写入一个字节
    
    ;对256~65535的端口进行读写时,端口号放在dx中:
    mov dx, 3f8h  ;将端口号3f8h送入dx
    in al, dx  ;从3f8h端口读入一个字节
    out dx, al ;向3f8h端口写入一个字节
    

    2、CMOS RAM芯片

    PC机中,有一个CMOS RAM芯片,一般简称为CMOS。此芯片的特征如下

    • 包含一个实时钟和一个有128个存储单元的RAM存储器
    • 该芯片靠电池供电。关机后内部的实时钟正常工作,RAM中的信息不丢失
    • 128个字节的RAM中,内部实时钟占用0~0dh单元来保存时间信息,其余大部分单元用于保存系统配置信息,供系统启动时BIOS程序读取。BIOS也提供了相关的程序,使我们可以在开机的时候配置CMOS RAM中的系统信息。
    • 该芯片内部有两个端口,端口地址为70h和71h。CPU通过这两个端口来读写CMOS RAM
    • 70h为地址端口,存放要访问的CMOS RAM单元的地址;71h为数据端口,存放从选定的CMOS RAM单元中读取的数据,或要写入到其中的数据。
      可见,CPU对CMOS RAM的读写分两步进行,比如,读CMOS RAM的2号单元:
      ①将2送入端口70h;
      ②从端口71h读出2号单元的内容。

    CMOS RAM中存储的时间信息

    在CMOS RAM中,存放着当前的时间:年、月、日、时、分、秒。长度都为1个字节,
    存放单元为:

    9 8 7 6 5 4 3 2 1 0

    BCD码是以4位二进制数表示十进制数码的编码方法 4 == 0100B

    一个字节可表示两个BCD码。则CMOS RAM存储时间信息的单元中,存储了用两个BCD码表示的两位十进制数,高4位的BCD码表示十位,低4位的BCD码表示个位。比如,00010100b表示14。

    ;编程,在屏幕中间显示当前的月份。
    assume cs:code
    code segment 
    start:	mov al,8 ;从CMOS RAM的8号单元读出当前月份的BCD码。
    		out 70h,al 
    		in al, 71h ;从数据端口71h中取得指定单元中的数据:
    		
    		mov ah, al ;al中为从CMOS RAM的8号单元中读出的数据
    		mov cl, 4
    		shr ah, cl ;ah中为月份的十位数码值,左移四位空出四位
    		and al, 00001111b ;al中为月份的个位数码值
    		
    		add ah, 30h ;BCD码值+30h=十进制数对应的ASCII
    		add al, 30h 
    		
    		mov bx, 0b800h 
    		mov es, bx 
    		mov byte ptr es:[160*12+40*2], ah ;显示月份的十位数码
    		mov byte ptr es:[160*12+40*2+2], al ;接着显示月份的个位数码
    		
    		mov ax,4c00h
    		int 21h
    code ends
    end start
    

    3、shl和shr指令

    shl和shr是逻辑移位指令

    shl是逻辑左移指令,它的功能为:

    1. 将一个寄存器或内存单元中的数据向左移位;
    2. 将最后移出的一位写入CF中;
    3. 最低位用0补充。

    shr是逻辑右移指令,同理

    mov al, 01001000b 
    shl al, 1 ;将a1中的数据左移一位执行后(al)=10010000b,CF=0。
    
    mov al, 01010001b 
    mov cl, 3 ;如果移动位数大于1时,必须将移动位数放在cl中
    shl al, c1
    
    mov al, 10000001b 
    shr al, 1  ;将al中的数据右移一位执行后(al)=01000000b,CF=1。
    

    将X逻辑左移一位,相当于执行X=X*2。
    将X逻辑右移一位,相当于执行X=X/2

    十三、外中断

    1、外中断

    CPU在计算机系统中,除了能够执行指令,进行运算以外,还应该能够对外部设备进行控制,接收它们的输入,向它们进行输出(I/O能力)

    PC系统的接口卡和主板上,装有各种接口芯片。这些外设接口芯片的内部有若干寄存器,CPU将这些寄存器当作端口来访问

    外设的输入不直接送入内存和CPU,而是送入相关的接口芯片的端口中;
    CPU向外设的输出也不是直接送入外设,而是先送入端口中,再由相关的芯片送到外设。
    CPU还可以向外设输出控制命令,而这些控制命令也是先送到相关芯片的端口中,然后再由相关的芯片根据命令对外设实施控制。

    即:CPU通过端口和外部设备进行联系

    当CPU外部有需要处理的事情发生的时候,比如说,外设的输入到达,相关芯片将向CPU发出相应的中断信息。CPU在执行完当前指令后,可以检测到发送过来的中断信息,引发中断过程,处理外设的输入。

    PC系统中,外中断源有两类

    1、可屏蔽中断

    可屏蔽中断是CPU可以不响应的外中断。CPU是否响应可屏蔽中断,要看标志寄存器的IF位的设置。
    当CPU检测到可屏蔽中断信息时,如果IF=1,则CPU在执行完当前指令后响应中断,引发中断过程;如果IF=0,则不响应可屏蔽中断。

    可屏蔽中断信息来自于CPU外部,中断类型码是通过数据总线送入CPU的;而内中断的中断类型码是在CPU内部产生的。

    中断过程中将IF置0的原因就是,在进入中断处理程序后,禁止其他的可屏蔽中断。
    如果在中断处理程序中需要处理可屏蔽中断,可以用指令将IF置1。

    8086CPU提供的设置IF的指令:sti,设置IF=1;cli,设置IF=0。

    2、不可屏蔽中断

    不可屏蔽中断是CPU必须响应的外中断。当CPU检测到不可屏蔽中断信息时,则在执行完当前指令后,立即响应,引发中断过程。

    对于8086CPU,不可屏蔽中断的中断类型码固定为2,所以中断过程中,不需要取中断类型码。则不可屏蔽中断的中断过程为:①标志寄存器入栈,IF=0,TF=0;②CS、IP入栈;③(IP)=(8),(CS)=(0AH)。

    几乎所有由外设引发的外中断,都是可屏蔽中断。当外设有需要处理的事件(比如说键盘输入)发生时,相关芯片向CPU发出可屏蔽中断信息。不可屏蔽中断是在系统中有必须处理的紧急情况发生时用来通知CPU的中断信息。

    2、PC机键盘的处理过程

    键盘中有一个芯片对键盘上的每一个键的开关状态进行扫描。按下一个键时,开关接通,该芯片就产生一个扫描码,扫描码说明了按下的键在键盘上的位置。扫描码被送入主板上的相关接口芯片的寄存器中,该寄存器的端口地址为60h。松开按下的键时,也产生一个扫描码,扫描码说明了松开的键在键盘上的位置。松开按键时产生的扫描码也被送入60h端口中。

    一般将按下一个键时产生的扫描码称为通码,松开一个键产生的扫描码称为断码。

    扫描码长度为一个字节,通码的第7位为0,断码的第7位为1
    即:断码 = 通码 + 80h。比如,g键的通码为22h,断码为a2h

    键盘的输入到达60h端口时,相关的芯片就会向CPU发出中断类型码为9的可屏蔽中断信息。CPU检测到该中断信息后,如果IF=1,则响应中断,引发中断过程,转去执行int 9中断例程。

    在这里插入图片描述

    BIOS提供了int 9中断例程,用来进行基本的键盘输入处理,主要的工作如下:
    (1)读出60h端口中的扫描码;
    (2)如果是字符键的扫描码,将该扫描码和它所对应的字符码(即ASCII码)送入内存中的BIOS键盘缓冲区; 如果是控制键(比如Ctrl)和切换键(比如CapsLock)的扫描码,则将其转变为状态字节写入内存中存储状态字节的单元;
    (3)对键盘系统进行相关的控制,比如说,向相关芯片发出应答信息。

    BIOS键盘缓冲区可以存储15个键盘输入,一个键盘输入用一个字单元存放,高位字节存放扫描码,低位字节存放字符码。

    0040:17单元存储键盘状态字节,该字节记录了控制键和切换键的状态。键盘状态字节各位记录的信息如下。

    0 右shift状态 置1表示按下右shift键
    1 左shift状态 置1表示按下左shift键
    2 Ctrl状态 置1表示按下Ctrl键
    3 Alt状态 置1表示按下Alt键
    4 ScrollLock状态 置1表示Scroll指示灯亮
    5 NumLock状态 置1表示小键盘输入的是数字
    6 CapsLock状态 置1表示输入大写字母
    7 Insert状态 置1表示处于删除态

    编写int 9中断例程

    ;编程:在屏幕中间依次显示“a”~“z”,并可以让人看清。在显示的过程中,按下'Esc'键后,改变显示的颜色。
    
    ;完整功能代码:
    
    assume cs:code
    
    stack segment
    	db 128 dup (0)
    stack ends
    
    data segment
    	dw 0,0
    data ends
    
    code segment
    start:	
    	mov ax,stack
    	mov ss,ax
    	mov sp,128
    	mov ax,data
    	mov ds,ax
    	mov ax,0
    	mov es,ax
    
    	push es:[9*4]
    	pop ds:[0]
    	push es:[9*4+2]
    	pop ds:[2]		;将原来的int 9中断例程的入口地址保存在ds:0、ds:2单元中
    
    	mov word ptr es:[9*4], offset int9
    	mov es:[9*4+2], cs	;在中断向量表中设置新的int 9中断例程的入口地址
    
    ;显示字符串
    	mov ax, 0b800h
    	mov es, ax
    	mov ah, 'a'
    s:	
    	mov  es:[160*12+40*2], ah
    	call delay
    	inc ah
    	cmp ah, 'z'
    	jna s
    	mov ax,0
    	mov es,ax
    
    	push ds:[0]
    	pop es:[9*4]
    	push ds;[2]
    	pop es;[9*4+2]   	;将中断向量表中int 9中断例程的入口恢复为原来的地址
    
    	mov ax,4c00h
    	int 21h
    
    ;将循环延时的程序段写为一个子程序
    delay:	
    	push ax 
    	push dx
    	mov dx, 2000h  ;用两个16位寄存器来存放32位的循环次数
    	mov ax, 0
    s1: 	
    	sub ax, 1
    	sbb dx, 0
    	cmp ax, 0
    	jne s1
    	cmp dx, 0
    	jne s1
    	pop dx
    	pop ax
    	ret
    
    ;------以下为新的int 9中断例程--------------------
    
    int9:	
    	push ax
    	push bx
    	push es
    
    	in al, 60h;从端口60h读出键盘的输入
    
    	pushf ;标志寄存器入栈
    
    	pushf   
    	pop bx
    	and bh,11111100b
    	push bx
    	popf	;TF=0,IF=0
    	
    	call dword ptr ds:[0] 	;对int指令进行模拟,调用原来的int 9中断例程
    
    	cmp al,1
    	jne int9ret
    
    	mov ax,0b800h
    	mov es,ax
    	inc byte ptr es:[160*12+40*2+1]  ;属性增加1,改变颜色
    
    int9ret:
    	pop es
    	pop bx
    	pop ax
    	iret
    
    code ends
    
    end start
    
    

    CPU对外设输入的通常处理方法
    (1)外设的输入送入端口;
    (2)向CPU发出外中断(可屏蔽中断)信息;
    (3)CPU检测到可屏蔽中断信息,如果IF=1,CPU在执行完当前指令后响应中断,执行相应的中断例程;
    (4)可在中断例程中实现对外设输入的处理。

    端口和中断机制,是CPU进行I/O的基础。

    十四、直接定址表

    assume cs:code
    code segment
             a : db 1,2,3,4,5,6,7,8  ;在后面加有“:”的地址标号,只能在代码段中使用,不能在其他段中使用。
             b : dw 0
    start :mov si,offset a
             mov bx,offset b
             mov cx,8
        s : mov al,cs:[si]
             mov ah,0
             add cs:[bx],ax
             inc si
             loop s
             mov ax,4c00h
             int 21h
    code ends
    end start
    
    

    程序中,code、a、b、start、s都是标号。这些标号仅仅表示了内存单元的地址

    描述了单位长度的标号

    assume cs:code
    code segment
              a db 1,2,3,4,5,6,7,8 ;标号a、b后面没有":",因此它们是可以同时描述内存地址和单元长度的标号。
                                   ;标号a,描述了地址code:0,和从这个地址开始,以后的内存单元都是字节单元
              b dw 0               ;标号b描述了地址code:8,和从这个地址开始,以后的内存单元都是字单元。
    start :  mov si,0
              mov cx,8
        s :   mov al,a[si]
              mov ah,0
              add b,ax
              inc si
              loop s
              mov ax,4c00h
              int 21h
    code ends
    end start
    
    

    使用数据标号来描述存储数据的单元的地址和长度。

    assume cs:code,ds:data ;用伪指令assume将标号所在的段和一个段寄存器联系起来(编译器需要)
    data segment          
              a db 1,2,3,4,5,6,7,8
              b dw 0
    data ends
    code segment
    start:  mov ax,data
              mov ds,ax ;真正确定ds寄存器
              mov si,0
              mov cx,8
    s:       mov al,a[si] ;编译为:mov al,[si+0] 默认所访问单元的段地址在ds
              mov ah,0
              add b,ax ;编译为:add [8],ax
              inc si
              loop s
              mov ax,4c00h
              int 21h
    code ends
    end start
    
    data segment
    	a db 1,2,3,4,5,6,7,8
    	b dw 0
    	c dw a, b ;等价于c dw offset a, offset b
    	;数据标号c处存储的两个字型数据为标号a、b 的偏移地址
    data ends
    
    data segment
    	a db 1,2,3,4,5,6,7,8
    	b dw 0
    	c dd a,b ;等价于c dw offset a, seg a, offset b, seg b
    	;数据标号c处存储的两个双字型数据为标号a的偏移地址和段地址、标号b 的偏移地址和段地址
    data ends
    
    

    seg操作符,功能为取得某一标号的段地址

    建立一张表,表中依次存储字符“0”~“F”,我们可以通过数值0 ~ 15直接查找到对应的字符

    assume cs:code
    
    code segment
    start:  
    		mov al,0eh
    
            call showbyte
    
            mov ax,4c00h
            int 21h
    
    ;子程序:
    ;用al传送要显示的数据
    
    showbyte:
            jmp short show
    
            table db '0123456789ABCDEF'	;字符表
    
    show:   push bx
            push es
    
            mov ah,al
            shr ah,1           
            shr ah,1
            shr ah,1
            shr ah,1			    ;右移4位,ah中得到高4位的值
            and al,00001111b		;al中为低4位的值
    
            mov bl,ah
            mov bh,0
            mov ah,table[bx]		;用高4位的值作为相对于table的偏移,取得对应的字符
    
            mov bx,0b800h
            mov es,bx
            mov es:[160*12+40*2],ah
    
            mov bl,al
            mov bh,0
            mov al,table[bx]		;用低4位的值作为相对于table的偏移,取得对应的字符
            
            mov es:[160*12+40*2+2],al
    
            pop es
            pop bx
            ret
    
    code ends
    end start
    
    

    十五、 指令系统总结

    我们对8086CPU的指令系统进行一下总结。读者若要详细了解8086指令系统中的各个指令的用,可以查看有关的指令手册。

    8086CPU提供以下几大类指令。

    1. 数据传送指令
      mov、push、pop、pushf、popf、xchg 等都是数据传送指令,这些指令实现寄存器和内存、寄器和寄存器之间的单个数据传送。
    2. 算术运算指令
      add、sub、adc、sbb、inc、dec、cmp、imul、idiv、aaa等都是算术运算指令,这些指令实现存器和内存中的数据的算数运算。它们的执行结果影响标志寄存器的sf、zf、of、cf、pf、af位。
    3. 逻辑指令
      and、or、not、xor、test、shl、shr、sal、sar、rol、ror、rcl、rcr等都是逻辑指令。除了not指外,它们的执行结果都影响标志寄存器的相关标志位。
    4. 转移指令
      可以修改IP,或同时修改CS和IP的指令统称为转移指令。转移指令分为以下几类。
      (1)无条件转移指令,比如,jmp
      (2)条件转移指令,比如,jcxz、je、jb、ja、jnb、jna等;
      (3)循环指令,比如,loop
      (4)过程,比如,call、ret、retf
      (5)中断,比如,int、iret
    5. 处理机控制指令
      对标志寄存器或其他处理机状态进行设置,cld、std、cli、sti、nop、clc、cmc、stc、hlt、wait、esc、lock等都是处理机控制指令。
    6. 串处理指令
      对内存中的批量数据进行处理,movsb、movsw、cmps、scas、lods、stos等。若要使用这些指令方便地进行批量数据的处理,则需要和rep、repe、repne 等前缀指令配合使用。

    文中大部分的图片来自王爽《汇编语言》有些图片来自刘宏伟·计算机组成原理课件和王道考研计算机组成原理
    博主靠这本书入门汇编,只是匆匆看了一遍,很多地方理解片面甚至错误,将来发现一定修正


    展开全文
  • 汇编指令入门级整理

    千次阅读 多人点赞 2020-05-10 20:49:34
    我们大都是被高级语言惯坏了的一代,源源不断的新特性正在逐步添加到各类高级语言之中,汇编作为最接近机器指令的低级语言,已经很少被直接拿来写程序了,不过我还真的遇到了一个,那是之前的一个同事,因为在写代码...

    前言

    我们大都是被高级语言惯坏了的一代,源源不断的新特性正在逐步添加到各类高级语言之中,汇编作为最接近机器指令的低级语言,已经很少被直接拿来写程序了,不过我还真的遇到了一个,那是之前的一个同事,因为在写代码时遇到了成员函数权限及可见性的问题,导致他无法正确调用想执行的函数,结果他就开始在 C++ 代码里嵌入汇编了,绕过了种种限制终于如愿以偿,但是读代码的我们傻眼了…

    因为项目是跨平台的,代码推送的 Linux 上编译的时候他才发现,汇编代码的语法在 Linux 和 Windows 上居然是不一样的,结果他又用一个判断平台的宏定义“完美”的解决了,最终这些代码肯定是重写了啊,因为可读性太差了,最近在学习左值、右值、左引用和右引用的时候,总是有人用程序编译生成的中间汇编代码来解释问题,看得我迷迷糊糊,所以决定熟悉一下简单的汇编指令,边学习边记录,方便今后忘记了可以直接拿来复习。

    什么是汇编语言

    汇编语言是最接近机器语言的编程语言,引用百科中的一段话解释为:

    汇编语言(assembly language)是一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。在汇编语言中,用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址。汇编语言又被称为第二代计算机语言。

    汇编语言产生的原因

    对于绝大多数人来说,二进制程序是不可读的,当然有能人可以读,比如第一代程序员,但这类人快灭绝了,直接看二进制不容易看出来究竟做了什么事情,比如最简单的加法指令二进制表示为 00000011,如果它混在一大串01字符串中就很难把它找出来,所以汇编语言主要就是为了解决二进制编码的可读性问题。

    汇编与二进制的关系

    换句话来说,汇编语言就是把给机器看的二进制编码翻译成人话,汇编指令是机器指令的助记符,与机器指令是一一对应的关系,是一种便于阅读和记忆的书写格式。有效地解决了机器指令编写程序难度大的问题,并且使用编译器,可以很方便的把汇编程序转译成机器指令程序,比如之前提到的 00000011 加法指令,对应的汇编指令是 ADD,在调用汇编器时就会把 ADD 翻译成 00000011

    寄存器

    说到汇编指令不得不提到寄存器,寄存器本身是用来存数据的,因为 CPU 本身只负责逻辑运算,数据需要单独储存在其他的地方,但是对于不熟悉寄存器的人来说会有疑惑,数据不是存在硬盘上吗?或者说数据不是存在内存中吗?这些想法都没错,那么寄存器是用来做什么的呢?

    寄存器作用

    其实硬盘、内存都是用来存储数据的,但是 CPU 的运算速度远高于内存的读写速度,更不用说从硬盘上取数据了,所以为了避免被拖慢速度影响效率,CPU 都自带一级缓存和二级缓存,一些 CPU 甚至增加了三级缓存,从这些缓存中读写数据要比内存快很多,但是还是无法使用飞速运转的 CPU,所以才会有寄存器的存在。

    寄存器不是后来增加的,在最初的计算中就已经设计出来,相比而言,多级缓存出现的更晚一些,通常那些最频繁读写的数据都会被放在寄存器里面,CPU 优先读写寄存器,再通过寄存器、缓存跟内存来交换数据,达到缓冲的目的,因为可以通过名称访问寄存器,这样访问速度是最快的,因此也被称为零级缓存。

    存取速度比较

    通过上面的叙述我们可以知道存取速度从高到低分别是: 寄存器 > 1级缓存 > 2级缓存 > 3级缓存 > 内存 > 硬盘,关于它们的存取速度,举个例子很容易就能明白了,比如我们做菜(CPU工作)时,取手中(寄存器)正拿着的肉和蔬菜肯定是最快的,如果没有就需要把案板上(1级缓存)处理好的菜拿过来,如果案板上没有就在更远一点的洗菜池(2级缓存)中找一找,还没找到的话就要到冰箱(3级缓存)中看一看了,这时发现家里真没有,那去楼下的菜店(内存)去买点吧,转了一圈发现没有想要的,最后还是开车去农贸市场(硬盘)买吧。

    通过上面这个例子应该能明白它们的速度关系了,既然缓存这么快,为什么不用缓存代替内存,或者将2、3级缓存都换成1级缓存呢?这里边有一个成本问题,速度越快对应着价格越高,如果你买过机械硬盘和固态硬盘应该很容易就理解了。

    寄存器分类

    常用的 x86 CPU 寄存器有8个:EAXEBXECXEDXEDIESIEBPESP,据说现在寄存器总数已经超过100个了,等我找到相关资料再来补充,上面这几个寄存器是最常用的,这些名字也常常出现在汇编的代码中。

    我们常说的32位、64位 CPU 是指数据总线的宽度或根数,而寄存器是暂存数据和中间结果的单元,因此寄存器的位数也就是处理数据的长度与数据总线的根数是相同的,所以32位 CPU 对应的寄存器也应该是32位的。

    常用寄存器用途

    上面提到大8个寄存器都有其特定的用途,我们以32位 CPU 为例简单说明下这些寄存器的作用,整理如下表:

    寄存器 含义 用途 包含寄存器
    EAX 累加(Accumulator)寄存器 常用于乘、除法和函数返回值 AX(AH、AL)
    EBX 基址(Base)寄存器 常做内存数据的指针, 或者说常以它为基址来访问内存. BX(BH、BL)
    ECX 计数器(Counter)寄存器 常做字符串和循环操作中的计数器 CX(CH、CL)
    EDX 数据(Data)寄存器 常用于乘、除法和 I/O 指针 DX(DH、DL)
    ESI 来源索引(Source Index)寄存器 常做内存数据指针和源字符串指针 SI
    EDI 目的索引(Destination Index)寄存器 常做内存数据指针和目的字符串指针 DI
    ESP 堆栈指针(Stack Point)寄存器 只做堆栈的栈顶指针; 不能用于算术运算与数据传送 SP
    EBP 基址指针(Base Point)寄存器 只做堆栈指针, 可以访问堆栈内任意地址, 经常用于中转 ESP 中的数据, 也常以它为基址来访问堆栈; 不能用于算术运算与数据传送 BP

    寄存器EAX、AX、AH、AL的关系

    在上面的图标中每个常用寄存器后面还有其他的名字,它们是同一个寄存器不同用法下的不同名字,比如在32位 CPU 上,EAX是32位的寄存器,而AX是EAX的低16位,AH是AX的高8位,而AL是AX的低8位,它们的对照关系如下:

    00000000 00000000 00000000 00000000
    |===============EAX===============|---4个字节
                      |======AX=======|---2个字节
                      |==AH===|-----------1个字节
                              |===AL==|---1个字节
    

    汇编语言指令

    终于说到汇编常用指令了,因为 linuxwindows 下的汇编语法是有些不同的,所以下面我们先通过 windows 下的汇编指令来简单学习一下,后续再来比较两者的不同。

    数据传送指令

    指令 名称 示例 备注
    MOV 传送指令 MOV dest, src 将数据从src移动到dest
    PUSH 进栈指令 PUSH src 把源操作数src压入堆栈
    POP 出栈指令 POP desc 从栈顶弹出字数据到dest

    算术运算指令

    指令 名称 示例 备注
    ADD 加法指令 ADD dest, src 在dest基础上加src
    SUB 减法指令 SUB dest, src 在dest基础上减src
    INC 加1指令 INC dest 在dest基础上加1
    DEC 减1指令 DEC dest 在dest基础上减1

    逻辑运算指令

    指令 名称 示例 备注
    NOT 取反运算指令 NOT dest 把操作数dest按位取反
    AND 与运算指令 AND dest, src 把dest和src进行与运算之后送回dest
    OR 或运算指令 OR dest, src 把dest和src进行或运算之后送回dest
    XOR 异或运算 XOR dest, src 把dest和src进行异或运算之后送回dest

    循环控制指令

    指令 名称 示例 备注
    LOOP 计数循环指令 LOOP label 使ECX的值减1,当ECX的值不为0的时候跳转至label,否则执行LOOP之后的语句

    转移指令

    指令 名称 示例 备注
    JMP 无条件转移指令 JMP lable 无条件地转移到标号为label的位置
    CALL 过程调用指令 CALL labal 直接调用label
    JE 条件转移指令 JE lable zf =1 时跳转到标号为label的位置
    JNE 条件转移指令 JNE lable zf=0 时跳转到标号为label的位置

    linux 和 windows 下汇编的区别

    前面说到 linuxwindows 下的汇编语法是不同的,其实两种语法的不同和系统不同没有绝对的关系,一般在 linux 上会使用 gcc/g++ 编译器,而在 windows 上会使用微软的 cl 也就是 MSBUILD,所以产生不同的代码是因为编译器不同,gcc 下采用的是AT&T的汇编语法格式,MSBUILD 采用的是Intel汇编语法格式。

    差异 Intel AT&T
    引用寄存器名字 eax %eax
    赋值操作数顺序 mov dest, src movl src, dest
    寄存器、立即数指令前缀 mov ebx, 0xd00d movl $0xd00d, %ebx
    寄存器间接寻址 [eax] (%eax)
    数据类型大小 操作码后加后缀字母,“l” 32位,“w” 16位,“b” 8位(mov dx, word ptr [eax]) 操作数前面加dword ptr, word ptr,byte ptr的格式 (movb %bl %al)

    总结

    • 汇编指令是机器指令的助记符,与机器指令是一一对应的
    • AT&T的汇编语法格式和Intel汇编语法格式的是不同的
    • 常用寄存器:EAXEBXECXEDXEDIESIEBPESP
    • 存取速度从高到低分别是: 寄存器 > 1级缓存 > 2级缓存 > 3级缓存 > 内存 > 硬盘
    • 常用的汇编指令:movjejmpcalladdsubincdecandor

    如今的每分每秒都是人生,不要总想着将自然发生的事情拖到预定的时刻才进行~

    展开全文
  • 汇编语言入门教程

    千人学习 2019-12-26 10:57:49
    汇编语言入门级教程 游戏安全,游戏逆向必学的基础语言 
  • 本课程分析Linux0.11内核源码的汇编启动部分,并讲解涉及到的软硬件等相关知识,包括x86的保护模式,分页机制。
  • C与汇编语言

    千次阅读 2019-06-19 14:32:28
    一、汇编指令 二、汇编器和链接器 三、 汇编语法 四、C中嵌入汇编代码 五、GDB反汇编 1、x86_64通用寄存器 2、 调用栈 3、导出汇编代码 4、反汇编调试详解 5、结构体反汇编 一、汇编指令 汇编指令是指...

    目录

    一、汇编指令

    二、汇编器和链接器

    三、 汇编语法

    四、C中嵌入汇编代码

    五、GDB反汇编

    1、x86_64通用寄存器

    2、 调用栈

    3、导出汇编代码

    4、反汇编调试详解

    5、结构体反汇编


    一、汇编指令

           汇编指令是指特定CPU架构的指令码的助记符,比如Intel x86 32位下ADD指令对应的16进制机器码有04/05/80/81等,被操作对象不同同一个指令对应的指令码不同,操作对象通常是寄存器,内存地址,I/O端口等,具体操作对象通过指令码后面的一个或者多个的1字节辅助操作码指定,通过objdump命令将生成的可执行程序进行了反汇编可查看汇编指令对应的指令码。

           参考:简单学习看机器码的方法

                     Intel汇编指令格式解析

                     x86指令格式

    二、汇编器和链接器

           汇编器(assembler)的作用是将用汇编语言编写的源程序转换成二进制形式的目标代码。Linux 平台的标准汇编器是 GAS, 使用标准的 AT&T 汇编语法它是 GCC 所依赖的后台汇编工具,通常包含在 binutils 软件包中,对应的命令是as。Linux 平台上另一个经常用到的汇编器是 NASM,与GAS最大的不同是,NASM使用Intel 汇编语法,除此之外NASM和GAS都有自己特定的汇编器指令和宏结构。MSAM是Intel平台上所有汇编器的鼻祖,微软的VS中自带该汇编器,同样使用Intel汇编语法。

          汇编器生成的目标代码只局限于单个源代码文件,对源代码中依赖的来自其他源代码文件中的全局变量或者函数未做解析,这时需要链接器,将这些未解析的符号翻译成实际的内存地址,最终将存在依赖关系的多个目标代码文件合并成一个可执行文件,GCC使用的链接器是ld。

           参考:Java程序员自我修养——编译过程和ELF文件

                     Linux 汇编器:对比 GAS 和 NASM

    三、 汇编语法

          汇编语法与汇编器的关系,类似于C/C++标准与C/C++编译器的关系,主要有两种语法,Intel 和AT&T 汇编语法,最大的区别是后者是与平台无关的,前者只局限于Inter平台。特定CPU架构下汇编指令集和寄存器名称相当于汇编语法中的关键字,支持多平台的GAS汇编器会根据当前系统的CPU架构自动识别并转换成对应平台下的机器码。

         参考:Linux 汇编语言开发指南

    四、C中嵌入汇编代码

         C标准中并未规定如何嵌入汇编代码,所以嵌入汇编代码的语法只适用于特定的编译器,下面主要介绍GCC中的内联汇编,其格式为:__asm__ __volatile__("Instruction List" : Output : Input : Clobber/Modify)。

    1、__asm__和__volatile__宏

        __asm__可简写为asm,用于标识一段汇编指令,__volatile__可简写为volatile,用于告诉编译器不需要对asm中的汇编指令做优化,原封不动的保留即可。

    2、Instruction List

       Instruction List是指汇编指令列表,可以为空,多条指令时,通常一条指令一行,以分号";"或者换行符"\n"表示结束,并用双引号包住,注意指令中使用寄存器时前面必须加两个%。

    3、Output

      Output指定汇编语句的输出,即将寄存器中的值存入到哪个变量中,如"=a"(a),表示将a对应寄存器的值赋值给变量a,a是寄存器eax/ax /al的简写,编译器根据汇编指令自动推测是eax还是ax。

    4、Input

      Input指定汇编语句的执行参数,可以是一个常量,也可以是一个复杂的表达式,执行参数可以通过占位符或者寄存器的方式传入,占位符按照Input/Output表达式出现的顺序从0开始编号,最多十个表达式,编号最大9。

    5、操作数约束

      Output表达式"=a"(a) 中a就是操作数约束中的寄存器约束,其他约束如下,其中I O表示Input和Output适用:

    • r I,O 表示使用一个通用寄存器,由GCC在%eax/%ax/%al, %ebx/%bx/%bl, %ecx/%cx/%cl, %edx/%dx/%dl中选取一个GCC认为合适的。 
    • q I,O 表示使用一个通用寄存器,和r的意义相同。 
    • a I,O 表示使用%eax / %ax / %al 
    • b I,O 表示使用%ebx / %bx / %bl 
    • c I,O 表示使用%ecx / %cx / %cl 
    • d I,O 表示使用%edx / %dx / %dl 
    • D I,O 表示使用%edi / %di 
    • S I,O 表示使用%esi / %si 
    • f I,O 表示使用浮点寄存器 
    • t I,O 表示使用第一个浮点寄存器 
    • u I,O 表示使用第二个浮点寄存器
    • m I,O 表示使用系统所支持的任何一种内存方式,不需要借助寄存器
    • i I 表示输入表达式是一个立即数(整数),不需要借助任何寄存器 
    • F I 表示输入表达式是一个立即数(浮点数),不需要借助任何寄存器
    • g I,O 表示可以使用通用寄存器,内存,立即数等任何一种处理方式。

     测试用例:

    #include <stdio.h>
    
    int main()
    {
        int result = 0;
        int input = 1;
    
        int a = 1;
        int b = 2;
    
        asm volatile (
            "movl %1, %0\n"
            : "=r"(result)
            : "r"(input)
            );
    
        printf("result = %d\n", result);
        printf("input = %d\n", input);
    
        asm volatile (
            "movl %%eax, %%ecx;"
            "movl %%ebx, %%eax\n"
            "movl %%ecx, %%ebx;"
            : "=a"(a), "=b"(b)
            : "a"(4), "b"(5)
            );
    
        printf("a = %d\n", a);
        printf("b = %d\n", b);
    
        return 0;
    }
    

       参考:__asm__ volatile 之 C语言嵌入式汇编

                 《汇编语言程序设计》

    五、GDB反汇编

        可通过 gcc -S test.c -o test.s 或者objdump -S test.o > test.s查看生成的汇编代码,但是同源代码的对应关系不够直观而且有很多无关的汇编代码,最理想的是通过gdb反汇编命令disassemble查看每行源代码对应的汇编代码,也可通过ni/si命令单步调试汇编指令,查看执行汇编时寄存器和堆栈的状态。

    1、x86_64通用寄存器

          X86-64中,所有寄存器都是64位,相对32位的x86来说,标识符变了,比如从原来的ebp变成了rbp,eax变成了rax。为了向后兼容性,ebp依然可以使用,不过指向了rbp的低32位,即汇编代码中保存到ebp实际是保存到rbp的低32位中。除此之外,新增加寄存器r8到r15,加上x86的原有8个,一共16个通用寄存器,具体用途如下:

    • %rax 通常用于存储函数调用的返回结果,同时也用于乘法和除法指令中。
    • %rsp 是堆栈指针寄存器,通常会指向栈顶位置
    • %rbp 是栈帧指针寄存器,用于标识当前栈帧的起始位置
    • %rdi,%rsi,%rdx,%rcx,%r8,%r9 用来传递函数参数,依次对应第1参数,第2参数至第6参数
    • %rbx,%r12,%r13,%14,%15 ,%r10,%r11 用作数据存储,属于通用性更为广泛的寄存器,编译器或汇编程序可以根据需要存储任何数据。

    2、 调用栈

          调用栈按照进程所处的状态分为用户栈和内核栈,这两个栈都是操作系统分配给进程的一片连续内存空间,64位下通常是10k,可通过ulimit -s查看,由操作系统负责维护这两个调用栈的栈顶地址和当前栈帧地址,进程切换时由操作系统负责将当前执行进程的调用栈的栈顶地址放到rbp寄存器中,上一次执行时的当前栈帧地址放到rsp寄存器中,从而使CPU恢复到上一次执行时的调用栈状态。

         每次进入到一个新方法,就产生一个新的栈帧,这时需要保存调用方的rbp栈帧指针和方法结束后下一条执行指令的地址,即rip指令寄存器中的指令地址,将这两个压入栈,然后将调用方rsp栈顶指针复制到rbp中,表示栈帧指针往下移动到了原来的栈顶指针处,如果新方法中有调用其他的方法,rsp指针也会从高地址往低地址往下移动足够的空间,方便新方法在该空间中初始化方法内的局部变量,如果不调用其他方法则不需要移动rsp栈顶指针。新方法执行完毕后把rbp保存的调用方的rsp堆栈指针恢复到rsp中,把调用栈中保存的调用方的rbp栈帧指针和rip指令弹出,恢复到rbp寄存器和rip指令寄存器中,即完成了方法调用,如下图:

     下面结合测试代码和gdb反汇编代码具体说明。

    3、导出汇编代码

        测试代码如下,保存为asm2.c: 

    #include <stdio.h>
    
    int outer=12;
    
    int add(int a,int b);
    
    int mult(int a,int b);
    
    int main(){
       int a=123;
       int b=456;
       int c=add(a,a+b);
       int d=234;
       c=mult(c,d);
       printf("c=%d\n",c);
    }
    
    int add(int a,int b){
    	int c=mult(a,b);
    	for(int i=0;i<3;i++){
    	  c+=outer;
    	}
    	return c;
    }
    
    int mult(int a,int b){
    	return (a+b)*2;
    }
    

     1、执行sudo gcc -g -std=c99 asm2.c -o asm2.o,生成可执行文件asm2.o,因为for循环中初始化变量i是从C99开始支持的,如果gcc版本较低需要指定C标准版本。

     2、执行gdb -q ./asm2.o |tee asm2.s 启动gdb,-q表示不输出gdb的版本信息等,tee命令用于将gdb的输出保存到指定文件asm2.s。

    3、执行disassemble main 输出main方法的汇编代码,如下图:

    4、执行disassemble /m main 输出main方法的汇编代码和对应的源码,disassemble 还有一个/r选项,这个是默认值,和不带的效果一样。如下图:

    5、适当编辑asm2.s文件,去掉多余的输出,就可以得到可读性很好的汇编代码文件了

    4、反汇编调试详解

     测试代码同上,反汇编调试步骤如下: 

    1、执行 sudo gcc -g asm2.c -o asm2 -std=c99 生成可执行文件,

    2、执行gdb -q -tui ./asm2 启动gdb,-tui表示启动用于显示源码和汇编代码等的文本窗口,也可用gdbtui -q ./asm2替代。

    3、执行layout split:显示源代码和汇编窗口,主要是为了查看执行main方法前的汇编指令,因为断点调试只能从int a=123; 这行代码开始,之前的汇编指令在开始断点调试后看不到。

    push   %rbp,将rbp寄存器保存的栈帧指针压入栈,栈帧指针用于标识当前栈帧的起始位置

    mov    %rsp,%rbp,将rsp寄存器保存的堆栈指针复制到rbp寄存器,堆栈指针指向栈顶位置,因为栈帧是从上往下,从高地址往低地址生长的,栈顶是指生长方向上的栈顶,所以栈顶地址是整个堆栈的最低地址。

    sub    $0x10,%rsp,将rsp寄存器中的堆栈指针地址减去16字节

    4、执行layout regs,显示寄存器窗口,可直接查看所有寄存器的值,layout src显示源码窗口,查看当前执行的源码,如果寄存器窗口没了,再执行layout regs

    5、执行show disassembly-flavor 查看显示的汇编语法类型,默认是att,即AT&T 汇编语法,也可通过set disassembly-flavor intel设置成Intel汇编语法。

    6、执行set disassemble-next-line on,当程序暂停时反汇编下一行代码,默认是off

    7、执行start, gdb自动停在main方法的第一行代码上,如下图:

    寄存器窗口中,第一列是寄存器的名称,第二列是寄存器中的值的16进制,第三列是对应的10进制整数,rbp和rsp保存的都是内存地址,所以第二列和第三列相同都是16进制形式的,两者相差16字节。

    movl   $0x7b,-0x10(%rbp) ,将堆栈指针减去16字节的起始地址之后的4字节内存的值初始化为0x7b,即123

    8、执行ni可查看下一步的汇编代码

    movl   $0x1c8,-0xc(%rbp)  ,将堆栈指针减去12字节的起始地址之后的4字节内存的值初始化为0x1c8,即456

    9,、执行到int c=add(a,a+b);时,汇编代码会显示有多行:

    因为多行代码未显示完所以出现最下面的一行---Type <return> to continue, or q <return> to quit---,点击回车往下查看未显示的部分,全部显示完自动退出,也可输入q手动退出,然后执行ni继续下一步调试,当执行到callq的时候可以,执行si可以进入到这个方法的汇编代码执行调试

    mov    -0xc(%rbp),%eax  因为eax保存32位即4字节的数据,这里将堆栈指针减去8个字节的起始地址往后的4个字节的数据,即456复制到eax寄存器中,64位下实际是rax,汇编代码使用eax主要是告诉CPU操作的字节数

    mov    -0x10(%rbp),%edx,将堆栈指针减去16个字节的起始地址往后的4个字节的数据,即123复制到edx寄存器中,64位下实际是rdx。

    此时可通过info registers查看当前所有寄存器的值,也可通过寄存器窗口查看:

    add    %eax,%edx,将eax中的值加到edx中的值上,结果保存到edx中,64位下是rdx中,如下:

    mov    -0x10(%rbp),%eax 将堆栈指针减去16个字节的起始地址往后的4个字节的数据,即123复制到eax寄存器中,实际是rax

    mov    %edx,%esi 将edx的值即579复制到esi寄存器,实际是rsi寄存器,保存函数的第二个参数

    mov    %eax,%edi 将eax的值即123复制到edi寄存器,实际是rdi寄存器,保存函数的第一个参数

    此时寄存器状态:

    10、注意此时rbp的变化,进入前rbp是0x7fffffffe4a0,rsp是0x7fffffffe490,执行si 进入到add方法的汇编代码调试,进入后rbp的值并未改变,rsp变成0x7fffffffe488,即往下移动了8个字节,这是call指令保存的方法调用结束后下一条执行的指令的地址,即保存rip指令寄存器中的数据。

    push   %rbp  把rbp的栈帧指针压入栈中,rbp不变,rsp变成0x7fffffffe480,即又往下移动了8字节,此时栈顶指针保存在rsp上方的8个字节中,此时rbp下方的8字节还是空白的

    mov    %rsp,%rbp  把rsp的栈顶指针复制到rbp中,即栈帧指针往下移动到原来的栈顶指针处,此时rbp变成0x7fffffffe480

    sub    $0x20,%rsp  把rsp的栈顶指针减去20字节,即栈顶指针往下移动32个字节,此时rsp变成0x7fffffffe460

    mov    %edi,-0x14(%rbp) 把edi中的函数参数拷贝到rbp栈帧指针减去20个字节的起始地址之后的4个字节,即123

    mov    %esi,-0x18(%rbp) 把esi中的函数参数拷贝到rbp栈帧指针减去24个字节的起始地址之后的4个字节,即579

    mov    -0x18(%rbp),%edx  把rbp栈帧指针减去24个字节的起始地址之后的4个字节的数据拷贝到edx,64位下是rdx

    mov    -0x14(%rbp),%eax 把rbp栈帧指针减去20个字节的起始地址之后的4个字节的数据拷贝到eax,64位下是rax

    mov    %edx,%esi  把edx寄存器中的数据拷贝到esi中,作为函数的第二个参数

    mov    %eax,%edi 把eax寄存器中的数据拷贝到edi中,作为函数的第一个参数

    上述四条指令实际是无意义的,如果开启编译器优化,这四条指令可能就没了。

    11、执行si 进入到mult方法的汇编代码调试,rbp不变,rsp变成0x7fffffffe458,即往下移动了8个字节,用于保存方法调用结束后下一条执行指令的地址。

    push   %rbp,把rbp的栈帧指针压入栈中,rbp不变,rsp变成0x7fffffffe450,即往下移动了8个字节,用于保存rbp栈帧指针

    mov    %rsp,%rbp 把rsp的栈顶指针拷贝到rbp中,rbp和rsp都变成0x7fffffffe450

    mov    %edi,-0x4(%rbp)  把edi的值拷贝到rbp栈帧指针减去4字节的起始地址之后的4字节,即123

    mov    %esi,-0x8(%rbp)  把edi的值拷贝到rbp栈帧指针减去8字节的起始地址之后的4字节,即579

    mov    -0x8(%rbp),%eax  把rbp栈帧指针减去8字节的起始地址之后的4字节的数据拷贝到eax中,即579

    mov    -0x4(%rbp),%edx  把rbp栈帧指针减去4字节的起始地址之后的4字节的数据拷贝到edx中,即123

    lea    (%rdx,%rax,1),%eax , 把rdx的值加上rax的值,结果放到eax中

    add    %eax,%eax   把eax的值加上eax的值
    leaveq  相当于两条指令mov %rbp, %rsp和pop %rbp,即把rbp的值复制到rsp中,即0x7fffffffe450,然后把rbp的地址弹出放到rbp中,rsp自动加上保存rbp的8个字节,rbp的值为0x7fffffffe480,rsp的值为0x7fffffffe458

    retq  相当于popq %rip,即把调用方法结束后下一条执行的指令的地址弹出并放到rip寄存中,rsp自动加上保存rip的8字节,rsp的值变成0x7fffffffe460,rbp变成0x7fffffffe480,即恢复到调用mult方法前的状态,在mult方法执行过程中分配的两个4字节可以被重新分配给其他方法,相当于自动释放掉了。注意,因为mult方法是最下面的一个栈帧,所以没有往下移动rsp栈顶指针,方法执行期间rsp和rbp指针是一样的,此时栈帧状态如下:

    12、mult方法执行完成,继续ni

    mov    %eax,-0x8(%rbp) 把mult方法调用结果从eax拷贝到rbp栈帧指针减去8个字节的起始地址之后的4字节中,即1404

    movl   $0x0,-0x4(%rbp)  把rbp栈帧指针减去4个字节的起始地址之后的4字节内存初始化为0,即初始化for循环的变量i

    jmp    0x40055e  跳转到执行i<3比较的指令,如下图:

    cmpl   $0x2,-0x4(%rbp) cmp比较指令,实际是减法,用2减去rbp栈帧指针减去4字节的起始地址之后的4字节数据,即变量i,只是结果不保存,只影响CPU内部的标志位,其他指令根据标志位判断比较结果

    jle    0x400551 jle是转移跳转指令,如果cmpl指令返回true,则跳转到0x400551处的指令,否则继续执行下一条指令,如下图:

    mov    0x20040d(%rip),%eax 把rip指令地址加上0x20040d字节的起始地址之后的4字节数据拷贝到eax寄存器,即全局变量outer拷贝到eax中,全局变量是在单独的.data段中保存,在程序加载时初始化,这里使用相对地址获取该变量的值。

    add    %eax,-0x8(%rbp)  把rbp栈帧地址减去8个字节的起始地址之后的4字节的数据同eax中的值相加,即c变量加上outer,结果保存到c变量中。

    addl   $0x1,-0x4(%rbp)  把变量i加上1

    cmpl   $0x2,-0x4(%rbp) 比较变量i和2

    jle    0x400551 跳转到c+=outer,后面的就是重复的,直到cmpl比较返回-1,jre就跳到jre下面的即for循环结束后的一条指令,如下图:

    mov    -0x8(%rbp),%eax    将变量c的值拷贝到eax中

    leaveq  同上,恢复rbp

    retq  同上,恢复rip,至此add 方法执行完毕,此时栈帧状态如下:

    13、add方法执行完成,继续ni:

    mov    %eax,-0x8(%rbp)  把eax中的值拷贝到rbp栈帧指针减去8字节的起始地址之后的4字节,即变量c的初始化

    movl   $0xea,-0x4(%rbp)  把rbp栈帧指针减去4字节的起始地址之后的4字节初始化为0xea,即变量d初始化

    mov    -0x4(%rbp),%edx 把变量d复制到edx中

    mov    -0x8(%rbp),%eax 把变量c复制到eax中

    mov    %edx,%esi 把edx中的值复制到esi中,作为函数的第二个参数

    mov    %eax,%edi 把eax中的值复制到edi中,作为函数的第一个参数

    callq  0x400569    <mult> 调用mult方法,执行指令同add方法调用mult方法,因为mult方法未调用其他方法,所以执行过程中rbp和rsp一样,rsp未向下移动,直接使用rsp下面的两个4字节储存变量。

    此时main方法的栈帧状态如下:

    14、mult方法执行完成,继续ni

    mov    %eax,-0x8(%rbp)  将mult方法的调用结果保存到原来的变量c中

    mov    $0x400678,%eax  把0x400678拷贝到eax中,0x400678应该是字符串"c=%d\n"指针的地址

    mov    -0x8(%rbp),%edx  把变量c拷贝到edx中

    mov    %edx,%esi 把edx中的值拷贝到esi中,作为函数的第二参数

    mov    %rax,%rdi 把rax中的值拷贝到rdi中,作为函数的第一参数

    mov    $0x0,%eax 用0初始化eax

    callq  0x4003b8    <printf@plt> 调用printf方法,printf方法是标准库函数,看不到源码,而且涉及系统调用相关,整体流程比较复杂,不展开了。

    mov    $0x0,%eax 用0初始化eax,0是返回值

    leaveq 同上,恢复rbp

    retq 同上,恢复rip,至此main方法结束,返回0,进程自动退出。

    5、结构体反汇编

         上面讲的是普通变量操作的反汇编,普通变量或者指针都可以通过寄存器传递,但是结构体如何传递了?结构体是如何通过汇编初始化了?

    测试代码如下:

    #include <stdio.h>
    
    struct user{
    	int age;
    	char name[10];
    };
    
    typedef struct user user;
    
    user change(user user);
    
    int main ()
    {
       user test={14,"test"};
       user a=change(test);
       int age=a.age+1;
       printf("age:%d",age);
    }
    
    user change(user user){
    	user.age=12;
    	return user;
    }
    
    

    结构体初始化的指令如下:

    movq   $0x0,-0x20(%rbp) 将离rbp栈帧指针32字节的起始地址之后的8字节初始化为0

    movq   $0x0,-0x18(%rbp) 将离rbp栈帧指针24字节的起始地址之后的8字节初始化为0

    movl   $0xe,-0x20(%rbp)  将离rbp栈帧指针32字节的起始地址之后的4字节初始化为14,即结构体test中age属性的初始化

    movq   $0x74736574,-0x1c(%rbp)  将离rbp栈帧指针28字节的起始地址之后的8字节初始化为$0x74736574,这个是字符串test在静态存储区的地址,即结构体中name属性的初始化

    movw   $0x0,-0x14(%rbp)  将离rbp栈帧指针20字节的起始地址之后的2字节初始化为0

    进入函数调用前的指令如下:

    mov    -0x20(%rbp),%rdx 将离rbp栈帧指针32字节的起始地址之后的8字节拷贝到rdx中

    mov    -0x18(%rbp),%rax  将离rbp栈帧指针24字节的起始地址之后的8字节拷贝到rax中

    mov    %rdx,%rdi   将rdx寄存器的数据拷贝到rdi中

    mov    %rax,%rsi   将rax寄存器的数据拷贝到rsi中

    进入到change方法的指令如下:

    mov    %rdi,%rdx   将rdi中的数据拷贝到rdx中

    mov    %rsi,%rax   将rsi中的数据拷贝到rax中

    mov    %rdx,-0x20(%rbp)  将rdx中的数据拷贝到离rbp栈帧指针32字节的起始地址之后的8字节中

    mov    %rax,-0x18(%rbp)  将rax中的数据拷贝到离rbp栈帧指针24字节的起始地址之后的8字节中

    movl   $0xc,-0x20(%rbp)    将rdx中的数据拷贝到离rbp栈帧指针32字节的起始地址之后的4字节赋值为12

    change方法准备返回的指令如下:

    mov    -0x20(%rbp),%rax 

    mov    %rax,-0x10(%rbp) 将离rbp栈帧指针32字节的起始地址之后的8字节拷贝到离rbp栈帧指针16字节的起始地址之后的8字节

    mov    -0x18(%rbp),%rax

    mov    %rax,-0x8(%rbp)  将离rbp栈帧指针24字节的起始地址之后的8字节拷贝到离rbp栈帧指针8字节的起始地址之后的8字节

    mov    -0x10(%rbp),%rax  将离rbp栈帧指针16字节的起始地址之后的8字节拷贝到rax中

    mov    -0x8(%rbp),%rdx  将离rbp栈帧指针8字节的起始地址之后的8字节拷贝到rdx中

    方法返回以后执行的指令如下:

    mov    %rax,%rcx

    mov    %rdx,%rax

    mov    %rcx,-0x40(%rbp)

    mov    %rax,-0x38(%rbp) 上述指令将change方法返回的数据拷贝到离rbp栈帧指针64字节的起始地址之后的16字节中

    mov    -0x40(%rbp),%rax

    mov    %rax,-0x30(%rbp) 

    mov    -0x38(%rbp),%rax

    mov    %rax,-0x28(%rbp)  上述指令将离rbp栈帧指针64字节的起始地址之后的16字节拷贝到离rbp栈帧指针48字节的起始地址之后的16字节,即用change方法的返回值重新初始化了变量a

    mov    -0x30(%rbp),%eax

    add    $0x1,%eax

    mov    %eax,-0x4(%rbp)  上述指令完成a.age+1,并把结果保存到离rbp栈帧指针4字节的起始地址之后的4字节。

    至此结构体变量的初始化和参数传递分析完成,从中可以得出结论,传递结构体时实际是将结构体对应的内存以8字节为单位拷贝到寄存器中,将寄存器中的数据拷贝到内存即完成对应结构体的初始化。如果结构体占用的内存超过可用寄存器允许传递的最大字节数怎么办了?把上述结构体中char数组的长度改成200,继续测试。汇编指令比较长,尤其是rep指令不好调试,可通过gcc -S生成汇编代码查看。

    其中传递结构体的核心指令如下:

     leaq    -432(%rbp), %rbx
     movq    %rsp, %rdx
     leaq    -224(%rbp), %rax
     movl    $25, %ecx
     movq    %rdx, %rdi
     movq    %rax, %rsi

     rep movsq       上述指令将距离rbp栈帧指针224字节的起始地址之后的25*8=200字节拷贝到rsp栈顶指针后面的200字节中,拷贝结束,rdi指向拷贝的最后一个字节,rsi指向离rbp栈帧指针24字节处

     movq    %rsi, %rax
     movq    %rdi, %rdx
     movl    (%rax), %ecx
     movl    %ecx, (%rdx)
      上述指令将离rbp栈帧指针24字节处后的4字节拷贝到200字节的后面,至此结构体的204字节都已拷贝完成。

    所以当结构体的大小超过寄存器传递允许的最大字节数时就直接从当前栈帧的内存拷贝至调用方法的栈帧内。
     参考:GDB调试之TUI界面

               GDB 单步调试汇编

                disassemble command

     

     

     

    展开全文
  • 汇编

    2020-05-18 01:15:01
    文章目录一、基础知识1、指令2、存储器3、总线1、总线2、CPU对存储器的读写3、CPU对外设的控制4、内存地址空间二、寄存器...
  • 汇编语言中的'#'

    千次阅读 2018-12-04 16:39:46
    汇编语言中的“#”代表的是寻址方式为立即寻址。 比如指令: MOV A,#21H MOV意思是单片机片内RAM之间传送,该指令的意思是:将值21H送给寄存器A中暂存; 如果把**“#”去掉**,MOV A,21H该指令寻址方式变了,为直接...
  • 汇编

    2020-11-17 18:46:13
    自用,与其他人的笔记和老师的ppt结合的 ...基础知识 80x86 的指令系统和寻址方式 指令:指令通常由操作码和地址码(操作数)两部分组成 指令系统:一组指令的集合(指计算机能够执行的全部指令的集合 ...
  • 汇编常用指令

    万次阅读 多人点赞 2018-10-06 14:28:08
    通用寄存器及使用 IA-32系列有8个32位通用寄存器,名称分别为:EAX,EBX,ECX,EDX,ESP,EBP,ESI,EDI,如图: 简单传送指令 指令 中文名 格式 解释 备注 location ...把操作数oper1的内容与操作数oper...
  • 汇编语言之寄存器总结

    千次阅读 2018-09-19 10:40:55
    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;...现在的电脑已经从以前的32位变为现在的64位,但是,CUP的老祖宗8086,我们依然不能忘记呀,所以趁着有时间今天就早期8086总结一下各种寄存器的相关属性。...
  • 汇编语言(王爽)实验十 编写子程序

    万次阅读 多人点赞 2016-07-28 16:09:21
    标 题:汇编实验10—— 编写子程序 作 者: XHS_12302 时 间: 链 接: 实验10编写子程序 在这次实验中,我们将要编写3个子程序,通过它们来认识几个常见的问题和掌握解决这些问题的方法。同前面的所有实验一样,...
  • 汇编windows的exe文件

    万次阅读 2020-06-27 19:54:02
    汇编伪指令db,dw,dd db定义字节类型变量,一个字节数据占1个字节单元,读完一个,偏移量加1 dw定义字类型变量,一个字数据占2个字节单元,读完一个,偏移量加2 dd定义双字类型变量,一个双字数据占4个字节单元,读...
  • 汇编语言的所有指令

    万次阅读 多人点赞 2017-11-11 09:43:15
    1、数据传送指令集MOV 功能: 把源操作数送给目的操作数 语法: MOV 目的操作数,源操作数 格式: MOV r1,r2 MOV r,m MOV m,r MOV r,data XCHG 功能: 交换两个操作数的数据 语法: XCHG ...语法
  • [编程语言][汇编语言]计算机与汇编语言

    千次阅读 多人点赞 2015-11-30 18:47:31
    汇编语言
  • 用gdb 查看,执行汇编代码

    万次阅读 2014-05-23 14:45:27
    用gdb 查看汇编代码, 采用disassemble 和 x 命令。 nexti, stepi 可以单步指令执行 如下例: ------------------------------------------------------------ 源代码: ----------------------------------------...
  • 锲子 我们在各自的电脑上写下代码,得明白我们代码究竟是如何产生的,不想了解1,0什么的,但这几个环节必须掌握吧。 我们的代码会经过这4个环节,从而形成最终文件,c语言作为编译语言,用来向计算机发出指令。...
  • c语言和汇编语言的区别

    万次阅读 多人点赞 2016-11-04 17:57:16
    什么是c语言:   C语言是一门通用计算机编程语言,应用广泛。C语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言;...
  • 链接: https://pan.baidu.com/s/1iI42N13HDOsAFohCJ6XYfA 提取码: z21p 下载dosbox虚拟机安装包以及debug.exe文件 下载后安装dosbox 如下 将masm文件夹复制到C:/根目录(文件夹内有debug.exe程序) ...
  • 什么是汇编语言

    万次阅读 多人点赞 2018-11-19 21:21:37
    汇编语言(assembly language)是一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。在汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地址符号(Symbol)或标号...

空空如也

1 2 3 4 5 ... 20
收藏数 364,284
精华内容 145,713
关键字:

汇编