汇编介绍

1. 为什么要学习汇编?

2. 逆向为什么要学汇编?

正向: C/C++源代码 —> 汇编语言 —> 机器语言 逆向: 机器语言 —> 汇编语言 {—>C/C++源代码 可用性非常低}

3. 教学

8086汇编语言参考王爽的《汇编语言》

汇编基础知识

1. 机器语言(二进制语言)

1.1 机器语言是机器指令的集合

2. 汇编语言的产生

机器码不适合阅读,便产生了汇编语言

3. 汇编语言的组成

1.汇编指令(机器码的助记符) 2.伪指令 (由编译器执行) 3.其他符号(由编译器识别)

4. 存储器

1.CPU是计算机的核心部件,他控制整个计算机的运作并进行运算,要想让一个CPU工作,就必须向他提供指令和数据-指令告诉CPU怎么运算数据就是告诉CPU运算什么。 2.离开了内存,性能再好的CPU也无法工作。 3.磁盘不同于内存,磁盘上的数据或程序如果不读到内存中,就无法被CPU使用。 硬盘—>内存—>CPU

5. 指令和数据

  1. 指令和数据是应用上的概念。
  2. 在内存或磁盘上,指令和数据没有任何区别,都是二进制信息。
  3. 二进制数据

    6. 存储单位

    计算机存储单位一般用bit、B、KB、MB、GB、TB、PB、EB、ZB、YB、BB、NB、DB……来表示。

    7. CPU对存储器和读写

    CPU要想进行数据的读写,必须和外部器件(标准的说法是芯片)进行三类信息的交互: 存储单元的地址(地址信息)(硬盘地址,内存地址,显卡显存地址) 器件的选择,读或写命令(控制信息) 读或写的数据(数据信息)

地址总数 传输 地址信息 数据总数 传输 数据信息 控制总数 传输 控制信息

CPU是通过地址总线来指定存储单元的地址的。 地址总线上能传送多少个不同的信息,CPU就可以对多少个存储单元进行寻址。 CPU 系统 软件程序 64 64 64 => 真正的64位的运算速度

8. 地址总线

一个CPU有N根地址总线,这可以说这个CPU的地址总线的宽度为N。 这样的CPU最多可以寻找2的N次方个内存单位。

9. 数据总线(高速公路)

CPU与内存或其他器件之间的数据传送是通过数据总线来进行的。 数据总线的宽度决定了CPU和外界的数据传送速度。(传输量)

10. 控制总线

CPU对外部器件的控制是通过控制总线来进行。在这里控制总线是个总称,控制总线是一些不同控制线的集合。 有多少根控制总线,就意味着CPU提供了对外部器件的多少种控制。

11. 内存地址空间的概念

寄存器(CPU工作原理)

1. CPU概述

  1. 一个典型的CPU由运算器、控制器、寄存器等器件组成,这些器件靠内部总线相连。
  2. 内部总线实现CPU内部各个器件之间的联系。
  3. 外部总线实现CPU和主板上其他器件的联系。

    2. 寄存器概述

  4. 8086CPU有14个寄存器 他们的名称为: AX、BA、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW。
  5. 通用寄存器 8086CPU所有的寄存器都是16为。可以存放两个字节(即一个字)。 AX、BA、CX、DX、通常用来存放一般性数据被称为通用寄存器。 8086上一代CPU中的寄存器都是8位的: 为保证兼容性,这四个寄存器都可以分为两个独立的8位寄存器使用。AX可以分为AH和AL;BX可以分为BH和BL;CX可以分为CH和CL;DX可以分为DH和DL。

    3. 字在寄存器中存储

    word = 2Byte

    4. 几条汇编指令

    汇编指令不区分大小写

    5. 物理地址

    CPU访问内存单元时要给出内存单元的地址。所有的内存单元构成的存储空间是一个一维的线性空间。 每一个内存单元在这个空间中都有唯一的地址,这个唯一的地址称为物理地址。

    6. 16位结构的CPU

    概括的讲,16位结构描述了一个CPU具有以下几个方面特征: -运算器一次最多可以处理16位的数据。 -寄存器的最大宽度为16位。 -寄存器和运算器之间的通路是16位的。

7. 8086CPU给出物理地址的方法

