同济大学

Linux操作系统实现原理

内核程序集

 

赵炯

2017/9/18

 

 

Linux 0.12版内核已注释程序集

 

 


 

目录

 

 


5 内核编译批处理程序... 1

5.1 程序5‑1 linux/Makefile文件... 1

6 引导启动程序... 6

6.1 程序6 ‑1 linux/boot/bootsect.S. 6

6.2 程序 6‑2 linux/boot/setup.S. 21

6.3 程序6-3 linux/boot/head.s. 38

7 内核初始化程序... 48

7.1 程序7-1 linux/init/main.c. 48

8 内核核心程序... 56

8.1 程序8-1 linux/kernel/asm.s. 56

8.2 程序8-2 linux/kernel/traps.c. 61

8.3 程序8-3 linux/kernel/sys_call.s. 66

8.4 程序8-4 linux/kernel/mktime.c程序... 74

8.5 程序8-5 linux/kernel/sched.c. 76

8.6 程序8-6 linux/kernel/signal.c. 89

8.7 程序8-7 linux/kernel/exit.c. 96

8.8 程序8-8 linux/kernel/fork.c. 109

8.9 程序8-9 linux/kernel/sys.c程序... 114

8.10 程序8-10 linux/kernel/vsprintf.c. 129

8.11 程序8-11 linux/kernel/printk.c. 135

8.12 程序8-12 linux/kernel/panic.c. 136

9 内核块设备程序... 137

9.1 程序9-1 linux/kernel/blk_drv/blk.h.. 137

9.2 程序9-2 linux/kernel/blk_drv/hd.c. 142

9.3 程序9-3 linux/kernel/blk_drv/ll_rw_blk.c. 155

9.4 程序9-4 linux/kernel/blk_drv/ramdisk.c. 162

9.5 程序9-5 linux/kernel/blk_drv/floppy.c. 166

10 字符设备程序... 181

10.1 程序10-1 linux/kernel/chr_drv/keyboard.S. 181

10.2 程序10-2 linux/kernel/chr_drv/console.c. 196

10.3 程序10-3 linux/kernel/chr_drv/serial.c. 224

10.4 程序10-4 linux/kernel/chr_drv/rs_io.s. 226

10.5 程序10-5 linux/kernel/chr_drv/tty_io.c. 230

10.6 程序10-6 linux/kernel/chr_drv/tty_ioctl.c. 245

11 协处理器仿真程序... 253

11.1 程序11-1 linux/kernel/math/math_emulate.c  253

11.2 程序11-2 linux/kernel/math/error.c. 265

11.3 程序11-3 linux/kernel/math/ea.c. 266

11.4 程序11-4 linux/kernel/math/convert.c. 269

11.5 程序11-5 linux/kernel/math/add.c. 274

11.6 程序11-6 linux/kernel/math/compare.c. 277

11.7 程序11-7 linux/kernel/math/get_put.c. 279

11.8 程序11-8 linux/kernel/math/mul.c. 286

11.9 程序11-9 linux/kernel/math/div.c. 288

12 文件系统程序... 291

12.1 程序12-1 linux/fs/buffer.c. 291

12.2 程序 12-2  linux/fs/bitmap.c. 303

12.3 程序12-3 linux/fs/truncate.c. 308

12.4 程序12-4 linux/fs/inode.c. 311

12.5 程序12-5 linux/fs/super.c. 321

12.6 程序12-6 linux/fs/namei.c. 330

12.7 程序12-7 linux/fs/file_table.c. 357

12.8 程序12-8 linux/fs/block_dev.c. 358

12.9 程序12-9 linux/fs/file_dev.c. 361

12.10 程序12-10 linux/fs/pipe.c. 364

12.11 程序12-11 linux/fs/char_dev.c. 368

12.12 程序12-12 linux/fs/read_write.c. 371

12.13 程序12-13 linux/fs/open.c. 374

12.14 程序12-14 linux/fs/exec.c. 382

12.15 程序12-15 linux/fs/stat.c. 395

12.16 程序12-16 linux/fs/fcntl.c. 398

12.17 程序12-17 linux/fs/ioctl.c. 401

12.18 程序12-18 linux/fs/select.c. 403

13 内存管理程序... 411

13.1 程序13-1 linux/mm/memory.c. 411

13.2 程序13-2 linux/mm/page.s. 429

13.3 程序13-3 linux/mm/swap.c. 430

14 内核包含程序... 438

14.1 程序14-1 linux/include/a.out.h.. 438

14.2 程序14-2 linux/include/const.h.. 444

14.3 程序14-3 linux/include/ctype.h.. 445

14.4 程序14-4  linux/include/errno.h.. 446

14.5 程序14-5  linux/include/fcntl.h.. 448

14.6 程序14-6  linux/include/signal.h.. 450

14.7 程序14-7 linux/include/stdarg.h.. 453

14.8 程序14-8 linux/include/stddef.h.. 454

14.9 程序14-9 linux/include/string.h.. 455

14.10 程序14-10 linux/include/termios.h.. 465

14.11 程序14-11 linux/include/time.h.. 471

14.12 程序14-12 linux/include/unistd.h.. 473

14.13 程序14-13 linux/include/utime.h.. 480

14.14 程序14-14 linux/include/asm/io.h.. 481

14.15 程序14-15 linux/include/asm/memory.h.. 482

14.16 程序14-16 linux/include/asm/segment.h.. 483

14.17 程序14-17 linux/include/asm/system.h.. 485

14.18 程序14-18 linux/include/linux/config.h.. 487

14.19 程序14-19 linux/include/linux/fdreg.h.. 489

14.20 程序14-20 linux/include/linux/fs.h.. 492

14.21 程序14-21 linux/include/linux/hdreg.h.. 498

14.22 程序14-22 linux/include/linux/head.h.. 500

14.23 程序14-23 linux/include/linux/kernel.h.. 501

14.24 程序14-24 linux/include/linux/math_emu.h   503

14.25 程序14-25 linux/include/linux/mm.h.. 509

14.26 程序14-26 linux/include/linux/sched.h.. 511

14.27 程序14-27 linux/include/linux/sys.h.. 519

14.28 程序14-28 linux/include/linux/tty.h.. 522

14.29 程序14-29 linux/include/sys/param.h.. 525

14.30 程序14-30 linux/include/sys/resource.h.. 526

14.31 程序14-31 linux/include/sys/stat.h.. 528

14.32 程序14-32 linux/include/sys/time.h.. 530

14.33 程序14-33 linux/include/sys/times.h.. 532

14.34 程序14-34 linux/include/sys/types.h.. 533

14.35 程序14-35 linux/include/sys/utsname.h.. 535

14.36 程序14-36 linux/include/sys/wait.h.. 536

15 内核库函数程序... 537

15.1 程序15-1 linux/lib/_exit.c. 537

15.2 程序15-2 linux/lib/close.c. 538

15.3 程序15-3 linux/lib/ctype.c. 539

15.4 程序15-4 linux/lib/dup.c. 540

15.5 程序15-5 linux/lib/errno.c. 541

15.6 程序15-6 linux/lib/execve.c. 542

15.7 程序15-7 linux/lib/malloc.c. 543

15.8 程序15-8 linux/lib/open.c. 550

15.9 程序15-9 linux/lib/setsid.c. 551

15.10 程序15-10 linux/lib/string.c. 552

15.11 程序15-11 linux/lib/wait.c. 553

15.12 程序15-12 linux/lib/write.c. 554

16 内核创建组合程序... 555

16.1 程序16-1 linux/tools/build.c. 555


 

 

 


 

第5章 内核编译批处理程序

 

5.1 程序5‑1 linux/Makefile文件


  1 #

  2 # if you want the ram-disk device, define this to be the

  3 # size in blocks.

  4 #

    # 如果你要使用RAM(RAMDISK)设备的话就定义块的大小。这里默认RAMDISK没有定义(注释掉了),

    # 否则gcc编译时会带有选项'-DRAMDISK=512',参见第13行。

  5 RAMDISK = #-DRAMDISK=512

  6

  7 AS86    =as86 -0 -a       # 8086汇编编译器和连接器,见列表后的介绍。后带的参数含义分别

  8 LD86    =ld86 -0          # 是:-0 生成8086目标程序;-a 生成与gasgld部分兼容的代码。

  9

 10 AS      =gas              # GNU汇编编译器和连接器,见列表后的介绍。

 11 LD      =gld

    # 下面是GNU链接器gld运行时用到的选项。含义是:-s 输出文件中省略所有的符号信息;-x 删除

    # 所有局部符号;-M 表示需要在标准输出设备(显示器)上打印连接映像(link map),是指由连接程序

    # 产生的一种内存地址映像,其中列出了程序段装入到内存中的位置信息。具体来讲有如下信息:

    # • 目标文件及符号信息映射到内存中的位置;

    # • 公共符号如何放置;

    # • 连接中包含的所有文件成员及其引用的符号。

 12 LDFLAGS =-s -x -M

 

    # gccGNU C程序编译器。对于UNIX类的脚本(script)程序而言,在引用定义的标识符时,需在前

    # 面加上$符号并用括号括住标识符。

 13 CC      =gcc $(RAMDISK)

 

    # 下面指定gcc使用的选项。前一行最后的'\'符号表示下一行是续行。选项含义为:-Wall 打印所有

    # 警告信息;-O 对代码进行优化。'-f标志'指定与机器无关的编译标志。其中-fstrength-reduce

    # 于优化循环语句;-fcombine-regs用于指明编译器在组合编译阶段把复制一个寄存器到另一个寄存

    # 器的指令组合在一起。-fomit-frame-pointer 指明对于无需帧指针(Frame pointer)的函数不要

    # 把帧指针保留在寄存器中。这样在函数中可以避免对帧指针的操作和维护。-mstring-insns

    # Linus在学习gcc编译器时为gcc增加的选项,用于gcc-1.40在复制结构等操作时使用386 CPU

    # 字符串指令,可以去掉。

 14 CFLAGS  =-Wall -O -fstrength-reduce -fomit-frame-pointer \ 

 15 -fcombine-regs -mstring-insns

 

    # 下面cppgcc的前()处理器程序。前处理器用于进行程序中的宏替换处理、条件编译处理以及

    # 包含进指定文件的内容,即把使用'#include'指定的文件包含进来。源程序文件中所有以符号'#'

    # 开始的行均需要由前处理器进行处理。程序中所有'#define'定义的宏都会使用其定义部分替换掉。

    # 程序中所有'#if''#ifdef''#ifndef''#endif'等条件判别行用于确定是否包含其指定范围中

    # 的语句。

    # '-nostdinc -Iinclude'含义是不要搜索标准头文件目录中的文件,即不用系统/usr/include/目录

    # 下的头文件,而是使用'-I'选项指定的目录或者是在当前目录里搜索头文件。

 16 CPP     =cpp -nostdinc -Iinclude

 17

 18 #

 19 # ROOT_DEV specifies the default root-device when making the image.

 20 # This can be either FLOPPY, /dev/xxxx or empty, in which case the

 21 # default of /dev/hd6 is used by 'build'.

 22 #

    # ROOT_DEV指定在创建内核映像(image)文件时所使用的默认根文件系统所

    # 在的设备,这可以是软盘(FLOPPY)/dev/xxxx或者干脆空着,空着时

    # build程序(在tools/目录中)就使用默认值/dev/hd6

    #

    # 这里/dev/hd6对应第2个硬盘的第1个分区。这是Linus开发Linux内核时自己的机器上根

    # 文件系统所在的分区位置。/dev/hd2表示把第1个硬盘的第2个分区用作交换分区。

 23 ROOT_DEV=/dev/hd6

 24 SWAP_DEV=/dev/hd2

 25

    # 下面是kernel目录、mm目录和fs目录所产生的目标代码文件。为了方便引用在这里将它们用

    # ARCHIVES(归档文件)标识符表示。

 26 ARCHIVES=kernel/kernel.o mm/mm.o fs/fs.o

 

    # 块和字符设备库文件。'.a'表示该文件是个归档文件,也即包含有许多可执行二进制代码子程序

    # 集合的库文件,通常是用GNUar程序生成。arGNU的二进制文件处理程序,用于创建、修改

    # 以及从归档文件中抽取文件。

 27 DRIVERS =kernel/blk_drv/blk_drv.a kernel/chr_drv/chr_drv.a

 28 MATH    =kernel/math/math.a        # 数学运算库文件。

 29 LIBS    =lib/lib.a                 # lib/目录中的文件所编译生成的通用库文件。

 30

    # 下面是make老式的隐式后缀规则。该行指示make利用下面的命令将所有的'.c'文件编译生成'.s'

    # 汇编程序。':'表示下面是该规则的命令。整句表示让gcc采用前面CFLAGS所指定的选项以及仅使

    # include/目录中的头文件,在适当地编译后不进行汇编就停止(-S),从而产生与输入的各个C

    # 文件对应的汇编语言形式的代码文件。默认情况下所产生的汇编程序文件是原C文件名去掉'.c'

    # 再加上'.s'后缀。'-o'表示其后是输出文件的形式。其中'$*.s'(或'$@')是自动目标变量,'$<'

    # 代表第一个先决条件,这里即是符合条件'*.c'的文件。

    # 下面这3个不同规则分别用于不同的操作要求。若目标是.s文件,而源文件是.c文件则会使

    # 用第一个规则;若目录是.o,而原文件是.s,则使用第2个规则;若目标是.o文件而原文件

    # c文件,则可直接使用第3个规则。

 31 .c.s:

 32         $(CC) $(CFLAGS) \

 33         -nostdinc -Iinclude -S -o $*.s $<

 

    # 表示将所有.s汇编程序文件编译成.o目标文件。 整句表示使用gas编译器将汇编程序编译成.o

    # 目标文件。-c 表示只编译或汇编,但不进行连接操作。

 34 .s.o:

 35         $(AS) -c -o $*.o $<

    # 类似上面,*.c文件-à*.o目标文件。整句表示使用gccC语言文件编译成目标文件但不连接。

 36 .c.o:

 37         $(CC) $(CFLAGS) \

 38         -nostdinc -Iinclude -c -o $*.o $<

 39

 

    # 下面'all'表示创建Makefile所知的最顶层的目标。这里即是Image文件。这里生成的Image文件

    # 即是引导启动盘映像文件bootimage。若将其写入软盘就可以使用该软盘引导Linux系统了。在

    # Linux下将Image写入软盘的命令参见46行。DOS系统下可以使用软件rawrite.exe

 40 all:    Image

 41

    # 说明目标(Image文件)是由冒号后面的4个元素产生,分别是boot/目录中的bootsectsetup

    # 文件、tools/目录中的systembuild文件。42--43行这是执行的命令。42行表示使用tools

    # 录下的build工具程序(下面会说明如何生成)将bootsectsetupsystem文件以$(ROOT_DEV)

    # 为根文件系统设备组装成内核映像文件Image。第43行的sync同步命令是迫使缓冲块数据立即写盘

    # 并更新超级块。

 42 Image: boot/bootsect boot/setup tools/system tools/build

 43         tools/build boot/bootsect boot/setup tools/system $(ROOT_DEV) \

 44                 $(SWAP_DEV) > Image

 45         sync

 46

    # 表示disk这个目标要由Image产生。ddUNIX标准命令:复制一个文件,根据选项进行转换和格

    # 式化。bs=表示一次读/写的字节数。if=表示输入的文件,of=表示输出到的文件。这里/dev/PS0

    # 指第一个软盘驱动器(设备文件)。在Linux系统下使用/dev/fd0

 47 disk: Image

 48         dd bs=8192 if=Image of=/dev/PS0

 49

 50 tools/build: tools/build.c            # tools目录下的build.c程序生成执行程序build

 51         $(CC) $(CFLAGS) \

 52         -o tools/build tools/build.c  # 编译生成执行程序build的命令。

 53

 54 boot/head.o: boot/head.s              # 利用上面给出的.s.o规则生成head.o目标文件。

 55

    # 表示tools目录中的system文件要由冒号右边所列的元素生成。56--61行是生成system的命令。

    # 最后的 > System.map 表示gld需要将连接映像重定向存放在System.map文件中。

    # 关于System.map文件的用途参见注释后的说明。

 56 tools/system:   boot/head.o init/main.o \

 57                 $(ARCHIVES) $(DRIVERS) $(MATH) $(LIBS)

 58         $(LD) $(LDFLAGS) boot/head.o init/main.o \

 59         $(ARCHIVES) \

 60         $(DRIVERS) \

 61         $(MATH) \

 62         $(LIBS) \

 63         -o tools/system > System.map

 64

    # 数学协处理函数文件math.a64行上的命令实现:进入kernel/math/目录;运行make工具程序。

 65 kernel/math/math.a:

 66         (cd kernel/math; make)

 67

 68 kernel/blk_drv/blk_drv.a:           # 生成块设备库文件blk_drv.a,其中含有可重定位目标文件。

 69         (cd kernel/blk_drv; make)

 70

 71 kernel/chr_drv/chr_drv.a:           # 生成字符设备函数文件chr_drv.a

 72         (cd kernel/chr_drv; make)

 73

 74 kernel/kernel.o:                    # 内核目标模块kernel.o

 75         (cd kernel; make)

 76

 77 mm/mm.o:                            # 内存管理模块mm.o

 78         (cd mm; make)

 79

 80 fs/fs.o:                            # 文件系统目标模块fs.o

 81         (cd fs; make)

 82

 83 lib/lib.a:                          # 库函数lib.a

 84         (cd lib; make)

 85

 86 boot/setup: boot/setup.s                        # 这里开始的三行是使用8086汇编和连接器

 87         $(AS86) -o boot/setup.o boot/setup.s    # setup.s文件进行编译生成setup文件。

 88         $(LD86) -s -o boot/setup boot/setup.o   # -s 选项表示要去除目标文件中的符号信息。

 89

 90 boot/setup.s:   boot/setup.S include/linux/config.h       # 执行C语言预处理,替换*.S

 91         $(CPP) -traditional boot/setup.S -o boot/setup.s  # 件中的宏生成对应的*.s文件。

 92

 93 boot/bootsect.s:        boot/bootsect.S include/linux/config.h

 94         $(CPP) -traditional boot/bootsect.S -o boot/bootsect.s

 95

 96 boot/bootsect:  boot/bootsect.s                 # 同上。生成bootsect.o磁盘引导块。

 97         $(AS86) -o boot/bootsect.o boot/bootsect.s

 98         $(LD86) -s -o boot/bootsect boot/bootsect.o

 99

    # 当执行'make clean'时,就会执行98--103行上的命令,去除所有编译连接生成的文件。

    # 'rm'是文件删除命令,选项-f含义是忽略不存在的文件,并且不显示删除信息。

100 clean:

101         rm -f Image System.map tmp_make core boot/bootsect boot/setup \

102                 boot/bootsect.s boot/setup.s

103         rm -f init/*.o tools/system tools/build boot/*.o

104         (cd mm;make clean)       # 进入mm/目录;执行该目录Makefile文件中的clean规则。

105         (cd fs;make clean)

106         (cd kernel;make clean)

107         (cd lib;make clean)

108

    # 该规则将首先执行上面的clean规则,然后对linux/目录进行压缩,生成'backup.Z'压缩文件。

    # 'cd .. '表示退到linux/的上一级(父)目录;'tar cf - linux'表示对linux/目录执行tar归档

    # 程序。'-cf'表示需要创建新的归档文件 '| compress -'表示将tar程序的执行通过管道操作('|')

    # 传递给压缩程序compress,并将压缩程序的输出存成backup.Z文件。

109 backup: clean

110         (cd .. ; tar cf - linux | compress - > backup.Z)

111         sync                                    # 迫使缓冲块数据立即写盘并更新磁盘超级块。

112

113 dep: 

# 该目标或规则用于产生各文件之间的依赖关系。创建这些依赖关系是为了让make命令用它们来确定

# 是否需要重建一个目标对象。比如当某个头文件被改动过后,make就能通过生成的依赖关系,重新

# 编译与该头文件有关的所有*.c文件。具体方法如下:

    # 使用字符串编辑程序sedMakefile文件(这里即是本文件)进行处理,输出为删除了Makefile

    # 文件中'### Dependencies'行后面的所有行,即删除了下面从122开始到文件末的所有行,并生成

    # 一个临时文件tmp_make(也即114行的作用)。然后对指定目录下(init/)的每一个C文件(其实

    # 只有一个文件main.c)执行gcc预处理操作。标志'-M'告诉预处理程序cpp输出描述每个目标文件

    # 相关性的规则,并且这些规则符合make语法。对于每一个源文件,预处理程序会输出一个规则,其

    # 结果形式就是相应源程序文件的目标文件名加上其依赖关系,即该源文件中包含的所有头文件列表。

    # 然后把预处理结果都添加到临时文件tmp_make中,最后将该临时文件复制成新的Makefile文件。

    # 115行上的'$$i'实际上是'$($i) '。这里'$i'是这句前面的shell变量'i'的值。

114         sed '/\#\#\# Dependencies/q' < Makefile > tmp_make

115         (for i in init/*.c;do echo -n "init/";$(CPP) -M $$i;done) >> tmp_make

116         cp tmp_make Makefile

117         (cd fs; make dep)                  # fs/目录下的Makefile文件也作同样的处理。

118         (cd kernel; make dep)

119         (cd mm; make dep)

120

121 ### Dependencies:

122 init/main.o : init/main.c include/unistd.h include/sys/stat.h \

123   include/sys/types.h include/sys/time.h include/time.h include/sys/times.h \

124   include/sys/utsname.h include/sys/param.h include/sys/resource.h \

125   include/utime.h include/linux/tty.h include/termios.h include/linux/sched.h \

126   include/linux/head.h include/linux/fs.h include/linux/mm.h \

127   include/linux/kernel.h include/signal.h include/asm/system.h \

128   include/asm/io.h include/stddef.h include/stdarg.h include/fcntl.h \

129   include/string.h


 


 

 

第6章 引导启动程序

6.1 程序6 ‑1 linux/boot/bootsect.S


  1 !

  2 ! SYS_SIZE is the number of clicks (16 bytes) to be loaded.

  3 ! 0x3000 is 0x30000 bytes = 196kB, more than enough for current

  4 ! versions of linux

    ! SYS_SIZE是要加载的系统模块长度,单位是节,每节16字节。0x3000共为0x30000字节=196KB

    ! 若以1024字节为1KB计,则应该是192KB。对于当前内核版本这个空间长度已足够了。当该值为

    ! 0x8000时,表示内核最大为512KB。因为内存0x90000处开始存放移动后的bootsectsetup

    ! 的代码,因此该值最大不得超过0x9000(表示584KB)。

    ! 这里感叹号'!'或分号';'表示程序注释语句开始。

  5 !

    ! 头文件linux/config.h中定义了内核用到的一些常数符号和Linus自己使用的默认硬盘参数块。

    ! 例如其中定义了以下一些常数:

    ! DEF_SYSSIZE  = 0x3000 - 默认系统模块长度。单位是节,每节为16字节;

    ! DEF_INITSEG  = 0x9000 - 默认本程序代码移动目的段位置;

    ! DEF_SETUPSEG = 0x9020 - 默认setup程序代码段位置;

    ! DEF_SYSSEG   = 0x1000 - 默认从磁盘加载系统模块到内存的段位置。

  6 #include <linux/config.h>

  7 SYSSIZE = DEF_SYSSIZE           ! 定义一个标号或符号。指明编译连接后system模块的大小。

  8 !

  9 !       bootsect.s              (C) 1991 Linus Torvalds

 10 !       modified by Drew Eckhardt

 11 !

 12 ! bootsect.s is loaded at 0x7c00 by the bios-startup routines, and moves

 13 ! iself out of the way to address 0x90000, and jumps there.

 14 !

 15 ! It then loads 'setup' directly after itself (0x90200), and the system

 16 ! at 0x10000, using BIOS interrupts.

 17 !

 18 ! NOTE! currently system is at most 8*65536 bytes long. This should be no

 19 ! problem, even in the future. I want to keep it simple. This 512 kB

 20 ! kernel size should be enough, especially as this doesn't contain the

 21 ! buffer cache as in minix

 22 !

 23 ! The loader has been made as simple as possible, and continuos

 24 ! read errors will result in a unbreakable loop. Reboot by hand. It

 25 ! 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 (31KB)处,并将自己移到了地址0x90000

    ! (576KB)处,并跳转至那里。

    !

    ! 它然后使用BIOS中断将'setup'直接加载到自己的后面(0x90200)(576.5KB),并将system

    ! 载到地址0x10000处。

    !

    ! 注意! 目前的内核系统最大长度限制为(8*65536)(512KB)字节,即使是在将来这也应该没有问

    ! 题的。我想让它保持简单明了。这样512KB的最大内核长度应该足够了,尤其是这里没有象

    ! MINIX中一样包含缓冲区高速缓冲。

    !

    ! 加载程序已经做得够简单了,所以持续地读操作出错将导致死循环。只能手工重启。只要可能,

    ! 通过一次读取所有的扇区,加载过程可以做得很快。

 26

    ! 伪指令(伪操作符).globl.global用于定义随后的标识符是外部的或全局的,并且即使不

    ! 使用也强制引入。 .text.data.bss用于分别定义当前代码段、数据段和未初始化数据段。

    ! 在链接多个目标模块时,链接程序(ld86)会根据它们的类别把各个目标模块中的相应段分别

    ! 组合(合并)在一起。这里把三个段都定义在同一重叠地址范围中,因此本程序实际上不分段。

    ! 另外,后面带冒号的字符串是标号,例如下面的'begtext:'

    ! 一条汇编语句通常由标号(可选)、指令助记符(指令名)和操作数三个字段组成。标号位于

    ! 一条指令的第一个字段。它代表其所在位置的地址,通常指明一个跳转指令的目标位置。

 27 .globl begtext, begdata, begbss, endtext, enddata, endbss

 28 .text                              ! 文本段(代码段)。

 29 begtext:

 30 .data                              ! 数据段。

 31 begdata:

 32 .bss                               ! 未初始化数据段。

 33 begbss:

 34 .text                              ! 文本段(代码段)。

 35

    ! 下面等号'='或符号'EQU'用于定义标识符或标号所代表的值。

 36 SETUPLEN = 4                       ! nr of setup-sectors

                                       ! setup程序代码占用磁盘扇区数(setup-sectors)值;

 37 BOOTSEG  = 0x07c0                  ! original address of boot-sector

                                       ! bootsect代码所在内存原始段地址;

 38 INITSEG  = DEF_INITSEG             ! we move boot here - out of the way

                                       ! bootsect移到位置0x90000 - 避开系统模块占用处;

 39 SETUPSEG = DEF_SETUPSEG            ! setup starts here

                                       ! setup程序从内存0x90200处开始;

 40 SYSSEG   = DEF_SYSSEG              ! system loaded at 0x10000 (65536).

                                       ! system模块加载到0x1000064 KB)处;

 41 ENDSEG   = SYSSEG + SYSSIZE        ! where to stop loading

                                       ! 停止加载的段地址;

 42

 43 ! ROOT_DEV & SWAP_DEV are now written by "build".

    ! 根文件系统设备号ROOT_DEV和交换设备号SWAP_DEV 现在由tools目录下的build程序写入。

    ! 设备号0x306指定根文件系统设备是第2个硬盘的第1个分区。当年Linus是在第2个硬盘上

    ! 安装了Linux 0.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版后就已经使用与现在内核相同的命名方法了。

 44 ROOT_DEV = 0                       ! 根文件系统设备使用与系统引导时同样的设备;

 45 SWAP_DEV = 0                       ! 交换设备使用与系统引导时同样的设备;

 46

    ! 伪指令entry迫使链接程序在生成的执行程序(a.out)中包含指定的标识符或标号。这里是

    ! 程序执行开始点。49 -- 58行作用是将自身(bootsect)从目前段位置 0x07c0(31KB) 移动到

    ! 0x9000(576KB) 处,共256字(512字节),然后跳转到移动后代码的go标号处,也即本程

    ! 序的下一语句处。

 47 entry start                        ! 告知链接程序,程序从start标号开始执行。

 48 start:

 49         mov     ax,#BOOTSEG        ! ds段寄存器置为0x7C0

 50         mov     ds,ax

 51         mov     ax,#INITSEG        ! es段寄存器置为0x9000

 52         mov     es,ax

 53         mov     cx,#256            ! 设置移动计数值=256字(512字节);

 54         sub     si,si              ! 源地址   ds:si = 0x07C0:0x0000

 55         sub     di,di              ! 目的地址 es:di = 0x9000:0x0000

 56         rep                        ! 重复执行并递减cx的值,直到cx = 0为止。

 57         movw                       ! movs指令。从内存[si]处移动cx个字到[di]处。

 58         jmpi    go,INITSEG         ! 段间跳转(Jump Intersegment)。这里INITSEG

                                       ! 指出跳转到的段地址,标号go是段内偏移地址。

 59

    ! 从下面开始,CPU在已移动到0x90000位置处的代码中执行。

    ! 这段代码设置几个段寄存器,包括栈寄存器sssp。栈指针sp只要指向远大于512字节偏移

    ! (即地址0x90200)处都可以。因为从0x90200地址开始处还要放置setup程序,而此时setup

    ! 程序大约为4个扇区,因此sp要指向大于(0x200 + 0x200 * 4 +堆栈大小)位置处。这里sp

    ! 设置为 0x9ff00 - 12(参数表长度),即sp = 0xfef4。在此之上位置会存放一个自建的驱动

    ! 器参数表,见下面说明。实际上BIOS把引导扇区加载到0x7c00 处并把执行权交给引导程序时,

    ! ss = 0x00sp = 0xfffe

    ! 另外,第65行上push指令的期望作用是想暂时把段值保留在栈中,然后等下面执行完判断磁道

    ! 扇区数后再弹出栈,并给段寄存器 fsgs赋值(第109行)。但是由于第6768两语句修改

    ! 了栈段的位置,因此除非在执行栈弹出操作之前把栈段恢复到原位置,否则这样设计就是错误的。

    ! 因此这里存在一个bug。改正的方法之一是去掉第65行,并把第109行修改成“mov ax,cs”。

 60 go:     mov     ax,cs              ! dsesss都置成移动后代码所在的段处(0x9000)

 61         mov     dx,#0xfef4         ! arbitrary value >>512 - disk parm size

 62

 63         mov     ds,ax

 64         mov     es,ax

 65         push    ax                 ! 临时保存段值(0x9000),供109行使用。(滑头!

 66

 67         mov     ss,ax              ! put stack at 0x9ff00 - 12.

 68         mov     sp,dx

 69 /*

 70  *      Many BIOS's default disk parameter tables will not

 71  *      recognize multi-sector reads beyond the maximum sector number

 72  *      specified in the default diskette parameter tables - this may

 73  *      mean 7 sectors in some cases.

 74  *

 75  *      Since single sector reads are slow and out of the question,

 76  *      we must take care of this by creating new parameter tables

 77  *      (for the first disk) in RAM.  We will set the maximum sector

 78  *      count to 18 - the most we will encounter on an HD 1.44. 

 79  *

 80  *      High doesn't hurt.  Low does.

 81  *

 82  *      Segments are as follows: ds=es=ss=cs - INITSEG,

 83  *              fs = 0, gs = parameter table segment

 84  */

    /*

     *      对于多扇区读操作所读的扇区数超过默认磁盘参数表中指定的最大扇区数时,

     *      很多BIOS将不能进行正确识别。在某些情况下是7个扇区。

     *

     *      由于单扇区读操作太慢,不予以考虑,因此我们必须通过在内存中重创建新的

     *      参数表(为第1个驱动器)来解决这个问题。我们将把其中最大扇区数设置为

     *      18 -- 即在1.44MB磁盘上会碰到的最大数值。

     *     

     *      这个数值大了不会出问题,但是太小就不行了。

     *

     *      段寄存器将被设置成:ds=es=ss=cs - 都为INITSEG0x9000),

     *      fs = 0gs = 参数表所在段值。

     */

 85 ! BIOS设置的中断0x1E的中断向量值是软驱参数表地址。该向量值位于内存0x1E * 4 = 0x78

    ! 处。这段代码首先从内存0x0000:0x0078处复制原软驱参数表到0x9000:0xfef4处,然后修改

    ! 表中的每磁道最大扇区数为18

 86

 87         push    #0                 ! 置段寄存器fs = 0

 88         pop     fs                 ! fs:bx指向存有软驱参数表地址处(指针的指针)。

 89         mov     bx,#0x78           ! fs:bx is parameter table address

    ! 下面指令表示下一条语句的操作数在fs段寄存器所指的段中。它只影响其下一条语句。这里

    ! fs:bx 所指内存位置处的表地址放到寄存器对 gs:si 中作为原地址。寄存器对 es:di =

    ! 0x9000:0xfef4 为目的地址。

 90         seg fs

 91         lgs     si,(bx)            ! gs:si is source

 92

 93         mov     di,dx              ! es:di is destination ! dx=0xfef4,在61行被设置。

 94         mov     cx,#6              ! copy 12 bytes

 95         cld                        ! 清方向标志。复制时指针递增。

 96

 97         rep                        ! 复制12字节的软驱参数表到0x9000:0xfef4处。

 98         seg gs

 99         movw

