汇编原来是这样(一)

2021/12/9 22:23:08

本文主要是介绍汇编原来是这样(一),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

汇编原来是这样(一)

(一)基础知识

一、机器语言

  • 机器语言是机器指令的集合。

  • 机器指令展开来讲就是一台机器可以正确执行的命令。(只有0和1)

  • 比如堆栈指令:0101 0000(PUSH AX)

二、汇编语言的产生以及组成

  • 汇编语言的主体是汇编指令。

  • 汇编指令与机器指令的差别在于指令的表示方法上。汇编指令是机器指令便于记忆的书写格式。

  • 汇编指令是机器指令的助记符。

例如:

机器指令:1000100111011000

操作:寄存器BX内容送到AX中

汇编指令:MOV AX,BX

  • 寄存器:简单的讲就是CPU中可以存储数据的器件,一个CPU中有多个寄存器。而上面所说的AX是其中一个寄存器的代号;BX是另一个寄存器的代号。

  • 汇编语言由以下三类组成:

    • 汇编指令(机器码的助记符)

    • 伪指令(由编译器执行)

    • 其他符号(由编译器识别)

  • 汇编语言的核心是汇编指令,它决定了汇编语言的特性。

三、存储器

  1. CPU是计算机的核心部件,它控制整个计算机的运作并进行运算,想要一个CPU工作,就必须向他提供指令和数据。

  2. 指令和数据在存储器中存放,也就是平时所说的内存。

  3. 磁盘不同于内存,磁盘上的数据或程序如果不读到内存中,就无法被CPU使用。

四、指令和数据

  1. 指令和数据是应用上的概念。

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

二进制信息:

1000100111011000 ->89D8H (数据)

1000100111011000 -> MOV AX BX(程序)

五、存储单元

  1. 存储器被划分为若干个存储单元,每个存储单元从0开始顺序编号。

  2. 对于大容量的存储器一般还用以下单位来计量容量(以下用B来代表Byte),微机中常用的计量单位:

    • 1KB = 1024B

    • 1MB = 1024KB

    • 1GB = 1024MB

    • 1TB = 1024GB

  3. CPU对存储器的读写

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

    • 存储单元的地址(地址信息)

    • 器件的选择,读或写命令(控制信息)

    • 读或写的数据(数据信息)

  1. CPU是通过什么将地址、数据和控制信息传到存储芯片中的?

    电子计算机能处理、传输的信息都是电信号,电信号当然要用导线传送。

  • 在计算机中有专门连接CPU和其他芯片的导线,通常称为总线。

    • 物理上:可以称为一根根导线的集合。

    • 逻辑上可以划分为:

      地址总线

      数据总线

      控制总线

  • 总线在逻辑上划分的图示:读写流程:(地址总线(存储单元)-->控制总线(读/写)-->数据总线(读数据/写数据))

  •  

  1. 地址总线(CPU的寻址能力,32位代表有32根地址总线)

    CPU是通过地址总线来指定存储单元的。地址总线上能传送多少个不同的信息,CPU就可以对多少个存储单元进行寻址。

补充:如果要有64位的性能,需要有:64位的CPU、64位的系统、64位的软件。

  • 一个CPU有N根地址总线,则可以说这个CPU的地址总线宽度为N。

  • 这样的CPU最多可以寻找2的N次方个内存单元

  1. 数据总线

    CPU与内存或其他器件之间的数据传送是通过数据总线来进行的。

    数据总线的宽度决定了CPU与外界的数据传送速度。

  2. 控制总线

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

    前面所讲的内存读或写命令是由几根控制总线综合发出的,其中有一根名为读信号输出的控制线负责由CPU向外传送读信号,CPU向该控制线上输出低电平表示将要读取数据;有一根名为写信号输出控制线负责由CPU向外传送写信号。