8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址 (1) CPU中的相关部件提供两个16位的地址,一个称为段地址,另一个称为偏移地址; (2)段地址和偏移地址通过内部总线送入一个称为地址加法器的部件; (3)地址加法器将两个16位地址合成为一个20位的物理地址; (4)地址加法器通过内部总线将20位物理地址送入输入输出控制电路(5)输入输出控制电路将20位物理地址送上地址总线; (6)20彷物理地址被地址总线传送到存储器

8. 地址加法器

地址加法器合成物理地址的方法: 物理地址=段地址×16+偏移地址

9. “段地址×16+偏移地址=物理地址”的本质含义

“基础地址+偏移地址 = 物理地址” “段地址×16+偏移地址=物理地址”

10. 段的概念

错误认识: 内存被划分成了一个一个的段,每一个段有一个段地址。 其实: 内存并没有分段,段的划分来自于CPU,由于8086CPU用“(段地址×16)+偏移地址=物理地址”的方式给出内存单元的物理地址,使得我们可以用分段的方式来管理内存。

以后,在编程时可以根据需要,将若干地址连续的内存单元看作一个段,用段地址×16定位段的起始地址(基础地址),用偏移地址定位段中的内存单元。

(1)段地址×16 必然是 16的倍数,所以一个段的起始地址也一定是16的倍数; (2)偏移地址为16位,16 位地址的寻址能力为 64K,所以一个段的长度最大为64K。

“数据在21F60H内存单元中。”对于8086PC机的两种描述: (a)数据存在内存2000:1F60单元中; (b)数据存在内存的2000段中的1F60H单元中。

11. 段寄存器

段寄存器就是提供段地址的。 8086CPU有4个段寄存器: CS(Code segment)、DS(Data segment)、SS(Stack segment)、ES(EXtract segment)

当8086CPU要访问内存时,由这4个段寄存器提供内存单元的段地址。

12. CS和IP

CS和IP是8086CPU中最关键的寄存器,它们指示了CPU当前要读取指令的地址。

CS为代码段寄存器;
IP为指令指针寄存器(Instruction Pointer Register)。

低地址放低位,小端模式(8086) 高地址放低位,大端模式

  1. 8086PC工作过程的简要描述: (1)从CS:IP指向内存单元读取指令,读取的指令进入指令缓冲器; (2)IP = IP + 所读取指令的长度,从而指向下一条指令; (3)执行指令。 转到步骤 (1),重复这个过程。 在 8086CPU 加电启动或复位后( 即 CPU刚开始工作时)CS和IP被设置为CS=FFFFH,IP=0000H,即在8086PC机刚启动时,CPU从内存FFFF0H单元中读取指令执行,FFFF0H单元中的指令是8086PC机开机后执行的第一条指令。 也就是改变CS和IP的值可以控制CPU的执行的指令

  2. CPU根据什么将内存中的信息看作指令? CPU将CS:IP指向的内存单元中的内容看作指令。

  3. 修改CS和IP的指令 在CPU中,程序员能够用指令读写的部件只有寄存器,程序员可以通过改变寄存器中的内容实现对CPU的控制。CPU从何处执行指令是由CS、IP中的内容决定的,程序员可以通过改变CS、IP中的内容来控制CPU执行目标指令。

mov指令不能用于设置CS、IP的值,8086CPU没有提供这样的功能。8086CPU为CS、IP提供了另外的指令来改变它们的值:转移指令 3.1 转移指令 同时修改CS、IP的内容: jmp 段地址:偏移地址 jmp 2AE3:3 jmp 3:0B16 功能:用指令中给出的段地址修改CS,偏移地址修改IP。 仅修改IP的内容: jmp 某一合法寄存器 jmp ax (类似于 mov IP,ax) jmp bx 功能:用寄存器中的值修改IP。

13. 代码段

对于8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段。 可以将长度为 N( N≤64KB )的一组代码,存在一组地址连续、起始地址为 16的倍数的内存单元中,这段内存是用来存放代码的,从而定义了一个代码段。

如何使得代码段中的指令被执行呢? 将一段内存当作代码段,仅仅是我们在编程时的一种安排,CPU 并不会由于这种安排,就自动地将我们定义得代码段中的指令当作指令来执行。 CPU 只认被 CS:IP 指向的内存单元中的内容为指令。 所以要将CS:IP指向所定义的代码段中的第一条指令的首地址。 CS = 123BH,IP = 0000H。