100

101         mov     di,dx              ! es:di指向新表,修改表中偏移4处的最大扇区数为18

102         movb    4(di),*18          ! patch sector count

103

104         seg fs                     ! 让中断向量0x1E的值指向新表。

105         mov     (bx),di

106         seg fs

107         mov     2(bx),es

108

109         pop     ax                 ! 此时ax中是上面第65行保留下来的段值(0x9000)。

110         mov     fs,ax              ! 设置fs = gs = 0x9000

111         mov     gs,ax

112        

113         xor     ah,ah              ! reset FDC ! 复位软盘控制器,让其采用新参数。

114         xor     dl,dl              ! dl = 0,第1个软驱。

115         int     0x13   

116

117 ! load the setup-sectors directly after the bootblock.

118 ! Note that 'es' is already set up.

    ! bootsect程序块后紧根着加载setup模块的代码数据。

    ! 注意es已经设置好了。(在移动代码时es已经指向目的段地址处0x9000)。

119

    ! 121--137行的用途是利用ROM 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中是出错码。

120 load_setup:

121         xor     dx, dx                  ! drive 0, head 0

122         mov     cx,#0x0002              ! sector 2, track 0

123         mov     bx,#0x0200              ! address = 512, in INITSEG

124         mov     ax,#0x0200+SETUPLEN     ! service 2, nr of sectors

125         int     0x13                    ! read it

126         jnc     ok_load_setup           ! ok - continue

127

128         push    ax                      ! dump error code ! 显示出错信息。出错码入栈。

129         call    print_nl                ! 屏幕光标回车。

130         mov     bp, sp                  ! ss:bp指向欲显示的字(word)。

131         call    print_hex               ! 显示十六进制值。

132         pop     ax     

133        

134         xor     dl, dl                  ! reset FDC ! 复位磁盘控制器,重试。

135         xor     ah, ah

136         int     0x13

137         j       load_setup              ! j jmp指令。

138

139 ok_load_setup:

140

141 ! Get disk drive parameters, specifically nr of sectors/track

    ! 这段代码取磁盘驱动器的参数,实际上是取每磁道扇区数,并保存在位置sectors处。

    ! 取磁盘驱动器参数INT 0x13调用格式和返回信息如下:

    ! ah = 0x08     dl = 驱动器号(如果是硬盘则要置位71)。

    ! 返回信息:

    ! 如果出错则CF置位,并且ah = 状态码。

    ! ah = 0 al = 0          bl = 驱动器类型(AT/PS2

    ! ch = 最大磁道号的低8位,  cl = 每磁道最大扇区数(0-5),最大磁道号高2(6-7)

    ! dh = 最大磁头数,          dl = 驱动器数量,

    ! es:di -à 软驱磁盘参数表。

142

143         xor     dl,dl

144         mov     ah,#0x08           ! AH=8 is get drive parameters

145         int     0x13

146         xor     ch,ch

    ! 下面指令表示下一条语句的操作数在 cs段寄存器所指的段中。它只影响其下一条语句。实际

    ! 上,由于本程序代码和数据都被设置处于同一个段中,即段寄存器csdses的值相同,因

    ! 此本程序中此处可以不使用该指令。

147         seg cs

    ! 下句保存每磁道扇区数。对于软盘来说(dl=0),其最大磁道号不会超过256ch已经足够表

    ! 示它,因此cl的位6-7肯定为0。又146行已置ch=0,因此此时cx中是每磁道扇区数。

148         mov     sectors,cx

149         mov     ax,#INITSEG

150         mov     es,ax              ! 因为上面取磁盘参数中断改了es值,这里重新改回。

151

152 ! Print some inane message

    ! 显示信息:“'Loading'+回车+换行”,共显示包括回车和换行控制字符在内的9个字符。

    ! BIOS中断0x10功能号 ah = 0x03,读光标位置。

    ! 输入:bh = 页号

    ! 返回:ch = 扫描开始线;cl = 扫描结束线;dh = 行号(0x00顶端)dl = 列号(0x00最左边)

    !

    ! BIOS中断0x10功能号 ah = 0x13,显示字符串。

    ! 输入:al = 放置光标的方式及规定属性。0x01-表示使用bl中的属性值,光标停在字符串结尾处。

    ! es:bp 此寄存器对指向要显示的字符串起始位置处。cx = 显示的字符串字符数。bh = 显示页面号;

    ! bl = 字符属性。dh = 行号;dl = 列号。

153

154         mov     ah,#0x03           ! read cursor pos

155         xor     bh,bh              ! 首先读光标位置。返回光标位置值在dx中。

156         int     0x10               ! dh - 行(0--24);dl - (0--79)

157        

158         mov     cx,#9              ! 共显示9个字符。

159         mov     bx,#0x0007         ! page 0, attribute 7 (normal)

160         mov     bp,#msg1           ! es:bp指向要显示的字符串。

161         mov     ax,#0x1301         ! write string, move cursor

162         int     0x10               ! 写字符串并移动光标到串结尾处。

163

164 ! ok, we've written the message, now

165 ! we want to load the system (at 0x10000)

    ! 现在开始将system模块加载到0x1000064KB)开始处。

166

167         mov     ax,#SYSSEG

168         mov     es,ax              ! segment of 0x010000 ! es = 存放system的段地址。

169         call    read_it            ! 读磁盘上system模块,es为输入参数。

170         call    kill_motor         ! 关闭驱动器马达,这样就可以知道驱动器的状态了。

171         call    print_nl           ! 光标回车换行。

172

173 ! After that we check which root-device to use. If the device is

174 ! defined (!= 0), nothing is done and the given device is used.

175 ! Otherwise, either /dev/PS0 (2,28) or /dev/at0 (2,8), depending

176 ! on the number of sectors that the BIOS reports currently.

    ! 此后,我们检查要使用哪个根文件系统设备(简称根设备)。如果已经指定了设备(!=0)

    ! 就直接使用给定的设备。否则就需要根据BIOS报告的每磁道扇区数来确定到底使用/dev/PS0

    ! (2,28),还是 /dev/at0 (2,8)

    !! 上面一行中两个设备文件的含义:

    !! Linux中软驱的主设备号是2(参见第43行的注释),次设备号 = type*4 + nr,其中

    !! nr0-3分别对应软驱ABCDtype是软驱的类型(2à1.2MB7à1.44MB等)。

    !! 因为7*4 + 0 = 28,所以 /dev/PS0 (2,28)指的是1.44MB A驱动器,其设备号是0x021c

    !! 同理 /dev/at0 (2,8)指的是1.2MB A驱动器,其设备号是0x0208

 

    ! 下面root_dev定义在引导扇区508509字节处,指根文件系统所在设备号。0x0306指第2

    ! 个硬盘第1个分区。这里默认为0x0306是因为当时 Linus 开发Linux系统时是在第2个硬

    ! 盘第1个分区中存放根文件系统。这个值需要根据你自己根文件系统所在硬盘和分区进行修

    ! 改。例如,如果你的根文件系统在第1个硬盘的第1个分区上,那么该值应该为0x0301,即

    ! 0x01, 0x03)。如果根文件系统是在第2Bochs软盘上,那么该值应该为0x021D,即

    ! 0x1D,0x02)。当编译内核时,你可以在Makefile文件中另行指定你自己的值,内核映像

    ! 文件Image的创建程序tools/build会使用你指定的值来设置你的根文件系统所在设备号。

177

178         seg cs

179         mov     ax,root_dev        ! 508,509字节处的根设备号并判断是否已被定义。

180         or      ax,ax

181         jne     root_defined

    ! 取上面第148行保存的每磁道扇区数。如果sectors=15则说明是1.2MB的驱动器;如果

    ! sectors=18,则说明是1.44MB软驱。因为是可引导的驱动器,所以肯定是A驱。

182         seg cs

183         mov     bx,sectors

184         mov     ax,#0x0208         ! /dev/ps0 - 1.2Mb

185         cmp     bx,#15             ! 判断每磁道扇区数是否=15

186         je      root_defined       ! 如果等于,则ax中就是引导驱动器的设备号。

187         mov     ax,#0x021c         ! /dev/PS0 - 1.44Mb

188         cmp     bx,#18

189         je      root_defined

190 undef_root:                        ! 如果都不一样,则死循环(死机)。

191         jmp undef_root

192 root_defined:

193         seg cs

194         mov     root_dev,ax        ! 将检查过的设备号保存到root_dev中。

195

196 ! after that (everyting loaded), we jump to

197 ! the setup-routine loaded directly after

198 ! the bootblock:

    ! 到此,所有程序都加载完毕,我们就跳转到被加载在bootsect后面的setup程序去。

    ! 下面段间跳转指令(Jump Intersegment)。跳转到0x9020:0000(setup.s程序开始处)去执行。

199

200         jmpi    0,SETUPSEG         !!!! 到此本程序就结束了。!!!!

 

    ! 下面是几个子程序。read_it用于读取磁盘上的system模块。kill_moter用于关闭软驱马达。

    ! 还有一些屏幕显示子程序。

201

202 ! This routine loads the system at address 0x10000, making sure

203 ! no 64kB boundaries are crossed. We try to load it as fast as

204 ! possible, loading whole tracks whenever we can.

205 !

206 ! in:   es - starting address segment (normally 0x1000)

207 !

    ! 该子程序将系统模块加载到内存地址0x10000处,并确定没有跨越64KB的内存边界。

    ! 我们试图尽快地进行加载,只要可能,就每次加载整条磁道的数据。

    ! 输入:es – 开始内存地址段值(通常是0x1000

    !

    ! 下面伪操作符.word定义一个2字节目标。相当于C语言程序中定义的变量和所占内存空间大小。

    ! '1+SETUPLEN'表示开始时已经读进1个引导扇区和setup程序所占的扇区数SETUPLEN

208 sread:  .word 1+SETUPLEN        ! sectors read of current track !当前磁道中已读扇区数。

209 head:   .word 0                 ! current head    !当前磁头号。

210 track:  .word 0                 ! current track   !当前磁道号。

211

212 read_it:

    ! 首先测试输入的段值。从盘上读入的数据必须存放在位于内存地址 64KB 的边界开始处,否则

    ! 进入死循环。清bx寄存器,用于表示当前段内存放数据的开始位置。

    ! 153行上的指令test以比特位逻辑与两个操作数。若两个操作数对应的比特位都为1,则结果

    ! 值的对应比特位为1,否则为0。该操作结果只影响标志(零标志ZF等)。例如若AX=0x1000

    ! 那么test指令的执行结果是(0x1000 & 0x0fff) = 0x0000,于是ZF标志置位。此时即下一条

    ! 指令jne 条件不成立。

213         mov ax,es

214         test ax,#0x0fff

215 die:    jne die                 ! es must be at 64kB boundary ! es值必须位于64KB边界!

216         xor bx,bx               ! bx is starting address within segment! bx为段内偏移。

217 rp_read:

    ! 接着判断是否已经读入全部数据。比较当前所读段是否就是系统数据末端所处的段(#ENDSEG)

    ! 如果不是就跳转至下面ok1_read标号处继续读数据。否则退出子程序返回。

218         mov ax,es

219         cmp ax,#ENDSEG          ! have we loaded all yet?    ! 是否已经加载了全部数据?

220         jb ok1_read

221         ret

222 ok1_read:

    ! 然后计算和验证当前磁道需要读取的扇区数,放在ax寄存器中。

    ! 根据当前磁道还未读取的扇区数以及段内数据字节开始偏移位置,计算如果全部读取这些未读

    ! 扇区,所读总字节数是否会超过64KB段长度的限制。若会超过,则根据此次最多能读入的字节

    ! (64KB –段内偏移位置),反算出此次需要读取的扇区数。

223         seg cs

224         mov ax,sectors             ! 取每磁道扇区数。

225         sub ax,sread               ! 减去当前磁道已读扇区数。

226         mov cx,ax                  ! cx = ax = 当前磁道未读扇区数。

227         shl cx,#9                  ! cx = cx * 512 字节 + 段内当前偏移值(bx)

228         add cx,bx                  !   = 此次读操作后,段内共读入的字节数。

229         jnc ok2_read               ! 若没有超过64KB字节,则跳转至ok2_read处执行。

230         je ok2_read

    ! 若加上此次将读磁道上所有未读扇区时会超过64KB,则计算此时最多能读入的字节数:

    ! (64KB–段内读偏移位置),再转换成需读取的扇区数。其中0减某数就是取该数64KB的补值。

231         xor ax,ax

232         sub ax,bx

233         shr ax,#9

234 ok2_read:

    ! 读当前磁道上指定开始扇区(cl)和需读扇区数(al)的数据到 es:bx开始处。然后统计当前

    ! 磁道上已经读取的扇区数并与磁道最大扇区数 sectors作比较。如果小于sectors说明当前磁

    ! 道上的还有扇区未读。于是跳转到ok3_read处继续操作。

235         call read_track            ! 读当前磁道上指定开始扇区和需读扇区数的数据。

236         mov cx,ax                  ! cx = 该次操作已读取的扇区数。

237         add ax,sread               ! 加上当前磁道上已经读取的扇区数。

238         seg cs

239         cmp ax,sectors             ! 若当前磁道上的还有扇区未读,则跳转到ok3_read处。

240         jne ok3_read

    ! 若该磁道的当前磁头面所有扇区已经读取,则读该磁道的下一磁头面(1号磁头)上的数据。

    ! 如果已经完成,则去读下一磁道。

241         mov ax,#1

242         sub ax,head                ! 判断当前磁头号。

243         jne ok4_read               ! 如果是0磁头,则再去读1磁头面上的扇区数据。

244         inc track                  ! 否则去读下一磁道。

245 ok4_read:

246         mov head,ax                ! 保存当前磁头号。

247         xor ax,ax                  ! 清当前磁道已读扇区数。

248 ok3_read:

    ! 如果当前磁道上的还有未读扇区,则首先保存当前磁道已读扇区数,然后调整存放数据处的开

    ! 始位置。若小于64KB边界值,则跳转到rp_read(217)处,继续读数据。

249         mov sread,ax               ! 保存当前磁道已读扇区数。

250         shl cx,#9                  ! 上次已读扇区数*512字节。

251         add bx,cx                  ! 调整当前段内数据开始位置。

252         jnc rp_read

    ! 否则说明已经读取64KB数据。此时调整当前段,为读下一段数据作准备。

253         mov ax,es

254         add ah,#0x10               ! 将段基址调整为指向下一个64KB内存开始处。

255         mov es,ax

256         xor bx,bx                  ! 清段内数据开始偏移值。

257         jmp rp_read                ! 跳转至rp_read(217)处,继续读数据。

258

    ! read_track 子程序。读当前磁道上指定开始扇区和需读扇区数的数据到 es:bx 开始处。参见

    ! 67行下对BIOS磁盘读中断int 0x13ah=2的说明。

    ! al – 需读扇区数;es:bx – 缓冲区开始位置。

259 read_track:

    ! 首先调用BIOS中断0x10,功能0x0e(以电传方式写字符),光标前移一位置。显示一个'.'

260         pusha                      ! 压入所有寄存器(push all)。

261         pusha                      ! 为调用显示中断压入所有寄存器值。

262         mov ax, #0xe2e             ! loading... message 2e = .

263         mov bx, #7                 ! 字符前景色属性。

264         int 0x10

265         popa           

266

    ! 然后正式进行磁道扇区读操作。

267         mov dx,track               ! 取当前磁道号。

268         mov cx,sread               ! 取当前磁道上已读扇区数。

269         inc cx                     ! cl = 开始读扇区。

270         mov ch,dl                  ! ch = 当前磁道号。

271         mov dx,head                ! 取当前磁头号。

272         mov dh,dl                  ! dh = 磁头号,dl = 驱动器号(0表示当前A驱动器)

273         and dx,#0x0100             ! 磁头号不大于1

274         mov ah,#2                  ! ah = 2,读磁盘扇区功能号。

275        

276         push dx                    ! save for error dump

277         push cx                    ! 为出错情况保存一些信息。

278         push bx

279         push ax

280

281         int 0x13

282         jc bad_rt                  ! 若出错,则跳转至bad_rt

283         add sp,#8                  ! 没有出错。因此丢弃为出错情况保存的信息。

284         popa

285         ret

286

    ! 读磁盘操作出错。则先显示出错信息,然后执行驱动器复位操作(磁盘中断功能号0),再跳转

    ! read_track处重试。

287 bad_rt: push ax                    ! save error code

288         call print_all             ! ah = error, al = read

289        

290        

291         xor ah,ah

292         xor dl,dl

293         int 0x13

294        

295

296         add sp, #10                ! 丢弃为出错情况保存的信息。

297         popa   

298         jmp read_track

299

300 /*

301  *      print_all is for debugging purposes. 

302  *      It will print out all of the registers.  The assumption is that this is

303  *      called from a routine, with a stack frame like

304  *      dx

305  *      cx

306  *      bx

307  *      ax

308  *      error

309  *      ret <- sp

310  *

311 */

    /*

     *      子程序print_all用于调试目的。它会显示所有寄存器的内容。前提条件是需要从

     *      一个子程序中调用,并且栈帧结构为如下所示:(见上面)

     */

    ! 若标志寄存器的CF=0,则不显示寄存器名称。

312 

313 print_all:

314         mov cx, #5                 ! error code + 4 registers  ! 显示值个数。

315         mov bp, sp                 ! 保存当前栈指针sp

316

317 print_loop:

318         push cx                    ! save count left   ! 保存需要显示的剩余个数。

319         call print_nl              ! nl for readability  ! 为可读性先让光标回车换行。

320         jae no_reg                 ! see if register name is needed

321                                    ! FLAGS的标志CF=0则不显示寄存器名,于是跳转。

    ! 对应入栈寄存器顺序分别显示它们的名称“AX:”等。

322         mov ax, #0xe05 + 0x41 - 1  ! ah =功能号(0x0e);al =字符(0x05 + 0x41 -1)。

323         sub al, cl

324         int 0x10

325

326         mov al, #0x58              ! X     ! 显示字符'X'

327         int 0x10

328

329         mov al, #0x3a              ! :     ! 显示字符':'

330         int 0x10

331

    ! 显示寄存器bp所指栈中内容。开始时bp指向返回地址。

332 no_reg:

333         add bp, #2                 ! next register   ! 栈中下一个位置。

334         call print_hex             ! print it        ! 以十六进制显示。

335         pop cx

336         loop print_loop

337         ret

338

    ! 调用BIOS中断0x10,以电传方式显示回车换行。

339 print_nl:

340         mov ax, #0xe0d             ! CR

341         int 0x10

342         mov al, #0xa               ! LF

343         int 0x10

344         ret

345

346 /*

347  *      print_hex is for debugging purposes, and prints the word

348  *      pointed to by ss:bp in hexadecmial.

349 */

    /*

     *      子程序print_hex用于调试目的。它使用十六进制在屏幕上显示出

     *      ss:bp指向的字。

     */

350

    ! 调用BIOS中断0x10,以电传方式和4个十六进制数显示ss:bp指向的字。

351 print_hex:

352         mov     cx, #4             ! 4 hex digits         ! 要显示4个十六进制数字。

353         mov     dx, (bp)           ! load word into dx    ! 显示值放入dx中。

354 print_digit:

    ! 先显示高字节,因此需要把dx中值左旋4比特,此时高4比特在dx的低4位中。

355         rol     dx, #4             ! rotate so that lowest 4 bits are used

356         mov     ah, #0xe           ! 中断功能号。

357         mov     al, dl             ! mask off so we have only next nibble

358         and     al, #0xf           ! 放入al中并只取低4比特(1个值)。

    ! 加上'0' ASCII码值0x30,把显示值转换成基于数字'0' 的字符。若此时al 值超过 0x39

    ! 表示欲显示值超过数字9,因此需要使用'A'--'F'来表示。

359         add     al, #0x30          ! convert to 0 based digit, '0'

360         cmp     al, #0x39          ! check for overflow

361         jbe     good_digit

362         add     al, #0x41 - 0x30 - 0xa       ! 'A' - '0' - 0xa

363

364 good_digit:

365         int     0x10

366         loop    print_digit        ! cx--。若cx>0则去显示下一个值。

367         ret

368

369

370 /*

371  * This procedure turns off the floppy drive motor, so

372  * that we enter the kernel in a known state, and

373  * don't have to worry about it later.

374  */

    /* 这个子程序用于关闭软驱的马达,这样我们进入内核后就能

     * 知道它所处的状态,以后也就无须担心它了。

     */

    ! 下面第377行上的值0x3f2是软盘控制器的一个端口,被称为数字输出寄存器(DOR)端口。它是

    ! 一个8位的寄存器,其位7--4分别用于控制4个软驱(D--A)的启动和关闭。位3--2用于

    ! 允许/禁止DMA和中断请求以及启动/复位软盘控制器FDC 1--0用于选择选择操作的软驱。

    ! 378行上在al中设置并输出的0值,就是用于选择A驱动器,关闭FDC,禁止DMA和中断请求,

    ! 关闭马达。有关软驱控制卡编程的详细信息请参见kernel/blk_drv/floppy.c程序后面的说明。

375 kill_motor:

376         push dx

377         mov dx,#0x3f2              ! 软驱控制卡的数字输出寄存器端口,只写。

378         xor al, al                 ! A驱动器,关闭FDC,禁止DMA和中断请求,关闭马达。

379         outb                       ! al中的内容输出到dx指定的端口去。

380         pop dx

381         ret

382

383 sectors:

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

385

386 msg1:                              ! 开机调用BIOS中断显示的信息。共9个字符。

387         .byte 13,10                ! 回车、换行的ASCII码。

388         .ascii "Loading"

389

    ! 表示下面语句从地址508(0x1FC)开始,所以root_dev在启动扇区的第508开始的2个字节中。

390 .org 506

391 swap_dev:

392         .word SWAP_DEV             ! 这里存放交换系统所在设备号(init/main.c中会用)

393 root_dev:

394         .word ROOT_DEV             ! 这里存放根文件系统所在设备号(init/main.c中会用)

 

    ! 下面是启动盘具有有效引导扇区的标志。仅供BIOS中的程序加载引导扇区时识别使用。它必须

    ! 位于引导扇区的最后两个字节中。

395 boot_flag:

396         .word 0xAA55

397

398 .text

399 endtext:

400 .data

401 enddata:

402 .bss

403 endbss:

404


 

 


 

6.2 程序 6‑2 linux/boot/setup.S


  1 !

  2 !       setup.s         (C) 1991 Linus Torvalds

  3 !

  4 ! setup.s is responsible for getting the system data from the BIOS,

  5 ! and putting them into the appropriate places in system memory.

  6 ! both setup.s and system has been loaded by the bootblock.

  7 !

  8 ! This code asks the bios for memory/disk/other parameters, and

  9 ! puts them in a "safe" place: 0x90000-0x901FF, ie where the

 10 ! boot-block used to be. It is then up to the protected mode

 11 ! system to read them from there before the area is overwritten

 12 ! for buffer-blocks.

 13 !

    ! setup.s负责从BIOS中获取系统数据,并将这些数据放到系统内存的适当

    ! 地方。此时setup.ssystem已经由bootsect引导块加载到内存中。

    !

    ! 这段代码询问bios有关内存/磁盘/其他参数,并将这些参数放到一个

    ! “安全的”地方:0x90000-0x901FF,也即原来bootsect代码块曾经在

    ! 的地方,然后在被缓冲块覆盖掉之前由保护模式的system读取。

 14

 15 ! NOTE! These had better be the same as in bootsect.s!

    ! 以下这些参数最好和bootsect.s中的相同!

 16 #include <linux/config.h>

    ! config.h中定义了DEF_INITSEG = 0x9000DEF_SYSSEG = 0x1000DEF_SETUPSEG = 0x9020

 17

 18 INITSEG  = DEF_INITSEG  ! we move boot here - out of the way ! 原来bootsect所处的段。

 19 SYSSEG   = DEF_SYSSEG   ! system loaded at 0x10000 (65536).  ! system0x10000处。

 20 SETUPSEG = DEF_SETUPSEG ! this is the current segment        ! 本程序所在的段地址。

 21

 22 .globl begtext, begdata, begbss, endtext, enddata, endbss

 23 .text

 24 begtext:

 25 .data

 26 begdata:

 27 .bss

 28 begbss:

 29 .text

 30

 31 entry start

 32 start:

 33

 34 ! ok, the read went well so we get current cursor position and save it for

 35 ! posterity.

    ! ok,整个读磁盘过程都正常,现在将光标位置保存以备今后使用(相关代码在59--62行)。

 36

    ! 下句将ds置成INITSEG(0x9000)。这已经在bootsect程序中设置过,但是现在是setup程序,

    ! Linus觉得需要再重新设置一下。

 37         mov     ax,#INITSEG

 38         mov     ds,ax

 39

 40 ! Get memory size (extended mem, kB)

    ! 取扩展内存的大小值(KB)。

    ! 利用BIOS中断0x15 功能号ah = 0x88 取系统所含扩展内存大小并保存在内存0x90002处。

    ! 返回:ax = 0x1000001M)处开始的扩展内存大小(KB)。若出错则CF置位,ax = 出错码。

 41

 42         mov     ah,#0x88

 43         int     0x15

 44         mov     [2],ax             ! 将扩展内存数值存在0x90002处(1个字)。

 45

 46 ! check for EGA/VGA and some config parameters

    ! 检查显示方式(EGA/VGA)并取参数。

    ! 调用BIOS中断0x10,附加功能选择方式信息。功能号:ah = 0x12bl = 0x10

    ! 返回:bh =显示状态。0x00 -彩色模式,I/O端口=0x3dX0x01 -单色模式,I/O端口=0x3bX

    ! bl = 安装的显示内存。0x00 - 64k0x01 - 128k0x02 - 192k0x03 = 256k

    ! cx = 显示卡特性参数(参见程序后对BIOS视频中断0x10的说明)

 47

 48         mov     ah,#0x12

 49         mov     bl,#0x10

 50         int     0x10

 51         mov     [8],ax           ! 0x90008 = ??

 52         mov     [10],bx          ! 0x9000A =安装的显示内存;0x9000B=显示状态(/单色)

 53         mov     [12],cx          ! 0x9000C =显示卡特性参数。

    ! 检测屏幕当前行列值。若显示卡是VGA卡时则请求用户选择显示行列值,并保存到0x9000E处。

 54         mov     ax,#0x5019       ! ax中预置屏幕默认行列值(ah = 80列;al=25行)。

 55         cmp     bl,#0x10         ! 若中断返回bl值为0x10,则表示不是VGA显示卡,跳转。

 56         je      novga

 57         call    chsvga           ! 检测显示卡厂家和类型,修改显示行列值(第215行)。

 58 novga:  mov     [14],ax          ! 保存屏幕当前行列值(0x9000E0x9000F)。

 

    ! 这段代码使用BIOS中断取屏幕当前光标位置(列、行),并保存在内存0x90000处(2字节)。

    ! 控制台初始化程序会到此处读取该值。

    ! BIOS中断0x10功能号 ah = 0x03,读光标位置。

    ! 输入:bh = 页号

    ! 返回:ch = 扫描开始线;cl = 扫描结束线;dh = 行号(0x00顶端)dl = 列号(0x00最左边)

 59         mov     ah,#0x03         ! read cursor pos

 60         xor     bh,bh

 61         int     0x10             ! save it in known place, con_init fetches

 62         mov     [0],dx           ! it from 0x90000.

 63        

 64 ! Get video-card data:

    ! 下面这段用于取显示卡当前显示模式。

    ! 调用BIOS中断0x10,功能号 ah = 0x0f

    ! 返回:ah = 字符列数;al = 显示模式;bh = 当前显示页。

    ! 0x90004(1)存放当前页;0x90006存放显示模式;0x90007存放字符列数。

 65        

 66         mov     ah,#0x0f

 67         int     0x10

 68         mov     [4],bx          ! bh = display page

 69         mov     [6],ax          ! al = video mode, ah = window width

 70

 71 ! Get hd0 data

    ! 取第一个硬盘的信息(复制硬盘参数表)。

    ! 1个硬盘参数表的首地址竟然是中断向量0x41的向量值!而第2个硬盘参数表紧接在第1

    ! 表的后面,中断向量0x46的向量值也指向第2个硬盘的参数表首址。表的长度是16个字节。

    ! 下面两段程序分别复制ROM BIOS中有关两个硬盘的参数表,0x90080处存放第1个硬盘的表,

    ! 0x90090处存放第2个硬盘的表。

 72

    ! 75行语句从内存指定位置处读取一个长指针值并放入dssi寄存器中。ds中放段地址,

    ! si是段内偏移地址。这里是把内存地址4 * 0x41= 0x104)处保存的4个字节读出。这4

    ! 节即是硬盘参数表所处位置的段和偏移值。

 73         mov     ax,#0x0000

 74         mov     ds,ax

 75         lds     si,[4*0x41]        ! 取中断向量0x41的值,即hd0参数表的地址èds:si

 76         mov     ax,#INITSEG

 77         mov     es,ax

 78         mov     di,#0x0080         ! 传输的目的地址: 0x9000:0x0080 è es:di

 79         mov     cx,#0x10           ! 共传输16字节。

 80         rep

 81         movsb

 82

 83 ! Get hd1 data

 84

 85         mov     ax,#0x0000

 86         mov     ds,ax

 87         lds     si,[4*0x46]        ! 取中断向量0x46的值,即hd1参数表的地址èds:si

 88         mov     ax,#INITSEG

 89         mov     es,ax

 90         mov     di,#0x0090         ! 传输的目的地址: 0x9000:0x0090 è es:di

 91         mov     cx,#0x10

 92         rep

 93         movsb

 94

 95 ! Check that there IS a hd1 :-)

    ! 检查系统是否有第2个硬盘。如果没有则把第2个表清零。

    ! 利用BIOS中断调用0x13的取盘类型功能,功能号 ah = 0x15

    ! 输入:dl = 驱动器号(0x8X是硬盘:0x80指第1个硬盘,0x812个硬盘)

    ! 输出:ah = 类型码;00 - 没有这个盘,CF置位;01 - 是软驱,没有change-line支持;

    !                    02 - 是软驱(或其他可移动设备),有change-line支持; 03 - 是硬盘。

 96

 97         mov     ax,#0x01500

 98         mov     dl,#0x81

 99         int     0x13

