第六个实验其实非常简单,然而一开始我的思路就是错误的,我过分依赖于sunner的实验指导书,而不是自己的判断,把太多注意力放在 keyboard.S上,以为真的就像sunner说的:“所以每次按键有动作,keyboard_interrupt函数就会被调用,它在文件 kernel/chr_drv/keyboard.S(注意,扩展名 是大写的S)中实现。所有与键盘输入相关的功能都是在此文件中实现的,所以本实验的部分功能也可以在此文件中实现。详读《注释》一书中对此文件的注解会大 有裨益。”而我却走偏了路,导致一晚上都没做出来,还是第二天下午看到王意林的日志才做出来。
这 个实验的内容是用F12作为开关来切换键盘输入,如果F12按下,那么用键盘敲入任何数据后,电脑身上输出的字母都变成‘*’。如果再次按下F12,就会 恢复正常。在我们通过键盘输入的时候,键盘会通过终端获得cpu,这个终端服务程序就在keyboard.S中大概,他的实现过程是当有按键按下的时候, 按键的具体内容,也就是这次按键事件的键盘扫描码将会放在0x60端口,然后keyboard.S通过inb $0x60,%al将其取出放在al寄存器里面,一个扫描码是半个字节。这个扫描码被获取后会一些列检测,看这次终端是什么情况,如果是e0或者e1则先 进入单独的处理代码,如果不是,那么将会通过一个分发的机制跳到各自的处理程序中执行。这个分发的机制是这样实现的,在代码的最后一部分有个叫做 key_table的跳转表:
key_table:
.long none,do_self,do_self,do_self /* 00-03 s0 esc 1 2 */
.long do_self,do_self,do_self,do_self /* 04-07 3 4 5 6 */
.long do_self,do_self,do_self,do_self /* 08-0B 7 8 9 0 */
.long do_self,do_self,do_self,do_self /* 0C-0F + ' bs tab */
.long do_self,do_self,do_self,do_self /* 10-13 q w e r */
.long do_self,do_self,do_self,do_self /* 14-17 t y u i */
.long do_self,do_self,do_self,do_self /* 18-1B o p } ^ */
.long do_self,ctrl,do_self,do_self /* 1C-1F enter ctrl a s */
.long do_self,do_self,do_self,do_self /* 20-23 d f g h */
.long do_self,do_self,do_self,do_self /* 24-27 j k l | */
.long do_self,do_self,lshift,do_self /* 28-2B { para lshift , */
.long do_self,do_self,do_self,do_self /* 2C-2F z x c v */
.long do_self,do_self,do_self,do_self /* 30-33 b n m , */
.long do_self,minus,rshift,do_self /* 34-37 . - rshift * */
.long alt,do_self,caps,func /* 38-3B alt sp caps f1 */
.long func,func,func,func /* 3C-3F f2 f3 f4 f5 */
.long func,func,func,func /* 40-43 f6 f7 f8 f9 */
.long func,num,scroll,cursor /* 44-47 f10 num scr home */
.long cursor,cursor,do_self,cursor /* 48-4B up pgup - left */
.long cursor,cursor,do_self,cursor /* 4C-4F n5 right + end */
.long cursor,cursor,cursor,cursor /* 50-53 dn pgdn ins del */
.long none,none,do_self,func /* 54-57 sysreq ? < f11 */
.long func,none,none,none /* 58-5B f12 ? ? ? */
.long none,none,none,none /* 5C-5F ? ? ? ? */
.long none,none,none,none /* 60-63 ? ? ? ? */
.long none,none,none,none /* 64-67 ? ? ? ? */
.long none,none,none,none /* 68-6B ? ? ? ? */
.long none,none,none,none /* 6C-6F ? ? ? ? */
.long none,none,none,none /* 70-73 ? ? ? ? */
.long none,none,none,none /* 74-77 ? ? ? ? */
.long none,none,none,none /* 78-7B ? ? ? ? */
.long none,none,none,none /* 7C-7F ? ? ? ? */
.long none,none,none,none /* 80-83 ? br br br */
.long none,none,none,none /* 84-87 br br br br */
.long none,none,none,none /* 88-8B br br br br */
.long none,none,none,none /* 8C-8F br br br br */
.long none,none,none,none /* 90-93 br br br br */
.long none,none,none,none /* 94-97 br br br br */
.long none,none,none,none /* 98-9B br br br br */
.long none,unctrl,none,none /* 9C-9F br unctrl br br */
.long none,none,none,none /* A0-A3 br br br br */
.long none,none,none,none /* A4-A7 br br br br */
.long none,none,unlshift,none /* A8-AB br br unlshift br */
.long none,none,none,none /* AC-AF br br br br */
.long none,none,none,none /* B0-B3 br br br br */
.long none,none,unrshift,none /* B4-B7 br br unrshift br */
.long unalt,none,uncaps,none /* B8-BB unalt br uncaps br */
.long none,none,none,none /* BC-BF br br br br */
.long none,none,none,none /* C0-C3 br br br br */
.long none,none,none,none /* C4-C7 br br br br */
.long none,none,none,none /* C8-CB br br br br */
.long none,none,none,none /* CC-CF br br br br */
.long none,none,none,none /* D0-D3 br br br br */
.long none,none,none,none /* D4-D7 br br br br */
.long none,none,none,none /* D8-DB br ? ? ? */
.long none,none,none,none /* DC-DF ? ? ? ? */
.long none,none,none,none /* E0-E3 e0 e1 ? ? */
.long none,none,none,none /* E4-E7 ? ? ? ? */
.long none,none,none,none /* E8-EB ? ? ? ? */
.long none,none,none,none /* EC-EF ? ? ? ? */
.long none,none,none,none /* F0-F3 ? ? ? ? */
.long none,none,none,none /* F4-F7 ? ? ? ? */
.long none,none,none,none /* F8-FB ? ? ? ? */
.long none,none,none,none /* FC-FF ? ? ? ? */
这 个表的每一行都是四个函数名,是一个函数入口地址的数组,根据键盘扫描码的制成,将每个按键的扫描码的值的大小作为序号,也就是数组的下标,然后将每个键 盘扫描码应该去的函数作为键盘扫描码的值放进去,这样排成了一个一个索引。然后当等描码来的时候通过call key_table(,%eax,4)查找到自己应该去的入口去,其中key_table(,%eax,4)就是自己应该去的函数的入口地址,这条语句的 意思是key_table + 扫描码 * 4,因为每个入口地址会占用4个字节的大小,相邻的没四个字节的内容就是相邻的函数入口地址值。当相应的函数执行完之后,都会将其对应的结构放入al,或 者ax,或者eax中,这些函数中会设置caps,num locks的键盘灯,会设置ctrl,shift,alt等等的按下还是抬起的状态标识符mode,如果是a - z的扫描码还会根据相应的标识符设置分清现在应该是大写还是小写然后将正确的大小写ASCII代码放在al寄存器中,这里要说一下,我们平时按的 Shift + 字母会将键盘上画的小写字母转化成相应的大写字母,而Ctrl + c, Ctrl + v的道理也是一样的,而且令我意想不到的是,他们竟然也是用的ASCII码,这部分的就是我们常说的控制字符,就是那些看不见的字符。扫描码是只有半个字 节,用一个al寄存器就能放下,但是翻译之后就不一定是多长了,比如说我们这次要跟踪的F12,像这样的控制键翻译之后的结果就是两个字节,要用eax才 能放得下。然后将这个eax的内容以半个字节为单位,也就是4bit为单位,挨个放在结果队列里面去,这个结果队列就是书上说的“读队 列”:read_q,放入的过程统一使用这段名字叫做put_queue的代码:
put_queue:
pushl %ecx
pushl %edx
movl table_list,%edx # read-queue for console
movl head(%edx),%ecx
1: movb %al,buf(%edx,%ecx)
incl %ecx
andl $size-1,%ecx
cmpl tail(%edx),%ecx # buffer full - discard everything
je 3f
shrdl $8,%ebx,%eax
je 2f
shrl $8,%ebx
jmp 1b
2: movl %ecx,head(%edx)
movl proc_list(%edx),%ecx
testl %ecx,%ecx
je 3f
movl $0,(%ecx)
3: popl %edx
popl %ecx
ret
最 终都会通过movb %al,buf(%edx,%ecx)放入这个队列里面去,其中还涉及到唤醒等待的进程什么的,具体都做了什么我没仔细看,因为对AT&T汇编不太了解。 这里要说一下赋值的时候,AT&T汇编和Intel汇编是正好相反的,比如如果将al的内容放到bl里面去,正常Intel汇编应该是mov bl,al然而AT&T汇编确是movb %al,%bl所有设计到赋值的语句都是这个方向,想必当年他们两个公司在这方面也应该是不可开交的对手,现在的原始UNIX代码的版权就在AT&T手 里,而且当年的Bell实验室也是AT&T的一个部门,所以UNIX的代码顺理成章地就归AT&T了,但我仍然觉得,智慧应该属于全人类,就算不允许别人 商用,至少应该给大家看一下吧。。。这段put_queue执行结束之后就cpu就去tty_io.c那里了,keyboard.S执行结束,本次终端的 所有结果都已经正确地放在read_q中。然而read_q这里的内容却只是原材料,在tty_io.c中的copy_to_cooked()里,会将根 据read_q的内容加工出一个队列,叫做secondary,这个队列才是供其他程序使用的容易读取的格式良好的所谓的“输入”。另外如果回显标志置位 的话还会将read_q的内容写道write_q中,然后输出到屏幕上。这个地方就是我出错的地方,我以为所有输入都是通过这个显示到屏幕上的,但是事实 好像不是,我在这个地放设置了检查,但是当我输入什么的时候,那些输入并没有经过他显示到屏幕上,所以后来我猜测这个“回显”应该是特殊情况都的道,正常 情况应该是到了secondary以后,后面进行输出的,他们应该走的secondary那条路。我一开始的设想是,将所有字符终端都进行加工,因为字符 有ascii码有127个,正好最高位没有用到,我就利用最高位,如果是在F12功能打开时来的字符中断,那么他们的ascii码最终会在最高位上置1, 然后在copy_to_cooked()中读给seconkary的时候还原,读给write_q的时候转换成‘*’,但是我错了,因为正常的输出走的是 seconkary那条道,而且很可能是在其他程序扫描完数据之后才将其输出的,这要是还原再处理的话就麻烦多了。所以我最后走的还是王意林的路线,也应该是标准的解法,就是在所有终端显示的函数console.c的copy_to_cooked()里面动的手脚,就是在最终的出口进行处理,这样就一个都跑不了了,连程序的输出都一起处理了,如果向我那样,还得额外去抓程序给的输出,所以现在一下都解决了。
具体实现方法是这样的,我在keyboard里面对F12经过的路线动了一下手脚:
/*
* this routine handles function keys
*/
func:
pushl %eax
pushl %ecx
pushl %edx
call show_stat
popl %edx
popl %ecx
popl %eax
subb $0x3B,%al
jb end_func
cmpb $9,%al
jbe ok_func
subb $18,%al
cmpb $10,%al
jb end_func
cmpb $11,%al
ja end_func
/*
* jump if it is F12
*/
cmpb $11,%al
je F12_func
ok_func:
cmpl $4,%ecx /* check that there is enough room */
jl end_func
movl func_table(,%eax,4),%eax
xorl %ebx,%ebx
jmp put_queue
end_func:
ret
/*
* set F12 function
*/
F12_func:
pushl %eax
pushl %ecx
pushl %edx
call tty_F12
popl %edx
popl %ecx
popl %eax
jmp ok_func
所有F*中断都会走这条道,当是F12的时候我让他跳到F12_func这段,目的是触发一个tty_F12()函数,这个函数我写在tty_io.c里面了:
void tty_F12(void)
{
flag = !flag;
}
其 实就是,将负责表示F12功能的那个标志flag反转一下。这个flag标志是我新添加在内核里面的,我把它定义在#include <linux/tty.h>中,因为那些回显标志啊什么的都在这个地方定义的,最后果然能用。然后在输出到终端的函数,console.c的 con_write()中,他读取万字符以后,我让他检查一下是不是在flag打开的情况下而且是英文字符,如果是这种情况的话,那么就把这个字符换成 ‘*’,然后继续执行,这样就都搞定了。
另外通过这次实验我发现两件事很有价值,就是在找写出的函数的时候,我不知道那个函数在哪,我只知道是在tty_table[]的第一项的一个函数,tty->write,但是当我找头文件的时候发现没有具体说这个函数是哪个,而只有一个指针在那占位置:
struct tty_queue {
unsigned long data;
unsigned long head;
unsigned long tail;
struct task_struct * proc_list;
char buf[TTY_BUF_SIZE];
};
后来找了半天才找到在这个地方,tty_io.c开头,定义完宏常量的紧下面:
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,""}
}
尼玛这根本就是json的用法啊。后来我跟sunner还讨论了这个事儿:
原 来结构体里面也可以定义函数域,而且还可以使用指针先占位置,我突然发现结构体简直跟类一样了,这个tty_table[]的声明完全可以说是一个实例 化,这跟json太像了,我想当时Linus也许是有意识的面向对象地写,还是说程序员写结构化程序写多了慢慢就会对象化,更没想到C语言也可以当成面向 对象语言来使用。
后来sunner会回信说我“你现在对C语言的理解已经升了一个境界了”。
还有一件事也挺令我吃惊的,就是 在研究怎样在扫描到F12的时候立刻置位flag的时候,我去找do_tty_interrupt()的声明的地方,因为这个函数在keyboard.S 中call过,就是通过他才进入的copy_to_cooked(),所以在那个地方声明我的tty_F12()应该就可以在keyboard.S中 call了,结果我找了半天,不仅把tty_io.c上面包进来的所有 头文件都找过了,连头文件里面包含的头文件都找过了,但是没有这个函数的声明。也就是说这个函数没有在任何地方声明。这就奇怪了,于是我试探着 tty_io.c里面随便写了个函数void tty_f12(void) { printk("f12 set"); };,也没有在任何.h文件中注册,但是结果令我很震惊,他竟然神奇地可以工作,在keyboard.S里竟然可以无条件调用这个函数。我本以为跨语言调 用会很麻烦,结果竟然是什么都不用做。后来我也和sunner讨论这件事,他回答我说:
这是连接器做到的。说到底,.s和.c都被编译成单独的目标代码,每个目标代码里有各个文件自己定义的函数。链接器把它们揉到一起,这时做到的一个文件可以调用另一个文件中的函数。
好吧,原来是这样。至此,这次实验就完成了,这次实验本来非常简单,但是我的反思却很多,我不应你该迷信sunner的实验指导书,应该更多地进行亲自去判断和分析。

浙公网安备 33010602011771号