二:操作系统的引导(1)

前面应该有一章,“一:操作系统的概述”,懒得写,但是很重要,最好去看下视频,如果有人看的话,以后有空再补

首先我们要有一个认知,就是计算机是怎么运行的

从白纸到图灵机

二:操作系统的引导(1)_第1张图片
大脑计算的过程

计算机开始的时候就是一个做计算的机器
那我们从人计算的过程来思考
比如说,我们在纸上看到 3 + 2
大脑算出结果是5
那就用笔在纸上写上5这个答案

那我们用一个自动设备来模拟这个过程


二:操作系统的引导(1)_第2张图片
图灵机

纸带模拟纸
控制器模拟大脑
读写头来模拟眼睛和笔

这里控制里的表是固定的,比如它只能进行加法运算
你给3和5,它只会算出8

从图灵机到通用图灵机

二:操作系统的引导(1)_第3张图片
通用图灵机

上面说的图灵机,控制器里面的逻辑是固定的,就像一个只会做一道菜的厨子,他的脑子里只够装下一个道菜的做法,不会学习新的菜谱。
那么通用图灵机就是一个可以看懂菜谱的厨师了。
他的控制器就像这个聪明的厨师的大脑,一直处于一个求知的状态。每看到一道菜谱,就做一道菜。控制器从纸带中载入一个新的控制器动作,启动这个动作后,后面获取的数据,就是在这套逻辑下开始运行。比如载入一个qq的逻辑,那么控制器就在给出qq的逻辑判断,你点发送,它就知道你是要把这消息发送过去。

从通用图灵机到计算机

二:操作系统的引导(1)_第4张图片
冯·诺依曼存储程序思想

冯·诺依曼存储程序思想
将程序和数据存放到计算机内部的存储器中,计算机在程序的控制下一步一步进行处理

存储器:那个厚厚的菜谱

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位的物理地址。

二:操作系统的引导(1)_第5张图片
image.png

也就是: 段地址X16 + 段偏移地址 = 物理地址
二:操作系统的引导(1)_第6张图片
image.png

  • 段地址X16

其实就是左移4位,这里的位指的是二进制的位,像图中给的1230其实是16进制的,比如这里的十六进制的0,实际上就是二进制的0000,二进制左移了4位,也就是16进制的0向左移动一个下。那原本1230,向左移动一下,就是12300再加上00C8,就可以得出了123C8了

我们再明确一点,CS和IP两个的寄存器,结合起来指示了CPU当前要读取指令的地址。
综上,可得
CS:代码段寄存器
IP:指令寄存器
等等,要是有人不知道寄存器的话,我只能说,真是正个人被你打败了呢。


image.png

这里的方格就好比一个寄存器,你看第二个方格里写了9,第三个方格里与第四个方格,合起来是13,那,你怎么想都知道,一个方格只能写0~9,不可能写出一个11吧。那下面的16位寄存器就应该明了了,寄存器就一个暂时存放数据的地方


16位寄存器的逻辑结构

那计算机里只能存1和0,那这里面的方格就只能存1和0咯
二:操作系统的引导(1)_第7张图片
image.png

都说到这里了,我们就顺便说下,不同的CPU,寄存器的个数,结构是不想同的。那8086CPU有14个寄存器,每个寄存器有一个名称,我们可以给他们分类
  • 通用寄存器
  • 控制寄存器
  • 段寄存器


    二:操作系统的引导(1)_第8张图片
    image.png

    等等有人要吐槽我刚刚的分类了,说怎么没有指令寄存器,随便分的嘛,你大概知道是干啥的就好了嘛,又不是要考试。你没看我图都是到处乱截的吗?
    这里要说下通用寄存器AX,BX,CX,DX,细心的同学发现了他们是可以再分的分成一个AH和AL,H就是高的意思,那L就是低咯


    二:操作系统的引导(1)_第9张图片
    image.png

    为什么要这样呢?还记得我刚刚说过8086前面有8085,8080的CPU吗?我说过他们是8位机,也就是他们只能处理8位,所以为了兼容他们,我们就把AX再细分了一下,这样,我们就可以通过AH传送一个8位的数据了,不然你传输16位,他们是识别不全的,就会造成混乱。

