跟小甲鱼学汇编(二)——寄存器与动手写程序

前言

我们来继续学习汇编语言,承接上一次的内容,来继续学习寄存器相关的知识。

注:小甲鱼的这系列课程讲的有点乱、比较浅,新手可以入个门,老手就别跟这个教程了。

 

 


与内存互动的寄存器

内存中字的存储

在内存中,字的存储遵循基本规则:高地址放高位、低地址放低位。

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

 

DS和[address]

DS寄存器

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

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

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

DS(Data Segment):数据段寄存器

[]内存单元

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

mov  bx,1000H
mov  ds,bx
mov  al,[0]

这样就将10000H(1000:0)中的数据读到al中。

这里有个细节,1000先移入了通用寄存器BX,再将BX的数据移入了段寄存器DS,这是因为8086CPU不支持将数据直接送入段寄存器(此例中DS即是一个段寄存器)

正确路线:数据->通用寄存器->段寄存器

 

这里的[]是哪家的写法呢?

已知的MOV指令可完成的两种传送功能:

  1. 将数据直接送入寄存器
  2. 将一个寄存器中的内容送入另一个寄存器中

其实还有第三种,还可以将一个内存单元的数据直接送入一个寄存器,而这个内存单元的地址就是靠DS做段地址、操作数[XX]中的XX做偏移地址。

“[…]”表示一个内存单元,“[…]”中的数字表示内存单元的偏移地址。

不要局限于MOV,指令都是这样,操作数类型可以是数据、寄存器或内存单元。

而如上例,当我们直接去用内存单元作为操作数时,系统会自动用DS寄存器的内容做段地址,用我们给的内存单元里的地址做偏移地址,得到最后的物理地址的内容。

小实践

写几条指令,将al中的数据送入内存单元10000H?

mov  bx,1000H
mov  ds,bx
mov [0],al

 

字的传送

规则

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

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

在汇编操作的时候,如果是给8位寄存器/内存传,则按字节传;如果是给16位寄存器传,则按字传。

实验

我们来一个小实验,打开你的DEBUG

首先我们用命令来填满10000~10003:

-e 1000:0 23 11 22 11

我们可以使用命令来查看内存情况:

-d 1000:0

就可以看到这部分的内存已经填充好了刚才填充的内容。

下面,我们来填写指令(注意,我的机子的CS:IP是073F:0100,所以我将指令填在这里了,你的要根据自己的情况来看):

-a 073F:0100
mov ax,1000
mov ds,ax
mov ax,2c34
mov [0],ax
mov bx,[0]
sub bx,[2]
mov [2],bx

11316转十六进制就是2C34

下面就靠t命令来一步一步地执行,一步一步地分析。

这些指令执行完后,1000:0的内存前四个字节依次是:

34  2C  12  1B

寄存器中,AX=2C34,BX=1B12,DS=1000

 

mov、add、sub指令

mov指令的形式

已学mov指令的几种形式:

  • mov 寄存器,数据
  • mov 寄存器,寄存器
  • mov 寄存器,内存单元
  • mov 内存单元,寄存器
  • mov 段寄存器,寄存器

其实下面也是可以行得通的:

  • mov 寄存器,段寄存器

加减指令

同样,对于加减指令,有:

这后面还有超级多的汇编指令,一个个这样细说有些繁琐也不好整理,后面我有的指令就简单一带了,这里推荐大家一个好工具——汇编金手指,我这边提供一个下载链接:

链接:https://pan.baidu.com/s/1Bk7NPfc6iIrTW3WUmWN9Qg
提取码:7d04

 

数据段

定义

前面讲过,对于8086机,我们可以根据需要将一组内存单元定义为一个段(可以是代码段、数据段等)。

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

访问

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

操作实例

我们将123B0H~123BAH的内存单元自定为数据段,我们现在要累加这个数据段中的前3个单元中的数据

先给123B0填一些数据:

-e 123b:0 11 22 21

然后找个空间我们写一些代码(-a),最好是CS:IP的位置,一会直接执行了。

代码如下:

mov ax,123B
mov ds,ax
mov al,0
add al,[0]
add al,[1]
add al,[2]

不出意外,最后我们的AX的低两位就变成了54。

 

概念与特性

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

操作

  • 入栈:将一个新元素放到栈顶
  • 出栈:从栈顶取出一个元素

CPU提供的栈机制

现今的CPU都有栈的设计

8086CPU提供相关的指令来以栈的方式访问内存空间。

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

栈也是人为主观的概念,对CPU来说都是数据而已

指令

