现在的位置: 首页 > 综合 > 正文

linux内核学习(10)启动全过程概述之一

2013年09月10日 ⁄ 综合 ⁄ 共 14872字 ⁄ 字号 评论关闭

下面这段时间,我要好好分析一下内核启动过程的源代码,怎么来分析,而且更好的和网友们进行交流,我想,最好的方式莫过于采用赵炯博士编著的《linux
内核完全注释》一书的编写规范。将中文注释夹杂在代码中是最好的方式了吧。我将采用分段注释,以免代码太长导致读了后面的忘记了前面的,在其中会有些重要
的知识点也是我们要好好学习的。

要找到第一个源代码文件不是太困难,它就是始源,注意我们这里全是在x86机器上,内核版本为2.6.36.2。那么arch/x86/boot目录则是我们的入口点,看看里面的Makefile。

setup-y        += a20.o bioscall.o cmdline.o copy.o cpu.o cpucheck.o
setup-y        += early_serial_console.o edd.o header.o main.o mca.o memory.o
setup-y        += pm.o pmjump.o printf.o regs.o string.o tty.o video.o
setup-y        += video-mode.o version.o
setup-$(CONFIG_X86_APM_BOOT) += apm.o

# The link order of the video-*.o modules can matter.  In particular,
# video-vga.o *must* be listed first, followed by video-vesa.o.
# Hardware-specific drivers should follow in the order they should be
# probed, and video-bios.o should typically be last.
setup-y        += video-vga.o
setup-y        += video-vesa.o
setup-y        += video-bios.o

可以看见setup-y目标,就是我们前面那篇说的setup.elf,我们可以发现header.o,可以想到,就是我们要找的始源。它是由header.S汇编文件产生的,顺便说一下,x86的汇编是AT&T,如果不晓得最好自己把这块学习一下。

来自:arch/x86/boot/header.S:
/*
 *    header.S
 *
 *    Copyright (C) 1991, 1992 Linus Torvalds
 *
 *    Based on bootsect.S and setup.S
 *    modified by more people than can be counted
 *
 *    Rewritten as a common file by H. Peter Anvin (Apr 2007)
 *
 * BIG FAT NOTE: We're in real mode using 64k segments.  Therefore segment
 * addresses must be multiplied by 16 to obtain their respective linear
 * addresses. To avoid confusion, linear addresses are written using leading
 * hex while segment addresses are written as segment:offset.
 *
 */

#include <asm/segment.h>
#include <generated/utsrelease.h>
#include <asm/boot.h>
#include <asm/e820.h>
#include <asm/page_types.h>
#include <asm/setup.h>
#include "boot.h"
#include "voffset.h"
#include "zoffset.h"

BOOTSEG        = 0x07C0        /* original address of boot-sector */
SYSSEG        = 0x1000        /* historical load address >> 4 */

#ifndef SVGA_MODE
#define SVGA_MODE ASK_VGA
#endif

#ifndef RAMDISK
#define RAMDISK 0
#endif

#ifndef ROOT_RDONLY
#define ROOT_RDONLY 1
#endif

    .code16
    .section ".bstext", "ax"

    .global bootsect_start
bootsect_start:

    # Normalize the start address
    ljmp    $BOOTSEG, $start2

start2:
    movw    %cs, %ax                     #现在的cs=0x7c00
    movw    %ax, %ds                    #初始化段寄存器
    movw    %ax, %es
    movw    %ax, %ss                    #注意堆栈段为0x7c00
    xorw    %sp, %sp
    sti                                             #开中断
    cld                                            #di,si ++

    movw    $bugger_off_msg, %si                #bugger_off_msg在下面

msg_loop:                                   #打印信息
    lodsb                                       #将ds:si处的字节读入al
    andb    %al, %al
    jz    bs_die                               #如果al==0,则跳转到bs_die
    movb    $0xe, %ah
        movw    $7, %bx
    int    $0x10                              #INT0x10是BIOS视频中断,打印字符。功能号AH
=0x0e
,表示在Teletype模式下显示字符,
                                                    #AL

=字符,BH
=页码,BL
=前景色(
图形模式)

    jmp    msg_loop

bs_die:
    # Allow the user to press a key, then reboot
    xorw    %ax, %ax                
    int    $0x16                            #键盘中断,接收一个字符
    int    $0x19

    # int 0x19 should never return.  In case it does anyway,
    # invoke the BIOS reset code...
    ljmp    $0xf000,$0xfff0         #jump to 0xffff0, 重新设置BIOS

    .section ".bsdata", "a"
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

    # Kernel attributes; used by setup.  This is part 1 of the
    # header, from the old boot sector.

    .section ".header", "a"
    .globl    hdr
