实验分析
F12 状态设定
在 include/linux/sched.h 中定义一个 int 类型变量为 f12_state,用来标志当前是否将所有字母替换为 * 显示。当 f12_state 为 1 时,所有字符将替换为 * 显示,否则不替换。
int f12_state;
在 kernel/sched.c 里实现状态切换函数 switch_f12 。
f12_state = 0; //初始时正常显示
void switch_f12(void)
{
if (f12_state == 1)
f12_state = 0;
else
f12_state = 1;
}
终端显示字符修改
现在要根据 f12_state 的值修改终端中显示的字符。找到 kernel/chr_drv/console.c 中的 con_write 函数。 该函数用到了三个变量: int nr: 缓冲队列中现有的字符数 char c: 当前正在处理字符 int state: 转义状态 外层循环 while (nr--) 对队列中的每个字符进行处理。 事实上,我们暂时只关心 state == 0 (转义初始状态) 并且 c 处于空格(' ', 32)和波浪号('~', 127)中间的情况。这种情况程序会调用汇编向终端写字符。
加入如下代码,在写字符之前根据 f12_state 的状态判断是否要将字符修改为 * 。根据要求,只将字母显示为 * 。
if((c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') && f12_state==1)
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();
}
if((c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') && f12_state==1)
c = '*';
__asm__("movb attr,%%ah\n\t"
"movw %%ax,%1\n\t"
::"a" (c),"m" (*(short *)pos)
);
pos += 2;
x++;
} else if
......
响应 F12 切换状态
kernel/chr_drv/keyboard.S 是键盘驱动汇编程序,主要包括键盘中断处理程序。在英文惯用法中,make 表示键被按下;break表示键被松开。该程序通过对按键键盘扫描码的识别来响应键盘中断。 根据我们的需要,找到 210 行的 func 标号下的代码。
func: /* 该子程序处理功能键 */
pushl %eax /* 保护现场 */
pushl %ecx
pushl %edx
call show_stat /* 调用显示各任务状态函数(sched.c) */
popl %edx
popl %ecx
popl %eax
subb $0x3B,%al /* 对 0x3B 做减法,0x3B 为 F1 的扫描码 */
jb end_func
cmpb $9,%al
jbe ok_func /* 0x3B ~ 0x44 分别为 F1 ~ F9 扫描码,满足则跳转至 ok_func */
subb $18,%al
cmpb $10,%al
jb end_func
cmpb $11,%al /* 0x57, 0x58 分别为 F11, F12 扫描码,满足则跳转至 ok_func */
ja end_func
ok_func: /* 为 F1 ~ F12 功能键,则跳转至此处。此时 al 恰为 0 ~ 11 */
cmpl $4,%ecx /* check that there is enough room */
jl end_func
movl func_table(,%eax,4),%eax
xorl %ebx,%ebx
jmp put_queue /* 依次将 func_table 对应的四个字符放入缓冲队列中 */
end_func:
ret
func_table: /* 分别为 'esc [ [ A', 'esc [ [ B', ... , 'esc [ [ L' */
.long 0x415b5b1b,0x425b5b1b,0x435b5b1b,0x445b5b1b
.long 0x455b5b1b,0x465b5b1b,0x475b5b1b,0x485b5b1b
.long 0x495b5b1b,0x4a5b5b1b,0x4b5b5b1b,0x4c5b5b1b
其中 0x58 为 F12 键的扫描码。现做出如下修改:判断为 0x58 时,不再调用 show_stat 函数,亦不再将 F12 对应字符放入缓冲队列,改为调用 switch_f12 。
func:
cmpb $0x58,%al /* 判断是否 F12 键 */
jne continue_func /* 不是则照常执行 */
pushl %eax
pushl %ecx
pushl %edx
call switch_f12 /* 切换状态 */
popl %edx
popl %ecx
popl %eax
jmp end_func /* 不再执行其他操作 */
continue_func:
pushl %eax
pushl %ecx
pushl %edx
call show_stat
...
至此,所要求功能已经实现。 
问题回答
1. 在原始代码中,按下F12,中断响应后,中断服务程序会调用func?它实现的是什么功能?
首先调用 kernel/sched.c show_stat() 函数显示各任务状态。然后判断 al 是否在 F1 ~ F12 之间,如为真,依次将 'esc [ [ X' 字符序列送到缓冲队列中。其中 ‘X' 分别为 'A', 'B', ... , 'L' (对应 F1 ~ F12)。 在 kernel/chr_drv/console.c con_write(struct tty_struct*) 函数中,state 用来表示转义状态。esc 后接 '[' 表明是一个控制序列引导码 CSI,跳转到状态 2 处理,实现不可打印的功能。猜测后面继续跟 '[' 表示功能键,A则表示 F1。 但是,根据目前的 con_write 函数,程序遇到第二个 '[' 时会连续越过 state == 3, state == 4 两个状态重新置 0,随后紧跟的字母会通过 state == 0 状态输出到终端中。猜测其作用范围不局限于 con_write 函数,linux 0.11可能在其他地方亦有控制序列引导码的调用,能够正确识别 F1 ~ F12 所需的功能。又或是目前的 linux 版本仅将 CSI 保留用于下一版本的扩展,并未实现功能键实际识别功能。
2. 在你的实现中,是否把向文件输出的字符也过滤了?如果是,那么怎么能只过滤向终端输出的字符?如果不是,那么怎么能把向文件输出的字符也一并进行过滤?

否。上文修改仅限于命令行显示,没有过滤向文件输出。事实上无论向文件输出还是向命令行输出,都要经由 fs/read_write.c sys_write(unsigned int, char*, int) 函数。在这里修改即可实现向文件输出时亦过滤。代码如下:
int sys_write(unsigned int fd,char * buf,int count)
{
struct file * file;
struct m_inode * inode;
if (fd>=NR_OPEN || count < 0 || !(file=current->filp[fd]))
return -EINVAL;
if (!count)
return 0;
if (f12_state == 1)
{
int tmpi;
for (tmpi = 0; tmpi < count; tmpi++)
{
char tmpc = get_fs_byte(buf + tmpi);
if (tmpc >= 'A' && tmpc <= 'Z' || tmpc >= 'a' && tmpc <= 'z')
put_fs_byte('*', buf + tmpi);
}
}
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;
}
其中 if (f12_state == 1) 代码段为新加入的内容。此时注释掉前文对 console.c 的修改,仍然能对终端显示 * 号,并过滤向文件输出的字符。 
部分图片出自《Linux 内核完全注释》