8086CPU提供入栈和出栈指令:

  • PUSH(入栈)
    • PUSH  ax:将寄存器ax中的数据送入栈中
    • PUSH  内存单元:将一个内存单元处的字入栈(栈操作都是以字为单位)
  • POP(出栈)
    • POP ax:从栈顶取出数据送入ax
    • POP 内存单元:出栈,用一个内存字单元接收出栈的数据

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

CPU执行PUSH/POP指令时是两个步骤,MOV只是一个

实例

下面举例说明,我们可以将10000~1000F这段内存当作栈来使用。

下面一段指令的执行过程:

mov ax,0123
push ax
mov bx,2266
push bx
mov cx,1122
push cx
pop ax
pop bx
pop cx

我们将数据0123、2266、1122依次送入栈,此时AX=0123、BX=2266、CX=1122;然后我们又弹出栈并将数据送给AX、BX、CX,最后AX=1122、BX=2266、CX=0123。

栈的寄存器

上例看完我们不禁疑问,CPU怎么知道哪一段内存作为栈的?再者,执行PUSH、POP的时候,CPU又怎么知道哪里是栈顶呢?

这主要是靠SS和SP。

  • SS(Stack Segment):栈段寄存器,存放栈顶的段地址
  • SP(Stack Pointer):栈指针,存放栈顶的偏移地址

任何时刻,SS:SP都指向栈顶元素。

PUSH指令的执行过程

  • push ax
    • SP=SP-2
    • 将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶

当我们的栈是空着的时候,我们的SP指针指向栈空间最高地址单元的下一个单元。

POP运算同理,先将数据复制出去,SP再加二。

POP命令

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

这里注意POP的时候只是将数据复制出去,那个栈空间的数据并没有消失,下次PUSH的话会被新数据覆盖。

引申出来,硬盘格式化后还是可以恢复,因为硬盘格式化只是清零了一个索引,内部数据其实并没有删除,只是等着被新数据覆盖而已。

 

栈的应用

栈的应用其实是非常广泛的,这里举个小例子,还记得编程语言中的局部变量吗?为什么在定义局部变量的方法外局部变量就失效了呢?

因为它是放在栈里面的变量,只要运行结束这个方法,那么这个局部变量也就被“踢出”栈了。

在实际编程时,我们其实不用很考虑栈这个东西,因为它一般是系统自动分配的,我们更多地考虑的是“堆”,我们山水有相逢,慢慢就学到了。

栈顶越界问题

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

可是,如何能够保证在入栈、出栈时,栈顶不会超出栈空间。

当栈空间已经满了,我们再PUSH数据,那就新数据就会覆盖到栈空间以外的地方(SS更低的地址);当栈空间已经空了,我们再POP数据,那就会弹出栈空间以外的数据了,同时SP指针也就指向了不该指向的地方。这两种情况就是“栈溢出”,栈顶越界是十分危险的。

我们既然将一段空间安排为栈,那么在栈空间之外的空间里很可能存放了具有其他用途的数据、代码等,这些数据、代码可能是我们的程序中的,也可能是别的程序中的。我们入栈出栈时的不小心,而将这些数据、代码以外地改写,将会引发一连串的错误。

而对于栈顶越界问题的解决方案,通常是依赖CPU:比如在CPU中有记录栈顶上限和下限的寄存器,我们可以通过填写这些寄存器来指定栈空间的范围,然后,CPU在执行PUSH指令的时候靠检测栈顶上限寄存器,在执行POP指令的时候靠检测栈顶下限寄存器保证不会越界。

不过8086CPU中是没有这样的设置的。

我们在编程时要自己操心栈顶越界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界。

执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。

 

栈段

定义

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

这仅仅是我们在编程时的安排,CPU只会根据SS:SP来去木那地执行PUSH、POP。

栈空指向问题

一般情况下,当堆栈是空的时候,SP指向栈底,此时SP的值取决于你的堆栈区大小,例如,你设置栈空间为1KB,则SP=400H。

而SP作为16位寄存器,最大的时候可以将栈空间设置为64KB,而若这种情况下,栈空时,SP=0000H,当第一个元素压栈(PUSH),SP借位指向FFFEH。如果不断压栈,直到栈满,则再次压栈后,原栈内容将开始被覆盖。

 

小骚话

一段内存,既可以是代码的存储空间,也可以是数据的存储空间,还可以是栈的存储空间,也可以什么都不是,关键在于寄存器们的指向。

 

 

 


开始编程

现在我们将开始编写完整的汇编语言程序,用编译器将它们编译成为可执行文件(如:*.exe文件),在操作系统中运行。

源程序从编写到执行的过程

编写汇编源程序

使用文本编辑器(如记事本、Nodepad++、UltraEdit等),用汇编语言编写汇编源程序。

对源程序进行编译链接

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

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

