0%

今天晚上的学习任务是用汇编写一个可执行程序,接下来开始吧

第一个程序

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

  1. 编写汇编原程序

    产生一个存储源程序的文本文件

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

    先使用汇编语言编译程序对源程序进行编译,产生目标文件;再用连接程序对目标程序进行连接,生成可在系统中运行的可执行程序

    可执行程序包含两部分内容:

    • 程序(从源程序中的汇编指令翻译过来的机器码)和数据(源程序中定义的数据)
    • 相关的描述信息(比如,程序有多大,要占用多少内存空间)
  3. 执行可执行文件中的程序

    在操作系统中,执行可执行文件中的程序

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

源程序

我们以一段汇编语言源程序为例

1
2
3
4
5
6
7
8
9
10
11
12
13
assume cs:codesg
codesg segment

mov ax,0123H
mov bx,0456H
add ax,bx
add ax,ax

mov ax,4c00H
int 21H

codesg ends
end

伪指令

在汇编语言源程序中包含两种指令。一是汇编指令,二是伪指令。

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

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

我们先对这段程序中的三处伪代码进行说明:

1
2
3
XXX segment
...
XXX ends

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

其功能是定义一个段,segment 用来定义一个段的开始,ends用来定义一个断的结束

通常一个源程序是由多个段组成的,一个程序中所有要被处理的信息:指令,数据,栈被划分到了不同的段中

1
end

end是汇编程序结束的标记,编译器在进行编译时,如果碰到了伪指令end就结束对源程序的编译

1
assume cs:codesg

这条伪指令的含义为”假设”。它假设某一段寄存器和程序中的某一个用 segments...ends定义的段相关联

这一段程序的含义便是将 codesg段与 cs段寄存器相关联

程序返回

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

这两条指令实现的功能便是程序返回:

1
2
mov ax,4c00H
int 21H

我们暂时无法理解这两句的含义,不必深究

编辑源程序

我们在edit程序中编辑程序

image.png

我们将其保存至C盘中为1.asm

编译

我们使用masm对其进行编译

image.png

我们一一分析这些信息的作用:

  1. [.ASM]:提示我们默认的文件拓展名是asm,当我们输入名称XXX便在当前目录下调用XXX.asm(如果要用其他拓展名则需输入完全)
  2. [1.OBJ]:提示我们我们生成目标文件为1.obj ,我们可以在后面指定生成的路径,也可以用Enter跳过,使用当前文件夹
  3. [NUL.LST]:提示输入列表名称,这个文件是编译器翻译源程序的过程中的中间结果,使用Enter跳过
  4. [NUL.CRF]:提示输入交叉引用文件的名称,也是中间产物,跳过
  5. 当出现下面的标志后,代表编译成功结束

连接

我们在得到目标文件后,需要对目标文件进行连接,从而得到可执行程序

image.png

我们接着分析这些信息:

  1. [.OBJ]:提示我们默认的文件拓展名是obj,当我们输入名称XXX便在当前目录下调用XXX.obj(如果要用其他拓展名则需输入完全)
  2. [1.EXE]:提示我们我们生成目标文件为1.exe,我们可以在后面指定生成的路径,也可以用Enter跳过,使用当前文件夹
  3. [NUL.MAP]:提示输入映像文件的名称,这个文件是目标文件生成可执行程序的中间结果,使用Enter跳过
  4. [.LIB]:提示输入库文件的名称,库文件里面包含了一些可以调用的子程序,如果调用了某一个库文件的子程序,就需要在连接时,将这个库文件和目标文件连接在一起生成可执行程序,这里我们跳过
  5. 最后显示出现了“没有栈段”的错误,我们直接忽视,此时连接成功

在下面我们简单的介绍以下连接的作用:

  • 当源程序很大时可以分为多个源程序文件生成目标文件,最后再将目标文件连接到一起
  • 程序中调用了某个库文件的子程序,需要将这个库文件和目标文件连接到一起生成可执行程序
  • 一个源程序在编译后,得到了有机器码的目标文件,目标文件中的内容还不能直接生成可执行程序,所以需要连接程序处理

以简化的方式进行编译和连接

image.png

直接在命令后面加一个 ; 可以直接忽略中间产物的生成,实现快速的编译连接

程序执行过程的跟踪

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

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

我们先展示一下EXE文件中程序加载的过程:

image.png