100         jc      no_disk1

101         cmp     ah,#3              ! 是硬盘吗?(类型 = 3 )

102         je      is_disk1

103 no_disk1:

104         mov     ax,#INITSEG        ! 2个硬盘不存在,则对第2个硬盘表清零。

105         mov     es,ax

106         mov     di,#0x0090

107         mov     cx,#0x10

108         mov     ax,#0x00

109         rep

110         stosb

111 is_disk1:

112

113 ! now we want to move to protected mode ...

    ! 现在我们要进入保护模式中了...

114

115         cli                     ! no interrupts allowed !     ! 从此开始不允许中断。

116

117 ! first we move the system to it's rightful place

    ! 首先我们将system模块移到正确的位置。

    ! bootsect引导程序会把 system 模块读入到内存 0x1000064KB)开始的位置。由于当时假设

    ! system模块最大长度不会超过0x80000512KB),即其末端不会超过内存地址0x90000,所以

    ! bootsect会把自己移动到0x90000开始的地方,并把setup加载到它的后面。下面这段程序的

    ! 用途是再把整个system模块移动到 0x00000位置,即把从 0x100000x8ffff 的内存数据块

    ! 512KB)整块地向内存低端移动了0x1000064KB)的位置。

118

119         mov     ax,#0x0000

120         cld                     ! 'direction'=0, movs moves forward

121 do_move:

122         mov     es,ax           ! destination segment ! es:di是目的地址(初始为0x0:0x0)

123         add     ax,#0x1000

124         cmp     ax,#0x9000      ! 已经把最后一段(从0x8000段开始的64KB)代码移动完?

125         jz      end_move        ! 是,则跳转。

126         mov     ds,ax           ! source segment  ! ds:si是源地址(初始为0x1000:0x0)

127         sub     di,di

128         sub     si,si

129         mov     cx,#0x8000      ! 移动0x8000字(64KB字节)。

130         rep

131         movsw

132         jmp     do_move

133

134 ! then we load the segment descriptors

    ! 此后,我们加载段描述符。

    ! 从这里开始会遇到32位保护模式的操作,因此需要Intel 32位保护模式编程方面的知识了,

    ! 有关这方面的信息请查阅列表后的简单介绍或附录中的详细说明。这里仅作概要说明。在进入

    ! 保护模式中运行之前,我们需要首先设置好需要使用的段描述符表。这里需要设置全局描述符

    ! 表和中断描述符表。

    !

    ! 下面指令lidt用于加载中断描述符表(IDT)寄存器。它的操作数(idt_48)有6字节。前2

    ! 字节(字节0-1)是描述符表的字节长度值;后4字节(字节2-5)是描述符表的32位线性基

    ! 地址,其形式参见下面218--220行和222--224行说明。中断描述符表中的每一个8字节表项

    ! 指出发生中断时需要调用的代码信息。与中断向量有些相似,但要包含更多的信息。

    !

    ! lgdt指令用于加载全局描述符表(GDT)寄存器,其操作数格式与lidt指令的相同。全局描述

    ! 符表中的每个描述符项(8字节)描述了保护模式下数据段和代码段(块)的信息。 其中包括

    ! 段的最大长度限制(16位)、段的线性地址基址(32位)、段的特权级、段是否在内存、读写

    ! 许可权以及其他一些保护模式运行的标志。参见后面205--216行。

135

136 end_move:

137         mov     ax,#SETUPSEG    ! right, forgot this at first. didn't work :-)

138         mov     ds,ax           ! ds指向本程序(setup)段。

139         lidt    idt_48          ! load idt with 0,0                  ! 加载IDT寄存器。

140         lgdt    gdt_48          ! load gdt with whatever appropriate ! 加载GDT寄存器。

141

142 ! that was painless, now we enable A20

    ! 以上的操作很简单,现在我们开启A20地址线。

    ! 为了能够访问和使用1MB以上的物理内存,我们需要首先开启A20地址线。参见本程序列表后

    ! 有关A20信号线的说明。关于所涉及的一些端口和命令,可参考kernel/chr_drv/keyboard.S

    ! 程序后对键盘接口的说明。至于机器是否真正开启了A20地址线,我们还需要在进入保护模式

    ! 之后(能访问1MB以上内存之后)在测试一下。这个工作放在了head.S程序中(32--36行)。

143

144         call    empty_8042              ! 测试8042状态寄存器,等待输入缓冲器空。

                                            ! 只有当输入缓冲器为空时才可以对其执行写命令。

145         mov     al,#0xD1                ! command write ! 0xD1命令码-表示要写数据到

146         out     #0x64,al                ! 8042P2端口。P2端口位1用于A20线的选通。

147         call    empty_8042              ! 等待输入缓冲器空,看命令是否被接受。

148         mov     al,#0xDF                ! A20 on        ! 选通A20地址线的参数。

149         out     #0x60,al                ! 数据要写到0x60口。

150         call    empty_8042              ! 若此时输入缓冲器为空,则表示A20线已经选通。

151

152 ! well, that went ok, I hope. Now we have to reprogram the interrupts :-(

153 ! we put them right after the intel-reserved hardware interrupts, at

154 ! int 0x20-0x2F. There they won't mess up anything. Sadly IBM really

155 ! messed this up with the original PC, and they haven't been able to

156 ! rectify it afterwards. Thus the bios puts interrupts at 0x08-0x0f,

157 ! which is used for the internal hardware interrupts as well. We just

158 ! have to reprogram the 8259's, and it isn't fun.

    !

    ! 希望以上一切正常。现在我们必须重新对中断进行编程 :-( 我们将它们放在正好

    ! 处于Intel保留的硬件中断后面,即int 0x20--0x2F。在那里它们不会引起冲突。

    ! 不幸的是IBM在原PC机中搞糟了,以后也没有纠正过来。所以PCBIOS把中断

    ! 放在了0x08--0x0f,这些中断也被用于内部硬件中断。所以我们就必须重新对8259

    ! 中断控制器进行编程,这一点都没意思。

    !

    ! PC机使用28259A芯片,关于对可编程控制器8259A芯片的编程方法请参见本程序后的介绍。

    ! 162行上定义的两个字(0x00eb)是直接使用机器码表示的两条相对跳转指令,起延时作用。

    ! 0xeb是直接近跳转指令的操作码,带1个字节的相对位移值。因此跳转范围是-127127CPU

    ! 通过把这个相对位移值加到EIP寄存器中就形成一个新的有效地址。此时EIP指向下一条被执行

    ! 的指令。执行时所花费的CPU时钟周期数是710个。0x00eb 表示跳转值是0的一条指令,因

    ! 此还是直接执行下一条指令。这两条指令共可提供14--20CPU时钟周期的延迟时间。在as86

    ! 中没有表示相应指令的助记符,因此Linussetup.s等一些汇编程序中就直接使用机器码来表

    ! 示这种指令。另外,每个空操作指令NOP的时钟周期数是3个,因此若要达到相同的延迟效果就

    ! 需要67NOP指令。

159

    ! 8259芯片主片端口是0x20-0x21,从片端口是0xA0-0xA1。输出值0x11表示初始化命令开始,

    ! 它是ICW1命令字,表示边沿触发、多片8259级连、最后要发送ICW4命令字。

160         mov     al,#0x11                ! initialization sequence

161         out     #0x20,al                ! send it to 8259A-1  ! 发送到8259A主芯片。

162         .word   0x00eb,0x00eb           ! jmp $+2, jmp $+2    ! '$'表示当前指令的地址,

163         out     #0xA0,al                ! and to 8259A-2      ! 再发送到8259A从芯片。

164         .word   0x00eb,0x00eb

    ! Linux系统硬件中断号被设置成从0x20开始。参见表3-2:硬件中断请求信号与中断号对应表。

165         mov     al,#0x20                ! start of hardware int's (0x20)

166         out     #0x21,al                ! 送主芯片ICW2命令字,设置起始中断号,要送奇端口。

167         .word   0x00eb,0x00eb

168         mov     al,#0x28                ! start of hardware int's 2 (0x28)

169         out     #0xA1,al                ! 送从芯片ICW2命令字,从芯片的起始中断号。

170         .word   0x00eb,0x00eb

171         mov     al,#0x04                ! 8259-1 is master

172         out     #0x21,al                ! 送主芯片ICW3命令字,主芯片的IR2连从芯片INT

                                            !参见代码列表后的说明。

173         .word   0x00eb,0x00eb

174         mov     al,#0x02                ! 8259-2 is slave

175         out     #0xA1,al                ! 送从芯片ICW3命令字,表示从芯片的INT连到主芯

                                            ! 片的IR2引脚上。

176         .word   0x00eb,0x00eb

177         mov     al,#0x01                ! 8086 mode for both

178         out     #0x21,al                ! 送主芯片ICW4命令字。8086模式;普通EOI、非缓冲

                                            ! 方式,需发送指令来复位。初始化结束,芯片就绪。

179         .word   0x00eb,0x00eb

180         out     #0xA1,al                !送从芯片ICW4命令字,内容同上。

181         .word   0x00eb,0x00eb

182         mov     al,#0xFF                ! mask off all interrupts for now

183         out     #0x21,al                ! 屏蔽主芯片所有中断请求。

184         .word   0x00eb,0x00eb

185         out     #0xA1,al                !屏蔽从芯片所有中断请求。

186

187 ! well, that certainly wasn't fun :-(. Hopefully it works, and we don't

188 ! need no steenking BIOS anyway (except for the initial loading :-).

189 ! The BIOS-routine wants lots of unnecessary data, and it's less

190 ! "interesting" anyway. This is how REAL programmers do it.

191 !

192 ! Well, now's the time to actually move into protected mode. To make

193 ! things as simple as possible, we do no register set-up or anything,

194 ! we let the gnu-compiled 32-bit programs do that. We just jump to

195 ! absolute address 0x00000, in 32-bit protected mode.

    !

    ! 哼,上面这段编程当然没劲:-(,但希望这样能工作,而且我们也不再需要乏味的BIOS

    ! 了(除了初始加载:-)BIOS子程序要求很多不必要的数据,而且它一点都没趣。那是

    ! “真正”的程序员所做的事。

    !

    ! 好了,现在是真正开始进入保护模式的时候了。为了把事情做得尽量简单,我们并不对

    ! 寄存器内容进行任何设置。我们让gnu编译的32位程序去处理这些事。在进入32位保

    ! 护模式时我们仅是简单地跳转到绝对地址0x00000处。

196

    ! 下面设置并进入32位保护模式运行。首先加载机器状态字(lmsw-Load Machine Status Word)

    ! 也称控制寄存器CR0,其比特位01将导致CPU切换到保护模式,并且运行在特权级0中,即

    ! 当前特权级CPL=0。此时段寄存器仍然指向与实地址模式中相同的线性地址处(在实地址模式下

    ! 线性地址与物理内存地址相同)。在设置该比特位后,随后一条指令必须是一条段间跳转指令以

    ! 用于刷新CPU当前指令队列。因为CPU是在执行一条指令之前就已从内存读取该指令并对其进行

    ! 解码。然而在进入保护模式以后那些属于实模式的预先取得的指令信息就变得不再有效。而一条

    ! 段间跳转指令就会刷新CPU的当前指令队列,即丢弃这些无效信息。另外,在Intel公司的手册

    ! 上建议80386或以上CPU应该使用指令“mov cr0,ax”切换到保护模式。lmsw指令仅用于兼容以

    ! 前的286 CPU

 

197         mov     ax,#0x0001      ! protected mode (PE) bit        ! 保护模式比特位(PE)

198         lmsw    ax              ! This is it!                    ! 就这样加载机器状态字!

199         jmpi    0,8             ! jmp offset 0 of segment 8 (cs) ! 跳转至cs段偏移0处。

    ! 我们已经将system模块移动到0x00000开始的地方,所以上句中的偏移地址是0。而段值8已经

    ! 是保护模式下的段选择符了,用于选择描述符表和描述符表项以及所要求的特权级。段选择符长

    ! 度为16位(2字节);位0-1表示请求的特权级0--3,但Linux操作系统只用到两级:0级(内

    ! 核级)和3级(用户级);位2用于选择全局描述符表(0)还是局部描述符表(1);位3-15是描

    ! 述符表项的索引,指出选择第几项描述符。所以段选择符80b0000,0000,0000,1000)表示请求

    ! 特权级0、使用全局描述符表GDT中第2个段描述符项,该项指出代码的基地址是0(参见571行),

    ! 因此这里的跳转指令就会去执行system中的代码。另外,

200

201 ! This routine checks that the keyboard command queue is empty

202 ! No timeout is used - if this hangs there is something wrong with

203 ! the machine, and we probably couldn't proceed anyway.

    ! 下面这个子程序检查键盘命令队列是否为空。这里不使用超时方法 -

    ! 如果这里死机,则说明PC机有问题,我们就没有办法再处理下去了。

    !

    ! 只有当输入缓冲器为空时(键盘控制器状态寄存器位1 = 0)才可以对其执行写命令。

204 empty_8042:

205         .word   0x00eb,0x00eb

206         in      al,#0x64        ! 8042 status port         ! AT键盘控制器状态寄存器。

207         test    al,#2           ! is input buffer full?    ! 测试位1,输入缓冲器满?

208         jnz     empty_8042      ! yes - loop

209         ret

210

211 ! Routine trying to recognize type of SVGA-board present (if any)

212 ! and if it recognize one gives the choices of resolution it offers.

213 ! If one is found the resolution chosen is given by al,ah (rows,cols).

    ! 下面是用于识别SVGA显示卡(若有的话)的子程序。若识别出一块就向用户

    ! 提供选择分辨率的机会,并把分辨率放入寄存器alah(行、列)中返回。

    !

    ! 注意下面215--566行代码牵涉到众多显示卡端口信息,因此比较复杂。但由于这段代码与内核

    ! 运行关系不大,因此可以跳过不看。

    ! 下面首先显示588行上的msg1字符串("<回车键>查看存在的SVGA模式,或按任意键继续"),

    ! 然后循环读取键盘控制器输出缓冲器,等待用户按键。如果用户按下回车键就去检查系统具有

    ! SVGA模式,并在ALAH中返回最大行列值,否则设置默认值AL=25行、AH=80列并返回。

214

215 chsvga: cld

216         push    ds               ! 保存ds值。将在231行(或490492行)弹出。

217         push    cs               ! 把默认数据段设置成和代码段同一个段。

218         pop     ds

219         mov     ax,#0xc000

220         mov     es,ax            ! es 指向0xc000段。此处是VGA卡上的ROM BIOS区。

221         lea     si,msg1          ! ds:si指向msg1字符串。

222         call    prtstr           ! 显示以NULL结尾的msg1字符串。

223 nokey:  in      al,#0x60         ! 读取键盘控制器输出缓冲器(来自键盘的扫描码或命令)。

224         cmp     al,#0x82         ! 如果收到比0x82小的扫描码则是接通扫描码,因为0x82

225         jb      nokey            ! 最小断开扫描码值。小于0x82表示还没有按键松开。

226         cmp     al,#0xe0         ! 如果扫描码大于0xe0,表示收到的是扩展扫描码前缀。

227         ja      nokey

228         cmp     al,#0x9c         ! 如果断开扫描码是0x9c,表示用户按下/松开了回车键,

229         je      svga             ! 于是程序跳转去检查系统是否具有SVGA模式。

230         mov     ax,#0x5019       ! 否则把AX中返回行列值默认设置为AL=25行、AH=80列。

231         pop     ds

232         ret

    ! 下面根据VGA显示卡上的ROM BIOS指定位置处的特征数据串或者支持的特别功能来判断机器上

    ! 安装的是什么牌子的显示卡。本程序共支持10种显示卡的扩展功能。注意,此时程序已经在第

    ! 220行把es指向VGA卡上ROM BIOS所在的段0xc000(参见第2章)。

    ! 首先判断是不是ATI显示卡。我们把 ds:si指向595行上ATI显示卡特征数据串,并把es:si

    ! VGA BIOS中指定位置(偏移0x31)处。因为该特征串共有9个字符("761295520"),因此我

    ! 们循环比较这个特征串。如果相同则表示机器中的VGA卡是ATI牌子的,于是让ds:si指向该显

    ! 示卡可以设置的行列模式值dscati(第615行),让di指向ATI卡可设置的行列个数和模式,

    ! 并跳转到标号selmod438行)处进一步进行设置。

233 svga:   lea     si,idati         ! Check ATI 'clues'  ! 检查判断 ATI显示卡的数据。

234         mov     di,#0x31         ! 特征串从0xc000:0x0031开始。

235         mov     cx,#0x09         ! 特征串有9个字节。

236         repe

237         cmpsb

238         jne     noati            ! 若特征串不同则表示不是ATI显示卡。跳转继续检测卡。

239         lea     si,dscati        ! 如果9个字节都相同,表示系统中有一块ATI牌显示卡。

240         lea     di,moati         ! 于是si指向ATI卡具有的可选行列值,di指向可选个数

241         lea     cx,selmod        ! 和模式列表,然后跳转到selmod438行)处继续处理。

242         jmp     cx

 

    ! 现在来判断是不是Ahead牌子的显示卡。首先向EGA/VGA 图形索引寄存器0x3ce写入想访问的

    ! 主允许寄存器索引号0x0f,同时向0x3cf端口(此时对应主允许寄存器)写入开启扩展寄存器

    ! 标志值0x20。然后通过0x3cf端口读取主允许寄存器值,以检查是否可以设置开启扩展寄存器

    ! 标志。如果可以则说明是Ahead牌子的显示卡。注意word输出时alè端口nahè端口n+1

243 noati:  mov     ax,#0x200f       ! Check Ahead 'clues'

244         mov     dx,#0x3ce        ! 数据端口指向主允许寄存器(0x0fè0x3ce端口),

245         out     dx,ax            ! 并设置开启扩展寄存器标志(0x20è0x3cf端口)。

246         inc     dx               ! 然后再读取该寄存器,检查该标志是否被设置上。

247         in      al,dx

248         cmp     al,#0x20         ! 如果读取值是0x20,则表示是Ahead A显示卡。

249         je      isahed           ! 如果读取值是0x21,则表示是Ahead B显示卡。

250         cmp     al,#0x21         ! 否则说明不是Ahead显示卡,于是跳转继续检测其余卡。

251         jne     noahed

252 isahed: lea     si,dscahead      ! si 指向Ahead显示卡可选行列值表,di指向扩展模式个

253         lea     di,moahead       ! 数和扩展模式号列表。然后跳转到selmod438行)处继

254         lea     cx,selmod        ! 续处理。

255         jmp     cx

 

    ! 现在来检查是不是Chips & Tech生产的显示卡。通过端口0x3c30x940x46e8)设置VGA允许

    ! 寄存器的进入设置模式标志(位4),然后从端口0x104读取显示卡芯片集标识值。如果该标识值

    ! 0xA5,则说明是Chips & Tech生产的显示卡。

256 noahed: mov     dx,#0x3c3        ! Check Chips & Tech. 'clues'

257         in      al,dx            ! 0x3c3端口读取VGA允许寄存器值,添加上进入设置模式

258         or      al,#0x10         ! 标志(位4)后再写回。

259         out     dx,al

260         mov     dx,#0x104        ! 在设置模式时从全局标识端口0x104读取显示卡芯片标识值,

261         in      al,dx            ! 并暂时存放在bl寄存器中。

262         mov     bl,al

263         mov     dx,#0x3c3        ! 然后把0x3c3端口中的进入设置模式标志复位。

264         in      al,dx

265         and     al,#0xef

266         out     dx,al

267         cmp     bl,[idcandt]     ! 再把bl中标识值与位于idcandt处(第596行)的Chips &

268         jne     nocant           ! Tech的标识值0xA5作比较。如果不同则跳转比较下一种显卡。

269         lea     si,dsccandt      ! si指向这种显示卡的可选行列值表,di指向扩展模式个数

270         lea     di,mocandt       ! 和扩展模式号列表。然后跳转到selmod438行)进行设置

271         lea     cx,selmod        ! 显示模式的操作。

272         jmp     cx

 

    ! 现在检查是不是Cirrus显示卡。方法是使用CRT控制器索引号0x1f寄存器的内容来尝试禁止扩展

    ! 功能。该寄存器被称为鹰标(Eagle ID)寄存器,将其值高低半字节交换一下后写入端口0x3c4

    ! 引的6号(定序/扩展)寄存器应该会禁止Cirrus显示卡的扩展功能。如果不会则说明不是Cirrus

    ! 显示卡。因为从端口0x3d4索引的0x1f鹰标寄存器中读取的内容是鹰标值与0x0c索引号对应的显

    ! 存起始地址高字节寄存器内容异或操作之后的值,因此在读0x1f中内容之前我们需要先把显存起始

    ! 高字节寄存器内容保存后清零,并在检查后恢复之。另外,将没有交换过的Eagle ID值写到0x3c4

    ! 端口索引的6号定序/扩展寄存器会重新开启扩展功能。

273 nocant: mov     dx,#0x3d4        ! Check Cirrus 'clues'

274         mov     al,#0x0c         ! 首先向CRT控制寄存器的索引寄存器端口0x3d4写入要访问

275         out     dx,al            ! 的寄存器索引号0x0c(对应显存起始地址高字节寄存器),

276         inc     dx               ! 然后从0x3d5端口读入显存起始地址高字节并暂存在bl中,

277         in      al,dx            ! 再把显存起始地址高字节寄存器清零。

278         mov     bl,al

279         xor     al,al

280         out     dx,al

281         dec     dx               ! 接着向0x3d4端口输出索引0x1f,指出我们要在0x3d5端口

282         mov     al,#0x1f         ! 访问读取“Eagle ID”寄存器内容。

283         out     dx,al

284         inc     dx

285         in      al,dx            ! 0x3d5端口读取“Eagle ID”寄存器值,并暂存在bh中。

286         mov     bh,al            ! 然后把该值高低4比特互换位置存放到cl中。再左移8

287         xor     ah,ah            ! 后放入ch中,而cl中放入数值6

288         shl     al,#4

289         mov     cx,ax

290         mov     al,bh

291         shr     al,#4

292         add     cx,ax

293         shl     cx,#8

294         add     cx,#6            ! 最后把cx值存放入ax中。此时ah中是换位后的“Eagle

295         mov     ax,cx            ! ID”值,al中是索引号6,对应定序/扩展寄存器。把ah

296         mov     dx,#0x3c4        ! 写到0x3c4端口索引的定序/扩展寄存器应该会导致Cirrus

297         out     dx,ax            ! 显示卡禁止扩展功能。

298         inc     dx

299         in      al,dx            ! 如果扩展功能真的被禁止,那么此时读入的值应该为0

300         and     al,al            ! 如果不为0则表示不是Cirrus显示卡,跳转继续检查其他卡。

301         jnz     nocirr

302         mov     al,bh            ! Cirrus显示卡,则利用第286行保存在bh中的“Eagle

303         out     dx,al            ! ID”原值再重新开启Cirrus卡扩展功能。此时读取的返回

304         in      al,dx            ! 值应该为1。若不是,则仍然说明不是Cirrus显示卡。

305         cmp     al,#0x01

306         jne     nocirr

307         call    rst3d4           ! 恢复CRT控制器的显示起始地址高字节寄存器内容。

308         lea     si,dsccirrus     ! si指向Cirrus显示卡的可选行列值,di指向扩展模式个数

309         lea     di,mocirrus      ! 和对应模式号。然后跳转到selmod处去选择显示模式。

310         lea     cx,selmod

311         jmp     cx

    ! 该子程序利用保存在bl中的值(第278行)恢复CRT控制器的显示起始地址高字节寄存器内容。

312 rst3d4: mov     dx,#0x3d4

313         mov     al,bl

314         xor     ah,ah

315         shl     ax,#8

316         add     ax,#0x0c

317         out     dx,ax            ! 注意,这是word输出!! al è0x3d4ah è0x3d5

318         ret    

 

    ! 现在检查系统中是不是Everex显示卡。方法是利用中断int 0x10功能0x70ax =0x7000

    ! bx=0x0000)调用Everex的扩展视频BIOS功能。对于Everes类型显示卡,该中断调用应该

    ! 会返回模拟状态,即有以下返回信息:

    ! al = 0x70,若是基于TridentEverex显示卡;

    ! cl = 显示器类型:00-单色;01-CGA02-EGA03-数字多频;04-PS/205-IBM 851406-SVGA

    ! ch = 属性:位7-600-256K01-512K10-1MB11-2MB;位4-开启VGA保护;位0-6845模拟。

    ! dx = 板卡型号:位15-4:板类型标识号;位3-0:板修正标识号。

    !      0x2360-Ultragraphics II0x6200-Vision VGA0x6730-EVGA0x6780-Viewpoint

    ! di = BCD码表示的视频BIOS版本号。

319 nocirr: call    rst3d4           ! Check Everex 'clues'

320         mov     ax,#0x7000       ! 设置ax = 0x7000, bx=0x0000,调用int 0x10

321         xor     bx,bx

322         int     0x10

323         cmp     al,#0x70         ! 对于Everes显示卡,al中应该返回值0x70

324         jne     noevrx

325         shr     dx,#4            ! 忽律板修正号(位3-0)。

326         cmp     dx,#0x678        ! 板类型号是0x678表示是一块Trident显示卡,则跳转。

327         je      istrid

328         cmp     dx,#0x236        ! 板类型号是0x236表示是一块Trident显示卡,则跳转。

329         je      istrid

330         lea     si,dsceverex     ! si指向Everex显示卡的可选行列值表,让di指向扩展

331         lea     di,moeverex      ! 模式个数和模式号列表。然后跳转到selmod去执行选择

332         lea     cx,selmod        ! 显示模式的操作。

333         jmp     cx

334 istrid: lea     cx,ev2tri        ! Trident类型的Everex显示卡,则跳转到ev2tri处理。

335         jmp     cx

 

    ! 现在检查是不是Genoa显示卡。方式是检查其视频BIOS中的特征数字串(0x770x000x66

    ! 0x99)。注意,此时es已经在第220行被设置成指向VGA卡上ROM BIOS所在的段0xc000

336 noevrx: lea     si,idgenoa       ! Check Genoa 'clues'

337         xor     ax,ax            ! ds:si指向第597行上的特征数字串。

338         seg es

339         mov     al,[0x37]        ! VGA卡上BIOS0x37处的指针(它指向特征串)。

340         mov     di,ax            ! 因此此时es:di指向特征数字串开始处。

341         mov     cx,#0x04

342         dec     si

343         dec     di

344 l1:     inc     si               ! 然后循环比较这4个字节的特征数字串。

345         inc     di

346         mov     al,(si)

347         seg es

348         and     al,(di)

349         cmp     al,(si)

350         loope   l1

351         cmp     cx,#0x00         ! 如果特征数字串完全相同,则表示是Genoa显示卡,

352         jne     nogen            ! 否则跳转去检查其他类型的显示卡。

353         lea     si,dscgenoa      ! si指向Genoa显示卡的可选行列值表,让di指向扩展

354         lea     di,mogenoa       ! 模式个数和模式号列表。然后跳转到selmod去执行选择

355         lea     cx,selmod        ! 显示模式的操作。

356         jmp     cx

 

    ! 现在检查是不是Paradise显示卡。同样是采用比较显示卡上BIOS中特征串(“VGA=”)的方式。

357 nogen:  lea     si,idparadise    ! Check Paradise 'clues'

358         mov     di,#0x7d         ! es:di指向VGA ROM BIOS0xc000:0x007d处,该处应该有

359         mov     cx,#0x04         ! 4个字符“VGA=”。

360         repe

361         cmpsb

362         jne     nopara           ! 若有不同的字符,表示不是Paradise显示卡,于是跳转。

363         lea     si,dscparadise   ! 否则让si指向Paradise显示卡的可选行列值表,让di

364         lea     di,moparadise    ! 向扩展模式个数和模式号列表。然后跳转到selmod处去选

