手写操作系统(二十四)-assert断言

代码、内容参考来自于包括《操作系统真象还原》、《一个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:一个操作系统的实现

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