作者简介
张孝家,西邮陈莉君教授2019级研究生
我们都知道cpu的主要功能是取指令和执行指令,但是当cpu加电或者RESTET引脚的电平由低变高时,cpu是从什么地方执行第一条指令,第一条指令存储在什么地方。今天,我将带大家了解处理器上电或者复位之后,它是从什么地方开始取指令和执行指令的。再编写一个简单的加载器和应用程序。让我们的加载器跑我们的应用程序。为了能够让大家看见实验的结果,我们在应用程序中实现向屏幕显示点什么,要不然的话,谁知道我们的程序是不是成功运行了呢?
其实,在处理器的众多的引脚中,有一个是RESET,用于接受复位信号。每当处理器加电,或者RESET引脚的电平由低变高时,处理器都会执行一个硬件初始化,以及一个可选的内部自测试(Build-in Self-Test, BIST),然后将内部所有寄存器的内容初始到一个预置的状态。 比如,对于Intel8086来说,复位将使代码段寄存器(CS)的内容为0xFFFF,其他所有寄存器的内容都为0x0000,包括指令指针寄存器(IP)。虽然8086之后的处理器并未延续这种设计,但毫无疑问,无论怎么设计,都是有目的的。如图1,是8086内部的构造图
在硬件初始化之后,它就会立刻尝试去做这样的工作(取指令和执行指令)。不过在这个时候,内存中还没有任何有意义的指令和数据,它该怎么办呢?在解开谜底之前,我们先来看看Intel 8086地址空间的分布:Intel 8086可以访问1MB的内存空间,地址范围为0x00000到0xFFFFF但是设计者并没有将这1MB的空间全部用来访问DRAM(内存条),而是划分了几个部分,只不过大部分用于访问DRAM(掉电丢失),剩余的部分给了只读存储器ROM(掉电不丢失)和外围的板卡。如图2所示
在Intel 8086处理器中,ROM占据着整个地址空间的顶端的64KB,物理地址范围0xF0000~0xFFFFF,里面固化了开机时的要执行的指令。因为8086加电或者复位时,CS=0xFFFF,IP=0x0000,所以,它的第一条指令位于物理地址0xFFFF0,正好位于ROM中,那里固化了开机时需要的执行的指令。但是如果从0xFFFF0开始执行,这个位置离1MB内存的顶端 只有16个字节的长度,一旦IP寄存器的值超过0x000F,那么,它与CS一起形成的物理地址将因为溢出而变成0x00001,这将回绕到1MB地址空间的最低端。所以,ROM中位于物理地址0xFFFF0的地方,通常是一个跳转指令,它是通过该表CS和IP的内容,使处理器从ROM中的较低地址处开始取指令执行。一个典型的跳转指令就像这样:jmp 0xf000:0xe05b。
这块ROM芯片中的内容包括很多部分,主要是进行硬件的诊断、检测和初始化。所谓初始化,就是让硬件处于一个正常的、默认的工作状态。最后,它还负责提供一套软件例程,让人们在不必了解硬件细节的情况下从外围设备(比如键盘)获取输入数据,或者向外围设备(比如显示器)输出数据。设备当然是很多的,所以这块ROM芯片只针对那些最基本的、对于使用计算机而言最重要的设备,而它所提供的软件例程,也只包含最基本、最常规的功能。正因为如此,这块芯片又叫基本输入输出系统(Base Input & Output System, BIOS)ROM。
以上就解决了,处理器加电和复位时,从什么地方开始取指令和执行指令。
ROM-BIOS的容量是有限的,当它完成自己的使命后,最后所要做的,就是从辅助存储设备读取指令数据,然后转到那里开始执行。辅助寄存器(以硬盘为例)因为ROM只读取了主引导扇区的内容(512个字节的一个扇区,0面0道1扇区),将它加载到内存地址0x0000:0x7c00处(也就是物理地址0x07c00),然后用一个jmp指令跳到哪里接着执行:jmp 0x0000:0x7c00。通常,主引导扇区的功能是继续从硬盘的其他部分读取更多的内容加以执行。像Windows这样的操作系统,就是采用这种接力的方法一步一步把自己运行起来的。
说到这里,我们可以想象,如果我们把自己编译好的程序写到主引导扇区,不也能够让处理器执行吗?我们将主引导扇区的内容写成一个加载器,然后,用加载器来加载在硬盘其他位置的应用程序(当然也可以是操作系统)。接下就是我们要实现的内容。
加载器的代码如下:
;加载器
;创建日期:2019/11/23
;====================================
;定义一个mbr段,段是以16字节对齐,汇编地址开始的地址是0x7c00
SECTION mbr align=16 vstart=0x7c00
;栈和栈指针的设置
mov ax,0
mov ss,ax
mov sp,ax
;读取磁盘数据,存放的物理内存的位置
mov ax,[cs:phy_base]
mov dx,[cs:phy_base+2]
shr ax,4
ror dx,4
and dx,0xf000 ;and是与,and是加,我写成了加
or ax,dx
mov ds,ax ;存放数据的数据段的段基地址
;以上的写法还可以改为32位数据的操作,除以16,就可以实现左移4位的操作了,如下:
; mov ax,[cs:phy_base]
; mov dx,[cs:phy_base+0x02]
; mov bx,16
; div bx
; mov ds,ax
;====================================
;读取磁盘扇区号的位置
mov si,[cs:start_local]
mov di,[cs:start_local+0x02]
call read_hdisk
;将磁盘数据读到,以ds为段基地址的起始内存地址空间中
;====================================
;读到的第一个扇区里面的内容包含了程序头部的一些信息(程序文件的大小,段的数量,程序的入口点,段的起始地址)
;从这个头部信息,我们做哪些事情?
;1.根据文件的大小,可以判断,这个文件有多少个扇区,我们还需要再读几次的扇区
;2.段的数量和段的起始地址,我们可以在加载器中修改多少次的段的逻辑地址,再重新写入覆盖,得到最终的段的逻辑地址(以上的做法叫做段的重定位)
;3.根据程序的入口点,我们可以从加载器,跳转到加载程序的地方执行。将加载器的控制权交给用户程序
;因此,下面就是要对这些的内容操作
mov ax,[0]
mov dx,[0x02]
mov bx,512
div bx
cmp ax,0x00
jnz aa
jmp direct
aa:
mov cx,ax
cmp dx,0x00
jnz b
dec cx
cmp cx,0x00
jz direct
b:
mov si,[cs:start_local]
mov di,[cs:start_local+0x02]
push ds ;将数据段ds的段基地址压入栈中
a:
add si,1
adc di,0
mov ax,ds
add ax,0x20
mov ds,ax
call read_hdisk
loop a
pop ds
;将压入栈中的段基地址,弹出,放入ds数据段寄存器中
direct: ;修改程序的入口地址,即修改程序的代码段的实际物理内存段基地址。
mov ax,[0x06]
mov dx,[0x08]
call calu
mov [0x06],ax
mov cx,[0x0a]
mov bx,0x0c
relocate:;下面的功能就是,段的重定位
mov ax,[bx]
mov dx,[bx+0x02]
call calu
mov [bx],ax
add bx,0x04
loop relocate
jmp far [0x04]
;跳转到ds:0x04的用户程序的地方,开始执行用户程序的代码
calu:
add ax,[cs:phy_base]
adc dx,[cs:phy_base+0x02]
shr ax,4
ror dx,4
and dx,0xf000
or ax,dx
ret
;====================================
;在这个磁盘的读写操作用到了那些寄存器:
;ax:ax,dx主要用于磁盘的端口的操作的,也只能够用这两个寄存器来操作端口
;dx:同上
;bx:在这边主要用于基址的偏移地址的操作
;cx:用于计数的,来使用它多次循环的操作的判断
;si:保存逻辑扇区号的低16位(逻辑扇区号使用28位来保存的)
;di:用于保存逻辑扇区号的高12位(还有4位是没有用到的),
;但是在端口寄存器中0x1f6端口的高4位保存了,四个重要的属性(CHS还是LBA模式,主硬盘还是从硬盘)。
;====================================
read_hdisk:
;保护现场数据
push ax
push bx
push cx
push dx
;读取一个磁盘的扇区的个数:1
mov dx,0x1f2
mov al,0x1
out dx,al
;从磁盘的哪个逻辑扇区开始读取:我们用di,si来存储28的逻辑扇区号
inc dx
mov ax,si
out dx,al
inc dx
mov al,ah
out dx,al
inc dx
mov ax,di
out dx,al
inc dx
or ah,0xe0
mov al,ah
out dx,al
;从磁盘里面读还是写的操作
inc dx
mov al,0x20
out dx,al
.wait: ;判断磁盘是否已经准备就绪了
in al,dx
and al,0x88
cmp al,0x08
jnz .wait
;从磁盘里面读写数据,每次读写一个字的大小,总共要读256次
mov cx,0x100
mov dx,0x1f0
xor bx,bx
.read:
in ax,dx
mov [bx],ax ;把读出的数据,放在ds:bx的内存里面
add bx,2
loop .read
;恢复现场
pop dx
pop cx
pop bx
pop ax
ret
;从内存地址那边开始存放数据
phy_base: dd 0x10000
;存放开始读取磁盘扇区号
start_local: dd 0x100
;最后固定的扇区的填充,但是最后的两个字节必须为0x55,0xaa
times 510-($-$$) db 0x00
db 0x55,0xaa
加载器的功能就是:将指定的磁盘上面的程序,加载到内存中并跳转到程序执行,将控制权交给加载到内存上的程序。
编译生成.bin文件