faryou 发布的文章

汇编环境的搭建

我们学习的是在8086CPU下编写汇编语言,因此使用MS-DOS自带的记事本就是一个不错的选择。早期汇编语言程序的封装包括编译(翻译成机器码)和链接(套一个DOS下的程序外壳)两步,都有专门的软件实现了这些功能。下面这个链接是我在网上找到的编译器+链接器:https://huaijiuchuang.faryou.eu.org/viewthread.php?tid=289&extra=page%3D1,复制到DOS下即可运行,使用时只需要输入生成程序的文件名即可~

Debug的使用

Debug是DOS自带的一个用于调试程序的工具,它能够一步步执行汇编指令,并且每一步之后都能查看内存、寄存器的状态,使我们可以快速找到程序的问题。下面我们学习Debug的使用。

进入Debug

在命令行下输入以下指令,即可以调试指定的程序:

debug 指定程序文件名(可以是绝对路径或者相对路径)

R命令

用于查看各寄存器的值,并显示当前指令。

U命令

用于查看其他的指令。

T命令

这个命令是Debug的核心功能,实现单步执行。会执行当前指向地址的命令,并更新CS:IP的值。

P命令

单步执行int指令。

前言

学习汇编之后,我们或许能够思考一些之前无法考虑的问题。本文是个人的一些想法,分享给大家,也许可以激发大家的思维。

操作系统的思考

学了汇编,我们可以对一些结构较为简单的操作系统进行研究。
我们来考虑一个DOS操作系统,我们想想一个DOS系统由哪些部分组成,无非是引导、硬件控制(最基本的,磁盘、键盘和屏幕)、提供一些中断程序(即所谓"API"),以及一个基本的命令行。这也是MS-DOS1.25中的全部。
很显然,我们如果要独立制作一个DOS操作系统,就应该从这些方面入手。我们已经学完了汇编的基础知识。很显然,前面三个部分以我们目前的汇编能力,外加学习一点硬件知识,有希望能完成;最后一个部分涉及到字符串的处理,这需要我们学习一些相关的算法(如果想实现复杂点的语法使用C语言更为方便,但这里仅考虑最简单的情况)。
因此,我们不妨将这样一个较为基础的DOS作为练习16位汇编的大作业~

高级语言的思考

我们思考一下C语言里的函数、变量、结构体等概念的实现。
首先,我们在使用函数的时候经常进行嵌套及递归操作,那么如何保存之前函数中的数据就成为了一个问题。汇编语言中,我们使用call调用子程序之后,子程序会先把寄存器入栈。而在C语言的函数中,我们需要暂存局部变量,并且函数过程结束之后,应当及时释放局部变量占用的内存。很显然,函数调用及嵌套具有“先进后出”的性质,即越先进入的函数越靠后结束,这完美契合了栈的特性。如果我们构建一个“栈”,存下局部变量,那么只需要在函数调用前后进出栈,就可以很好的完成内存管理。而我们还有全局变量,两者应该分开存。通常的做法是:在内存中靠前的地方划出一块作为栈,分界线之后的区域存放全局变量,称为“堆”。其他高级语言的内存管理也与此大致相同。这也解释了为什么局部变量不能定义过大——栈的空间有限,会爆栈。
结构体的原理很简单,实质上是一堆挤在一起的变量,只需要用多个寄存器访问即可。

关于CPU

我们前面已经学习了8086CPU的工作原理,选择8086是因为其机制较为简单,同时作为早期CPU的代表,适合新手进行学习,现实生活中应该是见不到这种三十多年前的CPU的。8086CPU为单任务CPU,即只能同时执行一条指令,并且没有任何内存保护机制,也就是说所有程序的内存可以互相看到,非常不安全。因此Intel在之后的CPU版本中加入了保护模式,实现了内存的权限层级管理。保护模式下我们需要学习许多新东西,机制也更复杂。我的观点是:先把8086下的硬件之类的事情折腾清楚搞熟练,再逐步接触保护模式。因此,我后面的教程也会对8086机的其他硬件调用进行详细介绍,以助读者理解。

(以下内容将作为汇编教程系列的前言)
(阅读本教程系列如果有问题可直接于评论区提出,我长期提供免费解答)

