2-1 课堂练习2.1:外部中断
本实训分析 Linux 0.11 对外部中断的响应和处理过程。在每条指令执行的末尾,如果没有关中断,CPU 会检查是否收到了外部中断信号,如果有信号,则 CPU 就切换到核心态去执行对应的中断处理程序,在处理完毕后,会执行 iret 这个中断返回指令,回到原状态(一般是用户态),继续执行原程序。
第1关时钟中断的发生
任务描述
本关任务:通过实际操作回答在输出第一行 0/1 字符的过程中(如下图所示),共发生了几次时钟中断?
相关知识
为了完成本关任务,你需要掌握: 1.设置版本 1 内核为分析对象; 2.开始用 gdb 调试内核; 3.跟踪分析时钟中断。
设置版本1内核为分析对象
首先解压版本1内核源码。使用cp
命令将/data/workspace/myshixun/exp1
中的1.tgz
复制到~/os/
目录下;
切换到~/os/linux-0.11-lab
目录下,将1.tgz
解压到当前目录下;
然后调整cur的指向。先使用rm -rf cur
将cur
删除,再使用ln
命令创建符号链接。
现在可以编译和测试版本 1 内核。首先进入1/linux
目录下编译内核;
确认内核映像文件Image
已经生成;
然后回到目录~/os/linux-0.11-lab
,并使用./run
启动虚拟机检测内核是否正常;
如果正常虚拟机在加载完毕之后将会出现如下画面。
第2关第一次时钟中断
任务描述
分析版本1内核,找到第一次时钟中断的恢复点地址。
相关知识
为了完成本关任务,你需要掌握: 1.用 gdb 调试内核; 2.跟踪分析时钟中断; 3.中断/异常的恢复点分析。
准备阶段
本关卡的分析对象是版本1内核,可以基于前一关卡环境进行后续实验,如果重置实验环境则需要重新将版本1内核设置为分析对象,详见第一关的相关知识。
使用 gdb 调试内核
启动两个终端,在一个终端里切换到目录~/os/linux-0.11-lab
,然后运行脚本 rungdb,以启动 bochs 虚拟机并等待 gdb 连接;
在另一个终端里切换到目录~/os/linux-0.11-lab
,然后执行脚本./mygdb
,以启动 gdb 并读入符号信息,跟踪到 main 函数入口。
跟踪分析时钟中断
在函数 do_timer(由时钟中断的处理函数 timer_interrupt 调用)处设置断点; 跟踪到该断点第 1 次出现;
中断/异常的恢复点分析
当一个中断/异常被 gdb 捕获时,通常正在运行中断处理程序,这时可以继续跟踪,直至回到恢复点指令。以时钟中断为例,为了从函数 do_timer 跟踪到恢复点,可以如下操作:
由上图可见时钟中断处理程序的入口是 timer_interrupt 函数。 跟踪到当前函数(do_timer)执行完毕返回到 timer_interrupt 函数;
跟踪到 timer_interrupt 函数(用汇编语言写的)末尾的 iret 指令;
通过单步执行命令 si 来执行该 iret 指令,返回到恢复点;
再通过反汇编命令:disas ,分析恢复点指令的地址。
关闭 gdb 调试: 在评测通关之后为了保证环境的正常,不影响下一关的操作,需要先输入 kill 指令关闭虚拟机,然后输入 quit 退出 gdb 调试。
答案
第3关第六次时钟中断
任务描述
本关任务:通过相关知识以及实验回答:版本 1 内核的第 6 次时钟中断发生时,断点和恢复点(指令地址)分别是多少?此时 bochs 虚拟机输出的 0/1 字符串是什么?(忽略空格)
相关知识
为了完成本关任务,你需要掌握: 1.使用 gdb 调试内核; 2.跟踪分析时钟中断。
准备阶段
本关卡基于前面关卡环境进行后续实验,如果重置实验环境请从第一关重新开始。
使用 gdb 调试内核
启动两个终端,在一个终端里切换到目录~/os/linux-0.11-lab
,然后运行脚本 rungdb,以启动 bochs 虚拟机并等待 gdb 连接;
在另一个终端里切换到目录~/os/linux-0.11-lab
,然后执行脚本./mygdb
,以启动 gdb 并读入符号信息,跟踪到 main 函数入口。
跟踪分析时钟中断
开始用 gdb 调试内核,跟踪到 main 函数入口。方法与第 1 关的步骤(2)一样。 (点击右下角上一关可以直接查看上一关内容,不会对关卡造成影响,但是不要点击评测,会改变环境)
捕获第 6 次时钟中断的发生
方法与第 2 关的步骤(3)类似,跟踪到断点 do_timer 第 6 次出现时即可,此时 jiffies 的值也是 6 。
中断/异常的恢复点分析
当一个中断/异常被 gdb 捕获时,通常正在运行中断处理程序,这时可以继续跟踪,直至回到恢复点指令。以时钟中断为例,为了从函数 do_timer 跟踪到恢复点,可以类似如下操作:
由上图可见时钟中断处理程序的入口是 timer_interrupt 函数。 跟踪到当前函数(do_timer)执行完毕返回到 timer_interrupt 函数;
跟踪到 timer_interrupt 函数(用汇编语言写的)末尾的 iret 指令;
使用调试命令 si 来执行该 iret 指令,返回到恢复点;
然后通过反汇编命令:disas ,来分析恢复点指令的地址。
断点指令和恢复点指令的分析
对于外部中断而言,恢复点指令是断点指令的后一条。需要说明的是,loop 指令的功能是先将 ecx 寄存器减一,然后检查其值,如果其值非 0 ,则继续循环,否则中止循环,执行下一条指令。以如下指令为例:
其功能是:在地址 0x7977 处循环,每次 ecx 寄存器都减一,直到其值为 0 。因此,loop 指令的上一条有可能是它自己。 在 gdb 中查看寄存器值的命令是 info reg:
断点指令和恢复点指令的分析
对于外部中断而言,恢复点指令是断点指令的后一条。需要说明的是,loop 指令的功能是先将 ecx 寄存器减一,然后检查其值,如果其值非 0 ,则继续循环,否则中止循环,执行下一条指令。以如下指令为例:
其功能是:在地址 0x7977 处循环,每次 ecx 寄存器都减一,直到其值为 0 。因此,loop 指令的上一条有可能是它自己。 在 gdb 中查看寄存器值的命令是 info reg:
答案
2-2 课后作业2.1:外部中断
第1关修改版本 1 内核源码,使得每次时钟中断发生时,都在屏幕上输出字符 ‘t’
任务描述
本关任务:修改版本 0 内核,使得每发生 100 次时钟中断,就在屏幕上输出一个字符‘t’和当时的进程号,如“t(0)”表示0号进程运行时发生了时钟中断。
相关知识
为了完成本关任务,你需要掌握: 1.内核态下的字符输出; 2.判断已经发生了多少次时钟中断。
内核态下的字符输出
在版本 0 内核里,可以使用函数printk
来输出字符,其用法类似于printf
。 注: 在版本 0 内核中,没有实现用int 0x81
指令输出字符这个功能。
printk
用法示例:
printk("trying to free inode with count=%d\n",inode->i_count);
判断已经发生了多少次时钟中断
已经发生的时钟中断的次数记录在全局变量 jiffies 中,每发生一次时钟中断,该变量的值就增加 1 。
编程要求
根据相关知识,以及上课内容,对版本 0 内核进行修改,使得每发生 100 次时钟中断,就在屏幕上输出一个字符‘t’和当时的进程号。
使用VScode打开1/linux文件夹,获取版本1内核源代码
在timer_interrupt函数开头添加输出字符t的汇编代码:
movb $116, %al
int $0x81
回到命令行中重新编译1/linux,再启动./rungdb和./mygdb虚拟机即可过关
第2关修改版本 0 内核
任务描述
本关任务:修改版本 0 内核,使得每发生 100 次时钟中断,就在屏幕上输出一个字符‘t’和当时的进程号,如“t(0)”表示0号进程运行时发生了时钟中断。
相关知识
为了完成本关任务,你需要掌握: 1.内核态下的字符输出; 2.判断已经发生了多少次时钟中断。
内核态下的字符输出
在版本 0 内核里,可以使用函数printk
来输出字符,其用法类似于printf
。 注: 在版本 0 内核中,没有实现用int 0x81
指令输出字符这个功能。
printk
用法示例:
printk("trying to free inode with count=%d\n",inode->i_count);
判断已经发生了多少次时钟中断
已经发生的时钟中断的次数记录在全局变量 jiffies 中,每发生一次时钟中断,该变量的值就增加 1 。
编程要求
根据相关知识,以及上课内容,对版本 0 内核进行修改,使得每发生 100 次时钟中断,就在屏幕上输出一个字符‘t’和当时的进程号。
通过vscode找到do_timer
在函数中添加
if(jiffies%100==0)
prink("t(%d),sys_getpid());
2-3 课堂练习2.2:中断/异常的处理过程
第1关除零异常分析
任务描述
分析版本 1.1 内核,回答下列问题: 1.在函数 main 的语句jiffies = jiffies/0;
所对应的汇编指令片段中,有一个 idiv 指令,此指令的地址是多少? 2.在该 idiv 指令执行之前,当前指令位置(CS:EIP)和栈位置(SS:ESP)分别是多少? 3.使用 si 命令执行了该指令后,新指令位置和栈位置分别是多少?此时栈中保存的恢复点位置和用户栈位置分别是多少?
相关知识
为了完成本关任务,你需要掌握: 1.如何设置某版本的内核为分析对象; 2.如何开始用 gdb 调试内核; 3.查看 C 语句编译之后对应的汇编指令片段; 4.分析响应中断/异常时,CPU 做了哪些工作; 5.查看当前寄存器的状态; 6.查看当前栈顶的状态。
实验准备
本关卡使用版本 1.1 内核作为分析对象,内核文件存放在/data/workspace/myshixun/exp1
文件夹中,可以将其解压到linux-0.11-lab
下使用。
如何设置某版本的内核为分析对象
下面以版本1内核为例进行讲解。
首先解压版本1内核源码。使用cp
命令将/data/workspace/myshixun/exp1
中的1.1.tgz
复制到~/os/
目录下;
切换到~/os/linux-0.11-lab
目录下,将1.1.tgz
解压到当前目录下;
然后调整cur的指向。先使用rm -rf cur
将cur
删除,再使用ln
命令创建符号链接。
如何开始用 gdb 调试内核
先关闭bochs虚拟机,然后打开两个终端,其中一个终端在linux-0.11-lab
目录下运行rungdb
脚本,以启动 bochs 虚拟机并等待 gdb 连接;
在另一个终端里切换到目录~/os/linux-0.11-lab/
,然后启动脚本./mygdb
,这个命令会启动 gdb 并读入内核符号信息,同时会通过执行0.gdb
中的调试命令来连接到 bochs 虚拟机,并进而跟踪到 main 函数入口。
查看 C 语句编译之后对应的汇编指令片段
如果要查看某条 C 语句编译之后对应的汇编指令片段,可以在该 C 语句处设置断点,并跟踪到该断点,然后反汇编,所看到的当前指令之后的一段汇编指令就对应于该 C 语句。
例如,jiffies = jiffies/0;
是文件 main.c 的第 147 行,可以如下方式查看:
上面显示的汇编指令中,有一行前面有箭头标识,此即为当前指令,即马上将要执行的指令。
分析响应中断/异常时,CPU 做了哪些工作
-
切换到核心栈,并在其中保存中断现场。
-
转到中断处理程序去运行,并切换到核心态。
如下图所示:
上图显示了栈中中断现场的结构,(OLD SS:OLD ESP) 描述了用户栈顶的位置,(OLD CS:OLD EIP) 描述了恢复点的位置。
如何查看当前寄存器的状态
使用 gdb 调试命令 info registers 即可,如下所示:
也可以单独查看某一个寄存器:
如何查看当前栈顶的状态
可以使用命令 x 来查看:
上面显示了栈顶的 5 个长字,是某异常发生时的中断现场,其中存储的用户栈顶的位置是 0x17:0x2573c ,存储的恢复点的位置是 0xf:0x7967 。需要注意的是,x86 中栈是从高地址向低地址方向增长的,这里的栈顶位置是 0x1fa0c 。
编程要求
根据相关知识,回答问题:(将答案填写在/data/workspace/myshixun/第一关.txt
中) 1.在函数 main 的语句jiffies = jiffies/0;
所对应的汇编指令片段中,有一个 idiv 指令,此指令的地址是多少? 2.在该 idiv 指令执行之前,当前指令位置(CS:EIP)和栈位置(SS:ESP)分别是多少? 3.使用 si 命令执行了该指令后,新指令位置和栈位置分别是多少?此时栈中保存的恢复点位置和用户栈位置分别是多少?