dosbox操作: 挂载设备:mount C D:/MASM 进入debug功能: debug debug功能下的命令: r 查看、改变CPU寄存器的内容 如 r / r cs d 查看内存中的内容 如 d / d 1000:0 / d 1000:0 ff(长度) e 修改内存中的内容 如 e 1000:0 30 31 / e 1000:0 'a' 'b' 'c' / e 1000:0(询问式) u 将内存中的机器指令翻译成汇编指令 如 u 1000:0 a 以汇编指令的格式在内存中写入机器指令 如 a 1000:0 t (trace)单步执行一条机器指令 (使用前应该用r命令修改CS和IP)

寄存器(内存访问)

1. 内存中字的存储

20000 0x4E 20 Little - Endian (小端)就是低位字节放在内存的低地址端, 高位字节放在内存的高地址端 Big - Endian (大端)就是低位字节放在内存的高地址端, 高位字节放在内存的低地址端 网络字节顺序(也就是大端模式)

任何两个地址连续的内存单元,N号单元和 N+1号单元,可以将它们看成两个内存单元 ,也可以看成一个地址为N的字单元中的高位字节单元和低位字节单元。

2. DS和[address]

8086CPU中有一个 DS寄存器,通常用来存放要访问的数据的段地址。 例如:我们要读取10000H单元的内容可以用如下程序段进行: mov bx,1000H mov ds,bx mov al,[0] 上面三条指令将10000H(1000:0)中的数据读到al中(为什么不直接为ds赋值?ds是段寄存器,所有段寄存器都不能直接载入数据)

已知的mov指令可完成的两种传送功能: (1)将数据直接送入寄存器; (2)将一个寄存器中的内容送入另一个寄存器中。 (3) mov 指令 还可以将一个内存单元中的内容送入一个寄存器。或者将寄存器中的的数据写入内存单元(如mov [0],al)。

执行指令时,8086CPU自动取DS中的数据为内存单元的段地址。 如何用mov指令从10000H中读取数据? 10000H表示为1000:0(段地址:偏移地址) 将段地址1000H放入ds 用mov al,[0]完成传送(mov指令中的[]说明操作对象是一个内存单元,[]中的0说明这个内存单元的偏移地址是0,它的段地址默认放在ds中)

如何把1000H送入ds? 传送指令 mov ax,1 相似的方式 mov ds,1000H? 8086CPU不支持将数据直接送入段寄存器的操作,ds是一个段寄存器。 (硬件设计的问题) mov ds,1000H 是非法的。 数据->一般的寄存器->段寄存器

3. 字的传送

因为8086CPU是16位结构,有16根数据线,所以,可以一次性传送16位的数据,也就是一次性传送一个字。 mov bx,1000h mov ds,bx mov ax,[0] //1000:0处的字型数据送入ax mov [0],cx //cx中的16位数据送到1000:0处

4. mov、add、sub指令

mov指令的几种形式 mov 寄存器,数据  mov 寄存器,寄存器  mov 寄存器,内存单元  mov 内存单元,寄存器  mov 段寄存器,寄存器 mov 寄存器,段寄存器 mov 内存单元,段寄存器 mov 段寄存器,内存单元 add,sub指令不能对段寄存器操作

5. 数据段

一个字型数据占两个单元,所以偏移地址是0、2、4。

6. 栈

  1. 栈是一种具有特殊的访问方式的存储空间。它的特殊性就在于,最后进入这个空间的数据,最先出去。 栈有两个基本的操作:入栈和出栈。 入栈:将一个新的元素放到栈顶; 出栈:从栈顶取出一个元素。 栈顶的元素总是最后入栈,需要出栈时,又最先被从栈中取出。 栈的操作规则:LIFO (Last In First Out,后进先出)

  2. CPU提供的栈机制 1)8086CPU提供入栈和出栈指令: (最基本的)

      PUSH(入栈)
      POP  (出栈)
    

    push ax:将寄存器ax中的数据送入栈中; pop ax :从栈顶取出数据送入ax。 8086CPU的入栈和出栈操作都是以字为单位进行的。

2)8086CPU中,有两个寄存器: 段寄存器SS  存放栈顶的段地址 寄存器SP  存放栈顶的偏移地址 任意时刻,SS:SP指向栈顶元素

3)push 指令的执行过程 push ax (1)SP=SP–2; (2)将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶。

初始状态栈是空的,则SS:SP指向占空间最高地址单元的下一个单元 如10000H~1000FH 这段空间当作栈,初始状态栈是空的,此时,SS=1000H,SP=0010H 任意时刻,SS:SP指向栈顶,当栈中只有一个元素的时候,SS = 1000H,SP=000EH。栈为空,SP=SP+2

