阅读linux内核源码的过程,是一个将计算机学科的所有理论课程与实际的产品紧密联系起来的过程。阅读类UNIX的源码,在国外操作系统课程的教学过程中,曾经伴随了一代计算机人才的培养。遗憾的是,我学了几年计算机专业的课程,直到最近才感觉有了一定的积累,进而阅读了linux内核的源码,切实感受到了作为计算机科学与技术精髓的操作系统的魅力所在。
1.Linux和linux内核
很多linux的用户一直争论和比较各种linux发行版的易用性,但是大家都认同一点:linux,通常是指linux内核,而且尽管软件包管理方式不同、图形界面不同以及上层应用软件也相应不同,但却都具有相同的内核--linux内核。内核是一个操作系统最核心的部分,相当于汽车发动机、PC机处理器在各自系统中的核心地位。
由于我内核的阅读才刚刚起步。所以仅仅针采用x86处理器的PC机器的加电启动直到linux内核程序被成功加载的过程做一点源码阅读级别的阐述。最新的内核是2.6版本的,2.6内核与上一个版本2.4内核的差异还是较大的,这不仅仅表现在源码的组织上,更在于新内核所采用的诸多新的高性能算法上。
2.4内核的资料在网上众多,成为我读懂内核源码的最为重要的参考。试图通过寻找内核开发组织的开发者邮件列表以求了解2.4和2.6差异的过程也是艰辛的。时间所限,我并未采取这一方法,而是采取了阅读2.4资料,对比2.6新版源码的策略。
在搜索并阅读相关资料以求搞清楚linux内核到底是如何运转起来的过程中,我决定借这次课程论文的机会,求同存异,对2.6版本的内核做一点阐述。
linux内核相当复杂。且不说作为操作系统的内核这样的程序,编写源码需要各类技巧,仅仅是所涉及到的知识,分门别类的就有许多许多。可见,内核的阅读是个长期和反复的过程。也正是如此,一方面,linux牵涉到了计算机领域几乎所有的知识及其实际运用的过程,内核的阅读彰显着迷人的魅力;另一方面,linux内核在操作系统领域的应用如此广泛,以至于广大的嵌入式系统开发者都选择了在某种程度上深入学习linux内核。
2. X86 IBM PC机的启动过程
由于准备写这篇文章的时候,最新的稳定版的内核是2.6.35.4,鉴于学习要面向新技术的策略,源码采用了这个最新的版本。
我对linux内核上层软件所运行的intel x86处理器相关的内存管理、中断处理的过程并不太精通,这也限制了我目前对源码的理解。当然,最为重要的是,是因为机器启动和内核启动的过程中,大量的代码都是纯AT&T汇编(.S文件)写成的,尽管汇编都是直来直去,但面向实际运用的汇编程序也确实是一个难关,何况要想深入理解,还需要大量的intel x86处理器的知识。
一般说来,运行linux 内核的PC计算机的启动过程是这样的:
power on -> bios -> bootloader--->kernel boot --->系统初始化(main.c init.c)--->运行应用程序
3. Linux内核启动协议
阅读文档\linux-2.6.35\Documentation\x86\boot.txt
现代支持bzImage的内核的内存布局
程序段地址是由grub的大小来决定的。地址x应该在bootloader所允许的范围内,尽可能的低。
启动过程的内存地址分配情况:
| 保护模式的内核 |
100000 +------------------------+
| I/O memory hole |
0A0000 +------------------------+
| 为 BIOS保留 | Leave as much as possible unused
~ ~
| 命令行 | (Can also be below the X+10000 mark)
X+10000 +------------------------+
| 栈/堆 | 栈/堆空间是给内核实模式代码用的
X+08000 +------------------------+
| Kernel setup | setup代码运行在内核的实模式下
| Kernel boot 扇区 | The kernel legacy boot sector.
X +------------------------+
| Boot loader |
001000 +------------------------+
| Reserved for MBR/BIOS |
000800 +------------------------+
| Typically used by MBR |
000600 +------------------------+
| BIOS use only |
000000 +------------------------+
传统上支持image和zimage的内存布局如下(2.4内核就是这样的布局):
| |
0A0000 +------------------------+
| Reserved for BIOS | Do not use. Reserved for BIOS EBDA.
09A000 +------------------------+
| Command line |
| Stack/heap | For use by the kernel real-mode code.
098000 +------------------------+
| Kernel setup | The kernel real-mode code.
090200 +------------------------+ | Kernel boot sector
| The kernel legacy boot sector.
090000 +------------------------+
| Protected-mode kernel | The bulk of the kernel image.
010000 +------------------------+
| Boot loader | <- Boot sector entry point 0000:7C00
001000 +------------------------+
| Reserved for MBR/BIOS |
000800 +------------------------+
| Typically used by MBR |
000600 +------------------------+
| BIOS use only |
000000 +------------------------+
4. BIOS POST
对于BIOS,主要由两家制造商制造,驻留在主板的ROM里。有了BIOS,硬件制造商可以只需要关注硬件而不需要关注软件。BIOS的服务程序,是通过调用中断服务程序来实现的。BIOS加载bootloader程序,Bootloader也可以通过BIOS提供的中断,向BIOS获取系统的信息。
1)电源启动时钟发生器并在总线上产生一个#POWERGOOD的中断。
2)产生CPU的RESET中断(此时CPU处于8086工作模式)。
3) %ds=%es=%fs=%gs=%ss=0,%cs=0xFFFF0000,
%eip = 0x0000FFF0 (ROM BIOS POST code).
(指令指针eip,数据段寄存器ds,代码段寄存器cs)
4)在中断无效状态下执行所有POST检查。
5)在地址0初始化中断向量表IVT。
6) 0x19中断以启动设备号为参数调用BIOS启动装载程序。这个程序从启动设备 (硬盘)的0扇面1扇区读取数据到内存物理地址0x7C00开始装载。
就是说,CPU 在 BIOS的入口(CS:IP=FFFF:0000)处执行BIOS的汇编程序,BIOS程序功能有系统硬件的检测,提供中断访问接口以访问硬件。而后被BIOS程序通过中断0x19调用磁盘MBR上的bootloader程序,将bootloader程序加载到ox7c00处,而后跳转到0x7c00,这样,位于0x7c00处的bootloader程序,就可以执行了。
从BIOS执行MBR中的程序开始,就是linux的代码在做的事情了。
5. BOOTLOADER
bootloader程序是为计算机加载(load)计算机操作系统的。boot(引导)是bootstrap的简写,bootstrap是引导指令的意思。
传统意义上,由于CPU加电之后,CPU只能访问ROM或者RAM里的数据,而这个时候是没有计算机操作系统的,所以需要有一段程序能够完成加载存储在非易失性存储介质(比如硬盘)上的操作系统到RAM中的功能。这段程序存储在ROM里,BIOS就是这类程序中的一种。
还有一种bootloader程序,位于硬盘上,被BIOS调用,用于加载内核。这样的bootloader程序,在PC机上主要有grub、lilo、syslinux等。
GRUB(GRand Unified Bootloader)是当前linux诸多发行版本默认的引导程序。嵌入式系统上,最常见的bootloader是U-BOOT。
这样的bootloader一般位于MBR的最前部。在linux系统中,bootloader也可以写入文件系统所在分区中。比如,grub程序就非常强大。Gurb运行后,将初始化设置内核运行所需的环境。然后加载内核镜像。
grub磁盘引导全过程
stage1 :
grub读取磁盘第一个512字节(硬盘的 0 道 0 面 1 扇区)被称为MBR(主引导记录),也称为bootsect)。MBR由一部分bootloader的引导代码、分区表和魔数三部分组成。
stage1_5 :
识别各种不同的文件系统格式。这使得grub识别到文件系统。
stage2 :
加载系统引导菜单,加载vmlinuz和initrd
6. 内核加载过程
启动过程是和体系结构相关的,对于x86体系结构,对于2.6的内核,可以分为以下过程:
1) BOIS选择启动设备。
2)从启动设备装载bootsector,。
3)Bootsector中的grub程序装载bzImage(包含有setup、解压缩程序和内核映像)。
4)在保护模式下解压内核。
5)汇编代码执行低级初始化(主要是对硬件如CPU和内存的初始化)。
6)执行上层C语言的初始化。
bootsect.o setup.o 解压缩程序misc.o以及内核镜像vmlinuz被压缩成bzImage文件.linux 2.6中,bootsect.S和setup.S被整合为header.S。
注 :ELF可重定位object文件(.o) 静态链接库文件.a),这是和启动密切相关的两个文件。因为.o和.a文件被链接成为可执行文件vmlinux。
bzImage的文件构成图如下:
vmlinuz构成:
1. 第一个512字节 (以前是在arch/i386/boot/bootsect.S)
2. 第二个一段代码,若干不多个512字节 (以前是在arch/i386/boot/setup.S)
3. 保护模式下的内核代码(在\arch\x86\boot\main.c)
Vmlinuz文件
vmlinux是采用linux所支持的可执行文件格式的包含有linux内核的静态链接的可执行文件,传统上,vmlinux被称为可引导的内核镜像。vmlinuz是vmlinux的压缩文件。
bzImage文件
使用make bzImage 命令编译内核源代码,可以得到采用zlib算法压缩的zImage文件,即big zImage文件。老的zImage解压缩内核到低端内存,bzImage解压缩内核到高端内存(1M(0x100000)以上),在保护模式下执行。
bzImage文件包含有bootsect.o + setup.o + misc.o + piggy.o.
Initrd文件
initrd是initialized ram disk 的意思。主要用于加载硬件驱动模块,辅助内核的启动。
header.S
D:\linux-2.6.35\arch\x86\boot\header.S 部分代码:
第一部分定义了3个节,
.bstext,
.bsdata,
.header,
这3个节共同构成了vmlinuz的第一个512字节
BOOTSEG = 0x07C0
SYSSEG = 0x1000
ljmp $BOOTSEG, $start2
start2:
movw %cs, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
xorw %sp, %sp
sti
cld
movw $bugger_off_msg, %si
bugger_off_msg:
.ascii "Direct booting from floppy is no longer supported.\r\n"
.ascii "Please use a boot loader program instead.\r\n"
.ascii "\n"
.ascii "Remove disk and press any key to reboot . . .\r\n"
.byte 0
下面设置内核的属性,setup的过程需要。这些属性是:
.section ".header", "a"
.globl hdr
hdr:
setup_sects: .byte 0 /* Filled in by build.c */
root_flags: .word ROOT_RDONLY
syssize: .long 0 /* Filled in by build.c */
ram_size: .word 0 /* 已经不用了 */
vid_mode: .word SVGA_MODE
root_dev: .word 0 /* Filled in by build.c */
boot_flag: .word 0xAA55
header.S的第二部分,作用如同以前的setup.S
略。
最后,
# Jump to C code (should not return)
calll main
调用main.c
head_32.S
arch/x86/boot/compressed/head_32.S 是汇编写成的32位启动代码。
其前身是linux/boot/head.S文件。
startup发生在在绝对地址0x00001000处的。
head_32.s调用misc.c中的decompress_kernel()函数,将内核vmlinuz解压到0x100000处。
head_32.S 部分源码
/*
* Do the decompression, and jump to the new kernel..
*/
leal z_extract_offset_negative(%ebx), %ebp
/* push arguments for decompress_kernel: */
pushl %ebp /* output address */
pushl $z_input_len /* input_len */
leal input_data(%ebx), %eax
pushl %eax /* input_data */
leal boot_heap(%ebx), %eax
pushl %eax /* heap area */
pushl %esi /* real mode pointer */
call decompress_kernel
addl $20, %esp
零号页面也就是这个
/* The so-called "zeropage" */
struct boot_params {
struct screen_info screen_info; /* 0x000 */
struct apm_bios_info apm_bios_info; /* 0x040 */
__u8 _pad2[4]; /* 0x054 */
__u64 tboot_addr; /* 0x058 */
struct ist_info ist_info; /* 0x060 */
__u8 _pad3[16]; /* 0x070 */
__u8 hd0_info[16]; /* obsolete! */ /* 0x080 */
__u8 hd1_info[16]; /* obsolete! */ /* 0x090 */
struct sys_desc_table sys_desc_table; /* 0x0a0 */
__u8 _pad4[144]; /* 0x0b0 */
struct edid_info edid_info; /* 0x140 */
struct efi_info efi_info; /* 0x1c0 */
__u32 alt_mem_k; /* 0x1e0 */
__u32 scratch; /* Scratch field! */ /* 0x1e4 */
__u8 e820_entries; /* 0x1e8 */
__u8 eddbuf_entries; /* 0x1e9 */
__u8 edd_mbr_sig_buf_entries; /* 0x1ea */
__u8 _pad6[6]; /* 0x1eb */
struct setup_header hdr; /* setup header */ /* 0x1f1 */
__u8 _pad7[0x290-0x1f1-sizeof(struct setup_header)];
__u32 edd_mbr_sig_buffer[EDD_MBR_SIG_MAX]; /* 0x290 */
struct e820entry e820_map[E820MAX]; /* 0x2d0 */
__u8 _pad8[48]; /* 0xcd0 */
struct edd_info eddbuf[EDDMAXNR]; /* 0xd00 */
__u8 _pad9[276]; /* 0xeec */
} __attribute__((packed));
enum {
X86_SUBARCH_PC = 0,
X86_SUBARCH_LGUEST,
X86_SUBARCH_XEN,
X86_SUBARCH_MRST,
X86_NR_SUBARCHS,
};
Misc.c部分源码
D:\linux-2.6.35.4\arch\x86\boot\compressed\misc.c
做字符串显示用的scroll和putstr好难懂啊:
static void scroll(void) {
int i;
memcpy(vidmem, vidmem + cols * 2, (lines - 1) * cols * 2);
for (i = (lines - 1) * cols * 2; i < lines * cols * 2; i += 2)
vidmem[i] = ' ';
}
static void __putstr(int error, const char *s)
{
int x, y, pos;
char c;
#ifndef CONFIG_X86_VERBOSE_BOOTUP
if (!error)
return;
#endif
if (real_mode->screen_info.orig_video_mode == 0 &&
lines == 0 && cols == 0)
return;
x = real_mode->screen_info.orig_x;
y = real_mode->screen_info.orig_y;
while ((c = *s++) != '\0') {
if (c == '\n') {
x = 0;
if (++y >= lines) {
scroll();
y--;
}
} else {
vidmem[(x + cols * y) * 2] = c;
if (++x >= cols) {
x = 0;
if (++y >= lines) {
scroll();
y--;
}
}
}//end else
}//end while
real_mode->screen_info.orig_x = x;
real_mode->screen_info.orig_y = y;
pos = (x + cols * y) * 2; /* Update cursor position */
outb(14, vidport);
outb(0xff & (pos >> 9), vidport+1);
outb(15, vidport);
outb(0xff & (pos >> 1), vidport+1);
}
Misc.c中这段用于解压缩内核的代码,需要具备专业知识才能写出来:
asmlinkage void decompress_kernel(void *rmode, memptr heap,
unsigned char *input_data,
unsigned long input_len,
unsigned char *output)
{
real_mode = rmode;
if (real_mode->hdr.loadflags & QUIET_FLAG)
quiet = 1;
if (real_mode->screen_info.orig_video_mode == 7) {
vidmem = (char *) 0xb0000;
vidport = 0x3b4;
} else {
vidmem = (char *) 0xb8000;
vidport = 0x3d4;
}
lines = real_mode->screen_info.orig_video_lines;
cols = real_mode->screen_info.orig_video_cols;
free_mem_ptr = heap; /* Heap */
free_mem_end_ptr = heap + BOOT_HEAP_SIZE;
if ((unsigned long)output & (MIN_KERNEL_ALIGN - 1))
error("Destination address inappropriately aligned");
#ifdef CONFIG_X86_64
if (heap > 0x3fffffffffffUL)
error("Destination address too large");
#else
if (heap > ((-__PAGE_OFFSET-(512<<20)-1) & 0x7fffffff))
error("Destination address too large");
#endif
#ifndef CONFIG_RELOCATABLE
if ((unsigned long)output != LOAD_PHYSICAL_ADDR)
error("Wrong destination address");
#endif
if (!quiet)
putstr("\nDecompressing Linux... ");
decompress(input_data, input_len, NULL, NULL, output, NULL, error);
parse_elf(output);
if (!quiet)
putstr("done.\nBooting the kernel.\n");
return;
}
而后,进入main.c函数
D:\linux-2.6.35.4\arch\x86\boot\main.c
内核便开始初始化各进程运行的环境,创建进程,加载文件系统。后面的内容在这篇文章中,暂不讨论了。
7.后记
内核的引导和加载过程,普遍采用AT&T汇编代码完成。用于引导内核的bootloader代码,涉及到具体的硬件知识。加载内核的代码涉及到linux各个部分的相关数据结构的初始化设置。由于学习尚未深入,各文件之间的具体调用关系以及具体代码的详细分析有待于今后补充。