六、小结

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

  2. 每一种CPU都有自己的汇编指令集。

  3. CPU可以直接使用的信息在存储器中存放。

  4. 在存储器中指令和数据没有任何区别,都是二进制信息。

  5. 存储单元从0开始顺序编号。

  6. 一个存储单元可以存储8个bit(用作单位写成”b“),即8位二进制数。

  7. 每一个CPU芯片都有许多管脚,这些管脚和总线相连。也可以说,这些管脚引出总线。一个CPU可以引出三种总线的宽度标志了这个CPU的不同方面的性能:

    • 地址总线的宽度决定了CPU的寻找能力。

    • 数据总线的宽度决定了CPU与其他器件进行数据传送时的一次数据传送量。

    • 控制总线宽度决定了CPU对系统中其他器件的控制能力。

  8. 什么是内存地址空间?

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

    主板:每一台PC机中都有一个主板,主板上有核心器件和一些主要器件。这些器件通过总线(地址总线、数据总线、控制总线)相连。

    接口卡:计算机系统中,所有可用程序控制其工作的设备,必须受到CPU的控制。CPU对外部设备不能直接控制,如显示器、音箱、打印机等。直接控制这些设备进行工作的是插在扩展插槽上的接口卡。

  9. 各类存储器芯片:

    • 从读写属性上看分为两类:随机存储器(RAM)只读存储器(ROM)

    • 从功能和连接上分类:

      随机存储器RAM

      装有BIOS的ROM

      接口卡上的RAM

    • 装有BIOS的ROM:

      BIOS:Basic Input/Output System,基本输入输出系统。BIOS有什么用?就是当我们开机时,电脑上会出现一串一串的数字、字母等,这些都是BIOS作用的结果,一上电就运行BIOS的程序。BIOS是由主板和各类接口卡(如:显卡、网卡等)厂商提供的软件系统,可以通过它利用该硬件设备进行最基本的输入输出。在主板和某些接口卡上插有存储相应的BIOS的ROM。

    • PC集中各类存储器的逻辑连接

       

  10. 内存空间地址

    上述的存储器在物理上是独立的器件。但是他们有以下两个共同点:

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

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

(1)所有的物理存储器被看作一个由若干存储单元组成的逻辑存储器。

(2)每个物理存储器在这个逻辑存储器中占有一个地址段,即一段地址空间。

(3)CPU在这段地址空间中读写数据,实际上就是在相对应得物理存储器中读写数据。

最终运行程序的是CPU,我们用汇编编程的时候,必须要从CPU角度考虑问题。

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

CPU:一个典型的CPU由运算器、控制器、寄存器等器件组成,这些器件靠内部总线相连。

内部总线:实现CPU内部各个器件之间的联系。

外部总线:实现CPU和主板上其他器件的联系。

  1. 通用寄存器

8086CPU有14个寄存器:AX,BX,CX,DX,SI,DI,SP,BP,IP,CS,SS,DS,ES,PSW。

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

  • AX,BX,CX,DX通常用来存放一般性数据被称为通用寄存器。8086上一代CPU中的寄存器都是8位的。为了保证兼容性,这四个寄存器都可以分为两个独立的8位的寄存器使用。

  • AX可以分为AH和AL。(BX,CX,DX一样的道理)

  1. 字(word)在寄存器中的存储

  • 一个字可以存在一个16位的寄存器中,这个字的高位字节和低位字节自然就存在这个寄存器的高8位寄存器和低8位寄存器中。

  1. 几条汇编指令

    MOV AX,4E20H //将4E20H放入AX中

    ADD AX,1406H //将1406H与AX相加

    MOV AX,BX //将BX的值放入AX中

当所相加的值超过16位的时候会溢出到哪呢?当超过所限制的位数,超过的位数会自动地被丢失,比如我们一个八位的寄存器,所算出来的值是0158H,这个时候它会报错58H,而丢失01。这里的丢失,是指进位制不能在8位寄存器中报错,但是CPU并不是真的丢弃这个进位值。

  1. 物理地址

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

  1. 16位结构的CPU,到底什么是16位?

  • 运算器一次最多可以处理16位的数据。

  • 寄存器的最大宽度为16位。

  • 寄存器和运算器之间的通路是16位的。

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

8086有20位地址总线,可传送20位地址,寻址能力为1M(2的20次方)。

8086内部为16位结构,它只能传送16位的地址,表现出的寻址能力却只有64K。那它如何使用内部的16位数据转换成20位的地址?8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址。

  • CPU中的相关部件提供两个16位的地址,一个称为段地址,另一个称为偏移地址。

  • 段地址和偏移地址通过内部总线送入一个称为地址加法器的部件。

  • 地址加法器将两个16位地址合并成一个20位的地址。

地址加法器工作原理:地址加法器合成物理地址的方法:

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

 

