这节内容和操作系统无关,但是这些函数会在后面的教程中广泛使用,它们被用来替代C标准库中的函数。如果你不关心它们是怎么实现的,只需要记住kprintf就像C中的printf一样,print_c在屏幕上输出黑底白字的一个字符。你可以安全地跳过此节。
printf是程序员的好朋友,因此为了输出字符串、数字之类的东西,我要实现一个类似printf的函数而不是再在B8000处瞎搞。
我不打算实现完全版的printf,在Skelix中我们只需要输出字符串、十六进制和二进制数字、正整数和字符而已。最重要的是它必须接受变长参数。
默认情况下,当调用函数func(时,它的汇编代码如下,int arg1, int arg2, int arg3)
pushl arg3
pushl arg2
pushl arg1
call func
参数被从右到左一个个地压入堆栈,所以更多的参数不过是更深地压栈而已。我们怎么才能知道有多少个参数呢?答案就是格式串中有多少个%X就有多少个参数。
在32位模式下,所有长度小于整形的数据都会被当做整形压栈。也就是说哪怕一个字符也会在栈中占据4字节空间,我们必须正确地处理栈中的参数。
我们准备让kprintf这样接受参数kprintf(color, format string, arguments...),第一个参数定义了输出的前景和背景色。还有一些为处理栈而定义的宏,如果你熟悉C的话,那么你一定也熟悉这些宏。
03/kprintf.c
#define args_list char *
kprintf用这个宏把栈空间转换成字符流以方便处理。
#define _arg_stack_size(type) \
((((type)-1)/sizeof(sizeof)+1)*int(sizeof))int
这个宏将参数对4圆整,以计算它在栈中需占用多少个4字节空间。
#define args_start(ap, fmt) do { \
ap = (char *)((unsigned int)&fmt + _arg_stack_size(&fmt)); \
} while (0)
参数在格式串之后,或者我应该说是在栈中fmt的顶上,这个宏获取参数的开始地址。
#define args_end(ap)
什么也不做。
#define args_next(ap, type) (((type *)(ap+=_arg_stack_size(type)))[-1])
获得下一个参数的地址。
static char buf[1024] = {-1};
static int ptr = -1;
将所有要输出的字符放入缓冲中区,并用指针指向下一个可用空间。
/* valid base: 2, 8, 10 */
static void
parse_num(unsigned value, intunsigned base) {int
unsigned int n = value / base;
int r = value % base;
if (r < 0) {
r += base;
--n;
}
if (value >= base)
parse_num(n, base);
buf[ptr++] = "0123456789"[r];
}
static void
parse_hex(unsigned int value) {
int i = 8;
while (i-- > 0) {
buf[ptr++] = "0123456789abcdef"[(value>>(i*4))&0xf];
}
}
这两个函数转换数值到不同进制。
/* %s, %c, %x, %d, %% */
void
kprintf(enum KP_LEVEL kl, const char *fmt, ...) {
int i = 0;
char *s;
/* must be the same size as enum KP_LEVEL */
struct KPC_STRUCT {
COLOUR fg;
COLOUR bg;
} KPL[] = {
{BRIGHT_WHITE, BLACK},
{YELLOW, RED},
};
的定义在enum KP_LEVEL {KPL_DUMP, KPL_PANIC}03/include/kprintf.h中,它定义了输出的两种颜色方案,KPL_DUMP输出黑底白字,KPL_PANIC输出红底黄字。细节在03/include/scr.h中,后面介绍它们。
args_list args;
args_start(args, fmt);
ptr = 0;
for (; fmt[i]; ++i) {
if ((fmt[i]!='%') && (fmt[i]!='\\')) {
buf[ptr++] = fmt[i];
continue;
} else if (fmt[i] == '\\') {
/* \a \b \t \n \v \f \r \\ */
switch (fmt[++i]) {
case 'a': buf[ptr++] = '\a'; break;
case 'b': buf[ptr++] = '\b'; break;
case 't': buf[ptr++] = '\t'; break;
case 'n': buf[ptr++] = '\n'; break;
case 'r': buf[ptr++] = '\r'; break;
case '\\':buf[ptr++] = '\\'; break;
}
continue;
}
像printf一样接受转义字符串。
/* fmt[i] == '%' */
switch (fmt[++i]) {
case 's':
s = ( *)args_next(args, char *);char
while (*s)
buf[ptr++] = *s++;
break;
case 'c':
/* why is int?? */
buf[ptr++] = (char)args_next(args, int);
break;
case 'x':
parse_hex((unsigned)args_next(args, longunsigned));long
break;
case 'd':
parse_num((unsigned)args_next(args, longunsigned), 10);long
break;
case '%':
buf[ptr++] = '%';
break;
default:
buf[ptr++] = fmt[i];
break;
}
}
buf[ptr] = '\0';
在缓冲中放入结尾的\0。
args_end(args);
for (i=0; i<ptr; ++i)
print_c(buf[i], KPL[kl].fg, KPL[kl].bg);
}
用print_c在屏幕上打印缓冲中的所有字符。
在继续之前,你的编译器版本可能造成一些问题。即使使用了-nostdlib选项,根据GCC手册:“编译器可能在System V(和 ISO C)环境下生成对memcmp、memset和memcpy的调用;可能在BSD环境下生成对bcopy和bzero的调用。”。GCC可能会给你“找不到memcpy”之类的错误。我用之前旧版本的GCC是没有问题的,但现在有了。我们必须自己实现这些函数了。
/* result is currect, even when both area overlap */
void
bcopy(const *src, void *dest, voidunsigned int n) {
const *s = (charconst *)src;char
*d = (char *)dest;char
if (s <= d)
for (; n>0; --n)
d[n-1] = s[n-1];
else
for (; n>0; --n)
*d++ = *s++;
}
void
bzero(void *dest, unsigned int n) {
memset(dest, 0, n);
}
void *
memcpy( *dest, voidconst *src, voidunsigned int n) {
bcopy(src, dest, n);
return dest;
}
void *
memset(void *dest, c, intunsigned n) {int
*d = (char *)dest;char
for (; n>0; --n)
*d++ = (char)c;
return dest;
}
int
memcmp(const *s1, voidconst *s2, voidunsigned int n) {
const *s3 = (charconst *)s1;char
const *s4 = (charconst *)s2;char
for (; n>0; --n) {
if (*s3 > *s4)
return 1;
else if (*s3 < *s4)
return -1;
++s3;
++s4;
}
return 0;
}
int
strcmp(const *s1, charconst *s2) {char
while (*s1 && *s2) {
int r = *s1++ - *s2++;
if (r)
return r;
}
return (*s1)?1:-1;
}
char *
strcpy( *dest, charconst *src) {char
char *p = dest;
while ( (*dest++ = *src++))
;
*dest = 0;
return p;
}
unsigned int
strlen(const char *s) {
unsigned int n = 0;
while (*s++)
++n;
return n;
}
直接操作视频内存很麻烦,我们需要一个模块去处理屏幕输出。先定义一些常量,
03/include/scr.h
#define MAX_LINES 25
#define MAX_COLUMNS 80
默认使用80列25行的屏幕。
#define TAB_WIDTH 8 /* must be power of 2 */
/* color text mode, the video ram starts from 0xb8000,
we all have color text mode, right? :) */
#define VIDEO_RAM 0xb8000
假定我们都处于彩色文本模式下,此时适配器使用0xB8000-0xBF000作为视频内存。通常情况下,有80列、25行、16色。这段内存空间被分成多个4K大小的内存页。我们可以同时使用所有的视频页(我写的多视频页程序),但是只有一个视频页是可见的。需要两个字节来显示一个祖父,一个是字符字节另一个字节表示颜色. 字符字节包含了字符的值。颜色属性字节定义如下,
| Bit 7 | Blinking |
| Bits 6-4 | Background color |
| Bit 3 | Bright |
| Bit3 2-0 | Foreground color |
一会儿就会用到这个表格。
#define LINE_RAM (MAX_COLUMNS*2)
#define PAGE_RAM (MAX_LINE*MAX_COLUMNS)
#define BLANK_CHAR (' ')
#define BLANK_ATTR (0x70) /* white fg, black bg */ //#Bug 002
#define CHAR_OFF(x,y) (LINE_RAM*(y)+2*(x))
计算x,y代表的内存偏移。
typedef enum COLOUR_TAG {
BLACK, BLUE, GREEN, CYAN, RED, MAGENTA, BROWN, WHITE,
GRAY, LIGHT_BLUE, LIGHT_GREEN, LIGHT_CYAN,
LIGHT_RED, LIGHT_MAGENTA, YELLOW, BRIGHT_WHITE
} COLOUR;
根据上面的表格定义如下的颜色标记。
03/scr.c
static int csr_x = 0;
static int csr_y = 0;
因为我们仅用到一个视频页,所以光标位置全局性地储存在csr_x和csr_y。
static void
scroll(int lines) {
这个函数用行数作为参数值向下滚屏,实际上它只是覆写一些内存让它看起来是在滚屏。
*p = (short *)(VIDEO_RAM+CHAR_OFF(MAX_COLUMNS-1, MAX_LINES-1));short
int i = MAX_COLUMNS-1;
memcpy(( *)VIDEO_RAM, (void *)(VIDEO_RAM+LINE_RAM*lines),void
LINE_RAM*(MAX_LINES-lines));
for (; i>=0; --i)
*p-- = (short)((BLANK_ATTR<<4)|BLANK_CHAR); //#Bug 002
}
清理最底行的所有字符让它看起来在滚屏。(#Bug 002 Song Jiang指出了这个问题,BLANK_ATTR应该左移8字节而不是4字节。通过修复这个问题,我发现我在BLANK_ATTR值上犯了错,它应该是0x07而不是0x70。另一个问题是scroll本应该只滚一行,在本页搜#Bug 002找到相应的代码改变)。
void
set_cursor( x, int y) {int
这个函数设置光标位置。
csr_x = x;
csr_y = y;
设置光标位置可以构成竞争条件,但是print_c只会在内核模式中被使用,所以我没有关掉所有的中断。它可能会造成一些未发现的问题。
outb(0x0e, 0x3d4);
outb(((csr_x+csr_y*MAX_COLUMNS)>>8)&0xff, 0x3d5);
outb(0x0f, 0x3d4);
outb(((csr_x+csr_y*MAX_COLUMNS))&0xff, 0x3d5);
通过端口0x3D4,0x3D5设置光标位置。
}
void
print_c(char c, COLOUR fg, COLOUR bg) {
使用这个函数在当前光标位置输出一个字符。
char *p;
char attr;
p = (char *)VIDEO_RAM+CHAR_OFF(csr_x, csr_y);
获得当前光标指向的视频内存地址。
attr = (char)(bg<<4|fg);
switch (c) {
case '\r':
csr_x = 0;
break;
case '\n':
for (; csr_x<MAX_COLUMNS; ++csr_x) {
*p++ = BLANK_CHAR;
*p++ = attr;
}
break;
case '\t':
c = csr_x+TAB_WIDTH-(csr_x&(TAB_WIDTH-1));
c = c<MAX_COLUMNS?c:MAX_COLUMNS;
for (; csr_x<c; ++csr_x) {
*p++ = BLANK_CHAR;
*p++ = attr;
}
break;
case '\b':
if ((! csr_x) && (! csr_y))
return;
if (! csr_x) {
csr_x = MAX_COLUMNS - 1;
--csr_y;
} else
--csr_x;
(( *)p)[-1] = (short)((BLANK_ATTR<<4)|BLANK_CHAR);short
break;
default:
*p++ = c;
*p++ = attr;
++csr_x;
break;
}
if (csr_x >= MAX_COLUMNS) {
csr_x = 0;
if (csr_y < MAX_LINES-1)
++csr_y;
else
scroll(1);
}
set_cursor(csr_x, csr_y);
输出后重置光标位置。
}
你可以自由使用我的代码,如有疑问请联系我。