手写操作系统(四十一)-printf实现

代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。

C语言的printf函数是格式化输出函数,将格式化后的信息输出到标准输出(通常是屏幕),但是真正起到格式化作用的是vsprintf函数,真正起输出作用的是write系统调用。

ssize_t write(int fd, const void *buf, size_t count);

write是个系统调用,write接受3个参数,其中的fd是文件描述符,buf是被输出数据所在的缓冲区,count是输出的字符数,write的功 能是把buf中count个字符写到文件描述符fd指向的文件中。

不过我们还没有实现文件系统,现在就先完成简单版 write 系统调用。

/lib/user/syscall.h

E在syscall.h 中的结构 enum SYSCALL_NR里添加新的子功能号SYS_WRITE

#ifndef __LIB_USER_SYSCALL_H
#define __LIB_USER_SYSCALL_H

#include "stdint.h"

/* 用来存放子功能号 */
enum SYSCALL_NR {
    SYS_GETPID,
    SYS_WRITE
};

uint32_t getpid(void);

uint32_t write(char* str);

#endif

 

在syscall.c 中增加系统调用的用户接口

修改/lib/user/syscall.c,只要加入个函数即可,它只需要一个参数,作用是向屏幕上打印str。

/* 打印字符串 str */
uint32_t write(char* str) {
    return _syscall1(SYS_WRITE, str);
}

 

在 syscall-init.c 中定义子功能处理函数 sys_write 并在 syscall_table 中注册

直接用console_put_str(str)输出str。最后用strlen(str)返回str的长度,也就是 write 会返回输出的字符个数。

/* 打印字符串 str(未实现文件系统前的版本) */
uint32_t sys_write(char* str) {
    console_put_str(str);   // 在终端输出字符串 str
    return strlen(str);
}


/* 初始化系统调用 */
void syscall_init(void) {
    put_str("syscall_init start\n");
    syscall_table[SYS_GETPID] = sys_getpid;
    syscall_table[SYS_WRITE] = sys_write;
    put_str("syscall_init done\n");
}

对应修改/userprog/syscall-init.h

#ifndef __USERPROG_SYSCALLINIT_H
#define __USERPROG_SYSCALLINIT_H

#include "stdint.h"

void syscall_init(void);

uint32_t sys_getpid(void);

uint32_t sys_write(char* str);

#endif

 

printf是vsprintf和write的封装,接下来完成vsprintf,用于可变参数解析的3个宏以及转换函数itoa,这些实现后就完成了基本的printf,本节的目标是使printf支持十六进制输出,即完成”%x”的功能。

int vsprintf(char *str, const char *format, va_list ap);

参数解释:

  • str:指向目标缓冲区的指针,格式化后的结果将写入此缓冲区。
  • format:格式控制字符串,定义输出的格式。
  • ap:va_list 类型的可变参数列表。

此函数的功能是把ap指向的可变参数,以字符串格式format中的符号%为替换标记,不修改原格式字符串format,将format中除”%类型字符”以外的内容复制到str,把”%类型字符”替换成具体参数后写入str中对应”%类型字符”的位置。

我们仿照实现

/lib/stdio.h

#ifndef __LIB_STDIO_H
#define __LIB_STDIO_H

#include "stdint.h"

typedef char* va_list;

uint32_t printf(const char* str, ...);

uint32_t vsprintf(char* str, const char* format, va_list ap);

#endif

/lib/stdio.c

#include "stdio.h"
#include "../kernel/interrupt.h"
#include "../kernel/global.h"
#include "string.h"
#include "user/syscall.h"
#include "kernel/print.h"

#define va_start(ap, v) ap = (va_list) &v   // 把 ap 指向第一个固定参数 v
// 这里把第一个char*地址赋给 ap 强制转换一下
#define va_arg(ap, t) *((t*) (ap += 4))     // ap 指向下一个参数并返回其值(强制类型转换 得到栈中参数)
#define va_end(ap) ap = NULL                // 清除 ap


/* 将整型转换成字符(integer to ascii), value:待转换的整数, buf_ptr_addr: 缓冲区指针的地址, base: 进制 */
static void itoa(uint32_t value, char** buf_ptr_addr, uint8_t base) {
    uint32_t m = value % base;      // 求模, 最先掉下来的是最低位(求出每一位)
    uint32_t i = value / base;      // 取整
    if(i) {
        // 如果倍数不为 0 则递归调用, 直到没有数位
        // 后面才修改指针是因为高位要写入低地址, 通过递归先在低地址写入高位
        // 在递归中会修改指针指向高地址, 因此在递归返回时低位会对应高地址
        itoa(i, buf_ptr_addr, base);
    }
    if(m < 10) {
        // 如果余数是 0~9, 转换后写入缓冲区并更新缓冲区指针(一级指针)
        // (*buf_ptr_addr) 得到一级指针, (*buf_ptr_addr)++: 一级指针指向下一个可写入位置
        // *((*buf_ptr_addr)++): (*buf_ptr_addr, 为一级指针) 指向的位置的值
        *((*buf_ptr_addr)++) = m + '0';         // 将数字 0~9 转换为字符'0'~'9'
    } else {
        // 否则余数是 A~F, 转换后写入缓冲区并更新缓冲区指针(一级指针)
        *((*buf_ptr_addr)++) = m - 10 + 'A';    // 将数字 A~F 转换为字符'A'~'F'
    }
}


