• 操作系统之启动过程


    本文参考MOOC哈工大操作系统课程与课件
    主要基于Linux 0.11系统展开
    ”Author:Mayiming“

    打开电源后,计算机执行了什么指令?

    1. ROM BIOS

    1. x86 PC开机后CPU处于实模式,寻址方式为CS:IP(CS左移4位+IP)
    2. 开机时,初始化的CS=0xFFFF,IP=0x0000,此时指向的地址为0xFFFF0(ROM BIOS映射区,上电后内存中只有此处有代码)
      此时载入的是BIOS程序,还没有进入操作系统的部分,BIOS程序是固化在主板上的一段程序负责基本输入输出、系统设置信息、开机后自检程序和系统自启动程序。其主要功能是为计算机提供最底层的、最直接的硬件设置和控制。目前大多烧录在可擦写ROM中,因此也可以进行BIOS升级。
    3. 此时通过0xFFFF0此处的BIOS代码检查RAM、键盘、鼠标、磁盘等硬件和IO设备(此处执行不通过则不会执行操作系统,代表硬件有问题)
    4. 然后读取磁盘0磁道0扇区位置的数据到0x7c00处(从磁盘读取到内存0x7c00处),磁盘0磁道0扇区(一个扇区512字节)处的数据即为引导扇区,存放着操作系统的引导程序
    5. 设置CS=0x07c0IP=0x0000即指向地址0x7c00
    6. 读入引导扇区里的代码之后,我们就离开了BIOS,操作系统的故事就从这里开始了
      在这里插入图片描述

    2. bootsect.s

    下面解析离开BIOS后,首先执行的bootsect.s汇编代码

    .globl begtext,begdata,begbss,endtext,enddata,endbss
    .text //文本段
    begtext:
    .data //数据段
    begdata:
    .bss //未初始化数据段
    begbss:
    entry start //关键字entry告诉链接器“程序入口”
    start:
    mov ax, #BOOTSEG mov ds, ax // 设置ax=0x07c0,ds=ax
    mov ax, #INITSEG mov es, ax //  设置ax=0x9000,es=ax
    mov cx, #256 //  设置cx=256 (十进制256)
    sub si, si sub di, di //  si,di归零
    rep movw //  rep movw 重复执行移动直到cx归零,即移动256个16位字(也就是512字节) 源地址ds:si,目的地址es:di
    jmpi go, INITSEG // jmpi go, INITSET 间接跳转,意思就是跳转到go标签处的代码处
    // BOOTSEG = 0x07c0
    // INITSEG = 0x9000
    // SETUPSEG = 0x9020
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    上述代码将0x07c00处的512字节也就是引导扇区传送到0x90000
    此时start下面的这部分代码已经是移动到了0x90000处了

    从上一段代码跳转到go处:

    go: mov ax,cs //cs=0x9000
    mov ds,ax mov es,ax mov ss,ax mov sp,#0xff00 // 设置ds=0x9000,es=0x9000,ss=0x9000,sp=0xff00
    load_setup: //载入setup模块
    mov dx,#0x0000 mov cx,#0x0002 mov bx,#0x0200 // 设置dx=0x0000,cx=0x0002,bx=0x0200,ax=0x0200+setup长度
    mov ax,#0x0200+SETUPLEN int 0x13 //BIOS中断
    jnc ok_load_setup
    mov dx,#0x0000
    mov ax,#0x0000 //复位
    int 0x13
    jmp load_setup //重读
    // SETUPLEN=4
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    上述代码从驱动器0的柱面0磁头号0扇区号2开始读取SETUPLEN = 4个扇区,放到内存地址0x90200后面(也就是引导扇区的512字节后面)
    在这里插入图片描述
    INT 0x13中断解释:
    ax寄存器高八位为ah,低八位为al,此时ah=0x02代表读磁盘,al为setup长度,代表读取的扇区个数
    同样cx分为chcldx分为dhdl,此时读取的磁盘的位置为柱面号(磁道号)ch = 0开始扇区为cl = 2磁头号为dh = 0驱动器号为dl = 0,读取到的内存地址为es:bx=0x9000:0x0200=0x902000x900000x90200之间刚好512个字节)
    INT 0x08 参数参考

    载入setup模块的代码后,执行bootsect.s最后一段代码
    boot扇区的代码最后跳转到Ok_load_setup

    Ok_load_setup: //载入setup模块
    mov dl,#0x00 mov ax,#0x0800 //ah=8获得磁盘参数
    int 0x13 mov ch,#0x00 mov sectors,cx 
    mov ah,#0x03 xor bh,bh int 0x10 //读光标
    mov cx,#24 mov bx,#0x0007 // cx=24为显示字符长度 bx=7是显示属性
    mov bp,#msg1 mov ax,#1301 int 0x10 //显示msg1位置的字符 Loading system...
    mov ax,#SYSSEG //SYSSEG=0x1000 
    mov es,ax 
    call read_it //读入system模块
    jmpi 0,SETUPSEG // 转入0x9020:0x0000 执行setup.s
    // bootsect.s中的数据 //在文件末尾
    // sectors: .word 0 //磁道扇区数
    // msg1:.byte 13,10
    // .ascii “Loading system...”
    // .byte 13,10,13,10
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    上述代码获取磁盘参数放到sectors,读取光标,显示加载系统字符,读入system模块,最后jmpi跳转到setup模块

    总结一下bootsect.s代码的功能就是:

    1. 0x07c00处的512字节也就是引导扇区传送到0x90000
    2. 从驱动器0的柱面0磁头号0扇区号2开始读取SETUPLEN = 4个扇区(setup模块),放到内存地址0x90200后面(也就是boot扇区的512字节后面)
    3. 获取磁盘参数放到sectors,读取光标,显示加载系统字符,读入system模块,最后jmpi跳转到setup模块

    下面单独解析一下读入system模块的call read_it
    system模块可能很大,要跨越磁道
    jb代表jump below,此时应该是为了防止程序跳到system

    read_it: mov ax,es cmp ax,#ENDSEG jb ok1_read // ax=es,ax与#ENDSEG比较 ENGSEG代表镜像结束的位置(具体与镜像大小SYSSIZE有关)
    ret // 函数返回 return
    ok1_read:
    mov ax,sectors // 设置ax为磁盘扇区数目
    sub ax,sread //sread是当前磁道已读扇区数, ax = ax - sread 等于未读扇区数
    call read_track //读磁道...
    // ENDSEG=SYSSEG+SYSSIZE
    // SYSSIZE=0x8000 //该变量可根据
    // Image大小设定(编译操作系统时)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    上述这段代码把system整体读取到内存中

    3. setup模块

    至此bootsect.s引导扇区的代码执行完毕,此时需要转入setup模块进行执行。
    setup模块作用是完成OS启动前的设置部分

    3.1 移动system模块

    start: mov ax,#INITSEG mov ds,ax mov ah,#0x03 // #INITSEG=0x9000
    xor bh,bh int 0x10 mov [0],dx //取光标位置dx放入内存[0]=0x90000表示间接寻址,段地址ds:偏移地址[0]
    mov ah,#0x88 int 0x15 mov [2],ax ... // 获取扩展内存大小ax, 放入[2]=0x90002位置
    cli ///不允许中断
    mov ax,#0x0000 cld
    do_move: mov es,ax add ax,#0x1000
    cmp ax,#0x9000 jz end_move
    mov ds,ax sub di,di
    sub si,si
    mov cx,#0x8000
    rep // 将system模块移动到0x00000地址
    movsw  // 源地址ds:si,目的地址es:di
    jmp do_move
    // SYSSEG = 0x1000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述
    因为此时CS:IP最多指向的地址空间为1M(地址位数为20位),2^20bit=1M,所以需要扩展内存。

    SYSSET=0x1000system模块的代码在地址0x10000处起始,上述代码将0x10000-0x90000的代码平移到0x00000-0x80000
    此时可以解释为何上面要移动0x07c00的代码到0x90000,就是因为这段地址要放system

    3.2 转入保护模式

    下面CPU要转入保护模式,因为实模式的内存寻址空间太小了,转入保护模式后CS:IP寻址方式会改变,可以寻址32位地址空间(4G),同时INT中断的方式也会发生一些变化。

    end_move: mov ax,#SETUPSEG mov ds,ax
    lidt idt_48 lgdt gdt_48//设置保护模式下的中断和寻址
    进入保护模式的命令...
    idt_48:.word 0 .word 0,0 //保护模式中断函数表
    gdt_48:.word 0x800 .word 512+gdt,0x9 
    gdt: .word 0,0,0,0
    .word 0x07FF, 0x0000, 0x9A00, 0x00C0
    .word 0x07FF, 0x0000, 0x9200, 0x00C0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    上述代码的功能为写GDT表,保护模式寻址的过程为从GDT表中寻找CS指向的地址,取出表中的地址与IP组合成32位地址。
    在这里插入图片描述
    在这里插入图片描述

    call empty_8042 mov al,#0xD1 out #0x64,al  // 0xD1表示写数据到8042的P2端口
    //8042是键盘控制器,其输出端口P2用来控制A20地址线
    call empty_8042 mov al,#0xDF out #0x60,al
    //选通A20地址线 call empty_8042
    初始化8259(中断控制) //一段非常机械化的程序
    mov ax,#0x0001 mov cr0,ax // cr0寄存器最低位 置1 
    jmpi 0,8 // IP=0, CS=8, 此时是保护模式了,需要根据CS去查GDT表
    // 通过查表该地址指向 0 地址
    
    empty_8042:
    .word 0x00eb,0x00eb
    in al,#0x64 // 读取到al 
    test al,#2  // 跳转test
    jnz empty_8042
    ret
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述
    在这里插入图片描述

    4. system模块

    system模块(目标代码)中的第一部分代码? head.s
    为什么head.ssystem模块的第一段代码,这是由makefile控制的

    linux/Makefile:

    disk: Image
    dd bs=8192 if=Image of=/dev/PS0  // /dev/PS0是软驱
    Image: boot/bootsect boot/setup tools/system tools/build
    tools/build boot/bootsect boot/setup tools/system > Image
    tools/system: boot/head.o init/main.o $(DRIVERS) …
    $(LD) boot/head.o init/main.o $(DRIVERS)-o tools/system
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    4.1 head.s

    下面来看head.s做了什么:

    stratup_32: movl $0x10,%eax mov %ax,%ds mov %ax,%es
    mov %as,%fs mov %as,%gs //指向gdt的0x10项(数据段)
    lss _stack_start,%esp //设置栈(系统栈)
    call setup_idt // 设置idt
    call setup_gdt // 设置gdt
    xorl %eax,%eax
    1:incl %eax
    movl %eax,0x000000 cmpl %eax,0x100000
    je 1b //0地址处和1M地址处相同(A20没开启),就死循环
    jmp after_page_tables //页表,什么东东?
    setup_idt: lea ignore_int,%edx 
    movl $0x00080000,%eax movw %dx,%ax
    lea _idt,%edi movl %eax,(%edi)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    head.s做了一系列设置(堆栈、idtgdt、设置地址线等)执行后需要执行main.c,转入C语言执行

    after_page_tables:
    pushl $0 pushl $0 pushl $0 pushl $L6 // 将0,0,0,L6压入堆栈
    pushl $_main jmp set_paging  // 将_main压入堆栈 跳转到设置页表
    L6: jmp L6
    setup_paging: 设置页表 ret //设置页表  然后return 
    
    • 1
    • 2
    • 3
    • 4
    • 5

    为什么堆栈要压入 0,0,0,L6,_main这样的顺序,和下图C执行函数的堆栈结构有关系。
    在这里插入图片描述
    在这里插入图片描述

    4.2 main(0,0,0)

    main(0,0,0)三个参数其实没有用只是为了envp,argv,argc完整性。
    下面main函数中执行了一系列的初始化操作,初始化了内存、时间、硬盘、缓冲区等。
    在这里插入图片描述

    4.3 解析mem_init()

    在linux/mm/memory.c中

    void mem_init(long start_mem,long end_mem) // start_mem与系统大小有关,end_men参数从3.1中扩展内存获取处得到
    { 
    int i;
    for(i=0; i<PAGING_PAGES; i++)
    mem_map[i] = USED; // 将内存的页表从0开始的地方设置一段为USED, 这一段即为系统所在的地址
    i = MAP_NR(start_mem);
    end_mem -= start_mem; // end_men-start_men为剩余的内存的大小
    end_mem >>= 12; // end_men >>=12,end_men右移12位,代表除以4k, 此时mem_map每一位代表了一个4k的内存页是否被使用
    while(end_mem -- > 0)
    mem_map[i++] = 0; } // 将除系统地址外的内存页初始化为未使用
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    通过main.c初始化完成后,操作系统即启动了,main()函数是一个永不返回的函数,会一直执行下去。
    在这里插入图片描述

  • 相关阅读:
    【Golang】DFA算法过滤敏感词Golang实现
    【计算机网络】HTTP 重定向的应用场景
    SpringBoot基于AOP实现RocketMQ发送与消费
    Java 反射详解:动态创建实例、调用方法和访问字段
    面试官:什么是JIT、逃逸分析、锁消除、栈上分配和标量替换?
    leetcode做题笔记169. 多数元素
    P1903 [国家集训队] 数颜色 / 维护队列
    Windows平台 使用jarsigner对Apk签名
    驱动三种IO模型
    MySQL学习笔记之单行函数
  • 原文地址:https://blog.csdn.net/qq_37207042/article/details/128051165