段地址*16相对于向左偏移一位。也可以说是数据左移4位。

  1. 段的概念

内存并没有分段,段的划分来自于CPU,由于8086CPU用“段地址 * 16+偏移地址=物理地址”的方式给出内存单元的物理地址,使得我们可以用分段的方式来管理内存。段是我们在使用时由于需要才加以描述,在编程时可以根据需要,将若干地址连续的内存单元看作一个段,用段地址 * 16定位段的起始地址(基础地址),用偏移地址定位段中的内存单元。

注意:

  • 段地址 * 16必然是16的倍数,所以一个段的起始地址也一定是16的倍数。

  • 偏移地址为16位,16位地址的寻址能力为64KB,所以一个段的长度最大为64KB。

8.小结与注意

  •  

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

  • 给定段地址1000H,用偏移地址寻址,CPU的寻址范围为:10000H~1FFFFH。

  • 在8086PC中,存储单元地址用两个元素来描述,即段地址和偏移地址。

  • 数据在21F60H内存单元中,对于8086PC机的两种描述:

    • 数据存在内存2000:1F60单元中。

    • 数据存在内存的2000段中的1F60H单元中。

  • 可根据需要,将地址连续、起始地址为16的倍数的一组内存单元定义为一个段。

  1. 段寄存器

  • 段寄存器就是提供段地址的:8086CPU有4个段寄存器:CS、DS、SS、ES

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

  1. CS和IP

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

    CS为代码段寄存器

    IP为指令指针寄存器。(偏移地址)

8086PC工作过程:

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

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

(3)执行指令,转回步骤(1)

  • 在8086CPU加电启动或复位后(即CPU刚开始工作时)CS和IP被设置为CS=FFFFH,IP=0000H。即在8086PC机刚启动时,CPU从内存FFFF0H单元中读取指令执行。

  • FFFF0H单元中的指令是8086PC机开机后执行的第一条指令。

  • 在任何时候,CPU将CS、IP中的内容当作指令的段地址和偏移地址,用它们合成指令的物理地址,到内存中读取指令码,执行。

  • 如果说,内存中的一段信息曾被CPU执行过的话,那么,它所在的内存单元必然被CS:IP指向过。

  1. 如何改变CS、IP的值呢?

mov指令不能用于设置CS、IP的值,8086CPU没有提供这样的功能。8086CPU为CS、IP提供了另外的指令来改变它们的值,也就是转移指令。

  • 同时修改CS、IP的内容:

    JMP 段地址:偏移地址

    JMP 2AE3:3 ​
    JMP 3:0B16
    

    功能:使用指令中给出的段地址修改CS,偏移地址修改IP。

  • 仅修改IP的内容:

    JMP 某一合法寄存器

    JMP AX  ;类似于mov IP,ax
    ​
    JMP BX

功能:用寄存器中的值修改IP。

举例:内存中存放的机器码和对应汇编指令情况:初始:CS=2000H,IP=0000H。当执行如下代码时,内存中的码会怎么变化?

MOV AX,6622
JMP 1000:3
MOV AX,0000
MOV BX,AX
JMP BX
MOV AX,0123H
....

  

最后会发现这是一个死循环,一直在内存10000H~10009H中反复横跳。

  1. 代码段

  • 对于8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段。

  • 可以将长度为N(N<=64KB)的一组代码,存在一组地址连续、起始地址为16的倍数的内存单元中,这段内存是用来存放代码的,从而定义了一个代码段。

 

例如以上的代码,这段长度为10字节的字节的指令,存在从123B0H~123B9H的一组内存单元中,我们就可以认为,123B0H~123B9H这段内存单元是用来存放代码的,是一个代码段,它的段地址为123BH,长度为10字节。

  1. 小结以及注意

总结一下8086CPU的工作过程:

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

(2)IP指向下一条指令。(IP=IP+长度)

(3)执行指令。

  1. 实验

(1)学会使用debug

使用DOSBox 0.74-3+Debug.exe进行调试:

常用指令:

R命令 查看、改变CPU寄存器的内容 D命令 查看内存中的内容 E命令 改写内存中的内容 U命令 将内存中的机器指令翻译成汇编指令 T命令 执行一条机器指令 A命令 以汇编指令的格式在内存中写入一条机器指令

