前面应该有一章,“一:操作系统的概述”,懒得写,但是很重要,最好去看下视频,如果有人看的话,以后有空再补
首先我们要有一个认知,就是计算机是怎么运行的
从白纸到图灵机
计算机开始的时候就是一个做计算的机器
那我们从人计算的过程来思考
比如说,我们在纸上看到 3 + 2
大脑算出结果是5
那就用笔在纸上写上5这个答案
那我们用一个自动设备来模拟这个过程
纸带模拟纸
控制器模拟大脑
读写头来模拟眼睛和笔
这里控制里的表是固定的,比如它只能进行加法运算
你给3和5,它只会算出8
从图灵机到通用图灵机
上面说的图灵机,控制器里面的逻辑是固定的,就像一个只会做一道菜的厨子,他的脑子里只够装下一个道菜的做法,不会学习新的菜谱。
那么通用图灵机就是一个可以看懂菜谱的厨师了。
他的控制器就像这个聪明的厨师的大脑,一直处于一个求知的状态。每看到一道菜谱,就做一道菜。控制器从纸带中载入一个新的控制器动作,启动这个动作后,后面获取的数据,就是在这套逻辑下开始运行。比如载入一个qq的逻辑,那么控制器就在给出qq的逻辑判断,你点发送,它就知道你是要把这消息发送过去。
从通用图灵机到计算机
冯·诺依曼存储程序思想
将程序和数据存放到计算机内部的存储器中,计算机在程序的控制下一步一步进行处理
存储器:那个厚厚的菜谱
IP:就像你看菜谱时的手指,慢慢的往下划,告诉自己我正在操作这一步,等你操作完这一步,你的手指就会划到下一步,告诉自己要执行下一步了。
IR:就像你小小的大脑,当你的手指指到那过程的时候,你就记住这个指令,然后一直默默记住这个指令,转身去执行他,所以IR就是存储IP里指到的指令
CPU[运算器,控制器]:就是你大脑,当你看到“把油倒到锅里”这六个字的时候,你知道它的实际意思,就是把油倒到锅里,是不是觉得这样说很傻,那如果我写put oil to the pan,如果你没学过英语你就根本不知道这是什么意思,如果你学过英语就知道,它的实际意思,就是把油倒到锅里。
mov ax, [100]:mov 是 就像是put,把A放到B那里,那ax就是the pan,[100] 就是oil, 那这句话的意思就是:put the [100] to the ax
所以说:计算机就一个永不停歇的苦力,这要一开始的时候,我们告诉他从哪里开始做,他就会一直一条一条的执行下去
打开电源,计算机执行的第 一句指令什么?
x86 PC
(1) x86 PC刚开机时CPU处于实模式
(2)开机时,CS=0xFFFF; IP=0x0000
(3)寻址0xFFFF0(ROM BIOS映射区)
(4) 检查RAM,键盘,显示器,软硬磁盘
(5) 将磁盘0磁道0扇区读入0x7c00处
(6) 设置cs=0x07c0,ip=0x0000
如果我就这么列出来,你们肯定是不懂的啦!我们大概可以知道,刚开机的时候,电脑从某个地方取指然后开始执行。
下面我就一个一个解释:
- x86
也就是8086,是CPU的一种型号,比如8086的上一代机就是8085,8080。为什么要指定说是x80PC呢,因为不同型号的CPU的结构是不一样的。比如说8085,8080是8位机,而8086是16位机,也可以说是8086是16位结构的CPU。那什么是16位结构的CPU呢?
- 运算器一次最多可以处理16位的数据;
- 寄存器的最大宽度为16位;
- 寄存器和运算器之间的通路为16位;
也就是说,在8086内部,能够一次性处理,传输,暂时存储的信息的最大长度是16位的。内存单元的地址在送上地址总线之前,必须在CPU中处理,传输,暂时存放,对于16位CPU,能一次性处理,传输,暂时存储16位的地址。
- CS和IP
我们刚刚说过IP就是你的手指来指定一个地址的地方,那CS又是什么呢?
我们刚刚说完8086的CPU一次只能处理16位,但是它可是有20位的地址总线,就好比说,你的车可以载20吨的土,你的挖掘机一次可以挖16吨的土,你会只让车装了16吨就走了吗?不会,你一定会利用它还有4吨可以装,让它装满再让它走的。那8086CPU就采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址。
也就是: 段地址X16 + 段偏移地址 = 物理地址
- 段地址X16
其实就是左移4位,这里的位指的是二进制的位,像图中给的1230其实是16进制的,比如这里的十六进制的0,实际上就是二进制的0000,二进制左移了4位,也就是16进制的0向左移动一个下。那原本1230,向左移动一下,就是12300再加上00C8,就可以得出了123C8了
我们再明确一点,CS和IP两个的寄存器,结合起来指示了CPU当前要读取指令的地址。
综上,可得
CS:代码段寄存器
IP:指令寄存器
等等,要是有人不知道寄存器的话,我只能说,真是正个人被你打败了呢。
这里的方格就好比一个寄存器,你看第二个方格里写了9,第三个方格里与第四个方格,合起来是13,那,你怎么想都知道,一个方格只能写0~9,不可能写出一个11吧。那下面的16位寄存器就应该明了了,寄存器就一个暂时存放数据的地方
那计算机里只能存1和0,那这里面的方格就只能存1和0咯
都说到这里了,我们就顺便说下,不同的CPU,寄存器的个数,结构是不想同的。那8086CPU有14个寄存器,每个寄存器有一个名称,我们可以给他们分类
- 通用寄存器
- 控制寄存器
段寄存器
等等有人要吐槽我刚刚的分类了,说怎么没有指令寄存器,随便分的嘛,你大概知道是干啥的就好了嘛,又不是要考试。你没看我图都是到处乱截的吗?
这里要说下通用寄存器AX,BX,CX,DX,细心的同学发现了他们是可以再分的分成一个AH和AL,H就是高的意思,那L就是低咯
为什么要这样呢?还记得我刚刚说过8086前面有8085,8080的CPU吗?我说过他们是8位机,也就是他们只能处理8位,所以为了兼容他们,我们就把AX再细分了一下,这样,我们就可以通过AH传送一个8位的数据了,不然你传输16位,他们是识别不全的,就会造成混乱。
后面要是都遇到什么寄存器,再慢慢说吧!我怕你们都快忘记这是一个操作系统教程了。我们就先不说实模式是什么了,因为这样还要说到保护模式,我们就暂缺忽略先。
那此时在看这图的时候,我们就知道了,刚开机的时候
CS=0xFFFF,IP=0x000,那么我们可以知道CPU现在指向内存的0xFFFF0处,也就是图中的ROM BIOS映射区那
- BIOS
也就是基本输入/输入程序拉,英文你就自己想嘛,base input output system??随便打的,不知道对不对?尴尬!我们就只要知道,它是固化在内存里面的,因为我们说过CPU是一个苦力,会一定不断的执行一条条步骤,那前提是,你要告诉他第一条是在哪里,他才会不停的做下去,BIOS就是他第一件要做的事,那这事就是计算机开机时执行系统各部分的自检,建立起系统需要使用的各种配置表,并且把处理器和系统其余本分初始化到一个已知状态,等等。有人会问,那ROM BIOS和ROM BIOS
映射区是啥区别?因为会设计到兼容等问题,我就不说了,你只要知道,ROM BIOS存放着我刚刚说的那些功能的代码,到时,那些代码会被复制到这个映射区,并被CPU执行。
那第4就不用说了咯,第5就有点意思了。
那这个0磁盘0扇区就是一个512k的引导扇区了。
这时候CS=0x7c00,IP=0x000
那就是CPU会在0x7C00处取指执行
那。。。
终于要开始代码了。
0x7c00处存放的代码
接下来代码,我会先放一份源码,其余的是抽取出来重要的代码。第一份看是有个整体的认知,别的就是摘取一些重要的代码分析,并不是全部,只是一些主干的代码
源码:boot/bootsect.s
!
! SYS_SIZE is the number of clicks (16 bytes) to be loaded.
! 0x3000 is 0x30000 bytes = 196kB, more than enough for current
! versions of linux
!
SYSSIZE = 0x3000
!
! bootsect.s (C) 1991 Linus Torvalds
!
! bootsect.s is loaded at 0x7c00 by the bios-startup routines, and moves
! iself out of the way to address 0x90000, and jumps there.
!
! It then loads 'setup' directly after itself (0x90200), and the system
! at 0x10000, using BIOS interrupts.
!
! NOTE! currently system is at most 8*65536 bytes long. This should be no
! problem, even in the future. I want to keep it simple. This 512 kB
! kernel size should be enough, especially as this doesn't contain the
! buffer cache as in minix
!
! The loader has been made as simple as possible, and continuos
! read errors will result in a unbreakable loop. Reboot by hand. It
! loads pretty fast by getting whole sectors at a time whenever possible.
.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text
SETUPLEN = 4 ! nr of setup-sectors
BOOTSEG = 0x07c0 ! original address of boot-sector
INITSEG = 0x9000 ! we move boot here - out of the way
SETUPSEG = 0x9020 ! setup starts here
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
ENDSEG = SYSSEG + SYSSIZE ! where to stop loading
! ROOT_DEV: 0x000 - same type of floppy as boot.
! 0x301 - first partition on first drive etc
ROOT_DEV = 0x306
entry _start
_start:
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep
movw
jmpi go,INITSEG
go: mov ax,cs
mov ds,ax
mov es,ax
! put stack at 0x9ff00.
mov ss,ax
mov sp,#0xFF00 ! arbitrary value >>512
! load the setup-sectors directly after the bootblock.
! Note that 'es' is already set up.
load_setup:
mov dx,#0x0000 ! drive 0, head 0
mov cx,#0x0002 ! sector 2, track 0
mov bx,#0x0200 ! address = 512, in INITSEG
mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
int 0x13 ! read it
jnc ok_load_setup ! ok - continue
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette
int 0x13
j load_setup
ok_load_setup:
! Get disk drive parameters, specifically nr of sectors/track
mov dl,#0x00
mov ax,#0x0800 ! AH=8 is get drive parameters
int 0x13
mov ch,#0x00
seg cs
mov sectors,cx
mov ax,#INITSEG
mov es,ax
! Print some inane message
mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10
mov cx,#24
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1
mov ax,#0x1301 ! write string, move cursor
int 0x10
! ok, we've written the message, now
! we want to load the system (at 0x10000)
mov ax,#SYSSEG
mov es,ax ! segment of 0x010000
call read_it
call kill_motor
! After that we check which root-device to use. If the device is
! defined (!= 0), nothing is done and the given device is used.
! Otherwise, either /dev/PS0 (2,28) or /dev/at0 (2,8), depending
! on the number of sectors that the BIOS reports currently.
seg cs
mov ax,root_dev
cmp ax,#0
jne root_defined
seg cs
mov bx,sectors
mov ax,#0x0208 ! /dev/ps0 - 1.2Mb
cmp bx,#15
je root_defined
mov ax,#0x021c ! /dev/PS0 - 1.44Mb
cmp bx,#18
je root_defined
undef_root:
jmp undef_root
root_defined:
seg cs
mov root_dev,ax
! after that (everyting loaded), we jump to
! the setup-routine loaded directly after
! the bootblock:
jmpi 0,SETUPSEG
! This routine loads the system at address 0x10000, making sure
! no 64kB boundaries are crossed. We try to load it as fast as
! possible, loading whole tracks whenever we can.
!
! in: es - starting address segment (normally 0x1000)
!
sread: .word 1+SETUPLEN ! sectors read of current track
head: .word 0 ! current head
track: .word 0 ! current track
read_it:
mov ax,es
test ax,#0x0fff
die: jne die ! es must be at 64kB boundary
xor bx,bx ! bx is starting address within segment
rp_read:
mov ax,es
cmp ax,#ENDSEG ! have we loaded all yet?
jb ok1_read
ret
ok1_read:
seg cs
mov ax,sectors
sub ax,sread
mov cx,ax
shl cx,#9
add cx,bx
jnc ok2_read
je ok2_read
xor ax,ax
sub ax,bx
shr ax,#9
ok2_read:
call read_track
mov cx,ax
add ax,sread
seg cs
cmp ax,sectors
jne ok3_read
mov ax,#1
sub ax,head
jne ok4_read
inc track
ok4_read:
mov head,ax
xor ax,ax
ok3_read:
mov sread,ax
shl cx,#9
add bx,cx
jnc rp_read
mov ax,es
add ax,#0x1000
mov es,ax
xor bx,bx
jmp rp_read
read_track:
push ax
push bx
push cx
push dx
mov dx,track
mov cx,sread
inc cx
mov ch,dl
mov dx,head
mov dh,dl
mov dl,#0
and dx,#0x0100
mov ah,#2
int 0x13
jc bad_rt
pop dx
pop cx
pop bx
pop ax
ret
bad_rt: mov ax,#0
mov dx,#0
int 0x13
pop dx
pop cx
pop bx
pop ax
jmp read_track
!/*
! * This procedure turns off the floppy drive motor, so
! * that we enter the kernel in a known state, and
! * don't have to worry about it later.
! */
kill_motor:
push dx
mov dx,#0x3f2
mov al,#0
outb
pop dx
ret
sectors:
.word 0
msg1:
.byte 13,10
.ascii "Loading system ..."
.byte 13,10,13,10
.org 508
root_dev:
.word ROOT_DEV
boot_flag:
.word 0xAA55
.text
endtext:
.data
enddata:
.bss
endbss:
重点代码:boot/bootsect.s
.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text
………………………………
SETUPLEN = 4 ! nr of setup-sectors
BOOTSEG = 0x07c0 ! original address of boot-sectors
INITSEG = 0x9000 ! we move boot here - out of the way
SETUPSEG = 0x9020 ! setup starts here
…………………………
entry _start
_start:
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep
movw
jmpi go,INITSEG
go: mov ax,cs
mov ds,ax
mov es,ax
……………………
load_setup:
mov dx,#0x0000 ! drive 0, head 0
mov cx,#0x0002 ! sector 2, track 0
mov bx,#0x0200 ! address = 512, in INITSEG
mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
int 0x13 ! read it
jnc ok_load_setup ! ok - continue
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette
int 0x13
j load_setup
大概说一下汇编
这里的汇编,源操作数在后面,目标操作数在前面
注意到一点就是,不可以直接把数据放到段寄存器,所以我们都是先把数据放到通用寄存器,再把通用寄存器的值赋到段寄存器
- 通用寄存器
8086有4个通用寄存器:
AX――累加器(Accumulator),使用频度最高
BX――基址寄存器(Base Register),常存放存储器地址
CX――计数器(Count Register),常作为计数器
DX――数据寄存器(Data Register),存放数据- 段寄存器:
8086有6个段寄存器:只有两个是特殊的CS和SS,CS讲过了,SS后面再讲
那剩下的四个就是DS,ES,GS和GS;当指令中没有指定所操作数据的段时,那么DS将会是默认的数据段寄存器。
BOOTSEG = 0x07c0
INITSEG = 0x9000
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
那就是
ds=07c0
es=9000
add是加法,sub是减法,还是遵循源操作数在后面,目标操作数在前面
比如 :sub ax ,8 如果ax原本的值是5,那就是5+8=13,然后把13放到ax中
sub si,si
sub di,di
自己减自己,当然就是零呀!所以si,di都是0.
我们前面说过,CPU的地址是由段地址和偏移地址组成的,就是这个图
我们只知道段地址,是无法确定一个地址的,所以还需要两个偏移地址,那就是si和di了。
ds:si = 7c00
es:di = 9000
他们就是这么配对的,不要问我为什么di不和ds在一起?我也不知道!记住就好了。
mov cx,#256
rep !重复执行并递减cx的值,直到cx = 0 为止
movw !即movs指令。这里是从内存[si]处移动cx个字到[di]处
那就是移动256个字,256个字就是512个字节。
为什么是256个字,那是因为CX=256;
CX――计数器(Count Register),常作为计数器
计算机的字长决定了其CPU一次操作处理实际位数的多少.那我们说过8086是16位的CPU,那就是说这里1字=16位=2字节。因为一般1字节=8位。
这里的512k,是不是很熟悉,我们刚刚说过了引导扇区是512k,并我们知道movw指令是把内存[si]处的512k移动到了[di]处,[si]处地址就是7c00,也就是我们一开始存放bootsetct.s的地方。
jmpi go,INITSEG
go: mov ax,cs
mov ds,ax
mov es,ax
看图我们知道bootserct.s已经移到了90000处,那我们的说过CPU的指令是根据cs和ip所指的地方执行的,这时候内存只有9000有代码,我们当然在移动代码后,要让CPU指向他呀。这段代码就是这个作用
jmp为无条件转移指令,可以只修改IP,也可以同时修改CS和IP。
那jmpi是段间跳转指令,也是同样的道理
INITSEG = 0x9000
……
jmpi go,INITSEG !cs:INITSEG,ip:go
我们就知道这时候,CPU要执行的代码地址:段地址为 INITSEG,即cs=9000,那ip=go?那go呢?
go是一个标号,我们就要讲下标号的概念
标号实际上就是一个汇编的地址,汇编后,go就是从代码执行开始的地方,经过了的偏移量
好像有点难懂是不是?我们先跳出来,讲下为什么要有它,再反过来思考它的意思?
举个例子,我们在看一本书,比如说《百年孤独》,我是在宿舍看的!现在看到了第200页。好!这时候,上课了,我还想继续看,我把这本书带到了教室,那我到教室后,是不是还是打开这本书,然后翻到第200页。
那我们刚刚说过,我们把原本在7c00处的bootsect.s代码移动到了9000处,bootsect.s就像这本书,我们在7c00处的时候,已经执行过了几段代码了,就像我在宿舍已经看了一些了,那当这代码移动到9000处,就像我拿到教室了,那我还要继续看,当然要从我上次看到的地方开始看呀!那代码也是,要从上次执行到的地方开始执行,那上次执行到哪里了呢!就是执行到了
_start:
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep
movw
jmpi go,INITSEG
go: mov ax,cs
mov ds,ax
mov es,ax
go标记的这个地方呀!所以go就是我看过的页数!总结来说,书就是我的段地址,标号就是我翻过的页数。
那我们现在就也明白了,为什么要有jmpi和go的存在了,实际上就是还是这段代码继续往下执行,只是因为我们刚刚把这代码换了一个位置。
好了,我们折腾了这么多,总结成一句话,就是我们把磁盘的第一扇区(0磁道0扇区)中的一个512k的bootsect.s代码复制到了内存的7c00处,还没执行多少步,我们又把它移动到了9000处,然后继续执行后面的代码!那后面的代码呢?
我们先看下
再来一张图,告诉我们等等要干什么!
综上,我们就知道,我们要把磁盘有4个扇区,辣么大的setup模块,即setup.s移动到已经位于内存9000处的bootsect.s后面,我们说过bootsect.s是512k,那地址是多少呢,
我们的地址都是16进制的哦!所以我们就知道我们应该把setup.s移动到90200处!
SETUPLEN = 4 ! nr of setup-sectors
……………………
jmpi go,INITSEG !cs=9000
go: mov ax,cs !ds,es也等于9000
mov ds,ax
mov es,ax
………………
load_setup:
mov dx,#0x0000 ! drive 0, head 0
mov cx,#0x0002 ! sector 2, track 0
mov bx,#0x0200 ! address = 512, in INITSEG
mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
int 0x13 ! read it
jnc ok_load_setup ! ok - continue
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette
int 0x13
j load_setup
0x13是BIOS读磁盘扇区的中断: 我们后面再讲中断,我们只要知道,就是CPU停下现在的工作,去做另一个工作就行了!
ah=0x02-读磁盘,
al= 扇区数量(SETUPLEN=4),对应 mov ax,#0x0200+SETUPLEN SETUPLEN=4 那al=04
ch=柱面号, 对应 mov cx,#0x0002 , 那就是ch=00
dh=磁头号, 对应 mov dx,#0x0000 , 那就是 dh=00
cl=开始扇区, 对应mov bx,#0x0200了,那就是es:bx=内存地址90200处了
dl=驱动器号, 对应mov dx,#0x0000,那就是dl=00
在我们把我们读取磁盘的必要信息都存储在寄存器后,我们就调用了int 0x13中断,电脑就会到那里去执行读磁盘的操作,并从刚刚赋值的寄存器中获取必要的信息,那我们就把setup.s移到了复制到内存中的bootsect.s后面去了。
读入setup模块后: ok_load_setup
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
……………………
int 0x13
j load_setup
ok_load_setup:
! Get disk drive parameters, specifically nr of sectors/track
mov dl,#0x00
mov ax,#0x0800 ! AH=8 is get drive parameters
int 0x13
mov ch,#0x00
seg cs
mov sectors,cx
mov ax,#INITSEG
mov es,ax
! Print some inane message
mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10
mov cx,#24
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1
mov ax,#0x1301 ! write string, move cursor
int 0x10
! ok, we've written the message, now
! we want to load the system (at 0x10000)
mov ax,#SYSSEG
mov es,ax ! segment of 0x010000
call read_it
………………
sectors:
.word 0
msg1:
.byte 13,10
.ascii "Loading system ..."
.byte 13,10,13,10
我们就主要讲下那个打印的那段代码吧
mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10 !读光标
mov cx,#24
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1
mov ax,#0x1301 ! write string, move cursor
int 0x10 !显示字符
…………
msg1:
.byte 13,10
.ascii "Loading system ..."
.byte 13,10,13,10
我们就猜下吧,第一次的int 0x10这个中断,去获取了光标的位置,然后我们再把msg1这个地址赋给了bp,而这个地址在下面有写,看起来就是一个字符串,那再调用int 0x10的时候,就把这串字符显示在刚刚获取的光标位置那里。这里的int 0x10,我们先不要太纠结,我们可以思考成是一个嵌套函数,我们突然遇到这个函数,就跑过去执行,再加上因为参数的不同,他就会执行不一样的代码!有点像java的重载。也没必要去背,如果真的要自己写的话,到时一定有使用手册来说明每个中断代码分别如何使用。
这里做的工作,就像我们打开PC时
这个是一样的,只是别人有点高级,是动画效果的呢!我们就是显示字符串“Loading system……"
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
………………
mov bp,#msg1
mov ax,#0x1301 ! write string, move cursor
int 0x10
! ok, we've written the message, now
! we want to load the system (at 0x10000)
mov ax,#SYSSEG
mov es,ax ! segment of 0x010000
call read_it !读入system模块
我们再往下分析。
还记得我上次发的一张图吗?
这样我们就很容易知道了吧,先让ax=0x1000,调用了一个read_it的函数,我们就可以猜,
这代码八成是把原本磁盘上setup.s后面的代码拷贝到内存的0x1000处。那我们就大概的看一下这个函数
read_it //读入system模块
SETUPSEG = 0x9020 ! setup starts here
………………
! ok, we've written the message, now
! we want to load the system (at 0x10000)
mov ax,#SYSSEG
mov es,ax ! segment of 0x010000
call read_it !读入system模块
………………
jmpi 0,SETUPSEG
………………
read_it:
mov ax,es
test ax,#0x0fff
die: jne die ! es must be at 64kB boundary
xor bx,bx ! bx is starting address within segment
rp_read:
mov ax,es
cmp ax,#ENDSEG ! have we loaded all yet?
jb ok1_read
ret
ok1_read:
seg cs
mov ax,sectors
sub ax,sread
mov cx,ax
shl cx,#9
add cx,bx
jnc ok2_read
je ok2_read
xor ax,ax
sub ax,bx
shr ax,#9
ok2_read:
………………
ok1_read:
seg cs
mov ax,sectors
sub ax,sread
mov cx,ax
shl cx,#9
add cx,bx
jnc ok2_read
je ok2_read
xor ax,ax
sub ax,bx
shr ax,#9
首先我们看过启动盘里面代码存放的图,知道system模块,是一个好长好长的代码,所以复制过来是一件很麻烦的事,比如说代码好长,磁道都变了,等等复制出错了,检查一下有没有复制错呀?一堆事要做,所以我们调用了一个read_it函数来完成这个艰巨的任务,它怎么实现的,我们就先别理了!
有趣的是,我们又看到了一个熟悉的身影
SETUPSEG = 0x9020 ! setup starts here
……
jmpi 0,SETUPSEG
一看他,我们就知道CPU要执行的地方,又开始发生变化了。回顾一下:
jmpi 偏移地址,段地址
那我们就知道CPU要到0x90200去执行代码了!
好了,上面这张图,我引用了很多次,就是想告诉你,我们说了这么多,实际上,就是完成了这一点点功能。摊手!
实验一:修改开机的字符串
好了!大概就说到这里了,我们还有很多没说,比如一开始的实模式是什么?还有中断呀?还有刚刚call read_it 我们都说的语焉不详,但是没关系,一开始我们不要弄那么多,不然太容易迷失在代码中,对操作系统的整体概念却反而没有具体的认识,后面会慢慢说到这篇没有具体说到。
在下一章之前,我们来做个实验练练手:更改刚刚启动时的字符串,把Loading system 改成自己的名字,如wcdaren's os is loading
需要值的一提的就是
mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10 !读光标
mov cx,#24 !表示字符串的长度
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1
mov ax,#0x1301 ! write string, move cursor
int 0x10 !显示字符
…………
msg1:
.byte 13,10
.ascii "Loading system ..."
.byte 13,10,13,10
就是cx表示表示字符串的长度,到时如果我们写的字符串要是过长,一定要记得设置cx的值。
汇编知识补充:int 和 call
在这段代码的时候我们说因为system模块可能很大,要跨越磁道,我们调用了 read_track 这个函数来复制该函数。调用函数,在C语言的时候我们是学个学过的。就是调用完这个函数后,回到原代码处,继续往下执行。但是,那汇编是如何完成的呢?
! ok, we've written the message, now
! we want to load the system (at 0x10000)
mov ax,#SYSSEG
mov es,ax ! segment of 0x010000
call read_it
call kill_motor
………………
read_it:
mov ax,es
test ax,#0x0fff
我们先不用知道read_it这个函数到底是如何实现,我们先前说过
jmpi go,INITSEG
go: mov ax,cs
mov ds,ax
mov es,ax
知道go是一个标号,即一个地址,这个地址是代码开始到该标号的偏移量。
那我们就可以推出read_it也是一个标号。
那call read_it,我们一看就知道是跳到read_it,这里去执行。
这些我们都能理解,可我们讲go的跳转的时候,用的是jmpi
在说到jmpi的时候,我们说它跳到那9000处后,继续执行9000那边的代码(一条一条的执行下去)。
但是我们的call,就不一样了哦!他执行完了read_it后就会回到原来的地方执行下一条指令,即call kill_motor。
我们思考,计算机一定是有个地方,来存放当前的地址,等到那边的代码执行完了,就会来查看那存地址的地方,再跳回去。这就是栈了。
栈
栈:是一种具有特殊的访问方式的存储空间。它的特殊性就在于,最后进入这个空间的数据,最先出去。
我们用一个盒子和3本书来类比。
一个开口的盒子看成一个栈空间。现有有3本书,我们把他们放到盒子中,操作的过程如图。
问题来了,如果我们一次只能拿一本书,我们如何将3本书从盒子中取出来?
显然,必须从盒子的最上边取,取的顺序为:软件工程,C语言,高等数学,和放入的顺序相反。
从程序化的角度来讲,应该有一个标记,这个标记一直指示着盒子最上边的书。
如果说,上例中的盒子就是一个栈,我们可以看出,栈两个基本的操作:入栈和出栈,入栈就是加一个新的元素放到栈顶,出栈就是从栈顶取出一个元素。栈顶元素总是最后入栈,需要出栈时又最先被从栈中取出,栈的这种操作规则被称为:LIFO(Last In First Out,后进先出)。
现在的CPU都有栈的这种设计,并提供相关的指令以栈的方式访问内存空间:PUSH(入栈)和POP(出栈)。比如,push ax 表示将寄存器ax中的数据送入栈中,pop ax 表示从栈顶取出的数据送入ax。8086CPU的栈操作都是以字为单位进行的。
下面举例说明,我们把10000H~1000F这段内存当作栈来使用。
mov ax,0123H
push ax
mov bx,2266H
push bx
mov cx,1122H
push cx
pop ax
pop bx
pop cx
那我们如何告诉CPU我们把10000H~1000F这段内存当作栈呢?还有它怎么知道栈顶元素是什么呢?
先前,我们提到CS和IP,来定位一个地址。那栈也是如此的!8086CPU中,有两个寄存器,段寄存器SS和寄存器SP,栈顶的段地址存储在SS中, 偏移地址存储在SP中。
任意时刻,SS:SP指向栈顶元素
举例,push ax的执行,由以下两步
- SP = SP - 2, SS:SP指向当前栈顶前面的单元,以当前栈顶前的单元为新的栈顶;
-
将ax中的内容送入 SS:SP指向的内存单元处,SS:SP此时指向新的栈顶。
call
回到call的讲解。
CPU执行call指令时,进行两步操作:
- 将当前的IP或CS和IP压入栈中;
- 转移
call 指令有很多中格式,我们这里就单独那 call 标号 举例
- (sp) = (sp) - 2
((ss)*16 + (sp)) = (IP) - (IP) = (IP) + 16位移
16位位移 = 标号处的地址 - call指令后的第一个字节的地址;
16位位移的范围为-32768~32767,用补码表示;
16位位移由编译程序在编译时算出。
其实,简单来说就是
push IP
jmp near ptr 标号
哈哈,说到这更搞笑了,你们可能连jmp near ptr 是啥都不知道。
jmp near ptr 标号 的功能为:(IP) = (IP) + 16位移
- 16位位移 = 标号处的地址 - jmp指令后的第一个字节的地址
- near ptr 指名此处的位移为16位位移,进行的是段内近转移;
- 16位位移的范围为-32768~32767,用补码表示;
- 16位位移由编译程序在编译时算出
好了,我们确实把等等要回去的地址存储了,那,什么时候回去呢?也就是说回去的地址什么时候赋值回CS呢?
ret和retf
ret指令用栈中的数据,修改IP的内容,从而实现近转移;
retf指令用栈中的数据,修改CS和P的内容,从而实现远转移。
CPU执行ret指令时,进行下面两步操作
(1) (IP) = (ss)*16+(sp)
(2) (sp) = (sp)+2
CPU执行retf指令时,进行下面4步操作
(1) (IP) = (ss)*16+(sp)
(2) (sp) = (sp)+2
(3) (CS) = (ss)*16+(sp)
(4) (sp) = (sp)+2
可以看出,如果我们用汇编语法来解释ret和retf指令,则
CPU执行ret指令时,相当于进行:
pop IP
CPU执行retf指令时,相当于进行:
pop IP
pop CS
所以我们就明白
read_it:
mov ax,es
test ax,#0x0fff
die: jne die ! es must be at 64kB boundary
xor bx,bx ! bx is starting address within segment
rp_read:
mov ax,es
cmp ax,#ENDSEG ! have we loaded all yet?
jb ok1_read
ret
当我们跳转到read_it这里后,CPU会一直执行下去,直到ret,我们刚刚存储的值就会出栈,就会回到原来的地方。
中断
在我们开始说int指令的时候,我们先来说下中断。
任何一个通用的CPU,比如8086,都具备一种能力,可以在执行完当前正在执行的指令之后,检测到从CPU外部发送过来的或内部产生的一种特殊信息,并且可以立即对所接收到的信息进行处理。这种特殊的信息,我们可以称其为:中断信息。中断的意思是指,CPU不再接着(刚执行完的指令)向下执行,而是转去处理这个特殊信息。
那当CPU收到中断信息后,应该转去执行该中断信息的处理程序。既然要执行那里的程序,就需要修改CS:IP指向它的入口(即程序第一条指令的地址)。那地址如何获得呢?
中断信息中包含着标识中断源的类型码。中断类型码的作用就是用来定位中断程序处理程序。比如CPU根据中断类型码4,就可以找到4号中断的处理程序。可随之而来的问题是,若要定位中断处理程序,需要知道它的段地址和偏移地址,而如何根据8位的中断类型码得到中断处理程序的段地址和偏移地址呢?
通过中断向量表找到相应的中断处理程序的入口地址。
中断向量表在内存中保存,其中存放着256个中断源所对应的中断处理程序入口。
对于8086CPU机,中断向量表制定放在内存地址0处。从内存0000:0000到0000:03FF的1024个单元中存放着中断向量表。
中断处理,就是紧急处理,那处理完紧急的事,我们还是要回来原来的地方继续执行下去。那,就是像我们刚刚call一样,我们需要用到栈来保存我们现在的CS和IP。
大概说明中断这个过程
(1)(从中断信息中)取得中断类型码
(2)标志寄存器的值入栈(因为在中断过程中要改变标志寄存器的值,所以先将其保存在栈中);
(3)设置标志寄存器的第8位TF和第9位IF的值为0(这一步的目的后面将介绍)
(4)CS的内容入栈
(5)IP的内容入栈
(6)从内存地址为中断类型码x4 和 中断类型码x4+2的两个字单元中读取中断处理程序的入口地址设置IP和CS。
简洁点就是
1. 获得中断类型码N;
2. pushf
3. TF=0, IF=0
4. push CS
5. push IP
6. (IP) = (N * 4) , (CS) = ( N * 4 + 2)
既然我们把我们现在的CS和IP入栈了,可想而知,中断处理程序一定会有一个指令返回。
即,iret,可以描述为
pop IP
pop CS
popf
标志寄存器,先不讲
int指令
那现在再来说int 就简单多了。
int指令的格式为:int n,n为中断类型码,它的功能是引发中断过程。
CPU执行int n指令,相当于引发一个n号中断的中断过程,执行过程如下。
(1)取中断类型码n:
(2)标志寄存器入栈,IF=0,TF=0
(3)CS、IP入栈
(4) (IP)=(n*4),(CS)=(n*4+2)
从此处转去执行n号中断的中断处理程序。