365         lea     cx,selmod        ! 择想要使用的显示模式。

366         jmp     cx

 

    ! 现在检查是不是TridentTVGA)显示卡。TVGA显示卡扩充的模式控制寄存器10x3c4端口索引

    ! 0x0e)的位3--064K内存页面个数值。这个字段值有一个特性:当写入时,我们需要首先把

    ! 值与0x02进行异或操作后再写入;当读取该值时则不需要执行异或操作,即异或前的值应该与写

    ! 入后再读取的值相同。下面代码就利用这个特性来检查是不是Trident显示卡。

367 nopara: mov     dx,#0x3c4        ! Check Trident 'clues'

368         mov     al,#0x0e         ! 首先在端口0x3c4输出索引号0x0e,索引模式控制寄存器1

369         out     dx,al            ! 然后从0x3c5数据端口读入该寄存器原值,并暂存在ah中。

370         inc     dx

371         in      al,dx

372         xchg    ah,al

373         mov     al,#0x00         ! 然后我们向该寄存器写入0x00,再读取其值èal

374         out     dx,al            ! 写入0x00就相当于“原值”0x02异或0x02后的写入值,

375         in      al,dx            ! 因此若是Trident显示卡,则此后读入的值应该是0x02

376         xchg    al,ah            ! 交换后,al=原模式控制寄存器1的值,ah=最后读取的值。

    ! 下面语句右则英文注释是“真奇怪...书中并没有要求这样操作,但是这对我的Trident显示卡

    ! 起作用。如果不这样做,屏幕就会变模糊...”。这几行附带有英文注释的语句执行如下操作:

    ! 如果bl中原模式控制寄存器1的位1在置位状态的话就将其复位,否则就将位1置位。

    ! 实际上这几条语句就是对原模式控制寄存器1的值执行异或 0x02的操作,然后用结果值去设置

    ! (恢复)原寄存器值。

377         mov     bl,al            ! Strange thing ... in the book this wasn't

378         and     bl,#0x02         ! necessary but it worked on my card which

379         jz      setb2            ! is a trident. Without it the screen goes

380         and     al,#0xfd         ! blurred ...

381         jmp     clrb2            !

382 setb2:  or      al,#0x02         !

383 clrb2:  out     dx,al

384         and     ah,#0x0f         ! 375行最后读入值的页面个数字段(位3--0),如果

385         cmp     ah,#0x02         ! 该字段值等于0x02,则表示是Trident显示卡。

386         jne     notrid

387 ev2tri: lea     si,dsctrident    ! Trident显示卡,于是让si指向该显示卡的可选行列

388         lea     di,motrident     ! 值列表,让di指向对应扩展模式个数和模式号列表,然

389         lea     cx,selmod        ! 后跳转到selmod去执行模式选择操作。

390         jmp     cx

 

    ! 现在检查是不是Tseng显示卡(ET4000AXET4000/W32类)。方法是对0x3cd端口对应的段

    ! 选择(Segment Select)寄存器执行读写操作。该寄存器高4位(位7--4)是要进行读操作的

    ! 64KB段号(Bank number),低4位(位3--0)是指定要写的段号。如果指定段选择寄存器的

    ! 的值是 0x55(表示读、写第664KB段),那么对于Tseng显示卡来说,把该值写入寄存器

    ! 后再读出应该还是0x55

391 notrid: mov     dx,#0x3cd        ! Check Tseng 'clues'

392         in      al,dx            ! Could things be this simple ! :-)

393         mov     bl,al            ! 先从0x3cd端口读取段选择寄存器原值,并保存在bl中。

394         mov     al,#0x55         ! 然后我们向该寄存器中写入0x55。再读入并放在ah中。

395         out     dx,al

396         in      al,dx

397         mov     ah,al

398         mov     al,bl            ! 接着恢复该寄存器的原值。

399         out     dx,al

400         cmp     ah,#0x55         ! 如果读取的就是我们写入的值,则表明是Tseng显示卡。

401         jne     notsen

402         lea     si,dsctseng      ! 于是让si指向Tseng显示卡的可选行列值的列表,让di

403         lea     di,motseng       ! 指向对应扩展模式个数和模式号列表,然后跳转到selmod

404         lea     cx,selmod        ! 去执行模式选择操作。

405         jmp     cx

 

    ! 下面检查是不是Video7显示卡。端口0x3c2是混合输出寄存器写端口,而0x3cc是混合输出寄存

    ! 器读端口。该寄存器的位0是单色/彩色标志。如果为0则表示是单色,否则是彩色。判断是不是

    ! Video7显示卡的方式是利用这种显示卡的CRT控制扩展标识寄存器(索引号是0x1f)。该寄存器

    ! 的值实际上就是显存起始地址高字节寄存器(索引号0x0c)的内容和0xea进行异或操作后的值。

    ! 因此我们只要向显存起始地址高字节寄存器中写入一个特定值,然后从标识寄存器中读取标识值

    ! 进行判断即可。

    ! 通过对以上显示卡和这里Video7显示卡的检查分析,我们可知检查过程通常分为三个基本步骤。

    ! 首先读取并保存测试需要用到的寄存器原值,然后使用特定测试值进行写入和读出操作,最后恢

    ! 复原寄存器值并对检查结果作出判断。

406 notsen: mov     dx,#0x3cc        ! Check Video7 'clues'

407         in      al,dx

408         mov     dx,#0x3b4        ! 先设置dx为单色显示CRT控制索引寄存器端口号0x3b4

409         and     al,#0x01         ! 如果混合输出寄存器的位0等于0(单色)则直接跳转,

410         jz      even7            ! 否则dx设置为彩色显示CRT控制索引寄存器端口号0x3d4

411         mov     dx,#0x3d4

412 even7:  mov     al,#0x0c         ! 设置寄存器索引号为0x0c,对应显存起始地址高字节寄存器。

413         out     dx,al

414         inc     dx

415         in      al,dx            ! 读取显示内存起始地址高字节寄存器内容,并保存在bl中。

416         mov     bl,al

417         mov     al,#0x55         ! 然后在显存起始地址高字节寄存器中写入值0x55,再读取出来。

418         out     dx,al

419         in      al,dx

420         dec     dx               ! 然后通过CRTC索引寄存器端口0x3b40x3d4选择索引号是

421         mov     al,#0x1f         ! 0x1fVideo7显示卡标识寄存器。该寄存器内容实际上就是

422         out     dx,al            ! 显存起始地址高字节和0xea进行异或操作后的结果值。

423         inc     dx

424         in      al,dx            ! 读取Video7显示卡标识寄存器值,并保存在bh中。

425         mov     bh,al

426         dec     dx               ! 然后再选择显存起始地址高字节寄存器,恢复其原值。

427         mov     al,#0x0c

428         out     dx,al

429         inc     dx

430         mov     al,bl

431         out     dx,al

432         mov     al,#0x55         ! 随后我们来验证“Video7显示卡标识寄存器值就是显存起始

433         xor     al,#0xea         ! 地址高字节和0xea进行异或操作后的结果值。因此0x55

434         cmp     al,bh            ! 0xea进行异或操作的结果就应该等于标识寄存器的测试值。

435         jne     novid7           ! 若不是Video7显示卡,则设置默认显示行列值(492行)。

436         lea     si,dscvideo7     ! Video7显示卡,于是让si指向该显示卡行列值表,让di

437         lea     di,movideo7      ! 指向扩展模式个数和模式号列表。

 

    ! 下面根据上述代码判断出的显示卡类型以及取得的相关扩展模式信息(si指向的行列值列表;di

    ! 指向扩展模式个数和模式号列表),提示用户选择可用的显示模式,并设置成相应显示模式。最后

    ! 子程序返回系统当前设置的屏幕行列值(ah = 列数;al=行数)。例如,如果系统中是ATI显示卡,

    ! 那么屏幕上会显示以下信息:

    ! Mode:  COLSxROWS:

    ! 0.     132 x 25

    ! 1.     132 x 44

    ! Choose mode by pressing the corresponding number.

    !

    ! 这段程序首先在屏幕上显示NULL结尾的字符串信息“Mode:  COLSxROWS:”。

438 selmod: push    si

439         lea     si,msg2

440         call    prtstr

441         xor     cx,cx

442         mov     cl,(di)          ! 此时cl中是检查出的显示卡的扩展模式个数。

443         pop     si

444         push    si

445         push    cx

    ! 然后并在每一行上显示出当前显示卡可选择的扩展模式行列值,供用户选用。

446 tbl:    pop     bx               ! bx = 显示卡的扩展模式总个数。

447         push    bx

448         mov     al,bl

449         sub     al,cl

450         call    dprnt            ! 以十进制格式显示al中的值。

451         call    spcing           ! 显示一个点再空4个空格。

452         lodsw                    ! ax中加载si指向的行列值,随后si指向下一个word值。

453         xchg    al,ah            ! 交换位置后al = 列数。

454         call    dprnt            ! 显示列数;

455         xchg    ah,al            ! 此时al中是行数值。

456         push    ax

457         mov     al,#0x78         ! 显示一个小“x”,即乘号。

458         call    prnt1

459         pop     ax               ! 此时al中是行数值。

460         call    dprnt            ! 显示行数。

461         call    docr             ! 回车换行。

462         loop    tbl              ! 再显示下一个行列值。cx中扩展模式计数值递减1

    ! 在扩展模式行列值都显示之后,显示“Choose mode by pressing the corresponding number.”,

    ! 然后从键盘口读取用户按键的扫描码,根据该扫描码确定用户选择的行列值模式号,并利用ROM

    ! BIOS的显示中断int 0x10功能0x00来设置相应的显示模式。

    ! 468行的模式个数值+0x80”是所按数字键-1的松开扫描码。对于0--9数字键,它们的松开

    ! 扫描码分别是:0 - 0x8B1 - 0x822 - 0x833 - 0x844 - 0x85

    !               5 - 0x866 - 0x877 - 0x888 - 0x899 - 0x8A

    ! 因此,如果读取的键盘松开扫描码小于0x82就表示不是数字键;如果扫描码等于0x8B则表示用户

    ! 按下数字0键。

463         pop     cx               ! cl中是显示卡扩展模式总个数值。

464         call    docr

465         lea     si,msg3          ! 显示“请按相应数字键来选择模式。”

466         call    prtstr

467         pop     si               ! 弹出原行列值指针(指向显示卡行列值表开始处)。

468         add     cl,#0x80         ! cl + 0x80 = 对应“数字键-1”的松开扫描码。

469 nonum:  in      al,#0x60         ! Quick and dirty...

470         cmp     al,#0x82         ! 若键盘松开扫描码小于0x82则表示不是数字键,忽律该键。

471         jb      nonum

472         cmp     al,#0x8b         ! 若键盘松开扫描码等于0x8b,表示按下了数字键0

473         je      zero

474         cmp     al,cl            ! 若扫描码大于扩展模式个数值对应的最大扫描码值,表示

475         ja      nonum            ! 键入的值超过范围或不是数字键的松开扫描码。否则表示

476         jmp     nozero           ! 用户按下并松开了一个非0数字按键。

    ! 下面把松开扫描码转换成对应的数字按键值,然后利用该值从模式个数和模式号列表中选择对应的

    ! 的模式号。接着调用机器ROM BIOS中断int 0x10功能0把屏幕设置成模式号指定的模式。最后再

    ! 利用模式号从显示卡行列值表中选择并在ax中返回对应的行列值。

477 zero:   sub     al,#0x0a         ! al = 0x8b - 0x0a = 0x81

478 nozero: sub     al,#0x80         ! 再减去0x80就可以得到用户选择了第几个模式。

479         dec     al               ! 0起计数。

480         xor     ah,ah            ! int 0x10显示功能号=0(设置显示模式)。

481         add     di,ax

482         inc     di               ! di指向对应的模式号(跳过第1个模式个数字节值)。

483         push    ax

484         mov     al,(di)          ! 取模式号èal中,并调用系统BIOS显示中断功能0

485         int     0x10

486         pop     ax

487         shl     ax,#1            ! 模式号乘2,转换成为行列值表中对应值的指针。

488         add     si,ax

489         lodsw                    ! 取对应行列值到ax中(ah = 列数,al = 行数)。

490         pop     ds               ! 恢复第216行保存的ds原值。在ax中返回当前显示行列值。

491         ret

 

    ! 若都不是上面检测的显示卡,那么我们只好采用默认的80 x 25 的标准行列值。

492 novid7: pop     ds               ! Here could be code to support standard 80x50,80x30

493         mov     ax,#0x5019     

494         ret

495

496 ! Routine that 'tabs' to next col.

    ! 光标移动到下一制表位的子程序。

497

    ! 显示一个点字符'.'4个空格。

498 spcing: mov     al,#0x2e         ! 显示一个点字符'.'

499         call    prnt1

500         mov     al,#0x20

501         call    prnt1  

502         mov     al,#0x20

503         call    prnt1  

504         mov     al,#0x20

505         call    prnt1  

506         mov     al,#0x20

507         call    prnt1

508         ret    

509

510 ! Routine to print asciiz-string at DS:SI

    ! 显示位于DS:SI处以NULL0x00)结尾的字符串。

511

512 prtstr: lodsb

513         and     al,al

514         jz      fin

515         call    prnt1            ! 显示al中的一个字符。

516         jmp     prtstr

517 fin:    ret

518

519 ! Routine to print a decimal value on screen, the value to be

520 ! printed is put in al (i.e 0-255).

    ! 显示十进制数字的子程序。显示值放在寄存器al中(0--255)。

521

522 dprnt:  push    ax

523         push    cx

524         mov     ah,#0x00               

525         mov     cl,#0x0a

526         idiv    cl

527         cmp     al,#0x09

528         jbe     lt100

529         call    dprnt

530         jmp     skip10

531 lt100:  add     al,#0x30

532         call    prnt1

533 skip10: mov     al,ah

534         add     al,#0x30

535         call    prnt1  

536         pop     cx

537         pop     ax

538         ret

539

540 ! Part of above routine, this one just prints ascii al

    ! 上面子程序的一部分。显示al中的一个字符。

    ! 该子程序使用中断0x100x0E功能,以电传方式在屏幕上写一个字符。光标会自动移到下一个

    ! 位置处。如果写完一行光标就会移动到下一行开始处。如果已经写完一屏最后一行,则整个屏幕

    ! 会向上滚动一行。字符0x07BEL)、0x08BS)、0x0A(LF)0x0DCR)被作为命令不会显示。

    ! 输入:AL -- 欲写字符;BH -- 显示页号;BL -- 前景显示色(图形方式时)。

541

542 prnt1:  push    ax

543         push    cx

544         mov     bh,#0x00         ! 显示页面。

545         mov     cx,#0x01

546         mov     ah,#0x0e

547         int     0x10

548         pop     cx

549         pop     ax

550         ret

551

552 ! Prints <CR> + <LF>    ! 显示回车+换行。

553

554 docr:   push    ax

555         push    cx

556         mov     bh,#0x00

557         mov     ah,#0x0e

558         mov     al,#0x0a

559         mov     cx,#0x01

560         int     0x10

561         mov     al,#0x0d

562         int     0x10

563         pop     cx

564         pop     ax

565         ret    

566        

    ! 全局描述符表开始处。描述符表由多个8字节长的描述符项组成。这里给出了3个描述符项。

    ! 1项无用(568行),但须存在。第2项是系统代码段描述符(570-573行),第3项是系

    ! 统数据段描述符(575-578)

567 gdt:

568         .word   0,0,0,0         ! dummy   ! 1个描述符,不用。

569

    ! GDT表中这里的偏移量是0x08。它是内核代码段选择符的值。

570         .word   0x07FF          ! 8Mb - limit=2047  (0--2047,因此是2048*4096=8Mb)

571         .word   0x0000          ! base address=0

572         .word   0x9A00          ! code read/exec         ! 代码段为只读、可执行。

573         .word   0x00C0          ! granularity=4096, 386  ! 颗粒度为409632位模式。

574

    ! GDT表中这里的偏移量是0x10。它是内核数据段选择符的值。

575         .word   0x07FF          ! 8Mb - limit=2047 (2048*4096=8Mb)

576         .word   0x0000          ! base address=0

577         .word   0x9200          ! data read/write        ! 数据段为可读可写。

578         .word   0x00C0          ! granularity=4096, 386  ! 颗粒度为409632位模式。

579

    ! 下面是加载中断描述符表寄存器idtr的指令lidt要求的6字节操作数。前2字节是IDT表的

    ! 限长,后4字节是idt表在线性地址空间中的32位基地址。CPU要求在进入保护模式之前需设

    ! IDT表,因此这里先设置一个长度为0的空表。

580 idt_48:

581         .word   0               ! idt limit=0

582         .word   0,0             ! idt base=0L

583

    ! 这是加载全局描述符表寄存器gdtr的指令lgdt要求的6字节操作数。前2字节是gdt表的限

    ! 长,后4字节是 gdt表的线性基地址。这里全局表长度设置为 2KB0x7ff即可),因为每8

    ! 字节组成一个段描述符项,所以表中共可有 256项。4字节的线性基地址为 0x0009<<16 +

    ! 0x0200 + gdt,即0x90200 + gdt(符号gdt是全局表在本程序段中的偏移地址,见205)

584 gdt_48:

585         .word   0x800           ! gdt limit=2048, 256 GDT entries

586         .word   512+gdt,0x9     ! gdt base = 0X9xxxx

587

588 msg1:   .ascii  "Press <RETURN> to see SVGA-modes available or any other key to continue."

589                 db      0x0d, 0x0a, 0x0a, 0x00

590 msg2:           .ascii  "Mode:  COLSxROWS:"

591                 db      0x0d, 0x0a, 0x0a, 0x00

592 msg3:           .ascii  "Choose mode by pressing the corresponding number."

593                 db      0x0d, 0x0a, 0x00

594                

    ! 下面是4个显示卡的特征数据串。

595 idati:          .ascii  "761295520"

596 idcandt:        .byte   0xa5                   ! 标号idcandt意思是ID of Chip AND Tech.

597 idgenoa:        .byte   0x77, 0x00, 0x66, 0x99

598 idparadise:     .ascii  "VGA="

599

    ! 下面是各种显示卡可使用的扩展模式个数和对应的模式号列表。其中每一行第1个字节是模式个

    ! 数值,随后的一些值是中断0x10功能0AH=0)可使用的模式号。例如从602行可知,对于ATI

    ! 牌子的显示卡,除了标准模式以外还可使用两种扩展模式:0x230x33

600 ! Manufacturer:   Numofmodes:   Mode:

    ! 厂家:          模式数量:    模式列表:

601

602 moati:          .byte   0x02,   0x23, 0x33

603 moahead:        .byte   0x05,   0x22, 0x23, 0x24, 0x2f, 0x34

604 mocandt:        .byte   0x02,   0x60, 0x61

605 mocirrus:       .byte   0x04,   0x1f, 0x20, 0x22, 0x31

606 moeverex:       .byte   0x0a,   0x03, 0x04, 0x07, 0x08, 0x0a, 0x0b, 0x16, 0x18, 0x21, 0x40

607 mogenoa:        .byte   0x0a,   0x58, 0x5a, 0x60, 0x61, 0x62, 0x63, 0x64, 0x72, 0x74, 0x78

608 moparadise:     .byte   0x02,   0x55, 0x54

609 motrident:      .byte   0x07,   0x50, 0x51, 0x52, 0x57, 0x58, 0x59, 0x5a

610 motseng:        .byte   0x05,   0x26, 0x2a, 0x23, 0x24, 0x22

611 movideo7:       .byte   0x06,   0x40, 0x43, 0x44, 0x41, 0x42, 0x45

612

    ! 下面是各种牌子VGA显示卡可使用的模式对应的列、行值列表。例如第615行表示ATI显示卡两

    ! 种扩展模式的列、行值分别是 132 x 25132 x 44

613 !                       msb = Cols   lsb = Rows:

    !                       高字节=列数  低字节=行数:

614

615 dscati:         .word   0x8419, 0x842c                           ! ATI卡可设置列、行值。

616 dscahead:       .word   0x842c, 0x8419, 0x841c, 0xa032, 0x5042   ! Ahead卡可设置值。

617 dsccandt:       .word   0x8419, 0x8432

618 dsccirrus:      .word   0x8419, 0x842c, 0x841e, 0x6425

619 dsceverex:      .word   0x5022, 0x503c, 0x642b, 0x644b, 0x8419, 0x842c, 0x501e, 0x641b, 0xa040, 0x841e

620 dscgenoa:       .word   0x5020, 0x642a, 0x8419, 0x841d, 0x8420, 0x842c, 0x843c, 0x503c, 0x5042, 0x644b

621 dscparadise:    .word   0x8419, 0x842b

622 dsctrident:     .word   0x501e, 0x502b, 0x503c, 0x8419, 0x841e, 0x842b, 0x843c

623 dsctseng:       .word   0x503c, 0x6428, 0x8419, 0x841c, 0x842c

624 dscvideo7:      .word   0x502b, 0x503c, 0x643c, 0x8419, 0x842c, 0x841c

625        

626 .text

627 endtext:

628 .data

629 enddata:

630 .bss

631 endbss:


 

 


 

6.3 程序6-3 linux/boot/head.s


  1 /*

  2  *  linux/boot/head.s

  3  *

  4  *  (C) 1991  Linus Torvalds

  5  */

  6

  7 /*

  8  *  head.s contains the 32-bit startup code.

  9  *

 10  * NOTE!!! Startup happens at absolute address 0x00000000, which is also where

 11  * the page directory will exist. The startup code will be overwritten by

 12  * the page directory.

 13  */

    /*

     *  head.s含有32位启动代码。

     * 注意!!! 32位启动代码是从绝对地址0x00000000开始的,这里也同样是页目录将存在的地方,

     * 因此这里的启动代码将被页目录覆盖掉。

     */

 14 .text

 15 .globl _idt,_gdt,_pg_dir,_tmp_floppy_area

 16 _pg_dir:                   # 页目录将会存放在这里。

 

    # 再次注意!!! 这里已经处于32位运行模式,因此这里的$0x10并不是把地址0x10装入各个

    # 段寄存器,它现在其实是全局段描述符表中的偏移值,或者更准确地说是一个描述符表项

    # 的选择符。有关选择符的说明请参见setup.s193行下的说明。这里$0x10的含义是请求

    # 特权级0(0-1=0)、选择全局描述符表(2=0)、选择表中第2(3-15=2)。它正好指

    # 向表中的数据段描述符项。(描述符的具体数值参见前面setup.s212213行)

    # 下面代码的含义是:设置ds,es,fs,gssetup.s 中构造的数据段(全局段描述符表第2项)

    # 的选择符=0x10,并将堆栈放置在stack_start指向的user_stack数组区,然后使用本程序

    # 后面定义的新中断描述符表和全局段描述表。新全局段描述表中初始内容与setup.s中的基本

    # 一样,仅段限长从8MB修改成了16MBstack_start定义在kernel/sched.c69行。它是指向

    # user_stack数组末端的一个长指针。第23行设置这里使用的栈,姑且称为系统栈。但在移动到

    # 任务0执行(init/main.c137行)以后该栈就被用作任务0和任务1共同使用的用户栈了。

 

 17 startup_32:                  # 18-22行设置各个数据段寄存器。

 18         movl $0x10,%eax      # 对于GNU汇编,每个直接操作数要以'$'开始,否则表示地址。

                                 # 每个寄存器名都要以'%'开头,eax表示是32位的ax寄存器。

 19         mov %ax,%ds

 20         mov %ax,%es

 21         mov %ax,%fs

 22         mov %ax,%gs

 23         lss _stack_start,%esp      # 表示_stack_startèss:esp,设置系统堆栈。

                                       # stack_start定义在kernel/sched.c69行。

 24         call setup_idt             # 调用设置中断描述符表子程序。

 25         call setup_gdt             # 调用设置全局描述符表子程序。

 26         movl $0x10,%eax            # reload all the segment registers

 27         mov %ax,%ds                # after changing gdt. CS was already

 28         mov %ax,%es                # reloaded in 'setup_gdt'

 29         mov %ax,%fs                # 因为修改了gdt,所以需要重新装载所有的段寄存器。

 30         mov %ax,%gs                # CS代码段寄存器已经在setup_gdt中重新加载过了。

 

    # 由于段描述符中的段限长从setup.s中的8MB改成了本程序设置的16MB(见setup.s208-216

    # 和本程序后面的235-236行),因此这里再次对所有段寄存器执行加载操作是必须的。另外,通过

    # 使用bochs跟踪观察,如果不对CS再次执行加载,那么在执行到26行时CS代码段不可见部分中

    # 的限长还是8MB。这样看来应该重新加载CS。但是由于setup.s中的内核代码段描述符与本程序中

    # 重新设置的代码段描述符除了段限长以外其余部分完全一样,8MB的限长在内核初始化阶段不会有

    # 问题,而且在以后内核执行过程中段间跳转时会重新加载CS。因此这里没有加载它并没有让程序

    # 出错。

    # 针对该问题,目前内核中就在第25行之后添加了一条长跳转指令:'ljmp $(__KERNEL_CS),$1f'

    # 跳转到第26行来确保CS确实又被重新加载。

 

 31         lss _stack_start,%esp

 

    # 32-36行用于测试A20地址线是否已经开启。采用的方法是向内存地址0x000000处写入任意

    # 一个数值,然后看内存地址0x100000(1M)处是否也是这个数值。如果一直相同的话,就一直

    # 比较下去,也即死循环、死机。表示地址A20线没有选通,结果内核就不能使用1MB以上内存。

    #

    # 33行上的'1:'是一个局部符号构成的标号。标号由符号后跟一个冒号组成。此时该符号表示活动

    # 位置计数(Active location counter)的当前值,并可以作为指令的操作数。局部符号用于帮助

    # 编译器和编程人员临时使用一些名称。共有10个局部符号名,可在整个程序中重复使用。这些符号

    # 名使用名称'0''1'...'9'来引用。为了定义一个局部符号,需把标号写成'N:'形式(其中N

    # 表示一个数字)。为了引用先前最近定义的这个符号,需要写成'Nb',其中N是定义标号时使用的

    # 数字。为了引用一个局部标号的下一个定义,需要写成'Nf',这里N10个前向引用之一。上面

    # 'b'表示向后(backwards'f'表示向前(forwards。在汇编程序的某一处,我们最大

    # 可以向后/向前引用10个标号(最远第10个)。

 

 32         xorl %eax,%eax

 33 1:      incl %eax                  # check that A20 really IS enabled

 34         movl %eax,0x000000         # loop forever if it isn't

 35         cmpl %eax,0x100000

 36         je 1b                      # '1b'表示向后(backward)跳转到标号1去(33行)。

                                       # 若是'5f'则表示向前(forward)跳转到标号5去。

 37 /*

 38  * NOTE! 486 should set bit 16, to check for write-protect in supervisor

 39  * mode. Then it would be unnecessary with the "verify_area()"-calls.

 40  * 486 users probably want to set the NE (#5) bit also, so as to use

 41  * int 16 for math errors.

 42  */

    /*

     * 注意! 在下面这段程序中,486应该将位16置位,以检查在超级用户模式下的写保护,

     * 此后 "verify_area()" 调用就不需要了。486的用户通常也会想将NE(#5)置位,以便

     * 对数学协处理器的出错使用int 16

     */

    # 上面原注释中提到的486 CPUCR0控制寄存器的位16是写保护标志WPWrite-Protect),

    # 用于禁止超级用户级的程序向一般用户只读页面中进行写操作。该标志主要用于操作系统在创建

    # 新进程时实现写时复制(copy-on-write)方法。

    # 下面这段程序(43-65)用于检查数学协处理器芯片是否存在。方法是修改控制寄存器CR0,在

    # 假设存在协处理器的情况下执行一个协处理器指令,如果出错的话则说明协处理器芯片不存在,

    # 需要设置CR0中的协处理器仿真位EM(位2),并复位协处理器存在标志MP(位1)。

 

 43         movl %cr0,%eax             # check math chip

 44         andl $0x80000011,%eax      # Save PG,PE,ET

 45 /* "orl $0x10020,%eax" here for 486 might be good */

 46         orl $2,%eax                # set MP

 47         movl %eax,%cr0

 48         call check_x87

 49         jmp after_page_tables      # 跳转到135行。

 50

 51 /*

 52  * We depend on ET to be correct. This checks for 287/387.

 53  */

    /*

     * 我们依赖于ET标志的正确性来检测287/387存在与否。

     */

    # 下面fninitfstsw是数学协处理器(80287/80387)的指令。

    # finit 向协处理器发出初始化命令,它会把协处理器置于一个未受以前操作影响的已知状态,设置

    # 其控制字为默认值、清除状态字和所有浮点栈式寄存器。非等待形式的这条指令(fninit)还会让

    # 协处理器终止执行当前正在执行的任何先前的算术操作。fstsw 指令取协处理器的状态字。如果系

    # 统中存在协处理器的话,那么在执行了fninit指令后其状态字低字节肯定为0

 

 54 check_x87:

 55         fninit                     # 向协处理器发出初始化命令。

 56         fstsw %ax                  # 取协处理器状态字到ax寄存器中。

 57         cmpb $0,%al                # 初始化后状态字应该为0,否则说明协处理器不存在。

 58         je 1f                      /* no coprocessor: have to set bits */

 59         movl %cr0,%eax             # 如果存在则向前跳转到标号1处,否则改写cr0

 60         xorl $6,%eax               /* reset MP, set EM */

 61         movl %eax,%cr0

 62         ret

 

    # 下面是一汇编语言指示符。其含义是指存储边界对齐调整。"2"表示把随后的代码或数据的偏移位置

    # 调整到地址值最后2比特位为零的位置(2^2),即按4字节方式对齐内存地址。不过现在GNU as

    # 直接时写出对齐的值而非2的次方值了。使用该指示符的目的是为了提高32CPU访问内存中代码

    # 或数据的速度和效率。参见程序后的详细说明。

    # 下面的两个字节值是80287协处理器指令fsetpm的机器码。其作用是把80287设置为保护模式。

    # 80387无需该指令,并且将会把该指令看作是空操作。

 

 63 .align 2

 64 1:      .byte 0xDB,0xE4            /* fsetpm for 287, ignored by 387 */  # 287协处理器码。

 65         ret

 66

 67 /*

 68  *  setup_idt

 69  *

 70  *  sets up a idt with 256 entries pointing to

 71  *  ignore_int, interrupt gates. It then loads

 72  *  idt. Everything that wants to install itself

 73  *  in the idt-table may do so themselves. Interrupts

 74  *  are enabled elsewhere, when we can be relatively

 75  *  sure everything is ok. This routine will be over-

 76  *  written by the page tables.

 77  */

    /*

     * 下面这段是设置中断描述符表子程序 setup_idt

     *

     * 将中断描述符表idt设置成具有256个项,并都指向ignore_int中断门。然后加载中断

     * 描述符表寄存器(lidt指令)。真正实用的中断门以后再安装。当我们在其他地方认为一切

     * 都正常时再开启中断。该子程序将会被页表覆盖掉。

     */

    # 中断描述符表中的项虽然也是8字节组成,但其格式与全局表中的不同,被称为门描述符

    # (Gate Descriptor)。它的0-1,6-7字节是偏移量,2-3字节是选择符,4-5字节是一些标志。

    # 这段代码首先在edxeax中组合设置出8字节默认的中断描述符值,然后在idt表每一项中

    # 都放置该描述符,共256项。eax含有描述符低4字节,edx含有高4字节。内核在随后的初始

    # 化过程中会替换安装那些真正实用的中断描述符项。

 

 78 setup_idt:

 79         lea ignore_int,%edx        # ignore_int的有效地址(偏移值)值èedx寄存器

 80         movl $0x00080000,%eax      # 将选择符0x0008置入eax的高16位中。

 81         movw %dx,%ax               /* selector = 0x0008 = cs */

                                       # 偏移值的低16位置入eax的低16位中。此时eax含有

                                       # 门描述符低4字节的值。

 82         movw $0x8E00,%dx           /* interrupt gate - dpl=0, present */

 83                                    # 此时edx含有门描述符高4字节的值。

 84         lea _idt,%edi              # _idt是中断描述符表的地址。

 85         mov $256,%ecx

 86 rp_sidt:

 87         movl %eax,(%edi)           # 将哑中断门描述符存入表中。

 88         movl %edx,4(%edi)          # eax内容放到 edi+4 所指内存位置处。

 89         addl $8,%edi               # edi指向表中下一项。

 90         dec %ecx

 91         jne rp_sidt

 92         lidt idt_descr             # 加载中断描述符表寄存器值。

 93         ret

 94

 95 /*

 96  *  setup_gdt

 97  *

 98  *  This routines sets up a new gdt and loads it.

 99  *  Only two entries are currently built, the same

100  *  ones that were built in init.s. The routine

101  *  is VERY complicated at two whole lines, so this

102  *  rather long comment is certainly needed :-).

103  *  This routine will beoverwritten by the page tables.

104  */

    /*

     * 设置全局描述符表项 setup_gdt

     * 这个子程序设置一个新的全局描述符表gdt,并加载。此时仅创建了两个表项,与前

     * 面的一样。该子程序只有两行,“非常的”复杂,所以当然需要这么长的注释了J

     * 该子程序将被页表覆盖掉。

     */

