操作系统(一) -- 操作系统的启动(bootsect、setup、head、main)
阅读原文时间:2021年04月20日阅读:1

说明

这个系列的博客用来记录自己学习操作系统遇到的一些问题、笔记、总结等;博主小白一只,如果有什么地方不对,请诸位多多包涵,如果能在评论下面指出就更加感激不尽了。以后如果有新的理解会更新。

宏观认识操作系统

什么是操作系统

操作系统是底层计算机硬件与上层应用软件之间的一个软件,计算机的一切活动都是通过cpu、内存、显卡、显示器等硬件设备来实现的;那为什么我们平时操作计算机的时候从来都不用关心这些东西呢?在c语言里面为什么一个printf(“hello world!”);就可以在屏幕上面显示出”hello world”,而不需要关心cpu、内存这些东西呢?这都是因为有操作系统的存在,其实我们在printf(“hello world!”);的时候都是需要cpu、内存、显卡、总线等硬件的协调配合才能在屏幕上看到”hello world!”,但是我们不用要管这些东西,因为有操作系统帮我们做了。

操作系统可以干什么

从上面那段话可以看出,操作系统是帮助我们管理计算机提高工作效率的,在操作系统的帮助下,我们不需要了解底层是如何实现的,只需要直接用就可以了。那么操作系统能管理哪些东西呢?CPU、内存、磁盘、文件、网络、电源等等操作系统都可以进行管理。

开始进入操作系统

开机的一瞬间,电脑在干嘛

上图是win10电脑刚刚打开时的一张图片(来源百度),我们的重点并不是这张图,而是这张图背后在干什么。计算机的工作就是从磁盘取指令到内存,然后执行指令;即“取指执行”,既然计算机只能做这一件事,并且电脑开机就运行了,因此在这个图片背后,计算机肯定也是在“取指执行”,那么到底是取的什么指令呢,换句话说,电脑刚刚开机的时候CS和IP寄存器的值到底是多少呢?

开机的一瞬间CS和IP的值是多少这个问题是由硬件决定的,对于X86的PC机来说,开机时, CS=0xFFFF; IP=0x0000;因此地址为0xFFFF0(ROM BIOS映射区),刚开机时,内存里面只有这个地方有数据,这部分程序的功能是:检查RAM、键盘、显示器等硬件(这部分数据是固化在内存里面的,只能读不能写)。然后将磁盘0磁道0扇区(一个扇区是512个字节)内容读入0x7c00处,磁盘的0磁道0扇区存储的就是操作系统的引导程序(即bootsect.s);同时设置CS=0x07c0,ip=0x0000,即开始执行操作系统。

bootsect.s

操作系统引导程序文件名是:bootsect.s,为什么不是bootsect.c呢?.s说明是汇编程序,.c是c程序,操作系统在刚刚启动的时候每一条指令都是严格要求的,不能有任何的出入,汇编程序和机器指令是一一对应的;但是c程序就不一样,比如:int a;a这个变量的地址就是随机的,所以不能是.c。

操作系统代码非常庞大,不可能一一讲到,只能是抓住一条主线来说,下面是bootsect.s里面的一段代码:

BOOTSEG = 0x07c0
INITSEG = 0x9000
SETUPSEG = 0x9020

entry start //关键字entry告诉链接器“程序入口”
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 //cs=0x9000
    mov ds,ax mov es,ax mov ss,ax mov sp,#0xff00
load_setup: //载入setup模块
    mov dx,#0x0000 mov cx,#0x0002 mov bx,#0x0200
    mov ax,#0x0200+SETUPLEN int 0x13 //BIOS中断
    jnc ok_load_setup
    mov dx,#0x0000
    mov ax,#0x0000 //复位
    int 0x13
    j load_setup //重读

首先看start:部分,前两行就是将 BOOTSEG赋给ds,将INTTSEG赋给es;第四行将si、di都赋值为零,然后ds和si组成的地址是07c00,es和di组成的地址是90000;

