启动与开始的区别,什么叫做启动

首页 > 经验 > 作者:YD1662024-03-01 14:12:57

本章主要介绍Linux0.11的启动过程(开始main函数之前的过程),主要是对bootsect.s、setup.s、head.s三个程序的介绍,硬件环境为Linux0.11所在环境。

本章重点在于了解,而不是纠结在 cpu 上。操作系统的核心不在这里。

1.1 计算机上电

计算机上电后进行了如下过程:

  1. 在计算机开机时, 内存中没有其他程序,只有 bios 可执行(BISO是一段固化在内存中的程序,存放在 ROM 中)。CPU 处于实模式状态,即寻址方式和8086一样,寻址范围只有1M。CPU 设置 CS = 0xFFFF; IP = 0x0000,即让 PC 指针指向 ROM BIOS 映射区(计算机初始化过程中会将BIOS代码复制到ROM BIOS 映射区)。CPU 首先从 ROM BIOS 映射区的程序开始执行。
  1. CPU 执行 ROM BIOS 映射区的程序,该程序主要负责检测系统硬件是否正常,并建立中断向量表(这只是供操作系统启动时使用,在操作系统建立完成后会将它覆盖清理,并建立新的中断向量表)
  2. 在 ROM BIOS 映射区的程序执行的最后,该程序会将操作系统启动程序(bootsect.s)从磁盘第1个扇区(0磁头、0柱面、第1扇区)复制到内存 0X07C00开始的位置,并设置 CS = 0x07c0, IP = 0x0000。最后 CPU 转移到 bootsect.s(0X07C00处)开始执行。至此CUP才正式开始执行“我们自己编写的程序”。

在bios执行结束后,计算机内存中的内容如下图所示:

启动与开始的区别,什么叫做启动(1)

在计算机启动前,操作系统的程序已经存放在了磁盘之中,Linux0.11内核在磁盘中的分布情况如下图所示:

启动与开始的区别,什么叫做启动(2)

图中的 system 模块 也就是 Linux0.11 内核的其他部分,如:head.s、main.c 等等。当时的磁盘结构主要是通过磁头数(Heads), 柱面数(Cylinders), 扇区数(sectors)三个参数读写磁盘信息。其中:

1.2 执行bootsect.s

bootsect.s是操作系统的引导程序,是操作系统执行的第一个程序。bootsect.s主要进行了以下工作:

1、 将自己(bootsect.s)搬移到内存0X90000 开始的位置,然后跳转至0X90000 go 处执行程序,bootsect.s 的大小不会超过1个扇区,磁盘的 0扇区只用于存放 bootsect.s。(bootsect.s 的第46至第57行)

_start: !第46行 mov ax,#BOOTSEG mov ds,ax mov ax,#INITSEG mov es,ax mov cx,#256 sub si,si sub di,di rep movw !将自己搬到0X90000处 jmpi go,INITSEG !跳转至0X90000 go 处执行程序 go: mov ax,cs

2、 利用 BIOS 中断 (int 13) 将 setup.s 从磁盘加载到内存0X90200开始的位置,可以看出加载至内存后setup.s依旧紧跟在bootsect.s之后。(bootsect.s第67至第73行)

load_setup: !第67行 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

3、 获取磁盘的信息,这里不是很重要(bootsect.s第83至第85行)

mov dl,#0x00 !第83行 mov ax,#0x0800 ! AH=8 is get drive parameters int 0x13

4、检测我们要使用哪一个根文件系统设备(bootsect.s第117至第120行)。如果已经指定了根文件系统所在的设备,那么就直接使用给定的设备。

seg cs !第117行 mov ax,root_dev ! root_dev 在第508、509字节处被定义(在bootsect.s的第250行),其值为0x306,说明根文件系统在第2个磁盘的第1个分区。 cmp ax,#0 ! 若root_dev 不为0,则认为根文件系统所在的设备号已经被定义。 jne root_defined

5、在屏幕上打印"Loading system …"。(bootsect.s第98至第102行)

mov cx,#24 !第98行 mov bx,#0x0007 ! page 0, attribute 7 (normal) mov bp,#msg1 mov ax,#0x1301 ! write string, move cursor int 0x10

6、将 system 模块的代码从磁盘搬到内存0x10000开始的位置。SYSSIZE = 0x3000 is 0x30000 bytes = 196kB 。Linux0.11中默认内核大小不会超过196KB。

mov ax,#SYSSEG !第107行 mov es,ax ! segment of 0x010000 call read_it ... read_it: ... read_track: ... mov dx,head !head.s是system模块的第一个程序 mov dh,dl mov dl,#0 and dx,#0x0100 mov ah,#2 int 0x13 !这才是正式开始搬运system模块。 ...