105 setup_gdt:

106         lgdt gdt_descr             # 加载全局描述符表寄存器(内容已设置好,见234-238)

107         ret

108

109 /*

110  * I put the kernel page tables right after the page directory,

111  * using 4 of them to span 16 Mb of physical memory. People with

112  * more than 16MB will have to expand this.

113  */

    /* Linus将内核的内存页表直接放在页目录之后,使用了4个表来寻址16 MB的物理内存。

     * 如果你有多于16 Mb的内存,就需要在这里进行扩充修改。

     */

     # 每个页表长为4 Kb字节(1页内存页面),而每个页表项需要4个字节,因此一个页表共可以存放

     # 1024个表项。如果一个页表项寻址4 KB的地址空间,则一个页表就可以寻址4 MB的物理内存。

     # 页表项的格式为:项的前0-11位存放一些标志,例如是否在内存中(P0)、读写许可(R/W1)

     # 普通用户还是超级用户使用(U/S2)、是否修改过(是否脏了)(D6)等;表项的位12-31

     # 页框地址,用于指出一页内存的物理起始地址。

 

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

115 pg0:

116

117 .org 0x2000

118 pg1:

119

120 .org 0x3000

121 pg2:

122

123 .org 0x4000

124 pg3:

125

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

127 /*

128  * tmp_floppy_area is used by the floppy-driver when DMA cannot

129  * reach to a buffer-block. It needs to be aligned, so that it isn't

130  * on a 64kB border.

131  */

    /* DMA(直接存储器访问)不能访问缓冲块时,下面的tmp_floppy_area内存块

     * 就可供软盘驱动程序使用。其地址需要对齐调整,这样就不会跨越64KB边界。

     */

132 _tmp_floppy_area:

133         .fill 1024,1,0             # 共保留1024项,每项1字节,填充数值0

134

    # 下面这几个入栈操作用于为跳转到init/main.c中的main()函数作准备工作。第139行上

    # 的指令在栈中压入了返回地址,而第140行则压入了main()函数代码的地址。当head.s

    # 最后在第218行执行ret指令时就会弹出main()的地址,并把控制权转移到init/main.c

    # 程序中。参见第3章中有关C函数调用机制的说明。

    # 前面3个入栈0值应该分别表示envpargv指针和argc的值,但main()没有用到。

    # 139行的入栈操作是模拟调用main.c程序时首先将返回地址入栈的操作,所以如果

    # main.c程序真的退出时,就会返回到这里的标号L6处继续执行下去,也即死循环。

    # 140行将main.c的地址压入堆栈,这样,在设置分页处理(setup_paging)结束后

    # 执行'ret'返回指令时就会将main.c程序的地址弹出堆栈,并去执行main.c程序了。

    # 有关C函数调用机制请参见程序后的说明。

135 after_page_tables:

136         pushl $0                   # These are the parameters to main :-)

137         pushl $0                   # 这些是调用main程序的参数(指init/main.c)。

138         pushl $0                   # 其中的'$'符号表示这是一个立即操作数。

139         pushl $L6                  # return address for main, if it decides to.

140         pushl $_main               # '_main'是编译程序对main的内部表示方法。

141         jmp setup_paging           # 跳转至第198行。

142 L6:

143         jmp L6                     # main should never return here, but

144                                    # just in case, we know what happens.

                                       # main程序绝对不应该返回到这里。不过为了以防万一,

                                       # 所以添加了该语句。这样我们就知道发生什么问题了。

145

146 /* This is the default interrupt "handler" :-) */

    /* 下面是默认的中断“向量句柄”J */

147 int_msg:

148         .asciz "Unknown interrupt\n\r"     # 定义字符串“未知中断(回车换行)”。

149 .align 2                   # 4字节方式对齐内存地址。

150 ignore_int:

151         pushl %eax

152         pushl %ecx

153         pushl %edx

154         push %ds           # 这里请注意!!ds,es,fs,gs等虽然是16位的寄存器,但入栈后

155         push %es           # 仍然会以32位的形式入栈,也即需要占用4个字节的堆栈空间。

156         push %fs

157         movl $0x10,%eax    # 置段选择符(使ds,es,fs指向gdt表中的数据段)。

158         mov %ax,%ds

159         mov %ax,%es

160         mov %ax,%fs

161         pushl $int_msg     # 把调用printk函数的参数指针(地址)入栈。注意!若int_msg

162         call _printk       # 前不加'$',则表示把int_msg符号处的长字('Unkn')入栈J

163         popl %eax          # 该函数在/kernel/printk.c中。'_printk'printk编译后模块中

164         pop %fs            # 的内部表示法。

165         pop %es

166         pop %ds

167         popl %edx

168         popl %ecx

169         popl %eax

170         iret           # 中断返回(把中断调用时压入栈的CPU标志寄存器(32位)值也弹出)。

171

172

173 /*

174  * Setup_paging

175  *

176  * This routine sets up paging by setting the page bit

177  * in cr0. The page tables are set up, identity-mapping

178  * the first 16MB. The pager assumes that no illegal

179  * addresses are produced (ie >4Mb on a 4Mb machine).

180  *

181  * NOTE! Although all physical memory should be identity

182  * mapped by this routine, only the kernel page functions

183  * use the >1Mb addresses directly. All "normal" functions

184  * use just the lower 1Mb, or the local data space, which

185  * will be mapped to some other place - mm keeps track of

186  * that.

187  *

188  * For those with more memory than 16 Mb - tough luck. I've

189  * not got it, why should you :-) The source is here. Change

190  * it. (Seriously - it shouldn't be too difficult. Mostly

191  * change some constants etc. I left it at 16Mb, as my machine

192  * even cannot be extended past that (ok, but it was cheap :-)

193  * I've tried to show which constants to change by having

194  * some kind of marker at them (search for "16Mb"), but I

195  * won't guarantee that's all :-( )

196  */

    /*

     * 这个子程序通过设置控制寄存器cr0的标志(PG 31)来启动对内存的分页处理功能,

     * 并设置各个页表项的内容,以恒等映射前16 MB的物理内存。分页器假定不会产生非法的

     * 地址映射(也即在只有4Mb的机器上设置出大于4Mb的内存地址)。

     *

     * 注意!尽管所有的物理地址都应该由这个子程序进行恒等映射,但只有内核页面管理函数能

     * 直接使用>1Mb的地址。所有“普通”函数仅使用低于1Mb的地址空间,或者是使用局部数据

     * 空间,该地址空间将被映射到其他一些地方去 -- mm(内存管理程序)会管理这些事的。

     *

     * 对于那些有多于16Mb内存的家伙 真是太幸运了,我还没有,为什么你会有J。代码就在

     * 这里,对它进行修改吧。(实际上,这并不太困难的。通常只需修改一些常数等。我把它设置

     * 16Mb,因为我的机器再怎么扩充甚至不能超过这个界限(当然,我的机器是很便宜的J)。

     * 我已经通过设置某类标志来给出需要改动的地方(搜索“16Mb”),但我不能保证作这些

     * 改动就行了L)。

     */

    # 上面英文注释第2段的含义是指在机器物理内存中大于1MB的内存空间主要被用于主内存区。

    # 主内存区空间由mm模块管理。它涉及到页面映射操作。内核中所有其他函数就是这里指的一般

    #(普通)函数。若要使用主内存区的页面,就需要使用get_free_page()等函数获取。因为主内

    # 存区中内存页面是共享资源,必须有程序进行统一管理以避免资源争用和竞争。

    #

    # 在内存物理地址0x0处开始存放1页页目录表和4页页表。页目录表是系统所有进程公用的,而

    # 这里的4页页表则属于内核专用,它们一一映射线性地址起始16MB空间范围到物理内存上。对于

    # 新的进程,系统会在主内存区为其申请页面存放页表。另外,1页内存长度是4096字节。

 

197 .align 2                           # 4字节方式对齐内存地址边界。

198 setup_paging:                      # 首先对5页内存(1页目录 + 4页页表)清零。

199         movl $1024*5,%ecx          /* 5 pages - pg_dir+4 page tables */

200         xorl %eax,%eax

201         xorl %edi,%edi             /* pg_dir is at 0x000 */

                                       # 页目录从0x000地址开始。

202         cld;rep;stosl              # eax内容存到es:edi所指内存位置处,且edi4

 

    # 下面4句设置页目录表中的项,因为我们(内核)共有4个页表所以只需设置4项。

    # 页目录项的结构与页表中项的结构一样,4个字节为1项。参见上面113行下的说明。

    # 例如"$pg0+7"表示:0x00001007,是页目录表中的第1项。

    # 则第1个页表所在的地址 = 0x00001007 & 0xfffff000 = 0x1000

    # 1个页表的属性标志 = 0x00001007 & 0x00000fff = 0x07,表示该页存在、用户可读写。

203         movl $pg0+7,_pg_dir        /* set present bit/user r/w */

204         movl $pg1+7,_pg_dir+4      /*  --------- " " --------- */

205         movl $pg2+7,_pg_dir+8      /*  --------- " " --------- */

206         movl $pg3+7,_pg_dir+12     /*  --------- " " --------- */

 

    # 下面6行填写4个页表中所有项的内容,共有:4(页表)*1024(/页表)=4096(0 - 0xfff)

    # 也即能映射物理内存 4096*4Kb = 16Mb

    # 每项的内容是:当前项所映射的物理内存地址 + 该页的标志(这里均为7)。

    # 使用的方法是从最后一个页表的最后一项开始按倒退顺序填写。一个页表的最后一项在页表中的

    # 位置是1023*4 = 4092。因此最后一页的最后一项的位置就是$pg3+4092

 

207         movl $pg3+4092,%edi        # ediè最后一页的最后一项。

208         movl $0xfff007,%eax        /*  16Mb - 4096 + 7 (r/w user,p) */

                                       # 最后1项对应物理内存页面的地址是0xfff000

                                       # 加上属性标志7,即为0xfff007

209         std                        # 方向位置位,edi值递减(4字节)

210 1:      stosl                      /* fill pages backwards - more efficient :-) */

211         subl $0x1000,%eax          # 每填写好一项,物理地址值减0x1000

212         jge 1b                     # 如果小于0则说明全添写好了。

    # 设置页目录表基址寄存器cr3的值,指向页目录表。cr3中保存的是页目录表的物理地址。

213         xorl %eax,%eax             /* pg_dir is at 0x0000 */   # 页目录表在0x0000处。

214         movl %eax,%cr3             /* cr3 - page directory start */

    # 设置启动使用分页处理(cr0PG标志,位31

215         movl %cr0,%eax

216         orl $0x80000000,%eax       # 添上PG标志。

217         movl %eax,%cr0             /* set paging (PG) bit */

218         ret                        /* this also flushes prefetch-queue */

 

    # 在改变分页处理标志后要求使用转移指令刷新预取指令队列,这里用的是返回指令ret

    # 该返回指令的另一个作用是将140行压入堆栈中的main程序的地址弹出,并跳转到/init/main.c

    # 程序去运行。本程序到此就真正结束了。

 

219

220 .align 2                           # 4字节方式对齐内存地址边界。

221 .word 0                            # 这里先空出2字节,这样224行上的长字是4字节对齐的。

 

    ! 下面是加载中断描述符表寄存器idtr的指令lidt要求的6字节操作数。前2字节是idt表的限长,

    ! 4字节是idt表在线性地址空间中的32位基地址。

222 idt_descr:

223         .word 256*8-1              # idt contains 256 entries  # 256项,限长=长度 - 1

224         .long _idt

225 .align 2

226 .word 0

 

    ! 下面加载全局描述符表寄存器gdtr的指令lgdt要求的6字节操作数。前2字节是gdt表的限长,

    ! 4字节是gdt表的线性基地址。这里全局表长度设置为2KB字节(0x7ff即可),因为每8字节

    ! 组成一个描述符项,所以表中共可有256项。符号_gdt是全局表在本程序中的偏移位置,见234行。

227 gdt_descr:

228         .word 256*8-1              # so does gdt (not that that's any   # 注:not à note

229         .long _gdt                 # magic number, but it works for me :^)

230

231         .align 3                   # 82^3)字节方式对齐内存地址边界。

232 _idt:   .fill 256,8,0              # idt is uninitialized   # 256项,每项8字节,填0

233

    # 全局表。前4项分别是空项(不用)、代码段描述符、数据段描述符、系统调用段描述符,其中

    # 系统调用段描述符并没有派用处,Linus当时可能曾想把系统调用代码专门放在这个独立的段中。

    # 后面还预留了252项的空间,用于放置所创建任务的局部描述符(LDT)和对应的任务状态段TSS

    # 的描述符。

    # (0-nul, 1-cs, 2-ds, 3-syscall, 4-TSS0, 5-LDT0, 6-TSS1, 7-LDT1, 8-TSS2 etc...)

 

234 _gdt:   .quad 0x0000000000000000        /* NULL descriptor */

235         .quad 0x00c09a0000000fff        /* 16Mb */      # 0x08,内核代码段最大长度16MB

236         .quad 0x00c0920000000fff        /* 16Mb */      # 0x10,内核数据段最大长度16MB

237         .quad 0x0000000000000000        /* TEMPORARY - don't use */

238         .fill 252,8,0                   /* space for LDT's and TSS's etc */  # 预留空间。



 

 

第7章 内核初始化程序

7.1 程序7-1 linux/init/main.c


  1 /*

  2  *  linux/init/main.c

  3  *

  4  *  (C) 1991  Linus Torvalds

  5  */

  6

    // 定义宏“__LIBRARY__”是为了包括定义在unistd.h中的内嵌汇编代码等信息。

  7 #define __LIBRARY__

    // *.h头文件所在的默认目录是include/,则在代码中就不用明确指明其位置。如果不是UNIX

    // 标准头文件,则需要指明所在的目录,并用双引号括住。unistd.h是标准符号常数与类型文件。

    // 其中定义了各种符号常数和类型,并声明了各种函数。如果还定义了符号__LIBRARY__,则还会

    // 包含系统调用号和内嵌汇编代码syscall0()等。

  8 #include <unistd.h>

  9 #include <time.h>    // 时间类型头文件。其中最主要定义了tm结构和一些有关时间的函数原形。

 10

 11 /*

 12  * we need this inline - forking from kernel space will result

 13  * in NO COPY ON WRITE (!!!), until an execve is executed. This

 14  * is no problem, but for the stack. This is handled by not letting

 15  * main() use the stack at all after fork(). Thus, no function

 16  * calls - which means inline code for fork too, as otherwise we

 17  * would use the stack upon exit from 'fork()'.

 18  *

 19  * Actually only pause and fork are needed inline, so that there

 20  * won't be any messing with the stack from main(), but we define

 21  * some others too.

 22  */

    /*

     * 我们需要下面这些内嵌语句 - 从内核空间创建进程将导致没有写时复制(COPY ON WRITE)!!!

     * 直到执行一个execve调用。这对堆栈可能带来问题。处理方法是在fork()调用后不让main()

     * 使用任何堆栈。因此就不能有函数调用 - 这意味着fork也要使用内嵌的代码,否则我们在从

     * fork()退出时就要使用堆栈了。

     *

     * 实际上只有pausefork需要使用内嵌方式,以保证从main()中不会弄乱堆栈,但是我们同

     * 时还定义了其他一些函数。

     */

    // Linux在内核空间创建进程时不使用写时复制技术(Copy on write)。main()在移动到用户

    // 模式(到任务0)后执行内嵌方式的fork()pause(),因此可保证不使用任务0的用户栈。

    // 在执行moveto_user_mode()之后,本程序main()就以任务0的身份在运行了。而任务0是所

    // 有将创建子进程的父进程。当它创建一个子进程时(init进程),由于任务1代码属于内核

    // 空间,因此没有使用写时复制功能。此时任务0的用户栈就是任务1的用户栈,即它们共同

    // 使用一个栈空间。因此希望在main.c运行在任务0的环境下时不要有对堆栈的任何操作,以

    // 免弄乱堆栈。而在再次执行fork()并执行过execve()函数后,被加载程序已不属于内核空间,

    // 因此可以使用写时复制技术了。请参见5.3节“Linux内核使用内存的方法”内容。

 

    // 下面_syscall0()unistd.h中的内嵌宏代码。以嵌入汇编的形式调用Linux的系统调用中断

    // 0x80。该中断是所有系统调用的入口。该条语句实际上是int fork()创建进程系统调用。可展

    // 开看之就会立刻明白。syscall0名称中最后的0表示无参数,1表示1个参数。

    // 参见include/unistd.h133行。

 23 static inline _syscall0(int,fork)

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

 24 static inline _syscall0(int,pause)

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

 25 static inline _syscall1(int,setup,void *,BIOS)

    // int sync()系统调用:更新文件系统。

 26 static inline _syscall0(int,sync)

 27

 28 #include <linux/tty.h>    // tty头文件,定义了有关tty_io,串行通信方面的参数、常数。

 29 #include <linux/sched.h>  // 调度程序头文件,定义了任务结构task_struct、第1个初始任务

                              // 的数据。还有一些以宏的形式定义的有关描述符参数设置和获取的

                              // 嵌入式汇编函数程序。

 30 #include <linux/head.h>   // head头文件,定义了段描述符的简单结构,和几个选择符常量。

 31 #include <asm/system.h>   // 系统头文件。以宏形式定义了许多有关设置或修改描述符/中断门

                              // 等的嵌入式汇编子程序。

 32 #include <asm/io.h>       // io头文件。以宏的嵌入汇编程序形式定义对io端口操作的函数。

 33

 34 #include <stddef.h>       // 标准定义头文件。定义了NULL, offsetof(TYPE, MEMBER)

 35 #include <stdarg.h>       // 标准参数头文件。以宏的形式定义变量参数列表。主要说明了-

                              // 类型(va_list)和三个宏(va_start, va_argva_end)vsprintf

                              // vprintfvfprintf

 36 #include <unistd.h>

 37 #include <fcntl.h>        // 文件控制头文件。用于文件及其描述符的操作控制常数符号的定义。

 38 #include <sys/types.h>    // 类型头文件。定义了基本的系统数据类型。

 39

 40 #include <linux/fs.h>     // 文件系统头文件。定义文件表结构(file,buffer_head,m_inode等)。

 41                           // 其中有定义:extern int ROOT_DEV

 42 #include <string.h>       // 字符串头文件。主要定义了一些有关内存或字符串操作的嵌入函数。

 43

 44 static char printbuf[1024];           // 静态字符串数组,用作内核显示信息的缓存。

 45

 46 extern char *strcpy();

 47 extern int vsprintf();                // 送格式化输出到一字符串中(vsprintf.c92行)。

 48 extern void init(void);               // 函数原形,初始化(本程序168行)。

 49 extern void blk_dev_init(void);       // 块设备初始化子程序(blk_drv/ll_rw_blk.c,157行)

 50 extern void chr_dev_init(void);       // 字符设备初始化(chr_drv/tty_io.c, 347行)

 51 extern void hd_init(void);            // 硬盘初始化程序(blk_drv/hd.c, 343行)

 52 extern void floppy_init(void);        // 软驱初始化程序(blk_drv/floppy.c, 457行)

 53 extern void mem_init(long start, long end);      // 内存管理初始化(mm/memory.c, 399行)

 54 extern long rd_init(long mem_start, int length); // 虚拟盘初始化(blk_drv/ramdisk.c,52)

 55 extern long kernel_mktime(struct tm * tm);       // 计算系统开机启动时间(秒)。

 56

    // 内核专用sprintf()函数。该函数用于产生格式化信息并输出到指定缓冲区str中。参数'*fmt'

    // 指定输出将采用的格式,参见标准C语言书籍。该子程序正好是vsprintf如何使用的一个简单

    // 例子。函数使用vsprintf()将格式化字符串放入str缓冲区,参见第179行上的printf()函数。

 57 static int sprintf(char * str, const char *fmt, ...)

 58 {

 59         va_list args;

 60         int i;

 61

 62         va_start(args, fmt);

 63         i = vsprintf(str, fmt, args);

 64         va_end(args);

 65         return i;

 66 }

 67

 68 /*

 69  * This is set up by the setup-routine at boot-time

 70  */

    /*

     * 以下这些数据是在内核引导期间由setup.s程序设置的。

     */

    // 下面三行分别将指定的线性地址强行转换为给定数据类型的指针,并获取指针所指内容。由于

    // 内核代码段被映射到从物理地址零开始的地方,因此这些线性地址正好也是对应的物理地址。

    // 这些指定地址处内存值的含义请参见第6章的表6-2setup程序读取并保存的参数)。

    // drive_info结构请参见下面第125行。

 71 #define EXT_MEM_K (*(unsigned short *)0x90002)      // 1MB以后的扩展内存大小(KB)。

 72 #define CON_ROWS ((*(unsigned short *)0x9000e) & 0xff)  // 选定的控制台屏幕行、列数。

 73 #define CON_COLS (((*(unsigned short *)0x9000e) & 0xff00) >> 8)

 74 #define DRIVE_INFO (*(struct drive_info *)0x90080)  // 硬盘参数表32字节内容。

 75 #define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)  // 根文件系统所在设备号。

 76 #define ORIG_SWAP_DEV (*(unsigned short *)0x901FA)  // 交换文件所在设备号。

 77

 78 /*

 79  * Yeah, yeah, it's ugly, but I cannot find how to do this correctly

 80  * and this seems to work. I anybody has more info on the real-time

 81  * clock I'd be interested. Most of this was trial and error, and some

 82  * bios-listing reading. Urghh.

 83  */

    /*

     * 是啊,是啊,下面这段程序很差劲,但我不知道如何正确地实现,而且好象

     * 它还能运行。如果有关于实时时钟更多的资料,那我很感兴趣。这些都是试

     * 探出来的,另外还看了一些bios程序,呵!

     */

 84

    // 这段宏读取CMOS实时时钟信息。outb_pinb_pinclude/asm/io.h中定义的端口输入输出宏。

 85 #define CMOS_READ(addr) ({ \

 86 outb_p(0x80|addr,0x70); \       // 0x70是写地址端口号,0x80|addr是要读取的CMOS内存地址。

 87 inb_p(0x71); \                  // 0x71是读数据端口号。

 88 })

 89

    // 定义宏。将BCD码转换成二进制数值。BCD码利用半个字节(4比特)表示一个10进制数,因此

    // 一个字节表示210进制数。(val)&15BCD表示的10进制个位数,而 (val)>>4BCD表示

    // 10进制十位数,再乘以10。因此最后两者相加就是一个字节BCD码的实际二进制数值。

 90 #define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)

 91

    // 该函数取CMOS实时钟信息作为开机时间,并保存到全局变量startup_time()中。参见后面

    // CMOS内存列表说明。其中调用的函数kernel_mktime()用于计算从1970110时起到

    // 开机当日经过的秒数,作为开机时间(kernel/mktime.c 41行)。

 92 static void time_init(void)

 93 {

 94         struct tm time;                        // 时间结构tm定义在include/time.h中。

 95

    // CMOS的访问速度很慢。为了减小时间误差,在读取了下面循环中所有数值后,若此时CMOS

    // 秒值发生了变化,那么就重新读取所有值。这样内核就能把与CMOS时间误差控制在1秒之内。

 96         do {

 97                 time.tm_sec = CMOS_READ(0);       // 当前时间秒值(均是BCD码值)。

 98                 time.tm_min = CMOS_READ(2);       // 当前分钟值。

 99                 time.tm_hour = CMOS_READ(4);      // 当前小时值。

100                 time.tm_mday = CMOS_READ(7);      // 一月中的当天日期。

101                 time.tm_mon = CMOS_READ(8);       // 当前月份(1—12)。

102                 time.tm_year = CMOS_READ(9);      // 当前年份。

103         } while (time.tm_sec != CMOS_READ(0));

104         BCD_TO_BIN(time.tm_sec);                  // 转换成二进制数值。

105         BCD_TO_BIN(time.tm_min);

106         BCD_TO_BIN(time.tm_hour);

107         BCD_TO_BIN(time.tm_mday);

108         BCD_TO_BIN(time.tm_mon);

109         BCD_TO_BIN(time.tm_year);

110         time.tm_mon--;                            // tm_mon中月份范围是0—11

111         startup_time = kernel_mktime(&time);      // 计算开机时间。kernel/mktime.c 41行。

112 }

113

    // 下面定义一些局部变量。

114 static long memory_end = 0;                       // 机器具有的物理内存容量(字节数)。

115 static long buffer_memory_end = 0;                // 高速缓冲区末端地址。

116 static long main_memory_start = 0;                // 主内存(将用于分页)开始的位置。

117 static char term[32];                             // 终端设置字符串(环境参数)。

118

    // 读取并执行/etc/rc文件时所使用的命令行参数和环境参数。

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

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

121

    // 运行登录shell时所使用的命令行参数和环境参数。

    // 122行中argv[0]中的字符“-”是传递给shell程序sh的一个标志。通过识别该标志,

    // sh程序会作为登录shell执行。其执行过程与在shell提示符下执行sh不一样。

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

123 static char * envp[] = { "HOME=/usr/root", NULL, NULL };

124

125 struct drive_info { char dummy[32]; } drive_info;    // 用于存放硬盘参数表信息。

126

    // 内核初始化主程序。初始化结束后将以任务0idle任务即空闲任务)的身份运行。

    // 英文注释含义是“这里确实是void,没错。在startup程序(head.s)中就是这样假设的”。参见

    // head.s程序第136行开始的几行代码。

127 void main(void)         /* This really IS void, no error here. */