/* 将参数 ap 按照格式 format 输出到字符串 str, 并返回替换后 str 长度 */
uint32_t vsprintf(char* str, const char* format, va_list ap) {
    char* buf_ptr = str;
    const char* index_ptr = format;
    char index_char = *index_ptr;
    int32_t arg_int;
    // 逐个字符操作
    while(index_char) {
        if(index_char != '%') {
            // 如果当前字符不是 %, 在当前缓冲区写入字符, 缓冲区指针指向下一个地址
            *(buf_ptr++) = index_char;
            // index_char 指向下一个 format 的字符
            index_char = *(++index_ptr);
            continue;
        }

        // 如果当前字符是 %
        index_char = *(++index_ptr);    // 得到 % 后面的字符
        switch(index_char) {
            case 'x':
                arg_int = va_arg(ap, int);      // ap 指向下一个参数并返回其值
                itoa(arg_int, &buf_ptr, 16);
                index_char = *(++index_ptr);    // 跳过格式字符并更新 index_char
                break;
        }
    }
    return strlen(str);
}


/* 格式化输出字符串 format */
uint32_t printf(const char* format, ...) {
    va_list args;
    va_start(args, format);     // 使 args 指向 format
    char buf[1024] = {0};       // 用于存储拼接后的字符串
    vsprintf(buf, format, args);
    va_end(args);
    return write(buf);        // 在终端输出 buf 中的字符串
}

文件开头定义了用于处理可变参数的3个宏。

#define va_start(ap, v) ap = (va_list) &v   // 把 ap 指向第一个固定参数 v
// 这里把第一个char*地址赋给 ap 强制转换一下
#define va_arg(ap, t) *((t*) (ap += 4))     // ap 指向下一个参数并返回其值(强制类型转换 得到栈中参数)
#define va_end(ap) ap = NULL                // 清除 ap

在linux中有这三个宏,他们的功能是

  • va_start(ap,v):参数ap是用于指向可变参数的指针变量,参数v是支持可变参数的函数的第1个参数(如对于printf来说,参数v就是字符串format)。此宏的功能是使指针ap指向v的地址,它的调用必须先于其他两个宏,相当于初始化ap指针的作用。
  • va_arg(ap,t):参数 ap 是用于指向可变参数的指针变量,参数t 是可变参数的类型,此宏的功能是使指针ap指向栈中下一个参数的地址并返回其值。
  •  va_end(ap):将指向可变参数的变量ap置为null,也就是清空指针变量ap。

所以我们同样仿照实现。

其中用到的va_list定义在stdio.h中,代码是”typedefchar* va_list,”,因此va_list是字符指针。

我们定义的这三个宏的功能如下:

  • va_start(ap, v)的作用是初始化指针ap,即把ap指向栈中可变参数中的第一个参数v,其实现是ap = (va_list)&v。 ap和v的类型都是char*,但不同的是ap用来存储v的地址&v, &v的类型实际是二级指针,因此在&v前用(va_list)强制转换为一级指针后再赋值给ap。
  • va_arg(ap, t)的作用是使指针ap指向栈中下一个参数,并根据下一个参数的类型t返回下一个参数的值,其实现是*((t*)(ap +=4))。va_arg(ap, t)必须在va_start(ap, v)之后调用,否则指针ap未初始化将导致错误。经va_start初始化后,ap已经指向了栈中可变参数中的第1个参数,由于32位栈的存储单元是4字节,故(ap+=4)将指向下一个参数在栈中的地址,而后将其强制转换成t型指针(t),最后再用*号取值,即*((t*)(ap+=4))是下一个参数的值。
  • va_end(ap)的作用就是回收指针 ap,清空,其实现为 ap=NULL。

itoa函数的作用是将整型转换为字符串,