hdr:                                         #这个hdr会对应setup_header结构体,待会儿在说
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             /* Obsolete */
vid_mode:    .word SVGA_MODE
root_dev:    .word 0              /* Filled in by build.c */
boot_flag:    .word 0xAA55

这段就是启动扇区,俗称“bootsect”,其实细心的你可能会发现这段代码编译链接后生成的机器码会有512个字节吗,怎么算都没有。这里把犹豫了很长时间呢,还好找到了一个非常重要的文件boot/setup.ld,这是一个链接脚本文件。看一下关键的。

    . = 0;
    .bstext        : { *(.bstext) }
    .bsdata        : { *(.bsdata) }

    . = 497;
    .header        : { *(.header) }

看到.=0、.=497没,算算 .section ".header", "a"下面这些变量其实刚好为512-497=15个字节。现在应该清楚了。

然后我们说说这个启动扇区,启动如果正常,是不会执行的。现在我们从PC加电开始。


PC启动后,x86结构的CPU将自动进入实模式,并从地址0xFFFF0开始自动执行程序代码,这个地址通常是ROM-BIOS中的地址。PC
机的BIOS将执行某些系统的检测,在物理地址0处开始初始化中断向量。然后,将硬盘 MBR 中的 Boot Loader 读到系统的 RAM
中,然后将控制权交给操作系统Boot Loader。Boot Loader将内核映象从硬盘上读到 RAM
中,也即将setup.elf读到0x90000处,还有vmlinux读到x0100000处,将然后跳转到内核的入口点去运行,也即开始启动操作系
统,这个入口点是0x90200处,刚好略过512B启动扇区。如果从软盘启动的话就会直接将启动扇区读到RAM中,然后开始执行的代码就是上面的,于是
会打印出:
Direct booting from floppy is no longer
supported.,也就是bugger_off_msg的第一条信息。现在的内核启动不支持从软盘启动。也就是说真正的开始处在0x90200处。

 

接着arch/x86/boot/header.S:

# offset 512, entry point

    .globl    _start
_start:                                 # 当BootLoader执行完后就会跳到这里执行,也就是所谓的0x90200处
        # Explicitly enter this as bytes, or the assembler
        # tries to generate a 3-byte jump here, which causes
        # everything else to push off to the wrong offset.
        .byte    0xeb        # short (2-byte) jump            
        .byte    start_of_setup-1f         #跳到start_of_setup:
1:

    # Part 2 of the header, from the old setup.S

        .ascii    "HdrS"        # header signature
        .word    0x020a        # header version number (>= 0x0105)
                    # or else old loadlin-1.5 will fail)
        .globl realmode_swtch
realmode_swtch:    .word    0, 0        # default_switch, SETUPSEG
start_sys_seg:    .word    SYSSEG        # obsolete and meaningless, but just
                    # in case something decided to "use" it
        .word    kernel_version-512 # pointing to kernel version string
                    # above section of header is compatible
                    # with loadlin-1.5 (header v1.5). Don't
                    # change it.

type_of_loader:    .byte    0        # 0 means ancient bootloader, newer
                    # bootloaders know to change this.
                    # See Documentation/i386/boot.txt for
                    # assigned ids

# flags, unused bits must be zero (RFU) bit within loadflags
loadflags:
LOADED_HIGH    = 1            # If set, the kernel is loaded high
CAN_USE_HEAP    = 0x80                # If set, the loader also has set
                    # heap_end_ptr to tell how much
                    # space behind setup.S can be used for
                    # heap purposes.
                    # Only the loader knows what is free
        .byte    LOADED_HIGH

setup_move_size: .word  0x8000        # size to move, when setup is not
                    # loaded at 0x90000. We will move setup
                    # to 0x90000 then just before jumping
                    # into the kernel. However, only the
                    # loader knows how much data behind
                    # us also needs to be loaded.

code32_start:                # here loaders can put a different
                    # start address for 32-bit code.
        .long    0x100000    # 0x100000 = default for big kernel

ramdisk_image:    .long    0        # address of loaded ramdisk image
                    # Here the loader puts the 32-bit
                    # address where it loaded the image.
                    # This only will be read by the kernel.

ramdisk_size:    .long    0        # its size in bytes

bootsect_kludge:
        .long    0        # obsolete

