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

mac2022-06-30  28

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

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

intrw_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 intrw_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 - 写字节数。// 返回已写字节数。inttty_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 终端初始化函数。// 初始化串口终端和控制台终端。voidtty_init (void){  rs_init ();            // 初始化串行中断程序和串行接口1 和2。(serial.c, 37)  con_init ();            // 初始化控制台终端。(console.c, 617)}

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

 

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

Linus Torvalds 21岁能写出一个内核,而我,一个奔三的程序员连个printf都写不出来。我只能感慨,同样是人,差距咋这么大呢?囧囧囧

转载于:https://www.cnblogs.com/zdl110110/archive/2010/12/14/1905801.html

相关资源:JAVA上百实例源码以及开源项目
最新回复(0)