我们可以根据这副图得到以下信息:

  • 程序加载后ds中存放着程序所在内存区的段地址,这个内存区的偏移地址为0,则程序的内存区的地址为ds:0

  • 这个内存区的前256个字节存放的是PSP,DOS用来用来和程序进行通信。从256个字节之后存放的是程序

    因为PSP占256(100H)个字节,所以程序的物理地址是:

    SA * 16 + 0 + 256 = SA * 16 + 16 * 16 + 0 = (SA + 16) * 16 + 0 = (SA + 10H) + 0

    可以用段地址和偏移地址表示为 SA+10H:0

程序执行过程的跟踪

我们以刚刚的程序1.exe为例:

image.png

我们可以看到图中DS的值为075AH,则PSP的地址为075A:0 ,程序的地址为076A:0(即075A + 10:0)

同时可以看到从076A:0000~076A:000F都是我们的程序的机器码

现在我们开始单步执行跟踪:

image.png

当我们执行到 INT 21时需要用P指令退出程序,最后再使用Q指令返回command

昨天我们学习了简单寄存器的使用DEBUG程序的使用

这一篇我们将学习 寄存器的内存访问

寄存器(内存访问)

从访问内存的角度认识学习寄存器i

我们知道一个字的存储分为高字节和低字节,由于内存地址是自上而下向下递增的,所以高位字节从内存分布上看再地位字节的下面

也就是说当我们从0地址开始存放 数值20000(4E20H)

其在内存空间中的顺序为

1
2
3
4
5
+---------+----------+-----------------------------------
| 0 | 1 | ...
+---------+----------+-----------------------------------
| 20 | 4E | ...
+---------+----------+-----------------------------------

这样的分布特点我们称之为 小端序

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

DS和[address]

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

我们用下面的例子来展示它的用法,比如读取10000H单元的内容:

1
2
3
mov bx,1000H
mov ds,bx
mov al,[0]

我们通过上面的三个指令,实现读取,接下来一一解释

首先是前两句,为什么不能直接 mov ds,1000H呢?这是8086CPU的硬件问题,我们并不支持此行为,所以我们用一个寄存器来中转

第三句的 […] 又是什么意思呢? […]表示操作对象是一个内存单元,里面的数值代表内存单元的偏移地址

mov,sub,add指令

首先我们需要知道这三个指令的特点:他们都有两个操作对象

MOV指令

我们先看看至今我们所知的mov的用法:

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

根据这些我们可以合理的猜测一些其他的用法,并使用DEBUG程序来验证:

  • mov 内存单元,段寄存器 验证通过
  • mov 寄存器,段寄存器 验证通过
  • mov 段寄存器,内存单元 验证通过

SUB ADD指令

他们也可以有以下用法:

  • add 寄存器,数据
  • add 寄存器,寄存器
  • add 寄存器,内存单元
  • add 内存单元,寄存器
  • sub 寄存器,数据
  • sub 寄存器,寄存器
  • sub 寄存器,内存单元
  • sub 内存单元,寄存器

数据段

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

将一段内存作为数据段,是我们编程时的一种安排,我们可以在具体操作时,用DS存放数据段的段地址,从而进行访问

比如一段数据段 123B0H~123B9H 的内存单元定义为数据段 ,现在要累加这个数据段的前三个内存单元的值

1
2
3
4
5
6
mov ax,123B
mov ds,ax
mov al,0 ;给ax赋值为0
add al,[0] ;加上数据段的第一个值
add al,[1] ;......
add al,[2]

8086CPU 提供相关的指令来以栈的方式访问内存空间。这意味着,在基于8086CPU编程时,可以将一段内存作为栈来使用

8086CPU提供入栈和出栈指令,分别时POP和PUSH

  • push ax 将ax中的数据送入栈中
  • pop ax 从栈顶取出数据送入ax中

注意: 入栈和出栈的操作都是以字为单位进行的

SS:SP

CPU是怎么知道栈顶的位置呢?

在8086CPU中,有两个寄存器,分别是段寄存器SS 和 寄存器SP,栈顶的段地址存放在SS中,栈顶的偏移地址存放在SP中。任意时刻,SS:SP指向栈顶元素。执行pop和push时,CPU从SS和SP中得到栈顶的地址。

现在我们可以对pop和push进行完整的描述了:

push:

  1. SP = SP - 2,SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶
  2. 将ax中的内容送入SS:SP指向的内存单元,SS:SP 此时指向新的栈顶

pop:

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

当栈顶的数据出栈之后,其内存单元所存储的数据仍然存在,但其已经不在栈顶中。当再次进行入栈操作时,直接对其数据进行覆盖

pop push指令

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

push,pop可以是指令格式:

  • push,pop 寄存器
  • push,pop 段寄存器
  • push,pop 内存空间

栈顶超界问题

我们在此讨论一个问题,虽然我们可以通过SS 和SP来确保在进行入栈和出栈时找到栈顶。可是怎么保证栈顶不会超出栈空间呢?

当我们把一个空间容量为16个字节的内存空间当作栈时,向其中压入八个字后就已经达到了栈顶,此若是再使用push操作,其数据便会溢出栈空间,覆盖栈以外的数据。

同理当我们已经达到栈底时,我们再进行一次pop操作,我们会把栈空间以下的数据弹出。

以上这些操作我们都称为 栈顶越界问题

如果CPU中有记录栈顶上限和栈底的寄存器,那么可以检测越界问题。但是,在8086CPU中,并没有这个寄存器,因此其不保证我们对栈的操作不会越界。这一点需要操作者自行考虑

栈段

如果我们设置一段内存,将它当作栈,并以栈的形式进行访问,那么我们可以称之为 栈段

这里我们有一个问题,如果将10000H~1FFFFH这段空间当作栈段,SS = 1000,SP = FFFE。也就是说,此时栈段内还有一个数据,如果我们将这个数据进行出栈操作,那么此时SP = ?,栈顶指向哪里?

由于出栈后,SP = SP + 2,栈顶指向最底部单元下面的单元。所以此时SP = 0

那么我们可以说 SP=0,此时即是空栈也是满栈

实验二:用机器指令和汇编指令编程

正如前文所言,我们使用D命令查看内存单元的命令,那么我们有以下疑问:

  • Debug是靠什么来执行D命令的? 是一段程序

  • 谁来执行这段程序? 用CPU

  • CPU在访问内存单元时从哪里得到内存单元的段地址? 从段寄存器得到

所以我们得出结论 在处理D命令的程序段中,必须有将段地址送入段寄存器的代码

段寄存器有4个:SS,ES,CS DS,那么将段地址送入那个段寄存器呢?

由于CS要用来指向处理D命令的代码,而SS要作为指向栈顶的代码。再因为一般默认段地址再DS中,所以我们将段地址送入DS中

下一条指令执行了嘛?

我们有这样一个程序:

1
2
3
4
5
6
7
mov ax,2000
mov ss,ax
mov sp,10
mov ax,3123
push ax
mov ax,3366
push ax

我们使用T命令单步执行,看一看发生了什么?

image.png

注意看,在执行 mov ss,ax之后本来应该是 mov sp,10但是却直接来到了 mov ss,ax

但通过观察SP的值,我们可以知道 mov sp,10得到了执行,这是为什么呢?

这是因为设计到了之后的一个内容:中断机制

在这里我们只需要知道,T命令在执行修改寄存器 SS 的指令时,下一条指令也被紧接着执行

搞了那么久的博客现在终于可以开始汇编语言的学习了

这里我用的教材是王爽老师的《汇编语言》,使用的环境是基于DOSBOX的模拟DOS环境

接下来开始正式的学习

今天的内容是 寄存器实验一

寄存器

8086CPU共有14个寄存器,每个寄存器都有对应的名称,他们分别是:

AXBXCXDXSIDISPBPIPCSSSDSESPSW

我们不一次性的对其进行研究,我们在后续对这些寄存器的作用进行一一的讲解

通用寄存器

AX,BX,CX,DX 这四个寄存器用来放一般性的数据,所以被称为通用寄存器

8086的所有寄存器都是16位的,可以存放两个字节

为了保证CPU的兼容性,这四个通用寄存器都可以被分为两个可独立使用的八位寄存器:

  • AX = AH + AL
  • BX = BH + BL
  • CX = CH + CL
  • DX = DH + DL

XL 指的是 XX寄存器的低八位(0-7位),XH 指的是 XX 寄存器的高八位(8-15位)

字在寄存器中的存储

