Linux 内核启动过程分析

 

0 引言
从嵌入式系统到超级服务站, Linux 已获得广泛的应用。 Linux 是一个完整通用的 Unix 类分布式操作系统,它的结构紧凑、功能强、效率高、可移植性好且在 Internet 上可自由取用。
Linux Unix 操作系统一样,操作系统的主要功能集中在内核,内核中包含进程管理、文件管理、设备管理和网络管理等部分。本文深入探讨 Linux 操作系统内核的启动过程,希望对 Linux 学习和使用者有所启迪。
1 内核结构及平台相关性
本文分析的内核版本为 2.6.9 。当我们使用 tar 命令将 linux-2.6.9.tar.bz2 解开时,内核源代码被放到了 linux-2.6.9/ 目录中。 Linux 内核各功能文件分别存放在 linux-2.6.9/ 目录下的相应子目录中。
Linux 操作系统可以工作在多种不同硬件平台上,如 80x86CPU 系列 (80386 以上 ) SUN sparc64 arm26 等。为了让 Linux 体现优良的可移植性, Linux 内核代码针对不同的硬件平台包含有对应的启动和初始化程序。这些程序处于 arch/ 子目录中。用户完全可以根据自己的需要,从内核代码中各取所需,即时编译和更换系统内核,这也是 Linux 操作系统获得世界各地网络爱好者普遍支持的主要原因。鉴于绝大部分 Linux 应用于 Intel 80x86 系列平台,所以本文也仅限对 Linux 80x86 系列平台的启动过程进行分析。本文所要探讨的启动程序位于 arch/i386/boot/ 目录,系统的启动过程主要由 bootsect.s setup.s head.s 3 个汇编程序完成。
2 系统启动流程
PC 的电源打开后, 80x86 结构的 CPU 将自动进入实模式,并从地址 0XFFFF0 开始自动执行程序代码,这个地址通常是 ROM-BIOS 中的地址。 PC 机的 BIOS 将执行某些系统的检测,如测试内存等,并在物理地址 0 处开始初始化中断向量。此后,它将可启动设备的第 1 个扇区 ( 磁盘引导扇区, 512 字节 ) 读入内存绝对地址 0x7C00 处,并把控制权交给这段代码。 Linux 的这段代码 (boot/bootsect.s) 是用 8086 汇编语言编写,它首先把自己举到内存地址 0x90000 ,并跳转到这里执行。然后把启动设备中后 2KB 代码 (boot/setup.s) 读入到内存 0x90200 处,而内核的其它部分 (system 模块 ) 则被读入到从地址 0x10000 开始处。随后 setup.s 将把 system 模块移动到内存起始处,这样 system 模块中代码的地址就等于实际的物理地址,便于对内核代码的操作。
Linux 内核编译后生成的内核映像 (system 模块 ) 最大只能为 512 KB ,超出这个大小将无法装入。这虽可以满足一般内核要求,但在特殊情况下内核也会大于 512 K ,这时可用两种办法:①把内核映像压缩 (ZIP 格式 ) ,系统进入操作模式后再展开;②把某些非基本的具有独立功能的部分编译成模块,在系统启动后或启动过程中插入。待系统模块 (system) 头部运行结束,所有 32 位运行方式的设置启动都完成, IDT GDT 以及 LDT 被加载,处理器和协处理器被确认,分页工作也设置好了。最终调用 init/main.c 中的 main() 程序进入系统的初始化过程,至此系统获得正常启动。
3 启动过程分析
主要对 bootsect.s setup.s head.s 的工作机理做了较为详细的阐述。
3.1 bootsect 模块分析
bootsect.s 代码是磁盘引导块程序,驻留在引导盘的引导扇区 (0 磁道, 0 磁头,第 1 扇区 ) 。在 PC 加电 ROM BIOS 自检后, bootsect.s BIOS 自动加载到内存 0x7C00 处,然后将自己移到内存 0x90000 处。在 bootsect.s 的移动过程,其中 #BOOTSEG 0x7C00 #INITSEG 0x9000 。接下来,程序利用 BIOS 中断, INT 0x13 setup 模块从磁盘第 2 个扇区开始读到 0x90200 开始处,共读 4 个扇区。如果读出错误,则 CF 标志置位,程序复位驱动器,并重试。加载 setup 模块后,程序利用中断取磁盘驱动器参数,并将中断返回的每磁道扇区数保存在变量 sectors 中。然后程序将 system 模块加载到内存 0x10000 处。加载 system 模块期间,显示“ Loading system ? ”信息。为了提高加载速度,只要可能,就每次加载整条磁道的数据。从磁盘读取一次数据后,程序就比较当前所读段是否就
是系统数据末端所处的段 (#ENDSEG) ,如果不是,就跳转至 ok1_read 标号处继续读数据。最后,程序向软驱控制卡的驱动端口 0x3f2 0 ,关闭软驱电动机。程序运行 jmpi 0 SETUPSEG ,跳转到 0x9020:0000 处, CPU 开始执行 setup 模块。 3.2 setup 模块分析
setup.s 首先利用 ROMBIOS 中断读取机器系统参数 ( 光标位置、扩展内存数、硬盘参数表等 ) ,并将这些数据保存到内存 0x90000 开始的位置 ( 覆盖掉了 bootsect 程序 ) 。这些参数将被内核中相关程序使用,例如设备驱动程序集中的 ttyio.c 。随后系统进入保护模式运行。 CPU 在实模式下运行,寻址一个内存地址主要是使用段基址和段内偏移值,段值被存放在段寄存器中;而在保护模式运行方式下,段寄存器中存放的是一个描述表中某项的索引值。索引值指定的描述符项中含有
需要寻址的内存段的基地址、段的最大长度值和段的访问级别等信息。和实模式下的寻址相比,段寄存器值换成了段描述符项索引。接下来,程序关闭中断,将 system 模块整体向内存低端移动 0x1000 。每次移动 0x8000 字,循环执行 8 次。然后程序执行 lidt idt_48 加载中断描述符表 (idt) 寄存器;执行 lgdt gdt_48 加载全局描述符表 (gdt) 寄存器。此时中断描述符表中只有一个空项 ( 值全为 0) 。全局描述符表中有 3 个描述符项:第 1 项无用,但必须存在;第 2 ( 索引值 0x08) 是系统代码段描述符,所定义的段基址为 0 ,段中代码可被读和执行,段长为 8 M ;第 3 ( 索引值 0x10) 是系统数据段描述符,所定义的段的基址为 0 ,段中数据可读和可写,段限长为 8M 。最后,程序重置协处理器,对 8259 中断控制芯片编程,完成进入保护虚地址模式的所有准备工作。通过设置机器状态字 MSW( 0 号控制寄存器 CR0 的低 16 ) 中的 PE 位使 CPU 进入保护模式,开始运行 system 模块中的 head.s( 指令 jmpi 0,8) 。注意, CPU 已在保护模式下运行, CS 8 表示请求特权级 0 ,使用全局描述符表中的第 1 ( 索引值 0x08)
3.3 head 模块分析
全局描述中的第 1 项表明 CPU 开始执行内存基址为 0x0000 处的代码,这里存放的是 system 模块的头部,即为 32 位汇编程序 kernel/head.s head.s 采用的是 AT&T 汇编语言格式,并且需要使用 GNU gas gld 进行编译连接。
程序首先将 %ds %es %fs %gs 置立即数 0x10 ,这里的 0x10 并不是把地址 0x10 装入各个段寄存器,它是全局描述符表中的偏移值。然后程序设置系统新的中断描述符表 (lidt idt_drscr) ,新的 idt 处于 head.s 尾,共 256 ( 每项 8B) ,并都指向 ignore_int 哑中断门,真正实用的中断门以后再安装。
程序重新设置系统全局描述符表 (lgdt gdt_descr) ,新的 GDT head.s 末端,共 256 项。前 4 项分别是空项 ( 不用 ) 、代码段描述符、数据段描述符和系统段描述符,其中系统段描述符 Linux 没有派用处。后面还预留了 252 项的空间,用于放置所创建任务的局部描述符 (LDT) 和对应的任务状态段 TSS 的描述符。
然后程序设置页表并启动分页机制, Linux 386 系列平台使用 2 级页表,即页表目录和页表。从基地址 0x1000 处开始填充页表, 0x0000 处将存放页表目录。当前代码的物理基地址已变为 0x1000( 指令 .org 0x1000) ,程序连续分配 4 个页表,每个页表大小为 4 K 。页表项为 4 个字节,包含页框地址、内存页信息 ( 是否在内存、是否修改过、普通用户还是超级用户等 ) 。程序紧接对 5 页内存 (1 页目录 +4 页页表 ) 清零,开始设置页目录中的项。“ $pg0+7 ”表示 0x00001007 是页目录表中的第 1 项。则第 1 个页表所在的地址为: 0x00001007&0xfffff000 ;第 1 个页表的属性标志为: 0x00001007&00000fff ,表示该页存在,用户可读写。 xor%eax %eax movl %eax %cr3 设置页目录基址寄存器 cr3 Movl%cr0 %eax orl $0x80000000 %eax movl%eax %cr0 启动控制寄存器 cr0 的分页标志 (PG) 位。
至此 386 系列 CPU 的分段机制和分页机制均已启动,内核也置于最终的适当位置,与平台有关的启动过程基本完成。系统内核将进入与平台无关的内核数据结构初始化工作。采用的方法是,在 head.s 中将 main.c 程序的地址压入堆栈。这样,在设置分页处理 (setup_paging) 结束后执行“ ret ”返回指令时就会将 main.c 程序的地址弹出堆栈,并去执行 main.c 程序。 main.c 调用所有设置过程,如为设备申请 I/O 空间、其它页表的初始化、缓冲区初始化、网络初始化和设置多任务等,然后系统转移到用户模式,创建并启动初始进程 init 。这些初始化设置过程都与 Linux 的设备管理、文件管理、网络管理和 CPU 管理和内存管理有关。这样 Linux 操作系统完全启动。
4 结束语
Linux 是当前流行的一种操作系统, Linux 有许多不同的版本,比较流行的有 Red Hat Slackware Debian ,各版本的 Linux 之间的区别不大,其内核都差不多。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值