128 {                       /* The startup routine assumes (well, ...) this */

129 /*

130  * Interrupts are still disabled. Do necessary setups, then

131  * enable them

132  */

    /*

     * 此时中断仍被禁止着,做完必要的设置后就将其开启。

     */

    // 首先保存根文件系统设备号和交换文件设备号,并根据setup.s程序中获取的信息设置控制台

    // 终端屏幕行、列数环境变量TERM,并用其设置初始init进程中执行etc/rc文件和shell程序

    // 使用的环境变量,以及复制内存0x90080处的硬盘参数表。

    // 其中ROOT_DEV 已在前面包含进的include/linux/fs.h文件第206行上被声明为extern int

    // SWAP_DEVinclude/linux/mm.h文件内也作了相同声明。这里mm.h文件并没有显式地列在

    // 本程序前部,因为前面包含进的include/linux/sched.h文件中已经含有它。

133         ROOT_DEV = ORIG_ROOT_DEV;               // ROOT_DEV定义在fs/super.c29行。

134         SWAP_DEV = ORIG_SWAP_DEV;               // SWAP_DEV定义在mm/swap.c36行。

135         sprintf(term, "TERM=con%dx%d", CON_COLS, CON_ROWS);

136         envp[1] = term;

137         envp_rc[1] = term;

138         drive_info = DRIVE_INFO;                // 复制内存0x90080处的硬盘参数表。

 

    // 接着根据机器物理内存容量设置高速缓冲区和主内存区的位置和范围。

    // 高速缓存末端地址èbuffer_memory_end;机器内存容量èmemory_end

    // 主内存开始地址  èmain_memory_start

139         memory_end = (1<<20) + (EXT_MEM_K<<10); // 内存大小=1Mb + 扩展内存(k)*1024字节。

140         memory_end &= 0xfffff000;               // 忽略不到4Kb1页)的内存数。

141         if (memory_end > 16*1024*1024)          // 如果内存量超过16Mb,则按16Mb计。

142                 memory_end = 16*1024*1024;

143         if (memory_end > 12*1024*1024)          // 如果内存>12Mb,则设置缓冲区末端=4Mb

144                 buffer_memory_end = 4*1024*1024;

145         else if (memory_end > 6*1024*1024)      // 否则若内存>6Mb,则设置缓冲区末端=2Mb

146                 buffer_memory_end = 2*1024*1024;

147         else

148                 buffer_memory_end = 1*1024*1024; // 否则则设置缓冲区末端=1Mb

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

 

    // 如果在Makefile文件中定义了内存虚拟盘符号RAMDISK,则初始化虚拟盘。此时主内存将减少。

    //参见kernel/blk_drv/ramdisk.c

150 #ifdef RAMDISK

151         main_memory_start += rd_init(main_memory_start, RAMDISK*1024);

152 #endif

    // 以下是内核进行所有方面的初始化工作。阅读时最好跟着调用的程序深入进去看,若实在看

    // 不下去了,就先放一放,继续看下一个初始化调用 -- 这是经验之谈J

153         mem_init(main_memory_start,memory_end);  // 主内存区初始化。(mm/memory.c399

154         trap_init();          // 陷阱门(硬件中断向量)初始化。(kernel/traps.c181

155         blk_dev_init();       // 块设备初始化。    blk_drv/ll_rw_blk.c157

156         chr_dev_init();       // 字符设备初始化。  chr_drv/tty_io.c347

157         tty_init();           // tty初始化。      chr_drv/tty_io.c406

158         time_init();          // 设置开机启动时间。(见第92行)

159         sched_init();         // 调度程序初始化(加载任务0tr,ldtr)(kernel/sched.c385

160         buffer_init(buffer_memory_end); // 缓冲管理初始化,建内存链表等。(fs/buffer.c348

161         hd_init();            // 硬盘初始化。      blk_drv/hd.c343

162         floppy_init();        // 软驱初始化。      blk_drv/floppy.c457

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

 

    // 下面过程通过在堆栈中设置的参数,利用中断返回指令启动任务0执行。

164         move_to_user_mode();    // 移到用户模式下执行。(include/asm/system.h,第1行)

165         if (!fork()) {          /* we count on this going ok */

166                 init();         // 在新建的子进程(任务1init进程)中执行。

167         }

 

    // 下面代码开始以任务0的身份运行。

168 /*

169  *   NOTE!!   For any other task 'pause()' would mean we have to get a

170  * signal to awaken, but task0 is the sole exception (see 'schedule()')

171  * as task 0 gets activated at every idle moment (when no other tasks

172  * can run). For task0 'pause()' just means we go check if some other

173  * task can run, and if not we return here.

174  */

    /* 注意!! 对于任何其他的任务,'pause()'将意味着我们必须等待收到一个信号

     * 才会返回就绪态,但任务0task0)是唯一例外情况(参见'schedule()'),

     * 因为任务0在任何空闲时间里都会被激活(当没有其他任务在运行时),因此

     * 对于任务0'pause()'仅意味着我们返回来查看是否有其他任务可以运行,如果

     * 没有的话我们就回到这里,一直循环执行'pause()'

     */

    // pause()系统调用(kernel/sched.c,144)会把任务0转换成可中断等待状态,再执行调度函数。

    // 但是调度函数只要发现系统中没有其他任务可以运行时就会切换到任务0,而不依赖于任务0

    // 状态。

175         for(;;)

176                 __asm__("int $0x80"::"a" (__NR_pause):"ax");  // 即执行系统调用pause()

177 }

178

    // 下面函数产生格式化信息并输出到标准输出设备stdout(1),这里是指屏幕上显示。参数'*fmt'

    // 指定输出将采用的格式,参见标准C语言书籍。该子程序正好是vsprintf如何使用的一个简单

    // 例子。该程序使用vsprintf()将格式化的字符串放入printbuf缓冲区,然后用write()将缓冲

    // 区的内容输出到标准设备(1--stdout)。vsprintf()函数的实现见kernel/vsprintf.c

179 static int printf(const char *fmt, ...)

180 {

181         va_list args;

182         int i;

183

184         va_start(args, fmt);

185         write(1,printbuf,i=vsprintf(printbuf, fmt, args));

186         va_end(args);

187         return i;

188 }

189

 

    // main()中已经进行了系统初始化,包括内存管理、各种硬件设备和驱动程序。init()函数

    // 运行在任务01次创建的子进程(任务1)中。它首先对第一个将要执行的程序(shell

    // 的环境进行初始化,然后以登录shell方式加载该程序并执行之。

190 void init(void)

191 {

192         int pid,i;

193

    // setup() 是一个系统调用。用于读取硬盘参数包括分区表信息并加载虚拟盘(若存在的话)和

    // 安装根文件系统设备。该函数用25行上的宏定义,对应函数是sys_setup(),在块设备子目录

    // kernel/blk_drv/hd.c74行。

194         setup((void *) &drive_info);

 

    // 下面以读写访问方式打开设备“/dev/tty0”,它对应终端控制台。由于这是第一次打开文件

    // 操作,因此产生的文件句柄号(文件描述符)肯定是0。该句柄是UNIX类操作系统默认的控

    // 制台标准输入句柄stdin。这里再把它以读和写的方式分别打开是为了复制产生标准输出(写)

    // 句柄stdout和标准出错输出句柄stderr。函数前面的“(void)”前缀用于表示强制函数无需

    // 返回值。

195         (void) open("/dev/tty1",O_RDWR,0);

196         (void) dup(0);             // 复制句柄,产生句柄1--stdout标准输出设备。

197         (void) dup(0);             // 复制句柄,产生句柄2--stderr标准出错输出设备。

 

    // 下面打印缓冲区块数和总字节数,每块1024字节,以及主内存区空闲内存字节数。

198         printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS,

199                 NR_BUFFERS*BLOCK_SIZE);

200         printf("Free mem: %d bytes\n\r",memory_end-main_memory_start);

 

    // 下面fork()用于创建一个子进程(任务2)。对于被创建的子进程,fork()将返回0值,对于

    // 原进程(父进程)则返回子进程的进程号pid。所以第202--206行是子进程执行的内容。该子

    // 进程关闭了句柄0stdin)、以只读方式打开/etc/rc文件,并使用execve()函数将进程自身

    // 替换成 /bin/sh程序(即shell程序),然后执行 /bin/sh程序。所携带的参数和环境变量分

    // 别由argv_rcenvp_rc数组给出。关闭句柄0并立刻打开 /etc/rc文件的作用是把标准输入

    // stdin重定向到 /etc/rc文件。这样shell程序/bin/sh就可以运行rc文件中设置的命令。由

    // 于这里sh的运行方式是非交互式的,因此在执行完rc文件中的命令后就会立刻退出,进程2

    // 也随之结束。关于execve()函数说明请参见fs/exec.c程序,207行。

    // 函数_exit()退出时的出错码1 – 操作未许可;2 -- 文件或目录不存在。

201         if (!(pid=fork())) {

202                 close(0);

203                 if (open("/etc/rc",O_RDONLY,0))

204                         _exit(1);                // 若打开文件失败,则退出(lib/_exit.c,10)

205                 execve("/bin/sh",argv_rc,envp_rc);         // 替换成/bin/sh程序并执行。

206                 _exit(2);                                  // execve()执行失败则退出。

207         }

 

    // 下面还是父进程(1)执行的语句。wait()等待子进程停止或终止,返回值应是子进程的进程号

    // (pid)。这三句的作用是父进程等待子进程的结束。&i是存放返回状态信息的位置。如果wait()

    // 返回值不等于子进程号,则继续等待。

208         if (pid>0)

209                 while (pid != wait(&i))

210                         /* nothing */;       /* 空循环 */

 

    // 如果执行到这里,说明刚创建的子进程的执行已停止或终止了。下面循环中首先再创建一个子

    // 进程,如果出错,则显示“初始化程序创建子进程失败”信息并继续执行。对于所创建的子进

    // 程将关闭所有以前还遗留的句柄(stdin, stdout, stderr),新创建一个会话并设置进程组号,

    // 然后重新打开 /dev/tty0 作为 stdin,并复制成 stdoutstderr。再次执行系统解释程序

    // /bin/sh。但这次执行所选用的参数和环境数组另选了一套(见上面122--123行)。然后父进

    // 程再次运行 wait()等待。如果子进程又停止了执行,则在标准输出上显示出错信息“子进程

    // pid停止了运行,返回码是i”,然后继续重试下去,形成“大”死循环。

211         while (1) {

212                 if ((pid=fork())<0) {

213                         printf("Fork failed in init\r\n");

214                         continue;

215                 }

216                 if (!pid) {                           // 新的子进程。

217                         close(0);close(1);close(2);

218                         setsid();                     // 创建一新的会话期,见后面说明。

219                         (void) open("/dev/tty1",O_RDWR,0);

220                         (void) dup(0);

221                         (void) dup(0);

222                         _exit(execve("/bin/sh",argv,envp));

223                 }

224                 while (1)

225                         if (pid == wait(&i))

226                                 break;

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

228                 sync();                               // 同步操作,刷新缓冲区。

229         }

230         _exit(0);       /* NOTE! _exit, not exit() */  /*注意!是_exit(),非exit()*/

    // _exit()exit()都用于正常终止一个函数。但_exit()直接是一个sys_exit系统调用,而

    // exit()则通常是普通函数库中的一个函数。它会先执行一些清除操作,例如调用执行各终止

    // 处理程序、关闭所有标准IO等,然后调用sys_exit

231 }

232


 


 

第8章 内核核心程序

8.1 程序8-1 linux/kernel/asm.s


  1 /*

  2  *  linux/kernel/asm.s

  3  *

  4  *  (C) 1991  Linus Torvalds

  5  */

  6

  7 /*

  8  * asm.s contains the low-level code for most hardware faults.

  9  * page_exception is handled by the mm, so that isn't here. This

 10  * file also handles (hopefully) fpu-exceptions due to TS-bit, as

 11  * the fpu must be properly saved/resored. This hasn't been tested.

 12  */

    /*

     * asm.s程序中包括大部分的硬件故障(或出错)处理的底层次代码。页异常由内存管理程序

     * mm处理,所以不在这里。此程序还处理(希望是这样)由于TS-位而造成的fpu异常,因为

     * fpu必须正确地进行保存/恢复处理,这些还没有测试过。

     */

 13

    # 本代码文件主要涉及对Intel保留中断int0--int16的处理(int17-int31留作今后使用)。

    # 以下是一些全局函数名的声明,其原形在traps.c中说明。

 14 .globl _divide_error,_debug,_nmi,_int3,_overflow,_bounds,_invalid_op

 15 .globl _double_fault,_coprocessor_segment_overrun

 16 .globl _invalid_TSS,_segment_not_present,_stack_segment

 17 .globl _general_protection,_coprocessor_error,_irq13,_reserved

 18 .globl _alignment_check

 19

    # 下面这段程序处理无出错号的情况。

    # int0 -- 处理被零除出错的情况。  类型:错误;出错号:无。

    # 在执行DIVIDIV指令时,若除数是0CPU就会产生这个异常。当EAX(或AXAL)容纳

    # 不了一个合法除操作的结果时,也会产生这个异常。21行标号'_do_divide_error'实际上是

    # C语言函数do_divide_error()编译后所生成模块中对应的名称。函数'do_divide_error'

    # traps.c中实现(第101行开始)。

 20 _divide_error:

 21         pushl $_do_divide_error    # 首先把将要调用的函数地址入栈。

 22 no_error_code:                     # 这里是无出错号处理的入口处,见下面第56行等。

 23         xchgl %eax,(%esp)          # _do_divide_error的地址 è eaxeax被交换入栈。

 24         pushl %ebx

 25         pushl %ecx

 26         pushl %edx

 27         pushl %edi

 28         pushl %esi

 29         pushl %ebp

 30         push %ds                   # !!16位的段寄存器入栈后也要占用4个字节。

 31         push %es

 32         push %fs

 33         pushl $0                   # "error code"   # 将数值0作为出错码入栈。

 34         lea 44(%esp),%edx          # 取有效地址,即栈中原调用返回地址处的栈指针位置,

 35         pushl %edx                 # 并压入堆栈。

 36         movl $0x10,%edx            # 初始化段寄存器dsesfs,加载内核数据段选择符。

 37         mov %dx,%ds

 38         mov %dx,%es

 39         mov %dx,%fs

    # 下行上的'*'号表示调用操作数指定地址处的函数,称为间接调用。这句的含义是调用引起本次

    # 异常的C处理函数,例如do_divide_error()等。第41行是将堆栈指针加8相当于执行两次pop

    # 操作,弹出(丢弃)最后入堆栈的两个C函数参数(33行和35行入栈的值),让堆栈指针重新

    # 指向寄存器fs入栈处。

 40         call *%eax

 41         addl $8,%esp

 42         pop %fs

 43         pop %es

 44         pop %ds

 45         popl %ebp

 46         popl %esi

 47         popl %edi

 48         popl %edx

 49         popl %ecx

 50         popl %ebx

 51         popl %eax                  # 弹出原来eax中的内容。

 52         iret

 53

    # int1 -- debug调试中断入口点。处理过程同上。类型:错误/陷阱(Fault/Trap);无错误号。

    # eflagsTF标志置位时而引发的中断。当发现硬件断点(数据:陷阱,代码:错误);或者

    # 开启了指令跟踪陷阱或任务交换陷阱,或者调试寄存器访问无效(错误),CPU就会产生该异常。

 54 _debug:

 55         pushl $_do_int3         # _do_debug  # C函数指针入栈。以下同。

 56         jmp no_error_code

 57

    # int2 -- 非屏蔽中断调用入口点。  类型:陷阱;无错误号。

    # 这是仅有的被赋予固定中断向量的硬件中断。每当接收到一个NMI信号,CPU内部就会产生中断

    # 向量2,并执行标准中断应答周期,因此很节省时间。NMI通常保留为极为重要的硬件事件使用。

    # CPU收到一个 NMI 信号并且开始执行其中断处理过程时,随后所有的硬件中断都将被忽略。

 58 _nmi:

 59         pushl $_do_nmi

 60         jmp no_error_code

 61

    # int3 -- 断点指令引起中断的入口点。  类型:陷阱;无错误号。

    # int 3 指令引发的中断,与硬件中断无关。该指令通常由调式器插入被调式程序的代码中。

    # 处理过程同_debug

 62 _int3:

 63         pushl $_do_int3

 64         jmp no_error_code

 65

    # int4 -- 溢出出错处理中断入口点。  类型:陷阱;无错误号。

    # EFLAGSOF标志置位时CPU执行INTO指令就会引发该中断。通常用于编译器跟踪算术计算溢出。

 66 _overflow:

 67         pushl $_do_overflow

 68         jmp no_error_code

 69

    # int5 -- 边界检查出错中断入口点。  类型:错误;无错误号。

    # 当操作数在有效范围以外时引发的中断。当BOUND指令测试失败就会产生该中断。BOUND指令有

    # 3个操作数,如果第1个不在另外两个之间,就产生异常5

 70 _bounds:

 71         pushl $_do_bounds

 72         jmp no_error_code

 73

    # int6 -- 无效操作指令出错中断入口点。  类型:错误;无错误号。

    # CPU 执行机构检测到一个无效的操作码而引起的中断。

 74 _invalid_op:

 75         pushl $_do_invalid_op

 76         jmp no_error_code

 77

    # int9 -- 协处理器段超出出错中断入口点。  类型:放弃;无错误号。

    # 该异常基本上等同于协处理器出错保护。因为在浮点指令操作数太大时,我们就有这个机会来

    # 加载或保存超出数据段的浮点值。

 78 _coprocessor_segment_overrun:

 79         pushl $_do_coprocessor_segment_overrun

 80         jmp no_error_code

 81

    # int15 – 其他Intel保留中断的入口点。

 82 _reserved:

 83         pushl $_do_reserved

 84         jmp no_error_code

 85

    # int45 -- (0x20 + 13) Linux设置的数学协处理器硬件中断。

    # 当协处理器执行完一个操作时就会发出IRQ13中断信号,以通知CPU操作完成。80387在执行

    # 计算时,CPU会等待其操作完成。下面89行上0xF0是协处理端口,用于清忙锁存器。通过写

    # 该端口,本中断将消除CPUBUSY延续信号,并重新激活80387的处理器扩展请求引脚PEREQ

    # 该操作主要是为了确保在继续执行80387的任何指令之前,CPU响应本中断。

 86 _irq13:

 87         pushl %eax

 88         xorb %al,%al

 89         outb %al,$0xF0

 90         movb $0x20,%al

 91         outb %al,$0x20             # 8259主中断控制芯片发送EOI(中断结束)信号。

 92         jmp 1f                     # 这两个跳转指令起延时作用。

 93 1:      jmp 1f

 94 1:      outb %al,$0xA0             # 再向8259从中断控制芯片发送EOI(中断结束)信号。

 95         popl %eax

 96         jmp _coprocessor_error     # 该函数原在本程序中,现已放到system_call.s中。

 97

    # 以下中断在调用时CPU会在中断返回地址之后将出错号压入堆栈,因此返回时也需要将出错号

    # 弹出(参见图5.3(b))。

 

    # int8 -- 双出错故障。  类型:放弃;有错误码。

    # 通常当CPU在调用前一个异常的处理程序而又检测到一个新的异常时,这两个异常会被串行地进行

    # 处理,但也会碰到很少的情况,CPU不能进行这样的串行处理操作,此时就会引发该中断。

 98 _double_fault:

 99         pushl $_do_double_fault    # C函数地址入栈。

100 error_code:

101         xchgl %eax,4(%esp)         # error code <-> %eaxeax原来的值被保存在堆栈上。

102         xchgl %ebx,(%esp)          # &function <-> %ebxebx原来的值被保存在堆栈上。

103         pushl %ecx

104         pushl %edx

105         pushl %edi

106         pushl %esi

107         pushl %ebp

108         push %ds

109         push %es

110         push %fs

111         pushl %eax                 # error code   # 出错号入栈。

112         lea 44(%esp),%eax          # offset       # 程序返回地址处堆栈指针位置值入栈。

113         pushl %eax

114         movl $0x10,%eax            # 置内核数据段选择符。

115         mov %ax,%ds

116         mov %ax,%es

117         mov %ax,%fs

118         call *%ebx                 # 间接调用,调用相应的C函数,其参数已入栈。

119         addl $8,%esp               # 丢弃入栈的2个用作C函数的参数。

120         pop %fs

121         pop %es

122         pop %ds

123         popl %ebp

124         popl %esi

125         popl %edi

126         popl %edx

127         popl %ecx

128         popl %ebx

129         popl %eax

130         iret

131

    # int10 -- 无效的任务状态段(TSS)  类型:错误;有出错码。

    # CPU企图切换到一个进程,而该进程的TSS无效。根据TSS中哪一部分引起了异常,当由于TSS

    # 长度超过104字节时,这个异常在当前任务中产生,因而切换被终止。其他问题则会导致在切换

    # 后的新任务中产生本异常。

132 _invalid_TSS:

133         pushl $_do_invalid_TSS

134         jmp error_code

135

    # int11 -- 段不存在。  类型:错误;有出错码。

    # 被引用的段不在内存中。段描述符中标志指明段不在内存中。

136 _segment_not_present:

137         pushl $_do_segment_not_present

138         jmp error_code

139

    # int12 -- 堆栈段错误。  类型:错误;有出错码。

    # 指令操作试图超出堆栈段范围,或者堆栈段不在内存中。这是异常1113的特例。有些操作

    # 系统可以利用这个异常来确定什么时候应该为程序分配更多的栈空间。

140 _stack_segment:

141         pushl $_do_stack_segment

142         jmp error_code

143

    # int13 -- 一般保护性出错。  类型:错误;有出错码。

    # 表明是不属于任何其他类的错误。若一个异常产生时没有对应的处理向量(0--16),通常就

    # 会归到此类。

144 _general_protection:

145         pushl $_do_general_protection

146         jmp error_code

147

    # int17 -- 边界对齐检查出错。

    # 在启用了内存边界检查时,若特权级3(用户级)数据非边界对齐时会产生该异常。

148 _alignment_check:

149         pushl $_do_alignment_check

150         jmp error_code

151

    # int7 -- 设备不存在(_device_not_available)在kernel/sys_call.s158行。

    # int14 -- 页错误(_page_fault)在mm/page.s14行。

    # int16 -- 协处理器错误(_coprocessor_error)在kernel/sys_call.s140行。

    # 时钟中断int 0x20_timer_interrupt)在kernel/sys_call.s189行。

    # 系统调用int 0x80_system_call)在kernel/sys_call.s84行。


 


 

8.2 程序8-2 linux/kernel/traps.c


  1 /*

  2  *  linux/kernel/traps.c

  3  *

  4  *  (C) 1991  Linus Torvalds

  5  */

  6

  7 /*

  8  * 'Traps.c' handles hardware traps and faults after we have saved some

  9  * state in 'asm.s'. Currently mostly a debugging-aid, will be extended

 10  * to mainly kill the offending process (probably by giving it a signal,

 11  * but possibly by killing it outright if necessary).

 12  */

    /*

     * 在程序asm.s中保存了一些状态后,本程序用来处理硬件陷阱和故障。目前主要用于调试目的,

     * 以后将扩展用来杀死遭损坏的进程(主要是通过发送一个信号,但如果必要也会直接杀死)。

     */

 13 #include <string.h>       // 字符串头文件。主要定义了一些有关内存或字符串操作的嵌入函数。

 14

 15 #include <linux/head.h>   // head头文件,定义了段描述符的简单结构,和几个选择符常量。

 16 #include <linux/sched.h>  // 调度程序头文件,定义了任务结构task_struct、初始任务0的数据,

                              // 还有一些有关描述符参数设置和获取的嵌入式汇编函数宏语句。

 17 #include <linux/kernel.h> // 内核头文件。含有一些内核常用函数的原形定义。

 18 #include <asm/system.h>   // 系统头文件。定义了设置或修改描述符/中断门等的嵌入式汇编宏。

 19 #include <asm/segment.h>  // 段操作头文件。定义了有关段寄存器操作的嵌入式汇编函数。

 20 #include <asm/io.h>       // 输入/输出头文件。定义硬件端口输入/输出宏汇编语句。

 21

    // 以下语句定义了三个嵌入式汇编宏语句函数。有关嵌入式汇编的基本语法见本程序列表后的说明。

    // 用圆括号括住的组合语句(花括号中的语句)可以作为表达式使用,其中最后的__res是其输出值。

    // 23行定义了一个寄存器变量__res。该变量将被保存在一个寄存器中,以便于快速访问和操作。

    // 如果想指定寄存器(例如eax),那么我们可以把该句写成“register char __res asm("ax");”。

    // 取段seg中地址addr处的一个字节。

    // 参数:seg - 段选择符;addr - 段内指定地址。

    // 输出:%0 - eax (__res);输入:%1 - eax (seg)%2 - 内存地址 (*(addr))

 22 #define get_seg_byte(seg,addr) ({ \

 23 register char __res; \

 24 __asm__("push %%fs;mov %%ax,%%fs;movb %%fs:%2,%%al;pop %%fs" \

 25         :"=a" (__res):"0" (seg),"m" (*(addr))); \

 26 __res;})

 27

    // 取段seg中地址addr处的一个长字(4字节)。

    // 参数:seg - 段选择符;addr - 段内指定地址。

    // 输出:%0 - eax (__res);输入:%1 - eax (seg)%2 - 内存地址 (*(addr))

 28 #define get_seg_long(seg,addr) ({ \

 29 register unsigned long __res; \

 30 __asm__("push %%fs;mov %%ax,%%fs;movl %%fs:%2,%%eax;pop %%fs" \

 31         :"=a" (__res):"0" (seg),"m" (*(addr))); \

 32 __res;})

 33

    // fs段寄存器的值(选择符)。

    // 输出:%0 - eax (__res)

 34 #define _fs() ({ \

 35 register unsigned short __res; \

 36 __asm__("mov %%fs,%%ax":"=a" (__res):); \

 37 __res;})

 38

    // 以下定义了一些函数原型。

 39 void page_exception(void);                   // 页异常。实际是page_faultmm/page.s14)。

 40

 41 void divide_error(void);                     // int0kernel/asm.s20)。

 42 void debug(void);                            // int1kernel/asm.s54)。

 43 void nmi(void);                              // int2kernel/asm.s58)。

 44 void int3(void);                             // int3kernel/asm.s62)。

 45 void overflow(void);                         // int4kernel/asm.s66)。

 46 void bounds(void);                           // int5kernel/asm.s70)。

 47 void invalid_op(void);                       // int6kernel/asm.s74)。

 48 void device_not_available(void);             // int7kernel/sys_call.s158)。

 49 void double_fault(void);                     // int8kernel/asm.s98)。

 50 void coprocessor_segment_overrun(void);      // int9kernel/asm.s78)。

 51 void invalid_TSS(void);                      // int10kernel/asm.s132)。

 52 void segment_not_present(void);              // int11kernel/asm.s136)。

 53 void stack_segment(void);                    // int12kernel/asm.s140)。

 54 void general_protection(void);               // int13kernel/asm.s144)。

 55 void page_fault(void);                       // int14mm/page.s14)。

 56 void coprocessor_error(void);                // int16kernel/sys_call.s140)。

 57 void reserved(void);                         // int15kernel/asm.s82)。

 58 void parallel_interrupt(void);               // int39kernel/sys_call.s295)。

 59 void irq13(void);                            // int45 协处理器中断处理(kernel/asm.s86)。

 60 void alignment_check(void);                  // int46kernel/asm.s148)。

 61

    // 该子程序用来打印出错中断的名称、出错号、调用程序的EIPEFLAGSESPfs段寄存器值、

    // 段的基址、段的长度、进程号pid、任务号、10字节指令码。如果堆栈在用户数据段,则还

    // 打印16字节的堆栈内容。这些信息可用于程序调试。

 62 static void die(char * str,long esp_ptr,long nr)

 63 {

 64         long * esp = (long *) esp_ptr;

 65         int i;

 66

 67         printk("%s: %04x\n\r",str,nr&0xffff);

    // 下行打印语句显示当前调用进程的CS:EIPEFLAGSSS:ESP的值。参照错误!未找到引用源。可知,这里esp[0]

    // 即为图中的esp0位置。因此我们把这句拆分开来看为:

    // (1) EIP:\t%04x:%p\n  -- esp[1]是段选择符(cs),esp[0]eip

    // (2) EFLAGS:\t%p      -- esp[2]eflags

    // (3) ESP:\t%04x:%p\n  -- esp[4]是原ssesp[3]是原esp

 68         printk("EIP:\t%04x:%p\nEFLAGS:\t%p\nESP:\t%04x:%p\n",

 69                 esp[1],esp[0],esp[2],esp[4],esp[3]);

 70         printk("fs: %04x\n",_fs());

 71         printk("base: %p, limit: %p\n",get_base(current->ldt[1]),get_limit(0x17));

 72         if (esp[4] == 0x17) {             // 若原ss值为0x17(用户栈),则还打印出

 73                 printk("Stack: ");        // 用户栈中的4个长字值(16字节)。

 74                 for (i=0;i<4;i++)

 75                         printk("%p ",get_seg_long(0x17,i+(long *)esp[3]));

 76                 printk("\n");

 77         }

 78         str(i);                 // 取当前运行任务的任务号(include/linux/sched.h210行)。

 79         printk("Pid: %d, process nr: %d\n\r",current->pid,0xffff & i); // 进程号,任务号。

 80         for(i=0;i<10;i++)

 81                 printk("%02x ",0xff & get_seg_byte(esp[1],(i+(char *)esp[0])));

 82         printk("\n\r");

 83         do_exit(11);            /* play segment exception */

 84 }

 85

    // 以下这些以do_开头的函数是asm.s中对应中断处理程序调用的C函数。

 86 void do_double_fault(long esp, long error_code)

 87 {

 88         die("double fault",esp,error_code);

 89 }

 90

 91 void do_general_protection(long esp, long error_code)

 92 {

 93         die("general protection",esp,error_code);

 94 }

 95

 96 void do_alignment_check(long esp, long error_code)

 97 {

 98     die("alignment check",esp,error_code);

 99 }

100

101 void do_divide_error(long esp, long error_code)

102 {

103         die("divide error",esp,error_code);

104 }

105

    // 参数是进入中断后被顺序压入堆栈的寄存器值。参见asm.s程序第24--35行。

106 void do_int3(long * esp, long error_code,

107                 long fs,long es,long ds,

108                 long ebp,long esi,long edi,

109                 long edx,long ecx,long ebx,long eax)

110 {

111         int tr;

112

113         __asm__("str %%ax":"=a" (tr):"" (0));               // 取任务寄存器值ètr

114         printk("eax\t\tebx\t\tecx\t\tedx\n\r%8x\t%8x\t%8x\t%8x\n\r",

115                 eax,ebx,ecx,edx);

116         printk("esi\t\tedi\t\tebp\t\tesp\n\r%8x\t%8x\t%8x\t%8x\n\r",

117                 esi,edi,ebp,(long) esp);

118         printk("\n\rds\tes\tfs\ttr\n\r%4x\t%4x\t%4x\t%4x\n\r",

119                 ds,es,fs,tr);

120         printk("EIP: %8x   CS: %4x  EFLAGS: %8x\n\r",esp[0],esp[1],esp[2]);

121 }

122

123 void do_nmi(long esp, long error_code)

124 {

125         die("nmi",esp,error_code);

126 }

127

128 void do_debug(long esp, long error_code)

129 {

130         die("debug",esp,error_code);

131 }

132

133 void do_overflow(long esp, long error_code)

134 {

135         die("overflow",esp,error_code);

136 }

137

138 void do_bounds(long esp, long error_code)

139 {

140         die("bounds",esp,error_code);

141 }

142

143 void do_invalid_op(long esp, long error_code)

144 {

145         die("invalid operand",esp,error_code);

146 }

147

148 void do_device_not_available(long esp, long error_code)

149 {

150         die("device not available",esp,error_code);

151 }

152

153 void do_coprocessor_segment_overrun(long esp, long error_code)

154 {

155         die("coprocessor segment overrun",esp,error_code);

156 }

157

158 void do_invalid_TSS(long esp,long error_code)

159 {

160         die("invalid TSS",esp,error_code);

161 }

162

163 void do_segment_not_present(long esp,long error_code)

164 {

165         die("segment not present",esp,error_code);

166 }

167

168 void do_stack_segment(long esp,long error_code)

169 {

170         die("stack segment",esp,error_code);

171 }

172

173 void do_coprocessor_error(long esp, long error_code)

174 {

175         if (last_task_used_math != current)

176                 return;

177         die("coprocessor error",esp,error_code);

178 }

179

