从零编写linux0.11 - 第四章 部分系统初始化

本文详细介绍了Linux0.11内核初始化过程中的关键步骤,包括内存管理的初始化,设置了页目录和页表,并根据内存大小划分了高速缓冲区和主存储区。接着,对缓冲区进行了初始化,创建了缓冲头结构体并设置了哈希表和空闲链表。此外,还介绍了时钟初始化,通过读取CMOS获取开机时间,为进程管理做准备。最后,检查了协处理器的存在并进行初始化。这些初始化操作为后续的系统资源管理奠定了基础。
摘要由CSDN通过智能技术生成

编程环境: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函数中显示内存的大小。

4.1运行结果

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比较

从这个例子可以看出,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);
}

4.2运行结果

没什么大问题,程序逻辑有没有错只有后面用到的时候才知道了。

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);
}

要说读取时间当然是用我们万能的端口。将读取的时间保存到结构体中,看到结构体中变量的名字就应该知道它的含义了吧。

rtc

如上图所示,将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.3运行结果

这个时间好像有点不对啊,和电脑时间对不上。我一开始以为这是格林尼治时间,对比了发现不是,后来与各个城市的时间对比,发现这应该是洛杉矶时间。可为什么是洛杉矶时间呢?这就问到我知识的盲区了,还请大家自己找答案吧ε=ε=ε=( ̄▽ ̄)

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寄存器的控制位。

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。

由于没有什么可演示的,就不放图了。

本章内容到此结束。下一章会讲解中断与异常的处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值