当我在终端输入:

 

之后调用d查看内存:要注意修改起始地址。

 

我们发现在内存中的数字比较杂乱,我们可以将其转为汇编语言:

 

首先修改CS和IP的值,使其指向我们想要的所在语句:

 

我指的值是正确的,所以无需修改,若要修改则输入:

r cs
r ip

之后我们使用t命令执行所在的语句:

 

其实t语句的作用就相当于是单步调试,我们可以仔细的观察到内存的变化。

最后可以使用quit,退出调试。

(2)将下面3条指令写入2000:0开始的内存单元中,利用这3条指令计算2的8次方。

修改cs,ip指向的值:

 

 

最后当t跳转到ax=0100时,才是我们需要的效果。

(3)查看内存中的内容

PC机主板上的ROM中写有一个生产日期,在内存FFF00H~FFFFFH的某几个单元中,请找到这个生产日期并试图改变它。

最后那一行就是所对应的日期。上面的数字所代表的就是ASCII码的值,比如第六个数30代表的是十进制的47,对应表中的数字1,我们可以使用命令e来改变生产日期:

 

最后我们再调用命令d可以发现它并没有改变:

 

也就是我们主板里面的bios是无法更改的。

(4)向内存从B8100H开始的单元中填写数据,如:

-e B810:0 01 01 02 02 03 03 04 04

最后会惊喜的发现我们的调试器会出现这个:

 

我们可以随意改变任意数字,然后会在显示屏上显示不一样的字符,其实B810是我们显存的地址。

三、寄存器(内存访问)

  1. 内存中字的存储(两个字节=一个字)

在0地址处开始存放20000(4E20H),在内存中0号单元是低地址单元,1号单元是高地址单元。也就是说,0号地址存放的是20H,1号地址存放的是4EH。

字型数据:两个字节,比如0地址存放的字型数据为4E20H

字节型数据:一个字节,比如0地址存放的字节型数据为20H

 

那么,图中1地址字单元中存放的字型数据是多少?答案是124EH。

结论:

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

  1. DS和[address]

  • CPU要读取一个昵称单元的时候,必须先给出这个内存单元的地址。

  • 在8086PC中,内存地址由段地址和偏移地址组成。

  • 8086CPU中有一个DS寄存器,通常用来存放要访问的数据的段地址

例如:我们要读取10000H单元的内容可以用如下程序段进行:

mov bx,1000H    ;会自动向左偏移一位
​
mov ds,bx
​
mov al,[0]

上面三条指令将10000H(1000:0)中的数据读到al中。

mov al,[0]这条指令有三种传送功能:

(1)将数据直接送入寄存器

(2)将一个寄存器中的内存送入另一个寄存器中

(3)mov指令还可以将一个内存单元中的内容送入一个寄存器。

mov指令格式:

mov 寄存器名,内存单元地址

“[...]”表示一个内存单元,“[0]”中的0表示内存单元的偏移地址。在执行命令时,8086CPU自动取DS中的数据为内存单元的段地址。

那如何用mov指令从10000H中读取数据?

  • 10000H表示为1000:0(段地址:偏移地址)

  • 将段地址1000H放入ds

  • 用mov al,[0]完成传送(mov指令中的[]说明操作对象是一个内存单元,[]中的0说明这个内存单元的偏移地址是0,它的段地址默认放在ds中)

那如何把1000H送入ds?

也就是在最开始的时候使用mov将1000H送入,要注意的是8086CPU不支持将数据直接送入段寄存器的操作,ds是一个段寄存器,只能是通用寄存器。所以输送方式应该为:

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

所以反过来,如何将数据从寄存器中送入内存单元?

mov bx,1000H
mov ds,bx
mov [0],al
  1. 字的传送

因为8086CPU是16位结构,有16根数据线,所以可以一次性传送16位的数据,也就是一次性传送一个字

例题:内存中的情况如下图,写出下面指令执行后寄存器ax,bx,cx中的值:

先使用e命令,将我们所需要的值存入内存中:

用命令a将我们的指令输入:

 

之后对指针进行修改:

 

之后单步调试,发现程序运行正常:

 

  1. mov,add,sub指令

我们已经学会了mov指令的几种形式:

mov 寄存器,数据

mov 寄存器,寄存器

mov 寄存器,内存单元

