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上百实例源码以及开源项目