itoa接受3个参数:

  • 第1个参数value是待转换的整数,
  • 第2个参数buf_ptr_addr是保存转换结果的缓冲区指针的地址,指针的指针,即二级指针char**,在函数实现中要将转换后的字符写到缓冲区指针指向的缓冲区中的1个或多个位置,这取决于进制转换后的数值的位数,比如十六进制0xd转换成十进制后变成数值13,13要被转换成字符’1’和3’,所以数值13变成字符后将占用缓冲区中两个字符位置,字符写到哪里是由缓冲区指针决定的,因此每写一个字符到缓冲区后,要更新缓冲区指针的值以使其指向缓冲区中下一个可写入的位置,这种原地修改指针的操作,最方便的是用其下一级指针类型来保存此指针的地址,故将一级指针的地址作为参数传给二级指针 buf_ptr_addr,这样便于原地修改一级指针。
  • 第3个参数base是转换的基数,也就是进制。比如%d 是按照十进制输出,%x 是按照十六进制输出,因此必须要有个数制转换,且将转换结果再转换成字符的函数。

iota的任务有两个:

一个是数制转换原理是把数值对基数求模,先掉下来的是最低位(个位),然后递归调用,依次求出次低位……次高位、最高位,直到数值无法整除基数,也就是没有数位可取,倍数为0时结束。uint32_t m = value % base对基数base求模,逐步求出每一级的最低位。和uint32_t i = value / base对基数base取整数倍,此整数倍用于itoa(i, buf_ptr_addr, base)递归调用。

另一个是将转换后的数值转换成字符,如果掉下来的位m是数字0~9,将其转换成对应字符的ASCII码,字符0~9在ASCII码表中是连续的,因此转换原理就是将数字加上字符0的ASCII码,然后将结果写入缓冲区并更新缓冲区指针(一级指针),即*((*buf_ptr_addr)++) = m + ‘0’。若掉下来的位m大于9(默认为0xA~0xF),则用0xA~0xF减去10 (0xA)所得到的差,加上字符A的ASCII码,便是字符’A’~’F’的ASCII码,然后将结果写入缓冲区并更新缓冲区指针(一级指针),即*((*buf_ptr_addr)++) = m – 10 + ‘A’。

vsprintf函数的功能是将参数ap按照格式format 输出到字符串 str 并返回替换后str 的长度。 char* buf_ptr = str用变量 buf_ptr 指向 str,用 buf_ptr 指代 str。 用index_ptr指代形参format,此处形参format是用printf 函数中的字符串format作为实参代入的。 index_char 指向格式字符串format中的每个字符,我们用它来找字符”%’。

用while(index_char)循环判断 format 中的每个字符index_char,直到index_char 为结束字符’\0’。 用来复制format中除%以外的字符到buf_ptr,也就是复制到str中。

循环遍历中,当index_char为字符%时,也就是找到了待替换的“%类型字符”,如“%x”为了获取“类型字符”,index_char = *(++index_ptr)先使++index_ptr跳过字符%,然后取值,将获取到的类型字符更新index_char。

随后在用switch结构对index_char判断,目前只支持十六进制的输出,也就是类型符号为x,故switch中只有case为x’的分支。 在此分支中,通过宏va_arg(ap, int)获取下一个整型参数,将结果存储到变量arg_int中。

随后在itoa(arg_int, &buf_ptr, 16)调用itoa将arg_int转换为十六进制,并存储到buf_ptr中,这里传入的是buf_ptr的地址,此时index_ptr指向类型字符x’,故在“index_char=*(++index_ptr)”跨过类型字符x,更新index_char为字符x后面的下一个字符,继续下一轮循环在format中找字符%。

printf函数声明为uint32_t printf(const char* format, …),其中的”.”表示可变参数。 变量 args其实就是 ap),用它来指向参数,调用宏 va_start(args, format) 对其初始化。 1024字节大小的数组buf用来存储由vsprintf处理的结果,也就是str,完成之后通过宏va_end(args)使args清空。 最后执行系统调用write(buf),将处理后的字符串输出。

接下来修改main文件测试一下:

#include "print.h"
#include "init.h"
#include "debug.h"
#include "memory.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall-init.h"
#include "syscall.h"
#include "stdio.h"

void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);

int prog_a_pid = 0, prog_b_pid = 0;

int main() {
    put_str("I am kernel\n");
    init_all();

    process_execute(u_prog_a, "user_prog_a");
    process_execute(u_prog_b, "user_prog_b");

    intr_enable();    // 打开中断, 使时钟中断起作用
    console_put_str(" main_pid:0x");
    console_put_int(sys_getpid());
    console_put_char('\n');
    thread_start("k_thread_a", 31, k_thread_a, "argA ");
    thread_start("k_thread_b", 31, k_thread_b, "argB ");

    while(1);
    return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
    char* para = arg;
    console_put_str(" thread_a_pid:0x");
    console_put_int(sys_getpid());
    console_put_char('\n');
    while(1);
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
    char* para = arg;
    console_put_str(" thread_b_pid:0x");
    console_put_int(sys_getpid());
    console_put_char('\n');
    while(1);
}

/* 测试用户进程 */
void u_prog_a(void) {
    printf(" prog_a_pid:0x%x\n", getpid());
    while(1);
}