4) pop 指令的执行过程 pop ax (1)将SS:SP指向的内存单元处的数据送入ax中; (2)SP = SP+2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。 注意: 出栈后,SS:SP指向新的栈顶 ,pop操作前的栈顶元素依然存在 ,但是,它已不在栈中。 当再次执行push等入栈指令后,SS:SP移至该处,并在里面写入新的数据,它将被覆盖。

5)栈顶超界的问题 -当栈满的时候再使用push指令入栈, 栈空的时候再使用pop指令出栈, 都将发生栈顶超界问题。 -栈顶超界是危险的。 -8086CPU的工作机理,只考虑当前的情况: 当前栈顶在何处; 当前要执行的指令是哪一条。

结论: 我们在编程的时候要自己操心栈顶超界的问题 ,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。

6)栈与内存 栈空间当然也是内存空间的一部分,它只是一段可以以一种特殊的方式进行访问的内存空间。 7)push、pop指令 push和pop指令是可以在寄存器和内存之间传送数据的。 push和pop指令的格式(1) push 寄存器:将一个寄存器中的数据入栈 pop 寄存器:出栈,用一个寄存器接收出栈的数据 例如:push ax pop bx

push和pop指令的格式(2) push 段寄存器:将一个段寄存器中的数据入栈 pop 段寄存器:出栈,用一个段寄存器接收出栈的数据

例如:push ds pop es

push和pop指令的格式(3) push 内存单元:将一个内存单元处的字入栈(栈操作都是以字为单位) pop 内存单元:出栈,用一个内存字单元接收出栈的数据 例如:push [0] pop [2]

编程: 将10000H~1000FH 这段空间当作栈,初始状态是空的,将 AX、BX、DS中的数据入栈。 mov ax,1000h mov ss,ax ;设置栈的短地址,SS=1000h,不能直接向段寄存器SS送入数据,所以用ax中转 mov sp,0010h ;设置栈顶的偏移地址,栈为空,所以SP为0010H push ax push bx push ds

8)栈段 我们可以将长度为 N(N ≤64K )的一组地址连续、起始地址为16的倍数的内存单元,当作栈来用,从而定义了一个栈段。

9)段的综述 我们可以将一段内存定义为一个段,用一个段地址指示段,用偏移地址访问段内的单元。这完全是我们自己的安排。 我们可以用一个段存放数据,将它定义为“数据段”; 我们可以用一个段存放代码,将它定义为“代码段”; 我们可以用一个段当作栈,将它定义为“栈段”;

对于数据段,将它的段地址放在 DS中,用mov、add、sub等访问内存单元的指令时,CPU就将我们定义的数据段中的内容当作数据段来访问;

对于代码段,将它的段地址放在 CS中,将段中第一条指令的偏移地址放在IP中,这样CPU就将执行我们定义的代码段中的指令;

对于栈段,将它的段地址放在SS中,将栈顶单元的偏移地置放在 SP 中,这样CPU在需要进行栈操作的时候,比如执行 push、pop 指令等,就将我们定义的栈段当作栈空间来用。

总结:一段内存,可以既是代码的存储空间,又是数据的存储空间,还可以是栈空间,也可以什么也不是。关键在于CPU中寄存器的设置,即: CS、IP、SS、SP、DS的指向。

汇编程序

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

一个汇编语言程序从写出到最终执行的简要过程: 编写--〉编译--〉连接--〉执行 1)编写汇编源程序 使用文本编辑器(如Edit、记事本等),用汇编语言编写汇编源程序。 2)对源程序进行编译连接 使用汇编语言编译程序对源程序文件中的源程序进行编译,产生目标文件;再用连接程序对目标文件进行连接,生成可在操作系统中直接运行的可执行文件。 3)可执行文件 可执行文件中包含两部分内容: 程序(从原程序中的汇编指令翻译过来的机器码)和数据(源程序中定义的数据) 相关的描述信息(比如:程序有多大、要占多少内存空间等) 4)执行可执行文件中的程序 操作系统依照可执行文件中的描述信息,将可执行文件中的机器码和数据加载入内存,并进行相关的初始化(比如:设置CS:IP指向第一条要执行的指令),然后由CPU执行程序。

2. 源程序

