代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。
断言类似C语言里的assert。
在我们系统中,我们实现两种断言,一种是为内核系统使用的ASSERT,另一种是为用户进程使用的assert,这里先实现专供内核使用的ASSERT。
这里先实现两个自由开关中断的函数,
这里再说说开关中断的问题,CPU 是能够屏蔽可屏蔽中断的,就是通过 EFLAGS 的 IF 位,IF位为1表示允许中断,IF 为0表示屏蔽中断。 所以开关中断就是修改EFLAGS的IF位。
修改EFLAGS寄存器值的方法:
- sti指令将EFLAGS IF位置1,cli指令将EFLAGS IF位清 0,这两个指令有使用条件:CPL< IOPL,IOPL 也是 ELFAGS 里面的位域,指的是 IO特权级,
- pushf 指令将 EFLAGS 压栈,还有中断时 EFLAGS 也会压栈,压入栈中后我们就可以修改栈里面的EFLAGS 的值,待到后续 popf或者 iret中断退出将修改后的EFLAGS 弹出后,就相当于修改了 EFLAGS 的值。
- 通过中断门进入中断时会将EFLAGS的IF位清0
通常我们处理中断时并不需要额外地做开关中断处理,因为硬件CPU已经帮我们做了,通过中断门进入中断时自动地关闭中断,然后iret后又恢复中断。
修改/kernel/interrupt.c
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "io.h"
#include "print.h"
#define IDT_DESC_CNT 0x21 //目前共支持的中断数,33
#define PIC_M_CTRL 0x20 //主片的控制端口是0x20
#define PIC_M_DATA 0x21 //主片的数据端口是0x21
#define PIC_S_CTRL 0xa0 //从片的控制端口是0xa0
#define PIC_S_DATA 0xa1 //从片的数据端口是0xa1
#define EFLAGS_IF 0x00000200 //eflags寄存器中的if=1
#define GET_FLAGS(EFLAG_VAR) asm volatile("pushfl;popl %0":"=g"(EFLAG_VAR))
//"=g" 指示编译器将结果放在任意通用寄存器中,并将其赋值给 EFLAG_VAR。
//pushfl 指令将标志寄存器 EFLAGS 的值入栈,然后 popl %0 指令将栈顶的值弹出到指定的操作数 %0 中。
//当调用 GET_FLAGS(EFLAG_VAR) 宏时,它将 EFLAGS 寄存器的值存储到 EFLAG_VAR 变量中。
/*中断门描述符结构体*/
struct gate_desc{
uint16_t func_offset_low_word; //低32位——0~15位:中断处理程序在目标代码段内的偏移量的第15~0位
uint16_t selector; //低32位——16~31位:目标CS段选择子
uint8_t dcount; //高32位——0~7位:此项为双字计数字段,是门描述符中的第4字节,为固定值
uint8_t attribute; //高32位——8~15位:P+DPL+S+TYPE
uint16_t func_offset_high_word; //高32位——16~31位:中断处理程序在目标代码段内的偏移量的第16~31位
};
//静态函数声明,非必须
static void make_idt_desc(struct gate_desc* p_gdesc,uint8_t attr,intr_handler function);
static struct gate_desc idt[IDT_DESC_CNT]; //IDT是中描述符表,实际上是中断门描述符数组
extern intr_handler intr_entry_table[IDT_DESC_CNT]; //指针格式应该与数组类型一致,这里intr_entry_table中的元素类型就是function,就是.text的地址。所以用intr_handle来引用。声明引用定义在kernel.S中的中断处理函数入口数组
char* intr_name[IDT_DESC_CNT]; //用于保存异常的名字
intr_handler idt_table[IDT_DESC_CNT]; //idt_table为函数数组,里面保持了中断处理函数的指针
/*创建中断门描述符*/
static void make_idt_desc(struct gate_desc* p_gdesc,uint8_t attr,intr_handler function){ //intr_handler是个空指针类型,仅用来表示地址
//中断门描述符的指针、中断描述符内的属性、中断描述符内对应的中断处理函数
p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000ffff;
p_gdesc->selector = SELECTOR_K_CODE;
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->func_offset_high_word = ((uint32_t)function & 0xffff0000) >> 16;
}
/*初始化中断描述符表*/
static void idt_desc_init(void){
int i;
for(i = 0;i < IDT_DESC_CNT;i++){
make_idt_desc(&idt[i],IDT_DESC_ATTR_DPL0,intr_entry_table[i]);
}
put_str(" idt_desc_init done\n");
}
/*初始化可编程中断控制器8259A*/
static void pic_init(void){
/*初始化主片*/
outb(PIC_M_CTRL,0x11); //ICW1:边沿触发,级联8259,需要ICW4
outb(PIC_M_DATA,0x20); //ICW2:起始中断向量号为0x20,也就是IR[0-7]为0x20~0x27
outb(PIC_M_DATA,0x04); //ICW3:IR2接从片
outb(PIC_M_DATA,0x01); //ICW4:8086模式,正常EOI
/*初始化从片*/
outb(PIC_S_CTRL,0x11); //ICW1:边沿触发,级联8259,需要ICW4
outb(PIC_S_DATA,0x28); //ICW2:起始中断向量号为0x28,也就是IR[8-15]为0x28~0x2F
outb(PIC_S_DATA,0x02); //ICW3:设置从片连接到主片的IR2引脚
outb(PIC_S_DATA,0x01); //ICW4:8086模式,正常EOI
/*打开主片上IR0,也就是目前只接受时钟产生的中断*/
outb(PIC_M_DATA,0xfe);
outb(PIC_S_DATA,0xff);
put_str(" pic_init done\n");
}
/*通用的中断处理函数,一般用在异常出现时的处理*/
static void general_intr_handler(uint8_t vec_nr){
if(vec_nr == 0x27 || vec_nr == 0x2f){ //IRQ7和IRQ5会产生伪中断,无需处理;0x2f是从片8259A的最后一个IRQ引脚,保留项
return;
}
put_str("int vector : 0x");
put_int(vec_nr);
put_char('\n');
}
/*初始化idt_table*/
static void exception_init(void){
//将idt_table的元素都指向通用的中断处理函数,名称为unknown
int i;
for(i = 0;i < IDT_DESC_CNT;i++){
idt_table[i] = general_intr_handler; //默认为general_intr_handler,以后会由register_handler来注册具体函数
intr_name[i] = "unknown";
}
intr_name[0] = "#DE Divide Error";
intr_name[1] = "#DB Debug Exception";
intr_name[2] = "NMI Interrupt";
intr_name[3] = "BP Breakpoint Exception";
intr_name[4] = "#OF Overflow Exception";
intr_name[5] = "#BR BOUND Range Exceeded Exception";
intr_name[6] = "#UD Invalid Opcode Exception";
intr_name[7] = "#NM Device Not Available Exception";
intr_name[8] = "#DF Double Fault Exception";
intr_name[9] = "Coprocessor Segment Overrun";
intr_name[10] = "#TS Invalid TSS Exception";
intr_name[11] = "#NP Segment Not Present";
intr_name[12] = "#SS Stack Fault Exception";
intr_name[13] = "#GP General Protection Exception";
intr_name[14] = "#PF Page-Fault Exception";
//intr_name[15]是intel保留项,未使用
intr_name[16] = "#MF x87 FPU Floating-Point Error";
intr_name[17] = "#AC Alignment Check Exception";
intr_name[18] = "#MC Machine-Check Exception";
intr_name[19] = "#XF SIMD Floating-Point Exception";
}
/*完成所有有关中断的初始化工作*/
void idt_init(){
put_str("idt_init start\n");
idt_desc_init(); //初始化中断描述符表
exception_init();
pic_init(); //初始化8259A
/*加载idt*/
// uint64_t idt_operand = ((sizeof(idt)-1) | (uint64_t)((uint32_t)idt << 16)); //书上是错的
uint64_t idt_operand = ((sizeof(idt)-1) | ((uint64_t)(uint32_t)idt << 16)); //低16位是idt的大小,高48位是IDT的基址。因为idt是32位,左移16位后会丢失高16位,所以先转换为64位再左移
asm volatile("lidt %0" : : "m" (idt_operand)); //加载IDT,IDT的0~15位是表界限,16~47位是表基址
put_str("idt_init done\n");
}
/*开中断,并返回开中断前的状态*/
enum intr_status intr_enable(){
enum intr_status old_status;
if(INTR_ON == intr_get_status()){
old_status = INTR_ON;
return old_status;
}else{
old_status = INTR_OFF;
asm volatile("sti"); //开中断
return old_status;
}
}
/*关中断,并返回关中断前的状态*/
enum intr_status intr_disable(){
enum intr_status old_status;
if(INTR_ON == intr_get_status()){
old_status = INTR_ON;
asm volatile("cli" : : : "memory"); //关中断,cli指令将IF位置0
return old_status;
}else{
old_status = INTR_OFF;
return old_status;
}
}
/*将中断状态设置为status*/
enum intr_status intr_set_status(enum intr_status status){
return status & INTR_ON ? intr_enable() : intr_disable();
}
/*获取当前中断状态*/
enum intr_status intr_get_status(){
uint32_t eflags = 0;
GET_FLAGS(eflags);
return (EFLAGS_IF & eflags) ? INTR_ON : INTR_OFF;
}代码解释:
新添加定义了两个宏EFLAGS_IF和GET_FLAGS(EFLAG_VAR)用来获取中断状态。EFLAGS_IF 表示开中断时 eflags 寄存器中的IF的值,由于 IF 位于 eflags 中的第 9 位,故EFLAGS_IF的值为0x00000200。GET_EFLAGS 用来获取 eflags 寄存器的值。 它就是段内嵌汇编代码,其中 EFLAG_VAR是C代码中用来存储eflags值的变量,它用寄存器约束g来约束EFLAG_VAR可以放在内存中或寄存器中,之后用pushfl将eflags寄存器的值压入栈,然后再用popl指令将其弹出到与EFLAG_VAR关联的约束中,最后C变量EFLAG_VAR获得 eflags 的值。
在头文件interrupt.h中定义了enum intr_status枚举结构,就是INTR_OFF值为0表示关中断,INTR_ON值为1表示开中断。
intr_get_status 函数,它的作用是获取当前的中断状态。 它就是先利用了宏 GETEFLAGS来获取eflags寄存器的值并存到变量eflags中。 接下来再利用EFLAGS_IF与变量eflags的值进行按位与运算,判断变量eflags中IF位的值是否为1,从而返回不同的中断状态,即INTR_ON 或INTR_OFF。
intr_enable函数,它的功能是把中断打开,这就是所谓的”开中断”,再把执行开中断前的中断状态返回。 开中断的原理就是执行sti指令将eflags中的IF位置1,此函数先调用intr_get_status函数获取当前的中断状态,接下来判断如果当前已经是开中断的状态,那就直接返回,无需再执行一次汇编指令sti。 如果当前是关中断状态,就执行内联汇编asm volatile(“sti”),通过sti指令将中断打开。 最后,无论程序走哪个分支,都要将操作前的中断状态old_status返回。
只是每个分支中都有return语句时,能够避免将C编译为汇编代码时因为共用一行代码而额外添加jmp语句,虽然程序因此而大了一点,但也因此而快了一点,空间换时间。
intr_disable函数,它的功能是把中断关闭,就是关中断。 关中断的原理就是执行cli指令将eflags中的IF位置0,此函数的原理也是先通过intr_get_status获取当前的中断状态,若当前已经是关中断状态,则直接把INTR_OFF返回,否则就通过内联汇编”asm volatile(“cli” ::: “memory”)”将中断打开,然后返回旧中断状态。
函数intr_set_status函数,它的作用是把中断设置为参数status的状态,参数status的值通常是调用intr_disable和intr_enable之后的返回值old_status,故一般情况下intr_set_status用来配合intr_disable和intr_enable,以恢复之前的中断状态。
INTR_OFF值和INTR_ON定义在interrupt.h中的enum intr_status枚举结构
/kernel/interrupt.h
#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "stdint.h"
typedef void* intr_handler;
void idt_init(void);
/*定义中断的两种状态:
INTR_OFF值为0,表示关中断
INTR_ON 值为1,表示开中断*/
enum intr_status{
INTR_OFF = 0,
INTR_ON = 1
};
//枚举常量在 C 语言中被赋予默认的整数值,按照声明的顺序从 0 开始递增。因此,在这个例子中,INTR_OFF 的值为 0,INTR_ON 的值为 1。也可以显式地为枚举常量指定特定的值。
enum intr_status intr_get_status(void);
enum intr_status intr_set_status(enum intr_status);
enum intr_status intr_enable(void);
enum intr_status intr_disable(void);
#endif
接下来就是要实现ASSERT了
在C语言中ASSERT是用宏来定义的,其原理是判断传给ASSERT的表达式是否成立,若表达式成立则什么都不做,否则打印出错信息并停止执行,我们仿照它来实现自己的版本。
/kernel/debug.h
#ifndef __KERNEL_DEBUG_H
#define __KERNEL_DEBUG_H
void panic_spin(char *filename,int line,const char* func,const char* condition);
/*_VA_ARGS********************
代表所有与省略号相对应的参数。
"..."表示定义的宏其参数可变*/
#define PANIC(...) panic_spin(__FILE__,__LINE__,__func__,__VA_ARGS__)
/****************************/
#ifdef NDEBUG
#define ASSERT(CONDITION) ((void)0)
#else //CONDITION判断为真,将ASSERT变成0,相当于删除
#define ASSERT(CONDITION) \
if(CONDITION){}else{ \
PANIC(#CONDITION); }//符号#让编译器将宏的参数转化为字符串字面量
#endif
#endif代码解释:
#ifndef __KERNEL_DEBUG_H:这是条件编译指令,如果 __KERNEL_DEBUG_H 宏未定义,则执行接下来的代码;否则,跳过这部分代码。
#define __KERNEL_DEBUG_H:定义 __KERNEL_DEBUG_H 宏,以防止多重包含。
void panic_spin(char *filename, int line, const char* func, const char* condition)声明了一个函数 panic_spin,定义在debug.c
#define PANIC(…):这是一个宏定义,它用于简化调用 panic_spin 函数的操作。
PANIC 后面是(…),这是 C 预处理器所支持的一种用法,它允许宏支持个数不固定的参数, “….”表示所定义的宏其参数可变,术语为参数个数可变的宏,只要括号中用“…”来占位,就表示此宏的参数个数不固定。对应传递__VA_ARGS__
__FILE__:预定义宏,表示当前源文件名的字符串字面量。
__LINE__:预定义宏,表示当前源文件中的行号。
__func__:预定义宏,表示当前所在的函数名的字符串字面量。
__VA_ARGS__:代表可变数量的参数,用于在宏中处理不定数量的参数。
因此,PANIC(…) 宏将会将当前的文件名 (__FILE__)、行号 (__LINE__)、当前函数名 (__func__) 以及传入的额外参数传递给 panic_spin 函数,实现了一种简便的错误处理机制。
#define PANIC(...) panic_spin(__FILE__,__LINE__,__func__,__VA_ARGS__)
#ifdef NDEBUG:这是一个预处理器条件指令,用于检查是否定义了 NDEBUG 宏。
NDEBUG 通常用于控制调试相关的输出。如果定义了 NDEBUG,不需要进行断言检查,因此 ASSERT(CONDITION) 就被定义为空操作 (void)0。
如果未定义 NDEBUG,则 ASSERT(CONDITION) 宏会展开为一个 if-else 结构:
CONDITION 是一个表达式,宏会将其作为字符串 (#CONDITION) 传递给 PANIC 宏。
如果 CONDITION 不成立(即为假),则调用 PANIC 宏,导致程序进入 panic_spin 函数,用于处理断言失败的情况。
#ifdef NDEBUG
#define ASSERT(CONDITION) ((void)0)
#else //CONDITION判断为真,将ASSERT变成0,相当于删除
#define ASSERT(CONDITION) \
if(CONDITION){}else{ \
PANIC(#CONDITION); }//符号#让编译器将宏的参数转化为字符串字面量
#endif
#endif
接下来是kernel/debug.c
#include "debug.h"
#include "print.h"
#include "interrupt.h"
/*打印文件名、行号、函数名、条件并使程序悬停*/
void panic_spin(char* filename,int line,const char* func,const char* condition){
intr_disable();
put_str("|n|n|n!!!!!error!!!!!\n");
put_str("filename:");put_str(filename);put_str("\n");
put_str("line:0x");put_str(line);put_str("\n");
put_str("function:");put_str((char*)func);put_str("\n");
put_str("condition:");put_str((char*)condition);put_str("\n");
while(1);
}panic_spin 函数,除参数 line外,其他三个参数都是字符串指针,代码也比较简单,执行 intr_disable把中断关闭后,再打印调度相关的信息,之后就通过while(1)死循环悬停在此。
然后main.c调用
#include "print.h"
#include "init.h"
#include "debug.h"
int main(void){
put_str("I am kernel\n");
init_all();
ASSERT(1==2);
while(1);
return 0;
}这里把开中断sti的内联汇编去掉了,因为之前是为了演示中断机制才打开中断的,目前暂时将其关闭,等真正时机成熟咱们再打开。为引用ASSERT,“ASSERT(1=2)”来测试,很明显1=2是不成立的,故此ASSERT会开始工作,内部调用panic_spin来打印调试信息并使程序悬停。
我们这里开始使用makefile编译
BUILD_DIR = ./build
##用来存储生成的所有目标文件
ENTRY_POINT = 0xc0001500
AS = nasm
CC = gcc
LD = ld
LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/
ASFLAGS = -f elf
CFLAGS = -Wall -m32 -fno-stack-protector $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes
##-fno-builtin是告诉编译器不要采用内部函数 -Wstrict-prototypes是要求函数声明中必须有参数类型
## -Wmissing-prototypes要求函数必须有声明
LDFLAGS = -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \
$(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o $(BUILD_DIR)/debug.o
## OBJS用来存储所有目标文件名,不要用%.o,因为不能保证链接顺序
########## c代码编译 ##########
$(BUILD_DIR)/main.o:kernel/main.c lib/kernel/print.h lib/stdint.h kernel/init.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/init.o:kernel/init.c kernel/init.h lib/kernel/print.h lib/stdint.h \
kernel/interrupt.h device/timer.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/interrupt.o:kernel/interrupt.c kernel/interrupt.h lib/stdint.h \
kernel/global.h lib/kernel/io.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/timer.o:device/timer.c device/timer.h lib/stdint.h lib/kernel/io.h lib/kernel/print.h
$(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/debug.o:kernel/debug.c kernel/debug.h lib/kernel/print.h lib/stdint.h kernel/interrupt.h
$(CC) $(CFLAGS) $< -o $@
###########汇编代码编译############
$(BUILD_DIR)/kernel.o:kernel/kernel.S
$(AS) $(ASFLAGS) $< -o $@
$(BUILD_DIR)/print.o:lib/kernel/print.S
$(AS) $(ASFLAGS) $< -o $@
##########链接所有目标文件#############
$(BUILD_DIR)/kernel.bin:$(OBJS)
$(LD) $(LDFLAGS) $^ -o $@
.PHONY: mk_dir hd clean all
mk_dir:
if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fi
###fi为终止符
hd:
dd if=$(BUILD_DIR)/kernel.bin of=/bochs/bin/dreams.img bs=512 count=200 seek=9 conv=notrunc
clean: ##将build目录下文件清空
cd $(BUILD_DIR) && rm -f ./*
build:$(BUILD_DIR)/kernel.bin ##编译kernel.bin,只要执行make build就是编译文件
all:mk_dir build hd
##依次执行伪目标mk_dir build hd,只要执行make all就是完成了编译到写入硬盘的全过程然后make all执行
sudo make all

运行结果如下:

参考
郑钢著操作系统真象还原
田宇著一个64位操作系统的设计与实现
丁渊著ORANGE’S:一个操作系统的实现