rep movw

表示移动字,移动的个数是cx=256,也就是512个字节,也就是说这段代码的作用就是将从0x07c00地址处开始的512个字节移动到0x90000处,为什么要移动呢?后面会讲。

jmpi go, INITSEG

这条指令是间接跳转,go->ip,INITSEG->CS,前面已经说了INITSEG就是0x9000,go是一个标号,go表示的是距离start的偏移,现在因为已经将0x07c00处的512个字节移动到了0x90000处,所以go相较于start的偏移其实也就是相较于INITSEG的偏移,所以也就是顺序执行。

go肯定是在从start开始的512个字节里面的。

load_setup: //载入setup模块
    mov dx,#0x0000 mov cx,#0x0002 mov bx,#0x0200
    mov ax,#0x0200+SETUPLEN int 0x13 //BIOS中断
    jnc ok_load_setup
    mov dx,#0x0000
    mov ax,#0x0000 //复位
    int 0x13
    j load_setup //重读

这部分就是导入setup模块了。这个地方利用的是13号中断导入的。

0x13是BIOS读磁盘扇区的中断: ah=0x02(读磁盘),al=扇区数量(SETUPLEN=4),
    ch=柱面号,cl=开始扇区,dh=磁头号,dl=驱动器号,es:bx=内存地址

到目前为止,只有bootsect.s的一个扇区被读进内存了,其他的还没有读进去,因此其他内容使用
int 0x13 号中断读进去。
开始扇区cl:02;扇区数:al:4;就是说从第二个扇区开始读4个扇区到内存,
ip的值为 es:bx ;es:0x9000;bx:0x0200;也就是说从0x90200开始;对的,因为bootsect是512个字节。512用十六进制表示就是200。所以,load_setup: 的功能就是将setup读进内存,紧接着bootsect后面。

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
    mov bp,#msg1 mov ax,#1301 int 0x10 //显示字符
    mov ax,#SYSSEG //SYSSEG=0x1000
    mov es,ax
    call read_it //读入system模块
    jmpi 0,SETUPSEG

读入setup之后,注意 int 0x10 这个中断的功能就是在屏幕上显示字符,显示 bp 的字符,即msgl,

msg1:.byte 13,10
        .ascii “Loading system...”
        .byte 13,10,13,10

因此就是在屏幕上显示 “loading system…”, cx 是表示显示的字符数量(字节),bx 是显示属性。具体这些东西可以自己百度。

后面的 read_it 还是读操作系统到内存里面。然后bootsect.s结束。因此bootsect.s的功能就是将操作系统读入内存。
并且显示logo:”loading system…”。bootsect到此结束。

bootsect.s结束之后应该将控制权交给setup,如何跳转到setup呢?

    jmpi 0, SETUPSEG

前面说了setup的位置是0x90200,现在ip是0,SETUPSEG就应该要是0x9020,查看最前面宏定义,果然。

bootsect.s小结

bootsect.s是操作系统的最开始部分,共512个字节,在磁盘的0磁道0扇区位置,首先内存里面肯定是有代码的,具体存在哪个位置是由硬件决定的,然后从那个位置开始读入操作系统,首先读入的是操作系统的bootsect部分,对于x86PC来说,bootsect读进来是放在0x07c00这个位置,然后将其转移到0x90000这个位置,并继续执行;利用int 0x13中断,将操作系统的setup读入到0x90200开始的内存处,setup在磁盘上是第二到第五个扇区,第一个是bootsect扇区;读入setup之后,bootsect继续执行,在屏幕上显示开机logo “loading system…”。然后进入 read_it 继续读操作系统模块,然后将控制权转移到setup中,执行setup中的内容。

setup.s

setup.s主要是完成系统启动前的设置。

SYSSEG = 0x1000