汇编指令 有对应的机器码的指令,可以被编译为机器指令,最终为CPU所执行。 伪指令 没有对应的机器码的指令,最终不被CPU所执行。 谁来执行伪指令呢? 伪指令是由编译器来执行的指令,编译器根据伪指令来进行相关的编译工作。 1)定义一个段 segment和ends的功能是定义一个段,segment说明一个段开始,ends 说明一个段结束。 一个段必须有一个名称来标识,使用格式为: 段名 segment 段名 ends 一个汇编程序是由多个段组成的,这些段被用来存放代码、数据或当作栈空间来使用。 一个有意义的汇编程序中至少要有一个段,这个段用来存放代码。 2)程序结束标记 End 是一个汇编程序的结束标记,编译器在编译汇编程序的过程中,如果碰到了伪指令 end,就结束对源程序的编译。 如果程序写完了,要在结尾处加上伪指令end 。否则,编译器在编译程序时,无法知道程序在何处结束。 注意:不要搞混了end和ends。

3)寄存器与段的关联假设 assume:含义为“假设”。 它假设某一段寄存器和程序中的某一个用 segment … ends 定义的段相关联。 通过assume说明这种关联,在需要的情况下 ,编译程序可以将段寄存器和某一个具体的段相联系。 如:assume cs:codesg

4) 标号 一个标号指代了一个地址。 codesg:放在segment的前面,作为一个段的名称,这个段的名称最终将被编译、连接程序处理为一个段的段地址。 5) 程序返回 应该在程序的末尾添加返回的程序段。 mov ax,4c00H int 21H 这两条指令所实现的功能就是程序返回。

6) DosBox操作 edit(也可用其他编辑软件) -> 1.asm -> masm(编译) -> 1.obj -> link(连接) -> 1.exe -> command(运行exe) ->CPU

ml 1.asm 相当于masm和link两个步骤 跟踪: debug 1.exe 命令就跟debug中的命令一样

[bx]和loop指令

[bx]

  1. [bx]和内存单元的描述 [bx]是什么呢? 和[0]有些类似,[0]表示内存单元,它的偏移地址是0。 [bx]同样也表示一个内存单元,它的偏移地址在bx中,比如下面的指令: mov ax,[bx] mov al,[bx]

  2. 我们要完整地描述一个内存单元,需要两种信息: (1)内存单元的地址; (2)内存单元的长度(类型)。 我们用[0]表示一个内存单元时,0 表示单元的偏移地址,段地址默认在ds中,单元的长度(类型)可以由具体指令中的其他操作对象(比如说寄存器)指出。

loop指令

指令的格式是:loop 标号,CPU 执行loop指令的时候,要进行两步操作: ① (cx)=(cx)-1; ② 判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行。 cx中的值影响着loop指令的执行结果。 通常(注意,我们说的是通常)我们用loop指令来实现循环功能,cx 中存放循环次数。

用cx和loop 指令相配合实现循环功能的三个要点: (1)在cx中存放循环次数; (2)loop 指令中的标号所标识地址要在前面; (3)要循环执行的程序段,要写在标号和loop 指令的中间。

loop和[bx]的联合应用

在实际编程中,经常会遇到,用同一种方法处理地址连续的内存单位的数据的问题

我们需要用循环来解决这类问题,同时我们必须能够在每次循环的时候按照同一种方法来改变要访问的内存单元的地址

段前缀

指令“mov ax,[bx]”中,内存单元的偏移地址由bx给出,而段地址默认在ds中。 我们可以在访问内存单元的指令中显式地给出内存单元的段地址所在的段寄存器。 这些出现在访问内存单元的指令中,用于显式地指明内存单元的段地址的“ds:”、“cs:”、“ss:”或“es:”,在汇编语言中称为段前缀。
如: mov ds:[0],al

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

and和or指令

1)and 指令:逻辑与指令,按位进行与运算。 如 mov al, 01100011B and al, 00111011B 执行后:al = 00100011B

[bx+idata]

在前面,我们可以用[bx]的方式来指明一个内存单元, 我们还可以用一种更为灵活的方式来指明内存单元: [bx+idata]表示一个内存单元,它的偏移地址为(bx)+idata(bx中的数值加上idata)。 我们看一下指令mov ax,[bx+200]的含义: 将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为bx中的数值加上200,段地址在ds中。 数学化的描述为: (ax)=((ds)*16+(bx)+200)

SI和DI