这里插入一条注意:注意,对于Masm编译器,mov ax,[1],它会理解为mov ax,1,;对于前面的Debug则可以真正理解我们的意思,这应该是一个编译器的问题,我们知道就好了

生成可执行文件并执行

可执行文件中包含两部分内容:

  • 程序(从源程序中的汇编指令翻译过来的机器码)和数据(源程序中定义的数据)
  • 相关的描述信息(比如:程序有多大、要占多少内存空间)

生成可执行文件后,在操作系统中就会执行可执行文件的程序。

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

学习源程序(上)

源程序模板

下面给出一个经典的源程序的模板:

assume cs:codesg
codesg seqment
start: mov ax,0123H
         mov bx,0456H
         add ax,bx
         add ax,ax
         mov ax,4c00H
         int 21H
codesg ends
end

下面我们来依次学习

汇编指令

start: mov ax,0123H
         mov bx,0456H
         add ax,bx
         add ax,ax
         mov ax,4c00H
         int 21H

有对应的机器码的指令,可以被编译为机器指令,最终为CPU所执行。

伪指令

assume cs:codesg
codesg seqment
…………
codesg ends
end

没有对应的机器码的指令,最终不被CPU所执行。

伪指令是由编译器来执行的指令,编译器根据伪指令来进行相关的编译工作。

定义一个段

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

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

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

段名 segment
段名 ends

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

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

End伪指令

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

如果程序写完了,要在结尾处加上伪指令End,否则,编译器在编译程序时,无法知道程序在何处结束。

ends是段的结束,end是整个程序的结束

寄存器与段的关联假设

assume:含义为“假设”。

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

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

注意

我们可以将源程序文件中的所有内容称为源程序,将源程序中最终由计算机执行处理的指令或数据,称为程序。

程序最先以汇编指令的形式存在源程序中,经编译、连接后转变为机器码,存储在可执行文件中。

学习源程序(下)

标号

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

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

有点像C语言里面的指针

DOS中的程序运行

我们的程序最先以汇编指令的形式存在源程序中,经编译、链接后转变为机器码,存储在可执行文件中,那么,它怎么样得到运行呢?

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

一个程序P2在可执行文件中,则必须有一个正在运行的程序P1,将P2从可执行文件中加载入内存后,将CPU的控制权交给P2,P2才能得到运行。

P2开始运行后,P1将暂停运行。

而当P2运行完成后,再将CPU的控制权转交给P1,然后P1继续运行。

程序返回

通过上例,我们知道,一个程序结束后,将CPU的控制权交还给使得它可以运行的程序,我们称这个过程为:程序返回。

那么具体如何实现程序的返回呢?

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

mov  ax,4C00H
int 21H
这两条指令所实现的功能就是程序返回

这里我们可以简单小结一下:

语法错误和逻辑错误

语法错误:程序在编译时被编译器发现的错误。

逻辑错误:编译器不能发现,在运行时人为发现的错误。

 

上手小操

编译环境

我们的汇编编译器和链接器使用的是著名的Masm,这边我给大家提供一个6.15版本的Masm

链接:https://pan.baidu.com/s/1ZMAcCZeQq-JE0we-BB7uUA
提取码:u1sv

解压之后,即可得到Masm等可执行文件,这边建议直接配置好系统路径,方便直接调用执行。

编写程序

下面我们来实战操练一下,打开你的编辑器,我这边是Notepad++,创建一个文件:test.asm(.asm即为汇编文件)

assume cs:abc
abc segment
     mov ax,2
     add ax,ax
     add ax,ax
     mov ax,4c00H
     int 21H
abc ends
end

在Notepad++中的语言里选择A类的Assembly,即为“汇编语言”

汇编语言中,“;”后面即为注释

常见错误

一般来说,有两类错误使我们得不到所期待的目标文件:

  1. 我们的程序中有“Severe Errors”
  2. 找不到所给出的源程序文件

开始编译

假设你已经将Masm的路径配置到Window绝对路径下面了,好,下面我们找到源代码所在的目录,打开对应目录的cmd窗口,然后输入命令:

C:\Users\lenovo\Desktop\test>masm test.asm

然后会显示如下:

Microsoft (R) MASM Compatibility Driver
Copyright (C) Microsoft Corp 1993.  All rights reserved.

 Invoking: ML.EXE /I. /Zm /c /Ta test.asm

Microsoft (R) Macro Assembler Version 6.15.8803
Copyright (C) Microsoft Corp 1981-2000.  All rights reserved.

 Assembling: test.asm

恭喜你,编译成功,此时在该目录下,就会生成一个test.obj,这就是我们编译生成的目标文件。

开始链接

仅仅是编译还不是可执行文件,下面我们将刚才的目标文件链接起来。

C:\Users\lenovo\Desktop\test>link test.obj