一点感想
先说说我个人学习汇编语言的经历。首先,我是个怀旧的人,因而同时也对于那些古旧的技术心存向往。学习完C语言的指针之后,我希望更彻底的了解计算机底层原理,于是就决定学习汇编。我没有老师,身边的人也没有懂汇编的,当时就在淘宝上买了一本排在首位的王爽的《汇编语言》,开始自学起来。
当时我还是初一。看完书的简介和第一章节,觉得应该不很难,于是下了决心每周末啃一点,把整本书过掉。当时的我还是太天真了。到了第二章节,由于我没有系统的学习过计算机底层架构,到了CPU工作过程那里直接晕了,之后的内容也是看的云里雾里的,本站那篇《汇编中栈的原理》基本上就是直接抄书写的的。
当时觉得真的啃不下去了,又没人可以问,直接放弃。之后的一年时间里,每次心中重燃不死的希望,都去啃一点,居然一节节啃下来了!然后就是八下,竞赛冲刺。到了25年暑假,一气读完全书!到现在,就是复读、加深的过程。
个人对王爽《汇编语言》的一些认识:首先汇编语言不适合编程初学者,至少得会一门C/Pascal之类较底层的高级语言,才能去学习该书。这本书写的其实挺好的,包括作者提出的所谓“知识屏蔽”我也赞同,书中也充分体现了这一点。但是我觉得该书更适合作为老师上课的教材,对像我一样自学的人来说,前面部分对于8086CPU的底层原理介绍很生硬,需要长时间消化才能读懂。
我写这系列的教材,大体框架遵循了该书,但有些地方也根据我自认为更适合理解的方式进行了调换,目的在于更适合有C语言基础的人阅读。教程中部分需要专业术语的地方,直接用了书中原话,不再一一指出。

汇编语言简介
学习汇编语言,需要明白它作为“低级语言”,到底“低级”在什么地方。汇编语言,是一门直接对硬件编程的,也就是说,它的每一条指令(不包括伪指令,那是给编译器看的,类似高级语言中的宏),都是直接操纵硬件。计算机本质上是一堆电路,汇编语言的每一条指令所代表的功能,都代表了一块电路实现的功能。比如,mov ax,2表示将ax寄存器(寄存器是CPU中的一个电子元件,一个寄存器有16位,每一位能够以高低电平的形式存储一个1或0)的值改为2,即二进制下的0000000000000010,可以看出,直接改变了电平高低。所以,对于其中每一条指令的功能,不要有任何想法,它这个功能是由它对应的电路实现的,我们编程者并不能改变,只能使用。不要想着修改它,也不要提出“为什么一个内存单元是8位?”、“为什么不能直接改变段寄存器的值?”等问题,答案只有一个——电路就是这样接的。如果你真的想改变它的功能,请修改CPU的电路。
同时,汇编语言没有高级语言中那些抽象出来的“函数”“变量”或者“对象”这些概念,你可以操作的只有:CPU、内存、键盘、显示器等物理意义上的硬件。高级语言中的这些概念最终也是在汇编语言的基础上抽象得到,要实现这些概念,到最后仍然依赖于最底层的汇编语言。不过你可以自己在学汇编时思考高级语言中的这些概念可以如何通过汇编实现,这对于理解硬件原理帮助很大。

前言

BIOS为用户的键盘输入及磁盘I/O操作提供了专门的中断,以方便我们进行调用。下面我们学习如何使用这些中断。

键盘输入

BIOS提供了9h号中断来处理键盘的输入内容(对接硬件),并提供了16h号中断来方便应用程序读取键盘输入(对接软件)。我们可以把它当成一个API来使用。作为使用者,我们本课只关注16h号中断的调用方法即可。
我们前面说过,9h号中断会对键盘缓冲区进行操作。键盘缓冲区实质上是用环形队列进行管理的内存区,用于暂存键盘输入的信息。16h号中断则从键盘缓冲区中读入信息。下面是16h号中断中最重要的功能(编号0),使用格式:

mov ah,0
int 16h

执行上过指令后,CPU从键盘缓冲区中弹出一个键盘输入,存在ax中,其中ah存放扫描码,al存放ASCLL码。若此时键盘缓冲区中没有数据,则持续检查。我们编写应用程序时,只需要调用这个中断即可便捷地使用键盘缓冲区。

磁盘I/O

我们以3.5英寸软盘为例。其分为上下两面,每面有80个磁道,每个磁道分为18个扇区,每个扇区大512字节。BIOS提供了13h号中断来方便用户读写磁盘盘。下面是使用13h中断读写磁盘的格式:

mov al,1;读取扇区数量
mov ch,0;磁道号
mov cl,1;扇区号
mov dl,0;驱动器号(0-软驱A 1-软驱B 80h-硬盘C 81h-硬盘D)
mov dh,0;磁头号(对软盘相当于面号)
mov ah,2;int 13h的功能号2
int 13h