/* 测试用户进程 */
void u_prog_b(void) {
    printf(" prog_b_pid:0x%x\n", getpid());
    while(1);
}

makefile对应修改一下

执行结果如下:

 

上面我们只是让它支持十六进制,接下来支持“%c”、“%s”和“%d”。

/lib/stdio.c

/* 将参数 ap 按照格式 format 输出到字符串 str, 并返回替换后 str 长度 */
uint32_t vsprintf(char* str, const char* format, va_list ap) {
    char* buf_ptr = str;
    const char* index_ptr = format;
    char index_char = *index_ptr;
    int32_t arg_int;
    char* arg_str;

    // 逐个字符操作
    while(index_char) {
        if(index_char != '%') {
            // 如果当前字符不是 %, 在当前缓冲区写入字符, 缓冲区指针指向下一个地址
            *(buf_ptr++) = index_char;
            // index_char 指向下一个 format 的字符
            index_char = *(++index_ptr);
            continue;
        }

        // 如果当前字符是 %
        index_char = *(++index_ptr);    // 得到 % 后面的字符
        switch(index_char) {
            case 's':                            // %s: 字符串   
                arg_str = va_arg(ap, char*);     // ap 指向下一个参数并返回其值
                strcpy(buf_ptr, arg_str);        // 将 ap 指向的字符串拷贝到 buf_ptr 
                buf_ptr += strlen(arg_str);      // 更新 buf_ptr 跨过拷贝的字符串
                index_char = *(++index_ptr);     // 跳过格式字符并更新 index_char
                break;

            case 'c':                            // %c: 字符 char
                *(buf_ptr++) = va_arg(ap, char);
                index_char = *(++index_ptr);
                break;

            case 'd':                            // %d: 10进制数
                arg_int = va_arg(ap, int);       // ap 指向下一个参数并返回其值
                // 若是负数, 将其转为正数后, 在正数前面输出个负号 '-'
                if(arg_int < 0) {
                    arg_int = 0 - arg_int;
                    *buf_ptr++ = '-';
                }
                itoa(arg_int, &buf_ptr, 10);
                index_char = *(++index_ptr);     // 跳过格式字符并更新 index_char
                break;

            case 'x':                            // %x: 16进制数
                arg_int = va_arg(ap, int);       // ap 指向下一个参数并返回其值
                itoa(arg_int, &buf_ptr, 16);
                index_char = *(++index_ptr);     // 跳过格式字符并更新 index_char
                break;
        }
    }
    return strlen(str);
}


/* 同 printf 不同的地方就是字符串不是写到终端, 而是写到 buf 中 */
uint32_t sprintf(char* buf, const char* format, ...) {
    va_list args;
    uint32_t retval;        // buf 中字符串的长度
    va_start(args, format);
    retval = vsprintf(buf, format, args);
    va_end(args);
    return retval;
}

/lib/stdio.h

#ifndef __LIB_STDIO_H
#define __LIB_STDIO_H

#include "stdint.h"

typedef char* va_list;

uint32_t printf(const char* str, ...);

uint32_t vsprintf(char* str, const char* format, va_list ap);

uint32_t sprintf(char* buf, const char* format, ...);

#endif

main文件调用

#include "print.h"
#include "init.h"
#include "debug.h"
#include "memory.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall-init.h"
#include "syscall.h"
#include "stdio.h"

void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);

int prog_a_pid = 0, prog_b_pid = 0;

int main() {
    put_str("I am kernel\n");
    init_all();

    process_execute(u_prog_a, "u_prog_a");
    process_execute(u_prog_b, "u_prog_b");

    console_put_str(" I am main, my pid:0x");
    console_put_int(sys_getpid());
    console_put_char('\n');

    intr_enable();    // 打开中断, 使时钟中断起作用
    thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
    thread_start("k_thread_b", 31, k_thread_b, "I am thread_b ");

    while(1);
    return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
    char* para = arg;
    console_put_str(" I am thread_a, my pid:0x");
    console_put_int(sys_getpid());
    console_put_char('\n');
    while(1);
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
    char* para = arg;
    console_put_str(" I am thread_b, my pid:0x");
    console_put_int(sys_getpid());
    console_put_char('\n');
    while(1);
}

/* 测试用户进程 */
void u_prog_a(void) {
    char* name = "prog_a";
    printf(" I am %s, my pid:%d%c", name, getpid(),'\n');
    while(1);
}

/* 测试用户进程 */
void u_prog_b(void) {
    char* name = "prog_b";
    printf(" I am %s, my pid:%d%c", name, getpid(), '\n');
    while(1);
}

执行结果如图:

 

参考

郑钢著操作系统真象还原

田宇著一个64位操作系统的设计与实现

丁渊著ORANGE’S:一个操作系统的实现

暂无评论

发送评论 编辑评论

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