原创之--------printf详解

       最近摆弄2440开发板,想要研究printf的实现。google一下,发现结果不能令人恭维,几乎无一例外的在谈论C语言的可变参数,对其他的关键问题却只字不提。所以我想写这篇文章,记录一下自己的学习经历,也希望能够给其他人带来一些启发。
早在大学二年级学习C语言的时候,在Turbo C下写程序。printf,一个小黑框显示一些数字,诶!感觉很奇妙,但至于更深层的东西,虽然有疑问,但能力有限,因此没有深入研究过。如今发现,这个问题看似简单,其实不然,这其中牵涉到许多东西,比如汇编语言,操作系统,中断等。这里先声明,本人不是计算机专业出身,所学东西全靠自学,因此这里所讲到的大部分东西对计算机专业的来说可能不正确。欢迎大家提出宝贵意见.邮箱zdl110110163.com,或QQ467026758。
       现在说正题,首先C语言,与其说是一种编程语言,不如说是一个标准,如果你要遵循这个标准,你就要实现它的功能。printf函数就是一个例子,windows有它的实现,linux,unix也有自己的实现。C语言的可移植性可能就来自这里吧。printf函数通常叫输出函数,我觉得应该叫”字符串格式化函数”比较贴切。为什么这么说呢,我先贴一段CSDN里边的代码,原文地址可以参考这里-CSDN论坛
#include <stdarg.h>
#include <stdio.h>

/*
 * Conver int to string based on radix (usually 2, 8, 10, and 16)
 */
char *itoa(int num, char *str, int radix) 
{
    char string[] = "0123456789abcdefghijklmnopqrstuvwxyz";

    char* ptr = str;
    int i;
    int j;

    while (num)
    {
        *ptr++ = string[num % radix];
        num /= radix;

        if (num < radix)
        {
            *ptr++ = string[num];
            *ptr = '\0';
            break;
        }
    }

    j = ptr - str - 1;

    for (i = 0; i < (ptr - str) / 2; i++)
    {
        int temp = str[i];
        str[i] = str[j];
        str[j--] = temp;
    }

    return str;
}

/*
 * A simple printf function. Only support the following format:
 * Code Format
 * %c character
 * %d signed integers
 * %i signed integers
 * %s a string of characters
 * %o octal
 * %x unsigned hexadecimal
 */
int vprintf( const char* format, ...)
{
    va_list arg;
    int done = 0;

    va_start (arg, format);
    //done = vfprintf (stdout, format, arg);

    while( *format != '\0')
    {
        if( *format == '%')
        {
            if( *(format+1) == 'c' )
            {
                char c = (char)va_arg(arg, int);
                putc(c, stdout);
            } else if( *(format+1) == 'd' || *(format+1) == 'i')
            {
                char store[20];
                int i = va_arg(arg, int);
                char* str = store;
                itoa(i, store, 10);
                while( *str != '\0') putc(*str++, stdout); 
            } else if( *(format+1) == 'o')
            {
                char store[20];
                int i = va_arg(arg, int);
                char* str = store;
                itoa(i, store, 8);
                while( *str != '\0') putc(*str++, stdout); 
            } else if( *(format+1) == 'x')
            {
                char store[20];
                int i = va_arg(arg, int);
                char* str = store;
                itoa(i, store, 16);
                while( *str != '\0') putc(*str++, stdout); 
            } else if( *(format+1) == 's' )
            {
                char* str = va_arg(arg, char*);
                while( *str != '\0') putc(*str++, stdout);
            }

            // Skip this two characters.

            format += 2;
        } else {
            putc(*format++, stdout);
        }
    }

    va_end (arg);

    return done;
} 

int main(int argc, char* argv[])
{ 
    int n = 255;
    char str[] = "hello, world!";

    printf("n = %d\n", n);
    printf("n = %i\n", n);
    printf("n = %o\n", n);
    printf("n = %x\n", n); 
    printf("first char = %c\n", str[0]); 
    printf("str = %s\n", str);
    printf("%s\tn = %d\n", str, n);

    // Test vprintf function

    printf("---------------vprintf--------------\n");
    
    vprintf("n = %d\n", n);
    vprintf("n = %i\n", n); 
    vprintf("n = %o\n", n);
    vprintf("n = %x\n", n);
    vprintf("first char = %c\n", str[0]); 
    vprintf("str = %s\n", str);
    vprintf("%s\tn = %d\n", str, n);

    return 0;
}
 
---------------------------result:
n = 255
n = 255
n = 377
n = ff
first char = h
str = hello, 
hello, n = 255
---------------my_printf--------------
n = 255
n = 255
n = 377
n = ff
first char = h
str = hello, 
hello, n = 255
 
下边是微软的代码