mov 内存单元,寄存器

mov 段寄存器,寄存器

我们也可以根据已知指令推测:

mov 段寄存器,寄存器 --> mov 寄存器,段寄存器

mov 内存单元,寄存器-->mov 内存单元,段寄存器/mov 段寄存器,内存单元

add和sub指令同mov一样,都有两个操作对象:

 

不可以执行add ds,ax

  1. 数据段

对于8086PC机,我们可以根据需要将一组内存单元定义为一个段,可以是代码段也可以是数据段。

我们可以将一组长度为N(N<=64K)、地址连续、起始地址为16倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。其实也就是我们根据需要来分配内存

比如我们使用123B0H~123B9H这段空间来存放数据:

  • 段地址:123BH

  • 长度:10字节

如何访问数据段中的数据呢?

将一段内存当作数据段,是我们在编程时的一种安排,我们可以在具体操作的时候,用ds存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元。

  1. 小结

(1)字再内存中存储时,要用两个地址连续的内存单元来存放,字的低位字节存放再低地址单元中,高位字节存放在高地址单元中。

(2)用mov指令要访问内存单元,可以在mov指令中只给出单元的偏移地址,此时,段地址默认在DS寄存器中。

(3)[address]表示一个偏移地址为address的内存单元。

(4)在内存和寄存器之间传送字型数据时,高地址单元和高8位寄存器、低地址单元和低8位寄存器相对应。

(5)mov、add、sub是具有两个操作对象的指令。jmp是具有一个操作对象的指令。

从栈的角度理解栈:

栈是一种具有特殊的访问方式的存储空间。它的特殊就在于,最后进入这个空间的数据,最先出去。

栈有两个基本操作:入栈和出栈(后进先出)

  • 入栈:将一个新的元素放到栈顶

  • 出栈:从栈顶取出一个元素。

8086CPU提供相关的指令来以栈的方式访问内存空间。我们在基于8086CPU贬称的时候,可以将一段内存当作栈来使用。

8086CPU提供入栈和出栈指令(最基本):

PUSH(入栈)

POP(出栈)

push ax  ;将寄存器ax中的数据送入栈中
​
pop ax   ;从栈顶取出数据送入ax

8086CPU的入栈和出栈操作都是以为单位进行的。

  • CPU如何知道一段内存空间被当作栈使用?

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

  • 执行push和pop的时候,如何知道哪个单元是栈顶单元?

    当我们执行push ax操作时会发生以下操作:

    (1)SP=SP-2

    (2)将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新的栈顶。

    当我们执行pop ax操作时会发生以下操作:

    (1)将SS:SP指向的内存单元处的数据送入ax中

    (2)SP=SP+2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶。

  • 如果我们将10000H~1000FH这段空间当作栈,初始状态栈是空的,此时,SS=1000H,SP=?

    栈空,SS:SP指向栈空间最高地址单元的下一个单元。

例题:我们将10000H~1000FH这段空间当作栈段,SS=1000H,栈空间大小为16字节,栈最底部的字单元地址为1000:000E。

(1)任意时刻,SS:SP指向栈顶,当栈中只有一个元素的时候:SS=1000H,SP=000EH。

(2)栈为空,就相当于栈中唯一的元素出栈,出栈后,SP=SP+2,SP原来为000EH,加2后SP=10H。

任意时刻,SS:SP指向栈顶元素,当栈为空的时候,栈中没有元素,也就不存在栈顶元素。

所以SS:SP只能指向栈的最底部单元下面的单元,该单元的偏移地址为栈最底部的字单元的偏移地址+2。

栈最底部字单元的地址为1000:000E,所以栈空时,SP=0010H。

  • 注意:

    • 出栈后,SS:SP指向新的栈顶1000EH,pop操作前的栈顶元素,1000CH处的数据依然存在,但是它已不在栈中,当我们使用push等入栈指令后,会在里面写入新的数据,将它覆盖,而不是将它删除。

    • SS和SP只记录了栈顶的地址,依靠SS和SP可以保证在入栈和出栈时找到栈顶。

  1. 栈顶越界问题

当栈满的时候再使用push指令入栈,栈空的时候再使用pop指令出栈,都将发生栈顶超界问题。

栈顶超界是危险的。因为可能会涉及覆盖掉我们内存中一些重要的东西。