执行上述程序后,CPU将指定扇区的数据读入到es:bx处及其后的512N个字节中(N表示读取扇区数量)。

mov al,1;写入扇区数量
mov ch,0;磁道号
mov cl,1;扇区号
mov dl,0;驱动器号(0-软驱A 1-软驱B 80h-硬盘C 81h-硬盘D)
mov dh,0;磁头号(对软盘相当于面号)
mov ah,2;int 13h的功能号2
int 13h

执行上述程序后,CPU将es:bx处开始的数据写入对应扇区。

前言

中断是CPU中用于处理其内外突发情况的一种机制。CPU执行完当前的指令后,检测到CPU内部或外部传送的一种特殊信息(称“中断信息”),立刻处理这些信息,处理方式即立刻跳转到中断的对应处理程序,执行完这段程序后再返回。中断机制常在CPU及操作系统提供磁盘操作、键盘输入、屏幕输出等处理程序时使用,可以视为早期16位操作系统的一种“API”,本质是一种具有特殊触发条件的函数。下面我们将详细学习几种种情况引起的中断,以及编写程序时如何利用中断机制。

中断的过程

当8086CPU收到中断信息后,处理过程如下:

(1)取得中断类型码
(2)pushf
(3)修改TF、IF值为0
(4)CS、IP入栈
(5)进入中断程序(通过修改CS、IP的值)
(6)恢复CS、IP、标志寄存器的值(从而返回程序)

需要注意的是,如果进入中断的前一条指令修改了ss寄存器的值,那么即使触发了中断条件,也不会进入中断程序。因为ss:sp是一个整体,若ss:sp值不对应则会引起中断之后的错误,所以CPU提供了这一特性。

中断向量表

8086CPU支持256个中断,为此,规定内存0000:0000~0000:03FF的1024个单元存放0~255号中断的入口,从而建立中断类型码(即中断的编号)与中断程序之间的一一映射关系。其中,每两个字存放一个中断处理程序的地址,高位存放中断处理程序的偏移地址,低位存放其段地址。由此,不难理解,当CPU获得中断类型码N后,即通过设置CS:IP的值为(N4):(N4+2)跳转到中断处理程序,中断结束后再返回原来的位置。

内中断

内中断的中断信息由CPU内部产生。对8086CPU,当发生如下四种情况时,会引起内中断:

(1)除法错误(执行div等除法指令时,除数为0)
(2)单步执行(使用debug的时候)
(3)执行into指令
(4)执行int指令

下面将分别介绍说明~

除法错误中断

除法错误中断为0号中断,用于处理div等除法指令除数为零的错误。让我们执行下面的程序:

mov ah,0
mov bh,1
div bh

可以看到,屏幕上输出了“Divide overflow!”,但我们并没有写输出字符串的程序。原因是:这里出现了除数为0的错误,从而引发0号中断,0号中断处理程序的功能即输出字符串“Divide overflow”。

单步中断

单步中断为1号中断,用于提供Debug的单步执行功能。在使用Debug调试时,我们经常需要逐条调试代码(即使用T命令),单步中断就是提供这一功能的中断。其触发条件为TF值为1。这样我们就很容易理解为什么进入中断之前要设置TF值为0了:如果进入中断前TF值为1,那么执行中断处理程序的第一条指令后,CPU检测到TF值为1,触发单步中断,进入其中断处理程序;执行完中断处理程序之后,又检测到TF值为1,又重新执行1号中断的第一条指令;陷入死循环。

into指令引发内中断

into指令检测上一条计算指令是否发生了溢出,若是,则进入4号中断。我们可以通过修改4号中断对应的程序,实现需要的功能(后面会详细介绍)。into指令的格式:

into

int指令引发内中断

int指令的功能是引发中断过程,我们前面所说的中断承载的“API”功能即主要通过int指令实现。其使用格式:

int 中断类型码

操作系统把“API”以中断形式写到内存里,应用程序通过int指令即可方便地调用。同时,int指令也可以用于不同程序间的数据互通,这对于早期的单任务操作系统尤为重要。我们之前写程序最后总有一句:

int 21h

中断类型码21H对应的中断功能即为:返回操作系统。这为应用程序提供了快速返回操作系统的方式。
接下来我们学习如何自己利用中断机制~

自己编写中断程序

我们来实现一个40h中断,包括如下功能:

(0)清理屏幕
(1)显示日期
(2)显示时间

按照汇编的惯例,一个中断里常有多个功能,在调用其中一个子功能时,一般用ah寄存器传递功能号。我们之前的程序中:

mov ax,4c00h

