天衣有缝

冠盖满京华,斯人独憔悴~
posts - 35, comments - 115, trackbacks - 0, articles - 0
   :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

3课:辅助函数


声明:转载请保留

译者http://www.cppblog.com/jinglexy

原作者:xiaoming.mo at skelix dot org

MSN & Email: jinglexy at yahoo dot com dot cn

目标            下载源程序

 

这节课我们讲述的内容与操作系统暂无太大关系,但是这些基础函数非常重要,并且在后面的课程中经常用到。这就是我们经常听到的内核库。如果你对这些不是很感兴趣,知道kprintfc语言里面的print一样工作就行了。简单掠过即可。


C用户库里面的printf具有高度可伸缩性,也很容易理解,相比之下C++中的IO运算符就比较难了。为了在屏幕上显示字符串或数据,我们现在需要实现类似C库中的printf,显示字符在B8000开始的显存处。我并不像完全实现printf的所有功能,因为skelix内核只需要显示字符串,十进制和十六进制或二进制,正整数,字符就行了,并且需要支持可变参数。其他更高级的功能我们不会用到。

这里有一种方法来实现,我们直到象func(int arg1, int arg2, int arg3)这样一个函数被调用时,它汇编后的指令应该如下(所有从左向右入栈的编译器应该从地球上彻底消失):
pushl   arg3
pushl   arg2
pushl   arg1
call    func

 

我们看到,参数从右向左一个个入栈,参数越多,入栈越深。如果是可变参数那我们怎么知道有多少个参数呢?答案是printf格式化字符串中参数判断:有多少个%X,就有多少个参数要解析。在32位模式下,所有小于4字节的参数都被当作4字节处理。例如一个char型参数,入栈时就是int型了,所以在解析参数时务必保证正确。

我们这样设计kprintf参数:kprintf(color, format string, arguments...)

第一个参数定义输出的前景/背景颜色。我们定义了很多宏来解析栈,如果你熟悉C语言应该很容易理解它们。


03/kprintf.c

#define args_list char *            // 这个宏用例转换栈空间为字符串指针
#define _arg_stack_size(type)    (((sizeof(type)-1)/sizeof(int)+1)*sizeof(int))

                                    // 这个宏四舍五入参数大小为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])

                                    // 当前参数地址,然后设置指针为下一个参数地址,暧昧的函数名!

03/kprintf.c

static char buf[1024] = {-1};       // 注意没有锁保护,引用该变量的函数不可重入!
static int ptr = -1;

 

下面两个函数解析值为指定的进制数:
static void
parse_num(unsigned int value, unsigned int base) {            //
可以打印小于等于10进制的数
    unsigned int n = value / base;
    int r = value % base;
    if (r < 0) {
        r += base;
        --n;
    }
    if (value >= base)
        parse_num(n, base);
    buf[ptr++] = (r+'0');
}

static void                                                   //
打印16进制数
parse_hex(unsigned int value) {
    int i = 8;
    while (i-- > 0) {
        buf[ptr++] = "0123456789abcdef"[(value>>(i*4))&0xf];
    }
}

 

现在我们来看一下 kprintf这个函数,它支持的格式:%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} 
定义在 include/kprintf.h, 它表示两种输出方案, KPL_DUMP 使用黑色背景白色前景显示字符,KPL_PANIC 使用黄色前景和红色背景。颜色常量定义在 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;
        }

        /* 下面是支持的打印格式 */
        switch (fmt[++i]) {
        case 's':
            s = (char *)args_next(args, char *);
            while (*s)
                buf[ptr++] = *s++;
            break;
        case 'c':
            buf[ptr++] = (char)args_next(args, int);
            break;
        case 'x':
            parse_hex((unsigned long)args_next(args, unsigned long));
            break;
        case 'd':
            parse_num((unsigned long)args_next(args, unsigned long), 10);
            break;
        case '%':
            buf[ptr++] = '%';
            break;
        default:
            buf[ptr++] = fmt[i];
            break;
        }
    }
    buf[ptr] = '\0';
    args_end(args);
    for (i=0; i<ptr; ++i)
        print_c(buf[i], KPL[kl].fg, KPL[kl].bg);            /* print_c()
是下层的显示函数,本文后面会有讲解 */

}


 

由于是内核程序,我们无法使用C用户库。所以一下memcpymemsetmemcpy函数需要自己实现,但是需要注意的是在BSD系统中,即便使用了-nostdlib,编译器仍然会产生System V中相关的memcpy等代码,具体情况我也不是很清除。这些函数的效率当然无法和linux内核中的内嵌汇编相比!我们暂时这样实现它们吧。
03/libcc.c

 

 