出于对兼容性的考虑,8086CPU可以对以下两种尺寸的数据进行操作:

  • 字节(Byte):一个字节由八位组成,可以存储在八位寄存器中
  • 字(Word): 一个字由两个字节组成,这两个字节分别被称为这个字的高位字节和地位字节

一个字可以存在一个16位寄存器中,那么这个字的高位字节和低位字节便存储在这个寄存器的高8位寄存器和低8位寄存器中

基本的汇编指令

介绍 mov 与 add

这里我们用几个例子来展示他们的用法

MOV:

  • mov ax,18 -> 将18送入寄存器ax中
  • mov ah,15 -> 将15送入寄存器ah中
  • mov ax,bx -> 将寄存器bx中的值送入寄存器ax

ADD:

  • add ax,8 -> 将寄存器ax中的值加上8
  • add ax,bx -> 将ax和bx的值相加,并将结果保存在ax中

注意:

  1. 十六位寄存器的溢出:

    当ax = bx = 8226H时,执行 add ax,bx 后ax = ?

    ax本来应该等于 1044CH,但是由于最高位溢出了,所以ax = 044CH

  2. 八位寄存器的溢出:

    当 ax = 00C5H时,执行 add al,93H 后al = ?

    ax本来应该等于 0158H,但是由于最高位溢出了,所以ax = 0058H,al = 58H

8086给出物理地址的方法

所有的内存单元构成的存储空间是一个一维的线性空间,每一个内存单元在这个空间都有唯一的地址,我们将这个唯一的地址称为物理地址。在CPU发出物理地址之前,必须要在内部先生成这个地址

8086CPU有20位地址总线,最多可以传20位地址,达到1MB的寻址能力

可是8086是16位机器,按道理只能做到16位的64KB寻址,是怎么做到的呢?

这是因为我们使用了一种通过两个十六位地址合成一个二十位地址的方法

当8086CPU要读写内存时:

  • CPU相关部件提供两个地址:一个是16位段地址,一个是16位偏移地址
  • 这两个地址通过内部总线传输到地址加法器合成为一个20位的物理地址
  • 将这20位的物理地址传输到地址总线上

在这里地址加法器通过 **物理地址 = 段地址*16 + 偏移地址** 生成20位的物理地址

注意:段地址*16 本质上就是将段地址左移四位,将地址XXXXH变为XXXX0H

段的概念

在根据编程需要时我们将若干连续的内存单元看作一个段

先讲解一下的含义,在这里,段并不意味着内存被分为一段一段,而是我们可以通过分段的方式来管理内存

段的划分来源于CPU ,由于我们使用 **物理地址 = 段地址*16 + 偏移地址 ** 的方式给出内存的物理地址

所以我们用 段地址*16 定位段的起始地址(基础地址),用偏移地址定位段中的内存单元

我们需要注意:

  • 段地址*16,即一个段的起始地址一定是16的倍数
  • 偏移地址为16位,即其最大寻址位置为64KB,所以说一个段的最大长度为64KB

段寄存器

8086 有四个段寄存器CS、DS、SS、ES,这里我们先只看CS

其中CS,IP 是8086CPU中最重要的两个寄存器,他们指示了CPU当前要读取的指令的地址

CS 为代码段寄存器,IP为指令指针寄存器,我们可以这样理解,在8086机中任意时刻,CPU将CS:IP指向的内容当作指令执行

我们可以把8086CPU的工作过程表述为:

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

我们可以这么说,内存中的一段信息被CPU执行过,那么他所在的内存单元一定被CS:IP指向过

修改CS、IP指令

8086CPU大部分的寄存器的值都可以通过mov来改变,但是mov不能用于修改CS:IP 的值,mov被称为传送指令

能够修改CS:IP的值的指令通称为转移指令,如简单的jmp指令

  • 若想同时修改CS、IP的值,我们可以使用指令 jmp 段地址:偏移地址 的指令完成
  • 若想修改仅IP的内容,我们可以使用指令 jmp 某一合法的寄存器 的指令完成,这一步可以抽象理解为 mov IP 该寄存器中的值

第一条指令

在8086CPU加电启动或者复位后(即刚开始工作时),CS和IP被设置为 CS=FFFFH , IP=0000H

即在8086PC机刚启动时,CPU从内存FFFF0H单元中读取指令执行,FFFF0H单元存放着8086PC机开机后执行的第一条命令

实验一:查看CPU和内存,用机器指令和汇编指令编程