int __cdecl printf ( const char *format, ... ) /* * stdout 'PRINT', 'F'ormatted */ { va_list arglist; int buffing; int retval; va_start(arglist, format); _ASSERTE(format != NULL); _lock_str2(1, stdout); buffing = _stbuf(stdout); retval = _output(stdout,format,arglist); _ftbuf(buffing, stdout); _unlock_str2(1, stdout); return(retval); }

 

我们可以看到,在两者的代码中都用到其他的函数,比如putc,_ftbuf,_unlock_str2……这些完成什么工作我们不得而知。

下边大致讲一下我对printf整个过程的理解:首先利用C语言的可变参数对要输出的字符串进行格式化,将相应参数送入寄存器然后产生中断,比如windows下的int 21h,linux下的 int 80h.中断产生后,根据中断向量表进入由操作系统设置好的中断处理函数,至于中断处理函数如何让字符在显示器上显示出来,我们后边慢慢说。

我这里拿linux0.11版本做一下分析:

main.c中的printf函数:

static int printf (const char *fmt, ...)
    // 产生格式化信息并输出到标准输出设备stdout(1),这里是指屏幕上显示。参数'*fmt'指定输出将
    // 采用的格式,参见各种标准C 语言书籍。该子程序正好是vsprintf 如何使用的一个例子。
    // 该程序使用vsprintf()将格式化的字符串放入printbuf 缓冲区,然后用write()将缓冲区的内容
    // 输出到标准设备(1--stdout)。
{
  va_list args;
  int i;

    va_start (args, fmt);
    write (1, printbuf, i = vsprintf (printbuf, fmt, args));
    va_end (args);
    return i;
}

 

我们看到这里调用了write函数,我们跳转到write函数的定义处,位于unistd.h头文件中,那么,write函数实现在哪里呢?我通过查找,找到这个:

//// 写文件系统调用函数。
// 该宏结构对应于函数:int write(int fd, const char * buf, off_t count)
// 参数:fd - 文件描述符;buf - 写缓冲区指针;count - 写字节数。
// 返回:成功时返回写入的字节数(0 表示写入0 字节);出错时将返回-1,并且设置了出错号

_syscall3 (int, write, int, fd, const char *, buf, off_t, count);

  那么_syscall3这个到底是什么呢,我们进一步跟踪发现_syscall3的定义位于unistd.h头文件中:

 