然后它会出现一些互动选项,这里我们都先回车过掉(即选择它默认的选项)即可

Microsoft (R) Segmented Executable Linker  Version 5.60.339 Dec  5 1994
Copyright (C) Microsoft Corp 1984-1993.  All rights reserved.

Run File [test.exe]:
List File [nul.map]:
Libraries [.lib]:
Definitions File [nul.def]:
LINK : warning L4021: no stack segment
LINK : warning L4038: program has no starting address

上面的两个warning分别是说,没有栈段、没有入口地址。分析源代码,我们这就是一个最简单的程序。

链接的作用:

  1. 当源程序很大时,可以将它分为多个源程序文件来编译,每个源程序编译成为目标文件后,再用链接程序将他们链接到一起,生成一个可执行文件。
  2. 程序中调用了某个库文件中的子程序,需要将这个库文件和该程序生成的目标文件链接到一起,生成一个可执行文件。
  3. 一个源程序编译后,得到了存有机器码的目标文件,目标文件中的有些内容还不能直接用来生成可执行文件,链接程序将这些内容处理为最终的可执行信息(所以,在只有一个源程序文件,也不需要调用某个库的子程序的情况下, 也必须用链接程序对目标文件进行处理,生成可执行文件)

运行

下面我们在cmd窗口中运行刚刚链接生成的cmd程序:

阿偶,我的电脑弹出了警告,大意是这个程序不能在64位机器上执行,没关系,我们打开DOSBox,用它来执行,这边需要用命令挂载一下虚拟磁盘:

mount c C:\Users\lenovo\Desktop\test
c:

然后执行:

test.exe

然后好像什么也没有发生,其实这里应该已经执行了这个程序了,只是速度太快没有什么显示而已。

我们的程序没有向显示器输出任何信息。程序只是做了一些将数据送入寄存器和加法的操作,而这些事情,我们不可能从显示屏上看出来。

没关系,我们以后学到输入输出指令会有更有好的反馈。

 

分析执行过程

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

DOS(磁盘操作系统:Disk Operating System)中有一个程序command.com,这个程序在DOS中称为命令解释器,也就是DOS系统的shell。

  1. 我们在DOS中直接执行1.exe时,是正在运行的command将1.exe中的程序加载入内存。
  2. command设置CPU的CS:IP指向程序的第一条指令(即程序的入口),从而使程序得以运行。
  3. 程序运行结束后,返回到command中,CPU继续运行command

我们在windows中用的cmd只能说是用于执行DOS命令的一个shell程序,这些概念要缕清。

 

单步运行

为了观察控制,我们可以使用Debug。

Debug可以将程序加载入内存,设置CS:IP指向程序的入口,但Debug并不放弃对CPU的控制,这样,我们就可以使用Debug的相关命令来单步执行程序,查看每条指令的执行结果。

我们修改一下刚才的程序,添加一个入口地址,利用start标号:

assume cs:abc
abc segment
start:  mov ax,2
        add ax,ax
        add ax,ax
        mov ax,4c00H
        int 21H
abc ends
end start

start只是一个标号,你可以换成你喜欢的别的字符串

下面就是将这个新程序编译一下,链接一下,生成新的可执行文件。

这里,因为我的Win10不能运行这个汇编程序,故我计划用DOSBox来实验。

我将新生成的可执行文件放在了C盘下面,和我的DEBUG在同一个目录下,然后打开DOSBox:

mount  c  c:\
c:

这里我们就在虚拟的DOS里创建了一个C盘符,直接目录下有debug.exe、test.exe,下面我们开始单步执行。

开始Debug这个程序:

debug test.exe

进入debug后,我们来使用r查看一下各个寄存器的情况:

其中CS是我们这个程序的长度。整个程序占12个字节。

下面我们来不断用t命令来单步执行程序,这段就不截图了,应该没有问题,就是感受一下。

注意,在最后准备执行int 21(即中断返回)命令时,我们要用P命令来执行。

最后显示:“Program  terminated normally”,即执行结束。

如果最后你还是用T命令来执行Int 21,则程序会跳到一个莫名其妙的地址,这里算是非正常结束了。

 

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

这也就是为什么上例图中DS比CS小0100H,因为这个0100H(即256个字节)是该程序的PSP段,这个段被DOS用来和被加载的程序进行通信。

CS则在PSP的下面,所以DS比CS小0100H

程序运行时的追踪

我们在DOS中用“Debug  1.exe”运行Debug对1.exe进行跟踪时,程序加载的顺序是:command加载Debug、Debug加载test.exe,返回时的顺序相反。

 


 

 

 

 

 

商业转载 请联系作者获得授权,非商业转载 请标明出处,谢谢

发表评论

邮箱地址不会被公开。 必填项已用*标注