您的位置:首页 > 运维架构 > Linux

Linux内核学习笔记之系统启动(一)

2014-02-24 21:23 330 查看
我们要运行程序,必须先启动操作系统,但是刚开机的时候又是谁运行了操作系统呢?开机的那刻究竟发生了什么?接下来让我们一起来揭开操作系统启动过程的神秘面纱~

我先引用《Linux内核注释》的一段原话来让大家对开机后发生的情况有段简要的认识,然后结合代码来详细描述这个过程:

Linux 的最最前面部分是用 8086 汇编语言编写的(boot/bootsect.s),它将由 BIOS 读入到内存绝对地址 0x7C00(31KB)处,当它被执行时就会把自己移到绝对地址 0x90000(576KB)处,并把启动设备中后2kB 字节代码(boot/setup.s)读入到内存
0x90200 处,而内核的其它部分(system 模块)则被读入到从地址 0x10000 开始处,因为当时 system 模块的长度不会超过 0x80000 字节大小(即 512KB),所以它不会覆盖在 0x90000 处开始的 bootsect 和 setup 模块。随后将 system 模块移动到内存起始处,这样 system模块中代码的地址也即等于实际的物理地址。便于对内核代码和数据的操作。图
3.1 清晰地显示出 Linux系统启动时这几个程序或模块在内存中的动态位置。其中,每一竖条框代表某一时刻内存中各程序的映像位置图。在系统加载期间将显示信息"Loading..."。然后控制权将传递给 boot/setup.s 中的代码,这是另一个实模式汇编语言程序。