heap_end_ptr:    .word    _end+STACK_SIZE-512
                    # (Header version 0x0201 or later)
                    # space from here (exclusive) down to
                    # end of setup code can be used by setup
                    # for local heap purposes.

ext_loader_ver:
        .byte    0        # Extended boot loader version
ext_loader_type:
        .byte    0        # Extended boot loader type

cmd_line_ptr:    .long    0        # (Header version 0x0202 or later)
                    # If nonzero, a 32-bit pointer
                    # to the kernel command line.
                    # The command line should be
                    # located between the start of
                    # setup and the end of low
                    # memory (0xa0000), or it may
                    # get overwritten before it
                    # gets read.  If this field is
                    # used, there is no longer
                    # anything magical about the
                    # 0x90000 segment; the setup
                    # can be located anywhere in
                    # low memory 0x10000 or higher.

ramdisk_max:    .long 0x7fffffff
                    # (Header version 0x0203 or later)
                    # The highest safe address for
                    # the contents of an initrd
                    # The current kernel allows up to 4 GB,
                    # but leave it at 2 GB to avoid
                    # possible bootloader bugs.

kernel_alignment:  .long CONFIG_PHYSICAL_ALIGN    #physical addr alignment
                        #required for protected mode
                        #kernel
#ifdef CONFIG_RELOCATABLE
relocatable_kernel:    .byte 1
#else
relocatable_kernel:    .byte 0
#endif
min_alignment:        .byte MIN_KERNEL_ALIGN_LG2    # minimum alignment
pad3:            .word 0

cmdline_size:   .long   COMMAND_LINE_SIZE-1     #length of the command line,
                                                #added with boot protocol
                                                #version 2.06

hardware_subarch:    .long 0            # subarchitecture, added with 2.07
                        # default to 0 for normal x86 PC

hardware_subarch_data:    .quad 0

payload_offset:        .long ZO_input_data
payload_length:        .long ZO_z_input_len

setup_data:        .quad 0            # 64-bit physical pointer to
                        # single linked list of
                        # struct setup_data

pref_address:        .quad LOAD_PHYSICAL_ADDR    # preferred load addr

#define ZO_INIT_SIZE    (ZO__end - ZO_startup_32 + ZO_z_extract_offset)
#define VO_INIT_SIZE    (VO__end - VO__text)
#if ZO_INIT_SIZE > VO_INIT_SIZE
#define INIT_SIZE ZO_INIT_SIZE
#else
#define INIT_SIZE VO_INIT_SIZE
#endif
init_size:        .long INIT_SIZE        # kernel initialization size

# End of setup header #####################################################

 

上面这些其实就是和那个hdr下标一块的,组成了一个结构,在x86/include/asm/bootparam.h中,struct setup_header。你可以按照结构体顺序对对,非常符合。当然了,它们都有含义的。具体见表--

域名 偏移/大小 协议版本 类型 说明
setup_sect 0x1f1/1 所有 setup代码的大小在512字节扇区中,如果该域为0,则实际值为4,实模式代码由启动扇区(512字节)加上setup代码组成。
root_flags 0x1f2/2 所有 修改(可选) 该值为非0,表示根目录仅读,还可以代替地使用命令行选项"ro"或"rw"设置根目录为仅读或读写。
syssize 0x1f4/4 2.04+ 保护模式代码的尺寸,以16字节段为单位。
ram_size 0x1fa/2 所有 内核内部 不使用 - 为了仅仅给bootsect.S使用。
vid_mode 0x1fa/2 所有   视频模式控制。
root_dev 0x1fc/2 所有 修改(可选) 缺省的root设备,还可替代地在命令行使用命令选项"root="。root设备对于启动来说是/boot所在目录。
boot_flag 0x1fe/2 所有 含有魔数0xAA55,是启动扇区引导结束的标志。
jump 0x200/2 2.00+ 2个字节,为0xEB和跳转偏移字节。x86短跳转指令编码为0xEB。可用来判定内核头的大小。
header 0x202/4 2.00+ 含有魔数值"HdrS"(0x53726448)。如果没在偏移0x202处发现 "HdrS" (0x53726448) 魔数值,启动协议版本是旧的,装载旧的内核,即内核映像类型为zImage、不支持initrd、实模式内核必须位于0x90000。
version 0x206/2 2.00+ 启动协议版本号,格式为(major << 8)+minor,如:版本2.04值为 0x0204。
readmode_swtch 0x208/4 2.00+ 修改(可选) Boot loader hook。16位实模式远程子例程,在进入保护模式之前立即被调用。缺省的例程是关闭NMI。
start_sys 0x20c/4 2.00+ 装载的低位段(0x1000),已不用了,仅仅给bootsect.S使用。
kernel_version 0x20e/2 2.00+ 指向内核版本字符串,可用于显示内核版本给用户,值应小于( 0x200*setup_sects)。例如:该值设为 0x1c00,则内核版本字符串可以内核映像文件的偏移 0x1e00处找到。
type_of_loader 0x210/1 2.00+ Boot Loader的ID,格式为 0xTV,其中,T为Boot Loader的ID,V为其版本号。ID值列出如下:
0 LILO (0x00保留给2.00版本以前的Boot Loader使用)
1 Loadlin
2 bootsect-loader(0x20, 所有其他值保留)
3 SYSLINUX
4 EtherBoot
5 ELILO
7 GRuB
8 U-BOOT
9 Xen
A Gujin
B Qemu
loadflags 0x211/1 2.00+ 修改 启动协议选项标识位掩码。各位说明如下:
Bit 0 (读):LOADED_HIGH
- 0表示保护模式代码装载在0x10000。
- 1表示保护模式代码装载在0x100000。
Bit 6 (写): KEEP_SEGMENTS
:
在版本2.07+存在
:
- 0表示重装载段寄存器在32位入口处。
:
- 1表示不重装载段寄存器在32位入口处,假定%cs %ds %ss %es都设置为基地址为0的平面段。
Bit 7 (写): CAN_USE_HEAP
:
设置此位为1,表示heap_end_ptr的值有效,heap_end_ptr表示在setup.S后有多少空间可用于堆。如果清除此位,表示一些setup代码功能被关闭。
setup_move_size 0x212/2 2.00-2.01 修改 在使用版本为2.00或2.01时,如果实模式内核不装载在 0x90000,此值表示移动的尺寸。
code32_start 0x214/4 2.00+ 修改 它是在内核解压缩之前立即跳转到的32位扁平模式(flat-mode)例程。 Boot
Loader用来决定合适的装载地址。除了CS外,没有段建立,用户应设置段到KERNEL_DS
(0x18)。完成了应户的hook后,应跳转到用户Boot Loader覆盖写此域之前此域所在的地址。有两种目的修改此域:(1)作为Boot
Loader hook。(2)没有安装hook的Boot Loader装载可重定位内核在非标准位置。
ramdisk_image 0x218/4 2.00+ initrd(initial ramdisk)或ramfs的32位线性地址。
ramdisk_size 0x21c/4 2.00+ initrd(initial ramdisk)或ramfs映像的大小。
bootsect_kludge 0x220/4 2.00+ 内核内部 不再使用,仅仅给bootsect.S使用。
heap_end_ptr 0x224/2 2.01+ 是在setup结尾后面的空闲内存,它指向setup堆的偏移限制值,当loadflags设置了CAN_USE_HEAP(0x80位)时,此域才有效。heap_end_ptr为相对于setup开始处(0x0200)的值,即绝对值需要减去0x0200。
cmd_line_ptr 0x228/4 2.02+ 内核命令行的线性地址。内核命令行可以位于setup heap 堆结尾与0xA0000之间的任何位置。它不能位于与实模式代码本身相同的同一64K段。
initrd_addr_max 0x22c/4 2.03 initrd(initial ramdisk)/ramfs内容占用的最大地址。例如:initrd为131072字节长,此域值为 0x37FFFFFF,则initrd从0x37FE0000.开始。
kernel_alignment 0x230/4 2.05+ 内核要求的对齐单位(如果relocatable_kernel为true)。
relocatable_kernel 0x234/1 2.05+ 内核是否对齐。此域为非0,表示内核的保护模式部分可以位于满足kernel_alignment 的任何地址,在loading后,Boot Loader必须设置 code32_start域指向装载的代码或Boot Loader hook。
cmdline_size 0x238/4 2.06+ 命令行的最大尺寸(不包括结尾符0)。
hardware_subarch 0x23c/4 2.07+ 此域允许Boot Loader通知内核硬件的低级构架,在并行虚拟化环境中,硬件低级构架环境(如:中断处理、页表处理和访问进程控制寄存器)需要不同处理。此域允许Boot Loader通知内核多个这样的环境。
0x00000000缺省的x86/PC环境
0x00000001lguest
0x00000002Xen
hardware_subarch_data 0x240/8 2.07+ 指向特定硬件子构架的数据。

 

其实这个表,对于我们不是很重要,可以选择飘过~,继续看代码。

