上一篇文章记录说明了宏内核和微内核,通过选择,我们最终选择的是微内核方式。那么选择微内核后,首要的任务就比较明显了,那就是实现一个进程间通信(IPC)机制。 在开始实现IPC之前,我们先来添加两个函数assert()和panic(),以便在运行过程中随时发现错误,从而减轻调试的难度。这篇文章就来记录这两个函数的实现。
assert()
assert 将通过检查表达式 expression 的值来决定是否需要终止执行程序,也就是说,如果表达式 expression 的值为假(为0),那么它将首先打印出一条错误信息,然后终止函数运行,否则,什么都不做。。你或许早就开始使用这个函数,但之前你使用的都是现成的assert,只要包含一个头文件,就可以方便地使用。不过现如今什么都要自力更生了,我们需要自己写一个assert函数。
代码 include/const.h,assert。
#define ASSERT
#ifdef ASSERT
void assertion_failure(char *exp, char *file, char *base_file, int line);
#define assert(exp) if (exp) ; \
else assertion_failure(#exp, __FILE__, __BASE_FILE__, __LINE__)
#else
#define assert(exp)
#endif
注意其中的__FILE__、__BASE_FILE__和__LINE__这三个宏,它们的意义如下:
- __FILE__ 将被展开成当前输入的文件。在这里,它告诉我们哪个文件中产生了异常。
- __BASE_FILE__ 可被认为是传递给编译器的那个文件名。比如你在m.c中包含了n.h,而n.h中的某一个assert函数失败了,则__FILE__为n.h,__BASE_FILE__为m.c。
- __LINE__ 将被展开成当前的行号。
明白了这几个宏的含义,剩下的assertion_failure()这个函数就显得容易了,它的作用就是将错误发生的位置打印出来。
代码 lib/misc.c,这是新增的文件,assertion_failure。
PUBLIC void assertion_failure(char *exp, char *file, char *base_file, int line)
{
printl("%c assert(%s) failed: file: %s, base_file: %s, ln%d", MAG_CH_ASSERT, exp, file, base_file, line);
/**
* If assertion fails in a TASK, the system will halt before
* printl() returns. If it happens in a USER PROC, printl() will
* return like a comm routine and arrive here.
* @see sys_printx()
*
* We use a forever loop to prevent the proc from going on:
*/
spin("assertion_failure()");
/* should never arrive here */
__asm__ __volatile__("ud2");
}
PUBLIC void spin(char * func_name)
{
printl("\nspinning in %s ...\n", func_name);
while (1) {}
}
我想你一定注意到了“printl(“%c assert(%s) failed: file: %s, base_file: %s, ln%d”, MAG_CH_ASSERT, exp, file, base_file, line);”这条语句,其中使用了格式化打印字符串,但是我们之前只实现了’%x’,其它的都没有实现,所以,我们现在需要将该打印语句中使用的格式化字符全部实现。
代码 kernel/vsprintf.c,实现格式化打印。
/**
* 进制转换函数
* @param val 需要转换的数字
* @param base 进制
*/
PRIVATE char* i2a(int val, int base, char ** ps)
{
int m = val % base;
int q = val / base;
if (q) {
i2a(q, base, ps);
}
*(*ps)++ = (m < 10) ? (m + '0') : (m - 10 + 'A');
return *ps;
}
PUBLIC int vsprintf(char *buf, const char *fmt, va_list args)
{
char* p;
va_list p_next_arg = args;
int m;
char inner_buf[STR_DEFAULT_LEN];
char cs;
int align_nr;
for (p = buf; *fmt; fmt++) {
if (*fmt != '%') {
*p++ = *fmt;
continue;
} else { /* a format string begins */
align_nr = 0;
}
fmt++;
if (*fmt == '%') {
*p++ = *fmt;
continue;
} else if (*fmt == '0') {
cs = '0';
fmt++;
} else {
cs = ' ';
}
while (((unsigned char)(*fmt) >= '0') && ((unsigned char)(*fmt) <= '9')) {
align_nr *= 10;
align_nr += *fmt - '0';
fmt++;
}
char * q = inner_buf;
memset(q, 0, sizeof(inner_buf));
switch (*fmt) {
case 'c':
*q++ = *((char*)p_next_arg);
p_next_arg += 4;
break;
case 'x':
m = *((int*)p_next_arg);
i2a(m, 16, &q);
p_next_arg += 4;
break;
case 'd':
m = *((int*)p_next_arg);
if (m < 0) {
m = m * (-1);
*q++ = '-';
}
i2a(m, 10, &q);
p_next_arg += 4;
break;
case 's':
strcpy(q, (*((char**)p_next_arg)));
q += strlen(*((char**)p_next_arg));
p_next_arg += 4;
break;
default:
break;
}
int k;
for (k = 0; k < ((align_nr > strlen(inner_buf)) ? (align_nr - strlen(inner_buf)) : 0); k++) {
*p++ = cs;
}
q = inner_buf;
while (*q) {
*p++ = *q++;
}
}
*p = 0;
return (p - buf);
}
MAG_CH_ASSERT的宏定义在const.h中,同时在该头文件中也添加了MAG_CH_PANIC的定义(panic函数会用到)。
/* magic chars used by `printx' */
#define MAG_CH_PANIC '\002'
#define MAG_CH_ASSERT '\003'
注意这里使用了一个小伎俩,那就是使用了一个改进后的打印函数printl(),它其实就是一个定义成printf的宏,不过这里的printf和我们之前完成的printf是不一样的,它将调用一个叫做printx的系统调用,并最终调用函数sys_printx(),它位于tty.c中。
代码 include/proto.h,printl。
#define printl printf
现在我们来修改printf()函数,让它调用printx()。
代码 kernel/printf.c,修改调用为printx。
int printf(const char *fmt, ...)
{
int i;
char buf[256];
va_list arg = (va_list)((char*)(&fmt) + 4); /* 4是参数fmt所占堆栈中的大小 */
i = vsprintf(buf, fmt, arg);
buf[i] = 0;
printx(buf);
return i;
}
开始编写printx。代码 kernel/syscall.asm。
...
_NR_printx equ 1
...
printx:
mov eax, _NR_printx
mov edx, [esp + 4]
int INT_VECTOR_SYS_CALL
ret
printx的参数个数与以前的系统调用相比有所增加,所以需要修改kernel.asm中的sys_call。额外需要注意的是,我们新加的参数是通过edx这个参数传递的,而save这个函数中也用到了寄存器dx,所以我们同时需要修改save。
代码 kernel/kernel.asm,修改 save 和 sys_call。
save:
pushad ; `.
push ds ; |
push es ; | 保存原寄存器值
push fs ; |
push gs ; /
;; 注意,从这里开始,一直到 'mov esp, StackTop',中间坚决不能用 push/pop 指令,
;; 因为当前 esp 指向 proc_table 里的某个位置,push 会破坏掉进程表,导致灾难性后果!
mov esi, edx ; 保存 edx, 因为 edx 里保存了系统调用的参数
; (没用栈,而是用了另一个寄存器 esi)
mov dx, ss
mov ds, dx
mov es, dx
mov fs, dx
mov edx, esi ; 恢复 edx
mov esi, esp ; esi = 进程表起始位置
inc dword [k_reenter] ; k_reenter++;
cmp dword [k_reenter], 0 ; if(k_reenter==0)
jne .1 ; {
mov esp, StackTop ; mov esp, StackTop <--切换到内核栈
push restart ; push restart
jmp [esi + RETADR - P_STACKBASE] ; return;
.1: ; } else { 已经在内核,不需要再切换
push restart_reenter ; push restart_reenter
jmp [esi + RETADR - P_STACKBASE] ; return;
; }
sys_call:
call save
sti
push esi
push dword [p_proc_ready]
push edx
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 4
pop esi
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
接下来修改sys_call_table[]中的系统调用,将之前使用的sys_write修改为sys_printx。
代码 kernel/global.c,修改sys_call_table。
PUBLIC system_call sys_call_table[NR_SYS_CALL] = {sys_get_ticks, sys_printx};
现在我们来编写函数sys_printx()。
代码 kernel/tty.c,sys_printx。
PUBLIC int sys_printx(int _unused1, int _unused2, char *s, PROCESS *p_proc)
{
const char *p;
char ch;
char reenter_err[] = "? k_reenter is incorrect for unknown reason";
reenter_err[0] = MAG_CH_PANIC;
/**
* @note Code in both Ring 0 and Ring 1~3 may invoke printx().
* If this happens in Ring 0, no linear-physical address mapping
* is needed.
*
* @attention The value of 'k_reenter' is tricky here. When
* -# printx() is called in Ring 0
* - k_reenter > 0. When code in Ring 0 calls printx(),
* an 'interrupt re-enter' will occur (printx() generates
* a software interrupt). Thus 'k_reenter' will be increased
* by 'kernel.asm::ave' and be greater than 0.
* -# printx() is called in Ring 1~3
* - k_reenter == 0
*/
if (k_reenter == 0) { /* printx() called in Ring<1~3> */
p = va2la(proc2pid(p_proc), s);
} else if (k_reenter > 0) { /* printx() called in Ring<0> */
p = s ;
} else { /* this should NOT happen */
p = reenter_err;
}
/**
* @note if assertion fails in any TASK, then system will be halted;
* if it fails in a USER PROC, it'll return like any normal syscall
* does.
*/
if ((*p == MAG_CH_PANIC) || (*p == MAG_CH_ASSERT && p_proc_ready < &proc_table[NR_TASKS])) {
disable_int();
char *v = (char*)V_MEM_BASE;
const char *q = p + 1; /* +1: skip the magic char */
while (v < (char*)(V_MEM_BASE + V_MEM_SIZE)) {
*v++ = *q++;
*v++ = RED_CHAR;
if (!*q) {
while (((int)v - V_MEM_BASE) % (SCREEN_WIDTH * 16)) {
/* *v++ = ' ' */
v++;
*v++ = GRAY_CHAR;
}
q = p + 1;
}
}
__asm__ __volatile__("hlt");
}
while ((ch = *p++) != 0) {
if (ch == MAG_CH_PANIC || ch == MAG_CH_ASSERT) {
continue; /* skip the magic char */
}
out_char(tty_table[p_proc->nr_tty].p_console, ch);
}
return 0;
}
sys_printx首先判断首字符是否为预先设定的“Magic Char”,如果是的话,则做响应的特殊处理。我们的assertion_failure()就使用了MAG_CH_ASSERT作为“Magic Char”。当sys_printx发现传入字符串的第一个字符是MAG_CH_ASSERT时,会同时判断调用系统调用的进程是系统进程(TASK)还是用户进程(USER PROC),如果是系统进程,则停止整个系统的运转,并将要打印的字符串打印在显存的各处;如果是用户进程,则打印之后像一个普通的printx调用一样返回,届时该用户进程会因为assertion_failure()中对函数spin()的调用而进入死循环。换言之,系统进程的assert失败会导致系统停转,用户进程的失败仅仅使自己停转。
sys_printx代码中使用的proc2pid是一个宏,定义在proc.h中。
代码 include/proc.h,proc2pid。
#define proc2pid(x) (x - proc_table)
va2la()用来由虚拟地址求线性地址,它用到了ldt_seg_linear()。ldt_seg_linear()函数的作用是,每个进程都有自己的LDT,位于进程表的中间,这个函数就是根据LDT中描述符的索引来求得描述符所指向的基地址。这两个函数的代码如下所示。
代码 kernel/proc.c,va2la()和ldt_seg_linear()。
/**
* <Ring 0~1> Calculate the linear address of a certain segment of a given proc.
*
* @param p Whose (the proc ptr).
* @param idx Whick (one proc has more than one segments).
*
* @return The required linear address.
*/
PUBLIC int ldt_seg_linear(PROCESS *p, int idx)
{
DESCRIPTOR *d = &p->ldts[idx];
return d->base_high << 24 | d->base_mid << 16 | d->base_low;
}
PUBLIC void* va2la(int pid, void* va)
{
PROCESS *p = &proc_table[pid];
u32 seg_base = ldt_seg_linear(p, INDEX_LDT_RW);
u32 la = seg_base + (u32)va;
if (pid < NR_TASKS + NR_PROCS) {
assert(la == (u32)va);
}
return (void*)la;
}
到这里大家应该清楚了assert()函数的实现的方法了,我们不妨来试验一下,在系统进程TTY中添加一句“assert(0);”,运行,你将看到如下左图所示的画面。再在用户进程TestC中添加一句“asser(0);”,将看到如下右图所示的画面。需要注意的是,我们新添加了misc.c文件,不要忘记修改Makefile。
panic()
panic 函数是一种在程序运行中遇到非常严重的错误时,用于引发程序崩溃的函数;它会停止当前正在执行的程序,并输出错误信息。panic 函数通常用于表示程序运行时遇到了无法修复的错误。
panic()跟assert()类似,也用到了sys_printx()和“Magic Char”,不过它更简单一些。
代码 kernel/main.c,panic。
PUBLIC void panic(const char *fmt, ...)
{
int i;
char buf[256];
/* 4 is the size of fmt in this stack */
va_list arg = (va_list)((char*)&fmt + 4);
i = vsprintf(buf, fmt, arg);
printl("%c !!panic!! %s", MAG_CH_PANIC, buf);
/* should never arrive here */
__asm__ __volatile__("ud2");
}
由于 panic 只会用在系统任务所处的Ring1或Ring0,所以sys_printx()遇到MAG_CH_PANIC就直接叫停整个系统,因为我们使用panic的时候,必是发生了严重错误的时候。
我们同样可以在TTY中试验一下 panic 的效果,比如添加这么一行:
panic("in TTY");
运行,会看到如下图所示效果。
在我们接下来的代码中,很多地方用到了assert()和panic(),其实有些地方完全可以不用这两个函数,而是以返回值的形式向上层函数传递的,但使用assert()和panic()可以减少代码量,并在第一时间通知我们哪里出了问题,作为一个试验性的操作系统,这样做比使用某种方法来“消除”错误要好。
欢迎关注我的公众号