后面要是都遇到什么寄存器,再慢慢说吧!我怕你们都快忘记这是一个操作系统教程了。我们就先不说实模式是什么了,因为这样还要说到保护模式,我们就暂缺忽略先。

二:操作系统的引导(1)_第10张图片
image.png

那此时在看这图的时候,我们就知道了,刚开机的时候
CS=0xFFFF,IP=0x000,那么我们可以知道CPU现在指向内存的0xFFFF0处,也就是图中的ROM BIOS映射区那

  • BIOS

也就是基本输入/输入程序拉,英文你就自己想嘛,base input output system??随便打的,不知道对不对?尴尬!我们就只要知道,它是固化在内存里面的,因为我们说过CPU是一个苦力,会一定不断的执行一条条步骤,那前提是,你要告诉他第一条是在哪里,他才会不停的做下去,BIOS就是他第一件要做的事,那这事就是计算机开机时执行系统各部分的自检,建立起系统需要使用的各种配置表,并且把处理器和系统其余本分初始化到一个已知状态,等等。有人会问,那ROM BIOS和ROM BIOS
映射区是啥区别?因为会设计到兼容等问题,我就不说了,你只要知道,ROM BIOS存放着我刚刚说的那些功能的代码,到时,那些代码会被复制到这个映射区,并被CPU执行。

那第4就不用说了咯,第5就有点意思了。


二:操作系统的引导(1)_第11张图片
image.png

那这个0磁盘0扇区就是一个512k的引导扇区了。
这时候CS=0x7c00,IP=0x000
那就是CPU会在0x7C00处取指执行
那。。。
终于要开始代码了。

0x7c00处存放的代码

二:操作系统的引导(1)_第12张图片
image.png

接下来代码,我会先放一份源码,其余的是抽取出来重要的代码。第一份看是有个整体的认知,别的就是摘取一些重要的代码分析,并不是全部,只是一些主干的代码

源码: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

二:操作系统的引导(1)_第13张图片
image.png

大概说一下汇编
这里的汇编,源操作数在后面,目标操作数在前面


二:操作系统的引导(1)_第14张图片
mov指令格式

注意到一点就是,不可以直接把数据放到段寄存器,所以我们都是先把数据放到通用寄存器,再把通用寄存器的值赋到段寄存器

  • 通用寄存器
    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

二:操作系统的引导(1)_第15张图片
image.png

add是加法,sub是减法,还是遵循源操作数在后面,目标操作数在前面
比如 :sub ax ,8 如果ax原本的值是5,那就是5+8=13,然后把13放到ax中

sub si,si
sub di,di

自己减自己,当然就是零呀!所以si,di都是0.
我们前面说过,CPU的地址是由段地址和偏移地址组成的,就是这个图


二:操作系统的引导(1)_第16张图片
image.png

我们只知道段地址,是无法确定一个地址的,所以还需要两个偏移地址,那就是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的地方。


二:操作系统的引导(1)_第17张图片
bootsect.s移动

二:操作系统的引导(1)_第18张图片
image.png
    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就是从代码执行开始的地方,经过了的偏移量


二:操作系统的引导(1)_第19张图片
image.png

好像有点难懂是不是?我们先跳出来,讲下为什么要有它,再反过来思考它的意思?
举个例子,我们在看一本书,比如说《百年孤独》,我是在宿舍看的!现在看到了第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处,然后继续执行后面的代码!那后面的代码呢?
我们先看下


二:操作系统的引导(1)_第20张图片
Linux 0.11内核在1.44MB磁盘上的分布情况

再来一张图,告诉我们等等要干什么!


二:操作系统的引导(1)_第21张图片
image.png