.section ".entrytext", "ax"
start_of_setup:                                      #上面跳到这里执行真正的代码段
#ifdef SAFE_RESET_DISK_CONTROLLER
# Reset the disk controller.                     #复位硬盘控制器
    movw    $0x0000, %ax        # Reset disk controller
    movb    $0x80, %dl        # All disks
    int    $0x13
#endif

# Force %es = %ds
    movw    %ds, %ax
    movw    %ax, %es
    cld                                 #di,si ++

# Apparently some ancient versions of LILO invoked the kernel with %ss != %ds,
# which happened to work by accident for the old code.  Recalculate the stack
# pointer if %ss is invalid.  Otherwise leave it alone, LOADLIN sets up the
# stack behind its own code, so we can't blindly put it directly past the heap.

# 一些旧版本的LILO在进入内核时偶尔发生%
ss
 != %
ds

# 如果%
ss
无效,重计算栈指针,否则,不用管它,

# 装载器LOADLIN在它的代码后面会建立栈,

# 因此,不要盲目把栈直接放在堆后面

    movw    %ss, %dx     
    cmpw    %ax, %dx     # %ds == %ss?  # because ax==ds
    movw    %sp, %dx
    je    2f                       # -> assume %sp is reasonably set  #有效ss段

    # Invalid %ss, make up a new stack
    movw    $_end, %dx               # _end为setup.elf代码的末尾
    testb    $CAN_USE_HEAP, loadflags
    jz    1f
    movw    heap_end_ptr, %dx  # 看上面_end+STACK_SIZE-512
1:    addw    $STACK_SIZE, %dx   # 给栈分配空间
    jnc    2f                                    # 如果溢出,即超过了0xffff
    xorw    %dx, %dx    # Prevent wraparound   # 将dx=0

2:    # Now %dx should point to the end of our stack space
    andw    $~3, %dx    # dword align (might as well...)  # 双字对齐
    jnz    3f                    # 如果没有溢出,则jump to 3f:
    movw    $0xfffc, %dx    # Make sure we're not zero
3:    movw    %ax, %ss      # 这里的ax为上面设置的ds段
    movzwl    %dx, %esp    # Clear upper half of %esp       #将esp的高端清0
    sti            # Now we should have a working stack          #开中断

# We will have entered with %cs = %ds+0x20, normalize %cs so
# it is on par with the other segments.
    pushw    %ds
    pushw    $6f
    lretw                  # 就是跳到6f:,作用在于将cs设置成ds,和整个setup.elf的装入地址一致
6:

# Check signature at end of setup

# 检查位于setup.
elf代码末尾的签名标志“AA55”或者“5A5A”,以确定setup.
elf代码是否全部装入。

# 如果没有找到签名标志,说明setup.
elf并未完全装入,程序控制转移至setup_bad

    cmpl    $0x5a5aaa55, setup_sig         # setup_sig在boot/setup.ld文件中
    jne    setup_bad                                 

# Zero the bss   # 清空bss段,一些标记在boot/setup.ld中可以找到
    movw    $__bss_start, %di
    movw    $_end+3, %cx
    xorl    %eax, %eax 
    subw    %di, %cx
    shrw    $2, %cx
    rep                                    # if(cx--!=0)[es:di]=eax

# Jump to C code (should not return)       #  main函数位于boot/main.c,我们的下一站
    calll    main                            # call后面还有个l代表main这个地址是long型

# Setup corrupt somehow...

# 发生错误则打印错误信息
setup_bad:
    movl    $setup_corrupt, %eax
    calll    puts            # 在boot/tty.c中可以找到,打印函数

                                # 不过这里与C调用约定违背了,应该将字符串首地址入栈才对,这个疑问希望得到大家的帮助?

    # Fall through...

 

# 写个死机函数
    .globl    die
    .type    die, @function
die:
    hlt
    jmp    die

    .size    die, .-die

    .section ".initdata", "a"
setup_corrupt:
    .byte    7
    .string    "No setup signature found.../n"

总结一下,做了哪些事情:

1、硬盘复位

2、检查并设置堆栈

3、检查setup.elf是否装入完全

4、将.bss段清0

5、跳入main函数(boot/main.c)


夫不负有心人,我们终于正式走入了内核源代码,而且分析了第一个文件header.S,感觉这一切来的这么突然,又好像太慢了。不过我们可以自豪的说,这
都是努力的成果。虽然我们对代码没有细致分析,这是由于这块内容不是我们关注的重点,而且本身它的复杂性也导致了我们只需略懂即可。如果后面还有充裕的时
间,可以在详细分析。

抱歉!评论已关闭.