那如何解决这个问题呢?

  • 比如说在CPU记录栈顶上限和下限的寄存器,我们可以通过填写这些寄存器来指定栈空间的范围,然后,CPU在执行push指令的时候靠检测栈顶上限寄存器,在执行pop指令的时候靠检测栈顶下限寄存器保证不会超界。但8086并没有这个功能。

  • 当8086CPU工作时,只考虑当前的情况:

    • 当前栈顶在何处

    • 当前要执行的指令是哪一条

  1. push、pop指令

push和pop指令是可以在寄存器和内存之间传送数据的。

  • 栈空间当然也是内存空间的一部分,它只是一段可以以一种特殊的方式进行访问的内存空间。

(1)格式1

push 寄存器:将一个寄存器中的数据入栈。

pop 寄存器:出栈,用一个寄存器接收出栈的数据。

(2)格式2

push 段寄存器:将一个段寄存器中的数据入栈。

pop 段寄存器:出栈,用一个段寄存器接收出栈的数据。

如:

push ds
​
pop es

(3)格式3

push 内存单元:将一个内存单元处的字入栈(栈操作都是以字为单位)。

pop 内存单元:出栈,用一个内存字单元接收出栈的数据。

指令执行时,CPU要知道内存单元的地址,可以在push、pop指令中给出内存单元的偏移地址,段地址在指令执行时,CPU从ds中取得。

例子:将10000H~1000FH这段空间当作栈,初始状态是空的,将AX、BX、DS中的数据入栈。

mov ax,1000H
mov ss,ax       ;设置栈的段地址
mov sp,0010H    ;设置栈顶偏移地址,由于栈为空,所以SP=0010H
push ax
push bx
push ds

将寄存器ax和bx的数值清零有如下方法:

sub ax,ax
sub bx,bx   ;机器码为2个字节
​
mov ax,0
mov bx,0    ;机器码为3个字节

结论:

push、pop实质上就是一种内存传送指令,可以在寄存器和内存之间传送数据,与mov指令不同的是,push和pop指令访问的内存单元的地址不是在指令中给出的,而是由SS:SP指出的。

但是push和pop指令同mov指令不同,CPU执行mov指令只需一步操作,就是传送,而执行push、pop指令却要两步操作。

执行push时:先改变sp,后向ss:sp处传送

执行pop时:先读取ss:sp处的数据,后改变sp

需要注意的是:push、pop等栈操作指令,修改的只是sp,也就是说,栈顶的变化范围最大为:0~FFFFH。

提示:

SS、SP指示栈顶,改变SP后写内存的入栈指令,读内存后改变SP的出栈指令。(8086栈机制)

用栈来暂存以后需要恢复的寄存器的内容时,寄存器出栈的顺序要和入栈的顺序相反。

  1. 栈段

比如我们将10010H~1001FH这段长度为16字节的内存空间当作栈来用,以栈的方式进行访问。这段空间就可以成为栈段,段地址为1000H,大小为16字节。

将一段内存当作栈段,仅仅是我们在编程时的一种安排,CPU并不会由于这种安排,就在执行push、pop等栈操作指令时就自动地将我们定义的栈段当作栈空间来访问。

问题:如果我们将10000H~1FFFFH这段空间当作栈段,初始状态是空的,此时,SS = 1000H,SP=?

我们将10000H~1FFFFH这段空间当作栈段,SS=1000H,栈空间大小为64KB,栈最底部的字单元地址为1000:FFFE。任意时刻,SS:SP指向栈顶,当栈中只有一个元素的时候,SS=1000H,SP=FFFEH。