就是传递21h中断的参数。接下来考虑我们这个中断处理程序的实现有哪些步骤。很显然,我们需要如下的步骤:

(1)用到的寄存器入栈保存
(2)中断处理程序
(3)寄存器出栈
(4)返回

中断返回一般用iret指令,其功能相当于:

pop IP
pop CS
popf

中断程序也占用空间。我们这里只是进行练习,所以只要别让代码影响到操作系统即可。所以我们直接扔到闲置的中断向量表区,即0000:0200~0000:03FF区域。
下面开始编写~

assume cs:codesg

;安装程序
codesg segment
setup:
  mov ax,cs
  mov ds,ax
  mov si,offset code
  mov ax,0
  mov es,ax
  mov di,200h

  mov cx,offset codeend-offset code
  cld
  rep movsb

  mov ax,0
  mov ds,ax
  mov word ptr [4*40h],200h
  mov word ptr [4*40h+2],0

  mov ax,4c00h
  int 21h

;中断处理程序
code:
  cmp ah,2
  jae codeback
  mov bx,ax
  shl bx
  jmp word ptr codetable[bx]
run0:
  push cx
  mov cx,4000
  push ax
  push ds
  mov ax,0b800h
  mov ds,ax
  rep mov byte ptr [cx],0
  pop ds
  pop ax
  pop cx
run1:
  push cx
  mov cx,3
  push ax
  push bx
  push dx
  mov ax,cs
  mov ds,ax
  mov ax,0b800h
  mov es,ax
  mov bx,14
run1_loop:
  mov al,codedata1[cx]
  out al,70h
  in 71h,al
  mov ah,al
  shr ah,4
  and al,00001111b
  add al,30h
  add ah,30h
  mov byte ptr es:[bx],al
  sub bx,2
  mov byte ptr es:[bx],ah
  sub bx,4
  loop run1_loop
  mov byte ptr es:[4],'/'
  mov byte ptr es:[10],'/'
  pop dx
  pop bx
  pop ax
  pop cx
  iret
run2:
  push cx
  mov cx,3
  push ax
  push bx
  push dx
  mov ax,cs
  mov ds,ax
  mov ax,0b800h
  mov es,ax
  mov bx,14
run2_loop:
  mov al,codedata2[cx]
  out al,70h
  in 71h,al
  mov ah,al
  shr ah,4
  and al,00001111b
  add al,30h
  add ah,30h
  mov byte ptr es:[bx],al
  sub bx,2
  mov byte ptr es:[bx],ah
  sub bx,4
  loop run2_loop
  mov byte ptr es:[4],':'
  mov byte ptr es:[10],':'
  pop dx
  pop bx
  pop ax
  pop cx
  iret
codeback:iret
codetable dw run0,run1,run2
codedata1 db 9,8,7
codedata2 db 4,2,0
codeend:nop
codesg ends
end setup

执行过上述程序之后,40h号中断即安装进了系统,我们执行下面的指令即可调用:

mov ah,1;这里以1号功能为例,会显示日期
int 40h

外中断

外中断是用于外部硬件引发的中断,通常用来处理硬件相关的信息。外部设备全部通过端口接入PC机,而外中断就是在某些端口的信息送达CPU时引发的中断。下面我们来了解外中断。
首先,外接设备多种多样,而主板本身并没有这么多的端口来接入各种外部设备。况且对于外中断,在一般情况下都应该立刻作出反应。因此,我们需要一个中介,连接主板与外部设备,并向CPU发送外接设备的中断类型码。这个设备就是中断控制芯片。我们以8259A中断控制芯片为例(因为它是8086机上最常见的)。8259A上有8个端口,以接入8种不同的设备。当外部中断(如键盘输入)发生时,它通过预先设定好的外部中断-中断类型码映射关系,找到键盘中断对应的信息,并传递给CPU。然后,CPU执行中断处理程序。
BIOS中的9号中断有基本的键盘处理程序,以进行基本的输入输出处理,包括:

(1)读入扫描码
(2)如果该扫描码代表字符键,刚将该扫描码与对应的ASCLL码送入BIOS键盘缓冲区(该缓冲区由16h号中断管理);如扫描码代表控制键或切换键,则将其转变为状态字节,写入内存的0040:0017单元。
(3)完成对硬件系统的应答

其中最后一步是普遍适用于所有外中断的。它向8259A发送中断处理完毕的信息,从而继续工作。其实现如下:

mov al,0x20
out 0x20,al
out 0xa0,al
iret

这段代码向8259A发送EOI信息,以使其继续工作。
之后的内容我们会进一步学习各种硬件中断的具体实现,这里只作基本了解~