180 void do_reserved(long esp, long error_code)

181 {

182         die("reserved (15,17-47) error",esp,error_code);

183 }

184

    // 下面是异常(陷阱)中断程序初始化子程序。设置它们的中断调用门(中断向量)。

    // set_trap_gate()set_system_gate()都使用了中断描述符表IDT中的陷阱门(Trap Gate),

    // 它们之间的主要区别在于前者设置的特权级为0,后者是3。因此断点陷阱中断int3、溢出中断

    // overflow 和边界出错中断 bounds 可以由任何程序调用。 这两个函数均是嵌入式汇编宏程序,

    // 参见include/asm/system.h,第36行、39行。

185 void trap_init(void)

186 {

187         int i;

188

189         set_trap_gate(0,&divide_error);     // 设置除操作出错的中断向量值。以下雷同。

190         set_trap_gate(1,&debug);

191         set_trap_gate(2,&nmi);

192         set_system_gate(3,&int3);           /* int3-5 can be called from all */

193         set_system_gate(4,&overflow);       /* int3-5 可以被所有程序执行 */

194         set_system_gate(5,&bounds);

195         set_trap_gate(6,&invalid_op);

196         set_trap_gate(7,&device_not_available);

197         set_trap_gate(8,&double_fault);

198         set_trap_gate(9,&coprocessor_segment_overrun);

199         set_trap_gate(10,&invalid_TSS);

200         set_trap_gate(11,&segment_not_present);

201         set_trap_gate(12,&stack_segment);

202         set_trap_gate(13,&general_protection);

203         set_trap_gate(14,&page_fault);

204         set_trap_gate(15,&reserved);

205         set_trap_gate(16,&coprocessor_error);

206         set_trap_gate(17,&alignment_check);

 

    // 下面把int17-47的陷阱门先均设置为reserved,以后各硬件初始化时会重新设置自己的陷阱门。

207         for (i=18;i<48;i++)

208                 set_trap_gate(i,&reserved);

 

    // 设置协处理器中断0x2d45)陷阱门描述符,并允许其产生中断请求。设置并行口中断描述符。

209         set_trap_gate(45,&irq13);

210         outb_p(inb_p(0x21)&0xfb,0x21);          // 允许8259A主芯片的IRQ2中断请求。

211         outb(inb_p(0xA1)&0xdf,0xA1);            // 允许8259A从芯片的IRQ13中断请求。

212         set_trap_gate(39,&parallel_interrupt);  // 设置并行口1的中断0x27陷阱门描述符。

213 }

214


 


 

8.3 程序8-3 linux/kernel/sys_call.s


  1 /*

  2  *  linux/kernel/system_call.s

  3  *

  4  *  (C) 1991  Linus Torvalds

  5  */

  6

  7 /*

  8  *  system_call.s  contains the system-call low-level handling routines.

  9  * This also contains the timer-interrupt handler, as some of the code is

 10  * the same. The hd- and flopppy-interrupts are also here.

 11  *

 12  * NOTE: This code handles signal-recognition, which happens every time

 13  * after a timer-interrupt and after each system call. Ordinary interrupts

 14  * don't handle signal-recognition, as that would clutter them up totally

 15  * unnecessarily.

 16  *

 17  * Stack layout in 'ret_from_system_call':

 18  *

 19  *       0(%esp) - %eax

 20  *       4(%esp) - %ebx

 21  *       8(%esp) - %ecx

 22  *       C(%esp) - %edx

 23  *      10(%esp) - original %eax        (-1 if not system call)

 24  *      14(%esp) - %fs

 25  *      18(%esp) - %es

 26  *      1C(%esp) - %ds

 27  *      20(%esp) - %eip

 28  *      24(%esp) - %cs

 29  *      28(%esp) - %eflags

 30  *      2C(%esp) - %oldesp

 31  *      30(%esp) - %oldss

 32  */

    /*

     * system_call.s文件包含系统调用(system-call)底层处理子程序。由于有些代码比较类似,

     * 所以同时也包括时钟中断处理(timer-interrupt)句柄。硬盘和软盘的中断处理程序也在这里。

     *

     * 注意:这段代码处理信号(signal)识别,在每次时钟中断和系统调用之后都会进行识别。一般

     * 中断过程并不处理信号识别,因为会给系统造成混乱。

     *

     * 从系统调用返回('ret_from_system_call')时堆栈的内容见上面19-30行。

     */

    # 上面Linus原注释中一般中断过程是指除了系统调用中断(int 0x80)和时钟中断(int 0x20

    # 以外的其他中断。这些中断会在内核态或用户态随机发生,若在这些中断过程中也处理信号识别

    # 的话,就有可能与系统调用中断和时钟中断过程中对信号的识别处理过程相冲突,,违反了内核

    # 代码非抢占原则。因此系统既无必要在这些“其他”中断中处理信号,也不允许这样做。

 33

 34 SIG_CHLD        = 17               # 定义SIG_CHLD信号(子进程停止或结束)。

 35

 36 EAX             = 0x00             # 堆栈中各个寄存器的偏移位置。

 37 EBX             = 0x04

 38 ECX             = 0x08

 39 EDX             = 0x0C

 40 ORIG_EAX        = 0x10             # 如果不是系统调用(是其它中断)时,该值为-1

 41 FS              = 0x14

 42 ES              = 0x18

 43 DS              = 0x1C

 44 EIP             = 0x20             # 44 -- 48行 由CPU自动入栈。

 45 CS              = 0x24

 46 EFLAGS          = 0x28

 47 OLDESP          = 0x2C             # 当特权级变化时,原堆栈指针也会入栈。

 48 OLDSS           = 0x30

 49

    # 以下这些是任务结构(task_struct)中变量的偏移值,参见include/linux/sched.h105行开始。

 50 state   = 0             # these are offsets into the task-struct.  # 进程状态码。

 51 counter = 4             # 任务运行时间计数(递减)(滴答数),运行时间片。

 52 priority = 8            # 运行优先数。任务开始运行时counter=priority,越大则运行时间越长。

 53 signal  = 12            # 是信号位图,每个比特位代表一种信号,信号值=位偏移值+1

 54 sigaction = 16          # MUST be 16 (=len of sigaction) # sigaction结构长度必须是16字节。

 55 blocked = (33*16)       # 受阻塞信号位图的偏移量。

 56

    # 以下定义在sigaction结构中的偏移量,参见include/signal.h,第55行开始。

 57 # offsets within sigaction

 58 sa_handler = 0                     # 信号处理过程的句柄(描述符)。

 59 sa_mask = 4                        # 信号屏蔽码。

 60 sa_flags = 8                       # 信号集。

 61 sa_restorer = 12                   # 恢复函数指针,参见kernel/signal.c程序说明。

 62

 63 nr_system_calls = 82               # Linux 0.12版内核中的系统调用总数。

 64

 65 ENOSYS = 38                        # 系统调用号出错码。

 66

 67 /*

 68  * Ok, I get parallel printer interrupts while using the floppy for some

 69  * strange reason. Urgel. Now I just ignore them.

 70  */

    /* 好了,在使用软驱时我收到了并行打印机中断,很奇怪。呵,现在不管它。

     */

 71 .globl _system_call,_sys_fork,_timer_interrupt,_sys_execve

 72 .globl _hd_interrupt,_floppy_interrupt,_parallel_interrupt

 73 .globl _device_not_available, _coprocessor_error

 74

    # 系统调用号错误时将返回出错码-ENOSYS

 75 .align 2                           # 内存4字节对齐。

 76 bad_sys_call:

 77         pushl $-ENOSYS             # eax中置-ENOSYS

 78         jmp ret_from_sys_call

 

    # 重新执行调度程序入口。调度程序schedule()在(kernel/sched.c119行处开始。

    # 当调度程序schedule()返回时就从ret_from_sys_call处(107行)继续执行。

 79 .align 2

 80 reschedule:

 81         pushl $ret_from_sys_call   # ret_from_sys_call的地址入栈(107行)。

 82         jmp _schedule

 

    #### int 0x80 --linux系统调用入口点(调用中断int 0x80eax中是调用号)。

 83 .align 2

 84 _system_call:

 85         push %ds                   # 保存原段寄存器值。

 86         push %es

 87         push %fs

 88         pushl %eax                 # save the orig_eax   # 保存eax原值。

 

    # 一个系统调用最多可带有3个参数,也可以不带参数。下面入栈的ebxecxedx中放着系统

    # 调用相应C语言函数(见第99行)的调用参数。这几个寄存器入栈的顺序是由GNU gcc规定的,

    # ebx中可存放第1个参数,ecx中存放第2个参数,edx中存放第3个参数。

    # 系统调用语句可参见头文件include/unistd.h中第150200行的系统调用宏。

 89         pushl %edx             

 90         pushl %ecx                 # push %ebx,%ecx,%edx as parameters

 91         pushl %ebx                 # to the system call

 

    # 在保存过段寄存器之后,让ds,es指向内核数据段,而fs指向当前局部数据段,即指向执行本

    # 次系统调用的用户程序的数据段。注意,在Linux 0.12中内核给任务分配的代码和数据内存段

    # 是重叠的,它们的段基址和段限长相同。参见fork.c程序中copy_mem()函数。

 92         movl $0x10,%edx            # set up ds,es to kernel space

 93         mov %dx,%ds

 94         mov %dx,%es

 95         movl $0x17,%edx            # fs points to local data space

 96         mov %dx,%fs

 

 97         cmpl _NR_syscalls,%eax     # 调用号如果超出范围的话就跳转。

 98         jae bad_sys_call

 

    # 下面这句操作数的含义是:调用地址=[_sys_call_table + %eax * 4]。参见程序后的说明。

    # sys_call_table[]是一个指针数组,定义在include/linux/sys.h中,该数组中设置了内核

    # 所有82个系统调用C处理函数的地址。

 99         call _sys_call_table(,%eax,4)   # 间接调用指定功能C函数。

100         pushl %eax                      # 把系统调用返回值入栈。

 

    # 下面101-106行查看当前任务的运行状态。如果不在就绪状态(state不等于0)就去执行调度

    # 程序。如果该任务在就绪状态,但是其时间片已经用完(counter=0),则也去执行调度程序。

    # 例如当后台进程组中的进程执行控制终端读写操作时,那么默认条件下该后台进程组所有进程

    # 会收到SIGTTINSIGTTOU信号,导致进程组中所有进程处于停止状态。而当前进程则会立刻

    # 返回。

101 2:

102         movl _current,%eax         # 取当前任务(进程)数据结构指针èeax

103         cmpl $0,state(%eax)        # state

104         jne reschedule

105         cmpl $0,counter(%eax)      # counter

106         je reschedule

 

    # 以下这段代码执行从系统调用C函数返回后,对信号进行识别处理。其他中断服务程序退出时也

    # 将跳转到这里进行处理后才退出中断过程,例如后面131行上的处理器出错中断int 16

    # 首先判别当前任务是否是初始任务task0,如果是则不必对其进行信号量方面的处理,直接返回。

    # 109行上的_task对应C程序中的task[]数组,直接引用task相当于引用task[0]

107 ret_from_sys_call:

108         movl _current,%eax

109         cmpl _task,%eax            # task[0] cannot have signals

110         je 3f                      # 向前(forward)跳转到标号3处退出中断处理。

 

    # 通过对原调用程序代码选择符的检查来判断调用程序是否是用户任务。如果不是则直接退出中断。

    # 这是因为任务在内核态执行时不可抢占。否则对任务进行信号量的识别处理。这里比较选择符是

    # 否为用户代码段的选择符 0x000fRPL=3,局部表,代码段)来判断是否为用户任务。如果不是

    # 则说明是某个中断服务程序(例如中断16)跳转到第107行执行到此,于是跳转退出中断程序。

    # 另外,如果原堆栈段选择符不为0x17(即原堆栈不在用户段中),也说明本次系统调用的调用者

    # 不是用户任务,则也退出。

111         cmpw $0x0f,CS(%esp)        # was old code segment supervisor ?

112         jne 3f

113         cmpw $0x17,OLDSS(%esp)     # was stack segment = 0x17 ?

114         jne 3f

 

    # 下面这段代码(115-128)用于处理当前任务中的信号。首先取当前任务结构中的信号位图(32位,

    # 每位代表1种信号),然后用任务结构中的信号阻塞(屏蔽)码,阻塞不允许的信号位,取得数值

    # 最小的信号值,再把原信号位图中该信号对应的位复位(置0),最后将该信号值作为参数之一调

    # do_signal()do_signal()在(kernel/signal.c,128)中,其参数包括13个入栈的信息。

    # do_signal()或信号处理函数返回之后,若返回值不为0则再看看是否需要切换进程或继续处理

    # 其它信号。

115         movl signal(%eax),%ebx     # 取信号位图èebx,每1位代表1种信号,共32个信号。

116         movl blocked(%eax),%ecx    # 取阻塞(屏蔽)信号位图èecx

117         notl %ecx                  # 每位取反。

118         andl %ebx,%ecx             # 获得许可的信号位图。

119         bsfl %ecx,%ecx             # 从低位(位0)开始扫描位图,看是否有1的位,

                                       # 若有,则ecx保留该位的偏移值(即第几位0--31)。

120         je 3f                      # 如果没有信号则向前跳转退出。

121         btrl %ecx,%ebx             # 复位该信号(ebx含有原signal位图)。

122         movl %ebx,signal(%eax)     # 重新保存signal位图信息ècurrent->signal

123         incl %ecx                  # 将信号调整为从1开始的数(1--32)。

124         pushl %ecx                 # 信号值入栈作为调用do_signal的参数之一。

125         call _do_signal            # 调用C函数信号处理程序(kernel/signal.c128)。

126         popl %ecx                  # 弹出入栈的信号值。

127         testl %eax, %eax           # 测试返回值,若不为0则跳转到前面标号2101行)处。

128         jne 2b                     # see if we need to switch tasks, or do more signals

 

129 3:      popl %eax                  # eax中含有第100行入栈的系统调用返回值。

130         popl %ebx

131         popl %ecx

132         popl %edx

133         addl $4, %esp              # skip orig_eax    # 跳过(丢弃)原eax值。

134         pop %fs

135         pop %es

136         pop %ds

137         iret

138

    #### int16 -- 处理器错误中断。  类型:错误;无错误码。

    # 这是一个外部的基于硬件的异常。当协处理器检测到自己发生错误时,就会通过ERROR引脚

    # 通知CPU。下面代码用于处理协处理器发出的出错信号。并跳转去执行C函数math_error()

    # kernel/math/error.c 11)。返回后将跳转到标号ret_from_sys_call处继续执行。

139 .align 2

140 _coprocessor_error:

141         push %ds

142         push %es

143         push %fs

144         pushl $-1                   # fill in -1 for orig_eax  # -1,表明不是系统调用。

145         pushl %edx

146         pushl %ecx

147         pushl %ebx

148         pushl %eax

149         movl $0x10,%eax            # ds,es置为指向内核数据段。

150         mov %ax,%ds

151         mov %ax,%es

152         movl $0x17,%eax            # fs置为指向局部数据段(出错程序的数据段)。

153         mov %ax,%fs

154         pushl $ret_from_sys_call   # 把下面调用返回的地址入栈。

155         jmp _math_error            # 执行math_error()kernel/math/error.c11)。

156

    #### int7 -- 设备不存在或协处理器不存在。  类型:错误;无错误码。

    # 如果控制寄存器 CR0 EM(模拟)标志置位,则当CPU 执行一个协处理器指令时就会引发该

    # 中断,这样CPU就可以有机会让这个中断处理程序模拟协处理器指令(181行)。

    # CR0的交换标志TS是在 CPU执行任务转换时设置的。TS 可以用来确定什么时候协处理器中的

    # 内容与CPU 正在执行的任务不匹配了。当CPU 在运行一个协处理器转义指令时发现TS置位时,

    # 就会引发该中断。此时就可以保存前一个任务的协处理器内容,并恢复新任务的协处理器执行

    # 状态(176行)。参见kernel/sched.c92行。该中断最后将转移到标号ret_from_sys_call

    # 处执行下去(检测并处理信号)。

157 .align 2

158 _device_not_available:

159         push %ds

160         push %es

161         push %fs

162         pushl $-1                  # fill in -1 for orig_eax  # -1,表明不是系统调用。

163         pushl %edx

164         pushl %ecx

165         pushl %ebx

166         pushl %eax

167         movl $0x10,%eax            # ds,es置为指向内核数据段。

168         mov %ax,%ds

169         mov %ax,%es

170         movl $0x17,%eax            # fs置为指向局部数据段(出错程序的数据段)。

171         mov %ax,%fs

    # CR0中任务已交换标志TS,并取CR0值。若其中协处理器仿真标志EM没有置位,说明不是

    # EM引起的中断,则恢复任务协处理器状态,执行C函数 math_state_restore(),并在返回时

    # 去执行ret_from_sys_call处的代码。

172         pushl $ret_from_sys_call   # 把下面跳转或调用的返回地址入栈。

173         clts                       # clear TS so that we can use math

174         movl %cr0,%eax

175         testl $0x4,%eax            # EM (math emulation bit)

176         je _math_state_restore     # 执行math_state_restore()kernel/sched.c92行)。

 

    # EM标志置位,则去执行数学仿真程序math_emulate()

177         pushl %ebp

178         pushl %esi

179         pushl %edi

180         pushl $0                   # temporary storage for ORIG_EIP

181         call _math_emulate         # 调用C函数(math/math_emulate.c476行)。

182         addl $4,%esp               # 丢弃临时存储。

183         popl %edi

184         popl %esi

185         popl %ebp

186         ret                        # 这里的ret将跳转到ret_from_sys_call(107)

187

    #### int32 -- (int 0x20) 时钟中断处理程序。中断频率设置为100Hz(include/linux/sched.h,4)

    # 定时芯片8253/8254是在(kernel/sched.c,438)处初始化的。因此这里jiffies10毫秒加1

    # 这段代码将jiffies1,发送结束中断指令给8259控制器,然后用当前特权级作为参数调用

    # C函数do_timer(long CPL)。当调用返回时转去检测并处理信号。

188 .align 2

189 _timer_interrupt:

190         push %ds                   # save ds,es and put kernel data space

191         push %es                   # into them. %fs is used by _system_call

192         push %fs                   # 保存dses并让其指向内核数据段。fs将用于system_call

193         pushl $-1                  # fill in -1 for orig_eax  # -1,表明不是系统调用。

 

    # 下面我们保存寄存器eaxecxedx。这是因为gcc编译器在调用函数时不会保存它们。这里也

    # 保存了ebx寄存器,因为在后面ret_from_sys_call中会用到它。

194         pushl %edx                 # we save %eax,%ecx,%edx as gcc doesn't

195         pushl %ecx                 # save those across function calls. %ebx

196         pushl %ebx                 # is saved as we use that in ret_sys_call

197         pushl %eax

198         movl $0x10,%eax            # ds,es置为指向内核数据段。

199         mov %ax,%ds

200         mov %ax,%es

201         movl $0x17,%eax            # fs置为指向局部数据段(程序的数据段)。

202         mov %ax,%fs

203         incl _jiffies

    # 由于初始化中断控制芯片时没有采用自动EOI,所以这里需要发指令结束该硬件中断。

204         movb $0x20,%al             # EOI to interrupt controller #1

205         outb %al,$0x20

 

    # 下面从堆栈中取出执行系统调用代码的选择符(CS段寄存器值)中的当前特权级别(03)并压入

    # 堆栈,作为do_timer的参数。do_timer()函数执行任务切换、计时等工作,在kernel/sched.c

    # 324行实现。

206         movl CS(%esp),%eax

207         andl $3,%eax               # %eax is CPL (0 or 3, 0=supervisor)

208         pushl %eax

209         call _do_timer             # 'do_timer(long CPL)' does everything from

210         addl $4,%esp               # task switching to accounting ...

211         jmp ret_from_sys_call

212

    #### 这是sys_execve()系统调用。取中断调用程序的代码指针作为参数调用C函数do_execve()

    # do_execve()fs/exec.c207行。

213 .align 2

214 _sys_execve:

215         lea EIP(%esp),%eax         # eax指向堆栈中保存用户程序eip指针处。

216         pushl %eax

217         call _do_execve

218         addl $4,%esp               # 丢弃调用时压入栈的EIP值。

219         ret

220

    #### sys_fork()调用,用于创建子进程,是system_call功能2。原形在include/linux/sys.h中。

    # 首先调用C函数find_empty_process(),取得一个进程号last_pid。若返回负数则说明目前任务

    # 数组已满。然后调用copy_process()复制进程。

221 .align 2

222 _sys_fork:

223         call _find_empty_process   # 为新进程取得进程号last_pid。(kernel/fork.c143)。

224         testl %eax,%eax            # eax中返回进程号。若返回负数则退出。

225         js 1f

226         push %gs

227         pushl %esi

228         pushl %edi

229         pushl %ebp

230         pushl %eax

231         call _copy_process         # 调用C函数copy_process()kernel/fork.c68)。

232         addl $20,%esp              # 丢弃这里所有压栈内容。

233 1:      ret

234

    #### int 46 -- (int 0x2E) 硬盘中断处理程序,响应硬件中断请求IRQ14

    # 当请求的硬盘操作完成或出错就会发出此中断信号。(参见kernel/blk_drv/hd.c)

    # 首先向8259A中断控制从芯片发送结束硬件中断指令(EOI),然后取变量do_hd中的函数指针放入edx

    # 寄存器中,并置do_hdNULL,接着判断edx函数指针是否为空。如果为空,则给edx赋值指向

    # unexpected_hd_interrupt(),用于显示出错信息。随后向8259A主芯片送EOI指令,并调用edx

    # 指针指向的函数: read_intr()write_intr()unexpected_hd_interrupt()

235 _hd_interrupt:

236         pushl %eax

237         pushl %ecx

238         pushl %edx

239         push %ds

240         push %es

241         push %fs

242         movl $0x10,%eax            # ds,es置为内核数据段。

243         mov %ax,%ds

244         mov %ax,%es

245         movl $0x17,%eax            # fs置为调用程序的局部数据段。

246         mov %ax,%fs

    # 由于初始化中断控制芯片时没有采用自动EOI,所以这里需要发指令结束该硬件中断。

247         movb $0x20,%al

248         outb %al,$0xA0             # EOI to interrupt controller #1 # 送从8259A

249         jmp 1f                     # give port chance to breathe    # 这里jmp起延时作用。

250 1:      jmp 1f

    # do_hd定义为一个函数指针,将被赋值read_intr()write_intr()函数地址。放到edx寄存器后

    # 就将do_hd指针变量置为NULL。然后测试得到的函数指针,若该指针为空,则赋予该指针指向C

    # 函数unexpected_hd_interrupt(),以处理未知硬盘中断。

251 1:      xorl %edx,%edx

252         movl %edx,_hd_timeout      # hd_timeout置为0。表示控制器已在规定时间内产生了中断。

253         xchgl _do_hd,%edx

254         testl %edx,%edx

255         jne 1f                     # 若空,则让指针指向C函数unexpected_hd_interrupt()

256         movl $_unexpected_hd_interrupt,%edx

257 1:      outb %al,$0x20             # 8259A主芯片EOI指令(结束硬件中断)。

258         call *%edx                 # "interesting" way of handling intr.

259         pop %fs                    # 上句调用do_hd指向的C函数。

260         pop %es

261         pop %ds

262         popl %edx

263         popl %ecx

264         popl %eax

265         iret

266

    #### int38 -- (int 0x26) 软盘驱动器中断处理程序,响应硬件中断请求IRQ6

    # 其处理过程与上面对硬盘的处理基本一样。(kernel/blk_drv/floppy.c)。

    # 首先向8259A中断控制器主芯片发送EOI指令,然后取变量do_floppy中的函数指针放入eax

    # 寄存器中,并置do_floppyNULL,接着判断eax函数指针是否为空。如为空,则给eax赋值指向

    # unexpected_floppy_interrupt (),用于显示出错信息。随后调用eax指向的函数: rw_interrupt,

    # seek_interrupt,recal_interrupt,reset_interruptunexpected_floppy_interrupt

267 _floppy_interrupt:

268         pushl %eax

269         pushl %ecx

270         pushl %edx

271         push %ds

272         push %es

273         push %fs

274         movl $0x10,%eax            # ds,es置为内核数据段。

275         mov %ax,%ds

276         mov %ax,%es

277         movl $0x17,%eax            # fs置为调用程序的局部数据段。

278         mov %ax,%fs

279         movb $0x20,%al             # 送主8259A中断控制器EOI指令(结束硬件中断)。

280         outb %al,$0x20             # EOI to interrupt controller #1

    # do_floppy为一函数指针,将被赋值实际处理C函数指针。该指针在被交换放到eax寄存器后就将

    # do_floppy变量置空。然后测试eax中原指针是否为空,若是则使指针指向C函数

    # unexpected_floppy_interrupt()

281         xorl %eax,%eax

282         xchgl _do_floppy,%eax

283         testl %eax,%eax            # 测试函数指针是否=NULL?

284         jne 1f                 # 若空,则使指针指向C函数unexpected_floppy_interrupt()

285         movl $_unexpected_floppy_interrupt,%eax

286 1:      call *%eax                 # "interesting" way of handling intr.   # 间接调用。

287         pop %fs                    # 上句调用do_floppy指向的函数。

288         pop %es

289         pop %ds

290         popl %edx

291         popl %ecx

292         popl %eax

293         iret

294

    #### int 39 -- (int 0x27) 并行口中断处理程序,对应硬件中断请求信号IRQ7

    # 本版本内核还未实现。这里只是发送EOI指令。

295 _parallel_interrupt:

296         pushl %eax

297         movb $0x20,%al

298         outb %al,$0x20

299         popl %eax

300         iret



 

8.4 程序8-4 linux/kernel/mktime.c程序


  1 /*

  2  *  linux/kernel/mktime.c

  3  *

  4  *  (C) 1991  Linus Torvalds

  5  */

  6

  7 #include <time.h>            // 时间头文件,定义了标准时间数据结构tm和一些处理时间函数原型。

  8

  9 /*

 10  * This isn't the library routine, it is only used in the kernel.

 11  * as such, we don't care about years<1970 etc, but assume everything

 12  * is ok. Similarly, TZ etc is happily ignored. We just do everything

 13  * as easily as possible. Let's find something public for the library

 14  * routines (although I think minix times is public).

 15  */

 16 /*

 17  * PS. I hate whoever though up the year 1970 - couldn't they have gotten

 18  * a leap-year instead? I also hate Gregorius, pope or no. I'm grumpy.

 19  */

    /*

     * 这不是库函数,它仅供内核使用。因此我们不关心小于1970年的年份等,但假定一切均很正常。

     * 同样,时间区域TZ问题也先忽略。我们只是尽可能简单地处理问题。最好能找到一些公开的库函数

     * (尽管我认为minix的时间函数是公开的)。

     * 另外,我恨那个设置1970年开始的人 - 难道他们就不能选择从一个闰年开始?我恨格里高利历、

     * 罗马教皇、主教,我什么都不在乎。我是个脾气暴躁的人。

     */

 20 #define MINUTE 60                    // 1分钟的秒数。

 21 #define HOUR (60*MINUTE)             // 1小时的秒数。

 22 #define DAY (24*HOUR)                // 1天的秒数。

 23 #define YEAR (365*DAY)               // 1年的秒数。

 24

 25 /* interestingly, we assume leap-years */

    /* 有趣的是我们考虑进了闰年 */

    // 下面以年为界限,定义了每个月开始时的秒数时间。

 26 static int month[12] = {

 27         0,

 28         DAY*(31),

 29         DAY*(31+29),

 30         DAY*(31+29+31),

 31         DAY*(31+29+31+30),

 32         DAY*(31+29+31+30+31),

 33         DAY*(31+29+31+30+31+30),

 34         DAY*(31+29+31+30+31+30+31),

 35         DAY*(31+29+31+30+31+30+31+31),

 36         DAY*(31+29+31+30+31+30+31+31+30),

 37         DAY*(31+29+31+30+31+30+31+31+30+31),

 38         DAY*(31+29+31+30+31+30+31+31+30+31+30)

 39 };

 40

    // 该函数计算从1970110时起到开机当日经过的秒数,作为开机时间。

    // 参数tm中各字段已经在init/main.c中被赋值,信息取自CMOS

 41 long kernel_mktime(struct tm * tm)

 42 {

 43         long res;

 44         int year;

 45

    // 首先计算70年到现在经过的年数。因为是2位表示方式,所以会有2000年问题。我们可以

    // 简单地在最前面添加一条语句来解决这个问题:if (tm->tm_year<70) tm->tm_year += 100;

    // 由于UNIX计年份y是从1970年算起。到1972年就是一个闰年,因此过3年(717273

    // 就是第1个闰年,这样从1970年开始的闰年数计算方法就应该是为1 + (y - 3)/4,即为

    // (y + 1)/4res = 这些年经过的秒数时间 + 每个闰年时多1天的秒数时间 + 当年到当月时

    // 的秒数。另外,month[]数组中已经在2月份的天数中包含进了闰年时的天数,即2月份天数

    // 多算了 1天。因此,若当年不是闰年并且当前月份大于 2月份的话,我们就要减去这天。因

    // 为从70年开始算起,所以当年是闰年的判断方法是 (y + 2) 能被4除尽。若不能除尽(有余

    // 数)就不是闰年。

 46         year = tm->tm_year - 70;

 47 /* magic offsets (y+1) needed to get leapyears right.*/

    /* 为了获得正确的闰年数,这里需要这样一个魔幻值(y+1) */

 48         res = YEAR*year + DAY*((year+1)/4);

 49         res += month[tm->tm_mon];

 50 /* and (y+2) here. If it wasn't a leap-year, we have to adjust */

    /* 以及(y+2)。如果(y+2)不是闰年,那么我们就必须进行调整(减去一天的秒数时间)*/

 51         if (tm->tm_mon>1 && ((year+2)%4))

 52                 res -= DAY;

 53         res += DAY*(tm->tm_mday-1);         // 再加上本月过去的天数的秒数时间。

 54         res += HOUR*tm->tm_hour;            // 再加上当天过去的小时数的秒数时间。

 55         res += MINUTE*tm->tm_min;           // 再加上1小时内过去的分钟数的秒数时间。

 56         res += tm->tm_sec;                  // 再加上1分钟内已过的秒数。

 57         return res;                         // 即等于从1970年以来经过的秒数时间。

 58 }

 59


 


 