SI和DI是8086CPU中和bx功能相近的寄存器,SI和DI不能够分成两个8 位寄存器来使用。 下面的三组指令实现了相同的功能: (1) mov bx,0 mov ax,[bx] (2) mov si,0 mov ax,[si] (3) mov di,0 mov ax,[di] 还有: (4) mov bx,0 mov ax,[bx+123] (5) mov si,0 mov ax,[si+123] (6) mov di,0 mov ax,[di+123]

[bx+si]和[bx+di]

在前面,我们用[bx(si或di)]和[bx(si或di)+idata] 的方式来指明一个内存单元,我们还可以用更灵活的方式: [bx+si] [bx+di] [bx+si]表示一个内存单元,它的偏移地址为(bx)+(si)(即bx中的数值加上si中的数值)。

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

[bx+si+idata]表示一个内存单元 它的偏移地址为(bx)+(si)+idata。 (即bx中的数值加上si中的数值再加上idata)

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

如果我们比较一下前而用到的几种定位内存地址的方法(可称为寻址方式),就可以发现有以下几种方式: (1)[iata] 用一个常量来表示地址,可用于直接定位一个内存单元; (2)[bx]用一个变量来表示内存地址,可用于间接定位一个内存单元; (3)[bx+idata] 用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元; (4)[bx+si]用两个变量表示地址; (5)[bx+si+idata] 用两个变量和一个常量表示地址。 直接寻址 寄存器间接寻址 寄存器相对寻址 基址变址寻址 相对基址变址寻址

转移指令的原理

操作符offset

操作符offset在汇编语言中是由编译器处理的符号,它的功能是取得标号的偏移地址。 比如下面的程序: assume cs:codesg codeseg segment start:mov ax,offset start ; 相当于 mov ax,0 s:mov ax,offset s ; 相当于mov ax,3 codesg ends end start

转移指令

转移指令分为以下几类: 无条件转移指令 (如:jmp) 条件转移指令 循环指令(如:loop) 过程 中断

jmp指令

jmp为无条件转移,可以只修改IP,也可以同时修改CS和IP; jmp指令要给出两种信息: 转移的目的地址 转移的距离(段间转移、段内短转移,段内近转移)

1)jmp short 标号(转到标号处执行指令)

这种格式的 jmp 指令实现的是段内短转移,它对IP的修改范围为 -128~127,也就是说,它向前转移时可以最多越过128个字节,向后转移可以最多越过127个字节。

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。 在转移指令EB 03中并没有告诉CPU要转移的目的地址,却告诉了 CPU 要转移的位移,即将当前的IP向后移动3个字节。 这个位移,是编译器根据汇编指令中的“标号”计算出来的,

结论: CPU执行 jmp short 标号 指令时并不需要转移的目的地址,只需要知道转移的位移就行了。

(1)8位位移=“标号”处的地址-jmp指令后的第一个字节的地址; (2)short指明此处的位移为8位位移; (3)8位位移的范围为-128~127,用补码表示 (4)8位位移由编译程序在编译时算出 它实现的是段内短转移。 指令“jmp short 标号”的功能为(IP)=(IP)+8位位移。

2)jmp near ptr 标号(转到标号处执行指令)

指令“jmp near ptr 标号”的说明: (1)16位位移=“标号”处的地址-jmp指令后的第一个字节的地址; (2)near ptr指明此处的位移为16位位移,进行的是段内近转移; (3)16位位移的范围为 -32769~32767,用补码表示; (4)16位位移由编译程序在编译时算出。

它实现的是段内近转移。 指令“jmp near ptr 标号”的功能为(IP)=(IP)+16位位移。

3)jmp far ptr 标号

指令 “jmp far ptr 标号” 功能如下: (CS)=标号所在段的段地址; (IP)=标号所在段中的偏移地址。 far ptr指明了指令用标号的段地址和偏移地址修改CS和IP。 指令 “jmp far ptr 标号” 实现的是段间转移,又称为远转移。

db和dw定义字节型数据和字型数据。 dup是一个操作符,在汇编语言中同db、dw、dd 等一样,也是由编译器识别处理的符号。 它是和db、dw、dd 等数据定义伪指令配合使用的,用来进行数据的重复 db 3 dup (0) 定义了3个字节,它们的值都是0,相当于 db 0,0,0 如db 3 dup (0,1,2)

4)指令格式:jmp 16位寄存器

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

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

(1) jmp word ptr 内存单元地址(段内转移) 如 mov ax,0123H mov [bx],ax jmp word ptr [bx] 执行后,(IP)=0123H

