美文网首页
二:操作系统的引导(1)

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

作者: Wcdaren | 来源:发表于2018-09-17 15:19 被阅读0次

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

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

    从白纸到图灵机

    大脑计算的过程

    计算机开始的时候就是一个做计算的机器
    那我们从人计算的过程来思考
    比如说,我们在纸上看到 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位的物理地址。

    image.png
    也就是: 段地址X16 + 段偏移地址 = 物理地址
    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咯
    image.png
    都说到这里了,我们就顺便说下,不同的CPU,寄存器的个数,结构是不想同的。那8086CPU有14个寄存器,每个寄存器有一个名称,我们可以给他们分类
    • 通用寄存器
    • 控制寄存器
    • 段寄存器


      image.png

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


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

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

    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就有点意思了。


    image.png

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

    0x7c00处存放的代码

    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
    
    
    image.png

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


    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

    image.png

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

    sub si,si
    sub di,di
    

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


    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的地方。


    bootsect.s移动
    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就是从代码执行开始的地方,经过了的偏移量


    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处,然后继续执行后面的代码!那后面的代码呢?
    我们先看下


    Linux 0.11内核在1.44MB磁盘上的分布情况

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


    image.png
    综上,我们就知道,我们要把磁盘有4个扇区,辣么大的setup模块,即setup.s移动到已经位于内存9000处的bootsect.s后面,我们说过bootsect.s是512k,那地址是多少呢,
    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

    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时


    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模块
    

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

    image.png

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


    image.png

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

    read_it //读入system模块

    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去执行代码了!


    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
    image.png image.png

    汇编知识补充:int 和 call

    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


    image.png

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

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

    image.png

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


    image.png

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


    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此时指向新的栈顶。


      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个中断源所对应的中断处理程序入口。


    中断向量表

    对于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)

          本文链接:https://www.haomeiwen.com/subject/ivvtnftx.html