深入Linux 0.11内核:我是如何通过GDB跟踪键盘中断拿到‘密码’的
记得第一次在终端输入passwd命令时,屏幕突然停止回显字符的瞬间——那种既熟悉又陌生的体验,像触碰到了操作系统深处的隐秘开关。今天,我将用GDB调试器带您穿越到1991年的Linux 0.11内核,一起揭开键盘输入背后的魔法帷幕。这不是普通的操作指南,而是一次从按键触发到屏幕显示的完整数据流探险,适合已经熟悉C语言和基本汇编的开发者。
1. 实验环境搭建与调试起手式
在开始追踪键盘中断之前,我们需要一个能运行Linux 0.11的Bochs虚拟机环境。与直接使用预编译镜像不同,我更喜欢从源码构建调试版本:
# 获取特定调试版内核 git clone https://github.com/oldlinux/linux-0.11-lab.git cd linux-0.11-lab make debug启动时需要特别注意两个终端窗口的配合:
- 终端A运行Bochs虚拟机:
./run - 终端B启动GDB调试器:
./mygdb
提示:如果遇到段错误,尝试在Makefile中添加
-g编译选项重新构建内核
调试器的第一个断点应该设在键盘中断入口。通过查阅keyboard.S源码,我发现0x21号中断的处理函数地址存储在中断描述符表(IDT)中。用GDB验证这个关键地址:
(gdb) x/8x 0x00000000 0x0: 0x00eb8e00 0x00080000 0x0000ea00 0x00080000 0x10: 0x00eb8e00 0x00080000 0x0000ea00 0x00080000 (gdb) break *0x9470 # keyboard_interrupt的物理地址2. 从物理按键到扫描码的奇幻漂流
当我在Bochs窗口按下'a'键时,GDB立即在断点处暂停。此时查看CPU寄存器,能看到键盘控制器通过I/O端口0x60发送的原始扫描码:
eax 0x1 1 ecx 0x0 0 edx 0x60 96通过keyboard.S中的转换表,这个扫描码会被映射为ASCII字符。但有趣的是,Linux 0.11采用了一种双层缓冲机制:
- 原始扫描码队列:存储在
keyboard.S定义的keyboard_buffer数组 - 处理后字符队列:位于
tty_io.c的tty_table结构体
用GDB观察这个转换过程:
(gdb) x/16b 0x9020 # 查看键盘缓冲区 0x9020: 0x1e 0x9e 0x00 0x00 0x00 0x00 0x00 0x00 0x9028: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00这里0x1e对应'a'键按下事件,0x9e则是释放事件。转换后的字符最终会进入tty设备的secondary队列,这就是getchar()函数读取的数据源。
3. 终端回显的秘密:为什么第二次输入不可见
当实验要求输入passwd后接着输入secret时,第二个字符串的神秘消失其实与termios结构体中的c_lflag字段有关。通过GDB我们可以捕捉这个状态变化:
(gdb) p *(struct termios*)0x5a00 $1 = { c_iflag = 1280, c_oflag = 5, c_cflag = 191, c_lflag = 35387, # 包含ECHO标志位 c_line = 0 '\000' }在tty_ioctl.c中,当检测到TCSETSW操作时,内核会修改这个标志位。具体到密码输入场景:
- 第一次
passwd输入后,tty_ioctl()清除了ECHO标志 secret输入时,con_write()函数检查到无回显标志,直接丢弃显示输出- 数据仍存在于tty缓冲区,可以被
read()系统调用获取
这个机制解释了为什么密码输入时看不到字符,但程序仍能获取到正确内容。通过反汇编sys_read系统调用,我们能看到它最终调用tty_read()从secondary队列提取数据。
4. 字符设备驱动全景:从键盘到控制台的数据管道
Linux 0.11的字符设备驱动架构比想象中精巧。通过分析chr_dev数组,我发现键盘输入最终流向控制台设备的过程涉及多个关键函数:
| 函数名 | 所在文件 | 作用 |
|---|---|---|
keyboard_interrupt | keyboard.S | 处理硬件中断,填充扫描码缓冲区 |
copy_to_cooked | tty_io.c | 转换扫描码为ASCII,处理特殊控制符 |
tty_read | tty_io.c | 从secondary队列读取处理后的字符 |
con_write | console.c | 控制台输出(受c_lflag影响) |
用GDB跟踪gets()函数的完整调用链时,我设置了一组精妙的断点:
(gdb) break *0x2e3a # sys_read入口 (gdb) break *0x4d7c # tty_read (gdb) break *0x4a9a # copy_to_cooked当在Bochs输入abc时,能看到数据流经这些函数的完整轨迹。特别值得注意的是copy_to_cooked中的这段汇编:
movb %al, (%edi) # 存储处理后的字符 cmpl $0xa, %eax # 检查是否是回车 je 1f # 如果是则跳转处理这解释了为什么输入需要以回车结束——内核在收到0x0a(换行符)时才认为一行输入完成。