下面我将针对上图的1~6号步骤结合Linux代码进行详解,这节都是汇编,所以讲解起来可能很枯燥,如果你只是想知道这个时候操作系统做了什么,可以不必详读每步的内容,只需看标签上的总结和上面的图3.1基本上就可以了解了(学习资料传送门

步骤<1>硬件自动加载硬盘第一个扇区内容到内存

开机后谁运行了操作系统?其实开机后第一个运行的不是我们的Linux操作系统,而是主板里面一开始就已经存在的一个微型操作系统—BOIS,这是一个固化在主板的ROM里面,并且是建立在系统硬件基础上的操作系统,提供直接操作硬件的BOIS调用(最初级的系统调用)的操作系统,而它完成了启动Linux操作系统的第一步加载硬盘的第一个扇区内存绝对地址 0x7C00(31KB)处,而这个扇区就是我们Linux操作系统的引导扇区,里面保存了如何将Linux操作系统载入内存的指令(这些指令保存文件Boot.s中,0.11的内核bootsect.s和setup.s是不区分的)。这一切都是硬件的设定步骤,所以第一步是由硬件自动完成的。

为什么不直接让BOIS加载操作系统?由于BOIS只会自动加载硬盘的第一个扇区的512字节的内容,而操作系统的大小远远大于这个值,所以才会先加载操作系统自己的加载程序(这个可以很小),然后通过操作系统的加载程序加载操作系统(SYSTEM模块)到内存中。

步骤<2>将引导扇区内容移到0x90000

[cpp] view
plaincopy





.text

BOOTSEG = 0x07c0

INITSEG = 0x9000

SYSSEG = 0x1000 | system loaded at 0x10000 (65536).

ENDSEG = SYSSEG + SYSSIZE

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

这段汇编不是很难,应该不难看出,就是将ds(0x07c0):si指向的内容赋值给es(0x9000):di,一共赋值256字(MOVW)即512字节,然后跳到go这个标签地方,并且指明代码段为CS=INITSEG,即下条指令从INITSEG(0x9000)+offset go开始执行(段内跳转是跳转偏移不是绝对地址),执行的指令就等于现在的go标签后的内容(步骤3)。之所以要移动是因为0x10000-0x90000(实地址模式下地址是等于段寄存器值左移4位加上段内偏移的,所以这边是4个0不是前面段寄存器里面的3个了)等下要放操作系统的内核代码

步骤<3>完成新的段寄存器设置以及打印系统加载提示字符(注意0.11内核和0.12不同,没有setup.s)

[plain] view
plaincopy





go: mov ax,cs

mov ds,ax

mov es,ax

mov ss,ax

mov sp,#0x400 | arbitrary value >>512

;这边AH=0x03是BIOS调用读取光标位置,返回值CH=光标起始行 DH,DL=行,列

mov ah,#0x03 | read cursor pos

xor bh,bh

int 0x10

;这边AH=0x13是BIOS调用显示字符串,具体调用参数可以百度

mov cx,#24

mov bx,#0x0007 | page 0, attribute 7 (normal)

mov bp,#msg1

mov ax,#0x1301 | write string, move cursor

int 0x10

这段主要是通过BOIS调用(这个时候操作系统还未启动,所有系统功能都是通过调用BOIS中断完成INT 0x10)完成打印"Loading system ..."字符串

步骤<4>加载真正的Linux操作系统到内存

[cpp] view
plaincopy





mov ax,#SYSSEG

mov es,ax | segment of 0x010000

call read_it

call kill_motor

完成加载SYSTEM模块到0x10000的工作,关闭软驱马达用于读取其静态参数,有关read_it的流程图我已上传到本节学习资料

步骤<5>把Linux操作系统内核从0x10000移动到0x00000,在设置全局描述符后开启保护模式,并跳到0x0处执行操作系统内核指令

[cpp] view
plaincopy





mov ah,#0x03 | read cursor pos

xor bh,bh

int 0x10 | save it in known place, con_init fetches

mov [510],dx | it from 0x90510.

cli | no interrupts allowed !

把当前光标位置保存到0x90510处,以后会用到,然后关闭中断,准备移动内核

[cpp] view
plaincopy





mov ax,#0x0000

cld | 'direction'=0, movs moves forward

do_move:

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

j do_move

这边就是把内核从0x90000移动到0x00000过程了,汇编很简单,movsw表示一次拷贝一个字,每次循环0x8000次,就是0x10000字节,如果你熟悉实地址模式的汇编,就应该知道这是一个段的最大长度,即每次拷贝一个段;ds:di=0x0:di能表示的范围0x0~0x10000-1,ds:di=0x1000:di能表示的范围是0x10000~0x20000-1,所以上面每次为段寄存器add ax,0x1000其实是指向下个段,即下一个0x10000字节

[cpp] view
plaincopy





end_move:

mov ax,cs | right, forgot this at first. didn't work :-)

mov ds,ax

lidt idt_48 | load idt with 0,0

lgdt gdt_48 | load gdt with whatever appropriate

| that was painless, now we enable A20

call empty_8042

mov al,#0xD1 | command write

out #0x64,al

call empty_8042

mov al,#0xDF | A20 on

out #0x60,al

call empty_8042

这边设置中断描述符表和全局描述符表(有关全局描述符的内容下节详细介绍,这两张表只有在保护模式下才有用),并开启A20信号线,最初的CPU只能使用20根地址线来寻址,后面CPU的地址线增加了,能寻址更多的范围,但是为了保持向下兼容,所以设置了A20开关,当关闭的时候20比特以上的地址都被清除

[cpp] view
plaincopy





mov al,#0x11 | initialization sequence

out #0x20,al | send it to 8259A-1

.word 0x00eb,0x00eb | jmp $+2, jmp $+2

out #0xA0,al | and to 8259A-2

.word 0x00eb,0x00eb

mov al,#0x20 | start of hardware int's (0x20)

out #0x21,al

.word 0x00eb,0x00eb

mov al,#0x28 | start of hardware int's 2 (0x28)

out #0xA1,al

.word 0x00eb,0x00eb

mov al,#0x04 | 8259-1 is master

out #0x21,al

.word 0x00eb,0x00eb

mov al,#0x02 | 8259-2 is slave

out #0xA1,al

.word 0x00eb,0x00eb

mov al,#0x01 | 8086 mode for both

out #0x21,al

.word 0x00eb,0x00eb

out #0xA1,al

.word 0x00eb,0x00eb

mov al,#0xFF | mask off all interrupts for now

out #0x21,al

.word 0x00eb,0x00eb

out #0xA1,al

这边主要是对8259A中断控制器的编程,具体内容可以参见文献或者百度,目前我对研究这个没有太大兴趣,而且这个不影响我们理解操作系统

[cpp] view
plaincopy





mov ax,#0x0001 | protected mode (PE) bit

lmsw ax | This is it!

jmpi 0,8 | jmp offset 0 of segment 8 (cs)

gdt:

;gdt[0]

.word 0,0,0,0 | dummy

;gdt[1]

.word 0x07FF | 8Mb - limit=2047 (2048*4096=8Mb)

.word 0x0000 | base address=0

.word 0x9A00 | code read/exec

.word 0x00C0 | granularity=4096, 386

;gdt[2]

.word 0x07FF | 8Mb - limit=2047 (2048*4096=8Mb)

.word 0x0000 | base address=0

.word 0x9200 | data read/write

.word 0x00C0 | granularity=4096, 386

开启保护模式,并加载状态字,然后取出gdt[1](CS=0000 0000 0000 01000b,高13位为选择子,即gdt数组下标)对应表项的段基地址0,加上偏移0,即跳转到内存地址0:0这个位置开始执行指令,由于SYSTEM模块被移动到内存地址0x0处,所以这边就是要开始执行操作系统的第一条指令了这边简单介绍下实地址模式和保护模式,在实地址模式下一个逻辑地址cs:xx/ds:xx对应的物理地址为cs<<4+xx/ds<<4+xx,而保护模式下,cs/ds变成了选择子,他们只是一个索引,用于指示对应全局描述符表中对应表项(全局描述符表类似数组,选择子类似数组下标,这边还未开启分页模式,所以逻辑地址通过段映射得到的线性地址就是物理地址),全局描述符表具体内容在下节介绍

原来开机后就是做这些事情啊,是不是很激动自己终于揭开了操作系统的第一层面纱了?感觉自己一下子学到了很多,但是操作系统依然神秘?当然,现在都还没见到操作系统的核心~今天的内容只能算是开胃菜,下节内容更精彩~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: