当前位置:首页>Linux>《Linux-0.12 源码篇》- 01 boot引导程序分析

《Linux-0.12 源码篇》- 01 boot引导程序分析

  • 2026-07-04 04:14:19
《Linux-0.12 源码篇》- 01 boot引导程序分析

字数 12516,阅读大约需 63 分钟

第一章:boot引导程序分析

1.1 概述

当你按下计算机的电源按钮时,一系列精心设计的程序开始依次执行,从最简单的BIOS代码到引导扇区,再到setup程序,最后启动内核。每个阶段完成自己的任务后,将控制权交给下一个阶段,最终将系统带入正常运行状态。

boot引导程序

Linux 0.12的引导过程包含三个主要阶段的代码:bootsect.S(引导扇区)、setup.S(系统设置)和head.s(内核头部)。这三个程序是引导内核启动过程中的三个关键阶梯,引导系统从实模式切换到保护模式,从简单的16位环境进入更加强大的32位环境。理解这个过程不仅能让你掌握Linux的启动机制,更能深刻体会操作系统如何从无到有地建立起运行环境。

阅读这些汇编代码除了需要知道一些 8086 汇编语言知识以外,还需要了解一些采用 Intel 80X86 微处理器的 PC 机体系结构,以及在 32 位保护模式下进行编程的基本原理。

1.2 BIOS加载过程

当 PC 的电源打开后,80x86 结构的CPU 将自动进入实模式,并从固定的地址(0xFFFF0)开始自动执行程序代码,这个地址通常是 ROM-BIOS 中的地址。BIOS(Basic Input/Output System)是固化在主板芯片上的程序,它的第一项工作是进行加电自检(POST,Power-On Self Test),检查内存、显卡等硬件是否正常,并在物理地址 0 处开始设置和初始化中断向量。

Linux 0.12 启动后的页面

自检完成后,BIOS根据CMOS设置中的启动顺序,依次尝试从软盘、硬盘、光盘或网络启动。对于硬盘启动,BIOS读取硬盘的第一个扇区(主引导记录,MBR)到内存地址0x7C00,然后检查这个扇区的最后两个字节是否为0x55和0xAA(引导扇区标志)。如果是,BIOS将控制权转交给0x7C00处的代码,引导程序开始执行。

这个过程就像图书馆管理员打开图书馆大门后,将钥匙交给第一位到达的工作人员。BIOS完成了最基础的硬件初始化,引导扇区将继续后续的启动工作。

1.2.1 引导过程中内存布局变化

下图展示了从BIOS加载引导扇区到setup准备跳转到head.s的整个过程中,各个程序模块在内存中的位置变化:

启动内存布局

各阶段详细说明:

  1. 1. Stage 1 - BIOS加载引导扇区:BIOS将硬盘第一个扇区(bootsect)加载到0x7C00处,这是PC机BIOS约定的引导扇区加载地址。
  2. 2. Stage 2 - bootsect自移动:bootsect将自己的512字节从0x7C00复制到0x90000,为后续加载system模块腾出空间。
  3. 3. Stage 3 - 加载setup和system
    • • setup程序(4个扇区)被加载到0x90200,紧跟在bootsect之后
    • • system模块(包含内核代码)被加载到0x10000处,最大可达512KB
  4. 4. Stage 4 - 准备进入保护模式:setup程序将system模块从0x10000移动到0x00000(物理地址0),为进入保护模式和启动分页机制做准备。此时system模块中的head.s将作为保护模式下的第一段代码执行。

这种设计巧妙地利用了有限的实模式内存空间(1MB以内),通过程序的分阶段加载和移动,既保证了各个模块有足够空间,又避免了相互覆盖。

1.3 bootsect.S 引导扇区分析

引导扇区的代码空间非常有限——只有512字节,必须在这个狭小的空间内完成关键任务。Linux 0.12的bootsect.S就像一个高效的搬运工,它的主要任务是将自己和setup程序、内核模块从磁盘读入内存的合适位置。

1.3.1 实模式内存布局

在实模式下,内存布局相对简单,但也有很多区域被预留用于特殊用途。理解这个布局就像理解一座城市的分区规划,每个区域都有其特定的功能。

地址范围
大小
用途
阶段
0x00000-0x003FF
1KB
BIOS中断向量表
系统运行时
0x00400-0x004FF
256B
BIOS数据区
系统运行时
0x00500-0x07BFF
~30KB
可用内存
引导阶段临时使用
0x07C00-0x07DFF
512B
引导扇区初始位置
BIOS加载
0x08000-0x0FFFF
32KB
Setup程序
引导阶段
0x10000-0x8FFFF
512KB
System模块临时位置
引导阶段
0x90000-0x901FF
512B
引导扇区移动后位置
引导阶段
0x90200-0x903FF
~4KB
Setup程序移动后位置
引导阶段
0xA0000-0xFFFFF
384KB
显存和BIOS ROM
系统运行时

1.3.2 引导扇区自移动

bootsect.S被BIOS加载到0x7C00后,第一件事就是把自己移动到0x90000。这个操作看似多此一举,实际上是为后续步骤腾出空间。先将自己的东西移走,才能有足够的空间放置新的东西进去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

!! 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字节。0x3000共为0x30000字节=196KB。! 若以1024字节为1KB计,则因该就192KB。对应当前内核版本这个空间长度已足够了。当该值为! 0x8000时,表示内核最大为512KB。因为内存0x90000处开始存放移动后的bootsect和setup的代码,! 因此该值最大不得超过0x9000(表示584KB)。!! 头文件linux/config.h中定义了内核用到的一些常数符号和Linus自己使用的默认硬盘参数块。! 例如定义了以下一些常数:! DEF_INITSEG    0x9000                            //引导扇区程序将被移动到得段值。! DEF_SYSSEG    0x1000                            //引导扇区程序把系统模块加载到内存的段值。! DEF_SETUPSEG    0x9020                            //setup程序所处内存段位置。! DEF_SYSSIZE    0x3000                            //内核系统模块默认最大节数(16字节=1节)。!#include <linux/config.h>SYSSIZE = DEF_SYSSIZE                             !定义一个标号或符号。指明编译连接后system模块的大小。!!    bootsect.s        (C) 1991 Linus Torvalds!    modified by Drew Eckhardt!! 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        (C) 1991 Linus Torvalds!    Drew Eckhardt修改!! bootsect.S被ROM BIOS启动子程序加载至0x7c00(32KB)处,并将自己移到了地址0x90000! (576KB)处,并跳转至那里。!! 它然后使用BIOS中断将'setup'直接加载到自己的后面(0x90200)(576.5KB),并将system! 加载到地址0x10000处。!! 注意!目前的内核系统最大长度限制为(8*65536)(512KB)B,即使是在将来这也应该没有问题! 的。我想让他保持简单明了。这样512KB的最大内核长度应该足够了,尤其是这里没有像MINIX中! 一样包含缓冲区高速缓冲。.globl begtext, begdata, begbss, endtext, enddata, endbss.textbegtext:.databegdata:.bssbegbss:.textSETUPLEN = 4                ! nr of setup-sectors                            ! setup程序占用的扇区数BOOTSEG  = 0x07c0            ! original address of boot-sector                            ! bootsect代码所在内存原始段地址INITSEG  = DEF_INITSEG        ! we move boot here - out of the way                            ! 将bootsect移到位置0x90000 避开系统模块占用处SETUPSEG = DEF_SETUPSEG        ! setup starts here                            ! setup程序从内存0x90200处开始SYSSEG   = DEF_SYSSEG        ! system loaded at 0x10000 (65536).                            ! system模块加载到0x10000(64KB)处ENDSEG   = SYSSEG + SYSSIZE    ! where to stop loading                            ! 停止加载的段地址! ROOT_DEV & SWAP_DEV are now written by "build".! 根文件系统设备号ROOT_DEV和交换设备号SWAP_DEV现在由tools目录下的build程序写入ROOT_DEV = 0                !根文件系统设备使用与系统引导是同样的设备SWAP_DEV = 0                !交换设备使用与系统引导是同样的设备entry start                    ! 告知连接程序,程序从start标号开始执行start:    mov    ax, #BOOTSEG        ! 将DS段寄存器置为0x07c0    mov    ds, ax    mov    ax, #INITSEG        ! 将ES段寄存器置为0x9000    mov    es, ax    mov    cx, #256            ! 设置移动计数值=256字(512字节)    sub    si, si                ! 源地址    ds:si = 0x07c0:0x0000    sub    di, di                ! 目的地址  es:di = 0x9000:0x0000    rep                        ! 重复执行并递减cx的值,直到cx=0为止    movw                    ! 即movs指令。从内存[si]处移动cx个字到[di]处    jmpi    go, INITSEG        ! 段间跳转(Jump Intersegment)。这里INITSEG指出跳转到得段地址,                            ! 标号go是段内偏移地址

这段代码使用rep movw指令,将512字节的引导扇区从0x7C00复制到0x90000,然后通过段间跳转指令jmpi跳转到新位置继续执行。

1.3.3 加载setup和system模块

上一步调转到0x90000后,bootsect开始执行,bootsect继续从磁盘读取setup程序和system模块,并把setup读入到内存 0x90200 处(启动设备盘后 2KB 字节代码,也就是boot/setup.S)。内核的system 模块则被读入到从内存地址 0x10000(64KB)开始处。这个过程就像快递物流分拣,按照地址将不同的快递包裹送到指定位置。

1.3.4 bootsect加载setup程序

自移动完成后,bootsect从新位置0x90000继续执行。接下来的任务是从磁盘加载setup程序到内存0x90200处。setup程序共占用4个扇区(2KB),紧跟在引导扇区之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

go:    mov    ax, cs            ! 将ds、es、ss都设置为0x9000    mov    ds, ax    mov    es, ax! put stack at 0x9ff00.    mov    ss, ax    mov    sp, #0xFF00        ! arbitrary value >>512                        ! 栈指针sp指向0x9ff00(从0x9ff00向下增长)! load the setup-sectors directly after the bootblock.! Note that 'es' is already set up.! 加载setup模块到0x90200(在bootsect后面)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        ! 出错则重试ok_load_setup:

这段代码使用BIOS中断INT 0x13(磁盘服务)加载setup。AH=0x02表示读扇区功能,AL=SETUPLEN(4)表示读取4个扇区,CH=0表示柱面号0,CL=2表示从第2个扇区开始(第1个扇区是bootsect自己),DH=0表示磁头0,DL=0表示驱动器0,ES:BX=0x9000:0x0200指向目标缓冲区0x90200。

如果读取失败(CF标志置位),就复位磁盘(AH=0)后重试。这种简单的错误处理在现代系统中看似原始,但在启动阶段是必要的容错机制。

1.3.5 bootsect加载system模块

setup加载完成后,bootsect继续加载system模块。system模块是内核的主体部分,包含head.s和所有C编译的内核代码,体积可达数百KB。由于太大无法一次加载,bootsect采用循环方式逐个扇区读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

! Get disk drive parameters, specifically nr of sectors/track! 获取磁盘驱动器参数,特别是每磁道扇区数    mov    dx, #0x0000    mov    ax, #0x0800        ! AH=8 is get drive parameters    int    0x13    mov    ch, #0x00    seg cs                    ! seg cs表示下一条指令的操作数在cs段中    mov    sectors, cx        ! 保存每磁道扇区数    mov    ax, #INITSEG    mov    es, ax! Print some inane message! 打印一些信息    mov    ah, #0x03        ! read cursor pos    xor    bh, bh    int    0x10    mov    cx, #24            ! 显示24个字符    mov    bx, #0x0007        ! page 0, attribute 7 (normal)    mov    bp, #msg1    mov    ax, #0x1301        ! write string, move cursor    int    0x10! ok, we've written the message, now! we want to load the system (at 0x10000)! 现在开始加载system模块到0x10000处    mov    ax, #SYSSEG    mov    es, ax            ! segment of 0x010000    call    read_it        ! 调用read_it子程序读取system模块    call    kill_motor        ! 关闭驱动器马达

read_it是一个循环读取子程序,它计算要读取的扇区数(根据SYSSIZE),然后一个扇区一个扇区地读取system模块到0x10000开始的内存区域。每次读取后ES增加0x20(512字节/16=32),使下一次读取的目标地址后移512字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

read_it:    mov    ax, es    test    ax, #0x0fff        ! 测试是否到达64KB边界die:    jne    die            ! es must be at 64kB boundary    xor    bx, bx            ! bx is starting address within segmentrp_read:    mov    ax, es    cmp    ax, #ENDSEG        ! have we loaded all yet?    jb    ok1_read    retok1_read:    seg cs    mov    ax, sectors        ! 每磁道扇区数    sub    ax, sread        ! 减去已读扇区数    mov    cx, ax    shl    cx, #9            ! cx = cx * 512 字节    add    cx, bx    jnc    ok2_read    je    ok2_read    xor    ax, ax    sub    ax, bx    shr    ax, #9            ! ax = ax / 512ok2_read:    call    read_track        ! 读取当前磁道的剩余扇区    mov    cx, ax    add    ax, sread    seg cs    cmp    ax, sectors        ! 是否读完当前磁道    jne    ok3_read    mov    ax, #1    sub    ax, head    jne    ok4_read        ! 如果是0磁头,则去读1磁头    inc    track            ! 否则读下一磁道ok4_read:    mov    head, ax    xor    ax, axok3_read:    mov    sread, ax        ! 保存已读扇区数    shl    cx, #9    add    bx, cx            ! 调整缓冲区指针    jnc    rp_read    mov    ax, es    add    ax, #0x1000        ! 将es增加0x1000(64KB)    mov    es, ax    xor    bx, bx    jmp    rp_read

这段代码展示了在16位实模式下处理大文件的复杂性。由于段寄存器的限制(每个段最大64KB),必须在读取64KB后调整ES寄存器指向下一个64KB段。这种分段管理在保护模式下会通过分页机制得到简化。

1.3.6 跳转到setup执行

所有模块加载完毕后,bootsect的使命完成,它通过段间跳转指令jmpi将控制权交给setup程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

! 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.! 检查要使用哪个根文件系统设备    seg cs    mov    ax, root_dev        ! 取根设备号    cmp    ax, #0    jne    root_defined    seg cs    mov    bx, sectors    mov    ax, #0x0208        ! /dev/ps0 - 1.2Mb    cmp    bx, #15    je    root_defined    mov    ax, #0x021c        ! /dev/PS0 - 1.44Mb    cmp    bx, #18    je    root_definedundef_root:    jmp undef_root            ! 死循环,表示未定义根设备root_defined:    seg cs    mov    root_dev, ax! after that (everyting loaded), we jump to! the setup-routine loaded directly after! the bootblock:! 跳转到setup程序执行(setup位于0x90200)    jmpi    0, SETUPSEG

这个跳转标志着bootsect使命的结束。控制权传递给setup,系统引导进入下一阶段。jmpi指令的格式是"jmpi 偏移地址, 段地址",这里跳转到0x9020:0x0000,即物理地址0x90200。

bootsect加载到0x7c00

1.4 setup.S 系统设置分析

setup程序是引导过程中的关键环节,它就像一个全面的系统检查员,负责收集硬件信息、准备进入保护模式的环境,最后完成模式切换。setup程序的代码相对较长,但逻辑清晰,可以分为几个主要阶段。

1.4.1 硬件信息获取

setup程序使用BIOS中断获取系统硬件信息,这些信息对内核正常运行至关重要。这就像医生在手术前详细了解病人的身体状况。这些信息被存储在0x90000开始的内存区域(原来bootsect所在位置),供内核初始化时使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

! ok, the read went well so we get current cursor position and save it for! posterity.! 获取光标位置    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.! Get memory size (extended mem, kB)! 获取扩展内存大小(单位:KB)    mov    ah, #0x88    int    0x15    mov    [2], ax            ! 存储在 0x90002! Get video-card data:! 获取显卡数据    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和配置参数    mov    ah, #0x12    mov    bl, #0x10    int    0x10    mov    [8], ax    mov    [10], bx    mov    [12], cx! Get hd0 data! 获取第一个硬盘的参数表    mov    ax, #0x0000    mov    ds, ax    lds    si, [4*0x41]        ! 取中断向量0x41的值,就是硬盘参数表的地址    mov    ax, #INITSEG    mov    es, ax    mov    di, #0x0080        ! 传送目的地址0x90080    mov    cx, #0x10    rep    movsb                    ! 复制16字节! Get hd1 data! 获取第二个硬盘的参数表    mov    ax, #0x0000    mov    ds, ax    lds    si, [4*0x46]        ! 取中断向量0x46的值    mov    ax, #INITSEG    mov    es, ax    mov    di, #0x0090        ! 传送目的地址0x90090    mov    cx, #0x10    rep    movsb! Check that there IS a hd1 :-)! 检查第二个硬盘是否存在    mov    ax, #0x01500    mov    dl, #0x81    int    0x13    jc    no_disk1    cmp    ah, #3    je    is_disk1no_disk1:    mov    ax, #INITSEG    mov    es, ax    mov    di, #0x0090    mov    cx, #0x10    mov    ax, #0x00    rep    stosb                    ! 清零第二个硬盘参数表is_disk1:

这段代码收集了以下关键信息:

  1. 1. 光标位置 (0x90000): 4字节,包含行号和列号,供控制台初始化使用
  2. 2. 扩展内存大小 (0x90002): 2字节,单位KB,用于内存管理初始化
  3. 3. 显示模式信息 (0x90004-0x9000c): 包括显示页、视频模式、窗口宽度、EGA/VGA参数
  4. 4. 硬盘参数表 (0x90080和0x90090): 每个16字节,包含柱面数、磁头数、扇区数等信息

这些信息的存储位置是精心设计的:0x90000-0x901ff这512字节原本是bootsect所在位置,现在bootsect已经完成使命,这块内存可以被重新利用来存储系统参数。

系统参数在内存中的存储布局

1.4.2 移动system模块

在收集完硬件信息后,setup要将system模块从0x10000移动到0x00000。这一步非常关键,因为内核代码必须从物理地址0开始,这样才能正确设置分页机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

! now we want to move to protected mode ...! 现在要移动到保护模式了    cli                    ! no interrupts allowed !                        ! 关中断,以防止移动过程中发生中断! first we move the system to it's rightful place! 首先将system模块移动到正确的位置    mov    ax, #0x0000    cld                    ! 'direction'=0, movs moves forwarddo_move:    mov    es, ax            ! destination segment    add    ax, #0x1000    cmp    ax, #0x9000        ! 已经移动了所有的段了吗?    jz    end_move    mov    ds, ax            ! source segment    sub    di, di    sub    si, si    mov     cx, #0x8000        ! 移动64KB,0x8000个字(32KB个字=64KB)    rep    movsw    jmp    do_moveend_move:

这段代码将system模块从0x10000开始的位置移动到0x00000开始的位置。移动过程采用循环方式,每次移动64KB(一个段),直到0x90000为止(因为0x90000开始是bootsect和setup所在位置,不能覆盖)。移动后,system模块的内容从物理地址0x00000开始,这是内核分页机制的起始地址。

移动操作为什么采用这种复杂的方式?因为在实模式下,每次移动只能处理64KB的段。移动的源地址从0x10000开始,每次增加0x1000(即下一个64KB段);目的地址从0x00000开始,也是每次增加0x1000。移动直到0x90000为止,这样最多可以移动512KB的内核代码。

1.4.3 加载IDTR和GDTR

进入保护模式之前,必须准备好中断描述符表(IDT)和全局描述符表(GDT)。setup设置了临时的IDT和GDT,这些在head.s中会被重新设置。

1
2
3
4
5
6

! then we load the segment descriptors! 加载段描述符    mov    ax, cs            ! right, forgot this at first. didn't work :-)    mov    ds, ax    lidt    idt_48            ! load idt with 0,0    lgdt    gdt_48            ! load gdt with whatever appropriate

lidt和lgdt指令加载IDT和GDT寄存器。idt_48和gdt_48是两个6字节的数据结构,前2字节是表的限长(单位字节),后4字节是表的基地址。

1
2
3
4
5
6
7

idt_48:    .word    0                ! idt limit=0    .word    0, 0            ! idt base=0Lgdt_48:    .word    0x800            ! gdt limit=2048, 256 GDT entries    .word    512+gdt, 0x9    ! gdt base = 0X9xxxx

IDT被设置为空(限长0,基地址0),因为真正的IDT会在内核初始化时由trap_init()函数设置。GDT包含3个描述符:空描述符、代码段描述符和数据段描述符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

gdt:    .word    0, 0, 0, 0            ! dummy                            ! 空描述符,x86要求GDT第一项为空    .word    0x07FF                ! 8Mb - limit=2047 (2048*4096=8Mb)    .word    0x0000                ! base address=0    .word    0x9A00                ! code read/exec    .word    0x00C0                ! granularity=4096, 386                            ! 代码段,基址0,限长8MB,可读可执行    .word    0x07FF                ! 8Mb - limit=2047 (2048*4096=8Mb)    .word    0x0000                ! base address=0    .word    0x9200                ! data read/write    .word    0x00C0                ! granularity=4096, 386                            ! 数据段,基址0,限长8MB,可读可写

这个简单的GDT只是临时使用,为了能够进入保护模式并执行一些基本操作。真正完整的GDT会在head.s中重新设置,包含每个进程的TSS和LDT描述符。

GDT描述符表

1.4.4 A20地址线开启

A20地址线的开启是进入保护模式的必要步骤。这是一个历史遗留问题:在8086时代,地址总线只有20条,最大可寻址1MB内存。8086的分段寻址方式(段地址*16+偏移地址)可能产生超过1MB的地址,例如0xFFFF:0xFFFF=0x10FFEF。在实际硬件上,第21位地址线(A20)会被忽略,导致地址回绕到低端。0x100000会回绕到0x000000。

到了80286时代,地址总线扩展到24条,可寻块16MB内存。但为了保持向后兼容性(有些老软件依赖地址回绕特性),IBM在PC/AT设计中将A20地址线的控制连接到键盘控制器的空闲端口,默认关闭。只有主动开启A20,CPU才能访问1MB以上的内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

! that was painless, now we enable A20! 现在开启A20地址线    call    empty_8042        ! 等待输入缓冲器空    mov    al, #0xD1        ! command write    out    #0x64, al    call    empty_8042        ! 等待输入缓冲器空    mov    al, #0xDF        ! A20 on    out    #0x60, al    call    empty_8042        ! 等待输入缓冲器空! 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.empty_8042:    .word    0x00eb,0x00eb    ! jmp $+2, jmp $+2    in    al, #0x64        ! 8042 status port    test    al, #2            ! is input buffer full?    jnz    empty_8042        ! yes - loop    ret

A20地址线的开启通过键盘控制器8042完成:

  1. 1. 等待输入缓冲器空(读取状态端口0x64,检查第2位)
  2. 2. 发送命令写指令(0xD1)到0x64端口,表示要写输出端口
  3. 3. 等待输入缓冲器空
  4. 4. 发送开启A20命令(0xDF)到0x60端口
  5. 5. 等待命令执行完毕

empty_8042子程序负责等待键盘控制器的输入缓冲器空。它首先执行两次"jmp $+2"(原地跳转)指令,这是一个延时技巧,给键盘控制器留出反应时间。然后循环读取状态端口,直到输入缓冲器空为止。

A20地址线的开启是一个典型的历史包袱:为了向后兼容,现代系统不得不继续支持这30年前的奇技淋巧。这也提醒我们,计算机系统的演进总是带着历史的抈锁。

1.4.5 进入保护模式

现在一切准备就绪,setup即将完成其最重要的使命:将CPU从实模式切换到保护模式。这一切换是历史性的一刻,从此系统进入32位的现代操作系统时代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

! 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.! 现在必须重新编程8259A中断控制器    mov    al, #0x11        ! initialization sequence    out    #0x20, al        ! send it to 8259A-1    .word    0x00eb,0x00eb    ! jmp $+2, jmp $+2    out    #0xA0, al        ! and to 8259A-2    .word    0x00eb,0x00eb    mov    al, #0x20        ! start of hardware int's (0x20)    out    #0x21, al    .word    0x00eb,0x00eb    mov    al, #0x28        ! start of hardware int's 2 (0x28)    out    #0xA1, al    .word    0x00eb,0x00eb    mov    al, #0x04        ! 8259-1 is master    out    #0x21, al    .word    0x00eb,0x00eb    mov    al, #0x02        ! 8259-2 is slave    out    #0xA1, al    .word    0x00eb,0x00eb    mov    al, #0x01        ! 8086 mode for both    out    #0x21, al    .word    0x00eb,0x00eb    out    #0xA1, al    .word    0x00eb,0x00eb    mov    al, #0xFF        ! mask off all interrupts for now    out    #0x21, al    .word    0x00eb,0x00eb    out    #0xA1, al

这段代码重新编程8259A中断控制器。为什么需要这样做?BIOS将8259A设置为发送中断号0x08-0x0f,但这些中断号在保护模式下被Intel保留用于处理器异常(如除零错误、调试陷阱、非法指令等)。为了避免冲突,Linux将硬件中断重新映射到0x20-0x2f。

重新编程的步骤:

  1. 1. 发送ICW1(0x11)到两个8259A,启动初始化序列
  2. 2. 发送ICW2(0x20/0x28),设置中断向量偏移
  3. 3. 发送ICW3(0x04/0x02),设置主从关系
  4. 4. 发送ICW4(0x01),设置8086模式
  5. 5. 将所有中断屏蔽(0xFF),等待内核初始化后再逐个开启

最后,设置CR0寄存器的PE位(保护模式使能位),并跳转到保护模式下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13

! 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.! 现在是实际进入保护模式的时候了    mov    ax, #0x0001    ! protected mode (PE) bit    lmsw    ax            ! This is it!    jmpi    0, 8            ! jmp offset 0 of segment 8 (cs)

lmsw指令(Load Machine Status Word)加载机器状态字到CR0寄存器的低16位。这里将最低位(PE位)置1,启用保护模式。然后jmpi指令跳转到0x08:0x00000,即代码段选择符为8(GDT中的第2项,即代码段描述符),偏移地址0x00000。

CR0寄存器位域

这个跳转是历史性的:它标志着CPU从16位实模式切换到32位保护模式,从此可以访问全部4GB地址空间,使用分段和分页保护机制,运行现代操作系统。跳转的目标地址0x00000是system模块的起始位置,也就是head.s的开始。

实模式和保护模式的寻址方式

1.5 head.s 内核入口分析

head.s是内核真正的入口,此时处理器已经进入保护模式,但还需要做很多初始化工作才能跳转到C语言编写的main函数。head.s是system模块的第一部分,它使用32位汇编语言编写,这与之前的16位实模式代码有本质不同。

1.5.1 重新设置GDT和IDT

setup中设置的GDT只是临时使用的,head.s需要重新建立一个完整的GDT。这个GDT包含了更多的描述符,为后续的进程管理做准备。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

.textstartup_32:    movl $0x10,%eax            # 0x10是GDT中数据段的选择符    mov %ax,%ds    mov %ax,%es    mov %ax,%fs    mov %ax,%gs    lss stack_start,%esp    # 设置系统堆栈    call setup_idt            # 设置IDT    call setup_gdt            # 重新设置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    mov %ax,%gs    lss stack_start,%esp

head.s的开始标签是startup_32,这是setup跳转过来的位置。首先初始化所有段寄存器为数据段选择符(0x10),然后设置堆栈指针。接着调用setup_idt和setup_gdt函数设置IDT和GDT。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

setup_idt:    lea ignore_int,%edx        # 将ignore_int的地址送到edx    movl $0x00080000,%eax    # 将选择符0x0008置入eax的高半部    movw %dx,%ax                # selector = 0x0008 = cs    movw $0x8E00,%dx        # interrupt gate - dpl=0, present    lea idt,%edi            # idt是IDT表在内存中的地址    mov $256,%ecx            # 设置256个中断门rp_sidt:    movl %eax,(%edi)        # 将哑元中断门描述符的低4字节存入表中    movl %edx,4(%edi)        # 再将高4字节存入    addl $8,%edi            # edi指向下一项    dec %ecx    jne rp_sidt    lidt idt_descr            # 加载IDT寄存器    ret

setup_idt函数将IDT的所有256个项都设置为指向ignore_int的中断门。ignore_int是一个哑元中断处理程序,它只是返回而不做任何事情。真正的中断处理程序会在内核初始化时由trap_init()函数设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

setup_gdt:    lgdt gdt_descr            # 加载GDT寄存器    retgdt_descr:    .word 256*8-1            # so does gdt (not that that's any    .long gdt                # magic number, but it works for me :^)    .align 8idt:    .fill 256,8,0        # idt is uninitializedgdt:    .quad 0x0000000000000000    /* NULL descriptor */    .quad 0x00c09a0000000fff    /* 16Mb */    # 0x08, 内核代码段,可读/执行    .quad 0x00c0920000000fff    /* 16Mb */    # 0x10, 内核数据段,可读/写    .quad 0x0000000000000000    /* TEMPORARY - don't use */    .fill 252,8,0                        # 剩余252项留给TSS和LDT使用

setup_gdt函数重新加载GDT寄存器。这里GDT的大小被设置为256项,每项8字节。当前只填充了前3项:空描述符、代码段描述符和数据段描述符。剩余252项留给后续的进程TSS和LDT描述符使用。

1.5.2 分页机制建立

head.s的首要任务是建立分页机制。虽然已经在保护模式下,但分页还未启用。分页机制如同为整个系统建立了新的地址映射体系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

    movl $pg0+7,pg_dir        /* set present bit/user r/w */    movl $pg1+7,pg_dir+4        /*  --------- " " --------- */    movl $pg2+7,pg_dir+8        /*  --------- " " --------- */    movl $pg3+7,pg_dir+12        /*  --------- " " --------- */    movl $pg3+4092,%edi    movl $0xfff007,%eax        /*  16Mb - 4096 + 7 (r/w user,p) */    stdsetup_paging:    stosl                        /* fill pages backwards - more efficient :-) */    subl $0x1000,%eax    jge setup_paging    xorl %eax,%eax            /* pg_dir is at 0x0000 */    movl %eax,%cr3            /* cr3 - page directory start */    movl %cr0,%eax    orl $0x80000000,%eax    movl %eax,%cr0            /* set paging (PG) bit */    ret                        /* this also flushes prefetch-queue */

这段代码建立了一个简单的页目录和页表,将16MB的线性地址空间直接映射到16MB的物理地址。具体步骤:

  1. 1. 设置页目录:页目录位于物理地址0x0000处(原来system模块的位置,现在被head.s使用)。前4个页目录项分别指向pg0、pg1、pg2、pg3这4个页表,每个页表可以映射4MB地址空间,总共16MB。
  2. 2. 填充页表:std指令设置方向标志,使stosl指令向低地址方向填充。从edi=pg3+4092(最后一个页表项)开始,向前填充页表项。每个页表项的值从0xfff007开始递减,每次减0x1000(4KB)。这样最后一个页面映射到16MB-4KB,第二个映射到16MB-8KB,依此类推。
  3. 3. 启用分页:将CR3寄存器设为页目录的地址0x0000,然后设置CR0寄存器的PG位(0x80000000)启用分页。

分页机制启用后,所有的线性地址都需要通过页表转换为物理地址。由于这里建立的是恒等映射(线性地址=物理地址),代码可以继续执行而不需要修改。但这为后续的进程管理和虚拟内存制定了基础。

1.5.3 堆栈初始化

建立内核栈是为后续的C函数调用做准备。没有栈,函数调用、局部变量、参数传递都无法进行。

1
2
3
4
5
6
7

stack_start:    .long user_stack+PAGE_SIZE    # 堆栈指针指向user_stack数组的顶端    .word 0x10                    # 堆栈段选择符=0x10(内核数据段).org 0x5000                    # user_stack数组的起始位置user_stack:    .fill PAGE_SIZE/4, 4, 0    # 分配一个页面(4KB)作为内核栈

stack_start定义了堆栈的位置,它包含了堆栈指针和堆栈段选择符。lss stack_start,%esp指令同时加载ess和esp寄存器,设置好内核栈。

内核栈的大小只有4KB(一个页面),这在现代系统中看似很小,但对于Linux 0.12的内核来说已经足够。后续每个进程都会有自己的内核态栈,存储在进程的task_struct所在页面中。

1.5.4 跳转到main函数

完成所有初始化后,head.s跳转到main函数,控制权正式交给C代码,内核初始化进入新阶段。

1
2
3
4
5
6
7
8
9
10

after_page_tables:    pushl $0            # These are the parameters to main :-)    pushl $0    pushl $0    pushl $L6            # return address for main, if it decides to.    pushl $main    jmp setup_pagingL6:    jmp L6                # main should never return here, but                    # just in case, we know what happens.

这段代码将几个参数压栈,然后调用setup_paging建立分页。setup_paging执行完毕后会返回,而返回地址是after_page_tables之后的指令。但由于栈上压入了$main,实际上会跳转到main函数执行。

L6标签是一个死循环,用于捕获main函数返回的情况(理论上main不应该返回)。如果main意外返回,系统会在这里挂死。

跳转到main函数标志着引导阶段的结束和内核初始化阶段的开始。从此,系统进入了C语言的世界,开始初始化内存管理、进程调度、设备驱动等子系统。

1.6 实模式与保护模式的区别

理解实模式和保护模式的本质区别,是掌握系统引导过程的关键。这两种模式代表了x86处理器的两个时代。

1.6.1 实模式特征

地址计算方式:

1

物理地址 = 段地址 << 4 + 偏移地址

例如,段:偏移 = 0x9000:0x0100

1
2
3
4

// 计算物理地址物理地址 = 0x9000 * 16 + 0x0100         = 0x90000 + 0x0100         = 0x90100

实模式的限制:

  1. 1. 1MB内存限制:20位地址总线,最大寻址空间1MB
  2. 2. 无内存保护:任何程序都可以访问任何内存地址
  3. 3. 无特权级:所有代码运行在同一特权级
  4. 4. 段限长固定:所有段最大64KB
  5. 5. 中断向量表固定:0x00000-0x003FF为中断向量表

实模式寻址示例:

1
2
3
4
5
6
7

; 实模式下访问显存mov ax, 0xB800      ; 彩色文本模式显存段地址mov es, axmov byte ptr es:[0], 'A'     ; 写入字符Amov byte ptr es:[1], 0x07     ; 白色前景,黑色背景; 物理地址 = 0xB800 * 16 + 0 = 0xB8000

1.6.2 保护模式特征

地址计算方式:

1
2
3
4
5

1. 段选择符 → 查GDT/LDT → 得到段描述符2. 检查段界限和访问权限3. 线性地址 = 段基址 + 偏移地址4. 如果分页开启:线性地址 → 页表转换 → 物理地址   否则:线性地址 = 物理地址

段描述符结构(8字节):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

struct segment_descriptor {    unsigned limit_low:16;      // 段界限低16位    unsigned base_low:16;       // 基地址低16位    unsigned base_mid:8;        // 基地址中8位    unsigned type:4;            // 段类型(代码/数据/系统)    unsigned s:1;               // 描述符类型(0=系统,1=代码/数据)    unsigned dpl:2;             // 特权级(0-3)    unsigned present:1;         // 段存在位    unsigned limit_high:4;      // 段界限高4位    unsigned avl:1;             // 系统软件可用位    unsigned reserved:1;        // 保留位    unsigned db:1;              // 默认操作数大小(0=16位,1=32位)    unsigned granularity:1;     // 粒度(0=字节,1=4KB)    unsigned base_high:8;       // 基地址高8位};

段选择符结构(16位):

1
2
3
4
5
6
7
8

15              3  2  1 0+---------------+--+--+--+|     index     |TI|RPL |+---------------+--+--+--+index: 索引号(13位),指定描述符在GDT/LDT中的位置TI:    表指示符(1位),0=GDT,1=LDTRPL:   请求特权级(2位),0-3

段选择符结构(32位):

段描述符

保护模式的优势:

  1. 1. 4GB地址空间:32位地址总线,最大4GB
  2. 2. 内存保护:段界限检查、读/写/执行权限检查
  3. 3. 特权级保护:4个特权级(Ring 0-3)
  4. 4. 灵活的段大小:段界限可达4GB
  5. 5. 任务隔离:每个任务有独立的地址空间
  6. 6. 虚拟内存:通过分页机制实现

保护模式寻址示例:

1
2
3
4
5
6
7
8

; 保护模式下访问显存(假设GDT已设置)mov ax, 0x10        ; 数据段选择符(GDT[2])mov es, ax; CPU自动查GDT,获取段基址0x00000000; 检查段界限、访问权限mov byte ptr es:[0xB8000], 'A'    ; 写入字符A; 线性地址 = 0x00000000 + 0xB8000 = 0xB8000; 如果分页开启,还需要页表转换

1.6.3 模式切换的关键步骤

从实模式到保护模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

; 1. 准备GDTmov ax, csmov ds, axlgdt gdt_48         ; 加载GDTR寄存器; 2. 开启A20地址线call empty_8042     ; 等待键盘控制器空闲mov al, #0xD1      ; 命令:写输出端口out #0x64, alcall empty_8042mov al, #0xDF      ; A20 onout #0x60, al; 3. 设置CR0的PE位mov ax, #0x0001    ; PE位lmsw ax            ; 加载到CR0; 4. 跳转刷新指令队列jmpi 0, 8          ; 跳转到保护模式代码(段选择符8); 5. 重新加载段寄存器mov ax, 0x10       ; 数据段选择符mov ds, axmov es, axmov fs, axmov gs, axmov ss, ax

为什么需要跳转刷新指令队列?

CPU的指令预取队列中可能还有实模式的指令。切换到保护模式后,这些指令的解释方式完全不同:

  • • 实模式:段:偏移寻址
  • • 保护模式:选择符:偏移寻址

如果不刷新指令队列,可能会错误执行这些指令。jmpi指令(段间跳转)会自动刷新指令队列。

1.6.4 段描述符实例分析

Linux 0.12的内核代码段描述符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

原始值:0x00c09a0000000fff解析:  63-56: base_high    = 0x00  → 基地址高8位  55:    G(粒度)      = 1     → 4KB粒度  54:    D/B          = 1     → 32位段  53:    保留         = 0  52:    AVL          = 0  51-48: limit_high   = 0x0   → 段界限高4位  47:    P(存在)      = 1     → 段存在  46-45: DPL          = 0     → 特权级0(内核)  44:    S            = 1     → 代码/数据段  43-40: Type         = 0xA   → 可执行、可读、已访问  39-32: base_mid     = 0x00  → 基地址中8位  31-16: base_low     = 0x0000 → 基地址低16位  15-0:  limit_low    = 0x0FFF → 段界限低16位计算段界限:  界限 = (limit_high << 16 | limit_low) * 粒度       = (0x0 << 16 | 0xFFF) * 4096       = 0xFFF * 4096       = 16MB - 4KB段基址:0x00000000段界限:16MB访问权限:可读、可执行、特权级0

为什么段界限是16MB而不是4GB?

Linux 0.12限制内核代码段为16MB是出于以下考虑:

  1. 1. 内核大小限制:0.12版本的内核实际大小远小于16MB
  2. 2. 简化调试:较小的段界限有助于发现越界访问
  3. 3. 兼容性:保留与早期内核的兼容性

后续版本的Linux将段界限扩展到4GB,使用平坦内存模型(flat memory model)。

1.7 磁盘I/O与BIOS中断

引导过程中大量使用了BIOS提供的磁盘服务。理解这些中断的工作原理,有助于深入掌握底层硬件访问机制。

1.7.1 INT 0x13磁盘服务

INT 0x13是BIOS提供的磁盘I/O服务中断,支持多种功能。

主要功能号(AH寄存器):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

// 功能0x00: 复位磁盘系统AH = 0x00DL = 驱动器号(0x00=软盘A,0x80=硬盘C)返回:CF=0成功,CF=1失败// 功能0x02: 读扇区到内存AH = 0x02              // 功能号AL = 扇区数            // 要读取的扇区数(1-128)CH = 柱面号低8CL = 扇区号(0-5位) + 柱面号高2位(6-7位)DH = 磁头号DL = 驱动器号ES:BX = 缓冲区地址    // 数据存放位置返回:  CF = 0: 成功,AL = 实际读取的扇区数  CF = 1: 失败,AH = 错误码// 功能0x08: 获取驱动器参数AH = 0x08DL = 驱动器号返回:  CF = 0: 成功    CH = 最大柱面号低8    CL = 每磁道扇区数(0-5位) + 最大柱面号高2位(6-7位)    DH = 最大磁头号    DL = 驱动器数量  CF = 1: 失败

bootsect中读取setup的完整过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

load_setup:    ; 设置读取参数    mov dx, #0x0000         ; DH=磁头0, DL=驱动器0    mov cx, #0x0002         ; CH=柱面0, CL=扇区2(从第2扇区开始)    mov bx, #0x0200         ; 偏移地址=512(ES已经是0x9000)    mov ax, #0x0200+SETUPLEN  ; AH=0x02(读扇区), AL=4(读4个扇区)    ; 调用BIOS中断    int 0x13                ; 读取磁盘    ; 检查是否成功    jnc ok_load_setup       ; CF=0则跳转(成功)    ; 读取失败,复位磁盘    mov dx, #0x0000    mov ax, #0x0000         ; AH=0x00(复位功能)    int 0x13    j load_setup            ; 重试ok_load_setup:    ; 继续执行后续代码

CHS寻址模式:

CHS(Cylinder-Head-Sector)是传统的磁盘寻址方式:

1
2
3
4
5
6
7
8
9
10
11
12

物理扇区位置 = (柱面号 × 磁头数 + 磁头号) × 扇区数 + 扇区号 - 1例如:读取第100个扇区(假设18扇区/磁道,2磁头)100 = (柱面 × 2 + 磁头) × 18 + 扇区 - 1计算:  柱面 = 100 / (2 × 18) = 2  余数 = 100 % (2 × 18) = 28  磁头 = 28 / 18 = 1  扇区 = 28 % 18 + 1 = 11结果:柱面2,磁头1,扇区11

为什么扇区号从1开始?

这是软盘时代的历史遗留。软盘的第0扇区用于同步和标识,实际数据从扇区1开始。硬盘沿用了这个编号方式以保持兼容性。

1.7.2 read_it子程序详解

read_it是bootsect中最复杂的部分,负责将system模块完整加载到内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

read_it:    ; 检查ES是否在64KB边界    mov ax, es    test ax, #0x0fff        ; 测试低12位是否为0die:    jne die                 ; 不在64KB边界则死循环    xor bx, bx              ; BX=0,段内偏移从0开始rp_read:    ; 检查是否已加载所有扇区    mov ax, es    cmp ax, #ENDSEG         ; 比较当前段与结束段    jb ok1_read             ; 小于则继续    ret                     ; 完成,返回ok1_read:    ; 计算当前磁道还剩多少扇区    seg cs    mov ax, sectors         ; 每磁道扇区数    sub ax, sread           ; 减去已读扇区数    mov cx, ax              ; CX = 剩余扇区数    shl cx, #9              ; CX = 剩余字节数(乘以512)    add cx, bx              ; CX += 当前段内偏移    ; 检查是否会超过64KB段界限    jnc ok2_read            ; 不超过则跳转    je ok2_read             ; 正好等于也跳转    ; 会超过64KB,计算这次能读多少扇区    xor ax, ax              ; AX = 0    sub ax, bx              ; AX = 64KB - 当前偏移    shr ax, #9              ; AX = 可读扇区数(除以512)ok2_read:    ; 调用read_track读取当前磁道剩余扇区    call read_track    ; 更新已读扇区数和偏移    mov cx, ax              ; CX = 实际读取的扇区数    add ax, sread           ; 更新已读扇区数    seg cs    cmp ax, sectors         ; 是否读完整个磁道?    jne ok3_read            ; 未读完则跳转    ; 读完一个磁道,切换磁头或磁道    mov ax, #1    sub ax, head            ; AX = 1 - 当前磁头    jne ok4_read            ; 如果当前是磁头0,切换到磁头1    inc track               ; 否则切换到下一磁道ok4_read:    mov head, ax    xor ax, ax              ; 新磁道从扇区0开始ok3_read:    mov sread, ax           ; 保存已读扇区数    shl cx, #9              ; CX = 读取的字节数    add bx, cx              ; 更新段内偏移    jnc rp_read             ; 未超过64KB则继续    ; 超过64KB,切换到下一个段    mov ax, es    add ax, #0x1000         ; ES += 0x1000(64KB)    mov es, ax    xor bx, bx              ; 偏移归零    jmp rp_read             ; 继续读取

关键变量说明:

1
2
3
4
5

sread:    当前磁道已读扇区数head:     当前磁头号(01track:    当前柱面号sectors:  每磁道扇区数(通过INT 0x13功能0x08获取)ES:BX:    当前读取的目标地址

为什么需要这么复杂的逻辑?

  1. 1. 段界限:实模式段最大64KB,必须处理跨段问题
  2. 2. 磁道边界:读取不能跨越磁道,到磁道末尾必须切换
  3. 3. 磁头切换:软盘/硬盘通常有多个磁头,读完一面切换另一面
  4. 4. 容错:每次读取后检查是否成功

read_track子程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

read_track:    ; 保存要读取的扇区数    push ax                 ; AX = 扇区数    push bx                 ; BX = 段内偏移    push cx                 ; CX = 扇区数(参数)    push dx                 ; DX = 磁头和驱动器号    ; 设置读取参数    mov dx, track           ; DH = 磁头号    mov cx, sread           ; CL = 起始扇区号    inc cx                  ; 扇区号从1开始    mov ch, track           ; CH = 柱面号    mov dx, head            ; DH = 磁头号    mov dl, #0              ; DL = 驱动器0    ; 读取扇区    and dx, #0x0100         ; 只保留磁头号    mov ah, #2              ; 功能0x02:读扇区    int 0x13                ; 调用BIOS    ; 检查是否成功    jc bad_rt               ; CF=1则读取失败    ; 成功,恢复寄存器并返回    pop dx    pop cx    pop bx    pop ax    retbad_rt:    ; 读取失败,复位磁盘后重试    mov ax, #0    mov dx, #0    int 0x13                ; 复位磁盘    pop dx    pop cx    pop bx    pop ax    jmp read_track          ; 重试

1.8 本章总结

本章详细分析了Linux 0.12的引导过程,从BIOS加载到进入内核。引导过程如同接力赛跑,BIOS、bootsect、setup、head依次完成各自任务。bootsect负责将程序加载到内存,setup负责硬件信息收集和模式切换,head负责建立分页机制和跳转到main函数。这个过程体现了操作系统启动的复杂性和精巧设计。

核心知识点回顾

1. 引导流程四个阶段

  • • BIOS:加电自检、初始化硬件、加载引导扇区
  • • bootsect:自移动、加载setup和system、跳转setup
  • • setup:收集硬件信息、开启A20、重编8259A、切换保护模式
  • • head.s:重设 GDT/IDT、建立分页、跳转main()

2. 内存布局变化

  • • 0x7C00: BIOS加载bootsect的位置
  • • 0x90000: bootsect移动后的位置
  • • 0x90200: setup程序位置
  • • 0x10000: system模块临时加载位置
  • • 0x00000: system模块最终位置(启动地址)

3. 实模式与保护模式

  • • 实模式:段:偏移寻址0-1MB、无保护、同特权级
  • • 保护模式:选择符:偏移、0-4GB、内存保护、四特权级
  • • 切换关键:设置GDT、开启A20、设置CR0.PE、跳转刷新

4. 段描述符机制

  • • 8字节结构:基址(32位)+界限(20位)+属性(12位)
  • • 属性包括:类型、DPL、颗粒度、D/B位等
  • • Linux内核段:基址0x00000000、界镰16MB、DPL=0

5. A20地址线历史

  • • 8086:20位地址线,超过1MB回绕
  • • 80286:24位地址线,但为兼容默认关闭A20
  • • PC/AT:通过键盘控制器8042控制A20
  • • 现代:多种开启A20的方法(Fast A20、端口等)

6. BIOS中断服务

  • • INT 0x10:视频服务(显示字符、设置模式等)
  • • INT 0x13:磁盘服务(读写扇区、复位等)
  • • INT 0x15:系统服务(获取内存大小等)
  • • CHS寻址:柱面-磁头-扇区三维坐标

7. 分页机制基础

  • • 两级页表:页目录 + 页表
  • • 页目录址0x0000,包含1024项,每项指4MB
  • • 页表共四个(pg0-pg3),每个1024项,每项指4KB
  • • 归映射:线性地址=物理地址(16MB范围内)

关键技术细节

1. 为什么bootsect要自移动?

如果bootsect不移动,留在0x7C00:

  • • system模块加载到0x10000后会向低地址增长
  • • 当system超过87.5KB时会覆盖bootsect(0x7C00-0x7DFF)
  • • 移动到0x90000后,system可以安全加载最大512KB

2. 为什么setup要移动system?

保护模式的分页机制要求:

  • • 页目录必须在64KB边界(0x0000满足)
  • • 内核代码从物理地址0开始,方便映射
  • • head.s设置的恒等映射要求内核在地址0

3. 为什么需要重新设置GDT/IDT?

setup中的GDT只是临时的:

  • • 只有3个描述符:空、代码、数据
  • • 位置在90200附近,会被内核代码覆盖
  • • head.s重设:256项GDT,位置在内核数据段
  • • 为后续TSS/LDT预留空间

4. 为什么要重编8259A?

BIOS设置的中断向量:

  • • 硬件中断映射到0x08-0x0F
  • • 与Intel处理器异常冲突(也是0x08-0x0F)
  • • Linux重映射:硬件中断到0x20-0x2F
  • • 避免中断号混淆

设计思想

1. 模块化设计

  • • 每个阶段职责明确,互不干扰
  • • bootsect只负责加载,setup只负责准备
  • • head.s与main()之间界限清晰

2. 容错设计

  • • 磁盘读取失败后自动重试
  • • 关键资源检查(ES段边界、A20状态)
  • • 极限情况死循环(die标签)

3. 空间优化

  • • 内存区域重复利用(bootsect位置存放参数)
  • • 代码紧凑(512字节引导扇区)
  • • 分页用已有内存(页目录在0x0000)

4. 向前兼容

  • • 保留足够的扩展空间(GDT 256项)
  • • 灵活的内核大小(SYSSIZE可调)
  • • 通用的硬件接口(BIOS中断)

与现代系统的对比

Linux 0.12 vs 现代Linux

特征
Linux 0.12
现代Linux
引导加载器
bootsect
GRUB/UEFI
加载位置
0x10000
任意位置
内核大小
<512KB
数十MB
分页机制
两级页表
四级页表
地址空间
4GB
128TB/256TB
启动协议
BIOS
UEFI
早期文件系统
initramfs
多核支持
SMP
设备发现
静态
动态(udev)

不变的核心原理:

  • • CPU从实模式到保护模式的转换
  • • 分段和分页机制的基本原理
  • • 特权级隔离和内存保护
  • • 中断和异常的处理框架

实验与调试

使用Bochs调试引导过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 1. 在.bochsrc中设置断点# b 0x7c00              # bootsect入口# b 0x90000             # bootsect移动后# b 0x90200             # setup入口# b 0x00000             # head.s入口# 2. 查看关键寄存器bochs> info cpu       # 查看CPU状态bochs> info gdt       # 查看GDTbochs> info idt       # 查看IDTbochs> creg           # 查看CR0/CR3等# 3. 查看内存bochs> x /10 0x7c00   # 查看bootsect代码bochs> x /10 0x90000  # 查看移动后的bootsectbochs> x /10 0x90200  # 查看setup代码# 4. 单步执行bochs> s              # Step执行一条指令bochs> c              # Continue继续执行

关键检查点:

  1. 1. bootsect加载后,0x7C00处有512字节数据
  2. 2. 移动后,0x90000和0x7C00内容相同
  3. 3. setup加载后,0x90200处有数据
  4. 4. 进入保护模式后,CR0.PE=1
  5. 5. head.s执行后,CR0.PG=1,分页开启

本章思考

  1. 1. 为什么bootsect要先把自己移动到0x90000,而不是直接在0x7C00执行?
  2. 2. A20地址线的历史遗留问题反映了计算机架构演进中的什么设计权衡?
  3. 3. 为什么需要两次设置GDT(setup中设置临时GDT,head中重新设置)?

参考资料

  • • Intel® 64 and IA-32 Architectures Software Developer's Manual Volume 3
    • • 第9章:Processor Management and Initialization
  • • Linux 0.12源码:boot/bootsect.S, boot/setup.S, boot/head.s
  • • INTEL 80386 PROGRAMMER'S REFERENCE MANUAL

"千里之行,始于足下。"——老子《道德经》

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-07-04 09:37:32 HTTP/2.0 GET : https://f.mffb.com.cn/a/488531.html
  2. 运行时间 : 0.087951s [ 吞吐率:11.37req/s ] 内存消耗:5,053.07kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=af41c035fbed73aebe642599930dbd47
  1. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/runtime/temp/067d451b9a0c665040f3f1bdd3293d68.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000589s ] mysql:host=127.0.0.1;port=3306;dbname=f_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.000833s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000339s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000254s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000494s ]
  6. SELECT * FROM `set` [ RunTime:0.000196s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000573s ]
  8. SELECT * FROM `article` WHERE `id` = 488531 LIMIT 1 [ RunTime:0.000683s ]
  9. UPDATE `article` SET `lasttime` = 1783129052 WHERE `id` = 488531 [ RunTime:0.008998s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 67 LIMIT 1 [ RunTime:0.000309s ]
  11. SELECT * FROM `article` WHERE `id` < 488531 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.000494s ]
  12. SELECT * FROM `article` WHERE `id` > 488531 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.000385s ]
  13. SELECT * FROM `article` WHERE `id` < 488531 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.000756s ]
  14. SELECT * FROM `article` WHERE `id` < 488531 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.002332s ]
  15. SELECT * FROM `article` WHERE `id` < 488531 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.003331s ]
0.089556s