前言

从0开始的汇编语言系列,选用的参考书籍是清华大学出版社,王爽老师的《汇编语言第四版》。该系列属于博主的笔记系列,文中会采用一些书中的例子,图片以及思考题供读者阅读,如需详细学习汇编语言可以购入一本,谢谢。

学习之前我们做如下约定(随着学习深入还会出现新的约定):

  1. 十六进制数均以H结尾
  2. 使用8086CPU作为案例
  3. 我们使用(地址或寄存器名称)表示一个寄存器或一个内存单元的内容,()内地址是且一定是物理地址
  4. 我们将idata视作常量

话不多说我们马上开始。

对前言更新的解释

相信小伙伴们都发现了,我们的老伙伴前言终于更新啦!!这次更新了两条第一条是使用(),第二条是idata,我们一一说明。

首先是(地址或寄存器名称),为了描述上的简洁以后我们就都用(地址或寄存器名称)来表示一个内存单元或者寄存器中的内容啦。比如(ax)就代表寄存器ax中的内容,(20000H)代表20000H处内存单元中存放的内容。至于()所得到的内容到底是字型数据还是字节型数据要根据具体的运算决定,()中可以出现三种元素:

  1. 寄存器名
  2. 段寄存器名
  3. 地址(注意这里的地址是且必须是一个物理地址)

所以(AX)、(DS)、(AL)、(20000H)、((ds)×16+(bx))都是正确的使用方式,但(2000:0)这样是不被允许的。学会了这个我们举一个应用的实际例子,对于PUSH AX的过程我们可以这样描述:

1
2
(SP)=(SP)-2
((SS)×16+(SP))=(AX)

其次是idata,这个很简单,“[0]”偏移地址过去用此类形式表示,我们使用idata代替0,1,2,3这样的常量。

(Tips:寄存器DS不可以直接使用mov指令对其赋值。)

[BX]与内存单元的描述

相信看到”[]”有的小伙伴已经展开回忆了,我们先来复习一下。

1
2
MOV AX,[0]
MOV AL,[0]

这就是[]最基本的使用方法了,CPU在执行这两条指令的时候从寄存器DS中取出段地址,在[]中取出偏移地址,组成物理地址后再进行其他的处理。这两条指令分别完成了这样两件事:

第一条指令将一个长度为2的内存单元中的内容放在了寄存器AX中。

第二条指令将一个长度为1的内存单元中的内容放在了寄存器AL中。

注意这其中的不同点,对于不同大小的寄存器,CPU送入的数据大小也不相同。是的,我们在完整的描述一个内存单元需要两种信息:

  1. 内存单元的地址
  2. 内存单元的大小

其中内存单元的地址会由寄存器DS与[address]指明,内存单元的大小会由具体的操作对象指出。[BX]也是指出偏移地址的作用,只不过[BX]的意思是偏移地址在寄存器BX中。

试着用用[BX]

这里我们通过一段代码理解[BX]。(Tips:inc指令代表自增1,当还有一个dec指令代表自减1。)

1
2
3
4
5
6
7
8
9
10
11
12
13
mov ax,2000H  
mov ds,ax
mov ax,[bx]
inc bx
inc bx
mov [bx],ax
inc bx
inc bx
mov [bx],ax
inc bx
mov [bx],al
inc bx
mov [bx],al

其中0地址字单元内容为00BEH,2100:2-2100:7为空,试着写出程序运行完后的21000H-21007H单元中的内容。

答案:

21000H~21006H BE 00 BE 00 BE BE BE 21007H为空。

看完这段代码相信你也学到了一招,可以使用[BX]与inc指令实现偏移地址的改变。

LOOP指令

loop n. 循环。相信学习过C,JAVA等高级语言的小伙伴对循环一定不陌生,循环为我们简化代码做出了极大地贡献,在汇编语言中也是如此。先给大家剧个透loop指令有点像do····while。先来看一段使用loop指令的代码:

1
2
3
4
5
6
7
8
9
10
assume cs:code 
code segment
mov ax,2
mov cx,11
s: add ax,ax
loop s
mov ax,4c00H
int 21H
code ends
end

可能有的小伙伴已经可以猜的八九不离十了,没错这段代码计算了2的12次幂。

我们来根据这段代码讲述一下loop指令到底怎么工作的,当程序运行至loop时,首先做了(cx)=(cx)-1,然后判断cx中的值,不为0则跳转标号(也就是程序中的S)处执行,若为0则结束loop指令继续向下执行。标号S在程序运行阶段会变成一个地址,loop指令在判断CX的值不为0后,loop s(地址)将寄存器IP设置为S。了解了loop的工作机理后,我们发现寄存器CX的值会影响loop指令的执行结果,是的,通常我们用loop指令实现循环,会用寄存器CX存放循环次数,当然这也不是绝对的,不同场合,我们会有不同的处理方式。注意哦!loop指令不会跳过内部的程序段,它一定是先执行一次在去执行loop指令,这点和do····while很像。

Debug和MASM对指令的不同处理

这里的内容是为了下面的学习顺利进行所提供的预备知识。

还记得吗?我们在Debug中写过这样的指令:

1
mov ax,[0]

这条指令表示将DS:0处的数据送入AX中,但是在汇编源程序中,MASM会把它看做:

1
mov ax,0

OMG!这可不好,这完全是两种意思。那我们要在源程序中将内存单元的内容送入寄存器中怎么办呢?有两种解决办法:

1.使用[BX]

先将段地址送入DS中,再将偏移地址送入BX,使用MOV AL,[BX]就可以将((DS)×16+(BX))中的内容送入AL中啦。(Tips:寄存器DS不可以直接使用mov指令对其赋值。)

2.在偏移地址前显示指出段地址

这个方法就比较简单了,我们将段地址送入DS后,只需要使用DS,在偏移地址前显示的指明段地址如MOV AL,DS:[0]这样就可以将((DS)×16+(0))中的内容送入AL里啦。

段前缀

上面我们讲到了两种方法用来解决MASM操作内存单元的问题,其中第二种方法使用了段寄存器显示指明段地址的方式,当然了这个段寄存器不止可以是DS还可以是CS、SS、ES这都可以用来指明内存单元的段地址,CS:、SS:、ES:、DS:在汇编语言中被称作段前缀。

一段安全的空间

还记得吗?我们最最开始讲到过的内存地址空间,我们抽象了各个硬件的内存将他们汇总为一个整体叫做内存地址空间,其中有一部分存放着一些重要的系统数据和代码,这就意味着随意的修改一个地址中的内容是十分危险的。比如修改0:26H处内容,这将导致你的DOSBOX卡死,这是因为0:26H处放着重要的系统数据。可见,我们不可以在不能确定一段内存空间是否存放重要的数据或代码时,随意的向其中写入内容,我们要使用操作系统分配给我们的内存空间,下一章我们会对这一空间有所认识。

因为运行在CPU实模式下的DOS无法对硬件进行严格的管理,所以我们可以真正的去尝试、理解、体会硬件的工作,诸如Windows、Unix这些运行在CPU保护模式下的操作系统中,想要使用汇编语言去操作硬件这是不可能的。而且我们使用DOXBOX也不需要为这些危险行为买单,所以不要害怕这些危险操作。

虽然我们不需要为危险行为买单,但在学习过程中,因为不小心修改了某个关键的系统数据就要从头来过还是非常痛苦的事情,所以我们要找到一段安全的空间供我们使用。一般来讲0:200~0:2ff这256个字节空间就是非常好的选择。至于为什么一定是这里安全,我们以后会再讨论。