start: mov ax,          #INITSEG mov ds,ax mov ah,#0x03
    xor bh,bh int 0x10  //取光标位置dx mov [0],dx
    mov ah,#0x88 int 0x15 mov [2],ax ...
    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模块移到0地址
    movsw
    jmp do_move 

操作系统是管理各种硬件的,要管理这些硬件必要首先要知道这些硬件到底是什么东西,是什么型号的,应该用怎样的数据结构来管理。

上面这段代码的作用就是获取硬件参数,然后将这些信息放在0x90000开始的地方;最后将操作系统从0x90000开始处移到地址0开始处,这就是为什么一开始要移动bootsect的原因,因为操作系统可能会覆盖0x07c00这个地址。

0x90000 2 光标位置
0x90002 2 扩展内存数
0x9000C 2 显卡参数
0x901FC 2 根设备号

这个地方我有一点不明白,前面已经将bootsect以及后面的东西都放在0x90000处了,而且0x90000~0x90200这个位置存储的就是bootsect部分,现在又将硬件参数存放在这个地方,那就肯定会将bootsect部分的内容覆盖掉,那么这个移动有何意义呢?先mark一下。

另外注意一下,这段代码是将操作系统的system模块移动到0地址处的

也就是说,前面的bootsect、setup都没有移动的。并且以后操作系统的位置都不变了。

setup的其余部分就都不说了,但是在setup结束的时候,有这两行代码:

mov ax,#0x0001 mov cr0,ax
jmpi 0,8

前面提到过jmpi这条指令,意思是将前操作数赋给ip,后操作数赋给cs,然后跳转到cs、ip所表示的位置执行。但是这里能不能这样解释呢?如果是那么就是跳到0x00080处执行,对吗?刚刚执行完setup后面应该是继续执行system模块才是,system模块都被移动到0地址处了,也就是说现在system模块最开始位置是0地址处,即应该执行0x00000处代码而不是0x00080处,如果直接执行0x00080,结果肯定是死机。所以说现在jmpi这条指令肯定不能这么解释了。注意前面一行代码

mov ax,#0x0001 mov cr0,ax

这条指令的作用就是切换模式,从实模式到保护模式(也就是从16位到32位),这条指令
的作用就是将cr0这个寄存器的最后一位置为1,cr0最后一位PE=1就是启动保护模式,在
保护模式下

jmpi 0,8

这条指令应该被解释成查gdt表,关于什么是gdt表可以自己搜索。

以前CS里面放的是地址,现在CS里面放的是表的下标(称为”选择子”),这个表就是gdt(全局描述表)表,那么这个表是哪里来的呢?在setup.s前面有这样一段代码:

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      // 0
    .word 0x07FF, 0x0000, 0x9A00, 0x00C0     // 8
    .word 0x07FF, 0x0000, 0x9200, 0x00C0     // 16

可以看到有一个gdt表的构建,每一个word都是一个表项,后面是四个值,16位一个,共64位。gdt表的单位是字节,cs是8
所以表示的是:

.word 0x07FF, 0x0000, 0x9A00, 0x00C0

这个表项,那么这个表是怎么看的呢?

上图中段基址表示的部分就是:0x0000。也就是说接下来会跳到0x0000处执行。

那么0x00000000处是什么东西呢?
操作系统是一堆源码,但是内存ROM里面读取的第一条指令的地址是确定的,就是磁盘的0磁道0扇区,读取操作系统必须从bootsect开始,也就是说0磁道0扇区这里必须是操作系统的bootsect.s文件,这个怎么保证呢?通过makefile,一堆操作系统的源码经过makefile的控制最后形成bootsect、setup、system这种形式(就是第三个图),也就是Image(镜像),然后将bootsect存储在磁盘的0磁道0扇区位置,于是bootsect就到了0磁道0扇区了。

磁盘上面的Image需要很多东西的支持,类似于一个树状结构。

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

Image 需要 boot/bootsect、boot/setup、tools/system、tools/build
tools/build boot/bootsect boot/setup tools/system > Image
bootsect依赖于bootsect.s,setup依赖于setup.s,