综上,我们就知道,我们要把磁盘有4个扇区,辣么大的setup模块,即setup.s移动到已经位于内存9000处的bootsect.s后面,我们说过bootsect.s是512k,那地址是多少呢,
二:操作系统的引导(1)_第22张图片
image.png

我们的地址都是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

二:操作系统的引导(1)_第23张图片
image.png
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时


二:操作系统的引导(1)_第24张图片
image.png

这个是一样的,只是别人有点高级,是动画效果的呢!我们就是显示字符串“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模块

我们再往下分析。
还记得我上次发的一张图吗?

二:操作系统的引导(1)_第25张图片
image.png

这样我们就很容易知道了吧,先让ax=0x1000,调用了一个read_it的函数,我们就可以猜,


二:操作系统的引导(1)_第26张图片
image.png

这代码八成是把原本磁盘上setup.s后面的代码拷贝到内存的0x1000处。那我们就大概的看一下这个函数

read_it //读入system模块

二:操作系统的引导(1)_第27张图片
image.png
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去执行代码了!


二:操作系统的引导(1)_第28张图片
image.png

好了,上面这张图,我引用了很多次,就是想告诉你,我们说了这么多,实际上,就是完成了这一点点功能。摊手!

实验一:修改开机的字符串

好了!大概就说到这里了,我们还有很多没说,比如一开始的实模式是什么?还有中断呀?还有刚刚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的值。


image.png

二:操作系统的引导(1)_第29张图片
image.png
二:操作系统的引导(1)_第30张图片
image.png

汇编知识补充:int 和 call

二:操作系统的引导(1)_第31张图片
image.png

在这段代码的时候我们说因为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


二:操作系统的引导(1)_第32张图片
image.png

在说到jmpi的时候,我们说它跳到那9000处后,继续执行9000那边的代码(一条一条的执行下去)。
但是我们的call,就不一样了哦!他执行完了read_it后就会回到原来的地方执行下一条指令,即call kill_motor。
我们思考,计算机一定是有个地方,来存放当前的地址,等到那边的代码执行完了,就会来查看那存地址的地方,再跳回去。这就是栈了。

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

二:操作系统的引导(1)_第33张图片
image.png

我们用一个盒子和3本书来类比。
一个开口的盒子看成一个栈空间。现有有3本书,我们把他们放到盒子中,操作的过程如图。
问题来了,如果我们一次只能拿一本书,我们如何将3本书从盒子中取出来?
显然,必须从盒子的最上边取,取的顺序为:软件工程,C语言,高等数学,和放入的顺序相反。


二:操作系统的引导(1)_第34张图片
image.png

从程序化的角度来讲,应该有一个标记,这个标记一直指示着盒子最上边的书。
如果说,上例中的盒子就是一个栈,我们可以看出,栈两个基本的操作:入栈和出栈,入栈就是加一个新的元素放到栈顶,出栈就是从栈顶取出一个元素。栈顶元素总是最后入栈,需要出栈时又最先被从栈中取出,栈的这种操作规则被称为:LIFO(Last In First Out,后进先出)。
现在的CPU都有栈的这种设计,并提供相关的指令以栈的方式访问内存空间:PUSH(入栈)和POP(出栈)。比如,push ax 表示将寄存器ax中的数据送入栈中,pop ax 表示从栈顶取出的数据送入ax。8086CPU的栈操作都是以字为单位进行的。
下面举例说明,我们把10000H~1000F这段内存当作栈来使用。


二:操作系统的引导(1)_第35张图片
8086CPU的栈操作
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此时指向新的栈顶。


    二:操作系统的引导(1)_第36张图片
    image.png

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位移

  1. 16位位移 = 标号处的地址 - jmp指令后的第一个字节的地址
  2. near ptr 指名此处的位移为16位位移,进行的是段内近转移;
  3. 16位位移的范围为-32768~32767,用补码表示;
  4. 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个中断源所对应的中断处理程序入口。


二:操作系统的引导(1)_第37张图片
中断向量表

对于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号中断的中断处理程序。

你可能感兴趣的:(二:操作系统的引导(1))