使用DEBUG

DEBUG 是 DOS,Windows 都提供的实模式(8086方式)程序的调试工具。可以用它查看CPU的各种寄存器中的内容、内存使用的情况和在机器码级跟踪程序的运行

我们需要用到以下的Debug功能:

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

R命令

1
debug -r

开启了debug的R模式

image.png

我们可以看到一个用法 -r 寄存器名称 可以修改指定寄存器的值

D命令

1
debug -d

我们可以看出其显示的三部分:

  • 左边部分是每行的起始地址
  • 中间部分是从指定地址开始的128个内存单元的内容,注意每行中间的”-“,这是用来区分每行的前八个字节和后八个字节的标志
  • 右边是每个内存单元中的数据对应可显示的ASCII字符,如果不可显示,则用”.”替代

其有以下三种用法:

  • 一是 d 段地址:偏移地址 指定地址CS:IP,对其进行128字节的查看
  • 二是 d 段地址 :偏移地址 结尾偏移地址 可以指定查看的范围 即从偏移地址到结尾偏移地址之间的内存空间
  • 三是 d 直接查看,将列出Debug预设的地址处的内容

E命令

1
debug -e
image.png

我们可以通过E指令来进行内存空间的改写,其操作如下:

e 起始地址 数据1 数据2 数据3 ……这个操作对起始地址之后的内存空间进行覆盖

其中数据可以是字符,也可以是数值,甚至是字符串

或者我们可以使用提问式的方法来进行修改

image.png

有以下步骤:

  • 首先,输入 e 起始地址 ,按Enter
  • 我们从起始地址的第一个值开始,“.”之前的数值是该单元的原数值,之后则是要修改的数值
  • 我们使用空格跳过当前单元(无论是否修改),按下Enter表示结束修改

U指令

我们使用E指令写入以下的一段汲取嘛,然后用U指令对其内容翻译为汇编指令

1
2
3
b80100		mov ax,0001
b90200 mov cx,0002
01c8 add ax,cx

我们看到D命令的输出可以分为三部分:

  • 左边部分为指令占用的内存单元的起始地址和对应的机器码
  • 右边部分为翻译后的汇编语言

T命令

现在我们尝试执行我们写的汇编语言,使用T命令执行一条或多条汇编指令

首先我们需要修改CS:IP 指向的命令 然后开始执行

image.png

我们可以看到执行结果符合我们呢的期望

A命令

前面我们使用E命令写入机器指令,这样很不方便,所以我们使用A指令直接写入指令

其效果如下:

image.png

我们看到再给出起始地址后可以直接进行编辑

这是我的第一篇博客,这个博客搭建花了很多时间,遇到了很多问题,在这里分享一下

环境配置

首先是环境的配置

你需要安装 node.js 作为包管理器进行 Hexo 网站的搭建

其次你需要将 git 与你的 github 账号建立SSH通道

然后进行Hexo的安装:

1
npm install hexo -g

接着新建一个文件夹作为你的本地博客的文件,进行安装Hexo的依赖

1
npm install --save hexo-deployer-git

搭建博客

初始化

首先我们需要创建一个用于存放博客的文件目录,然后cd进入目录,并对其进行初始化

1
hexo init

因为我遇到一些问题,所以我是直接从Github下载解压到本地

如果你也遇到这个问题从这里

下载

当初始化结束后我们可以看到以下文件出现在我们的目录中

None

测试效果

接着我们生成页面

1
hexo generate

然后我们通过本地网站打开

1
hexo server

然后在http://localhost:4000/查看效果

上传部署

首先创建一个仓库(注意命名:用户名.gitihub.io)比如这里我是Ylin07.github.io

然后我们在本地博客存放的文件中对_config.yml进行以下编辑:

1
2
3
4
deploy:
type: git
repository: git@github.com:Ylin07/Ylin07.github.io.git
branch: main

接下来我们进行Hexo的三部曲

  • hexo clean
  • hexo g
  • hexo d

这里最后的 hexo d我遇到了一个问题,就是始终部署失败

我在网上找到的解决办法是在C:\Windows\System32\drivers\etc下的hosts文件添加github的IP地址,其操作如下:

1
notepad C:/Windows/System32/drivers/etc/hosts

在最下面添加

1
140.82.112.4 github.com

然后保存即可,这样就完成了对网站的部署,接下来让我们看看效果吧