7、 跳转到0x90200处执行,转移到 setup.s 去执行。

!SETUPSEG = 0x9020 jmpi 0,SETUPSEG !第139行1.3 执行setup.s

setup.s主要进行了以下工作:

  1. 利用BIOS的中断获取计算机参数(内存大小,磁盘参数、显示器参数等),并将参数存入0X90000开始的位置(将bootsect.s覆盖掉)。我们可以先不管这些参数,等后面用到了再看。关于 setup.s 程序具体读取的系统硬件参数,及其这些参数的存放位置请参考《Linux内核完全剖析——基于0.12内核》。
  2. 将system模块从内存0x10000 处移到物理内存起始位置 0X0000 0000。这意味着原来的 BIOS 中断向量表被覆盖了。

do_move: !第114行 mov es,ax ! destination segment add ax,#0x1000 cmp ax,#0x9000 jz end_move mov ds,ax ! source segment sub di,di sub si,si mov cx,#0x8000 rep movsw jmp do_move

  1. 设置GDT和idt。这只是个临时表,之后会重新设置gdt和idt,这里是为了方便cpu进入保护模式时能正确寻址(这里主要是为了保证能跳到system模块执行)。设置8259芯片,重新设置中断向量表。

lidt idt_48 ! load idt with 0,0 !第132行 lgdt gdt_48 ! load gdt with whatever appropriate

  1. CPU进入32位的保护模式,程序跳转至物理内存起始位置开始执行(head.s处)

mov ax,#0x0001 ! protected mode (PE) bit !第189行 lmsw ax ! This is it! jmpi 0,8 ! jmp offset 0 of segment 8 (cs)1.4 执行head.s

head.s 里面的内容有点复杂,还没完全弄懂,不过影响不大,现在只需要了解head.s的主要工作即可。
1、设置段选择器。将ds、es、fs等寄存器都赋值为0x10。这里的0x10表示:特权级为0、使用GDT的第2项即数据段描述符。

.globl idt,gdt,pg_dir,tmp_floppy_area #第15行 pg_dir: # 页目录存放的起始位置 .globl startup_32 startup_32: movl $0x10,?x mov %ax,%ds mov %ax,%es mov %ax,%fs mov %ax,%gs lss stack_start,%esp # 设置堆栈的位置,当然这只是个暂时的,后面还会修改。 # stack_start 定义在 kernel/sched.c 文件中

2、 设置 IDT 和 GDT。

call setup_idt #第 25 行 call setup_gdt

其中 IDT 共有256项且全部填充为 ignore_int 函数的偏移地址,ignore_int是一个只报错误的哑中断子函数。而 GDT 作成如下样子,可以看出这时候还没有设置 LDT :

gdt: .quad 0x0000000000000000 /* NULL descriptor 第0项不使用*/ /*第 236 行*/ .quad 0x00c09a0000000fff /* 16Mb 代码段描述符,其中代码段基地址为0,段的长度为16MB*/ .quad 0x00c0920000000fff /* 16Mb 数据段描述符,其中数据段基地址为0,段的长度为16MB*/ .quad 0x0000000000000000 /* TEMPORARY - don't use 保留,没有使用*/ .fill 252,8,0 /* space for LDT's and TSS's etc */

3、 检测A20线是否开启,检测数字协处理器等。

4、 开启分页机制,设置页目录等。setup_paging 函数就是设置分页机制的。页表从地址 0x0 的位置开始存放,至于页表被设置成什么样子,这里可以先不管,等到学习内存管理章节的时候再回过来看就好。head.s在最后控制程序进入了main函数。程序是利用setup_paging 中的 ret 指令(出栈)进入main函数的。

after_page_tables: # 第137行,在设置好页表后进入main函数。 pushl $0 # These are the parameters to main :-) pushl $0 pushl $0 pushl $L6 # return address for main, if it decides to. pushl $main # 先将main压栈,再利用 setup_paging 里面的 ret 进入 main 函数。 jmp setup_paging # jmp不会进行压栈操作,这里要和call区别一下。 L6: jmp L6 # main should never return here, but # just in case, we know what happens.

进入 main 函数之后会进行一堆初始化,主要是要建立起一些重要的数据结构。
在head.s执行完后内存的内容如下:

启动与开始的区别,什么叫做启动(3)

1.5 总结

栏目热文

文档排行

本站推荐

Copyright © 2018 - 2021 www.yd166.com., All Rights Reserved.