栈为空,就相当于栈中唯一的元素出栈,出栈后,SP=SP+2,SP原来为FFFEH,加2后SP=0,所以,当栈为空的时候,SS=1000H,SP=0。(溢出为0

总结:

  • 首先从栈操作指令所完成的功能的角度上来看,push、pop等指令在执行的时候只修改SP。SS不可以改变

  • 栈顶的变化范围是0~FFFFH,从栈空的时候的SP=0,一直压栈,直到栈满时SP=0;如果再次压栈,栈顶将环绕,覆盖了原来栈中的内容。

我们将一段内存定义为一个段,用一个段地址来指示段,用偏移地址访问段内的单元,这完全取决于我们的安排,如果想要存放数据,那就是数据段,如果想要存放代码,那就是代码段,如果想要设定为栈,那就是栈段,它是根据我们的需求来安排的。

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

  • 代码段:将它的段地址放在CS中,将段中的第一条指令的偏移地址放在ip中,这样CPU就将执行我们定义的代码段中的指令。

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

总而言之,CPU将内存中的某段内存当作代码,是因为CS:IP指向了那里,CPU将某段内存当作栈,是因为SS:IP指向了那里。

mov ax,1000H
mov ss,ax
mov sp,0020H    ;初始化栈顶
mov ax,cs
mov ds,ax   ;设置数据段段地址
mov ax,[0]
add ax,[2]
mov bx,[4]
add bx,[6]
push ax
push bx
pop ax
pop ax

以上的代码实现的是将ax,bx进行修改值后,入栈,出栈后交换数值的功能,一段内存可以既是代码的存储空间又可以是数据的存储空间,还可以是栈空间,也可以什么都不是,其实关键就在于CPU中寄存器的设置,即CS、IP、SS、SP、DS的指向。

四、第一个程序

汇编语言的程序可以使用编译器将它们编译成可执行文件(.exe),在操作系统中运行。

  1. 编写

  2. 对源程序进行编译连接

  • 使用汇编语言编译程序(MASM.EXE)对源程序文件中的源程序进行编译,产生目标文件。

  • 再用连接程序(LINK.EXE)对目标文件进行连接,生成可在操作系统中直接运行的可执行文件。

  1. 可执行文件(两部分内容)

  • 程序(从源程序中的汇编指令翻译过来的机器码)和数据(源程序中定义的数据)

  • 相关的描述信息(比如:程序有多大、要占多少内存空间等)

  1. 执行可执行文件中的程序

操作系统依照可执行文件中的描述信息,将可执行文件中的机器码和数据加载入内存,并进行相关的初始化(比如:设置CS:IP指向第一条要执行的指令),然后由CPU执行程序。

  1. 伪指令

没有对应的机器码的指令,最终不被CPU所执行。伪指令是由编译器来执行的指令,编译器根据伪指令来进行相关的编译工作。

  1. 定义一个段

  • segment和ends是一对成对使用的伪指令,这是在写可被编译器编译的汇编程序时,必须要用到的一对伪指令。

  • segment和ends的功能是定义一个段,segment说明一个段开始,ends说明一个段结束。

  • 一个段必须有一个名称来标识,使用格式为:

    段名 segment

    段名 ends

  • 一个汇编程序是由多个段组成的,这些段被用来存放代码、数据或当作栈空间来使用。

  • 一个有意义的汇编程序中至少有一个段,这个段用来存放代码。

  • end

End 是一个汇编程序的结束标记,编译器在编译汇编程序的过程中,如果碰到了伪指令end,就结束了对源程序的编译。

  • assume(寄存器与段的关联假设)

它假设某一段寄存器和程序中的某一个用segment...ends定义的段相关联。

通过assume说明这种关联,在需要的情况下,编译程序可以将段寄存器和某一个具体的段相联系。例如:

assume cs:codesg    ;代码段
codesg segment
start:  mov ax,0123H
        mov bx,0456H
        add ax,bx
        add ax,ax
        mov ax,4c00h
        int 21h
codesg ends
end
  1. 汇编源程序

汇编源程序:

伪指令 (编译器处理)

汇编指令 (编译为机器码)

程序:源程序中最终由计算机执行、处理的指令或数据。可以将源程序文件中的所有内容称为源程序,将源程序中最终由计算机执行处理的指令或数据,成为程序。程序最先以汇编指令的形式存在源程序中,经编译、连接后转变为机器码,存储在可执行文件中。

  1. 源程序

(1)标号

  • 一个标号指代了一个地址。

  • codesg:放在segment的前面,作为一个段的名称,这个段的名称最终将被编译、连接程序处理为一个段的段地址。

(2)Dos中的程序运行

  • DOS是一个单任务操作系统。

一个程序P2在可执行文件中,则必须有一个正在运行的程序P1,将P2从可执行文件中加载入内存后,将CPU的控制权交给P2,P2才能得以运行。P1暂停运行。而当P2运行完毕后,应该将CPU的控制权交还给使它得以运行的程序P1,此后,P1继续运行。

  • 程序返回

一个程序结束后,将CPU控制权交还给使它得以运行的程序,这个过程称为程序返回。

应该在程序的末尾添加返回的程序段:

mov ax,4c00H
int 21H

以上两条指令所实现的功能就是程序返回。

(3)汇编程序运行

下载软件MASM,之后写入程序:

.486
.model flat, stdcall
option casemap :none
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\msvcrt.lib
includelib \masm32\lib\masm32.lib
​
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\gdi32.inc
include \masm32\include\windows.inc
include \masm32\include\msvcrt.inc
include \masm32\include\masm32.inc
include \masm32\macros\macros.asm
​
.data
.code
start:
    print chr$("Hello Wordld!")
    print chr$(" ",13,10)
    mov eax, sval(input("Enter any press to continue..."))
end start
  • 创建文件: File -> New, 然后Ctrl+S保存(此处的文件的路径应与刚刚安装的盘符相同, 文件名格式为:xxx.asm)

 

  • 编译

点击Project -> Assemble ASM file进行编译,编译之后会在同级目录下生成后缀名为obj的文件

  • 链接

点击Project -> Assemble && file进行链接,链接之后会在同级目录下生成后缀名为exe的文件

  • 运行

点击Project -> Run Program, 出现如下图结果

 

  • .486

告诉汇编器应该生成486处理的伪代码,也可以使用.386。

  • .model flat, stdcall

使用平坦内存模式并使用stdcall调用习惯(stdcall指函数的参数从右往左压入,即最后的参数先压入,且函数在结束时清栈),这几乎是所有Windows API函数和dll的标准

  • option casemap :none

控制字符的映射为大写。为使“Windows.inc”文件正常工作,这里应用为“none”

  • includelib include

为了使用Windows API的函数, 需要导入dll文件。这里由静态库(.lib)完成,它们使系统能在内存的动态基地址处动态的载入dll。Includelib \masm32\lib\kernel32.lib我们不只需要包含静态库,还需要包含.inc文件,这是由l2inc工具根据库文件自动生成的。include \masm32\include\kernel32.inc 特殊的包含文件Windows.inc,其中包含了Windows API的所有常量和结构的定义。

  • .data

定义变量的地方

  • .code

代码区域

  • start ... end start

表示程序的开始/结束标签。这里不一定要用start, 可以使用任何单词和“end”语句后相同的标签。

转载链接:汇编环境搭建 -- MASM32 - 简书

由于我的系统不兼容,所以无法使用MASM.EXE/LINK.EXE等插件T T。

(4)关于编译和链接

编译:就是将指令转换为机器码。

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

  1. 操作系统的外壳

操作系统是由多个功能模块组成的庞大、复杂的软件系统。任何通过的操作系统,都要提供一个称为shell(外壳)的程序,用户(操作人员)使用这个程序来操作计算机系统工作。

DOS中有一个程序command.com,这个程序再DOS中称为命令解释器,也就是DOS系统的shell。

  • 当在DOS中直接执行.exe时,是正在运行的command将.exe中的程序加载入内存。

  • command设置CPU的CS:IP指向程序的第一条指令(即程序的入口),从而使程序得以运行。

  • 程序运行结束后,返回到command中,CPU继续运行command。

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

编程(edit)-->1.asm-->编译(masm)-->1.obj-->连接(link)-->1.exe-->加载-->内存中的程序-->运行(CPU)

  1. EXE文件中的程序的加载过程

 

也就是说,我们程序的指令,是在SA+10H开始的。

这个内城区的前256个字节中存放的是PSP,dos用来和程序进行通信。从256字节处向后的空间存放的是程序。

所以,我们从ds中得到的PSP的段地址SA,PSP的段地址SA,PSP的偏移地址为0,则物理地址为SA*16+0。因为PSP占256(100H)字节,所以程序的物理地址是SA16+0+256=SA16+16 * 16=(SA+16) * 16+0。可用段地址和偏移地址表示为:SA+10:0。

  1. 程序执行注意

要使用P命令执行int 21。(否则跑飞,下一句为NOP)如果显示“Program terminated normally”,返回到debug中。表示程序正常结束。

 

返回顺序:从.exe中的程序返回到debug,从debug返回到command。



这篇关于汇编原来是这样(一)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程