tools/system依赖于boot/head.o init/main.o $(DRIVERS) …,如果这些东西都有的话就链接成tools/system,同理上面的boot、setup……也都是如此,然后再将这些模块形成Image。这样Image的system部分第一个文件就是head.s,所以setup结束后就跳到head.s部分去了。

setup小结

setup是完成系统启动前设置的,它将硬件的参数存放在0x90000处,然后将system部分移动到从地址0开始的位置;临时建立gdt、idt表,并且从实模式进入到了保护模式(16位到32位)

head.s

system开始的第一个文件是head.s,存放在地址0处,因此setup结束后执行的就是head.s文件。
setup.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
    call setup_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)

在head.s里面会重新设置idt表、gdt表(call setup_idt、call_setup_gdt),前面setup里面设置的gdt和idt都是临时的;这里会重新设置。还会开启A20地址线(je 1b),开启A20地址线之后寻址范围就是4G而不再是1M。

IDT表是中断函数表,从此int n 不再是DOS中断了,而是在IDT表中找到中断函数的地址,执行
注意是:硬件查表,不是软件,idt、gdt表的查表方法都是硬件规定好的,目的就是为了加快速度。

注意,在head.s使用的汇编又和前面bootsect、setup里面使用的汇编不一样,在head.s里面使用的是产生32位代码汇编,而bootsect、setup里面使用的是产生16位代码的汇编。另外在操作系统的.c文件里面还使用了一种汇编,叫做“内嵌汇编”。

after_page_tables:
    pushl $0 pushl $0 pushl $0 pushl $L6
    pushl $_main jmp setup_paging
L6: jmp L6
setup_paging: 设置页表 ret

前面开启20号地址线之后就jmp到after_page_tables这个标号,在setup_paging执行完后,ret到哪里呢?到main()函数。在after_page_tables里面将main函数三个参数、L6、main函数的入口地址都压入栈中,在setup_paging的ret直接跳_main,如果main函数再返回的话就跳到L6处,从上面可以看到

L6: jmp L6

这是一个死循环,也就是说如果操作系统执行了这条指令,那么就会死机。

其实从head.s到main.c的过程和c语言里面的函数调用是一样的,首先将函数执行完之后的下一个地址和函数参数压入栈中,然后通过jmp命令跳到子函数的执行处,执行完了之后再利用ret跳到程序原来执行的地方。

main.c

main函数完成了各种硬件数据结构的初始化。永远不会退出,如果退出就死机了。

void main(void)
{
    mem_init();
    trap_init();
    blk_dev_init();
    chr_dev_init();
    tty_init();
    time_init();
    sched_init();
    buffer_init();
    hd_init();
    floppy_init();
    sti();
    move_to_user_mode();
    if(!fork()){init();}   // 这行永远不会退出
}

前面说了main函数由三个参数,为什么这里没写出来呢?main函数的三个参数为envp、argc、argv,但是此处并没有使用,所以此处的main只保留传统main形式。从main函数内容可以看到,main函数的工作就是init:内存、中断、设备、时钟、CPU等内容的初始化。

这里只介绍一个men_init()

void mem_init(long start_mem,long end_mem)
{
    int i;
    for(i=0; i<PAGING_PAGES; i++)
    mem_map[i] = USED;
    i = MAP_NR(start_mem);
    end_mem -= start_mem;
    end_mem >>= 12;
    while(end_mem -- > 0)
    mem_map[i++] = 0; 
}

其实这个函数就是初始化mem_map这个数组,start_men、end_men这些参数都是在setup的时候就获取到的。

总结

其实bootsect、setup、heads、main这些文件就做了两件事:
1,读入操作系统并移动到合适的位置,
2,初始化(为每一个硬件建立数据结构、并初始化)

参考资料

哈工大李志军操作系统
linux0.11内核完全注释 赵炯著