编程环境:Ubuntu Kylin 16.04、gcc-5.4.0
代码仓库:https://gitee.com/AprilSloan/linux0.11-project
linux0.11源码下载(不能直接编译,需进行修改)
本章目标
完成系统的部分初始化操作:内存管理的初始化、缓冲管理初始化、时钟初始化和协处理器的检查。为以后管理系统资源做准备。
1.内存管理的初始化
在head.s中我们已经设置了页目录和页表,现在要对内存的使用做出进一步的规定。
对于linux0.11内核,它默认最多支持16M物理内存。在一个具有16MB内存的系统中,linux内核占用物理内存最前端的一部分,图中_end表示内核模块的结束位置。随后是高速缓冲区,它的最高内存地址为4M。剩余的内存部分称为主存储区。当需要使用主内存区时就需要向内存管理程序申请,所申请的基本单位是内存页(一般大小位4KB)。整个物理内存各部分功能示意图如下图所示。
下面来看看main.c的代码。
#include <linux/tty.h>
#include <asm/system.h>
extern void mem_init(long start, long end);
extern int printk(const char *fmt, ...);
#define EXT_MEM_K (*(unsigned short *)0x90002)
static long memory_end = 0;
static long buffer_memory_end = 0;
static long main_memory_start = 0;
void main(void)
{
memory_end = (1 << 20) + (EXT_MEM_K << 10);
memory_end &= 0xfffff000; // 4KB对齐
if (memory_end > 16 * 1024 * 1024)
memory_end = 16 * 1024 * 1024;
if (memory_end > 12 * 1024 * 1024)
buffer_memory_end = 4 * 1024 * 1024;
else if (memory_end > 6 * 1024 * 1024)
buffer_memory_end = 2 * 1024 * 1024;
else
buffer_memory_end = 1 * 1024 * 1024;
main_memory_start = buffer_memory_end;
mem_init(main_memory_start, memory_end);
tty_init();
printk("memory_end = %x\n\r", memory_end);
cli();
while (1);
}
memory_end代表内存大小,其值等于1MB+扩展内存(KB)*1024。扩展内存的值在上一章中被保存在0x90002地址中,在我的机器上,它的值为0xFC00。所以memory_end的值应该是0x4000000(64MB)。之后将memory_end进行4KB对齐,因为我们分配的页面都是4KB的。
如果memory_end大于16MB,就把memory_end设置为16MB,这是为什么?还记得我们在head.s中设置的页表吗,我们一共设置了4个页表,能够管理16MB的内存空间,大于16MB的内存空间就管理不了了。
缓冲区的大小随内存大小而变化,在我的机器上把它的上限设置为4MB的。缓冲区主要用于存放文件系统的数据,下一节我们就会对缓冲区进行初始化。如上图所示,缓冲区结束的地方,就是主内存区开始的地方。
接下来就是内存管理的初始化操作,下面是memory.c的内容。
#define LOW_MEM 0x100000
#define PAGING_MEMORY (15*1024*1024)
#define PAGING_PAGES (PAGING_MEMORY>>12)
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)
#define USED 100
static long HIGH_MEMORY = 0;
static unsigned char mem_map[PAGING_PAGES] = {0, }; // 记录内存页面的使用情况
void mem_init(long start_mem, long end_mem)
{
int i;
HIGH_MEMORY = end_mem;
for (i = 0; i < PAGING_PAGES; i++)
mem_map[i] = USED;
i = MAP_NR(start_mem);
end_mem -= start_mem;
end_mem >>= 12; // 可分配内存的总页面数
while (end_mem-- >0)
mem_map[i++]=0;
}
PAGING_MEMORY为15MB代表我们只管理后面15MB内存,因为前面1MB的内容要么是代码,要么是缓冲区的数据结构,要么是显存和BIOS ROM,都是不能用的。PAGING_PAGES代表这15MB内存的页面数,每页大小4KB。mem_map字符数组用于记录了内存页面的使用情况,每一个字符对应一个页面,0代表未使用。
第16-22行代码相当于把缓冲区的页面都标记为已使用,把主内存区的页面标记为未使用。
到此,内存管理初始化的代码就结束了,这一节的代码不多,应该不难理解。下面,我们将要对代码进行整理。之前我把一些宏定义和用户栈放在main.c中,现在要把它们放在别的地方。
首先是mm.h,将页面大小的宏定义移至此处。
#ifndef _MM_H_
#define _MM_H_
#define PAGE_SIZE 4096
#endif
然后是sched.h,这个文件只是包含mm.h。
#ifndef _SCHED_H_
#define _SCHED_H_
#include <linux/mm.h>
#endif
接着是sched.c,这个文件用来写进程管理相关函数,现在就只是定义用户栈。
#include <linux/sched.h>
long user_stack[PAGE_SIZE >> 2];
struct {
long * a;
short b;
} stack_start = {&user_stack[PAGE_SIZE >> 2], 0x10};
添加了新的C文件自然要修改Makefile。先是要添加mm目录下的Makefile。
CC =gcc -march=i386
CFLAGS =-Wall -O -m32 -fstrength-reduce -fomit-frame-pointer -nostdinc -I../include -fno-stack-protector
AS =as --32
AR =ar
LD =ld -m elf_i386
CPP =gcc -E -nostdinc -I../include
.c.o:
$(CC) $(CFLAGS) \
-c -o $*.o $<
.s.o:
$(AS) -o $*.o $<
.c.s:
$(CC) $(CFLAGS) \
-S -o $*.s $<
OBJS = memory.o
all: mm.o
mm.o: $(OBJS)
$(LD) -r -o mm.o $(OBJS)
clean:
rm -f *.o *.a
for i in *.c;do rm -f `basename $$i .c`.s;done
memory.o: memory.c
kernel目录的Makefile要添加文件。
OBJS =sched.o printk.o vsprintf.o
还有主目录下的Makefile。
ARCHIVES=kernel/kernel.o mm/mm.o
mm/mm.o:
(cd mm; make)
clean:
rm -rf boot/*.bin boot/*.o init/*.o System.map system kernel.img
(cd mm;make clean)
(cd kernel;make clean)
因为只是初始化操作,没什么可以显示的,就在main函数中显示内存的大小。
0x1000000就是16MB,程序也没报什么错,就算memory.c的逻辑真的有问题,现在也看不出来ㄟ( ▔, ▔ )ㄏ。
2.缓冲区初始化
对缓冲区初始化主要就是初始化缓冲头结构体内的值,让我们看看缓冲头结构体的结构。以下是fs.h的代码。
#ifndef _FS_H_
#define _FS_H_
#include <sys/types.h>
void buffer_init(long buffer_end);
#define NR_HASH 307
#define NR_BUFFERS nr_buffers
#define BLOCK_SIZE 1024
struct buffer_head {
char *b_data; // 指向数据块(1KB)的指针
unsigned long b_blocknr; // 块号
unsigned short b_dev; // 设备号,0-空闲
unsigned char b_uptodate; // 是否更新
unsigned char b_dirt; // 脏位,0-未修改,1-修改过
unsigned char b_count; // 使用的用户数
unsigned char b_lock; // 0 - 未上锁,1 - 上锁
struct buffer_head *b_prev; // hash队列的上一块
struct buffer_head *b_next; // hash队列的下一块
struct buffer_head *b_prev_free; // 空闲表上一块
struct buffer_head *b_next_free; // 空闲表下一块
};
#endif
看到这个结构体是不是有点蒙?想问问这玩意儿到底是干什么用的?
首先,我们得明白缓冲区是干什么的。缓冲区会被划分为两部分,前一部分用来保存上面这个结构体,后一部分是一个个1KB大小的数据块,b_data指针就指向这些数据块。那这些数据是用来放什么东西的呢?块设备的数据,再说简单点就是软盘(硬盘)的文件内容。hash队列用于快速寻找缓冲头结构体,空闲链表用于快速找到未被使用的缓冲头结构体,其他的变量看名字都应该能猜到它们的作用,之后在做介绍吧。
另外,在fs.h中还定义了hash数组的数量为307,数据块大小为1024字节。
好了,现在就可以看看初始化操作了。以下是buffer.c的内容。
#include <linux/sched.h>
extern int _end;
struct buffer_head *start_buffer = (struct buffer_head *) &_end;
struct buffer_head *hash_table[NR_HASH];
static struct buffer_head *free_list;
int NR_BUFFERS = 0;
void buffer_init(long buffer_end)
{
struct buffer_head *h = start_buffer;
void *b;
int i;
if (buffer_end == 1 << 20)
b = (void *) (640 * 1024);
else
b = (void *) buffer_end;
while ((b -= BLOCK_SIZE) >= ((void *) (h + 1))) {
h->b_dev = 0;
h->b_dirt = 0;
h->b_count = 0;
h->b_lock = 0;
h->b_uptodate = 0;
h->b_next = NULL;
h->b_prev = NULL;
h->b_data = (char *) b;
h->b_prev_free = h - 1;
h->b_next_free = h + 1;
h++;
NR_BUFFERS++;
if (b == (void *) 0x100000) // 0xA0000-0x100000是显存和BIOS ROM
b = (void *) 0xA0000; // 跳过这一段
}
h--;
free_list = start_buffer;
free_list->b_prev_free = h;
h->b_next_free = free_list;
for (i = 0; i < NR_HASH; i++)
hash_table[i] = NULL;
}
在文件开头,我把缓冲区的起始地址(start_buffer)设置为_end
,_end
是什么呢?可以看看kernel.lds(不知道你是否还记得这个文件),它是链接脚本,我们在这里定义了一些变量,而_end
代表内核的结束地址,可以看看System.map(这个文件在make后才会有),_end
在这个文件的最后。另外还定义了缓冲头结构体哈希数组和空闲链表。
再看看buffer_init函数。这个函数需要讲的地方也就第20-35行。b代表数据块的起始地址,一个数据块大小为1KB,h代表缓冲头结构体的起始地址,缓冲头结构体大小为32B。第20行代码相当于把 b 减了1KB再与 h+1 向比较,为什么与 h+1 而不是与 h 进行比较呢?请看下图。
从这个例子可以看出,b 与 h+1 比较是为了防止缓冲块起始地址落在缓冲头结构体中。
对于每个缓冲头结构体,将其空闲链表指针指向的上一个和下一个缓冲头结构体。跳过0xA0000-0x100000这段内存地址,因为这里面是显存和BIOS ROM。NR_BUFFERS保存缓冲头结构体个数。在循环结束后,缓冲头结构体与缓冲块的连接情况如下如所示。
当h = start_buffer
时,h->b_prev_free = h - 1
明显时不正确的,第37-38行代码将这个错误纠正,第39行代码将所有缓冲头结构体形成一个循环链表。
最后将所有哈希数组指针设置为NULL。这样buffer.c就讲解完毕了。
在buffer.c中,我们使用了NULL,它的定义在types中,在这个文件中设置了不少重定义。
#ifndef _SYS_TYPES_H
#define _SYS_TYPES_H
#ifndef _SIZE_T
#define _SIZE_T
typedef unsigned int size_t;
#endif
#ifndef _TIME_T
#define _TIME_T
typedef long time_t;
#endif
#ifndef _PTRDIFF_T
#define _PTRDIFF_T
typedef long ptrdiff_t;
#endif
#ifndef NULL
#define NULL ((void *) 0)
#endif
typedef int pid_t;
typedef unsigned short uid_t;
typedef unsigned char gid_t;
typedef unsigned short dev_t;
typedef unsigned short ino_t;
typedef unsigned short mode_t;
typedef unsigned short umode_t;
typedef unsigned char nlink_t;
typedef int daddr_t;
typedef long off_t;
typedef unsigned char u_char;
typedef unsigned short ushort;
typedef struct { int quot,rem; } div_t;
typedef struct { long quot,rem; } ldiv_t;
struct ustat {
daddr_t f_tfree;
ino_t f_tinode;
char f_fname[6];
char f_fpack[6];
};
#endif
我们还需要在sched.h包含fs.h,让buffer.c能够使用fs.h中的结构体。
#ifndef _SCHED_H_
#define _SCHED_H_
#include <linux/fs.h>
#include <linux/mm.h>
#endif
现在到了修改Makefile的环节。
先是要添加fs目录下的Makefile。
AR =ar
AS =as --32
CC =gcc -march=i386
LD =ld -m elf_i386
CFLAGS =-Wall -m32 -fstrength-reduce -fno-stack-protector -fomit-frame-pointer -nostdinc -I../include
CPP =gcc -E -nostdinc -I../include
.c.s:
$(CC) $(CFLAGS) \
-S -o $*.s $<
.c.o:
$(CC) $(CFLAGS) \
-c -o $*.o $<
.s.o:
$(AS) -o $*.o $<
OBJS= buffer.o
fs.o: $(OBJS)
$(LD) -r -o fs.o $(OBJS)
clean:
-rm -f *.o *.a
for i in *.c;do rm -f `basename $$i .c`.s;done
buffer.o: buffer.c
再在主目录的Makefile中添加关于fs目录的编译规则。
ARCHIVES=kernel/kernel.o mm/mm.o fs/fs.o
fs/fs.o:
(cd fs; make)
clean:
rm -rf boot/*.bin boot/*.o init/*.o System.map system kernel.img
(cd mm;make clean)
(cd fs;make clean)
(cd kernel;make clean)
最后,需要在main函数中调用buffer_init函数。初始化操作没什么可展示的,只能打印一下缓冲区的结束地址。
#include <linux/sched.h>
void main(void)
{
memory_end = (1 << 20) + (EXT_MEM_K << 10);
memory_end &= 0xfffff000;
if (memory_end > 16 * 1024 * 1024)
memory_end = 16 * 1024 * 1024;
if (memory_end > 12 * 1024 * 1024)
buffer_memory_end = 4 * 1024 * 1024;
else if (memory_end > 6 * 1024 * 1024)
buffer_memory_end = 2 * 1024 * 1024;
else
buffer_memory_end = 1 * 1024 * 1024;
main_memory_start = buffer_memory_end;
mem_init(main_memory_start, memory_end);
tty_init();
buffer_init(buffer_memory_end);
printk("buffer_memory_end = 0x%x\n\r", buffer_memory_end);
cli();
while (1);
}
没什么大问题,程序逻辑有没有错只有后面用到的时候才知道了。
3.时钟初始化
前面两节的内容分别为内存管理和文件系统做了准备,这一章是为进程管理做准备。这么说,你可能以为还是初始化进程控制块(也称任务控制块),任务控制块的初始化会在任务管理的章节进行,我们这节要做的是初始化时钟。
时钟与进程有什么关系呢?为了高效利用计算机资源和处理任务,linux采用并发的方式执行任务,这样看起来所有任务就像同时运行一般,但其实每个进程每次只运行了几毫秒,我们需要知道进程实际的运行时间,以便更好地进行任务调度,这里就需要用到时钟。
首先需要获得开机时间,所谓开机时间,就是从1970年1月1日0时到开机的秒数,之后可以通过开机时间加上一个时间偏移得到当前时间。
在sched.c中定义一个变量startup_time
,表示开机时间。
#include <linux/sched.h>
long startup_time = 0;
long user_stack[PAGE_SIZE >> 2];
struct {
long * a;
short b;
} stack_start = {&user_stack[PAGE_SIZE >> 2], 0x10};
然后在sched.h中引进这个变量,方便在其它文件中使用这个变量。
#ifndef _SCHED_H_
#define _SCHED_H_
#include <linux/fs.h>
#include <linux/mm.h>
extern long startup_time;
#endif
让我们回到main函数看看怎么读取时间。
#include <time.h>
#include <linux/tty.h>
#include <linux/sched.h>
#include <asm/system.h>
#include <asm/io.h>
extern void mem_init(long start, long end);
extern long kernel_mktime(struct tm *tm);
extern int printk(const char *fmt, ...);
#define EXT_MEM_K (*(unsigned short *)0x90002)
#define CMOS_READ(addr) ({ \
outb_p(addr, 0x70); \
inb_p(0x71); \
})
#define BCD_TO_BIN(val) ((val) = ((val) & 15) + ((val) >> 4) * 10)
static void time_init(void)
{
struct tm time;
do {
time.tm_sec = CMOS_READ(0);
time.tm_min = CMOS_READ(2);
time.tm_hour = CMOS_READ(4);
time.tm_mday = CMOS_READ(7);
time.tm_mon = CMOS_READ(8);
time.tm_year = CMOS_READ(9);
} while (time.tm_sec != CMOS_READ(0));
BCD_TO_BIN(time.tm_sec);
BCD_TO_BIN(time.tm_min);
BCD_TO_BIN(time.tm_hour);
BCD_TO_BIN(time.tm_mday);
BCD_TO_BIN(time.tm_mon);
BCD_TO_BIN(time.tm_year);
printk("time.tm_year = %d\n\r", time.tm_year);
printk("time.tm_mon = %d\n\r", time.tm_mon);
printk("time.tm_mday = %d\n\r", time.tm_mday);
printk("time.tm_hour = %d\n\r", time.tm_hour);
printk("time.tm_min = %d\n\r", time.tm_min);
printk("time.tm_sec = %d\n\r", time.tm_sec);
time.tm_mon--; // 月份原本的数值范围为1-12,现在变为0-11
startup_time = kernel_mktime(&time);
}
static long memory_end = 0;
static long buffer_memory_end = 0;
static long main_memory_start = 0;
void main(void)
{
memory_end = (1 << 20) + (EXT_MEM_K << 10);
memory_end &= 0xfffff000;
if (memory_end > 16 * 1024 * 1024)
memory_end = 16 * 1024 * 1024;
if (memory_end > 12 * 1024 * 1024)
buffer_memory_end = 4 * 1024 * 1024;
else if (memory_end > 6 * 1024 * 1024)
buffer_memory_end = 2 * 1024 * 1024;
else
buffer_memory_end = 1 * 1024 * 1024;
main_memory_start = buffer_memory_end;
mem_init(main_memory_start, memory_end);
tty_init();
time_init(); // 洛杉矶时间
buffer_init(buffer_memory_end);
cli();
while (1);
}
要说读取时间当然是用我们万能的端口。将读取的时间保存到结构体中,看到结构体中变量的名字就应该知道它的含义了吧。
如上图所示,将CMOS地址写入0x70端口,然后读0x71端口得到数据。地址0、2、4、7、8、9分别代表秒、分、时、日、月、年。
CMOS_READ(addr)
函数将addr的值传入0x70端口,返回0x71端口的数据。所以通过26-31行代码,就可以获得时间。但是这里的时间采用BCD码表示,(BCD码)0x12=12(十进制),(BCD码)0x24=24(十进制),可以看到,BCD码与十进制数在数字上相同时,数值上也是相等的,而且BCD码不会使用 abcdef 这些字母。通过BCD_TO_BIN
函数将BCD码转换成十进制。第39-44行会将时间打印到屏幕上。
月份原本的数值范围为1-12,现在减1变为0-11,之后会用月份作为数组索引。
在CMOS_READ中用到了inb_p函数,它的定义在io.h中。
#define inb_p(port) ({ \
unsigned char _v; \
__asm__ volatile ("inb %%dx,%%al\n" \
"\tjmp 1f\n" \
"1:\tjmp 1f\n" \
"1:":"=a" (_v):"d" (port)); \
_v; \
})
这段代码和outb_p很相似。它在读取端口后会用jmp延时一会然后再返回数据。
kernel_mktime定义在mktime.c中。
#include <time.h>
#define MINUTE 60
#define HOUR (60 * MINUTE)
#define DAY (24 * HOUR)
#define YEAR (365 * DAY)
// 假设是闰年
static int month[12] = {
0,
DAY * (31),
DAY * (31 + 29),
DAY * (31 + 29 + 31),
DAY * (31 + 29 + 31 + 30),
DAY * (31 + 29 + 31 + 30 + 31),
DAY * (31 + 29 + 31 + 30 + 31 + 30),
DAY * (31 + 29 + 31 + 30 + 31 + 30 + 31),
DAY * (31 + 29 + 31 + 30 + 31 + 30 + 31 + 31),
DAY * (31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30),
DAY * (31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31),
DAY * (31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30)
};
// 返回从1970年1月1日0时到开机经过的秒数作为开机时间
long kernel_mktime(struct tm *tm)
{
long res;
int year;
year = tm->tm_year - 70; // 从1970年到现在经过的年数
// 为了获得正确地闰年数,需要使用year+1
res = YEAR * year + DAY * ((year + 1) / 4); // 每有一个闰年多加一天
res += month[tm->tm_mon];
// 如果是3月或以上,而且不是闰年,需要调整时间
if (tm->tm_mon > 1 && ((year + 2) % 4))
res -= DAY;
res += DAY * (tm->tm_mday - 1);
res += HOUR * tm->tm_hour;
res += MINUTE * tm->tm_min;
res += tm->tm_sec;
return res;
}
我们需要把年月日这些信息转换成秒,最难计算的是总共的天数,天数转秒数只需要乘以3600,而年月日转天数需要考虑闰年,导致计算变得很复杂。这里的计算方法还是比较简单的。
第32行代码计算本年以前的天数。先假设所有年份都不是闰年,其总天数再加上闰年数得到所有的天数。这里的year+1应该怎么理解呢?举个例子,1972年是闰年,但1970-1972以前没有闰年,闰年数为0,(year + 1) / 4
为0;1973年不是闰年,但在它之前有1个闰年,(year + 1) / 4
为1。这样处理后恰好得到一个正确的闰年数。
第33-36行代码计算本月以前的天数。从month
数组可以看出,先假设本年是闰年,如果本年不是闰年而且本月是三月或以后,就减去1天,如果是一二月,闰不闰年无所谓,不影响天数的计算。
之后的代码很简单就不多说了。
再来看看time.h吧。
#ifndef _TIME_H
#define _TIME_H
#ifndef _TIME_T
#define _TIME_T
typedef long time_t;
#endif
#ifndef _SIZE_T
#define _SIZE_T
typedef unsigned int size_t;
#endif
#define CLOCKS_PER_SEC 100
typedef long clock_t;
struct tm {
int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year;
int tm_wday;
int tm_yday;
int tm_isdst;
};
time_t mktime(struct tm * tp);
#endif
这个文件主要定义了时间结构体,也有一些重定义,不过现在用不到也就看看,不多说了。
最后还是要改改kernel目录下Makefile,添加mktime.c的编译规则
OBJS =sched.o printk.o vsprintf.o mktime.o
好了,运行内核。
这个时间好像有点不对啊,和电脑时间对不上。我一开始以为这是格林尼治时间,对比了发现不是,后来与各个城市的时间对比,发现这应该是洛杉矶时间。可为什么是洛杉矶时间呢?这就问到我知识的盲区了,还请大家自己找答案吧ε=ε=ε=( ̄▽ ̄)
4.协处理器检查
其实协处理器这一块可有可无,毕竟linux0.11本身功能并不强,没有设备管理,也没什么特殊的任务给协处理器,但是我觉得这篇博客的长度还不够,加上这一节凑凑字数。这节的内容也不算难。
这节添加的代码都在head.s中。
startup_32:
movl $0x10, %eax
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
lss stack_start, %esp
call setup_idt
call setup_gdt
movl $0x10, %eax
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
lss stack_start, %esp
xorl %eax, %eax
1: incl %eax # 检查是否开启A20
movl %eax, 0x0
cmpl %eax, 0x100000
je 1b
movl %cr0, %eax
andl $0x80000011, %eax # Save PG,PE,ET
orl $2, %eax # set MP
movl %eax, %cr0
call check_x87
jmp after_page_tables
check_x87:
fninit # 初始化命令
fstsw %ax # 取协处理器的状态字
cmpb $0, %al
je 1f
movl %cr0, %eax
xorl $6, %eax # reset MP, set EM
movl %eax, %cr0
ret
.align 4
1: .byte 0xDB, 0xE4 # fsetpm for 287, ignored by 387
ret
setup_idt:
lidt idt_descr
ret
setup_gdt:
lgdt gdt_descr
ret
本节添加的代码为第22-26、29-40行。首先是设置CR0寄存器,看看CR0寄存器的控制位。
第23行代码会把除PG、PE和ET以外的位复位。TS=1时不能使用协处理器,EM=0时允许使用协处理器,ET=1时表示系统使用的是387浮点协处理器,ET=0时则表示系统使用的是287协处理器。之后设置MP位(协处理器存在标志)。
fninit
会向协处理器发出初始化命令。fstsw
指令取协处理器的状态字。如果系统中存在协处理器的话,那么在执行了fninit指令后其状态字低字节肯定为0。所以,如果有协处理器就会跳转到第39行,对于387协处理器这行代码直接忽略掉,对于287则会执行浮点设置保护。如果没有协处理器,会将TS、EM设置为1,复位MP。
由于没有什么可演示的,就不放图了。
本章内容到此结束。下一章会讲解中断与异常的处理。