这节内容和操作系统无关,但是这些函数会在后面的教程中广泛使用,它们被用来替代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, int
unsigned
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, long
unsigned
));long
break
;
case
'd':
parse_num((unsigned
)args_next(args, long
unsigned
), 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, void
unsigned
int
n) {
const
*s = (char
const
*)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, void
const
*src, void
unsigned
int
n) {
bcopy(src, dest, n);
return
dest;
}
void
*
memset(void
*dest,
c, int
unsigned
n) {int
*d = (char
*)dest;char
for
(; n>0; --n)
*d++ = (char
)c;
return
dest;
}
int
memcmp(const
*s1, void
const
*s2, void
unsigned
int
n) {
const
*s3 = (char
const
*)s1;char
const
*s4 = (char
const
*)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, char
const
*s2) {char
while
(*s1 && *s2) {
int
r = *s1++ - *s2++;
if
(r)
return
r;
}
return
(*s1)?1:-1;
}
char
*
strcpy(
*dest, char
const
*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);
输出后重置光标位置。
}
你可以自由使用我的代码,如有疑问请联系我。