// 有3 个参数的系统调用宏函数。type name(atype a, btype b, ctype c)
// %0 - eax(__res),%1 - eax(__NR_name),%2 - ebx(a),%3 - ecx(b),%4 - edx(c)。
#define _syscall3(type,name,atype,a,btype,b,ctype,c) \
type name(atype a,btype b,ctype c) \
{ \
long __res; \
__asm__ volatile ( "int $0x80" \
: "=a" (__res) \
: "" (__NR_##name), "b" ((long)(a)), "c" ((long)(b)), "d" ((long)(c))); \
if (__res>=0) \
return (type) __res; \
errno=-__res; \
return -1; \
}

从上面我们可以看出,_syscall3实际上是一个宏,我们调用的write函数实际上被上面的内嵌汇编代码所替代。我们可以清楚得看到,调用了int 80中断,更重要的一句在于(__NR_##name……这一句,这一句的作用就是跳转到中断处理函数。这里我们调用write函数,经过这句,我们调用__NR_write所代表的中断处理函数。我们继续看__NR_write。查找发现__NR_write位于unistd.h中:

#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4

我们发现__NR_write的值为4,这个值有什么用呢?我们学过操作系统都知道,中断都是通过中断向量表(我不清楚这样叫对不对,大家理解就行)来实现中断处理的,而我们这个__NR_write就是我们要调用的中断处理函数在中断向量表中的索引。好了,下边只要我们找到了中断向量表,查找一下,就应该找到对应的中断处理函数了。在linux0.11版本中,中断向量表在sys.h头文件中。

sys.h文件部分代码:

extern int sys_write ();    // 写文件。 (fs/read_write.c, 83)

// 系统调用函数指针表。用于系统调用中断处理程序(int 0x80),作为跳转表。
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
  sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
  sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
  sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
  sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
  sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
  sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
  sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
  sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
  sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
  sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
  sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
  sys_setreuid, sys_setregid
};

通过上面的代码我们可以看到,我们用索引4找到了sys_write函数,同时我也在中断向量表的上边找到了sys_write函数的定义,我们可以看到它的实现位于read_write.c文件中,跟踪过去:

int  sys_write (unsigned int fd, char *buf, int count)
{
  struct file *file;
  struct m_inode *inode;

// 如果文件句柄值大于程序最多打开文件数NR_OPEN,或者需要写入的字节计数小于0,或者该句柄
// 的文件结构指针为空,则返回出错码并退出。
  if (fd >= NR_OPEN || count < 0 || !(file = current->filp[fd]))
    return -EINVAL;
// 若需读取的字节数count 等于0,则返回0,退出
  if (!count)
    return 0;
// 取文件对应的i 节点。若是管道文件,并且是写管道文件模式,则进行写管道操作,若成功则返回
// 写入的字节数,否则返回出错码,退出。
  inode = file->f_inode;
  if (inode->i_pipe)
    return (file->f_mode & 2) ? write_pipe (inode, buf, count) : -EIO;
// 如果是字符型文件,则进行写字符设备操作,返回写入的字符数,退出。
  if (S_ISCHR (inode->i_mode))
   return rw_char (WRITE, inode->i_zone[0], buf, count, &file->f_pos);
// 如果是块设备文件,则进行块设备写操作,并返回写入的字节数,退出。
  if (S_ISBLK (inode->i_mode))
    return block_write (inode->i_zone[0], &file->f_pos, buf, count);
// 若是常规文件,则执行文件写操作,并返回写入的字节数,退出。
  if (S_ISREG (inode->i_mode))
    return file_write (inode, file, buf, count);
// 否则,显示对应节点的文件模式,返回出错码,退出。
  printk ("(Write)inode->i_mode=%06o\n\r", inode->i_mode);
  return -EINVAL;
}

由于在类unix系统中,标准输入输出都是被当成“文件”来对待的,所以在上面的代码中,我们通过文件描述符找到对应的文件,判断文件类型,然后调用不同的函数。我们的标准输入输出应该是字符型文件,所以我们重点看rw_char这个函数。

int
rw_char (int rw, int dev, char *buf, int count, off_t * pos)
{
  crw_ptr call_addr;

// 如果设备号超出系统设备数,则返回出错码。
  if (MAJOR (dev) >= NRDEVS)
    return -ENODEV;
// 若该设备没有对应的读/写函数,则返回出错码。
  if (!(call_addr = crw_table[MAJOR (dev)]))
    return -ENODEV;
// 调用对应设备的读写操作函数,并返回实际读/写的字节数。
  return call_addr (rw, MINOR (dev), buf, count, pos);
}

这里我们看到了类似中断的方法,crw_table定义如下

// 字符设备读写函数指针表。
static crw_ptr crw_table[] = {
  NULL,                /* nodev *//* 无设备(空设备) */
  rw_memory,            /* /dev/mem etc *//* /dev/mem 等 */
  NULL,                /* /dev/fd *//* /dev/fd 软驱 */
  NULL,                /* /dev/hd *//* /dev/hd 硬盘 */
  rw_ttyx,            /* /dev/ttyx *//* /dev/ttyx 串口终端 */
  rw_tty,            /* /dev/tty *//* /dev/tty 终端 */
  NULL,                /* /dev/lp *//* /dev/lp 打印机 */
  NULL
};

我们接下来看看rw_ttyx函数,rw_tty跟rw_ttyx类似。

//// 串口终端读写操作函数。
// 参数:rw - 读写命令;minor - 终端子设备号;buf - 缓冲区;cout - 读写字节数;
// pos - 读写操作当前指针,对于终端操作,该指针无用。
// 返回:实际读写的字节数。
static int
rw_ttyx (int rw, unsigned minor, char *buf, int count, off_t * pos)
{
  return ((rw == READ) ? tty_read (minor, buf, count) :
      tty_write (minor, buf, count));
}

 

// tty 数据结构。
struct tty_struct
{
  struct termios termios;    // 终端io 属性和控制字符数据结构。
  int pgrp;            // 所属进程组。
  int stopped;            // 停止标志。
  void (*write) (struct tty_struct * tty);    // tty 写函数指针。
  struct tty_queue read_q;    // tty 读队列。
  struct tty_queue write_q;    // tty 写队列。
  struct tty_queue secondary;    // tty 辅助队列(存放规范模式字符序列),
};   

// tty 等待队列数据结构。
struct tty_queue
{
  unsigned long data;        // 等待队列缓冲区中当前数据指针字符数[??])。
// 对于串口终端,则存放串行端口地址。
  unsigned long head;        // 缓冲区中数据头指针。
  unsigned long tail;        // 缓冲区中数据尾指针。
  struct task_struct *proc_list;    // 等待进程列表。
  char buf[TTY_BUF_SIZE];    // 队列的缓冲区。
};

  

tty_write函数部分代码:

//// tty 写函数。
// 参数:channel - 子设备号;buf - 缓冲区指针;nr - 写字节数。
// 返回已写字节数。
int
tty_write (unsigned channel, char *buf, int nr)
{
  static cr_flag = 0;
  struct tty_struct *tty;
  char c, *b = buf;

// 本版本linux 内核的终端只有3 个子设备,分别是控制台(0)、串口终端1(1)和串口终端2(2)。
// 所以任何大于2 的子设备号都是非法的。写的字节数当然也不能小于0 的。
  if (channel > 2 || nr < 0)
    return -1;
// tty 指针指向子设备号对应ttb_table 表中的tty 结构。
  tty = channel + tty_table;

。。。。。。省略一部分代码

// 若字节全部写完,或者写队列已满,则程序执行到这里。调用对应tty 的写函数,若还有字节要写,
// 则等待写队列不满,所以调用调度程序,先去执行其它任务。
      tty->write (tty);
      if (nr > 0)

我们从红色部分可以看到,此时tty设备应该查找tty_table数组,随后调用我们跟中进去发现调用了tty设备的写函数。以下代码:红色部分就是控制台的写函数

struct tty_struct tty_table[] = {
  {
   {ICRNL,            /* change incoming CR to NL *//* 将输入的CR 转换为NL */
    OPOST | ONLCR,        /* change outgoing NL to CRNL *//* 将输出的NL 转CRNL */
    0,                // 控制模式标志初始化为0。
    ISIG | ICANON | ECHO | ECHOCTL | ECHOKE,    // 本地模式标志。
    0,                /* console termio */// 控制台termio。
    INIT_C_CC},            // 控制字符数组。
   0,                /* initial pgrp */// 所属初始进程组。
   0,                /* initial stopped */// 初始停止标志。
   con_write,            // tty 写函数指针。
   {0, 0, 0, 0, ""},        /* console read-queue */// tty 控制台读队列。
   {0, 0, 0, 0, ""},        /* console write-queue */// tty 控制台写队列。
   {0, 0, 0, 0, ""}        /* console secondary queue */// tty 控制台辅助(第二)队列。
   }, {
       {0,            /* no translation */// 输入模式标志。0,无须转换。
    0,            /* no translation */// 输出模式标志。0,无须转换。
    B2400 | CS8,        // 控制模式标志。波特率2400bps,8 位数据位。
    0,            // 本地模式标志0。
    0,            // 行规程0。
    INIT_C_CC},        // 控制字符数组。
       0,            // 所属初始进程组。
       0,            // 初始停止标志。
       rs_write,        // 串口1 tty 写函数指针。
       {0x3f8, 0, 0, 0, ""},    /* rs 1 */// 串行终端1 读缓冲队列。
       {0x3f8, 0, 0, 0, ""},    // 串行终端1 写缓冲队列。
       {0, 0, 0, 0, ""}        // 串行终端1 辅助缓冲队列。
       }, {
       {0,            /* no translation */// 输入模式标志。0,无须转换。
        0,            /* no translation */// 输出模式标志。0,无须转换。
        B2400 | CS8,    // 控制模式标志。波特率2400bps,8 位数据位。
        0,            // 本地模式标志0。
        0,            // 行规程0。
        INIT_C_CC},        // 控制字符数组。
       0,            // 所属初始进程组。
       0,            // 初始停止标志。
       rs_write,        // 串口2 tty 写函数指针。
       {0x2f8, 0, 0, 0, ""},    /* rs 2 */// 串行终端2 读缓冲队列。
       {0x2f8, 0, 0, 0, ""},    // 串行终端2 写缓冲队列。
       {0, 0, 0, 0, ""}    // 串行终端2 辅助缓冲队列。
       }
};

接下来我们继续看con_write函数,代码太多,我就省略了。它的功能就是从缓冲队列中取出字符,处理之后复制到显存中。我们知道计算机有显卡,显卡将内部的数据发送到显示器,显示器就能显示相应的字符。我们要做的就是不断更新显卡的内容(地址好像是0xb8000)。这里我要强烈推荐《IBM PC 8086汇编语言》这本书,里边对这部分内容讲解的非常好。

在linux main.c 文件main函数中调用了:

tty_init ();            // tty 初始化。 (kernel/chr_dev/tty_io.c,105 行)

//// tty 终端初始化函数。
// 初始化串口终端和控制台终端。
void
tty_init (void)
{
  rs_init ();            // 初始化串行中断程序和串行接口1 和2。(serial.c, 37)
  con_init ();            // 初始化控制台终端。(console.c, 617)
}

有兴趣的读者可以看看con_init()的代码,里边包括了获取显卡的模式,复制字符串到显卡等内容。

 

   到此为止,一个printf的介绍基本结束了,这其中有很多需要修改的地方,以后有时间慢慢整理。

  Linus Torvalds 21岁能写出一个内核,而我,一个奔三的程序员连个printf都写不出来。我只能感慨,同样是人,差距咋这么大呢?囧囧囧
posted @ 2010-12-14 16:37  赤子之心  阅读(2081)  评论(0编辑  收藏  举报