一核心思维
二故事讲解—细节讲解—回归源代码
从哪里开始这个故事呢?printf(“Host Name: %s”, name);
Linux0.11的main.c中:
// 下面函数产生格式化信息并输出到标准输出设备stdout(1),这里是指屏幕上显示。参数'*fmt'
// 指定输出将采用的格式,具体可以看标准C语言书籍。该子程序正好是vsprintf如何使用一个
// 简单例子。该程序使用vsprintf()将格式化的字符串放入printfbuf缓冲区,然后用write()将
// 缓冲区的内容输出到标准设备(1--stdout).vsprintf()函数实现在kernel/vsprintf.c中。
static int printf(const char *fmt, ...)
{
va_list args;
int i;
va_start(args, fmt);
write(1,printbuf,i=vsprintf(printbuf, fmt, args));
va_end(args);
return i;
}
在linux0.11/kernel/vsprintf.c中:
/*
* linux/kernel/vsprintf.c
*
* (C) 1991 Linus Torvalds
*/
/* vsprintf.c -- Lars Wirzenius & Linus Torvalds. */
/*
* Wirzenius wrote this portably, Torvalds fucked it up :-)
*/
// Lars Wirzenius 是Linus的好友,在Helsinki大学的时候曾同处一间办公室。在1991年夏季开发linux时,
// Linus当时对C语言还不是很熟悉,还不会使用可变参数列表函数功能。因此Lars Wirzenius就为他编写了
// 这段用于内核显示信息的代码。他后来(1998年)承认这段代码中有一个bug,直到1994年才有人发现,
// 并予以纠正。这个bug是在使用*作为输出域宽度时,忘记递增指针跳过这个星号了。在本代码中这个bug
// 还仍然存在(163行)。他的个人主页是http://liw.iki.fi/liw/
// 标准参数头文件,以宏的形式定义变量参数列表。主要说明了一个类型(va_list)和
// 三个宏(va_start,va_arg和va_end),用于vsprintf,vprintf,vfprintf函数。
#include <stdarg.h>
#include <string.h>
/* we use this so that we can do without the ctype library */
#define is_digit(c) ((c) >= '0' && (c) <= '9')
// 将字符数字转换成整数。输入是数字串指针的指针,返回是结果的数值,另外指针将前移。
static int skip_atoi(const char **s)
{
int i=0;
while (is_digit(**s))
i = i*10 + *((*s)++) - '0'; // 这里导致指针前移
return i;
}
// 定义常用符号常数
#define ZEROPAD 1 /* pad with zero */
#define SIGN 2 /* unsigned/signed long */
#define PLUS 4 /* show plus */
#define SPACE 8 /* space if plus */
#define LEFT 16 /* left justified */
#define SPECIAL 32 /* 0x */
#define SMALL 64 /* use 'abcdef' instead of 'ABCDEF' */
// 除法操作,输入:n为被除数,base为除数;结果:n为商,函数返回值为余数。
#define do_div(n,base) ({ \
int __res; \
__asm__("divl %4":"=a" (n),"=d" (__res):"0" (n),"1" (0),"r" (base)); \
__res; })
// 将整数转换为指定进制的字符串。
// 输入:num-整数;base-进制;size-字符串长度;precision-数字长度(精度);type-类型选项。
// 输出:str字符串指针
static char * number(char * str, int num, int base, int size, int precision, int type)
{
char c,sign,tmp[36];
const char *digits="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
int i;
// 根据类型定义字母集,默认是大写字母
if (type&SMALL) digits="0123456789abcdefghijklmnopqrstuvwxyz";
// 如果类型指出要左调整(靠左边界),则屏蔽类型中的填零标志。
// TODO: 这句没看懂?? 为什么要把最低位置为0
if (type&LEFT) type &= ~ZEROPAD;
// 本程序只能处理的进制范围:2-36
if (base<2 || base>36)
return 0;
// TODO: 这句跟LEFT相关??
c = (type & ZEROPAD) ? '0' : ' ' ;
if (type&SIGN && num<0) {
sign='-';
num = -num;
} else
sign=(type&PLUS) ? '+' : ((type&SPACE) ? ' ' : 0);
// 如果是带符号的,字符串宽度就减一
if (sign) size--;
if (type&SPECIAL) {
if (base==16) size -= 2; // 16进制需要减2,用于前面的0x
else if (base==8) size--; // 8进制的减1,因为前面的0
}
i=0;
if (num==0)
tmp[i++]='0';
else while (num!=0)
tmp[i++]=digits[do_div(num,base)];
if (i>precision) precision=i; // 若字符个数大于精度值,精度值扩展为字符个数
size -= precision; // 宽度再减去用于存放数值字符的个数
// 将转换结果放在str中,如果类型中没有填零(ZEROPAD)和左靠齐标志,
// 则在str中首先填放剩余宽度值指出的空格数。若需要带符号位,则存入符号
if (!(type&(ZEROPAD+LEFT)))
while(size-->0)
*str++ = ' ';
if (sign)
*str++ = sign;
// 如果是特殊转换的处理,8进制和16进制分别填入0/0x/0X
if (type&SPECIAL) {
if (base==8)
*str++ = '0';
else if (base==16) {
*str++ = '0';
*str++ = digits[33]; // 'x' or 'X'
}
}
// 如果类型么有左靠齐标志,则在剩余的宽度中存放c字符(‘0’或者空格)
if (!(type&LEFT))
while(size-->0)
*str++ = c;
// 若i存有数值num的数字个数,若数字个数小于精度值,则str中放入 精度值-i 个'0'
while(i<precision--)
*str++ = '0';
// 将数值转换好的数字字符填入str中,共i个
while(i-->0)
*str++ = tmp[i];
// 若宽度值仍大于零,则表达类型标志中有左靠齐标志,则在剩余宽度中放入空格
while(size-->0)
*str++ = ' ';
return str;
}
// 格式化输出
int vsprintf(char *buf, const char *fmt, va_list args)
{
int len;
int i;
char * str; // 用于存放转换过程中的字符串
char *s;
int *ip;
int flags; /* flags to number() */
int field_width; /* width of output field */
int precision; /* min. # of digits for integers; max
number of chars for from string */
int qualifier; /* 'h', 'l', or 'L' for integer fields */
// 扫描格式字符串,对于不是 % 的就依次存入str
for (str=buf ; *fmt ; ++fmt) {
if (*fmt != '%') {
*str++ = *fmt;
continue;
}
// 取得格式指示字符串中的标志域,并将标志常量放入flags变量中
/* process flags */
flags = 0;
repeat:
++fmt; /* this also skips first '%' */
switch (*fmt) {
case '-': flags |= LEFT; goto repeat;
case '+': flags |= PLUS; goto repeat;
case ' ': flags |= SPACE; goto repeat;
case '#': flags |= SPECIAL; goto repeat;
case '0': flags |= ZEROPAD; goto repeat;
}
// 取当前参数数字段宽度域值,放入field_width变量中,如果宽度域中是数值则直接取其为宽度值。
// 如果宽度域中是字符'*',表示下一个参数指定宽度。因此调用va_arg取宽度值。若此时宽度值
// 小于0,则该负数表示其带有标志域'-'标志(左靠齐),因此还需要在标志变量中填入该标志,并
// 将字段宽度值取为其绝对值。
/* get field width */
field_width = -1;
if (is_digit(*fmt))
field_width = skip_atoi(&fmt);
else if (*fmt == '*') {
/* it's the next argument */
field_width = va_arg(args, int); // 这里有个bug,应插入++fmt。// TODO: 不懂
if (field_width < 0) {
field_width = -field_width;
flags |= LEFT;
}
}
// 取格式转换串的精度域,并放入precision变量中。精度域开始的标志是'.'.
// 其处理过程与上面宽度域的类似。如果精度域中是数值则直接取其为精度值。如果精度域中是
// 字符'*',表示下一个参数指定精度。因此调用va_arg取精度值。若此时宽度值小于0,则将
// 字段精度值取为0.
/* get the precision */
precision = -1;
if (*fmt == '.') {
++fmt;
if (is_digit(*fmt))
precision = skip_atoi(&fmt);
else if (*fmt == '*') {
/* it's the next argument */
precision = va_arg(args, int);
}
if (precision < 0)
precision = 0;
}
/* get the conversion qualifier */
qualifier = -1;
if (*fmt == 'h' || *fmt == 'l' || *fmt == 'L') {
qualifier = *fmt;
++fmt;
}
// 分析转换指示符
switch (*fmt) {
// ‘c’ 表示对应参数应是字符。此时如果标志域表明不是左靠齐,则该字段前面
// 放入'宽度域值-1'个空格字符,然后再放入参数字符。如果宽度域大于0,
// 则表示为左靠齐,则在参数字符后面添加'宽度值-1'个空格字符
case 'c':
if (!(flags & LEFT))
while (--field_width > 0)
*str++ = ' ';
*str++ = (unsigned char) va_arg(args, int);
while (--field_width > 0)
*str++ = ' ';
break;
// 's'表示对应参数是字符串。首先取参数字符串的长度,若其超过了精度域值,
// 则扩展精度域=字符串长度。此时如果标志域表明不是左靠齐,则该字段前放入
// '宽度值-字符串长度'个空格字符。然后再放入参数字符串。如果宽度域还大于0,
// 则表示为左靠齐,则在参数字符串后面添加'宽度值-字符串长度'个空格字符。
case 's':
s = va_arg(args, char *);
len = strlen(s);
if (precision < 0)
precision = len;
else if (len > precision)
len = precision;
if (!(flags & LEFT))
while (len < field_width--)
*str++ = ' ';
for (i = 0; i < len; ++i)
*str++ = *s++;
while (len < field_width--)
*str++ = ' ';
break;
// 'o'表示8进制,通过number函数处理
case 'o':
str = number(str, va_arg(args, unsigned long), 8,
field_width, precision, flags);
break;
// 'p'表示一个指针类型,此时若该参数没有设置宽度域,默认宽度域为8
// 并且需要添零,然后用number函数处理
case 'p':
if (field_width == -1) {
field_width = 8;
flags |= ZEROPAD;
}
str = number(str,
(unsigned long) va_arg(args, void *), 16,
field_width, precision, flags);
break;
// 'x'-转成16进制
case 'x':
flags |= SMALL;
case 'X':
str = number(str, va_arg(args, unsigned long), 16,
field_width, precision, flags);
break;
// 'd' & 'i' 表示带符号整数;'u' 表示无符号整数
case 'd':
case 'i':
flags |= SIGN;
case 'u':
str = number(str, va_arg(args, unsigned long), 10,
field_width, precision, flags);
break;
// 'n'-表示要把到目前为止转换输出字符数保存到对应参数指针指定的位置中。
// 首先利用va_arg()取得该参数指针,然后将已经转换好的字符数存到指向的位置
case 'n':
ip = va_arg(args, int *);
*ip = (str - buf);
break;
// 若格式转换不是 % ,则表示格式字符串有错,直接将一个“%”写入输出串中。
// 如果格式转换符的位置还有字符,则也直接将该字符写入输入串中,并返回继续处理
// 格式字符串,否则表示已经处理到格式字符串的结尾处,退出循环。
default:
if (*fmt != '%')
*str++ = '%';
if (*fmt)
*str++ = *fmt;
else
--fmt;
break;
}
}
*str = '\0'; // 字符串结尾字符:'\0'
return str-buf; // 返回转换好的长度值
}
写文件系统调用
// 参数fd是文件句柄,buf是用户缓冲区,count是欲写字节数。
int sys_write(unsigned int fd,char * buf,int count)
{
struct file * file;
struct m_inode * inode;
// 同样地,我们首先判断函数参数的有效性。若果进程文件句柄值大于程序最多打开文件数
// NR_OPEN,或者需要写入的字节数小于0,或者该句柄的文件结构指针为空,则返回出错码
// 并退出。如果需读取字节数count等于0,则返回0退出。
if (fd>=NR_OPEN || count <0 || !(file=current->filp[fd]))
return -EINVAL;
if (!count)
return 0;
// 然后验证存放数据的缓冲区内存限制。并取文件的i节点。用于根据该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;
}
在linux/kernel/printk.c中:
// 内核使用的显示函数
// 只能在内核代码中使用。由于实际调用的显示函数tty_write()默认使用的显示数据在
// 段寄存器fs所指向的用户数据区中,因此这里需要暂时保存fs,并让fs指向内核数据段。
// 在显示完之后再恢复原fs段的内容。
int printk(const char *fmt, ...)
{
va_list args; // va_list是一个字符指针类型
int i;
// 运行参数处理开始函数。然后使用格式串fmt将参数列表args输出到buf中,返回值i等于
// 输出字符串的长度,再运行参数处理结束函数。
va_start(args, fmt);
i=vsprintf(buf,fmt,args);
va_end(args);
__asm__("push %%fs\n\t" // 保存fs
"push %%ds\n\t"
"pop %%fs\n\t" // 令fs = ds
"pushl %0\n\t" // 将字符串长度压入堆栈(这三个入栈是调用参数)
"pushl $buf\n\t" // 将buf的地址压入堆栈
"pushl $0\n\t" // 将数值0压入堆栈,是显示通道号 channel
"call tty_write\n\t" // 调用tty_write函数(tty_io.c)
"addl $8,%%esp\n\t" // 跳过(丢弃)两个入栈参数(buf, channel)
"popl %0\n\t" // 弹出字符串长度值,作为返回值
"pop %%fs" // 恢复原fs寄存器
::"r" (i):"ax","cx","dx"); // 通知编译器,寄存器ax,cx,dx值肯能已经改变。
return i; // 返回字符串长度
}
在linux/kernel/tty_io.c中
int tty_write(unsigned channel, char * buf, int nr)
{
static int cr_flag=0;
struct tty_struct * tty;
char c, *b=buf;
if (channel>2 || nr<0) return -1;
tty = channel + tty_table;
while (nr>0) {
sleep_if_full(&tty->write_q);
if (current->signal)
break;
while (nr>0 && !FULL(tty->write_q)) {
c=get_fs_byte(b);
if (O_POST(tty)) {
if (c=='\r' && O_CRNL(tty))
c='\n';
else if (c=='\n' && O_NLRET(tty))
c='\r';
if (c=='\n' && !cr_flag && O_NLCR(tty)) {
cr_flag = 1;
PUTCH(13,tty->write_q);
continue;
}
if (O_LCUC(tty))
c=toupper(c);
}
b++; nr--;
cr_flag = 0;
PUTCH(c,tty->write_q);
}
tty->write(tty);//写函数的函数指针。 tty不同,tty->write(tty);整个可视为不同的函数名,即调用不同的写函数
if (nr>0)
schedule();
}
return (b-buf);
}
在各个地方中:
struct tty_struct {
struct termios termios;
int pgrp;
int stopped;
void (*write)(struct tty_struct * tty);//函数指针,是写函数的函数指针
struct tty_queue read_q;
struct tty_queue write_q;
struct tty_queue secondary;
};
extern struct tty_struct tty_table[];
struct tty_struct tty_table[] = {
{
{ICRNL, /* change incoming CR to NL */
OPOST|ONLCR, /* change outgoing NL to CRNL */
0,
ISIG | ICANON | ECHO | ECHOCTL | ECHOKE,
0, /* console termio */
INIT_C_CC},
0, /* initial pgrp */
0, /* initial stopped */
con_write,
{0,0,0,0,""}, /* console read-queue */
{0,0,0,0,""}, /* console write-queue */
{0,0,0,0,""} /* console secondary queue */
},{
{0, /* no translation */
0, /* no translation */
B2400 | CS8,
0,
0,
INIT_C_CC},
0,
0,
rs_write,
{0x3f8,0,0,0,""}, /* rs 1 */
{0x3f8,0,0,0,""},
{0,0,0,0,""}
},{
{0, /* no translation */
0, /* no translation */
B2400 | CS8,
0,
0,
INIT_C_CC},
0,
0,
rs_write,
{0x2f8,0,0,0,""}, /* rs 2 */
{0x2f8,0,0,0,""},
{0,0,0,0,""}
}
};
函数tty_ioctl()中tty = dev + tty_table;
函数tty_read()中tty = &tty_table[channel];
函数tty_write()中tty = channel + tty_table;
在 linux/kernel/console.c中:
void con_write(struct tty_struct * tty)
{
int nr;
char c;
nr = CHARS(tty->write_q);
while (nr--) {
GETCH(tty->write_q,c);
switch(state) {
case 0:
if (c>31 && c<127) {
if (x>=video_num_columns) {
x -= video_num_columns;
pos -= video_size_row;
lf();
}
__asm__("movb attr,%%ah\n\t"
"movw %%ax,%1\n\t"
::"a" (c),"m" (*(short *)pos)
);
pos += 2;
x++;
} else if (c==27)
state=1;
else if (c==10 || c==11 || c==12)
lf();
else if (c==13)
cr();
else if (c==ERASE_CHAR(tty))
del();
else if (c==8) {
if (x) {
x--;
pos -= 2;
}
} else if (c==9) {
c=8-(x&7);
x += c;
pos += c<<1;
if (x>video_num_columns) {
x -= video_num_columns;
pos -= video_size_row;
lf();
}
c=9;
} else if (c==7)
sysbeep();
break;
case 1:
state=0;
if (c=='[')
state=2;
else if (c=='E')
gotoxy(0,y+1);
else if (c=='M')
ri();
else if (c=='D')
lf();
else if (c=='Z')
respond(tty);
else if (x=='7')
save_cur();
else if (x=='8')
restore_cur();
break;
case 2:
for(npar=0;npar<NPAR;npar++)
par[npar]=0;
npar=0;
state=3;
if ((ques=(c=='?')))
break;
case 3:
if (c==';' && npar<NPAR-1) {
npar++;
break;
} else if (c>='0' && c<='9') {
par[npar]=10*par[npar]+c-'0';
break;
} else state=4;
case 4:
state=0;
switch(c) {
case 'G': case '`':
if (par[0]) par[0]--;
gotoxy(par[0],y);
break;
case 'A':
if (!par[0]) par[0]++;
gotoxy(x,y-par[0]);
break;
case 'B': case 'e':
if (!par[0]) par[0]++;
gotoxy(x,y+par[0]);
break;
case 'C': case 'a':
if (!par[0]) par[0]++;
gotoxy(x+par[0],y);
break;
case 'D':
if (!par[0]) par[0]++;
gotoxy(x-par[0],y);
break;
case 'E':
if (!par[0]) par[0]++;
gotoxy(0,y+par[0]);
break;
case 'F':
if (!par[0]) par[0]++;
gotoxy(0,y-par[0]);
break;
case 'd':
if (par[0]) par[0]--;
gotoxy(x,par[0]);
break;
case 'H': case 'f':
if (par[0]) par[0]--;
if (par[1]) par[1]--;
gotoxy(par[1],par[0]);
break;
case 'J':
csi_J(par[0]);
break;
case 'K':
csi_K(par[0]);
break;
case 'L':
csi_L(par[0]);
break;
case 'M':
csi_M(par[0]);
break;
case 'P':
csi_P(par[0]);
break;
case '@':
csi_at(par[0]);
break;
case 'm':
csi_m();
break;
case 'r':
if (par[0]) par[0]--;
if (!par[1]) par[1] = video_num_lines;
if (par[0] < par[1] &&
par[1] <= video_num_lines) {
top=par[0];
bottom=par[1];
}
break;
case 's':
save_cur();
break;
case 'u':
restore_cur();
break;
}
}
}
set_cursor();
}
三故事最后
完成显示中最核心的秘密就是
mov pos, c
pos指向显存: pos=0xA0000
将输出缓冲队列write_q的字符放入显存
__asm__("movb attr,%%ah\n\t" "movw %%ax,%1\n\t"
:
:"a" (c),"m" (*(short *)pos)
);
感觉如下
1第一条指令:movb attr,%%ah 中Attr前没有%%,所以attr不是寄存器。但是肯定是给ah赋值
“a” ©:表达式c+表达式c的限制字符(将输入变量放入eax),即如图
2第二条指令:"movw %%ax,%1
“m” (*(short *)pos)的"m"表示操作数放在内存,而不是寄存器中
总结指令顺序是:
mov c, al
mov attr, ah
mov ax, [pos]