同济大学 |
Linux操作系统实现原理 |
内核程序集 |
|
赵炯 |
2017/9/18 |
Linux 0.12版内核已注释程序集 |
|
目录
6.1 程序6 ‑1 linux/boot/bootsect.S
8.2 程序8-2 linux/kernel/traps.c
8.3 程序8-3 linux/kernel/sys_call.s
8.4 程序8-4 linux/kernel/mktime.c程序
8.5 程序8-5 linux/kernel/sched.c
8.6 程序8-6 linux/kernel/signal.c
8.9 程序8-9 linux/kernel/sys.c程序
8.10 程序8-10 linux/kernel/vsprintf.c
8.11 程序8-11 linux/kernel/printk.c
8.12 程序8-12 linux/kernel/panic.c
9.1 程序9-1 linux/kernel/blk_drv/blk.h
9.2 程序9-2 linux/kernel/blk_drv/hd.c
9.3 程序9-3 linux/kernel/blk_drv/ll_rw_blk.c
9.4 程序9-4 linux/kernel/blk_drv/ramdisk.c
9.5 程序9-5 linux/kernel/blk_drv/floppy.c
10.1 程序10-1 linux/kernel/chr_drv/keyboard.S
10.2 程序10-2 linux/kernel/chr_drv/console.c
10.3 程序10-3 linux/kernel/chr_drv/serial.c
10.4 程序10-4 linux/kernel/chr_drv/rs_io.s
10.5 程序10-5 linux/kernel/chr_drv/tty_io.c
10.6 程序10-6 linux/kernel/chr_drv/tty_ioctl.c
11.1 程序11-1 linux/kernel/math/math_emulate.c
11.2 程序11-2 linux/kernel/math/error.c
11.3 程序11-3 linux/kernel/math/ea.c
11.4 程序11-4 linux/kernel/math/convert.c
11.5 程序11-5 linux/kernel/math/add.c
11.6 程序11-6 linux/kernel/math/compare.c
11.7 程序11-7 linux/kernel/math/get_put.c
11.8 程序11-8 linux/kernel/math/mul.c
11.9 程序11-9 linux/kernel/math/div.c
12.2 程序 12-2 linux/fs/bitmap.c
12.3 程序12-3 linux/fs/truncate.c
12.7 程序12-7 linux/fs/file_table.c
12.8 程序12-8 linux/fs/block_dev.c
12.9 程序12-9 linux/fs/file_dev.c
12.11 程序12-11 linux/fs/char_dev.c
12.12 程序12-12 linux/fs/read_write.c
12.16 程序12-16 linux/fs/fcntl.c
12.17 程序12-17 linux/fs/ioctl.c
12.18 程序12-18 linux/fs/select.c
14.1 程序14-1 linux/include/a.out.h
14.2 程序14-2 linux/include/const.h
14.3 程序14-3 linux/include/ctype.h
14.4 程序14-4 linux/include/errno.h
14.5 程序14-5 linux/include/fcntl.h
14.6 程序14-6 linux/include/signal.h
14.7 程序14-7 linux/include/stdarg.h
14.8 程序14-8 linux/include/stddef.h
14.9 程序14-9 linux/include/string.h
14.10 程序14-10 linux/include/termios.h
14.11 程序14-11 linux/include/time.h
14.12 程序14-12 linux/include/unistd.h
14.13 程序14-13 linux/include/utime.h
14.14 程序14-14 linux/include/asm/io.h
14.15 程序14-15 linux/include/asm/memory.h
14.16 程序14-16 linux/include/asm/segment.h
14.17 程序14-17 linux/include/asm/system.h
14.18 程序14-18 linux/include/linux/config.h
14.19 程序14-19 linux/include/linux/fdreg.h
14.20 程序14-20 linux/include/linux/fs.h
14.21 程序14-21 linux/include/linux/hdreg.h
14.22 程序14-22 linux/include/linux/head.h
14.23 程序14-23 linux/include/linux/kernel.h
14.24 程序14-24 linux/include/linux/math_emu.h
14.25 程序14-25 linux/include/linux/mm.h
14.26 程序14-26 linux/include/linux/sched.h
14.27 程序14-27 linux/include/linux/sys.h
14.28 程序14-28 linux/include/linux/tty.h
14.29 程序14-29 linux/include/sys/param.h
14.30 程序14-30 linux/include/sys/resource.h
14.31 程序14-31 linux/include/sys/stat.h
14.32 程序14-32 linux/include/sys/time.h
14.33 程序14-33 linux/include/sys/times.h
14.34 程序14-34 linux/include/sys/types.h
14.35 程序14-35 linux/include/sys/utsname.h
14.36 程序14-36 linux/include/sys/wait.h
15.6 程序15-6 linux/lib/execve.c
15.7 程序15-7 linux/lib/malloc.c
15.9 程序15-9 linux/lib/setsid.c
15.10 程序15-10 linux/lib/string.c
15.11 程序15-11 linux/lib/wait.c
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
7 AS86 =as86 -0 -a # 8086汇编编译器和连接器,见列表后的介绍。后带的参数含义分别
8 LD86 =ld86 -0 # 是:-0 生成8086目标程序;-a 生成与gas和gld部分兼容的代码。
10 AS =gas # GNU汇编编译器和连接器,见列表后的介绍。
11 LD =gld
# 下面是GNU链接器gld运行时用到的选项。含义是:-s 输出文件中省略所有的符号信息;-x 删除
# 所有局部符号;-M 表示需要在标准输出设备(显示器)上打印连接映像(link map),是指由连接程序
# 产生的一种内存地址映像,其中列出了程序段装入到内存中的位置信息。具体来讲有如下信息:
# • 目标文件及符号信息映射到内存中的位置;
# • 公共符号如何放置;
# • 连接中包含的所有文件成员及其引用的符号。
12 LDFLAGS =-s -x -M
# gcc是GNU 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
# 下面cpp是gcc的前(预)处理器程序。前处理器用于进行程序中的宏替换处理、条件编译处理以及
# 包含进指定文件的内容,即把使用'#include'指定的文件包含进来。源程序文件中所有以符号'#'
# 开始的行均需要由前处理器进行处理。程序中所有'#define'定义的宏都会使用其定义部分替换掉。
# 程序中所有'#if'、'#ifdef'、'#ifndef'和'#endif'等条件判别行用于确定是否包含其指定范围中
# 的语句。
# '-nostdinc -Iinclude'含义是不要搜索标准头文件目录中的文件,即不用系统/usr/include/目录
# 下的头文件,而是使用'-I'选项指定的目录或者是在当前目录里搜索头文件。
16 CPP =cpp -nostdinc -Iinclude
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
# 下面是kernel目录、mm目录和fs目录所产生的目标代码文件。为了方便引用在这里将它们用
# ARCHIVES(归档文件)标识符表示。
26 ARCHIVES=kernel/kernel.o mm/mm.o fs/fs.o
# 块和字符设备库文件。'.a'表示该文件是个归档文件,也即包含有许多可执行二进制代码子程序
# 集合的库文件,通常是用GNU的ar程序生成。ar是GNU的二进制文件处理程序,用于创建、修改
# 以及从归档文件中抽取文件。
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/目录中的文件所编译生成的通用库文件。
# 下面是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目标文件。整句表示使用gcc将C语言文件编译成目标文件但不连接。
36 .c.o:
37 $(CC) $(CFLAGS) \
38 -nostdinc -Iinclude -c -o $*.o $<
# 下面'all'表示创建Makefile所知的最顶层的目标。这里即是Image文件。这里生成的Image文件
# 即是引导启动盘映像文件bootimage。若将其写入软盘就可以使用该软盘引导Linux系统了。在
# Linux下将Image写入软盘的命令参见46行。DOS系统下可以使用软件rawrite.exe。
40 all: Image
# 说明目标(Image文件)是由冒号后面的4个元素产生,分别是boot/目录中的bootsect和setup
# 文件、tools/目录中的system和build文件。42--43行这是执行的命令。42行表示使用tools目
# 录下的build工具程序(下面会说明如何生成)将bootsect、setup和system文件以$(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
# 表示disk这个目标要由Image产生。dd为UNIX标准命令:复制一个文件,根据选项进行转换和格
# 式化。bs=表示一次读/写的字节数。if=表示输入的文件,of=表示输出到的文件。这里/dev/PS0是
# 指第一个软盘驱动器(设备文件)。在Linux系统下使用/dev/fd0。
47 disk: Image
48 dd bs=8192 if=Image of=/dev/PS0
50 tools/build: tools/build.c # 由tools目录下的build.c程序生成执行程序build。
51 $(CC) $(CFLAGS) \
52 -o tools/build tools/build.c # 编译生成执行程序build的命令。
54 boot/head.o: boot/head.s # 利用上面给出的.s.o规则生成head.o目标文件。
# 表示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
# 数学协处理函数文件math.a由64行上的命令实现:进入kernel/math/目录;运行make工具程序。
65 kernel/math/math.a:
66 (cd kernel/math; make)
68 kernel/blk_drv/blk_drv.a: # 生成块设备库文件blk_drv.a,其中含有可重定位目标文件。
69 (cd kernel/blk_drv; make)
71 kernel/chr_drv/chr_drv.a: # 生成字符设备函数文件chr_drv.a。
72 (cd kernel/chr_drv; make)
74 kernel/kernel.o: # 内核目标模块kernel.o
75 (cd kernel; make)
77 mm/mm.o: # 内存管理模块mm.o
78 (cd mm; make)
80 fs/fs.o: # 文件系统目标模块fs.o
81 (cd fs; make)
83 lib/lib.a: # 库函数lib.a
84 (cd lib; make)
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 选项表示要去除目标文件中的符号信息。
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
# 当执行'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)
# 该规则将首先执行上面的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 # 迫使缓冲块数据立即写盘并更新磁盘超级块。
113 dep:
# 该目标或规则用于产生各文件之间的依赖关系。创建这些依赖关系是为了让make命令用它们来确定
# 是否需要重建一个目标对象。比如当某个头文件被改动过后,make就能通过生成的依赖关系,重新
# 编译与该头文件有关的所有*.c文件。具体方法如下:
# 使用字符串编辑程序sed对Makefile文件(这里即是本文件)进行处理,输出为删除了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)
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
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处开始存放移动后的bootsect和setup
! 的代码,因此该值最大不得超过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模块加载到0x10000(64 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位置处的代码中执行。
! 这段代码设置几个段寄存器,包括栈寄存器ss和sp。栈指针sp只要指向远大于512字节偏移
! (即地址0x90200)处都可以。因为从0x90200地址开始处还要放置setup程序,而此时setup
! 程序大约为4个扇区,因此sp要指向大于(0x200 + 0x200 * 4 +堆栈大小)位置处。这里sp
! 设置为 0x9ff00 - 12(参数表长度),即sp = 0xfef4。在此之上位置会存放一个自建的驱动
! 器参数表,见下面说明。实际上BIOS把引导扇区加载到0x7c00 处并把执行权交给引导程序时,
! ss = 0x00,sp = 0xfffe。
! 另外,第65行上push指令的期望作用是想暂时把段值保留在栈中,然后等下面执行完判断磁道
! 扇区数后再弹出栈,并给段寄存器 fs和gs赋值(第109行)。但是由于第67、68两语句修改
! 了栈段的位置,因此除非在执行栈弹出操作之前把栈段恢复到原位置,否则这样设计就是错误的。
! 因此这里存在一个bug。改正的方法之一是去掉第65行,并把第109行修改成“mov ax,cs”。
60 go: mov ax,cs ! 将ds、es和ss都置成移动后代码所在的段处(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 - 都为INITSEG(0x9000),
* fs = 0,gs = 参数表所在段值。
*/
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
93 mov di,dx ! es:di is destination ! dx=0xfef4,在61行被设置。
94 mov cx,#6 ! copy 12 bytes
95 cld ! 清方向标志。复制时指针递增。
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)。
! 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 = 驱动器号(如果是硬盘则要置位7为1)。
! 返回信息:
! 如果出错则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段寄存器所指的段中。它只影响其下一条语句。实际
! 上,由于本程序代码和数据都被设置处于同一个段中,即段寄存器cs和ds、es的值相同,因
! 此本程序中此处可以不使用该指令。
147 seg cs
! 下句保存每磁道扇区数。对于软盘来说(dl=0),其最大磁道号不会超过256,ch已经足够表
! 示它,因此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模块加载到0x10000(64KB)开始处。
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,其中
!! nr为0-3分别对应软驱A、B、C或D;type是软驱的类型(2à1.2MB或7à1.44MB等)。
!! 因为7*4 + 0 = 28,所以 /dev/PS0 (2,28)指的是1.44MB A驱动器,其设备号是0x021c
!! 同理 /dev/at0 (2,8)指的是1.2MB A驱动器,其设备号是0x0208。
! 下面root_dev定义在引导扇区508,509字节处,指根文件系统所在设备号。0x0306指第2
! 个硬盘第1个分区。这里默认为0x0306是因为当时 Linus 开发Linux系统时是在第2个硬
! 盘第1个分区中存放根文件系统。这个值需要根据你自己根文件系统所在硬盘和分区进行修
! 改。例如,如果你的根文件系统在第1个硬盘的第1个分区上,那么该值应该为0x0301,即
! (0x01, 0x03)。如果根文件系统是在第2个Bochs软盘上,那么该值应该为0x021D,即
! (0x1D,0x02)。当编译内核时,你可以在Makefile文件中另行指定你自己的值,内核映像
! 文件Image的创建程序tools/build会使用你指定的值来设置你的根文件系统所在设备号。
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 0x13,ah=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
! 然后正式进行磁道扇区读操作。
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,读磁盘扇区功能号。
276 push dx ! save for error dump
277 push cx ! 为出错情况保存一些信息。
278 push bx
279 push ax
281 int 0x13
282 jc bad_rt ! 若出错,则跳转至bad_rt。
283 add sp,#8 ! 没有出错。因此丢弃为出错情况保存的信息。
284 popa
285 ret
! 读磁盘操作出错。则先显示出错信息,然后执行驱动器复位操作(磁盘中断功能号0),再跳转
! 到read_track处重试。
287 bad_rt: push ax ! save error code
288 call print_all ! ah = error, al = read
291 xor ah,ah
292 xor dl,dl
293 int 0x13
296 add sp, #10 ! 丢弃为出错情况保存的信息。
297 popa
298 jmp read_track
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,则不显示寄存器名称。
313 print_all:
314 mov cx, #5 ! error code + 4 registers ! 显示值个数。
315 mov bp, sp ! 保存当前栈指针sp。
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
326 mov al, #0x58 ! X ! 显示字符'X'。
327 int 0x10
329 mov al, #0x3a ! : ! 显示字符':'。
330 int 0x10
! 显示寄存器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
! 调用BIOS中断0x10,以电传方式显示回车换行。
339 print_nl:
340 mov ax, #0xe0d ! CR
341 int 0x10
342 mov al, #0xa ! LF
343 int 0x10
344 ret
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指向的字。
*/
! 调用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
364 good_digit:
365 int 0x10
366 loop print_digit ! cx--。若cx>0则去显示下一个值。
367 ret
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
383 sectors:
384 .word 0 ! 存放当前启动软盘每磁道的扇区数。
386 msg1: ! 开机调用BIOS中断显示的信息。共9个字符。
387 .byte 13,10 ! 回车、换行的ASCII码。
388 .ascii "Loading"
! 表示下面语句从地址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
398 .text
399 endtext:
400 .data
401 enddata:
402 .bss
403 endbss:
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.s和system已经由bootsect引导块加载到内存中。
!
! 这段代码询问bios有关内存/磁盘/其他参数,并将这些参数放到一个
! “安全的”地方:0x90000-0x901FF,也即原来bootsect代码块曾经在
! 的地方,然后在被缓冲块覆盖掉之前由保护模式的system读取。
15 ! NOTE! These had better be the same as in bootsect.s!
! 以下这些参数最好和bootsect.s中的相同!
16 #include <linux/config.h>
! config.h中定义了DEF_INITSEG = 0x9000;DEF_SYSSEG = 0x1000;DEF_SETUPSEG = 0x9020。
18 INITSEG = DEF_INITSEG ! we move boot here - out of the way ! 原来bootsect所处的段。
19 SYSSEG = DEF_SYSSEG ! system loaded at 0x10000 (65536). ! system在0x10000处。
20 SETUPSEG = DEF_SETUPSEG ! this is the current segment ! 本程序所在的段地址。
22 .globl begtext, begdata, begbss, endtext, enddata, endbss
23 .text
24 begtext:
25 .data
26 begdata:
27 .bss
28 begbss:
29 .text
31 entry start
32 start:
34 ! ok, the read went well so we get current cursor position and save it for
35 ! posterity.
! ok,整个读磁盘过程都正常,现在将光标位置保存以备今后使用(相关代码在59--62行)。
! 下句将ds置成INITSEG(0x9000)。这已经在bootsect程序中设置过,但是现在是setup程序,
! Linus觉得需要再重新设置一下。
37 mov ax,#INITSEG
38 mov ds,ax
40 ! Get memory size (extended mem, kB)
! 取扩展内存的大小值(KB)。
! 利用BIOS中断0x15 功能号ah = 0x88 取系统所含扩展内存大小并保存在内存0x90002处。
! 返回:ax = 从0x100000(1M)处开始的扩展内存大小(KB)。若出错则CF置位,ax = 出错码。
42 mov ah,#0x88
43 int 0x15
44 mov [2],ax ! 将扩展内存数值存在0x90002处(1个字)。
46 ! check for EGA/VGA and some config parameters
! 检查显示方式(EGA/VGA)并取参数。
! 调用BIOS中断0x10,附加功能选择方式信息。功能号:ah = 0x12,bl = 0x10
! 返回:bh =显示状态。0x00 -彩色模式,I/O端口=0x3dX;0x01 -单色模式,I/O端口=0x3bX。
! bl = 安装的显示内存。0x00 - 64k;0x01 - 128k;0x02 - 192k;0x03 = 256k。
! cx = 显示卡特性参数(参见程序后对BIOS视频中断0x10的说明)。
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 ! 保存屏幕当前行列值(0x9000E,0x9000F)。
! 这段代码使用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.
64 ! Get video-card data:
! 下面这段用于取显示卡当前显示模式。
! 调用BIOS中断0x10,功能号 ah = 0x0f
! 返回:ah = 字符列数;al = 显示模式;bh = 当前显示页。
! 0x90004(1字)存放当前页;0x90006存放显示模式;0x90007存放字符列数。
66 mov ah,#0x0f
67 int 0x10
68 mov [4],bx ! bh = display page
69 mov [6],ax ! al = video mode, ah = window width
71 ! Get hd0 data
! 取第一个硬盘的信息(复制硬盘参数表)。
! 第1个硬盘参数表的首地址竟然是中断向量0x41的向量值!而第2个硬盘参数表紧接在第1个
! 表的后面,中断向量0x46的向量值也指向第2个硬盘的参数表首址。表的长度是16个字节。
! 下面两段程序分别复制ROM BIOS中有关两个硬盘的参数表,0x90080处存放第1个硬盘的表,
! 0x90090处存放第2个硬盘的表。
! 第75行语句从内存指定位置处读取一个长指针值并放入ds和si寄存器中。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
83 ! Get hd1 data
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
95 ! Check that there IS a hd1 :-)
! 检查系统是否有第2个硬盘。如果没有则把第2个表清零。
! 利用BIOS中断调用0x13的取盘类型功能,功能号 ah = 0x15;
! 输入:dl = 驱动器号(0x8X是硬盘:0x80指第1个硬盘,0x81第2个硬盘)
! 输出:ah = 类型码;00 - 没有这个盘,CF置位;01 - 是软驱,没有change-line支持;
! 02 - 是软驱(或其他可移动设备),有change-line支持; 03 - 是硬盘。
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:
113 ! now we want to move to protected mode ...
! 现在我们要进入保护模式中了...
115 cli ! no interrupts allowed ! ! 从此开始不允许中断。
117 ! first we move the system to it's rightful place
! 首先我们将system模块移到正确的位置。
! bootsect引导程序会把 system 模块读入到内存 0x10000(64KB)开始的位置。由于当时假设
! system模块最大长度不会超过0x80000(512KB),即其末端不会超过内存地址0x90000,所以
! bootsect会把自己移动到0x90000开始的地方,并把setup加载到它的后面。下面这段程序的
! 用途是再把整个system模块移动到 0x00000位置,即把从 0x10000到0x8ffff 的内存数据块
! (512KB)整块地向内存低端移动了0x10000(64KB)的位置。
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
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行。
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寄存器。
142 ! that was painless, now we enable A20
! 以上的操作很简单,现在我们开启A20地址线。
! 为了能够访问和使用1MB以上的物理内存,我们需要首先开启A20地址线。参见本程序列表后
! 有关A20信号线的说明。关于所涉及的一些端口和命令,可参考kernel/chr_drv/keyboard.S
! 程序后对键盘接口的说明。至于机器是否真正开启了A20地址线,我们还需要在进入保护模式
! 之后(能访问1MB以上内存之后)在测试一下。这个工作放在了head.S程序中(32--36行)。
144 call empty_8042 ! 测试8042状态寄存器,等待输入缓冲器空。
! 只有当输入缓冲器为空时才可以对其执行写命令。
145 mov al,#0xD1 ! command write ! 0xD1命令码-表示要写数据到
146 out #0x64,al ! 8042的P2端口。P2端口位1用于A20线的选通。
147 call empty_8042 ! 等待输入缓冲器空,看命令是否被接受。
148 mov al,#0xDF ! A20 on ! 选通A20地址线的参数。
149 out #0x60,al ! 数据要写到0x60口。
150 call empty_8042 ! 若此时输入缓冲器为空,则表示A20线已经选通。
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机中搞糟了,以后也没有纠正过来。所以PC机BIOS把中断
! 放在了0x08--0x0f,这些中断也被用于内部硬件中断。所以我们就必须重新对8259
! 中断控制器进行编程,这一点都没意思。
!
! PC机使用2个8259A芯片,关于对可编程控制器8259A芯片的编程方法请参见本程序后的介绍。
! 第162行上定义的两个字(0x00eb)是直接使用机器码表示的两条相对跳转指令,起延时作用。
! 0xeb是直接近跳转指令的操作码,带1个字节的相对位移值。因此跳转范围是-127到127。CPU
! 通过把这个相对位移值加到EIP寄存器中就形成一个新的有效地址。此时EIP指向下一条被执行
! 的指令。执行时所花费的CPU时钟周期数是7至10个。0x00eb 表示跳转值是0的一条指令,因
! 此还是直接执行下一条指令。这两条指令共可提供14--20个CPU时钟周期的延迟时间。在as86
! 中没有表示相应指令的助记符,因此Linus在setup.s等一些汇编程序中就直接使用机器码来表
! 示这种指令。另外,每个空操作指令NOP的时钟周期数是3个,因此若要达到相同的延迟效果就
! 需要6至7个NOP指令。
! 8259芯片主片端口是0x20-0x21,从片端口是0xA0-0xA1。输出值0x11表示初始化命令开始,
! 它是ICW1命令字,表示边沿触发、多片8259级连、最后要发送ICW4命令字。
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 !屏蔽从芯片所有中断请求。
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处。
! 下面设置并进入32位保护模式运行。首先加载机器状态字(lmsw-Load Machine Status Word),
! 也称控制寄存器CR0,其比特位0置1将导致CPU切换到保护模式,并且运行在特权级0中,即
! 当前特权级CPL=0。此时段寄存器仍然指向与实地址模式中相同的线性地址处(在实地址模式下
! 线性地址与物理内存地址相同)。在设置该比特位后,随后一条指令必须是一条段间跳转指令以
! 用于刷新CPU当前指令队列。因为CPU是在执行一条指令之前就已从内存读取该指令并对其进行
! 解码。然而在进入保护模式以后那些属于实模式的预先取得的指令信息就变得不再有效。而一条
! 段间跳转指令就会刷新CPU的当前指令队列,即丢弃这些无效信息。另外,在Intel公司的手册
! 上建议80386或以上CPU应该使用指令“mov cr0,ax”切换到保护模式。lmsw指令仅用于兼容以
! 前的286 CPU。
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是描
! 述符表项的索引,指出选择第几项描述符。所以段选择符8(0b0000,0000,0000,1000)表示请求
! 特权级0、使用全局描述符表GDT中第2个段描述符项,该项指出代码的基地址是0(参见571行),
! 因此这里的跳转指令就会去执行system中的代码。另外,
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
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显示卡(若有的话)的子程序。若识别出一块就向用户
! 提供选择分辨率的机会,并把分辨率放入寄存器al、ah(行、列)中返回。
!
! 注意下面215--566行代码牵涉到众多显示卡端口信息,因此比较复杂。但由于这段代码与内核
! 运行关系不大,因此可以跳过不看。
! 下面首先显示588行上的msg1字符串("按<回车键>查看存在的SVGA模式,或按任意键继续"),
! 然后循环读取键盘控制器输出缓冲器,等待用户按键。如果用户按下回车键就去检查系统具有
! 的SVGA模式,并在AL和AH中返回最大行列值,否则设置默认值AL=25行、AH=80列并返回。
215 chsvga: cld
216 push ds ! 保存ds值。将在231行(或490或492行)弹出。
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卡可设置的行列个数和模式,
! 并跳转到标号selmod(438行)处进一步进行设置。
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 ! 和模式列表,然后跳转到selmod(438行)处继续处理。
242 jmp cx
! 现在来判断是不是Ahead牌子的显示卡。首先向EGA/VGA 图形索引寄存器0x3ce写入想访问的
! 主允许寄存器索引号0x0f,同时向0x3cf端口(此时对应主允许寄存器)写入开启扩展寄存器
! 标志值0x20。然后通过0x3cf端口读取主允许寄存器值,以检查是否可以设置开启扩展寄存器
! 标志。如果可以则说明是Ahead牌子的显示卡。注意word输出时alè端口n,ahè端口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 ! 数和扩展模式号列表。然后跳转到selmod(438行)处继
254 lea cx,selmod ! 续处理。
255 jmp cx
! 现在来检查是不是Chips & Tech生产的显示卡。通过端口0x3c3(0x94或0x46e8)设置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 ! 和扩展模式号列表。然后跳转到selmod(438行)进行设置
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 è0x3d4,ah è0x3d5。
318 ret
! 现在检查系统中是不是Everex显示卡。方法是利用中断int 0x10功能0x70(ax =0x7000,
! bx=0x0000)调用Everex的扩展视频BIOS功能。对于Everes类型显示卡,该中断调用应该
! 会返回模拟状态,即有以下返回信息:
! al = 0x70,若是基于Trident的Everex显示卡;
! cl = 显示器类型:00-单色;01-CGA;02-EGA;03-数字多频;04-PS/2;05-IBM 8514;06-SVGA。
! ch = 属性:位7-6:00-256K,01-512K,10-1MB,11-2MB;位4-开启VGA保护;位0-6845模拟。
! dx = 板卡型号:位15-4:板类型标识号;位3-0:板修正标识号。
! 0x2360-Ultragraphics II;0x6200-Vision VGA;0x6730-EVGA;0x6780-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中的特征数字串(0x77、0x00、0x66、
! 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卡上BIOS中0x37处的指针(它指向特征串)。
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 BIOS的0xc000: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
! 现在检查是不是Trident(TVGA)显示卡。TVGA显示卡扩充的模式控制寄存器1(0x3c4端口索引
! 的0x0e)的位3--0是64K内存页面个数值。这个字段值有一个特性:当写入时,我们需要首先把
! 值与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显示卡(ET4000AX或ET4000/W32类)。方法是对0x3cd端口对应的段
! 选择(Segment Select)寄存器执行读写操作。该寄存器高4位(位7--4)是要进行读操作的
! 64KB段号(Bank number),低4位(位3--0)是指定要写的段号。如果指定段选择寄存器的
! 的值是 0x55(表示读、写第6个64KB段),那么对于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索引寄存器端口0x3b4或0x3d4选择索引号是
421 mov al,#0x1f ! 0x1f的Video7显示卡标识寄存器。该寄存器内容实际上就是
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 - 0x8B;1 - 0x82;2 - 0x83;3 - 0x84;4 - 0x85;
! 5 - 0x86;6 - 0x87;7 - 0x88;8 - 0x89;9 - 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
496 ! Routine that 'tabs' to next col.
! 光标移动到下一制表位的子程序。
! 显示一个点字符'.'和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
510 ! Routine to print asciiz-string at DS:SI
! 显示位于DS:SI处以NULL(0x00)结尾的字符串。
512 prtstr: lodsb
513 and al,al
514 jz fin
515 call prnt1 ! 显示al中的一个字符。
516 jmp prtstr
517 fin: ret
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)。
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
540 ! Part of above routine, this one just prints ascii al
! 上面子程序的一部分。显示al中的一个字符。
! 该子程序使用中断0x10的0x0E功能,以电传方式在屏幕上写一个字符。光标会自动移到下一个
! 位置处。如果写完一行光标就会移动到下一行开始处。如果已经写完一屏最后一行,则整个屏幕
! 会向上滚动一行。字符0x07(BEL)、0x08(BS)、0x0A(LF)和0x0D(CR)被作为命令不会显示。
! 输入:AL -- 欲写字符;BH -- 显示页号;BL -- 前景显示色(图形方式时)。
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
552 ! Prints <CR> + <LF> ! 显示回车+换行。
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
! 全局描述符表开始处。描述符表由多个8字节长的描述符项组成。这里给出了3个描述符项。
! 第1项无用(568行),但须存在。第2项是系统代码段描述符(570-573行),第3项是系
! 统数据段描述符(575-578行)。
567 gdt:
568 .word 0,0,0,0 ! dummy ! 第1个描述符,不用。
! 在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 ! 颗粒度为4096,32位模式。
! 在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 ! 颗粒度为4096,32位模式。
! 下面是加载中断描述符表寄存器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
! 这是加载全局描述符表寄存器gdtr的指令lgdt要求的6字节操作数。前2字节是gdt表的限
! 长,后4字节是 gdt表的线性基地址。这里全局表长度设置为 2KB(0x7ff即可),因为每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
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
! 下面是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="
! 下面是各种显示卡可使用的扩展模式个数和对应的模式号列表。其中每一行第1个字节是模式个
! 数值,随后的一些值是中断0x10功能0(AH=0)可使用的模式号。例如从602行可知,对于ATI
! 牌子的显示卡,除了标准模式以外还可使用两种扩展模式:0x23和0x33。
600 ! Manufacturer: Numofmodes: Mode:
! 厂家: 模式数量: 模式列表:
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
! 下面是各种牌子VGA显示卡可使用的模式对应的列、行值列表。例如第615行表示ATI显示卡两
! 种扩展模式的列、行值分别是 132 x 25、 132 x 44。
613 ! msb = Cols lsb = Rows:
! 高字节=列数 低字节=行数:
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
626 .text
627 endtext:
628 .data
629 enddata:
630 .bss
631 endbss:
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.s中193行下的说明。这里$0x10的含义是请求
# 特权级0(位0-1=0)、选择全局描述符表(位2=0)、选择表中第2项(位3-15=2)。它正好指
# 向表中的数据段描述符项。(描述符的具体数值参见前面setup.s中212,213行)
# 下面代码的含义是:设置ds,es,fs,gs为setup.s 中构造的数据段(全局段描述符表第2项)
# 的选择符=0x10,并将堆栈放置在stack_start指向的user_stack数组区,然后使用本程序
# 后面定义的新中断描述符表和全局段描述表。新全局段描述表中初始内容与setup.s中的基本
# 一样,仅段限长从8MB修改成了16MB。stack_start定义在kernel/sched.c,69行。它是指向
# user_stack数组末端的一个长指针。第23行设置这里使用的栈,姑且称为系统栈。但在移动到
# 任务0执行(init/main.c中137行)以后该栈就被用作任务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.c,69行。
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.s行208-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',这里N是10个前向引用之一。上面
# '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 CPU中CR0控制寄存器的位16是写保护标志WP(Write-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存在与否。
*/
# 下面fninit和fstsw是数学协处理器(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的次方值了。使用该指示符的目的是为了提高32位CPU访问内存中代码
# 或数据的速度和效率。参见程序后的详细说明。
# 下面的两个字节值是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字节是一些标志。
# 这段代码首先在edx、eax中组合设置出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位存放一些标志,例如是否在内存中(P位0)、读写许可(R/W位1)、
# 普通用户还是超级用户使用(U/S位2)、是否修改过(是否脏了)(D位6)等;表项的位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值应该分别表示envp、argv指针和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所指内存位置处,且edi增4。
# 下面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 */
# 设置启动使用分页处理(cr0的PG标志,位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 # 按8(2^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 */ # 预留空间。
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()退出时就要使用堆栈了。
*
* 实际上只有pause和fork需要使用内嵌方式,以保证从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.h,133行。
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_arg和va_end),vsprintf、
// vprintf、vfprintf。
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.c,92行)。
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-2(setup程序读取并保存的参数)。
// 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_p和inb_p是include/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进制数,因此
// 一个字节表示2个10进制数。(val)&15取BCD表示的10进制个位数,而 (val)>>4取BCD表示
// 的10进制十位数,再乘以10。因此最后两者相加就是一个字节BCD码的实际二进制数值。
90 #define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)
91
// 该函数取CMOS实时钟信息作为开机时间,并保存到全局变量startup_time(秒)中。参见后面
// CMOS内存列表说明。其中调用的函数kernel_mktime()用于计算从1970年1月1日0时起到
// 开机当日经过的秒数,作为开机时间(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
// 内核初始化主程序。初始化结束后将以任务0(idle任务即空闲任务)的身份运行。
// 英文注释含义是“这里确实是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_DEV在include/linux/mm.h文件内也作了相同声明。这里mm.h文件并没有显式地列在
// 本程序前部,因为前面包含进的include/linux/sched.h文件中已经含有它。
133 ROOT_DEV = ORIG_ROOT_DEV; // ROOT_DEV定义在fs/super.c,29行。
134 SWAP_DEV = ORIG_SWAP_DEV; // SWAP_DEV定义在mm/swap.c,36行。
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; // 忽略不到4Kb(1页)的内存数。
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.c,399)
154 trap_init(); // 陷阱门(硬件中断向量)初始化。(kernel/traps.c,181)
155 blk_dev_init(); // 块设备初始化。 (blk_drv/ll_rw_blk.c,157)
156 chr_dev_init(); // 字符设备初始化。 (chr_drv/tty_io.c,347)
157 tty_init(); // tty初始化。 (chr_drv/tty_io.c,406)
158 time_init(); // 设置开机启动时间。(见第92行)
159 sched_init(); // 调度程序初始化(加载任务0的tr,ldtr)(kernel/sched.c,385)
160 buffer_init(buffer_memory_end); // 缓冲管理初始化,建内存链表等。(fs/buffer.c,348)
161 hd_init(); // 硬盘初始化。 (blk_drv/hd.c,343)
162 floppy_init(); // 软驱初始化。 (blk_drv/floppy.c,457)
163 sti(); // 所有初始化工作都做完了,于是开启中断。
// 下面过程通过在堆栈中设置的参数,利用中断返回指令启动任务0执行。
164 move_to_user_mode(); // 移到用户模式下执行。(include/asm/system.h,第1行)
165 if (!fork()) { /* we count on this going ok */
166 init(); // 在新建的子进程(任务1即init进程)中执行。
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()'将意味着我们必须等待收到一个信号
* 才会返回就绪态,但任务0(task0)是唯一例外情况(参见'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()函数
// 运行在任务0第1次创建的子进程(任务1)中。它首先对第一个将要执行的程序(shell)
// 的环境进行初始化,然后以登录shell方式加载该程序并执行之。
190 void init(void)
191 {
192 int pid,i;
193
// setup() 是一个系统调用。用于读取硬盘参数包括分区表信息并加载虚拟盘(若存在的话)和
// 安装根文件系统设备。该函数用25行上的宏定义,对应函数是sys_setup(),在块设备子目录
// kernel/blk_drv/hd.c,74行。
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行是子进程执行的内容。该子
// 进程关闭了句柄0(stdin)、以只读方式打开/etc/rc文件,并使用execve()函数将进程自身
// 替换成 /bin/sh程序(即shell程序),然后执行 /bin/sh程序。所携带的参数和环境变量分
// 别由argv_rc和envp_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,并复制成 stdout和 stderr。再次执行系统解释程序
// /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
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 -- 处理被零除出错的情况。 类型:错误;出错号:无。
# 在执行DIV或IDIV指令时,若除数是0,CPU就会产生这个异常。当EAX(或AX、AL)容纳
# 不了一个合法除操作的结果时,也会产生这个异常。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的地址 è eax,eax被交换入栈。
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 # 初始化段寄存器ds、es和fs,加载内核数据段选择符。
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);无错误号。
# 当eflags中TF标志置位时而引发的中断。当发现硬件断点(数据:陷阱,代码:错误);或者
# 开启了指令跟踪陷阱或任务交换陷阱,或者调试寄存器访问无效(错误),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 -- 溢出出错处理中断入口点。 类型:陷阱;无错误号。
# EFLAGS中OF标志置位时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是协处理端口,用于清忙锁存器。通过写
# 该端口,本中断将消除CPU的BUSY延续信号,并重新激活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 <-> %eax,eax原来的值被保存在堆栈上。
102 xchgl %ebx,(%esp) # &function <-> %ebx,ebx原来的值被保存在堆栈上。
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 -- 堆栈段错误。 类型:错误;有出错码。
# 指令操作试图超出堆栈段范围,或者堆栈段不在内存中。这是异常11和13的特例。有些操作
# 系统可以利用这个异常来确定什么时候应该为程序分配更多的栈空间。
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.s,158行。
# int14 -- 页错误(_page_fault)在mm/page.s,14行。
# int16 -- 协处理器错误(_coprocessor_error)在kernel/sys_call.s,140行。
# 时钟中断int 0x20(_timer_interrupt)在kernel/sys_call.s,189行。
# 系统调用int 0x80(_system_call)在kernel/sys_call.s,84行。
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_fault(mm/page.s,14)。
40
41 void divide_error(void); // int0(kernel/asm.s,20)。
42 void debug(void); // int1(kernel/asm.s,54)。
43 void nmi(void); // int2(kernel/asm.s,58)。
44 void int3(void); // int3(kernel/asm.s,62)。
45 void overflow(void); // int4(kernel/asm.s,66)。
46 void bounds(void); // int5(kernel/asm.s,70)。
47 void invalid_op(void); // int6(kernel/asm.s,74)。
48 void device_not_available(void); // int7(kernel/sys_call.s,158)。
49 void double_fault(void); // int8(kernel/asm.s,98)。
50 void coprocessor_segment_overrun(void); // int9(kernel/asm.s,78)。
51 void invalid_TSS(void); // int10(kernel/asm.s,132)。
52 void segment_not_present(void); // int11(kernel/asm.s,136)。
53 void stack_segment(void); // int12(kernel/asm.s,140)。
54 void general_protection(void); // int13(kernel/asm.s,144)。
55 void page_fault(void); // int14(mm/page.s,14)。
56 void coprocessor_error(void); // int16(kernel/sys_call.s,140)。
57 void reserved(void); // int15(kernel/asm.s,82)。
58 void parallel_interrupt(void); // int39(kernel/sys_call.s,295)。
59 void irq13(void); // int45 协处理器中断处理(kernel/asm.s,86)。
60 void alignment_check(void); // int46(kernel/asm.s,148)。
61
// 该子程序用来打印出错中断的名称、出错号、调用程序的EIP、EFLAGS、ESP、fs段寄存器值、
// 段的基址、段的长度、进程号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:EIP、EFLAGS和SS: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]是原ss,esp[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.h,210行)。
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,÷_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);
// 设置协处理器中断0x2d(45)陷阱门描述符,并允许其产生中断请求。设置并行口中断描述符。
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,¶llel_interrupt); // 设置并行口1的中断0x27陷阱门描述符。
213 }
214
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.h,105行开始。
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.c,119行处开始。
# 当调度程序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 0x80,eax中是调用号)。
83 .align 2
84 _system_call:
85 push %ds # 保存原段寄存器值。
86 push %es
87 push %fs
88 pushl %eax # save the orig_eax # 保存eax原值。
# 一个系统调用最多可带有3个参数,也可以不带参数。下面入栈的ebx、ecx和edx中放着系统
# 调用相应C语言函数(见第99行)的调用参数。这几个寄存器入栈的顺序是由GNU gcc规定的,
# ebx中可存放第1个参数,ecx中存放第2个参数,edx中存放第3个参数。
# 系统调用语句可参见头文件include/unistd.h中第150到200行的系统调用宏。
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),则也去执行调度程序。
# 例如当后台进程组中的进程执行控制终端读写操作时,那么默认条件下该后台进程组所有进程
# 会收到SIGTTIN或SIGTTOU信号,导致进程组中所有进程处于停止状态。而当前进程则会立刻
# 返回。
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处退出中断处理。
# 通过对原调用程序代码选择符的检查来判断调用程序是否是用户任务。如果不是则直接退出中断。
# 这是因为任务在内核态执行时不可抢占。否则对任务进行信号量的识别处理。这里比较选择符是
# 否为用户代码段的选择符 0x000f(RPL=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.c,128)。
126 popl %ecx # 弹出入栈的信号值。
127 testl %eax, %eax # 测试返回值,若不为0则跳转到前面标号2(101行)处。
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.c,11)。
156
#### int7 -- 设备不存在或协处理器不存在。 类型:错误;无错误码。
# 如果控制寄存器 CR0 中EM(模拟)标志置位,则当CPU 执行一个协处理器指令时就会引发该
# 中断,这样CPU就可以有机会让这个中断处理程序模拟协处理器指令(181行)。
# CR0的交换标志TS是在 CPU执行任务转换时设置的。TS 可以用来确定什么时候协处理器中的
# 内容与CPU 正在执行的任务不匹配了。当CPU 在运行一个协处理器转义指令时发现TS置位时,
# 就会引发该中断。此时就可以保存前一个任务的协处理器内容,并恢复新任务的协处理器执行
# 状态(176行)。参见kernel/sched.c,92行。该中断最后将转移到标号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.c,92行)。
# 若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.c,476行)。
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)处初始化的。因此这里jiffies每10毫秒加1。
# 这段代码将jiffies增1,发送结束中断指令给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 # 保存ds、es并让其指向内核数据段。fs将用于system_call。
193 pushl $-1 # fill in -1 for orig_eax # 填-1,表明不是系统调用。
# 下面我们保存寄存器eax、ecx和edx。这是因为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段寄存器值)中的当前特权级别(0或3)并压入
# 堆栈,作为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.c,207行。
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.c,143)。
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.c,68)。
232 addl $20,%esp # 丢弃这里所有压栈内容。
233 1: ret
234
#### int 46 -- (int 0x2E) 硬盘中断处理程序,响应硬件中断请求IRQ14。
# 当请求的硬盘操作完成或出错就会发出此中断信号。(参见kernel/blk_drv/hd.c)。
# 首先向8259A中断控制从芯片发送结束硬件中断指令(EOI),然后取变量do_hd中的函数指针放入edx
# 寄存器中,并置do_hd为NULL,接着判断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_floppy为NULL,接着判断eax函数指针是否为空。如为空,则给eax赋值指向
# unexpected_floppy_interrupt (),用于显示出错信息。随后调用eax指向的函数: rw_interrupt,
# seek_interrupt,recal_interrupt,reset_interrupt或unexpected_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
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
// 该函数计算从1970年1月1日0时起到开机当日经过的秒数,作为开机时间。
// 参数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年(71,72,73)
// 就是第1个闰年,这样从1970年开始的闰年数计算方法就应该是为1 + (y - 3)/4,即为
// (y + 1)/4。res = 这些年经过的秒数时间 + 每个闰年时多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
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_on、wakeup、schedule等)
* 以及一些简单的系统调用函数(比如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))
// 除了SIGKILL和SIGSTOP信号以外其他信号都是可阻塞的(…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
// PC机8253定时芯片的输入时钟频率约为1.193180MHz。Linux内核希望定时器发出中断的频率是
// 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.s,176)。
60 extern int system_call(void); // 系统调用中断处理程序(kernel/system_call.s,80)。
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之前它是内核栈,以后用作任务0和1的用
// 户态栈。下面结构用于设置堆栈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.h第139行。
133 if ((*p)->alarm && (*p)->alarm < jiffies) {
134 (*p)->signal |= (1<<(SIGALRM-1));
135 (*p)->alarm = 0;
136 }
// 如果信号位图中除被阻塞的信号外还有其他信号,并且任务处于可中断状态,则置任务为就绪
// 状态。其中'~(_BLOCKABLE & (*p)->blocked)'用于忽略被阻塞的信号,但SIGKILL和SIGSTOP
// 不能被阻塞。
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
// 仍然为-1,next=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_UNINTERRUPTIBLE或TASK_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 - 关闭。
// 位3 :1 - 允许DMA和中断请求;0 - 禁止DMA和中断请求。
// 位2 :1 - 启动软盘控制器; 0 - 复位软盘控制器。
// 位1-0:00 - 11,用于选择控制的软驱A-D。
// 这里设置初值为:允许DMA和中断请求、启动FDC。
223 unsigned char current_DOR = 0x0C;
224
// 指定软驱启动到正常运转状态所需等待时间。
// 参数nr -- 软驱号(0--3),返回值为滴答数。
// 局部变量selected是选中软驱标志(blk_drv/floppy.c,123行)。mask是所选软驱对应的
// 数字输出寄存器中启动马达比特位。mask高4位是各软驱启动马达标志。
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_interrupt(189行)被调用。
// 参数cpl是当前特权级0或3,是时钟中断发生时正被执行的代码选择符中的特权级。
// cpl=0时表示中断发生时正在执行内核代码;cpl=3时表示中断发生时正在执行用户代码。
// 对于一个进程由于执行时间片用完时,则进行任务切换。并执行一个计时更新工作。
324 void do_timer(long cpl)
325 {
326 static int blanked = 0;
327
// 首先判断是否经过了一定时间而让屏幕黑屏(blankout)。如果blankcount计数不为零,
// 或者黑屏延时间隔时间blankinterval为0的话,那么若已经处于黑屏状态(黑屏标志
// 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/hdc,318行)。
341
// 如果发声计数次数到,则关闭发声。(向0x61口发送命令,复位位0和1。位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.s,207行
// 上的英文注释。这种称呼来自于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