Linux0.11+x86 启动流程总结
阅读原文时间:2021年04月20日阅读:1

文章目录

前言

学习总结,看网上现有资料不是特别好,自己就梳理下自己知道的吧

参考资料:
Linux0.11 源码
《保护方式下的 80386 及其编程》
《Linux 内核完成注释 0.11》 主要参考来源
《汇编语言_王爽》
《计算器组成原理_唐朔飞》

x86 硬件介绍

看启动流程少不了要跟汇编打交道,而汇编又跟 CPU 寄存器,及 CPU 存储框架,微机框图高度相关,
所以会先介绍下相关硬件,再从软件讲

CPU 寄存器

简单介绍下 80386 的寄存器及功能

通用寄存器组:
    EAX:累加器,还用于乘、除法和输入、输出的数据寄存器
    ECX:计数寄存器,在循环和字符串操作中作为循环计数器
    EDX:数据寄存器,还用于乘、除法,在输入输出指令中作间址寄存器
    EBX:基地址寄存器,在循环和字符串操作中作为循环计数器
    ESP:堆栈指针,用于指示栈顶元素的地址
    ESI:源变址寄存器,在间接寻址时作为地址或变址寄存器,在字符串操作中作为源变址寄存器
    EDI:目的变址寄存器,在间接 寻址时作为地址或变址寄存器,在字符串操作中作为目的变址寄存器

状态控制寄存器:
    EIP: 指令指针寄存器,指向处理器要执行的下一条指令 
    EFLAGS: 处理器的状态和控制标志寄存器 EFLAGS, 包含若干个状态标志位和控制标志位

段寄存器:
    CS: 代码段寄存器
    DS; 数据段寄存器
    ES/FS/GS:  其中 ES 作为某些字符串指令作为目标操作数寄存器,它们通常用于寻址那些不太常引用的数据段
    SS: 堆栈段寄存器

段的由来

Intel 段起源于 8086 这个老祖宗,因为当前 CPU 内部总线宽度为 16 位,而扩展的外部存储总线宽度为 20 位,所以需要额外的寄存器来寻址

大概意思就类似于,一个人有十个手指头,那我掰着数只能到十,再在我想数到 100 怎么办?
简单点,来十个人,每个要名字取不一样,再每个人名字搞不一样,这样我就能数到 100 了

针对 x86 就形成了这样的寻址方式:

段地址+偏移地址 
段地址保存在:CS/DS/ES/FS/GS/SS
偏移地址保存在:IP/AX/BX/CX/DX/SP ...

程序为为代码,数据,堆栈,细分下就有了 代码段,数据段,堆栈段的区分

CS+IP: 代码段+代码偏移
SS+SP: 堆栈段+堆栈指针
DS: 数据段

内存保护

内存保护主要目的是在多任务操作系统中,让不同的任务只访问属于自己的地址空间,那怎么做呢,就是让段+偏移量不直接访问物理内存
中间添加转换过程用以保护:

内存管理寄存器


x86 编程官方本义模型是这个样子的:

每个任务有数据段和代码段,保存在自己的 LDT 中
而 LDT 又保存在 GDT 中
切换任务就是在 GDT 中找到不同任务的 LDT 代码段执行

    GDTR: 保存了系统中各个任务的局部描述符 LDT
    LDTR: 保存了本任务的段映射关系 LDT
    IDTR: 中断描述符表 IDT
    TR: 保存了当前任务的 TSS 任务状态段

相关关系如下图:

GDT 全局一个
IDT 全局一个
LDT x86 本来构想是每任务一个

权限保护

主要针对带操作系统的软件,将程序分几个特权级,相关敏感资源只能通过操作系统进行,不让其他程序瞎动


特权指令有如下部分:

控制寄存器


主要由页转换用基地址表等相关控制位

x86 计算机框架

细点如下:

软件流程

上电复位+BIOS 阶段

内存地址空间分布图


Linux0.11 阶段



加载模块存储位置变迁图:

bootsect.s

    !
    ! SYS_SIZE is the number of clicks (16 bytes) to be loaded.
    ! 0x3000 is 0x30000 bytes = 196kB, more than enough for current
    ! versions of linux
    ! 
    ! /*SYS_SIZE 是要加载的系统模块长度,单位是节,16字节为 1 节。0x3000 共为 0x30000 字节
    !   = 196KB(若以 1024 字节为 1KB 计,则应该为192KB),对于当前的版本空间已足够了。
    !这里的感叹号 ! 或 ; 号表示注释句的开始。*/
    SYSSIZE = 0x3000
    !
    !   bootsect.s      (C) 1991 Linus Torvalds
    !
    ! bootsect.s is loaded at 0x7c00 by the bios-startup routines, and moves
    ! iself out of the way to address 0x90000, and jumps there.
    !
    ! It then loads 'setup' directly after itself (0x90200), and the system
    ! at 0x10000, using BIOS interrupts. 
    !
    ! NOTE! currently system is at most 8*65536 bytes long. This should be no
    ! problem, even in the future. I want to keep it simple. This 512 kB
    ! kernel size should be enough, especially as this doesn't contain the
    ! buffer cache as in minix
    !
    ! The loader has been made as simple as possible, and continuos
    ! read errors will result in a unbreakable loop. Reboot by hand. It
    ! loads pretty fast by getting whole sectors at a time whenever possible.''"
    ! /* 以下是对前面这些文字的翻译:
    !   bootsect.s              1991 Linus Torvalds 版权所有
    !   
    !   bootsect.s 被 bios 启动子程序加载至 0x7c00(31KB)处,并将自己移到地址 0x90000(576KB)片
    ! 并跳转至那里。
    !   它然后使用 bios 中断将 setup 直加载到自己的后面(0x90200)(576.5KB),并将 system 加载
    ! 至地址 0x10000 处。
    !   注意!目前的内核系统最大长度限制为(8*65536)(512KB)字节,即使在将来这也应该没有问题的。
    ! 我想让它们保持简单明了。这样 512 KB的最大内核长度应该足够了,尤其是这里没有象 minix 中
    ! 一样包含缓冲区高速缓冲。
    !   加载程序已经做够简单了,所以持续的读出错将导致死循环。只能手工重启。
    ! 只要可能,通过一次读取所有的扇区,加载过程可以做的很快。*/

    ! /* 伪指令(伪操作符) .globl 或 .global 用于定义随后的标识符是外部的或全局的,并且即使
    ! 不使用也强制引入。 .text .data 和 .bss 用于分别定义当前代码段,数据段和未初始化数据段。
    ! 在链接多个目标模块时,链接程序(ld86)会根据它们的类别把各个目标模块中相应段分别组合(合并)
    ! 在一起。这里把三个段都定义在同一重叠地址范围中,因此本程序实际上不分段。
    !   另外,后面带冒号的字符串是标号,例如下面的 ‘begtext:',一条汇编语句通常由标号(可选)、
    ! 指令助记符(指令名)和操作数三个字段组成。标号位于一条指令的第一个字段。它代表其所在位
    ! 置的地址,通常指明一个跳转指令的目标位置。*/
    .globl begtext, begdata, begbss, endtext, enddata, endbss !// 定义了6个全局标识符
    .text                                           !// 文本段
    begtext:
    .data                                           !// 数据段
    begdata:
    .bss                                            !// 未初始化数据段(Block Started by Symbol)
    begbss:
    .text                                           !// 文本段

    SETUPLEN = 4                !// setup 的扇区数                          nr of setup-sectors
    BOOTSEG  = 0x07c0           !// bootsect 的原始地址(是段地址,以下同)    original address of boot-sector
    INITSEG  = 0x9000           !// 将bootsect移动到这里--避开              we move boot here - out of the way
    SETUPSEG = 0x9020           !// setup 程序从这里开始                   setup starts here
    SYSSEG   = 0x1000           !// system 模块加载到 0x10000(64KB)处     system loaded at 0x10000 (65536).
    ENDSEG   = SYSSEG + SYSSIZE     !//停止加载的段地址                     where to stop loading


    ! ROOT_DEV: 0x000 - same type of floppy as boot.
    !//                  根文件系统设备使用与引导时同样的软驱设置
    !           0x301 - first partition on first drive etc
    !//                  根文件系统设置在第一个硬盘的第一个分区上,等等
    !/* 设备号 0x306 指定根文件系统设置是第 2 个硬盘的第一个分区。当年 Linus 是在第2个硬盘
    ! 上安装了 Linux0.11 系统,所以这里 ROOT_DEV 被设置为 0x306 .在编译这个内核时你可以根据
    ! 自己根文件系统所在设置位置修改这个设备号。这个设置号是 linux 系统老式的硬盘设置命名方
    ! 式,硬盘设置号具体值的含义如下:
    !   设备号 = 主设备号 * 256 + 次设备号(也即 dev_no = (major<<8)+minor)
    !   (主设备号: 1 - 内存,2 - 磁盘,3 - 硬盘,4 - ttyx,5 - tty,6 - 并行口,7 - 非命名管道)
    ! 0x300 -/dev/hd0 - 代表整个第 1 个硬盘
    ! 0x301 -/dev/hd1 - 第 1 个硬盘的第 1 个分区
    ! ...
    ! 0x304 -/dev/hd4 - 第 1 个盘的第 4 个分区
    ! 0x305 -/dev/hd5 - 代表整个第 2 个硬盘
    ! 0x306 -/dev/hd6 - 第 2 个盘的第 1 个分区
    ! ...
    ! 0x309 -/dev/hd9 - 第 2 个盘的第 4 个分区
    ! 从Linux 内核 0.95 版后就已经使用与现在内核相同的命名方法了*/
    ROOT_DEV = 0x306

    ///
    //              程序入口处,将 bootsect 复制到 0x9000 处
    ///
    !/* 伪指令 entry 迫使用链接程序在生成的执行程序(a.out)中包含指定的标识符或标号。
    ! 88--97 行作用是将自身(bootsect)从目前段位置 0x07c0(31KB)移动到 0x9000(576KB)处,
    ! 共 256 字节(512字节),然后跳转到移动后的代码的 go 标号处,也即本程序的下一语句处。*/
    entry start                         !//告知链接程序,程序从 start 标号开始执行
    start:
        mov ax,#BOOTSEG                 !//将 ds 段寄存器置为 0x7c0;           
        mov ds,ax
        mov ax,#INITSEG                 !//将 es 段寄存器置为 0x9000;
        mov es,ax
        mov cx,#256                     !//设置移动计数值 = 256字 =512字节
        sub si,si
        sub di,di
        rep
        movw                            !//即 movs 指令,这里从内存[si]处移动 cx 个字到[di]处
        jmpi    go,INITSEG              !//段间跳转(Jump Intersegment),这里的 INITSEC 指出跳转
                                        !//到的段地址,标号 go 是段内偏移地址。

    !///
    !//             设置临时堆栈
    !///
    !/* 从下面开始,CPU 在已移动到 0x90000 位置处的代码中执行。
    ! 这段代码设置几个段寄存器,包括栈寄存器 ss 和 sp.栈指针 sp 只要指向远大于 512 字节偏移
    ! (即地址 0x90200)处都可以,因为为从 0x90200 地址开始处还要放置 setup 程序,而此时 setup
    ! 程序大约为 4 个扇区,因为此 sp 要指向大于(0x200+0x200 * 4 + 堆栈大小)处。
    !   实际上 BIOS 把引导扇区加载到 0x7c00 处并把执行权交给引导程序时, ss = 0x00,sp =0xfffe.*/
    go: mov ax,cs                   !// 将ds,es和ss 都置成移动后代码所在的段处(0x9000)
        mov ds,ax
        mov es,ax                   !//由于程序中有堆栈操作(push,pop,call),因此必须设置堆栈
    ! put stack at 0x9ff00.     !//将堆栈指针 sp 指向 0x9ff000(即 0x9000:0xff00)处
        mov ss,ax
        mov sp,#0xFF00              !//作意远大于 512 值的位置        arbitrary value >>512
    !///
    !//             通过 BIOS 的 0x13 中断从磁盘中读取 setup 模块到内存 0x90200 处
    !///
    ! load the setup-sectors directly after the bootblock.
    ! Note that 'es' is already set up.
    !/* 在bootsect 程序块后紧跟着加载 setup 模块代码数据。
    ! 注意 es 已经设置好了.(在移动代码时,es 已经指向目的段的地址处 0x9000)
    !    130--134 行的用途是利用 BIOS 中断 INT 0x13 将 setup 模块从磁盘第 2 个扇区开始
    ! 读到 0x90200 开始处,共读 4 个扇区。如果读出错,则复位驱动器,并重试,没有退路。
    !   INT 0x13 的使用方法如下:
    ! 读扇区:
    ! ah = 0x02 - 读磁盘扇区到内存;     al = 需要读出的扇区数量;
    ! ch = 磁道(柱面)号的低 8 位;       cl = 开始扇区(位0--5),磁道号高 2 位(位6--7)
    ! dh = 磁头号;                     dl = 驱动器号(如果是硬盘则位 7 要置位);
    ! es:bx ---> 指向数据缓冲区;如果出错则 CF 标志置位, ah 中是出错码。*/

    load_setup:
        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
        !//否则就重新操作
        mov dx,#0x0000
        mov ax,#0x0000      ! reset the diskette
        int 0x13
        j   load_setup      !//即 jmp 指令。

    ok_load_setup:
    ///
    //              获得 每磁道的扇区数存 --> sectors 中
    ///
    ! Get disk drive parameters, specifically nr of sectors/track
    !/* 取磁盘驱动器的参数,特别是每道的扇区数量。
    ! 即磁盘驱动器参数 INT 0x13 调用格式和返回信息如下:
    ! ah = 0x08             dl = 驱动器号(如果是硬盘则要置位 7 为 1)
    ! 返回信息:
    ! 如果出错则 CF 置位,并且 ah = 状态码。
    ! ah = 0,   al =0,                  bl = 驱动器类型(AT/PS2)
    ! ch = 最大磁道号的低 8 位,     cl = 每磁道最大扇区数(位0--5),最大磁道号高 2 位(位6--7)
    ! dh = 最大磁头数,                   dl = 驱动器数量,
    ! es:di --> 软驱磁盘参数表。*/
        mov dl,#0x00
        mov ax,#0x0800      ! AH=8 is get drive parameters
        int 0x13
        mov ch,#0x00        !// ch = CH 【CX的高位】

    !/* 下面指令表示下一条语句的操作数在 cs 段寄存器中所指的段中,它只影响下一条语句。实际上,
    ! 由于本程序代码和数据都被设置处于同一个段中,即段寄存器 cs 和 ds,es的值相同,因此本程序
    ! 此处可以不使用该指令。*/
        seg cs

    !/* 下句保存每磁道扇区数,对于软盘来说(dl = 0),其最大磁道号不会超过 256,ch 已经足够表示它
    ! 因此 cl 的位 6-7 肯定为 0,又 155 行已置 ch = 0,因此此时 cx 中是每磁道扇区数。*/
        mov sectors,cx      !//【每磁道扇区数】
        mov ax,#INITSEG
        mov es,ax           !//因为上面取磁盘参数中断改掉了 es 的值,这里重新改回

    !/
    !//                     显示信息: Loading system...回车换行
    !
    ! Print some inane message
    !/* 显示信息:"Loading system...'回车换行'",共显示包括回国和换行控制字符在内的 24 个字符
    ! BIOS 中断 0x10 功能号 ah = 0x03,读光标位置。
    ! 输入: bh = 页号
    ! 返回: ch = 扫描开始线; cl = 扫描结束线; dh = 行号(0x00顶端); dl = 列号(0x00 最左边)
    !
    ! BIOS 中断 0x10 功能号 ah = 0x13,显示字符串。
    ! 输入: al = 放置光标的方式及规定属性。 0x01 - 表示使用 bl 中的属性值,光标停在字符串结尾。
    ! es:bp 此寄存器对指向要显示的字符串起始位置处。 cx = 显示的字符串字符数。 bh = 显示页面号;
    ! bl = 字符属性。    dh = 列号。*/
        mov ah,#0x03        !// read cursor pos
        xor bh,bh           !// 首先读光标位置。返回光标位置值在 dx 中。
        int 0x10            !// dh = 行(0--24);   dl = 列(0--79).供显示串用


        mov cx,#24          !// 共显示 24 个字符
        mov bx,#0x0007      !// page 0, attribute 7 (normal)
        mov bp,#msg1        !//es:bp 寄存器对指向要显示的字符串
        mov ax,#0x1301      !// write string, move cursor
        int 0x10            !// 写字符串并移动光标到串结尾处。

    !
    !//             开始将 system 模块加载 0x10000(64KB)
    !///
    ! ok, we've written the message, now
    ! we want to load the system (at 0x10000)'"
    !// 现在开始将 system 模块加载到 0x10000(64KB) 开始处。

        mov ax,#SYSSEG
        mov es,ax               ! segment of 0x010000 // es = 存放 system 的段地址。
        call    read_it         !// 读磁盘上 system 模块,es 为输入参数。
        call    kill_motor      !// 关闭驱动器马达,这样就可以知道驱动器的状态了。

    ! After that we check which root-device to use. If the device is
    ! defined (!= 0), nothing is done and the given device is used.
    ! Otherwise, either /dev/PS0 (2,28) or /dev/at0 (2,8), depending
    ! on the number of sectors that the BIOS reports currently.
    !/* 此后,我们检查要使用哪个根文件系统设备(简称根设备)。如果已经指定了设备(!=0)
    ! 就直接使用给定的设备。否则就需要根据 BIOS 报告的每磁道扇区数来
    ! 确定到底使用/dev/PS0(2,28) 还是 /dev/at0(2,8)。
    ! 上面一行中两个设备文件的含义:
    ! 在 Linux 中软驱的主设备号是 2 (参见第 83 行的注释),次设备号 = type*4 + nr,其中
    ! nr 为 0-3 分别对应软驱 A,B,C或 D; type 是软驱的类型(2-->1.2MB或 7-->1.44MB等)。
    ! 因为 7*4 + 0 = 28,所以 /dev/PS0 (2,28)指的是 1.44MB A 驱动器,其设备号是 0x21c
    ! 同理 /dev/at0 (2,8) 指的是 1.2MB A驱动器,其设备号是 0x0208。
    ! 下面 root_dev 定义在引导扇区 508,509字节处,指根文件系统所在设备号。0x0306 指第 2
    ! 个硬盘第 1 个分区。这里默认为 0x0306 是因为当时 Linus 开发 Linux 系统时是在第 2 个硬
    ! 盘第 1 个分区中存放根文件系统。这个值需要根据你自己根文件系统所在硬盘和分区进行修
    ! 改。例如,如果你的根文件系统在第 1 个硬盘的第 1 个分区上,那么该值应该为 0x0301,即
    ! (0x01,0x03).如果根文件系统是在第 2 个 Bochs 软盘上,那么该值应该为 0x021D,即
    ! (0x1D,0x02).当编译内核时,你可以在 MakeFile 文件中另行指定你自己的值,内核映像
    ! 文件 Image 的创建程序 tools/build 会使用你指定的值来设置你的根文件系统所在设备号。*/
    !
    !//             设置 root_dev 根设备号
    !///
        seg cs
        mov ax,root_dev     !//【取 508,509字节处的根设备号并判断是否已被定义】
        cmp ax,#0
        jne root_defined
    !//取上面第 170 行保存的每磁道扇区数。如果 sectors = 15 则说明是 1.2MB 的驱动器;如果
    !// sectors = 18,则说明是 1.44MB 软驱。因为是可引导的驱动器,所以肯定是 A 驱。
        seg cs
        mov bx,sectors
        mov ax,#0x0208      !// /dev/ps0 - 1.2Mb
        cmp bx,#15          !//判断每磁道扇区数是否 = 15
        je  root_defined    !//如果等于,则 ax 中就是引导驱动器的设备号
        mov ax,#0x021c      !// /dev/PS0 - 1.44Mb
        cmp bx,#18
        je  root_defined
    undef_root:             !//如果都不一样,则死循环(死机)。
        jmp undef_root
    root_defined:
        seg cs
        mov root_dev,ax     !//将检查过的设备号保存到 root_dev 中。

    ! after that (everyting loaded), we jump to
    ! the setup-routine loaded directly after
    ! the bootblock:
    !//到此,所有的程序都加载完毕,我们就跳转到被加载在 bootsect 后面的 setup 程序去。
    !//段间跳转指令(Jump Intersegment)。跳转到 0x9020:0000(setup.s 程序开始处)去执行。
        jmpi    0,SETUPSEG  !//到此本程序就结束了。

    !//下面两个子程序。read_it 用于读取磁盘上的 system 模块。 kill_moter 用于关闭软驱的马达。
    ! This routine loads the system at address 0x10000, making sure
    ! no 64kB boundaries are crossed. We try to load it as fast as
    ! possible, loading whole tracks whenever we can.
    !
    ! in:   es - starting address segment (normally 0x1000)
    !
    !//该子程序将系统模块加载到内存地址 0x10000 处,并确定没有跨越 64KB 的内存边界。
    !//我们试图尽快地进行加载,只要可能,就每次加载整条磁道的数据。
    !//输入: es - 开始内存地址段值(通常是 0x1000)
    !//
    !//下面伪操作符 .word 定义一个 2 字节目标。相当于 C 语言程序中定义的变量和所占内存空间大小。
    !// '1+SETUPLEN' 表示开始时已经读进 1 个引导扇区和 setup 程序所占的扇区数 SETUPLEN。
    sread:  .word 1+SETUPLEN    ! sectors read of current track !//当前磁道中已读扇区数。
    head:   .word 0             ! current head                  !//当前磁头号    
    track:  .word 0             ! current track                 !//当前磁道号

    !/
    !// 读磁盘上 system 模块,es 为输入参数。es = 存放 system 的段地址。
    !//
    read_it:                //【64KB来历,8086兼容模式段偏移寻址范围为 2^16 = 64KB】
    !// 首先测试输入的段值。从盘上读入的数据必须存放在位于内存地址 64KB 的边界开始处,否则进
    !//入死循环。清 bx 寄存器,用于表示当前段内存放数据的开始位置。
    !//250 行上的指令 test 以比特位逻辑与两操作数。若两个操作数对应的比特位都为 1,则结果值
    !//对应比特位为 1,否则为 0.该操作结果只影响标志(零标志ZF等)。例如,若 AX =0x1000,那么
    !//test 指令的执行结果是(0x1000 & 0x0fff) = 0x0000,于是 ZF 标志置位。此时即下一条指令 jne
    !//条件不成立。
        mov ax,es
        test ax,#0x0fff
    die:    jne die     ! es must be at 64kB boundary // es 值必须位于 64KB 地址边界
        xor bx,bx       ! bx is starting address within segment // bx 为段内偏移


    rp_read:
    !//接着判断是否已经读入全部数据。比较当前所读段是否就是系统数据末端所处的段(#ENDSEG),如果
    !//不是就跳转至下面 ok1_read 标号处继续读数据。否则退子程序返回。
        mov ax,es
        cmp ax,#ENDSEG      ! have we loaded all yet?// 是否已经加载了全部数据?
        jb ok1_read
        ret
    ok1_read:
    !//计算和验证当前磁道需要读取的扇区数,放在 ax 寄存器中。
    !//根据当前磁道还未读取的扇区数以及段内数据字节开始偏移位置,计算如果全部读取这些未读扇区,
    !//所读总字节数是否会超过 64KB 段长度的限制。若会超过,则根据此次最多能读入的字节数(64KB-
    !//段内偏移位置),反算出此次需要读取的扇区数。
        seg cs
        mov ax,sectors      !//即每磁道扇区数。
        sub ax,sread        !//减去当前磁道已读扇区数【1+4=引导 1 + setup 4 】。
        mov cx,ax           !//cx = ax = 当前磁道未读扇区数。
        shl cx,#9           !//cx = cx * 512 字节 + 段内当前偏移值(bx)。
        add cx,bx           !// = 此次读操作后,段内其读入的字节数。 【2^16 = 64KB 所以加上超了就会进位】
        jnc ok2_read        !//若没有超过 64KB 字节,则跳转至 ok2_read 处执行。JNC (Jump if Not Carry )如果进位位为0,则转移
        je ok2_read         !//若相等也跳转。
    !//若加上此次将磁道上所有未读扇区时会超过 64KB.则计算此时最多能读入的字节数:
    !//(64KB - 段内读偏移位置),再转移成需读取的扇区数。其中 0 减某数就是取该数 64KB 的补值。
        xor ax,ax
        sub ax,bx           !// 0 减某数就是取该数 64KB 的补值。
        shr ax,#9

    ok2_read:
    !//读当前磁道上指定开始扇区(cl)和需读扇区(al)的数据到 es:bx 开始处。然后统计当前磁道
    !//上已经读取的扇区数并与磁道最大扇区数 sectors 作比较。如果小于sectors 说明当前磁道上的还
    !//有扇区未读。于是跳转到 ok3_read 处继续操作。
        call read_track     !//读当前磁道上指定开始扇区和需读扇区数的数据。 ax = 需要读的扇区数
        mov cx,ax           !//cx = 该次操作已读取的扇区数。
        add ax,sread        !//加上当前磁道上已经读取的扇区数。
        seg cs
        cmp ax,sectors      !//如果当前磁道上的还有扇区未读,则跳转到 ok3_read 处。
        jne ok3_read
        mov ax,#1           /*1.44MB的软盘参数是 80 个磁道,2 个磁头和每磁道有 18 个扇区,每个扇区有 512 字节。其扇区总数是 2880,总容量为 80 x 2 x 18 x 512 = 147560 字节。*/
        sub ax,head         !// 读完 0 号磁头读 1 号,读完 1 号再读 0 号,循环的。 
        jne ok4_read
        inc track
    ok4_read:
        mov head,ax
        xor ax,ax
    ok3_read:
    !//如果当前磁道上的还有未读扇区,则首先保存当前磁道已读扇区数,然后调整存放数据处的开始
    !//位置。若小于 64KB 边界值,则跳转到 rp_read(156行),继续读数据。
        mov sread,ax        !//保存当前磁道已读扇区数。
        shl cx,#9           !//上次已读扇区数 *512 字节。
        add bx,cx           !//调整当前段内数据开始位置。
        jnc rp_read
    !//否则说明已经读取 64KB 数据。此时调整当前段,为读下一段数据作准备。
        mov ax,es
        add ax,#0x1000      !//将段基址调整为指向下一个 64KB 内存开始处。
        mov es,ax
        xor bx,bx           !//清段内数据开始偏移值。
        jmp rp_read         !//跳转至 rp_read(156行)处,继续读数据。

    read_track: //传入参数: AX = 需要读的扇区数
    !//读当前磁道上指定开始扇区和需读扇区的数据到 es:bx 开始处。参见上文中对 BIOS 磁盘读中断
    !// INT 0x13,ah = 2 的说明。
    !//al - 需读扇区数;          es:bx - 缓冲区开始位置。
    /*! INT 0x13 的使用方法如下:
    ! 读扇区:
    ! ah = 0x02 - 读磁盘扇区到内存;     al = 需要读出的扇区数量;
    ! ch = 磁道(柱面)号的低 8 位;       cl = 开始扇区(位0--5),磁道号高 2 位(位6--7)
    ! dh = 磁头号;                     dl = 驱动器号(如果是硬盘则位 7 要置位);
    ! es:bx ---> 指向数据缓冲区;如果出错则 CF 标志置位, ah 中是出错码。*/
        push ax
        push bx
        push cx
        push dx
        mov dx,track        !//取当前磁道号
        mov cx,sread        !//取当前磁道上已读扇区数。
        inc cx              !// cl = 开始读扇区
        mov ch,dl           !// ch = 当前磁道号
        mov dx,head         !//取当前磁头号
        mov dh,dl           !// dh = 磁头号
        mov dl,#0           !// dl = 驱动器号(为 0 表示当前 A 驱动器)
        and dx,#0x0100      !//磁头号不大于 1.
        mov ah,#2           !// ah = 2. 读磁盘扇区功能号
        int 0x13
        jc bad_rt           !//若出错,则跳转到 bad_rt.
        pop dx
        pop cx
        pop bx
        pop ax
        ret

    !// 读磁盘操作出错,则执行驱动器复位操作(磁盘中断功能号 0 ),再跳转到 read_track 处重试。
    bad_rt: mov ax,#0
        mov dx,#0
        int 0x13
        pop dx
        pop cx
        pop bx
        pop ax
        jmp read_track

    !
    !//                  关闭软驱的马达
    !
    /*
     * This procedure turns off the floppy drive motor, so
     * that we enter the kernel in a known state, and
     * don't have to worry about it later.
     */
    /* 这个子程序用于关闭软驱的马达,这样我们进入内核后就有知道它所处的状态,以后也就无须担心它了。*/

    /* 下面的 0x3f2 是软盘控制器的一个端口,被称为数字输出寄存器(DOR)端口。它是一个
     8 位的寄存器,其位 7 - 位 4 分别用于控制 4 个软驱(D-A)的启动和关闭。位3 - 位 2 用于
     允许/禁止 DMA 和中断请求以及启动/复位软盘控制器 FDC。 位1 - 位 0 用于选择操作的软驱
     。以面第 4 行程序在 al 中设置并输出的 0 值,就是用于选择 A 的驱动器,关闭 FDC,禁止
     DMA和中断请求,关闭马达。有关软驱控制卡编程的详细信息请参见 kernel/blk_drv/floppy.c
     程序后面的说明。*/
    kill_motor:
        push dx
        mov dx,#0x3f2   !//软驱控制卡的数据输出寄存器(DOR)端口,只写
        mov al,#0       !//A 驱动器,关闭FDC,禁止 DMA 和中断请求,关闭马达。
        outb            !//将 al 中的内容输出到 dx 指定的端口去。
        pop dx
        ret

    sectors:
        .word 0         !//存放当前启动软盘每磁道的扇区数。

    msg1:                           !//调用 BIOS 中断显示的信息。
        .byte 13,10                 !//回车,换行的 ASCII 码。
        .ascii "Loading system ..." 
        .byte 13,10,13,10           !//其 24 个 ASCII 码字符

    /* 表示下面语句从地址 508(0x1FC)开始,所以 root_dev 在启动扇区的第 508 开始的 2 个字节中。*/
    .org 508
    root_dev:
        .word ROOT_DEV  !//这里存放根文件系统所在的设备号(init/main.c中会用)。

    /* 下面是启动盘具有有效引导扇区的标志。仅供 BIOS 中的程序加载引导扇区时识别使用。它必须位于
    引导扇区的最后那两个字节中。*/
    boot_flag:
        .word 0xAA55

    .text
    endtext:
    .data
    enddata:
    .bss
    endbss:

setup.s

    !
    !   setup.s     (C) 1991 Linus Torvalds
    !
    ! setup.s is responsible for getting the system data from the BIOS,
    ! and putting them into the appropriate places in system memory.
    ! both setup.s and system has been loaded by the bootblock.
    !
    ! This code asks the bios for memory/disk/other parameters, and
    ! puts them in a "safe" place: 0x90000-0x901FF, ie where the
    ! boot-block used to be. It is then up to the protected mode
    ! system to read them from there before the area is overwritten
    ! for buffer-blocks.
    !
    /* setup.s 负责从 BIOS 中获取系统数据,并将这些数据放到系统内存的适当地方。此时
     setup.s 和 system 已经由 bootsect 引导块加载到内存中。

     这段代码询问 bios 有关内存/磁盘/其他参数,并将这些参数放到一个"安全的"地方: 
     0x90000 - 0x901FF,也即原来 bootsect 代码曾经在的地方,然后在被缓冲块覆盖掉之
     前由保护模式的 system 读取。*/

    ! NOTE! These had better be the same as in bootsect.s!
    //以下这些参数最好和 bootsect.s 中的相同!

    INITSEG  = 0x9000   ! we move boot here - out of the way//原来 bootsect 所处的段
    SYSSEG   = 0x1000   ! system loaded at 0x10000 (65536).// system 在0x10000(64KB)处
    SETUPSEG = 0x9020   ! this is the current segment //本程序所在的段地址

    .globl begtext, begdata, begbss, endtext, enddata, endbss
    .text
    begtext:
    .data
    begdata:
    .bss
    begbss:
    .text
    /******************************************************************************************
    **                  程序入口,开始是将 bootsect 原来程序的位置填充系统参数
    *****************************************************************************************/
    entry start
    start:


    /******************************************************************************************
    **                  保存光标位置
    *****************************************************************************************/
    ! ok, the read went well so we get current cursor position and save it for
    ! posterity.
    /* OK,现在读整个磁盘过程都很正常,现在保存光标位置以备今后使用。*/

    /* 这段代码使用 BIOS 中断取屏幕当前的光标位置(列,行),并保存在内存 0x90000处(2字节)
    控制台初始化程序会到此处读取该值。
        BIOS 中断 0x10 功能号 ah = 0x03,读光标位置
        输入: bh = 页号
        返回: ch = 扫描开始线; cl = 扫描结束线; dh = 行号(0x00 顶端); dl = 列号(0x00 最左边)
      下句将 ds 设置成 INITSEG(0x9000),这已经在 bootsect 程序中设置过,但是现在是 setup 程序,
     Linus 常见得需要再重新设置一下。
    */
        mov ax,#INITSEG ! this is done in bootsect already, but...
        mov ds,ax
        mov ah,#0x03    ! read cursor pos
        xor bh,bh
        int 0x10        ! save it in known place, con_init fetches
        mov [0],dx      ! it from 0x90000. //1个字【保存光标位置】

    /******************************************************************************************
    **              保存扩展内存数                 
    *****************************************************************************************/
    ! Get memory size (extended mem, kB)
    /* 取扩展内存的大小值(KB)
      利用 BIOS 中断 0x15 功能号 ah = 0x88 取系统所含扩展内存大小并保存在内存 0x90002 处。
      返回: ax = 从 0x100000(1M) 处开始的扩展内存大小(KB)。若出错则 CF 置位,ax = 出错码。*/
        mov ah,#0x88
        int 0x15
        mov [2],ax  //将扩展内存数值存在 0x90002 处(1个字)【保存扩展内存数】

    /******************************************************************************************
    **              保存显示页页与显示模式                 
    *****************************************************************************************/
    ! Get video-card data:
    /* 下面这段用于取显示卡当前显示模式。调用 BIOS 中断 0x10 ,功能号 ah = 0x0f
      返回: ah = 字符列数; al = 显示模式; bh = 当前显示页。
      0x90004(1字)存放当前页; 0x90006 存放显示模式; 0x90007 存放字符列数。*/
        mov ah,#0x0f
        int 0x10
        mov [4],bx      ! bh = display page             //当前页【保存显示页面】
        mov [6],ax      ! al = video mode, ah = window width//【保存显示模式】

    /******************************************************************************************
    **              保存显卡内存,显示状态与显卡特性参数                  
    *****************************************************************************************/
    ! check for EGA/VGA and some config parameters
    /* 检查显示方式(EGA/VGA)并取参数。
      调用 BIOS 中断 0x10 ,附加功能选择方式信息。功能号: ah = 0x12, bl = 0x10
      返回: bh = 显示状态。 0x00 - 彩色模式, I/O 端口 = 0x3dX; 0x01 - 单色模式,I/O 端口 = 0x3bX.
      cx = 显示卡特性参数(参见程序后对 BIOS 视频中断 0x10 的说明)*/
        mov ah,#0x12
        mov bl,#0x10
        int 0x10
        mov [8],ax      // 0x90008 = ?                          【未知】
        mov [10],bx     // 【0x9000A = 安装的显示内存,0x9000B = 显示状态(彩色/单色)】
        mov [12],cx     // 【0x9000C = 显示卡特性参数。】

    /******************************************************************************************
    **                  保存第一个硬盘的信息
    *****************************************************************************************/
    ! Get hd0 data
    /* 取第一个硬盘的信息(复制硬盘参数表)。
    第 1 个硬盘参数表的首地址竟然是中断向量 0x41 的向量值! 而第 2 个硬盘参数表紧接在
    第 1 个表的后面,中断向量 0x46 的向量值也指向第 2 个硬盘的参数表首址。表的长度是
     16 个字节(0x10)。下面两段程序分别复制 BIOS 有关两个硬盘的参数表, 0x90080 处存
    放 第 1 个硬盘的表, 0x90090 处存放第 2 个硬盘的表。
        下面第三行语句从内存的指定位置读取一个长指针值并放入 ds 和 si 寄存器中。ds 中
    放段地址, si 是段内偏移地址。这里是把内存地址 4*0x41(=0x104)处保存的 4 个字节(
    段和偏移值)读出。*/
        mov ax,#0x0000
        mov ds,ax
        lds si,[4*0x41]  //取中断向量 0x41 的值,也即 hd0 参数表的地址 --> ds:si
        mov ax,#INITSEG
        mov es,ax
        mov di,#0x0080  //传输的目的地址: 0x9000:0x0080 -->es:di
        mov cx,#0x10    //共传输 16 字节
        rep
        movsb
    /******************************************************************************************
    **                  保存第二个硬盘的信息      
    *****************************************************************************************/
    ! Get hd1 data
        mov ax,#0x0000
        mov ds,ax
        lds si,[4*0x46] //取中断向量 0x46 的值,也即 hd1 参数表的地址 --> ds:si
        mov ax,#INITSEG
        mov es,ax
        mov di,#0x0090  //传输的目的地址: 0x9000:0x0090 -->es:di
        mov cx,#0x10
        rep
        movsb

    /******************************************************************************************
    **                  检查系统是否有第二个硬盘,没有则清零第二个硬盘信息                   
    *****************************************************************************************/
    ! Check that there IS a hd1 :-)
    /* 检查系统是否有第 2 个硬盘,如果没有则把第 2 个表清零。
    利用 BIOS 中断调用 0x13 的取盘类型功能,功能号 ah = 0x15;
    输入: dl = 驱动器号(0x8X 是硬盘: 0x80 指第 1 个硬盘,0x81 第 2 个硬盘)
    输出: ah = 类型码; 00 - 没有这个盘,CF 置位; 01 - 软驱,没用 change-line 支持;
                       02 - 是软驱(或其他可移动设备),有 change-line 支持; 03 - 是硬盘。*/
        mov ax,#0x01500
        mov dl,#0x81
        int 0x13
        jc  no_disk1
        cmp ah,#3       //是硬盘吗?(类型 = 3?)
        je  is_disk1
    no_disk1:
        mov ax,#INITSEG //第 2 个硬盘不存在,则对第 2 个硬盘表清零
        mov es,ax
        mov di,#0x0090
        mov cx,#0x10
        mov ax,#0x00
        rep
        stosb
    is_disk1:

    /
    //                  关中断
    /
    ! now we want to move to protected mode ...
    //现在我们要进入保护模式中了..
        cli         ! no interrupts allowed !//从此开始不允许中断
    /******************************************************************************************
    **                  将 system 模块从 0x10000 ---> 0x0000 位置
    *****************************************************************************************/
    ! first we move the system to it's rightful place'
    /* 首先我们将 system 模块移到正确的位置。
    bootsect 引导程序是将 system 模块读入到 0x10000(64KB)开始的位置。由于当时假设 system
    模块最大长度不会超过 0x80000(512KB),即其末端不会超过内存地址 0x90000,所以 bootsect
    会把自己移到到 0x90000 开始的地方,并把 setup 加载到它的后面。下面这段程序的用途是
    再把整个 system 模块移动到 0x0000 位置,即把从 0x10000 到 0x8ffff 的内存数据块(512KB)
    整块地向内存低端移动了 0x10000(64KB)的位置。*/
        mov ax,#0x0000
        cld             ! 'direction'=0, movs moves forward//后加
    do_move:
        mov es,ax       ! destination segment //es:di 是目的地址(初始为 0x0:0x0)
        add ax,#0x1000
        cmp ax,#0x9000  //已经把最后一段(从 0x8000 段开始的 64KB)代码移动完?
        jz  end_move    //是,则跳转。
        mov ds,ax       ! source segment// ds:si 是源地址(初始为 0x1000:0x0)
        sub di,di
        sub si,si
        mov     cx,#0x8000 //移动 0x8000 字(64KB字节)
        rep
        movsw
        jmp do_move

    /******************************************************************************************
    **                  加载临时的 GDT 表与 IDT 表。             
    *****************************************************************************************/
    ! then we load the segment descriptors
    /* 此后,我们加载段描述符。
    从这里开始会遇到 32 位保护模式的操作,因此需要 Intel 32 位保护模式编程方面的知识
    了,有关这方面的信息请查阅列表后的简单介绍或附录中详细说明。这里仅作概要说明。在
    进入保护模式中运行之前,我们需要首先设置好需要使用的段描述符表。这里需要设置全局
    描述符表和中断描述符表。

    下面指令 lidt 用于加载中断描述符表(IDT)寄存器。它的操作数(idt_48)有 6 字节。前两个
    字节(字节 0 - 1)是描述符表的字节长度值;后 4 字节(字节 2 - 5)是描述符表的 32 位线性
    基地址,其形式参见下面 355-366 行说明。中断描述符表中的每一个 8 字节表项
    指出发生中断时需要调用的代码信息。与中断向量有些相似,但要包含更多的信息。
     lgdt 指令用于加载全局信息描述符表(GDT)寄存器,其操作数格式与 lidt 指令的相同。全
     局描述符表中的每个描述符项(8 字节)描述了保护模式下数据段和代码段(块)的信息。其中
     包括段的最大长度限制(16位)、段的线性地址基址(32 位)、段的特权级、段是否在内存、读
     写许可权以及其他一些保护模式运行的标志。参见后面 337-344 行。*/
    end_move:
        mov ax,#SETUPSEG    ! right, forgot this at first. didn't work :-)'
        mov ds,ax           //ds 指向本程序(setup)段
        lidt    idt_48      !//【 load idt with 0,0】         //加载 IDT 寄存器
        lgdt    gdt_48      ! load gdt with whatever appropriate//加载 GDT 寄存器


    /******************************************************************************************
    **                  开启 A20 地址线问题
    *****************************************************************************************/
    /* A20 地址线描述: 当年 IBM 开始使用 8088 设置计算机时使用 20 位地址线环回地址,后来有了 286
    ,地址线有了 24 位,但当时有些程序已经利用环回编程了,所以为了兼容,使用了 8042 即键盘控制器
    上当时未使用的端口引脚(输出端口 P2,引脚 P21),来开关对 20 位以上的寻址的支持开关。此线称
    为 A20 地址线,开机默认情况下这条线是禁止的,所以操作系统要使用适当方法开启它。
    【详见 Linux内核完全。。 6.3.3.4 】*/
    ! that was painless, now we enable A20
    /* 以上的操作很简单,现在我们开启 A20 地址线。
    为了能够访问和使用 1MB 以上的物理内存,我们需要首先开启 A20 地址线。参见本程序列表后
    有关 A20 信号线的说明。关于所涉及的一些端口和命令,可参考 kernel/chr_drv/keyboard.S
    程序后对键盘接口的说明。至于机器是否真正开启了 A20 地址线,我们还需要在进入保护模式
    之后(能访问 1MB 以上内存之后)在测试一下。这个工作放在了 head.S 程序中(32-36行)*/

        call    empty_8042  //测试 8042 状态寄存器,等待输入缓冲器空
                            //只有当输入缓冲器为空时才可以其执行写命令。
        mov al,#0xD1        ! command write// 0xD1 命令码 - 表示要写数据至
        out #0x64,al        // 8042 的 P2 端口。P2 端口的位 1 用于 A20 线的选能。
                            //数据要写到 0x60 口。
        call    empty_8042  //等等输入缓冲器空,看命令是否被接受
        mov al,#0xDF        ! A20 on //选通 A20 地址线的参数。【0x1011 1111】
        out #0x60,al        
        call    empty_8042  //若此时输入缓冲器为空,则表示 A20 线已经选通。


    /******************************************************************************************
    **                  两个 8259 中断芯片编程
    *****************************************************************************************/
    ! well, that went ok, I hope. Now we have to reprogram the interrupts :-(
    ! we put them right after the intel-reserved hardware interrupts, at
    ! int 0x20-0x2F. There they won't mess up anything. Sadly IBM really
    ! messed this up with the original PC, and they haven't been able to
    ! rectify it afterwards. Thus the bios puts interrupts at 0x08-0x0f,
    ! which is used for the internal hardware interrupts as well. We just
    ! have to reprogram the 8259's, and it isn't fun.'''"
    /* 希望以下一切正常。现在我们必须重新对中断进行编程,我们将它们放在正好
    处于 Intel 保留的硬件中断后面,即 Int 0x20-0x2F. 在那里它们不会引起冲突。
    不幸的是 IBM 在原 PC 机中搞糟了,以后也没有纠正过来。所以 PC 机 BIOS 把中断
    放在了 0x80 - 0x0f , 这些中断也被用于内部硬件中断。所以我们就必须重新对 8259
    中断控制器进行编程,这一点都没意思。

     PC 机使用 2 个 8259A 芯片,关于对可编程控制器 8259A 芯片的编程方法请参见本程序后的介绍。
     第 156 行上定义的两个字(0x00eb)是直接使用机器码表示的两条相对跳转指令,起延时作用。
     0xeb 是直接近跳转指令的操作码,带 1 个字节的相对位移值。因为此跳转范围是 -127 到 127. CPu
     通过把这个相对位移值加到 EIP 寄存器中就形成一个新有有效地址。此时 EIP 指向下一条被执行
     的指令。执行时所花费的 CPU 时钟周期数是 7 至 10个。 0x00eb 表示跳转值是 0 的一条指令,因
     此还是直接执行下一条指令。这两条指令共可提供 14 - 20 个CPU时钟周期的延迟时间。在 as86
     中没有表示相应指令的助记符,因此 Linus 在 setup.s 等一些汇编程序中就直接使用机器码来表
     示这种指令。另外,每个空操作指令 NOP 的时钟周期数是 3 个,因此若要达到相同的延迟效果就
     需要 6 至 7 个NOP指令。
       8259 芯片主片端口是 0x20 - 0x21,从片端口是 0xA0 - 0xA1,输出值 0x11 表示初始化命令开始,它
     是 ICW1 命令字,表示边沿触发、多片 8259 级边、最后要发送 ICW4 命令字。*/
    //【设置 ICW1】
        mov al,#0x11        ! initialization sequence//【0x11 边沿触发,多片8259A级联,需发送ICW4】
        out #0x20,al        ! send it to 8259A-1        //发送到 8259A 主芯片
        .word   0x00eb,0x00eb       ! jmp $+2, jmp $+2// $ 表示当前指令的地址
        out #0xA0,al        ! and to 8259A-2          //再发送到 8259A 从芯片
        .word   0x00eb,0x00eb
    // Linux 系统硬件中断号被设置成从 0x20 开始。参见表 2-2: 硬件中断请求信号与中断号对应表
    //【设置 ICW2】
        mov al,#0x20        ! start of hardware int's (0x20)"'
        out #0x21,al        //送主芯片 ICW2 命令字,设置起始中断号,要送奇端口。【0x20-0x27】
        .word   0x00eb,0x00eb
        mov al,#0x28        ! start of hardware int's 2 (0x28)'
        out #0xA1,al        //送从芯片 ICW2 命令字,从芯片起始中断号。           【0x28-0x2f】 
        .word   0x00eb,0x00eb
    //【设置 ICW3】设置主从片连接
        mov al,#0x04        ! 8259-1 is master
        out #0x21,al        //送主芯片 ICW3 命令字,主芯片的 IR2 连从芯片 INT.
                            //参见代码列表后的说明。 
        .word   0x00eb,0x00eb
        mov al,#0x02        ! 8259-2 is slave
        out #0xA1,al        //送从芯片 ICW3 命令字,表示从芯片的 INT 连到主芯片
                            //的 IR2 引脚上。 
        .word   0x00eb,0x00eb
    //【设置 ICW4】
        mov al,#0x01        ! 8086 mode for both
        out #0x21,al        //送主片 ICW4 命令字。8086模式: 普通 EOI,非缓冲方式。
                            //需发送指令来复位。初始化结束,芯片就绪。
        .word   0x00eb,0x00eb
        out #0xA1,al        //送从芯片 ICW4 命令字,内容同上。
        .word   0x00eb,0x00eb
    //【设置 OCW1】屏蔽所有中断位
        mov al,#0xFF        ! mask off all interrupts for now
        out #0x21,al        //屏蔽主芯片所有中断请求。
        .word   0x00eb,0x00eb
        out #0xA1,al        //屏蔽从芯片所有中断请求。


    /******************************************************************************************
    **                  进入保护模式,跳转到 head.s 文件中执行
    *****************************************************************************************/
    ! well, that certainly wasn't fun :-(. Hopefully it works, and we don't
    ! need no steenking BIOS anyway (except for the initial loading :-).
    ! The BIOS-routine wants lots of unnecessary data, and it's less
    ! "interesting" anyway. This is how REAL programmers do it.
    !
    ! Well, now's the time to actually move into protected mode. To make
    ! things as simple as possible, we do no register set-up or anything,
    ! we let the gnu-compiled 32-bit programs do that. We just jump to
    ! absolute address 0x00000, in 32-bit protected mode.'"
    /* 哼,上面这段编程当然没劲,但希望这样能工作,而且我们也不再需要乏味的 BIOS
    了(除了初始加载)。BIOS 子程序要求很多不必要的数据,而它一点都没趣。那是"真正"
    的程序员所做的事。

    好了,现在是真正的开始进入保护模式的时候。为了把事情做得尽量简单,我们并不对
    内容进行任何设置。我们让 gnu 编译的 32 位程序去处理这些事。在进入 32 位保
    护模式时我们仅是简单地跳转到绝对地址 0x0000 处。

    下面设置并进入 32 位保护模式式运行。首先加载机器状态字(lmsw - Load Machine Status Word)
    也称控制器寄存器 CR0,其比特位 0 置 1 将导致 CPU 切换到保护模式,并且运行在特权级 0 中,即
    当前特权级 CPL = 0。此时段寄存器仍然指向与实地址模式中相同的线性地址处(在实地址模式下
    线性地址与物理内存地址相同)。在设置该比特位后,随后一条指令必须是一条段间跳转指令以
    用于刷新 CPU 当前指令队列。因为为 CPU 是在执行一条指令之前就已从内存读取该指令并对其进行
    解码。然而在进入保护模式以后那些属于实模式的预先取得的指令信息就变得不再有效。而一条
    段间跳转指令就会刷新 CPU 的当前指令队列,即丢失这些无效信息。另外,在 Intel 公司的手册
    上建议 80386 或以上 CPU 应该使用指令【 mov cr0,ax 】切换到保护模式。lmsw 指令仅用于兼容
    以前的 286 CPU。*/
        mov ax,#0x0001  ! protected mode (PE) bit//保护模式比特位(PE)
        lmsw    ax      ! This is it!           //就这样加载机器状态字
        jmpi    0,8     ! jmp offset 0 of segment 8 (cs)//跳转到 cs 段偏移 0 处


    /* 我们已经将 system 模块移动到 0x0000 开始的地方,所以上句中的偏移地址是 0,而段值 8 已经
    是保护模式下的段选择符了,用于选择描述符表和描述符表项以及所要求的特权级。段选择符长
    度为 16 位(2字节);位 0-1 表示请求的特权级 0-3,Linux 操作系统只用到两级: 0 级(内核
    级)和 3 级(用户级);位 2 用于选择全局描述符表(0)还是局部描述符表(1);位 3-15 是描述
    符表的索引,指出选择第几项描述符。所以段选择符 8 (0b0000 0000 0000 1000)表示请求特
    权级 0 ,使用全局描述符表 GDT 中第 2 个段描述符项,该项指出代码的基地址是 0 (参见200行)
    因为此这里的跳转指令就会去执行 system 中的代码*/



    ! This routine checks that the keyboard command queue is empty
    ! No timeout is used - if this hangs there is something wrong with
    ! the machine, and we probably couldn't proceed anyway.'
    /* 下面这个子程序检查键盘命令队列是否为空。这里不使用超时方法,如果这里死机,则说明
    PC 机有问题,我们就没有办法再处理下去了。*/


    /******************************************************************************************
    **                  判断 8042 缓冲是否为空
    *****************************************************************************************/
    /*
    i8042 有 4 个 8 bits 的寄存器,他们是 Status Register(状态寄存器),Output Buffer(输出缓冲器),Input Buffer(输入缓冲器),Control Register(控制寄存器)。使用两个 IO 端口,60h 和 64h。

    Status Register(状态寄存器)
    状态寄存器是一个8位只读寄存器,任何时刻均可被cpu读取。其各位定义如下
    Bit7: PARITY-EVEN(P_E): 从键盘获得的数据奇偶校验错误
    Bit6: RCV-TMOUT(R_T): 接收超时,置1
    Bit5: TRANS_TMOUT(T_T): 发送超时,置1
    Bit4: KYBD_INH(K_I): 为1,键盘没有被禁止。为0,键盘被禁止。
    Bit3: CMD_DATA(C_D): 为1,输入缓冲器中的内容为命令,为0,输入缓冲器中的内容为数据。
    Bit2: SYS_FLAG(S_F): 系统标志,加电启动置0,自检通过后置1
    Bit1: INPUT_BUF_FULL(I_B_F): 输入缓冲器满置1,i8042 取走后置0
    BitO: OUT_BUF_FULL(O_B_F): 输出缓冲器满置1,CPU读取后置0
    */
    //只有当输入缓冲器为空时(键盘控制器状态寄存器位 1 = 0),才可以对其进行写命令。
    empty_8042:
        .word   0x00eb,0x00eb       //这是两个跳转指令的机器码(跳到下一句),相当于延时操作
        in  al,#0x64    ! 8042 status port  //读 AT 键盘控制器状态寄存器
        test    al,#2   ! is input buffer full?//测试位 1,输入缓冲器满?
        jnz empty_8042  ! yes - loop
        ret
    /* 全局描述符表开始处。描述符表由多个 8 字节长的描述符项组成。这里给出了 3 个描述符项。
    第 1 项无用(206行),但须存在。第 2 项是系统代码段描述符(208-211行),第 3 项是系
    统数据段描述符(213-216行)。*/
    gdt:
        .word   0,0,0,0     ! dummy

    //在 GDT 表中这里的偏移量是 0x08.它是内核代码段选择符的值。
        .word   0x07FF      ! 8Mb - limit=2047 (2048*4096=8Mb)
        .word   0x0000      ! base address=0
        .word   0x9A00      ! code read/exec        //代码段为只读,可执行
        .word   0x00C0      ! granularity=4096, 386//颗粒度为 4096,32位模式

    //在 GDT 表中这里的偏移量是 0x10.它是内核数据段选择符的值。
        .word   0x07FF      ! 8Mb - limit=2047 (2048*4096=8Mb)
        .word   0x0000      ! base address=0
        .word   0x9200      ! data read/write       //数据段为可读可写
        .word   0x00C0      ! granularity=4096, 386//颗粒度为 4096,32位模式

    /* 下面是加载中断描述符表寄存器 idtr 的指令 lidt 要求的 6 字节操作数。前 2 字节是
    IDT 表的限长,后 4 字节是 idt 表在线性地址空间中的 32 位基地址。CPU 要求在进行保护
    模式之前需设置 IDT 表,因此这里先设置一个长度为 0 的空表。*/
    idt_48:
        .word   0           ! idt limit=0
        .word   0,0         ! idt base=0L

    /* 下面是加载中断描述符表寄存器 gdtr 的指令 lgdt 要求的 6 字节操作数。前 2 字节是
    GDT 表的限长,后 4 字节是 gdt 表在线性地址空间中的 32 位基地址。这里全局表长度设置
    为 2KB (0x7ff 即可),因为每 8 字节组成一个段描述符项,所以表中共可用 256 项。4 字
    节的线性基地址为 0x0009<<16 + 0x0200 + gdt, 即 0x90200 + gdt。(符号 gdt 是全局表
    在本程序段中的偏移地址,见 205 行。)*/
    gdt_48:
        .word   0x800       ! gdt limit=2048, 256 GDT entries
        .word   512+gdt,0x9 ! gdt base = 0X9xxxx//【即位置 0x90200+gdt】

    .text
    endtext:
    .data
    enddata:
    .bss
    endbss:

head.s

    /*
     *  linux/boot/head.s
     *
     *  (C) 1991  Linus Torvalds
     */

    /*
     *  head.s contains the 32-bit startup code.
     *
     * NOTE!!! Startup happens at absolute address 0x00000000, which is also where
     * the page directory will exist. The startup code will be overwritten by
     * the page directory.
     */
     /* head.s 含有 32 位启动代码。
     注意!!! 32 位启动代码是从绝对地址 0x00000000 开始的,这里也同样是页目录将存在的地方,
     因此这里的启动代码将被页目录覆盖掉。*/
    .text
    .globl _idt,_gdt,_pg_dir,_tmp_floppy_area

    /******************************************************************************************
    **                  页目录表定义符: 0x00000000 处。
    ** 两级页表:【每页大小 4K = 2^12,总共 32 位地址线,高 10 位页目录,中 10 位页表,低 12 位页偏移】 
            页目录【2^10= 1K个,每个 4 字节,占用 4K,即一页内存】
            页表【2^10= 1K个,每个 4 字节,占用 4K,即一页内存】每页表映射 4M 内存。
    *****************************************************************************************/
    _pg_dir:                    //页目录将会存放在这里,占用了 4K 地址,即 1K 项



    /* 再次注意!!!这里已经处于 32 位运行模式,因此这里的 $0x10 并不是把地址 0x10 装入各个
    段寄存器,它现在其实是全局段描述符中的偏移值,或者更准确地说是一个描述符表项
    的选择符。有关选择符的说明请参见 setup.s 中的说明。这里 $0x10 的含义是请求
    特权级 0 (位 0-1 = 0)、选择全局描述符表(位 2=0)、选择表中第 2 项(位 3-15=2)。它正好指
    向表中的数据段描述符项。(描述符的具体数值参见前面 setup.s 中 212,213 行)
    下面代码的含义是: 设置 ds, es, fs, gs,为 setup.s 中构造的数据段(全局段描述符表第 2 项)
    的选择符 = 0x10,并将堆栈放置在 stack_start 指向的 user_stack 数组区,然后使用本程序
    后面定义的新中断描述符表和全局段描述表。新全局段描述表中初始内容与 setup.s 中的基本
    一样,仅段限长从 8MB 修改成了 16MB. stack_star 定义在 kernel/sched.c,69行。它是指向
     user_stack 数组末端的一个长指。第 23 行设置这里使用的栈,如且称为系统栈。但在移动到
     任务 0 执行(init/main.c 中 137 行)以后该栈就被用作任务 0 和任务 1 共同使用的用户栈了。*/
    /******************************************************************************************
    **                  程序入口处
    *****************************************************************************************/
    startup_32:
        movl $0x10,%eax            // 18-22 行设置各个数据段寄存器
        mov %ax,%ds             // 对于 GNU 汇编,每个直接操作数要以 $ 开始,否则表示地址
        mov %ax,%es             //每个寄存器名都要以 % 开头,eax 表示是 32 位 ax 寄存器。
        mov %ax,%fs             // 0x10 段选择子,选择 setup.s 中设置的 GDT 表第 2 项。
        mov %ax,%gs
        /************************************************************
                设置内核使用的堆栈    
        *************************************************************/
        lss _stack_start,%esp   //表示 _stack_start -->ss:esp,设置系统堆栈。
                                //stack_start 定义在 kernel/sched.c, 69 行
        call setup_idt          //调用设置中断描述符表子程序
        call setup_gdt          //调用设置全局描述符表子程序
        movl $0x10,%eax            # reload all the segment registers//内核数据段
        mov %ax,%ds             # after changing gdt. CS was already
        mov %ax,%es             # reloaded in 'setup_gdt'
        mov %ax,%fs             //因为修改了 gdt,所以需要重新装载所有的段寄存器
        mov %ax,%gs             //cs 代码段寄存器已经在 setup_gdt 中重新加载过了。

    /* 由于段描述符中段限长从 setup.s 中的 8MB 改成了本程序设置的 16MB(见 setup.s 行208-216
    和本程序后面的 235-236行),因此这里再次对所有段寄存器执行加载操作是必须的。另外,通过
    使用 bochs 跟踪观察,如果不对 cs 再次执行加载,那么在执行到 44 行时 cs 代码段不可见部分中
    的限长还是 8MB。这样看来应该重新加载 CS。但是由于 setup.s 中的内核代码段描述符与本程序中
    重新设置的代码段描述符除了段限长以外其余部分完全一样,8MB 的限长在内核初始化阶段不会有
    问题,【而且在以后内核执行过程中段间跳转时会重新加载 cs】。因此这里没有加载它并没有让程序
    出错。
    针对该问题,目前内核中就在第 43 行添加了一条长跳转指令: ljmp $(_KERNEL_CS),#1f 
    跳转到第 44 行来确保 CS 确实又被重新加载。*/
        lss _stack_start,%esp   //stack_start


    /******************************************************************************************
    **                  测试 A20 地址线是否打开了。
    *****************************************************************************************/
    /* 73-77 行用于测试 A20 地址线是否已经开启。采用的方法是向内存地址 0x000000 处写入任意
    一个数值,然后看内存地址 0x100000(1M)处是否也是这个数值。如果一直相同的话,就一直
    比较下去,即死循环、死机。表示地址 A20 线没有选通,结果内核就不能使用 1MB 以上的内存。

    74行上的 1: 是一个局部号构成的标号。标号由符号后跟一个冒号组成。此时该符号表示活动
    位置计数(Active location counter)的当前值,并可以作为指令的操作数。局部符号用于帮助
    编译器和编程人员临时使用一些名称。共有 10 个局部符号名,可在整个程序中重复使用。这些符号
    名使用名称 0、1、、、、9 来引用。为了定义一个局部符号,需把标号写成 N: 形式(其中 N
    表示一个数字)。为了引用先前定义的这个符号,需要写成 Nb ,其中 N 是定义标号进使用的
    数字。为了引用一个局部标号的下一个定义,需要写成 Nf ,这里 N 是 10 个前向引用之一。上面
     b 表示 向后(backwards), f 表示 向前(forwards).在汇编程序的某一处,我们最大可以
     向后/向前 引用 10 个标号(最远第 10 个)*/
        xorl %eax,%eax
    1:  incl %eax           # check that A20 really IS enabled
        movl %eax,0x000000  # loop forever if it isn't'
        cmpl %eax,0x100000
        je 1b               // 1b 表示向后(backward)跳转到标号 1 去(33行)。
                            //若是 5f 则表示向前(forward)跳转到标号 5 去。
    /*
     * NOTE! 486 should set bit 16, to check for write-protect in supervisor
     * mode. Then it would be unnecessary with the "verify_area()"-calls.
     * 486 users probably want to set the NE (#5) bit also, so as to use
     * int 16 for math errors.
     */
     /* 注意! 在下面这段程序中,486 应该将位 16 置位,以检查在超级用户模式下的写保护,
    此后 verify_area() 调用就不需要了。486 的用户通常也会想将 NE(#5)置位,以便
    对数学协处理器的出错使用 int 16.*/

    /* 上面原注释中提到的 486 CPU 中的 CR0 控制寄存器的位 16 是写保护标志 WP(Write-Protect),
    用于禁止超级用户级的程序向一般用户只读页面中进行写操作。该标志主要用于操作系统在创建
    新进程时实现写时复制(copy-on-write)方法。
    下面这段程序(43-65)用于检查数学协处理器芯片是否存在。方法是修改控制寄存器 CR0,在
    假设存在协处理器的情况下执行一个协处理器指令,如果出错的话则说明协处理器芯片不存在,
    需要设置 CR0 中的协处理器仿真位 EM(位 2),并复位协处理器存在标志 MP(位 1)。*/

        movl %cr0,%eax          # check math chip
        andl $0x80000011,%eax  # // Save 【PG,PE,ET】
    /* "orl $0x10020,%eax" here for 486 might be good */
        orl $2,%eax                # set MP
        movl %eax,%cr0
        //检查数字协处理器是否存在并设置它
        call check_x87
        //用来设置页目录表与前 4 个页表来映射 16MB 内存,设置 cr3,开启分页
        jmp after_page_tables       //跳转到 135 行


    /******************************************************************************************
    **                  检查数字协处理器是否存在
    *****************************************************************************************/
    /*
     * We depend on ET to be correct. This checks for 287/387.
       我们依赖于 ET 标志的正确性来检测 287/387 存在与否。
     */
     /* 下面 fninit 和 fstsw 是数学协处理器(80287/80387)的指令。
    finit 向协处理器发出初始化命令,它会把协处理器置于一个未受以前操作系统影响的已知状态,设置
    其控制字为默认值、清除状态字和所有浮点栈式寄存器。非等待形式的这条指令(fninit)还会让
    协处理器终止执行当前正执行的任务先前的算术操作。 fstsw 指令取协处理器的状态字。如果系统
    中存在协处理器的话,那么在执行了 fninit 指令后其状态字低字节肯定为 0.*/
    check_x87:
        fninit
        fstsw %ax
        cmpb $0,%al
        je 1f           /* no coprocessor: have to set bits 【不跳转,没有协处理器】*/
        //没协处理器,设置 EM 模拟位。 
        movl %cr0,%eax
        xorl $6,%eax       /* reset MP, set EM */
        movl %eax,%cr0
        ret

    /* 下面是一汇编语言指示符。其含义是指存储边界对齐调整。"2"表示把随后的代码或数据的偏移位置
    调整到地址值最后 2 比特位为零的位置(2^2),即按 4 字节方式对齐内存地址。不过现在 GNU as
    直接时写对对齐的值而非 2 的次方值了。使用该指示符的目的是为了提高 32 位 CPU 访问内存中代码
    或数据的速度和效率。参见程序后详细说明。
    下面的两个字节值是 80287 协处理器指令 fsetpm 的机器码。其作用是把 80287 设置为保护模式。
    80387 无需该指令,并且将会把该指令看作是空操作。*/
    .align 2
    1:  .byte 0xDB,0xE4     /* fsetpm for 287, ignored by 387 : 287 协处理器码*/
        ret




    /*
     *  setup_idt
     *
     *  sets up a idt with 256 entries pointing to
     *  ignore_int, interrupt gates. It then loads
     *  idt. Everything that wants to install itself
     *  in the idt-table may do so themselves. Interrupts
     *  are enabled elsewhere, when we can be relatively
     *  sure everything is ok. This routine will be over-
     *  written by the page tables.
     */
     /* 下面这段是设置中断描述符表子程序 setup_idt 

    将中断描述符表 idt 设置成具有 256 个项,并都指向 ignore_int 中断门。然后加载中断
    描述符表寄存器(用 lidt 指令)。真正实用的中断门以后再安装。当我们在其他地方认为一切
    都正常时再开启中断。该子程序将会被页表覆盖掉。

        中断描述符表中的项虽然也是 8 字节组成,但其格式与全局表中的不同,被称为门描述符
    (Gate Descriptor)。它的 0-1,6-7 字节是偏移量, 2-3 字节是选择符,4-5 字节是一些标志。
    这段代码首先在 edx,eax 中组合设置出 8 字节默认的中断描述符值,然后在 idt 表每一项中
    都放置该描述符,共 256 项。eax 含有描述符低 4 字节,edx 含有高 4 字节。内核在随后的初始
    化过程中会替换安装那些真正实用的中断描述符项。*/
    /******************************************************************************************
    **                  设置 IDT 表,开始只是使用 ignore_int 填充 256 项的
    *****************************************************************************************/
    setup_idt:
        lea ignore_int,%edx  //将 ignore_int 的有效地址(偏移值)值 -> edx 寄存器
        movl $0x00080000,%eax//将选择符 0x0008 置入 eax 的高 16 位中。
        movw %dx,%ax        /* selector = 0x0008 = cs */
                            /* 偏移值的低 16 位置入 eax 的低 16 位中。此时 eax 含有
                               门描述符低 4 字节的值。*/

        movw $0x8E00,%dx   /* interrupt gate - dpl=0, present */
                            /* 此时 edx 含有门描述符高 4 字节的值。*/

        lea _idt,%edi       //_idt 是中断门描述符表的地址。
        mov $256,%ecx
    rp_sidt:
        movl %eax,(%edi)    //将哑中断门描述符存入表中
        movl %edx,4(%edi)   //eax 内容放到 edi+4 所指内存位置处。
        addl $8,%edi       //edi 指向表中下一项。
        dec %ecx
        jne rp_sidt
        lidt idt_descr      //加载中断描述符表寄存器值
        ret

    /*
     *  setup_gdt
     *
     *  This routines sets up a new gdt and loads it.
     *  Only two entries are currently built, the same
     *  ones that were built in init.s. The routine
     *  is VERY complicated at two whole lines, so this
     *  rather long comment is certainly needed :-).
     *  This routine will beoverwritten by the page tables.
     */
     /* 设置全局描述符表项 setup_gdt
    这个子程序设置一个新的全局描述符表 gdt,并加载。此时仅创建了两个表项,与前
    面的一样。该子程序只有两行, 非常的 复杂,所以当然需要这么长的注释了。
    该子程序将页表覆盖掉。*/
    /******************************************************************************************
    **                  设置 GDT 表,只是使用 lgdt 装入 GDT 表位置即可
    *****************************************************************************************/
    setup_gdt:
        lgdt gdt_descr     //加载全局描述符表寄存器(内容已设置好,见234-238行)
        ret

    /******************************************************************************************
    **                  以下代码为 4 个页表占位
    *****************************************************************************************/
    /*
     * I put the kernel page tables right after the page directory,
     * using 4 of them to span 16 Mb of physical memory. People with
     * more than 16MB will have to expand this.
     */
     /* Linus 将内核的内存页表直接放在页目录之后,使用 4 个表来寻址 16MB 的物理内存。
    如果你有多于 16MB 的内存,就需要在这里进行扩充修改。

    每个页表长为 4 Kb 字节(1页内存页面),而每个页表项需要 4 个字节,因此一个页表可以存放
    1024 个表项。如果一个页表项寻址 4KB 的地址空间,则一个页表就可以寻址 4MB 的物理内存。
    页表项的格式为: 项的前 0-11 位存放一些标志,例如是否在内存中(P位0)、读写许可(R/W 位 1)、
    普通用户还是超级用户使用(U/S 位 2)、是否修改过(是否脏了)(D位 6)等; 表项的位 12-31 是
    页框地址,用于指出一页内存的物理起始地址。*/

    .org 0x1000         //从偏移 0x1000 处开始是第 1 个页表(偏移 0 开始处将存放页表目录)。
    pg0:

    .org 0x2000
    pg1:

    .org 0x3000
    pg2:

    .org 0x4000
    pg3:

    .org 0x5000         //定义下面的内存数据块从偏移 0x5000 处开始。


    /*
     * tmp_floppy_area is used by the floppy-driver when DMA cannot
     * reach to a buffer-block. It needs to be aligned, so that it isn't
     * on a 64kB border.
     */
    /**************************************************************
    *               软盘缓冲区
    ***************************************************************/
    /* 当 DMA (直接存储访问)不能访问缓冲块时,下面的 tmp_floppy_area 内存块
    就可供软盘驱动程序使用。其地址需要对齐调整,这样就不会跨越 64KB 边界。*/
    _tmp_floppy_area:
        .fill 1024,1,0  //共保留 1024 项,每项 1 字节,填充数值 0.

    /* 下面这几个入栈操作用于为跳转到 init/main.c 中的 main() 函数作准备工作。第 263 行上
    的指令在栈中压入了返回地址,而第 264 行则压入了 main()函数代码的地址。当 head.s
    最后在第 395 行执行 ret 指令时就会弹出 main() 的地址,并把控制权转移到 init/main.c
    程序中。【参见第 3 章中有关 C 函数调用机制的说明。】
    【前面 3 个入栈 0 值 应该分别表示 envp、argv 指针和 argc 的值,】但 main() 没有用到。
    139 行的入栈操作是模拟调用 main.c 程序时首先将返回地址入栈的操作,所以如果
    main.c 程序真的退出时,就会返回到这里的标号 L6 处继续执行下去,也即死循环。
    140 行将 main.c 的地址压入堆栈,这样,在设置分页处理(setup_paging)结束后
    执行 ret 返回指令时就会将 main.c 程序的地址弹出堆栈,并去执行 main.c 程序了。
    有关 C 函数调用机制请参见程序后的说明。*/
    /******************************************************************************************
    **                  设置 main() 返回的栈以及设置页表什么的。
    *****************************************************************************************/
    after_page_tables:
        pushl $0       # These are the parameters to main :-)
        pushl $0       //这些是调用 main() 程序的参数(init/main.c)
        pushl $0       //其中 $ 符号表示这是一个立即操作数。
        pushl $L6      # return address for main, if it decides to.
        pushl $_main   //_main 是编译程序对 main 的内部表示法
        jmp setup_paging//跳转至第 358 行。
    L6:
        jmp L6          # main should never return here, but
                        # just in case, we know what happens.
                        //main 程序绝对不应该返回到这里。不过为了防止万一,
                        //所以添加了该语句。这样我们就知道发生什么问题了。



    /******************************************************************************************
    **                  默认的中断处理程序,主要使用 printk 输出 Unknown interrupt\n\r
    *****************************************************************************************/
    /* This is the default interrupt "handler" :-) */
    /* 下面是默认的中断【向量句柄】*/
    int_msg:
        .asciz "Unknown interrupt\n\r"  //定义字符串‘未知中断(回车换行)’
    .align 2            //按 4 字节方式对齐内存地址。
    ignore_int:
        pushl %eax
        pushl %ecx
        pushl %edx
        push %ds        //这里请注意! ds,es, fs, gs 等虽然是 16 位寄存器,但入栈后
                        //仍然会以 32 位的形式入栈,也即需要占用 4 个字节的堆栈空间。


        push %es
        push %fs
        movl $0x10,%eax//置段选择符(使 ds, es, fs 指向 gdt 表中的数据段)。
        mov %ax,%ds
        mov %ax,%es
        mov %ax,%fs
        pushl $int_msg//把调用 printk 函数的参数指针(地址)入栈。注意!若符号 int_msg
                      //前不加 $ ,则表示把 int_msg 符号处的长字('Unkn')入栈。
        call _printk  //该函数在 /kernel/printk.c 中
                      //_printk 是 printk 编译后模块中的内部表示法。
        popl %eax
        pop %fs
        pop %es
        pop %ds
        popl %edx
        popl %ecx
        popl %eax
        iret          //中断返回(把中断调用时压入栈的 CPU 标志寄存器(32位)值也弹出)。


    /*
     * Setup_paging
     *
     * This routine sets up paging by setting the page bit
     * in cr0. The page tables are set up, identity-mapping
     * the first 16MB. The pager assumes that no illegal
     * addresses are produced (ie >4Mb on a 4Mb machine).
     *
     * NOTE! Although all physical memory should be identity
     * mapped by this routine, only the kernel page functions
     * use the >1Mb addresses directly. All "normal" functions
     * use just the lower 1Mb, or the local data space, which
     * will be mapped to some other place - mm keeps track of
     * that.
     *
     * For those with more memory than 16 Mb - tough luck. I've
     * not got it, why should you :-) The source is here. Change
     * it. (Seriously - it shouldn't be too difficult. Mostly
     * change some constants etc. I left it at 16Mb, as my machine
     * even cannot be extended past that (ok, but it was cheap :-)
     * I've tried to show which constants to change by having
     * some kind of marker at them (search for "16Mb"), but I
     * won't guarantee that's all :-( )
     */
     /* 这个子程序通过设置控制寄存器 cr0 的标志(PG 位 31)来启动对内存的分页处理功能,
    并设置各个页表项的内容,以恒等映射前 16MB 的物理内存。分页器假定不会产生非法的
    地址映射(也即在只有 4Mb 的机器上设置出大于 4Mb 的内存地址)。

    注意!尽管所有的物理地址都应该由这个子程序进行恒等映射,但只有【内核页面管理函数能
    直接使用 > 1Mb 的地址】。所有"普通"函数仅使用低于 1Mb 的地址空间,或者是使用局部数据
    空间,该地址空间将被映射到其他一些地方去 --mm (内存管理程序)会管理这些事的。

    对于那些有多于 16Mb 内存的家伙 - 真是太幸运了,我还没有,为什么你会有。代码就在
    这里,对它进行修改吧。(实际上,这并不太困难的。通常只需修改一些常数等。我把它设置
    为 16Mb,因为我的机器再怎么扩充甚至不能超过这个界限(当然,我的机器是很便宜的)。
    我已经通过设置某类标志来给需要改动的地方(搜索 16Mb),但我不能保证这些改动就行了)。*/
    /*

      上面英文注释第 2 段的含义是指在机器物理内存中大于 1MB 的内存空间主要被用于主内存区。
    主内存区空间由 mm 模块管理。它涉及到页面映射操作。【内核中所有其他函数就是这里指的一般
    (普通)函数】。若要使用主内存区的页面,就需要使用 get_free_page() 等函数获取。因为主内
    存区中内存页面是共享资源,必须有程序进行统一管理以避免资源争用和竞争。

    在内存物理地址 0x0 处开始存放 1 页页目录表和 4 页页表。页目录表是系统所有进程公用的,而
    这里的 4 页页表则属于内核专用,它们一一映射线性地址起始 16MB 空间范围到物理内存上。对于
    新的进程,系统会在主内存区为其申请页面存放页表。另外,1 页内存长度是 4096 字节。*/

    /******************************************************************************************
    **              设置页目录表与前 4 项页表来映射 16MB 内存
    *****************************************************************************************/
    /* This is the default interrupt "handler" :-) */
    .align 2                    //按 4 字节方式对齐内存地址边界。
    setup_paging:               

        //【首先对 5 页内存(1 页目录 + 4 页页表)清零。】
        movl $1024*5,%ecx      /* 5 pages - pg_dir+4 page tables */
        xorl %eax,%eax
        xorl %edi,%edi          /* pg_dir is at 0x000 */
                                //页目录从 0x000 地址开始
        cld;rep;stosl           // eax 内容存到 es:edi 所指内存位置处,且 edi 增 4.

    /* 下面 4 句设置页目录表中的项,因为我们(内核)共有 4 个页表所以只需设置 4 项。
    页目录项的结构与页表中项的结构一样,4 个字节为 1 项。参见上面 217 行下的说明。
    例如 $pg0+7 表示: 0x00001007 ,是页目页表中的第 1 项。
    而第 1 个页表所在的地址 = 0x000010007 & 0xfffff0000 = 0x1000;
    第 1 个页表的属性标志 = 0x00001007 & 0x00000fff = 0x07 ,表示该页存在,用户可读写。*/

        //【设置页目录项的前 4 项】
        movl $pg0+7,_pg_dir            /* set present bit/user r/w *///【+ 7 表示属性】
        movl $pg1+7,_pg_dir+4      /*  --------- " " --------- */
        movl $pg2+7,_pg_dir+8      /*  --------- " " --------- */
        movl $pg3+7,_pg_dir+12     /*  --------- " " --------- */

    /* 下面 6 行填写 4 个页表中所有项的内容,共有: 4(页表) * 1024(项/页表) = 4096 项(0-0xfff),
    也即能映射物理内存 4096 * 4Kb = 16Mb。
    每项的内容是: 当前项所映射的物理内存地址 + 该页的标志(这里均为 7)。
      使用的方法是从最后一个页表的最后一项开始倒退顺序填写。一个页表的最后一项在页表中的
    位置是 1023 * 4 = 4092。因此最后一页的最后一项位置就是 $pg3 + 4092。*/

        //【初始化 4 个页表中的各项,用来映射 16MB 的内存值】
        movl $pg3+4092,%edi        //edi --> 最后一页的最后一项。
        movl $0xfff007,%eax        /*  16Mb - 4096 + 7 (r/w user,p) */
                                //最后 1 项对应物理内存页面的地址是 0xfff000,
                                //加上属性标志 7,即为 0xfff007。
        std                     //方向位属性,edi 值递减(4 字节)。
    1:  stosl                   /* fill pages backwards - more efficient :-) */
        subl $0x1000,%eax      //每填写好一项,物理地址值就减 0x1000。
        jge 1b                  //如果小于 0 ,则说明全添写好了。

    //设置页目录表基地址寄存器 cr3 的值,指向页目录表。cr3中保存的是页目录表的物理地址。
        //【设置 cr3 保存页目录表的物理地址 0x0000 】
        xorl %eax,%eax      /* pg_dir is at 0x0000 *///页目录表在 0x0000 处。
        movl %eax,%cr3      /* cr3 - page directory start */

    //设置启动使用分页处理(cr0 的 PG 标志,位 31)
        //【启动分页设置】
        movl %cr0,%eax
        orl $0x80000000,%eax//添上 PG 标志
        movl %eax,%cr0      /* set paging (PG) bit */

        /********************************************************
        **          此处返回去 main 函数。/init/main.c
        *********************************************************/
        ret                 /* this also flushes prefetch-queue */

    /* 在改变分页处理标志后要求使用转移指令刷新预取指令队列,这里用的是返回指令 ret。
    该返回指令的另一个作用是将 277 行压入堆栈中的 main 程序的地址弹出,并跳转到 /init/main.c
    程序去运行。本程序就到此真正结束了。*/


    .align 2                //按 4 字节方式对齐内存地址边界
    .word 0                 //这里先空出 2 字节,这样 224 行上的长字是 4 字节对齐的。


    /******************************************************************************************
    **                  lidt 加载中断描述符表需要操作的 6 字节数据
    *****************************************************************************************/
    /* 下面是加载中断描述符表寄存器 idtr 的指令 lidt 要求的 6 字节操作数。前 2 字节是 idt 表的限长,
    后 4 字节是 idt 表在线性地址空间中的 32 位基地址。*/
    idt_descr:
        .word 256*8-1       # idt contains 256 entries//共 256 项,限长 = 长度 -1.
        .long _idt          //32位地址
    .align 2
    .word 0


    /******************************************************************************************
    **                  lgdt 加载中断描述符表需要操作的 6 字节数据
    *****************************************************************************************/
    /* 下面加载全局描述符表寄存器 gdtr 的指令 lgdt 要求的 6 字节操作数。前 2 字节是 gdt 表的限长,
    后 4 字节是 gdt 表的线性基地址。这里全局表长度设置为 2KB 字节(0x7ff 即可),因为每 8 字节
    组成一个描述符项,所以表中共可有 256 项。符号 _gdt 是全局表在本程序中的偏移位置,见 234 行。*/
    gdt_descr:
        .word 256*8-1       # so does gdt (not that that's any'
        .long _gdt      # magic number, but it works for me :^)
        .align 3            //按 8 (2^3)字节方式对齐内存地址边界。

    /******************************************************************************************
    **                  IDT 中断描述符表位置【256 项】
    *****************************************************************************************/
    _idt:   .fill 256,8,0   # idt is uninitialized//共 256 项,每项 8 字节,填 0.



    /******************************************************************************************
    **                  GDT 全局描述符表位置【256 项】
    *****************************************************************************************/
    /* 全局表。前 4 项分别是空项(不用)、代码段描述符、数据段描述符、系统调用段描述符,其中
    系统调用段描述符并没有派用处,Linus 当时可能曾想把系统调用代码专门放在这个独立的段中。
    后面还预留了 252 项的空间,用于放置所创建任务的局部描述符(LDT)和对应的任务状态段 TSS 
    的描述符。
    (0-nul, 1-cs, 2-ds, 3-syscall, 4-TSS0, 5-LDTO, 6-TSS1, 7-LDT1, 8-TSS2 etc.... */
    _gdt:   .quad 0x0000000000000000    /* NULL descriptor */
        .quad 0x00c09a0000000fff    /* 16Mb */  //0x08,内核代码段最大长度 16MB 可读可执行,0x00000开始
        .quad 0x00c0920000000fff    /* 16Mb */  //0x10,内核数据段最大长度 16MB 可读可写,0x00000开始
        .quad 0x0000000000000000    /* TEMPORARY - don't use */
        .fill 252,8,0           /* space for LDT's and TSS's etc *///预留空间。

main.c



    /*
     * linux/init/main.c
     *
     * (C) 1991 Linus Torvalds
     */

    #define __LIBRARY__     // 定义该变量是为了包括定义在unistd.h 中的内嵌汇编代码等信息。
    #include <unistd.h>     // *.h 头文件所在的默认目录是include/,则在代码中就不用明确指明位置。
    // 如果不是UNIX 的标准头文件,则需要指明所在的目录,并用双引号括住。
    // 标准符号常数与类型文件。定义了各种符号常数和类型,并申明了各种函数。
    // 如果定义了__LIBRARY__,则还包括系统调用号和内嵌汇编代码_syscall0()等。
    #include <time.h>       // 时间类型头文件。其中最主要定义了tm 结构和一些有关时间的函数原形。

    /*
     * we need this inline - forking from kernel space will result
     * in NO COPY ON WRITE (!!!), until an execve is executed. This
     * is no problem, but for the stack. This is handled by not letting
     * main() use the stack at all after fork(). Thus, no function
     * calls - which means inline code for fork too, as otherwise we
     * would use the stack upon exit from 'fork()'.
     *
     * Actually only pause and fork are needed inline, so that there
     * won't be any messing with the stack from main(), but we define
     * some others too.
     */
    /*
     * 我们需要下面这些内嵌语句 - 从内核空间创建进程(forking)将导致没有写时复制(COPY ON WRITE)!!!
     * 直到一个执行execve 调用。这对堆栈可能带来问题。处理的方法是在fork()调用之后不让main()使用
     * 任何堆栈。因此就不能有函数调用 - 这意味着fork 也要使用内嵌的代码,否则我们在从fork()退出
     * 时就要使用堆栈了。
     * 实际上只有pause 和fork 需要使用内嵌方式,以保证从main()中不会弄乱堆栈,但是我们同时还
     * 定义了其它一些函数。
     */
    //
    //              此处使用宏定义定义了 fork,pause,setup,sync 等函数
    /
    static inline
    _syscall0 (int, fork)       // 是unistd.h 中的内嵌宏代码。以嵌入汇编的形式调用
        // Linux 的系统调用中断0x80。该中断是所有系统调用的
        // 入口。该条语句实际上是int fork()创建进程系统调用。
        // syscall0 名称中最后的0 表示无参数,1 表示1 个参数。

    static inline _syscall0 (int, pause)    // int pause()系统调用:暂停进程的执行,直到
        // 收到一个信号。

    static inline _syscall1 (int, setup, void *, BIOS)  // int setup(void * BIOS)系统调用,仅用于
        // linux 初始化(仅在这个程序中被调用)。

    static inline _syscall0 (int, sync) // int sync()系统调用:更新文件系统。【sys_sync】




    #include <linux/tty.h>      // tty 头文件,定义了有关tty_io,串行通信方面的参数、常数。
    #include <linux/sched.h>    // 调度程序头文件,定义了任务结构task_struct、第1 个初始任务
        // 的数据。还有一些以宏的形式定义的有关描述符参数设置和获取的
        // 嵌入式汇编函数程序。
    #include <linux/head.h>     // head 头文件,定义了段描述符的简单结构,和几个选择符常量。
    #include <asm/system.h>     // 系统头文件。以宏的形式定义了许多有关设置或修改
        // 描述符/中断门等的嵌入式汇编子程序。
    #include <asm/io.h>     // io 头文件。以宏的嵌入汇编程序形式定义对io 端口操作的函数。
    #include <stddef.h>     // 标准定义头文件。定义了NULL, offsetof(TYPE, MEMBER)。
    #include <stdarg.h>     // 标准参数头文件。以宏的形式定义变量参数列表。主要说明了-个
        // 类型(va_list)和三个宏(va_start, va_arg 和va_end),vsprintf、
        // vprintf、vfprintf。
    #include <unistd.h>
    #include <fcntl.h>      // 文件控制头文件。用于文件及其描述符的操作控制常数符号的定义。
    #include <sys/types.h>      // 类型头文件。定义了基本的系统数据类型。
    #include <linux/fs.h>       // 文件系统头文件。定义文件表结构(file,buffer_head,m_inode 等)。
         static char printbuf[1024];    // 静?址椤?

         extern int vsprintf ();    // 送格式化输出到一字符串中(在kernel/vsprintf.c,92 行)。
         extern void init (void);   // 函数原形,初始化(在168 行)。
         extern void blk_dev_init (void);   // 块设备初始化子程序(kernel/blk_drv/ll_rw_blk.c,157 行)
         extern void chr_dev_init (void);   // 字符设备初始化(kernel/chr_drv/tty_io.c, 347 行)
         extern void hd_init (void);    // 硬盘初始化程序(kernel/blk_drv/hd.c, 343 行)
         extern void floppy_init (void);    // 软驱初始化程序(kernel/blk_drv/floppy.c, 457 行)
         extern void mem_init (long start, long end);   // 内存管理初始化(mm/memory.c, 399 行)
         extern long rd_init (long mem_start, int length);  //虚拟盘初始化(kernel/blk_drv/ramdisk.c,52)
         extern long kernel_mktime (struct tm *tm); // 建立内核时间(秒)。
         extern long startup_time;  // 内核启动时间(开机时间)(秒)。

        /*
         * This is set up by the setup-routine at boot-time
         */
        /*
         * 以下这些数据是由setup.s 程序在引导时间设置的(参见第2 章2.3.1 节中的表2.1)。
         */
    #define EXT_MEM_K (*(unsigned short *)0x90002)  // 1M 以后的扩展内存大小(KB)。
    #define DRIVE_INFO (*(struct drive_info *)0x90080)  // 硬盘参数表基址。
    #define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)  // 根文件系统所在设备号。

        /*
         * Yeah, yeah, it's ugly, but I cannot find how to do this correctly
         * and this seems to work. I anybody has more info on the real-time
         * clock I'd be interested. Most of this was trial and error, and some
         * bios-listing reading. Urghh.
         */
        /*
         * 是啊,是啊,下面这段程序很差劲,但我不知道如何正确地实现,而且好象它还能运行。如果有
         * 关于实时时钟更多的资料,那我很感兴趣。这些都是试探出来的,以及看了一些bios 程序,呵!
         */

    #define CMOS_READ(addr) ({ \    // 这段宏读取CMOS 实时时钟信息。
    outb_p (0x80 | addr, 0x70);
    \               // 0x70 是写端口号,0x80|addr 是要读取的CMOS 内存地址。
      inb_p (0x71);
    \               // 0x71 是读端口号。
    }

    )
    // BCD 码: 一个字节,高 4 位存一个数,低 4 位存一个数。
    #define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)  // 将BCD 码转换成数字。 15 = 1111 刚好低 4 位。


    /* 该函数的主要作用是读取 CMOS 时钟,然后通过获得从 1970年 1 月 1 日到今天的秒数,设置 startup_time 全局变量
    输入参数: 无
    返回参数: 无*/
    static void time_init (void)    // 该子程序取CMOS 时钟,并设置开机时间??startup_time(秒)。
    {
           struct tm time;

           do
         {
           time.tm_sec = CMOS_READ (0); //参见后面CMOS 内存列表。【7.1.3.1 CMOS 信息】
           time.tm_min = CMOS_READ (2); //【】
           time.tm_hour = CMOS_READ (4);//【】
           time.tm_mday = CMOS_READ (7);//【】
           time.tm_mon = CMOS_READ (8);//【】
           time.tm_year = CMOS_READ (9);//【】
         }
           while (time.tm_sec != CMOS_READ (0));//貌似要在下一秒才能出来,如果相等则一直读,时钟不走?

           // BCD 码: 一个字节,高 4 位存一个数,低 4 位存一个数。
           BCD_TO_BIN (time.tm_sec);
           BCD_TO_BIN (time.tm_min);
           BCD_TO_BIN (time.tm_hour);
           BCD_TO_BIN (time.tm_mday);
           BCD_TO_BIN (time.tm_mon);
           BCD_TO_BIN (time.tm_year);
           time.tm_mon--;           //月份 芯片是 1-12,系统是 0-11 ,所以要减

           // 该函数计算从1970 年1 月1 日0 时起到开机当日经过的秒数,作为开机时间。
           //返回参数: 就是秒数。
           startup_time = kernel_mktime (&time);
    }

    static long memory_end = 0;         // 机器具有的内存(字节数)。
    static long buffer_memory_end = 0;  // 高速缓冲区末端地址。
    static long main_memory_start = 0;  // 主内存(将用于分页)开始的位置。

    struct drive_info
    {
      char dummy[32];
    }

    drive_info;         // 用于存放硬盘参数表信息。


    /********************************************************************************************
    **                  程序入口处: main.c
    ********************************************************************************************/
    void main (void)        /* This really IS void, no error here. */
    {               /* The startup routine assumes (well, ...) this */
      /* 这里确实是void,并没错。在startup 程序(head.s)中就是这样假设的。 */
      // 参见head.s 程序第136 行开始的几行代码。
      /*
       * Interrupts are still disabled. Do necessary setups, then
       * enable them
       */
      /*
       * 【此时中断仍被禁止着,做完必要的设置后就将其开启。】
       */

    /********************************************************************************************
                                复制根设备号和硬盘参数表,进行物理内存规划
    **********************************************************************************************/ 
      // 下面这段代码用于保存:
      // 根设备号 --> ROOT_DEV; 高速缓存末端地址 --> buffer_memory_end;
      // 机器内存数 --> memory_end;主内存开始地址 --> main_memory_start;
      ROOT_DEV = ORIG_ROOT_DEV;
      drive_info = DRIVE_INFO;//【结构体可以直接赋值的。】
      memory_end = (1 << 20) + (EXT_MEM_K << 10);   // 内存大小=1Mb 字节+扩展内存(k)*1024 字节。
      memory_end &= 0xfffff000; // 忽略不到4Kb(1 页)的内存数。

      //设置内存结束地址
      if (memory_end > 16 * 1024 * 1024)    // 如果内存超过16Mb,则按16Mb 计。
        memory_end = 16 * 1024 * 1024;

      //设置缓存地址
      if (memory_end > 12 * 1024 * 1024)    // 如果内存>12Mb,则设置缓冲区末端=4Mb
        buffer_memory_end = 4 * 1024 * 1024;
      else if (memory_end > 6 * 1024 * 1024)    // 否则如果内存>6Mb,则设置缓冲区末端=2Mb
        buffer_memory_end = 2 * 1024 * 1024;
      else
        buffer_memory_end = 1 * 1024 * 1024;    // 否则则设置缓冲区末端=1Mb

      main_memory_start = buffer_memory_end;    // 主内存起始位置=缓冲区末端;

    /********************************************************************************************
                                虚拟盘清0
    **********************************************************************************************/
      // 如果定义了虚拟盘,则主内存将减少。此函数只是将虚拟盘内存清 0 了,然后返回虚拟盘长度
    #ifdef RAMDISK          
      main_memory_start += rd_init (main_memory_start, RAMDISK * 1024);
    #endif

      // 以下是内核进行所有方面的初始化工作。阅读时最好跟着调用的程序深入进去看,实在看
      // 不下去了,就先放一放,看下一个初始化调用 -- 这是经验之谈?。

    /********************************************************************************************
                                内存管理结构 mem_map 初始化
    **********************************************************************************************/
      /* 内存映射初始化程序,系统会初始化主内存,使用情况保存在 映射表 mem_map[页号] 中,一项对应一页
      传入参数: 主内存开始地址,主内存结束地址
      返回参数: 无*/
      mem_init (main_memory_start, memory_end);

    /********************************************************************************************
                                中断向量表设置
    **********************************************************************************************/
      /* 此函数用于设置 IDT 相应位置的中断异常函数用的【4.6】
      传入参数: 无。
      返回参数: 无。*/
      trap_init ();         // 陷阱门(硬件中断向量)初始化。(kernel/traps.c,181 行)

    /********************************************************************************************
                                初始化块设备请求项结构
    **********************************************************************************************/
      /* 块设备初始化程序,只有将 request[NR_REQUEST] 中的相应项设置了一下
      传入参数: 无。
      返回参数: 无。*/
      blk_dev_init ();      // 块设备初始化。 (kernel/blk_dev/ll_rw_blk.c,157 行)

    /********************************************************************************************
                                初始化字符设备(当前未使用)
    **********************************************************************************************/
       字符设备初始化函数。空,为以后扩展做准备。
      chr_dev_init ();      // 字符设备初始化。 (kernel/chr_dev/tty_io.c,347 行)

    /********************************************************************************************
                                初始化 tty 串口终端与控制台终端以及键盘
    **********************************************************************************************/
       tty 终端初始化函数。
      // 初始化串口终端和控制台终端。
      tty_init ();          // tty 初始化。 (kernel/chr_dev/tty_io.c,105 行)

    /********************************************************************************************
                                开机启动时间设置
    **********************************************************************************************/
      /* 该函数的主要作用是读取 CMOS 时钟,然后通过获得从 1970 年 1 月 1 日到今天的秒数,设置 startup_time 全局变量
      输入参数: 无
      返回参数: 无*/
      time_init ();         // 设置开机启动时间startup_time(见76 行)。

    /********************************************************************************************
                                开始初始化进程 0 以及其他操作
    **********************************************************************************************/
      /* 调度程序的初始化子程序。
        该函数主要初始化了 任务0 的 LDT与TSS,并清 0 了其他 GDT 中未使用的项,同时清了 NT 位
      加载了任务 0 的 TSS 与 LDT,并设置了 8253 定时器及其中断处理程序,以及 系统调用中断程序
      传入参数: 无
      返回参数: 无*/
      sched_init ();        // 调度程序初始化(加载了任务0 的tr, ldtr) (kernel/sched.c,385)

    /********************************************************************************************
                                初始化缓冲区与哈希表
    **********************************************************************************************/
      /* 缓冲区初始化工作,用于建立内存双链环形链表,具体结构见【图 12-17 】,并初始化了哈希表*/ 
      buffer_init (buffer_memory_end);  // 缓冲管理初始化,建内存链表等。(fs/buffer.c,348)

    /********************************************************************************************
                                初始化硬盘
    **********************************************************************************************/
      // 硬盘系统初始化,主要设置了硬盘的操作函数,中断门向量,以及允许了硬盘中断
      hd_init ();           // 硬盘初始化。 (kernel/blk_dev/hd.c,343 行)

       软盘系统初始化。
      // 设置软盘块设备的请求处理函数(do_fd_request()),并设置软盘中断门(int 0x26,对应硬件
      // 中断请求信号IRQ6),然后取消对该中断信号的屏蔽,允许软盘控制器FDC 发送中断请求信号。
      floppy_init ();       // 软驱初始化。 (kernel/blk_dev/floppy.c,457 行)

    /********************************************************************************************
                                开中断
    **********************************************************************************************/  
      sti ();           // 所有初始化工作都做完了,开启中断。


    /********************************************************************************************
                                任务 0 开始在用户态运行了
    **********************************************************************************************/  
      // 下面过程通过在堆栈中设置的参数,利用中断返回指令切换到任务0,任务 0 即本段程序后面的死循环部分
      // 与上面不同的就是在这个函数之后,系统就运行在了 特权 3 模式中了。
      move_to_user_mode ();     // 移到用户模式。 (include/asm/system.h,第1 行)

      // main() 在移动到用户模式(到任务0)后执行内嵌方式的 fork()和 pause(),因此可保证不使用任务0的用户栈
      //【使用跟任务控制数据结构在一页的末端内核栈。】

    /********************************************************************************************
                                任务 1 运行此处代码
    **********************************************************************************************/  
      // 注意: 任务 1 与任务 0 的代码数据什么的其实是一致的,而这里只有任务 1 调用的,因为 fork 的返回值不同。
      //调用完 fork 这后,新的任务进程就建立了, 若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1
      if (!fork ())     // 【fork 定义在了本文件开始处,使用宏定义定义的。】  switch_to
        {               /* we count on this going ok */
          init ();
        }


    /********************************************************************************************
                                任务 0 运行此处代码
    **********************************************************************************************/  
      /*
       * NOTE!! For any other task 'pause()' would mean we have to get a
       * signal to awaken, but task0 is the sole exception (see 'schedule()')
       * as task 0 gets activated at every idle moment (when no other tasks
       * can run). For task0 'pause()' just means we go check if some other
       * task can run, and if not we return here.
       */
      /* 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到一个信号才会返
       * 回就绪运行态,但任务0(task0)是唯一的意外情况(参见'schedule()'),因为任务0 在
       * 任何空闲时间里都会被激活(当没有其它任务在运行时),因此对于任务0'pause()'仅意味着
       * 我们返回来查看是否有其它任务可以运行,如果没有的话我们就回到这里,一直循环执行'pause()'。
       */
      for (;;)
        pause ();
    }

    static int printf (const char *fmt, ...)
        // 产生格式化信息并输出到标准输出设备stdout(1),这里是指屏幕上显示。参数'*fmt'指定输出将
        // 采用的格式,参见各种标准C 语言书籍。该子程序正好是vsprintf 如何使用的一个例子。
        // 该程序使用vsprintf()将格式化的字符串放入printbuf 缓冲区,然后用write()将缓冲区的内容
        // 输出到标准设备(1--stdout)。
    {
      va_list args;
      int i;

        va_start (args, fmt);// args 指向 fmt 参数最后一个
        write (1, printbuf, i = vsprintf (printbuf, fmt, args));
        va_end (args);
        return i;
    }

    static char *argv_rc[] =
    {
    "/bin/sh", NULL};       // 调用执行程序时参数的字符串数组。
    static char *envp_rc[] =
    {
    "HOME=/", NULL};        // 调用执行程序时的环境字符串数组。

    static char *argv[] =
    {
    "-/bin/sh", NULL};      // 同上。
    static char *envp[] =
    {
    "HOME=/usr/root", NULL};



    /********************************************************************************************
                                任务 1 开始运行了
    **********************************************************************************************/  
    void init (void)
    {
      int pid, i;

    /  
      // 读取硬盘参数包括分区表信息并建立虚拟盘和安装根文件系统设备。
      // 该函数是在25 行上的宏定义的,对应函数是 sys_setup(),在kernel/blk_drv/hd.c,71 行。
    /
      // 本函数主要功能是读取CMOS 和硬盘参数表信息,用于设置硬盘分区结构hd,并加载RAM 虚拟盘和
      // 根文件系统。
        setup ((void *) &drive_info);//在本文件的首部定义的,使用的是宏定义。

    //
         打开文件函数。 sys_open
        // 打开并有可能创建一个文件。
        // 参数:filename - 文件名;flag - 文件打开标志;...
        // 返回:文件描述符【fd 即在 current->filp[fd]  数组中的位置】,若出错则置出错码,并返回-1。
        (void) open ("/dev/tty0", O_RDWR, 0);   // 用读写访问方式打开设备“/dev/tty0”,

        // 这里对应终端控制台。sys_dup
        // 返回的句柄号0 -- stdin 标准输入设备。
        (void) dup (0);     // 复制句柄,产生句柄1 号 -- stdout 标准输出设备 
        (void) dup (0);     // 复制句柄,产生句柄2 号 -- stderr 标准出错输出设备。
        printf ("%d buffers = %d bytes buffer space\n\r", NR_BUFFERS, NR_BUFFERS * BLOCK_SIZE); // 打印缓冲区块数和总字节数,每块1024 字节。
        printf ("Free mem: %d bytes\n\r", memory_end - main_memory_start);  //空闲内存字节数。

    //
    //                  创建进程 2
    //
      // 下面fork()用于创建一个子进程(子任务)。对于被创建的子进程,fork()将返回0 值,
      // 对于原(父进程)将返回子进程的进程号。所以下面一段是【子进程执行的内容】。该子进程
      // 关闭了句柄0(stdin),以只读方式打开/etc/rc 文件,并执行/bin/sh 程序,所带参数和
      // 环境变量分别由 argv_rc 和 envp_rc 数组给出。参见后面的描述。
      if (!(pid = fork ()))
        {
          // 关闭文件系统调用函数,将 current->filp【】包含的 inode 中的节点及节点文件数据同步到设备中。
          // 参数fd 是文件句柄,是本文件在当前进程 filp【中的位置】
          // 传入参数: 成功则返回0,否则返回出错码。
          close (0);    //即调用 sys_close

           打开文件函数。
          // 打开并有可能创建一个文件。
          // 参数:filename - 文件名;flag - 文件打开标志;...
          // 返回:文件描述符【fd 即在 current->filp[fd]  数组中的位置】,若出错则置出错码,并返回-1。
          if (open ("/etc/rc", O_RDONLY, 0))
          /*【/etc/rc 内容】
            /etc/update &
            echo "dev/hd1 /" > /etc/matb
            echo "OK. "
            */
          _exit (1);        // 如果打开文件失败,则退出(/lib/_exit.c,10)。


          execve ("/bin/sh", argv_rc, envp_rc); // 装入/bin/sh 程序并执行。 【sys_execve --> do_execve】

          _exit (2);        // 若execve()执行失败则退出(出错码2,“文件或目录不存在”)。
        }


      //            进程1 等待 进程 2 结束,然后重新创建新的 进程【主要作用是创建一个 update 程序】 

      // 下面是父进程执行的语句。wait()是等待子进程停止或终止,其返回值应是子进程的进程号(pid)。
      // 这三句的作用是父进程等待子进程的结束。&i 是存放返回状态信息的位置。如果wait()返回值不
      // 等于子进程号,则继续等待。
      if (pid > 0)
        while (pid != wait (&i))  // sys_waitpid,【这个进程最终会退出,留下一个 update 程序】
          /* nothing */ ;

    //
    //          重建 shell,创建进程 3
    //
      // 如果执行到这里,说明刚创建的子进程的执行已停止或终止了。下面循环中首先再创建一个子进程,
      // 如果出错,则显示“初始化程序创建子进程失败”的信息并继续执行。对于所创建的子进程关闭所有
      // 以前还遗留的句柄(stdin, stdout, stderr),新创建一个会话并设置进程组号,然后重新打开
      // /dev/tty0 作为stdin,并复制成stdout 和stderr。再次执行系统解释程序 /bin/sh。但这次执行所
      // 选用的参数和环境数组另选了一套(见上面165-167 行)。然后父进程再次运行wait()等待。如果
      // 子进程又停止了执行,则在标准输出上显示出错信息“子进程pid 停止了运行,返回码是i”,然后
      // 继续重试下去…,形成“大”死循环。
      while (1)
        {
          if ((pid = fork ()) < 0)
        {
          printf ("Fork failed in init\r\n");
          continue;
        }

          if (!pid)
        {
          close (0);
          close (1);
          close (2);
          setsid ();
          (void) open ("/dev/tty0", O_RDWR, 0);
          (void) dup (0);
          (void) dup (0);
          _exit (execve ("/bin/sh", argv, envp));//【argv ="-/bin/sh"】【envp="HOME=/usr/root"】
        }

          while (1)
        if (pid == wait (&i))
          break;

          printf ("\n\rchild %d died with code %04x\n\r", pid, i);

          /* 系统调用。同步设备和内存高速缓冲中数据,分为两部分
            1.是同步系统所有的 i 节点结构到设备中
            2.是同步本设备所有已修改的缓冲块产生写请求*/
          sync ();
        }
      _exit (0);            /* NOTE! _exit, not exit() */
    }
    /*  
        sys_mount (char *dev_name, char *dir_name, int rw_flag)
        dir_namei (const char *pathname, int *namelen, const char **name)
        sys_read (unsigned int fd, char *buf, int count)
        sys_creat (const char *pathname, int mode) --> 调用sys_open
        sys_write (unsigned int fd, char *buf, int count)
        BADNESS
        sys_signal (int signum, long handler, long restorer)





    */