/* 下面函数对重叠区域也进行了处理 */
void
bcopy(const void *src, void *dest, unsigned int n) {
    const char *s = (const char *)src;
    char *d = (char *)dest;
    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(void *dest, const void *src, unsigned int n) {
    bcopy(src, dest, n);
    return dest;
}

void *
memset(void *dest, int c, unsigned int n) {
    char *d = (char *)dest;
    for (; n>0; --n)
        *d++ = (char)c;
    return dest;
}

int
memcmp(const void *s1, const void *s2, unsigned int n) {
    const char *s3 = (const char *)s1;
    const char *s4 = (const char *)s2;
    for (; n>0; --n) {
        if (*s3 > *s4)
            return 1;
        else if (*s3 < *s4)
            return -1;
        ++s3;
        ++s4;
    }
    return 0;
}

int
strcmp(const char *s1, const char *s2) {
    while (*s1 && *s2) {
        int r = *s1++ - *s2++;
        if (r)
            return r;
    }
    if (*s1 == *s2)
        return 0
    else
        return (*s1)?1:-1;
}

char *
strcpy(char *dest, const char *src) {
    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;
}

print_c
函数
直接操作显存区域一点也不方便,所以我们需要一个显示模块。这个就是我们的显卡驱动了,是不是不敢相信驱动是这么简单的事情?我们先来看一下一些常量定义:
03/include/scr.h

 

#define MAX_LINES    25                // bios默认设置屏幕为 80x25大小,彩色字符模式
#define MAX_COLUMNS  80
#define TAB_WIDTH    8                 //
必须是:2^n
#define VIDEO_RAM    0xb8000           //
显存地址

 

我们曾简要提到过这个地址,在字符模式下,适配器使用0xB8000-0xBF000作为视频内存。通常我们处于80x25大小屏幕,有16种颜色。由于一个屏幕只需要80x25x2个字节,即4k,所以该视频内存可以分为多个页。我们使用所有的页,但是当前只能有一个页面可见。为了显示一个字符,将用到2个字节,一个字节是字符值,另一个字节是字符属性(即颜色)。属性字节定义如下:

To display a single character, two bytes are being used which called the character byte and the attribute byte. The character byte contains the value of the character. The attribute byte is defined like this:

Bit 7

闪烁

Bits 6-4

背景色

Bit 3

明亮模式

Bit3 2-0

前景色

 

#define LINE_RAM    (MAX_COLUMNS*2)
#define PAGE_RAM    (MAX_LINE*MAX_COLUMNS)

#define BLANK_CHAR    (' ')
#define BLANK_ATTR    (0x70)        /*
白色前景,黑色背景 */

#define CHAR_OFF(x,y)    (LINE_RAM*(y)+2*(x))        /*
计算给定坐标xy的偏移地址(相对0xB8000 */
Calculates the offset of a given ordinary x, y from 0xB8000

 

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;

 

坐标系如下:

  ___________________\

 | 00          /

 |

\|/



03/scr.c

 

static int csr_x = 0;
static int csr_y = 0;

由于我们只用到了一个视频页,所以上面两个变量就可以存储坐标了。关于多页显示可以在网络上查找相关资料。


static void
scroll(int lines) {        
向上滚动屏幕多少行,就是一些内存复写。
    short *p = (short *)(VIDEO_RAM+CHAR_OFF(MAX_COLUMNS-1, MAX_LINES-1));
    int i = MAX_COLUMNS-1;
    memcpy((void *)VIDEO_RAM, (void *)(VIDEO_RAM+LINE_RAM*lines),
           LINE_RAM*(MAX_LINES-lines));


    for (; i>=0; --i)            //
说明这个for循环有问题,觉得应该改成下面这样:

    // for (i = i * lines; i>=0; --i)

        *p-- = (short)((BLANK_ATTR<<4)|BLANK_CHAR);

}

 

下面函数设置光标可能会引发竞态条件,但是print_c只准备在内核中使用,所以没有关中断。它可能会引起一些bug,但是我没有找到。译注:全局变量没有锁保护在设计上就是一种错误。这里的代码保护确实是没有做!读者应用到自己的内核时要小心了。

void
set_cursor(int x, int y) {

    csr_x = x;

    csr_y = y;

 

    outb(0x0e, 0x3d4);                                   设置光标高8位的准备工作

    outb(((csr_x+csr_y*MAX_COLUMNS)>>8)&0xff, 0x3d5);    设置光标高8

    outb(0x0f, 0x3d4);                                   设置光标低8位的准备工作
    outb(((csr_x+csr_y*MAX_COLUMNS))&0xff, 0x3d5);      
设置光标低8   
}

void
get_cursor(int *x, int *y) {
    *x = csr_x;
    *y = csr_y;
}

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;
        ((short *)p)[-1] = (short)((BLANK_ATTR<<4)|BLANK_CHAR);
        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);        //
设置光标位置
}

函数比较简单,没有分析的必要了,大家自己琢磨吧。

 

 


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理