mov ax,0123H mov ds:[0],ax jmp word ptr ds:[0] 执行后,(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] 执行后, (CS)=0 (IP)=0123H CS:IP指向0000:0123。

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

6)jcxz指令

jcxz指令为有条件转移指令,所有的有条件转移指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为-128~127。 指令格式:jcxz 标号
(如果(cx)=0,则转移到标号处执行。) 当(cx)=!0时,不进行跳转(程序向下执行)。 指令“jcxz 标号”的功能相当于:if((cx)==0)jmp short 标号;

loop指令

loop指令为循环指令,所有的循环指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为-128~127。 指令格式:loop 标号 ((cx))=(cx)-1,如果(cx)≠0,转移到标号处执行。 指令“loop 标号”的功能相当于: (cx)--; if((cx)≠0) jmp short 标号

根据位移进行转移的意义

 jmp short 标号
 jmp near ptr 标号
 jcxz 标号
 loop 标号

等几种汇编指令,它们对 IP的修改是根据转移目的地址和转移起始地址之间的位移来进行的。在它们对应的机器码中不包含转移的目的地址,而包含的起始地址到目的地址的位移。 这样设计,方便了程序段在内存中的浮动装配。

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

注意,根据位移进行转移的指令,它们的转移范围受到转移位移的限制,如果在源程序中出现了转移范围超界的问题,在编译的时候,编译器将报错。

“jmp 2000:0100”的转移指令,是在 Debug 中使用的汇编指令,汇编编译器并不认识。 如果在源程序中使用,编译时也会报错。

call 和 ret 指令

ret 和 retf

1)ret指令用栈中的数据,修改IP的内容,从而实现近转移; CPU执行ret指令时,进行下面两步操作: (1)(IP)=((ss)*16+(sp)) (2)(sp)=(sp)+2

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

call 指令

CPU执行call指令,进行两步操作: (1)将当前的 IP 或 CS和IP 压入栈中; (2)转移。

1)call 标号(将当前的 IP 压栈后,转到标号处执行指令) CPU执行此种格式的call指令时,进行如下的操作: (1) (sp) = (sp) – 2 ((ss)*16+(sp)) = (IP) (2) (IP) = (IP) + 16位位移

CPU 执行指令“call 标号”时,相当于进行: push IP jmp near ptr 标号

2)call far ptr 标号 (段间转移) CPU执行“call far ptr 标号”这种格式的call指令时的操作: (1) (sp) = (sp) – 2 ((ss) ×16+(sp)) = (CS) (sp) = (sp) – 2 ((ss) ×16+(sp)) = (IP) (2) (CS) = 标号所在的段地址 (IP) = 标号所在的偏移地址

CPU 执行指令 “call far ptr 标号” 时,相当于进行: push CS push IP jmp far ptr 标号

3)call 16位寄存器 功能: (sp) = (sp) – 2 ((ss)*16+(sp)) = (IP) (IP) = (16位寄存器)

CPU执行call 16位reg时,相当于进行: push IP jmp 16位寄存器

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

5)call dword ptr 内存单元地址 汇编语法解释: 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

call 和 ret 的配合使用

call指令转去执行子程序之前,call指令后面的指令的地址将存储在栈中,所以可以在子程序的后面使用 ret 指令,用栈中的数据设置IP的值,从而转到 call 指令后面的代码处继续执行。 这样,我们可以利用call和ret来实现子程序的机制

mul 指令

mul是乘法指令 (1)相乘的两个数:要么都是8位,要么都是16位。 8 位: AL中和 8位寄存器或内存字节单元中; 16 位: AX中和 16 位寄存器或内存字单元中。

使用mul座乘法的时候: (2)结果 8位:AX中; 16位:DX(高位)和AX(低位)中。

格式如下: mul reg mul 内存单元

如: mul byte ptr ds:[0] 含义为: (ax)=(al)((ds)16+0);

mul word ptr [bx+si+8] 含义为: (ax)=(ax)((ds)16+(bx)+(si)+8)结果的低16位; (dx)=(ax)((ds)16+(bx)+(si)+8)结果的高16位;

模块化程序设计

1)call 与 ret 指令共同支持了汇编语言编程中的模块化设计。在实际编程中,程序的模块化是必不可少的。 2)因为现实的问题比较复杂,对现实问题进行分析时,把它转化成为相互联系、不同层次的子问题,是必须的解决方法。 3)call和ret 指令对这种分析方法提供了程序实现上的支持。利用 call和ret指令,我们可以用简洁的方法,实现多个互相联系、功能独立的子程序来解决一个复杂的问题。

