一、准备
1.32位系统的Linux
这里我使用了32位的Ubuntu 16.04镜像。
64位的我尝试挣扎了一下不过还是跪了,而且我也没有看到成功在64位Linux下跑成功过的博主。。
如果你是64位Linux,也可以考虑使用虚拟机VMware workstation,是的,这个Linux也是有相应版本的,然后你就可以看到Linux里面跑Linux的场景了。。
2.Nachos源码
百度应该能找到,不过下载前记得自己检查符不符合自己的要求,我们老师要求的是3.4的C++版本,本文也是基于此的,如果你是用Java的可以不用往下看了。。
3.Makefile可能遇见的坑
从其他博主那里看到的坑似乎还不少,不过我只遇见了编译时的问题:
- 将code目录下的Makefile中的gmake改成make
- 删除Makefile.common中的-fwritable-strings
然后在code目录下进行make即可,如果没有报错就说明make成功。
二、问题重述
1. trace the execution of Nachos and observe the executions of
(a) context switch function SWITCH()
(b) function ThreadRoot()
using gdb and
2. answer the following questions:
(a) What are the addresses of the following functions in your Nachos:
i.InterruptEnable()
ii.SimpleThread()
iii.ThreadFinish()
iv.ThreadRoot()
and describe how did you find them.
(b) What are the addresses of the thread objects for
i. the main thread of the Nachos
ii. the forked thread created by the main thread
and describe how did you find them.
(c) When the main thread executes SWITCH() function for the first time, to what address
the CPU returns when it executes the last instruction ret of SWITCH()? What
location in the program that address is referred to?
(d) When the forked thread executes SWITCH() function for the first time, to what
address the CPU returns when it executes the last instruction ret of SWITCH()?
What location in the program that address is referred to?
原题是英文的,不过基本上比较简单,后面解决部分会用中文来进行描述,此外还要注意,本文题目调试的是在threads文件夹下编译好的的nachos,因为问题都和线程的上下文切换有关。
三、问题解答
1.追踪SWITCH和ThreadRoot函数
想查看某个函数的执行情况,只需要为该函数打上断点然后运行程序即可,如:break SWITCH,然后run即可,但是由于SWITCH函数和ThreadRoot函数内部都是汇编语句,因此这里我们可以显示汇编代码来辅助我们进行分析,使用layout asm命令即可:
2.解答问题
a.找到几个函数的内存地址
gdb自带命令可以查看某个函数的地址空间,指令为
p 函数名
借此可以查看题目给出的四个函数在内存中的地址空间,如图:
b.找到主线程和子线程的地址空间
要想找到两个线程的地址空间,我们首先应该对源码进行分析,然后确定好断点要打的位置。
i.通过分析,我们发现在位于system.cc中的Initialize函数有这样的代码:
而根据Initialize函数在main函数中十分靠前的位置,我们可以确定,这里就是主线程的初始处。
因此,我们只需要给Initialize函数打断点,进入后执行到currentThread设置状态的部分,然后查看其内存地址即可,效果如下:
如图所示,其内存地址为0x8053a88。
ii.通过分析源码,我们知道主线程Fork子线程的地方在threadtest.cc的ThreadTest1函数中:
因此我们只需要在ThreadTest1函数打上断点,并执行到给t指针申请空间的部分,再查看t的地址即可,如图:
通过调试,我们可以知道子线程的内存地址为0x8053ae8。
事实上,这一问也可以通过打印currentThread里面的值来获取到两个线程的地址,但是这样不太好哪个是主线程哪个是子线程
c.找到主线程进入SWITCH最后一句话执行时CPU的返回值
这一问相对较复杂,通过对switch.s代码中的注释和gdb的汇编界面的分析:
我决定在SWITCH+84处打断点然后分析eax寄存器的数值
这里也是通过查阅资料知道原来gdb可以将断点打在汇编语句上面,并且还能查看寄存器的值,真的涨姿势。。
内部打断点和查看寄存器的值的指令如下:
(gdb) b *SWITCH +84 #在SWTICH偏移84的位置打断点
(gdb) i r eax #查看eax寄存器的值
效果如图:
如图可见,此时eax寄存器内部的值为0x804b1c2,当我们继续运行时可以发现,其跳转到了ThreadRoot,并且此函数的值恰好为0x0804b1c2,这说明这就是ret指令执行时的CPU返回值,他在程序中对应的位置为ThreadRoot函数。
d.找到主线程进入SWITCH最后一句话执行时CPU的返回值
这一问与上一问略有相似,但是不同之处在于我们需要分析的是在线程Fork之后的CPU返回值,因此要完成这一问,我们首先需要定位到线程Fork之后的位置,在代码中就是ThreadTest1函数中Fork后的第一个SWITCH函数执行处,因此我们首先打上ThreadTest1函数的断点并执行至Fork的位置:
此时,我们再为SWITCH打上断点,并按照刚才的方式来分析eax的。这里,我还打印了一下栈指针寄存器的值,该值前面提到过,为子线程的地址空间,这也可以证明这里是Fork后第一次进入SWITCH。
我们按上一问的方式继续查看eax寄存器的值,可见其仍为0x8041bc2。
当我们继续运行时,可以发现:
它再次到达了ThreadRoot的位置,这也说明在子线程fork后进入的第一个SWITCH函数的最后一句指令执行后CPU返回值指向的仍然是ThreadRoot函数的位置。
程序完整运行效果:
四、小结
由实验结果可以看出,程序运行与老师课上所讲基本一致,比如其中的一些关键点:
- 主线程初始化的方式和子线程不一致,前者在Initialize函数中完成,而后者使用Fork实现。
- SWITCH最后一条指令结束后总是会返回到ThreadRoot函数中,我想这也与汇编部分的实现方式有关。
而gdb功能的强大也真的是令我佩服。