8.5 程序8-5 linux/kernel/sched.c


  1 /*

  2  *  linux/kernel/sched.c

  3  *

  4  *  (C) 1991  Linus Torvalds

  5  */

  6

  7 /*

  8  * 'sched.c' is the main kernel file. It contains scheduling primitives

  9  * (sleep_on, wakeup, schedule etc) as well as a number of simple system

 10  * call functions (type getpid(), which just extracts a field from

 11  * current-task

 12  */

    /*

     * 'sched.c'是主要的内核文件。其中包括有关调度的基本函数(sleep_onwakeupschedule)

     * 以及一些简单的系统调用函数(比如getpid(),仅从当前任务中获取一个字段)。

     */

 

    // 下面是调度程序头文件。定义了任务结构task_struct、第1个初始任务的数据。还有一些以宏

    // 的形式定义的有关描述符参数设置和获取的嵌入式汇编函数程序。

 13 #include <linux/sched.h>

 14 #include <linux/kernel.h> // 内核头文件。含有一些内核常用函数的原形定义。

 15 #include <linux/sys.h>    // 系统调用头文件。含有82个系统调用C函数程序,'sys_'开头。

 16 #include <linux/fdreg.h>  // 软驱头文件。含有软盘控制器参数的一些定义。

 17 #include <asm/system.h>   // 系统头文件。定义了设置或修改描述符/中断门等的嵌入式汇编宏。

 18 #include <asm/io.h>       // io头文件。定义硬件端口输入/输出宏汇编语句。

 19 #include <asm/segment.h>  // 段操作头文件。定义了有关段寄存器操作的嵌入式汇编函数。

 20

 21 #include <signal.h>       // 信号头文件。定义信号符号常量,sigaction结构,操作函数原型。

 22

    // 该宏取信号nr在信号位图中对应位的二进制数值。信号编号1-32。比如信号5的位图数值等于

    // 1<<(5-1) = 16 = 00010000b

 23 #define _S(nr) (1<<((nr)-1))

    // 除了SIGKILLSIGSTOP信号以外其他信号都是可阻塞的(…1011,1111,1110,1111,1111b)

 24 #define _BLOCKABLE (~(_S(SIGKILL) | _S(SIGSTOP)))

 25

    // 内核调试函数。显示任务号nr的进程号、进程状态和内核堆栈空闲字节数(大约)。

 26 void show_task(int nr,struct task_struct * p)

 27 {

 28         int i,j = 4096-sizeof(struct task_struct);

 29

 30         printk("%d: pid=%d, state=%d, father=%d, child=%d, ",nr,p->pid,

 31                 p->state, p->p_pptr->pid, p->p_cptr ? p->p_cptr->pid : -1);

 32         i=0;

 33         while (i<j && !((char *)(p+1))[i])  // 检测指定任务数据结构以后等于0的字节数。

 34                 i++;

 35         printk("%d/%d chars free in kstack\n\r",i,j);

 36         printk("   PC=%08X.", *(1019 + (unsigned long *) p));

 37         if (p->p_ysptr || p->p_osptr)

 38                 printk("   Younger sib=%d, older sib=%d\n\r",

 39                         p->p_ysptr ? p->p_ysptr->pid : -1,

 40                         p->p_osptr ? p->p_osptr->pid : -1);

 41         else

 42                 printk("\n\r");

 43 }

 44

    // 显示所有任务的任务号、进程号、进程状态和内核堆栈空闲字节数(大约)。

    // NR_TASKS是系统能容纳的最大进程(任务)数量(64),定义在include/kernel/sched.h 6行。

 45 void show_state(void)

 46 {

 47         int i;

 48

 49         printk("\rTask-info:\n\r");

 50         for (i=0;i<NR_TASKS;i++)

 51                 if (task[i])

 52                         show_task(i,task[i]);

 53 }

 54

    // PC8253定时芯片的输入时钟频率约为1.193180MHzLinux内核希望定时器发出中断的频率是

    // 100Hz,也即每10ms发出一次时钟中断。因此这里LATCH是设置8253芯片的初值,参见438行。

 55 #define LATCH (1193180/HZ)

 56

 57 extern void mem_use(void);         // [??]没有任何地方定义和引用该函数。

 58

 59 extern int timer_interrupt(void);  // 时钟中断处理程序(kernel/system_call.s176)。

 60 extern int system_call(void);      // 系统调用中断处理程序(kernel/system_call.s80)。

 61

    // 每个任务(进程)在内核态运行时都有自己的内核态堆栈。这里定义了任务的内核态堆栈结构。

    // 这里定义任务联合(任务结构成员和stack字符数组成员)。因为一个任务的数据结构与其内核

    // 态堆栈放在同一内存页中,所以从堆栈段寄存器ss可以获得其数据段选择符。

 62 union task_union {

 63         struct task_struct task;

 64         char stack[PAGE_SIZE];

 65 };

 66

    // 设置初始任务的数据。初始数据在include/kernel/sched.h中,第156行开始。

 67 static union task_union init_task = {INIT_TASK,};

 68

    // 从开机开始算起的滴答数时间值全局变量(10ms/滴答)。系统时钟中断每发生一次即一个滴答。

    // 前面的限定符 volatile,英文解释是易改变的、不稳定的意思。这个限定词的含义是向编译器

    // 指明变量的内容可能会由于被其他程序修改而变化。通常在程序中申明一个变量时, 编译器会

    // 尽量把它存放在通用寄存器中,例如 ebx,以提高访问效率。当CPU把其值放到 ebx中后一般

    // 就不会再关心该变量对应内存位置中的内容。若此时其他程序(例如内核程序或一个中断过程)

    // 修改了内存中该变量的值,ebx中的值并不会随之更新。为了解决这种情况就创建了volatile

    // 限定符,让代码在引用该变量时一定要从指定内存位置中取得其值。这里即是要求 gcc不要对

    // jiffies 进行优化处理,也不要挪动位置,并且需要从内存中取其值。因为时钟中断处理过程

    // 等程序会修改它的值。

 69 unsigned long volatile jiffies=0;

 70 unsigned long startup_time=0;                 // 开机时间。从1970:0:0:0开始计时的秒数。

    // 这个变量用于累计需要调整地时间嘀嗒数。

 71 int jiffies_offset = 0;         /* # clock ticks to add to get "true

 72                                    time".  Should always be less than

 73                                    1 second's worth.  For time fanatics

 74                                    who like to syncronize their machines

 75                                    to WWV :-) */

    /* 为调整时钟而需要增加的时钟嘀嗒数,以获得“精确时间”。这些调整用嘀嗒数

     * 的总和不应该超过1秒。这样做是为了那些对时间精确度要求苛刻的人,他们喜

     * 欢自己的机器时间与WWV同步 :-)

     */

 76

 77 struct task_struct *current = &(init_task.task);  // 当前任务指针(初始化指向任务0)。

 78 struct task_struct *last_task_used_math = NULL;   // 使用过协处理器任务的指针。

 79

    // 定义任务指针数组。第1项被初始化指向初始任务(任务0)的任务数据结构。

 80 struct task_struct * task[NR_TASKS] = {&(init_task.task), };

 81

    // 定义用户堆栈,共1K项,容量4K字节。在内核初始化操作过程中被用作内核栈,初始化完成

    // 以后将被用作任务0的用户态堆栈。在运行任务0之前它是内核栈,以后用作任务01的用

    // 户态栈。下面结构用于设置堆栈ss:esp(数据段选择符,指针),见head.s,第23行。

    // ss被设置为内核数据段选择符(0x10),指针esp指在 user_stack数组最后一项后面。这是

    // 因为Intel CPU执行堆栈操作时是先递减堆栈指针sp值,然后在sp指针处保存入栈内容。

 82 long user_stack [ PAGE_SIZE>>2 ] ;

 83

 84 struct {

 85         long * a;

 86         short b;

 87         } stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };

 88 /*

 89  *  'math_state_restore()' saves the current math information in the

 90  * old math state array, and gets the new ones from the current task

 91  */

    /*

     * 将当前协处理器内容保存到老协处理器状态数组中,并将当前任务的协处理器

     * 内容加载进协处理器。

     */

    // 当任务被调度交换过以后,该函数用以保存原任务的协处理器状态(上下文)并恢复新调度进

    // 来的当前任务的协处理器执行状态。

 92 void math_state_restore()

 93 {

    // 如果任务没变则返回(上一个任务就是当前任务)。这里"上一个任务"是指刚被交换出去的任务。

 94         if (last_task_used_math == current)

 95                 return;

    // 在发送协处理器命令之前要先发WAIT指令。如果上个任务使用了协处理器,则保存其状态。

 96         __asm__("fwait");

 97         if (last_task_used_math) {

 98                 __asm__("fnsave %0"::"m" (last_task_used_math->tss.i387));

 99         }

    // 现在,last_task_used_math指向当前任务,以备当前任务被交换出去时使用。此时如果当前

    // 任务用过协处理器,则恢复其状态。否则的话说明是第一次使用,于是就向协处理器发初始化

    // 命令,并设置使用了协处理器标志。

100         last_task_used_math=current;

101         if (current->used_math) {

102                 __asm__("frstor %0"::"m" (current->tss.i387));

103         } else {

104                 __asm__("fninit"::);           // 向协处理器发初始化命令。

105                 current->used_math=1;          // 设置使用已协处理器标志。

106         }

107 }

108

109 /*

110  *  'schedule()' is the scheduler function. This is GOOD CODE! There

111  * probably won't be any reason to change this, as it should work well

112  * in all circumstances (ie gives IO-bound processes good response etc).

113  * The one thing you might take a look at is the signal-handler code here.

114  *

115  *   NOTE!!  Task 0 is the 'idle' task, which gets called when no other

116  * tasks can run. It can not be killed, and it cannot sleep. The 'state'

117  * information in task[0] is never used.

118  */

    /*

     * 'schedule()'是调度函数。这是个很好的代码!没有任何理由对它进行修改,因为

     * 它可以在所有的环境下工作(比如能够对IO-边界处理很好的响应等)。只有一件

     * 事值得留意,那就是这里的信号处理代码。

     *

     *   注意!!任务0是个闲置('idle')任务,只有当没有其他任务可以运行时才调用

     * 它。它不能被杀死,也不能睡眠。任务0中的状态信息'state'是从来不用的。

     */

119 void schedule(void)

120 {

121         int i,next,c;

122         struct task_struct ** p;              // 任务结构指针的指针。

123

124 /* check alarm, wake up any interruptible tasks that have got a signal */

    /* 检测alarm(进程的报警定时值),唤醒任何已得到信号的可中断任务 */

125

    // 从任务数组中最后一个任务开始循环检测alarm。在循环时跳过空指针项。

126         for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)

127                 if (*p) {

    // 如果设置过任务超时定时timeout,并且已经超时,则复位超时定时值,并且如果任务处于可

    // 中断睡眠状态TASK_INTERRUPTIBLE下,将其置为就绪状态(TASK_RUNNING)。

128                         if ((*p)->timeout && (*p)->timeout < jiffies) {

129                                 (*p)->timeout = 0;

130                                 if ((*p)->state == TASK_INTERRUPTIBLE)

131                                         (*p)->state = TASK_RUNNING;

132                         }

    // 如果设置过任务的定时值alarm,并且已经过期(alarm<jiffies),则在信号位图中置SIGALRM

    // 信号,即向任务发送SIGALARM信号。然后清alarm。该信号的默认操作是终止进程。jiffies

    // 是系统从开机开始算起的滴答数(10ms/滴答)。定义在sched.h139行。

133                         if ((*p)->alarm && (*p)->alarm < jiffies) {

134                                 (*p)->signal |= (1<<(SIGALRM-1));

135                                 (*p)->alarm = 0;

136                         }

    // 如果信号位图中除被阻塞的信号外还有其他信号,并且任务处于可中断状态,则置任务为就绪

    // 状态。其中'~(_BLOCKABLE & (*p)->blocked)'用于忽略被阻塞的信号,但SIGKILLSIGSTOP

    // 不能被阻塞。

137                         if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&

138                         (*p)->state==TASK_INTERRUPTIBLE)

139                                 (*p)->state=TASK_RUNNING;   //置为就绪(可执行)状态。

140                 }

141

142 /* this is the scheduler proper: */

    /* 这里是调度程序的主要部分 */

143

144         while (1) {

145                 c = -1;

146                 next = 0;

147                 i = NR_TASKS;

148                 p = &task[NR_TASKS];

    // 这段代码也是从任务数组的最后一个任务开始循环处理,并跳过不含任务的数组槽。比较每个

    // 就绪状态任务的counter(任务运行时间的递减滴答计数)值,哪一个值大,运行时间还不长,

    // next就指向哪个的任务号。

149                 while (--i) {

150                         if (!*--p)

151                                 continue;

152                         if ((*p)->state == TASK_RUNNING && (*p)->counter > c)

153                                 c = (*p)->counter, next = i;

154                 }

    // 如果比较得出有counter值不等于0的结果,或者系统中没有一个可运行的任务存在(此时c

    // 仍然为-1next=0),则退出144行开始的循环,执行161行上的任务切换操作。否则就根据

    // 每个任务的优先权值,更新每一个任务的counter值,然后回到125行重新比较。counter

    // 的计算方式为 counter = counter /2 + priority。注意,这里计算过程不考虑进程的状态。

155                 if (c) break;

156                 for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)

157                         if (*p)

158                                 (*p)->counter = ((*p)->counter >> 1) +

159                                                 (*p)->priority;

160         }

    // 用下面宏(定义在sched.h中)把当前任务指针current指向任务号为next的任务,并切换

    // 到该任务中运行。在146行上next被初始化为0。因此若系统中没有任何其他任务可运行时,

    // next 始终为0。因此调度函数会在系统空闲时去执行任务0。 此时任务0仅执行pause()

    // 系统调用,并又会调用本函数。

161         switch_to(next);                     // 切换到任务号为next的任务,并运行之。

162 }

163

    //// pause()系统调用。转换当前任务的状态为可中断的等待状态,并重新调度。

    // 该系统调用将导致进程进入睡眠状态,直到收到一个信号。该信号用于终止进程或者使进程

    // 调用一个信号捕获函数。只有当捕获了一个信号,并且信号捕获处理函数返回,pause()

    // 会返回。此时 pause()返回值应该是 -1,并且 errno被置为 EINTR。这里还没有完全实现

    // (直到0.95版)。

164 int sys_pause(void)

165 {

166         current->state = TASK_INTERRUPTIBLE;

167         schedule();

168         return 0;

169 }

170

    // 把当前任务置为指定的睡眠状态(可中断的或不可中断的),并让睡眠队列头指针指向当前任务。

    // 函数参数p是等待任务队列头指针。指针是含有一个变量地址的变量。这里参数p使用了指针的

    // 指针形式 '**p',这是因为C函数参数只能传值,没有直接的方式让被调用函数改变调用该函数

    // 程序中变量的值。但是指针'*p'指向的目标(这里是任务结构)会改变,因此为了能修改调用该

    // 函数程序中原来就是指针变量的值,就需要传递指针'*p'的指针,即'**p'。参见程序前示例图中

    // p指针的使用情况。

    // 参数state是任务睡眠使用的状态:TASK_UNINTERRUPTIBLETASK_INTERRUPTIBLE。处于不可

    // 中断睡眠状态(TASK_UNINTERRUPTIBLE)的任务需要内核程序利用wake_up()函数明确唤醒之。

    // 处于可中断睡眠状态(TASK_INTERRUPTIBLE)可以通过信号、任务超时等手段唤醒 (置为就绪

    // 状态TASK_RUNNING)。

    // *** 注意,由于本内核代码不是很成熟,因此下列与睡眠相关的代码存在一些问题,不宜深究。

171 static inline void __sleep_on(struct task_struct **p, int state)

172 {

173         struct task_struct *tmp;

174

    // 若指针无效,则退出。(指针所指的对象可以是NULL,但指针本身不会为0)

    // 如果当前任务是任务0,则死机(impossible!)

175         if (!p)

176                 return;

177         if (current == &(init_task.task))

178                 panic("task[0] trying to sleep");

    // tmp指向已经在等待队列上的任务(如果有的话),例如inode->i_wait。并且将睡眠队列头

    // 的等待指针指向当前任务。这样就把当前任务插入到了 *p的等待队列中。然后将当前任务置

    // 为指定的等待状态,并执行重新调度。

179         tmp = *p;

180         *p = current;

181         current->state = state;

182 repeat: schedule();

    // 只有当这个等待任务被唤醒时,程序才又会返回到这里,表示进程已被明确地唤醒并执行。

    // 如果等待队列中还有等待任务,并且队列头指针 *p 所指向的任务不是当前任务时,说明

    // 在本任务插入等待队列后还有任务进入等待队列。于是我们应该也要唤醒这个任务,而我

    // 们自己应按顺序让这些后面进入队列的任务唤醒,因此这里将等待队列头所指任务先置为

    // 就绪状态,而自己则置为不可中断等待状态,即自己要等待这些后续进队列的任务被唤醒

    // 而执行时来唤醒本任务。然后重新执行调度程序。

183         if (*p && *p != current) {

184                 (**p).state = 0;

185                 current->state = TASK_UNINTERRUPTIBLE;

186                 goto repeat;

187         }

    // 执行到这里,说明本任务真正被唤醒执行。此时等待队列头指针应该指向本任务,若它为

    // 空,则表明调度有问题,于是显示警告信息。最后我们让头指针指向在我们前面进入队列

    // 的任务(*p = tmp)。 若确实存在这样一个任务,即队列中还有任务(tmp不为空),就

    // 唤醒之。最先进入队列的任务在唤醒后运行时最终会把等待队列头指针置成NULL

188         if (!*p)

189                 printk("Warning: *P = NULL\n\r");

190         if (*p = tmp)

191                 tmp->state=0;

192 }

193

    // 将当前任务置为可中断的等待状态(TASK_INTERRUPTIBLE),并放入头指针*p指定的等待

    // 队列中。

194 void interruptible_sleep_on(struct task_struct **p)

195 {

196         __sleep_on(p,TASK_INTERRUPTIBLE);

197 }

198

    // 把当前任务置为不可中断的等待状态(TASK_UNINTERRUPTIBLE),并让睡眠队列头指针指向

    // 当前任务。只有明确地唤醒时才会返回。该函数提供了进程与中断处理程序之间的同步机制。

199 void sleep_on(struct task_struct **p)

200 {

201         __sleep_on(p,TASK_UNINTERRUPTIBLE);

202 }

203

    // 唤醒 *p指向的任务。*p是任务等待队列头指针。由于新等待任务是插入在等待队列头指针

    // 处的,因此唤醒的是最后进入等待队列的任务。若该任务已经处于停止或僵死状态,则显示

    // 警告信息。

204 void wake_up(struct task_struct **p)

205 {

206         if (p && *p) {

207                 if ((**p).state == TASK_STOPPED)         // 处于停止状态。

208                         printk("wake_up: TASK_STOPPED");

209                 if ((**p).state == TASK_ZOMBIE)          // 处于僵死状态。

210                         printk("wake_up: TASK_ZOMBIE");

211                 (**p).state=0;                           // 置为就绪状态TASK_RUNNING

212         }

213 }

214

215 /*

216  * OK, here are some floppy things that shouldn't be in the kernel

217  * proper. They are here because the floppy needs a timer, and this

218  * was the easiest way of doing it.

219  */

    /*

     * 好了,从这里开始是一些有关软盘的子程序,本不应该放在内核的主要部分

     * 中的。将它们放在这里是因为软驱需要定时处理,而放在这里是最方便的。

     */

    // 下面220 -- 281行代码用于处理软驱定时。在阅读这段代码之前请先看一下块设备一章中

    // 有关软盘驱动程序(floppy.c)后面的说明, 或者到阅读软盘块设备驱动程序时在来看这

    // 段代码。其中时间单位:1个滴答 = 1/100秒。

    // 下面数组wait_motor[]用于存放等待软驱马达启动到正常转速的进程指针。数组索引0-3

    // 分别对应软驱A--D。数组 mon_timer[]存放各软驱马达启动所需要的滴答数。程序中默认

    // 启动时间为50个滴答(0.5秒)。数组 moff_timer[] 存放各软驱在马达停转之前需维持

    // 的时间。程序中设定为10000个滴答(100秒)。

220 static struct task_struct * wait_motor[4] = {NULL,NULL,NULL,NULL};

221 static int  mon_timer[4]={0,0,0,0};

222 static int moff_timer[4]={0,0,0,0};

 

    // 下面变量对应软驱控制器中当前数字输出寄存器。该寄存器每位的定义如下:

    // 7-4:分别控制驱动器D-A马达的启动。1 - 启动;0 - 关闭。

    // 1 - 允许DMA和中断请求;0 - 禁止DMA和中断请求。

    // 1 - 启动软盘控制器;    0 - 复位软盘控制器。

    // 1-000 - 11,用于选择控制的软驱A-D

    // 这里设置初值为:允许DMA和中断请求、启动FDC

223 unsigned char current_DOR = 0x0C;

224

    // 指定软驱启动到正常运转状态所需等待时间。

    // 参数nr -- 软驱号(0--3),返回值为滴答数。

    // 局部变量selected是选中软驱标志(blk_drv/floppy.c123行)。mask是所选软驱对应的

    // 数字输出寄存器中启动马达比特位。mask4位是各软驱启动马达标志。

225 int ticks_to_floppy_on(unsigned int nr)

226 {

227         extern unsigned char selected;

228         unsigned char mask = 0x10 << nr;

229

    // 系统最多有4个软驱。首先预先设置好指定软驱nr停转之前需要经过的时间(100秒)。然后

    // 取当前DOR寄存器值到临时变量mask中,并把指定软驱的马达启动标志置位。

230         if (nr>3)

231                 panic("floppy_on: nr>3");

232         moff_timer[nr]=10000;           /* 100 s = very big :-) */   // 停转维持时间。

233         cli();                          /* use floppy_off to turn it off */ // 关中断。

234         mask |= current_DOR;

    // 如果当前没有选择软驱,则首先复位其他软驱的选择位,然后置指定软驱选择位。

235         if (!selected) {

236                 mask &= 0xFC;

237                 mask |= nr;

238         }

    // 如果数字输出寄存器的当前值与要求的值不同,则向FDC数字输出端口输出新值(mask),并且

    // 如果要求启动的马达还没有启动,则置相应软驱的马达启动定时器值(HZ/2 = 0.5秒或 50

    // 滴答)。若已经启动,则再设置启动定时为2个滴答,能满足下面 do_floppy_timer()中先递

    // 减后判断的要求。执行本次定时代码的要求即可。此后更新当前数字输出寄存器current_DOR

239         if (mask != current_DOR) {

240                 outb(mask,FD_DOR);

241                 if ((mask ^ current_DOR) & 0xf0)

242                         mon_timer[nr] = HZ/2;

243                 else if (mon_timer[nr] < 2)

244                         mon_timer[nr] = 2;

245                 current_DOR = mask;

246         }

247         sti();                          // 开中断。

248         return mon_timer[nr];           // 最后返回启动马达所需的时间值。

249 }

250

    // 等待指定软驱马达启动所需的一段时间,然后返回。

    // 设置指定软驱的马达启动到正常转速所需的延时,然后睡眠等待。在定时中断过程中会一直

    // 递减判断这里设定的延时值。当延时到期,就会唤醒这里的等待进程。

251 void floppy_on(unsigned int nr)

252 {

    // 关中断。如果马达启动定时还没到,就一直把当前进程置为不可中断睡眠状态并放入等待马达

    // 运行的队列中。然后开中断。

253         cli();

254         while (ticks_to_floppy_on(nr))

255                 sleep_on(nr+wait_motor);

256         sti();

257 }

258

    // 置关闭相应软驱马达停转定时器(3秒)。

    // 若不使用该函数明确关闭指定的软驱马达,则在马达开启100秒之后也会被关闭。

259 void floppy_off(unsigned int nr)

260 {

261         moff_timer[nr]=3*HZ;

262 }

263

    // 软盘定时处理子程序。更新马达启动定时值和马达关闭停转计时值。该子程序会在时钟定时

    // 中断过程中被调用,因此系统每经过一个滴答(10ms)就会被调用一次,随时更新马达开启或

    // 停转定时器的值。如果某一个马达停转定时到,则将数字输出寄存器马达启动位复位。

264 void do_floppy_timer(void)

265 {

266         int i;

267         unsigned char mask = 0x10;

268

269         for (i=0 ; i<4 ; i++,mask <<= 1) {

270                 if (!(mask & current_DOR))             // 如果不是DOR指定的马达则跳过。

271                         continue;

272                 if (mon_timer[i]) {                    // 如果马达启动定时到则唤醒进程。

273                         if (!--mon_timer[i])

274                                 wake_up(i+wait_motor);

275                 } else if (!moff_timer[i]) {           // 如果马达停转定时到则

276                         current_DOR &= ~mask;          // 复位相应马达启动位,并且

277                         outb(current_DOR,FD_DOR);      // 更新数字输出寄存器。

278                 } else

279                         moff_timer[i]--;               // 否则马达停转计时递减。

280         }

281 }

282

    // 下面是关于定时器的代码。最多可有64个定时器。

283 #define TIME_REQUESTS 64

284

    // 定时器链表结构和定时器数组。该定时器链表专用于供软驱关闭马达和启动马达定时操作。

    // 这种类型定时器类似现代Linux系统中的动态定时器(Dynamic Timer),仅供内核使用。

285 static struct timer_list {

286         long jiffies;                  // 定时滴答数。

287         void (*fn)();                  // 定时处理程序。

288         struct timer_list * next;      // 链接指向下一个定时器。

289 } timer_list[TIME_REQUESTS], * next_timer = NULL;  // next_timer是定时器队列头指针。

290

    // 添加定时器。输入参数为指定的定时值(滴答数)和相应的处理程序指针。

    // 软盘驱动程序(floppy.c)利用该函数执行启动或关闭马达的延时操作。

    // 参数jiffies – 10毫秒计的滴答数;*fn()- 定时时间到时执行的函数。

291 void add_timer(long jiffies, void (*fn)(void))

292 {

293         struct timer_list * p;

294

    // 如果定时处理程序指针为空,则退出。否则关中断。

295         if (!fn)

296                 return;

297         cli();

    // 如果定时值<=0,则立刻调用其处理程序。并且该定时器不加入链表中。

298         if (jiffies <= 0)

299                 (fn)();

300         else {

    // 否则从定时器数组中,找一个空闲项。

301                 for (p = timer_list ; p < timer_list + TIME_REQUESTS ; p++)

302                         if (!p->fn)

303                                 break;

    // 如果已经用完了定时器数组,则系统崩溃J。否则向定时器数据结构填入相应信息,并链入

    // 链表头。

304                 if (p >= timer_list + TIME_REQUESTS)

305                         panic("No more time requests free");

306                 p->fn = fn;

307                 p->jiffies = jiffies;

308                 p->next = next_timer;

309                 next_timer = p;

    // 链表项按定时值从小到大排序。在排序时减去排在前面需要的滴答数,这样在处理定时器时

    // 只要查看链表头的第一项的定时是否到期即可。[[?? 这段程序好象没有考虑周全。如果新

    // 插入的定时器值小于原来头一个定时器值时则根本不会进入循环中,但此时还是应该将紧随

    // 其后面的一个定时器值减去新的第1个的定时值。即如果第1个定时值<=2个,则第2

    // 定时值扣除第1个的值即可,否则进入下面循环中进行处理。]]

310                 while (p->next && p->next->jiffies < p->jiffies) {

311                         p->jiffies -= p->next->jiffies;

312                         fn = p->fn;

313                         p->fn = p->next->fn;

314                         p->next->fn = fn;

315                         jiffies = p->jiffies;

316                         p->jiffies = p->next->jiffies;

317                         p->next->jiffies = jiffies;

318                         p = p->next;

319                 }

320         }

321         sti();

322 }

323

    //// 时钟中断C函数处理程序,在sys_call.s中的_timer_interrupt189行)被调用。

    // 参数cpl是当前特权级03,是时钟中断发生时正被执行的代码选择符中的特权级。

    // cpl=0时表示中断发生时正在执行内核代码;cpl=3时表示中断发生时正在执行用户代码。

    // 对于一个进程由于执行时间片用完时,则进行任务切换。并执行一个计时更新工作。

324 void do_timer(long cpl)

325 {

326         static int blanked = 0;

327

    // 首先判断是否经过了一定时间而让屏幕黑屏(blankout)。如果blankcount计数不为零,

    // 或者黑屏延时间隔时间blankinterval0的话,那么若已经处于黑屏状态(黑屏标志

    // blanked = 1),则让屏幕恢复显示。若blankcount计数不为零,则递减之,并且复位

    // 黑屏标志。

328         if (blankcount || !blankinterval) {

329                 if (blanked)

330                         unblank_screen();

331                 if (blankcount)

332                         blankcount--;

333                 blanked = 0;

    // 否则的话若黑屏标志未置位,则让屏幕黑屏,并且设置黑屏标志。

334         } else if (!blanked) {

335                 blank_screen();

336                 blanked = 1;

337         }

    // 接着处理硬盘操作超时问题。如果硬盘超时计数递减之后为0,则进行硬盘访问超时处理。

338         if (hd_timeout)

339                 if (!--hd_timeout)

340                         hd_times_out();  // 硬盘访问超时处理(blk_drv/hdc318行)。

341

    // 如果发声计数次数到,则关闭发声。(0x61口发送命令,复位位01。位0控制8253

    // 计数器2的工作,位1控制扬声器)

342         if (beepcount)          // 扬声器发声时间滴答数(chr_drv/console.c,950行)。

343                 if (!--beepcount)

344                         sysbeepstop();

345

    // 如果当前特权级(cpl)0(最高,表示是内核程序在工作),则将内核代码运行时间stime

    // 递增;[ Linus把内核程序统称为超级用户(supervisor)的程序,见sys_call.s207

    // 上的英文注释。这种称呼来自于Intel CPU 手册。] 如果cpl > 0,则表示是一般用户程序

    // 在工作,增加utime

346         if (cpl)

347                 current->utime++;

348         else

349                 current->stime++;

350

    // 如果有定时器存在,则将链表第1个定时器的值减1。如果已等于0,则调用相应的处理程序,

    // 并将该处理程序指针置为空。然后去掉该项定时器。next_timer是定时器链表的头指针。

351         if (next_timer) {

352                 next_timer->jiffies--;

353                 while (next_timer && next_timer->jiffies <= 0) {

354