参数和结果传递的问题

1)子程序一般都要根据提供的参数处理一定的事务,处理后,将结果(返回值)提供给调用者。 其实,我们讨论参数和返回值传递的问题,实际上就是在探讨,应该如何存储子程序需要的参数和产生的返回值。

规范: 子程序: 说明:计算N的3次方 参数: (bx)=N 结果: (dx:ax)=N∧3 cube:mov ax,bx mul bx mul bx ret

注意,我们在编程的时候要注意良好的风格,对于程序应有详细的注释。子程序的注释信息应该包含对子程序的功能、参数和结果的说明。 因为今天写的子程序,以后可能还会用到;自己写的子程序,也很可能要给别人使用,所以一定要有全面的说明。

2)用寄存器来存储参数和结果是最常使用的方法。对于存放参数的寄存器和存放结果的寄存器,调用者和子程序的读写操作恰恰相反: 调用者将参数送入参数寄存器,从结果寄存器中取到返回值; 子程序从参数寄存器中取到参数,将返回值送入结果寄存器。

批量数据的传递

寄存器的数量终究有限,我们不可能简单地用寄存器来存放多个需要传递的数据。对于返回值,也有同样的问题。

在这种时候,我们将批量数据放到内存中,然后将它们所在内存空间的首地址放在寄存器中,传递给需要的子程序。对于具有批量数据的返回结果,也可用同样的方法。

寄存器冲突的问题

当子程序和主程序都使用了cx作为条件判断时,会发生冲突。

解决这个问题的简捷方法是,在子程序的开始将子程序中所有用到的寄存器中的内容都保存起来,在子程序返回前再恢复。我们可以用栈来保存寄存器中的内容。

标志寄存器

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

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

注意:
我们在使用一条指令的时候,要注意这条指令的全部功能,其中包括,执行结果对标记寄存器的哪些标志位造成影响。 某此指令将影响标志寄存器中的多个标志位,这些被影响的标记位比较全面地记录了指令的执行结果,为相关的处理提供了所需的依据。 比如指令sub al,al执行后,ZF、PF、SF等标志位都要受到影响,它们分别为:1、1、0。

1.ZF标志

flag的第6位是ZF,零标志位。 它记录相关指令执行后, 结果为0 ,ZF = 1 结果不为0,ZF = 0 比如: mov ax,1 sub ax,1
指令执行后,结果为0,则ZF = 1。 mov ax,2 sub ax,1
指令执行后,结果为1,则ZF = 0。

PF标志

flag的第2位是PF,奇偶标志位。 它记录指令执行后,结果的所有二进制位中1的个数: 为偶数,PF = 1; 为奇数,PF = 0。

SF标志

flag的第7位是SF,符号标志位。 它记录指令执行后, 结果为负,SF = 1; 结果为正,SF = 0。

有符号数与补码

计算机中通常用补码来表示有符号数据。计算机中的一个数据可以看作是有符号数,也可以看成是无符号数。 如: 00000001B ,可以看作为无符号数 1 ,或有符号数+1; 10000001B ,可以看作为无符号数129,也可以看作有符号数-127。

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

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

CF标志

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

其实CPU在运算的时候,并不丢弃这个进位值,而是记录在一个特殊的寄存器的某一位上。 8086CPU 就用flag的CF位来记录这个进位值。

OF标志

如果运算结果超出了机器所能表达的范围,将产生溢出。 注意,这里所讲的溢出,只是对有符号数运算而言。 示例指令 mov al,98
add al,99 执行后将产生溢出。 因为add al,99 进行的有符号数运算是:(al)=(al)+99=98+99=197 而结果197超出了机器所能表示的8位有符号数的范围:-128~127。

CF 和OF 所表示的进位和溢出,是分别对无符号数和有符号数运算而言的,它们之间没有任何关系。

cmp指令

cmp 是比较指令,功能相当于减法指令,只是不保存结果。 cmp 指令执行后,将对标志寄存器产生影响。 cmp指令 格式:cmp 操作对象1,操作对象2 功能:计算操作对象1–操作对象2 但并不保存结果,仅仅根据计算结果对标志寄存器进行设置。 cmp ax,ax
指令执行后: ZF=1, PF=1, SF=0, CF=0, OF=0。

results matching ""

    No results matching ""