|
|
# 用户故事 | 用好动态调试,助力课程学习
|
|
|
|
|
|
你好,我是leveryd。
|
|
|
|
|
|
先做个自我介绍,我在网络安全行业从事技术工作,目前在负责安全产品的研发工作,工作六年。
|
|
|
|
|
|
虽然在研发工作中,我们通常是遇到什么问题就去查,边查边学。虽然这样的学习方式能快速解决问题,但有时候这种方法也不灵,比方说学习语义分析时,就必须要把词法分析、语法分析先学了,一通搜索、查阅、汇总和学习,回头一看,需要花费的时间和精力还是不少的。
|
|
|
|
|
|
显然,只靠自己在网上搜索,学到的常常是零零散散,效率太低。尤其是和工作的关联程度很高的必修知识,我觉得不太适合边查边学,更需要系统学习。结合自己的工作需要,今年年初的时候,我给自己安排了近期学习计划,定下了相应的学习的优先级。
|
|
|
|
|
|
其中,补充操作系统的专业知识就是高优先级的一项。近期学习《操作系统实战45讲》的过程中,我也跟着课程内容开始动手实践,还在课程群里分享了自己的调试经验。接到LMOS老师的邀请,今天我就和你聊聊我是怎样学习这门课程,以及我是如何调试课程代码的。
|
|
|
|
|
|
## 我是怎么学习《操作系统实战45讲》的
|
|
|
|
|
|
根据我的学习需求,我给自己立下了两个学习目标:
|
|
|
|
|
|
第一,理解第十三课的代码:第十三课之前的内容包括了整个机器初始化过程;
|
|
|
|
|
|
第二,理解第二十六课的代码:比第十三课内容多了“内存”和“进程”。
|
|
|
|
|
|
在这个过程中,我会遇到一些问题,我把解决这些问题的实践经验写到[公众号](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzkyMDIxMjE5MA==&action=getalbum&album_id=2147374170542833668&scene=173&from_msgid=2247484665&from_itemidx=1&count=3&nolastread=1#wechat_redirect)(公众号上我记录了这门课的学习实验笔记,以及关于安全业务和技术的一些案例)上,以此加深自己的理解。
|
|
|
|
|
|
就目前我自己的学习经验来看,“内核实验”比较复杂。这主要是因为内核涉及的知识较多,比如C语言、汇编、硬件知识;而且这方面内容比较底层,某些概念我们平时接触得比较少,比如汇编层面的函数调用细节。
|
|
|
|
|
|
另外,部分算法乍一看确实有点难理解,比如[第二十五课](https://time.geekbang.org/column/article/391222)中进程的切换是利用“栈上的函数返回地址”,而“返回地址”包括初始化和后面被进程调度器更新这两种场景。我们需要弄清楚这两个场景都是怎么更新的,才能更好理解进程是如何切换运行的。
|
|
|
|
|
|
## Cosmos调试思路
|
|
|
|
|
|
因为刚才说的这些原因,当我们遇到疑问时,往往无法从网络上直接搜到答案。这个时候,就可以通过调试来辅助我们分析问题。
|
|
|
|
|
|
接下来,我就说一说我是怎么调试课程代码的,后面还会再分享一下我通过动态调试解决疑问的例子。
|
|
|
|
|
|
虽然我们可以在代码中打印日志,但这种方式效率不高,因为每次都需要编写代码、重新编译运行。我更喜欢用GDB和QEMU动态调试Cosmos。
|
|
|
|
|
|
结合下图中我们可以看到:使用GDB在Cosmos内核函数下了断点,并且断点生效。如果我想观察copy\_pages\_data的逻辑,就只需要在**单步调试**过程中观察内存的变化,这样就能知道copy\_pages\_data建立的**页表数据**长什么样子。
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/90/36/90370d43yyb1aa458ef5a0e323f17536.png?wh=1744x696)
|
|
|
|
|
|
总的来说,想要动态调试,我们首先需要编译一个带调试符号的elf文件出来,然后更新hd.img镜像文件。
|
|
|
|
|
|
接着我们用QEMU启动内核,具体命令如下:
|
|
|
|
|
|
```bash
|
|
|
➜ myos qemu-system-x86_64 -drive format=raw,file=hd.img -m 512M -cpu kvm64,smep,smap -s // 一定要加-s参数,此参数可以打开调试服务。
|
|
|
|
|
|
```
|
|
|
|
|
|
最后,我们用GDB加载调试符号并调试,具体命令如下:
|
|
|
|
|
|
```plain
|
|
|
(gdb) symbol-file ./initldr/build/initldrkrl.elf // 加载调试符号,这样才能在显示源码、可以用函数名下断点
|
|
|
Reading symbols from /root/cosmos/lesson13/Cosmos/initldr/build/initldrkrl.elf...done.
|
|
|
(gdb) target remote :1234 // 连接qemu-system-x86_64 -s选项打开的1234端口进行调试
|
|
|
Remote debugging using :1234
|
|
|
0x000000000000e82e in ?? ()
|
|
|
|
|
|
```
|
|
|
|
|
|
我已经将编译好的带调试符号的elf文件,以及对应的hd.img镜像文件放在了[GitHub](https://github.com/leveryd/cosmos-debug-file)上,你可以直接用这些文件和上面的命令来调试。仓库中目前我只放了对应第十三课和第二十六课的调试文件,如果你想要调试其他课的代码,不妨继续往下看。
|
|
|
|
|
|
## 制作“带调试符号的elf文件"的详细步骤
|
|
|
|
|
|
如果你调试过Linux内核,应该比较熟悉上面的流程。不过在制作“带调试符号的elf文件”时,Cosmos和Linux内核有些不同,下面我就详细说明一下。
|
|
|
|
|
|
先说说整体思路:通过修改编译选项,即可生成“带调试符号的elf文件”。然后再生成Cosmos.eki内核文件,最后替换hd.img镜像文件中的Cosmos.eki文件。这样,我们就可以用“带调试符号的elf文件”和hd.img来调试代码了。
|
|
|
|
|
|
### 修复两个bug
|
|
|
|
|
|
只有先修复后面这两个bug,才能成功编译,并且运行Cosmos内核代码。
|
|
|
|
|
|
第一个问题是:编译第十三课的代码时遇到一个报错,报错截图如下。
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/56/8a/5607fba7372a2a546fc3550d9830ce8a.png?wh=1894x1788)
|
|
|
|
|
|
解决办法很简单:将kernel.asm文件中的“kernel.inc”修改成“/kernel.inc”,你可以对照后面的截图看一下。
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/de/96/de8f78757363850f1920ffcb2922a096.png?wh=997x261)
|
|
|
|
|
|
第二个问题是第二十六课遇到的运行时报错,如下图所示。
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/a3/fb/a3364f1907b44a99c198f42e8a11e1fb.png?wh=1440x844)
|
|
|
|
|
|
因为acpi是和“电源管理”相关的模块,这里并没有用到,所以我们可以注释掉 initldr/ldrkrl/chkcpmm.c 文件中的init\_acpi 函数调用。
|
|
|
|
|
|
解决掉这两个问题,就可以成功编译第十三课和第二十六课的代码了。
|
|
|
|
|
|
### 修改“编译选项"
|
|
|
|
|
|
修复bug后,我们虽然能够成功编译运行,但是因为文件没有调试符号,所以我们在GDB调试时无法对应到c源码,也无法用函数名下断点。因此,我们需要通过**修改编译选项**来生成带调试符号的elf文件。
|
|
|
|
|
|
为了编译出带调试符号的执行文件,需要对编译脚本做两处修改。
|
|
|
|
|
|
第一处修改,GCC的`-O2`参数要修改成`O0 -g`参数:`-O0`是告诉GCC编译器,在编译时不要对代码做优化,这么做的原因是避免在GDB调试时源码和实际程序对应不上的情况;`-g`参数是为了告诉编译器带上**调试符号**。
|
|
|
|
|
|
第二处修改,去掉ld的`-s`参数:`-s`是告诉ld程序链接时去掉所有符号信息,其中包括了**调试符号**。
|
|
|
|
|
|
需要替换和修改的文件位置如下图:
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/4c/d8/4c0418816c6f65d09ee4825086482fd8.png?wh=1916x196)
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/a5/5a/a58776949362811af4a8d9251fec3b5a.png?wh=1368x230)
|
|
|
|
|
|
使用sed命令,即可批量将`-O2` 参数修改成`-O0-g` ,代码如下:
|
|
|
|
|
|
```bash
|
|
|
[root@instance-fj5pftdp Cosmos]# sed -i 's/-O2/-O0 -g/' ./initldr/build/krnlbuidcmd.mh ./script/krnlbuidcmd.S ./build/krnlbuidcmd.mki ./build/krnlbuidcmd.mk
|
|
|
[root@instance-fj5pftdp Cosmos]# sed -i 's/-Os/-O0 -g/' ./initldr/build/krnlbuidcmd.mh ./script/krnlbuidcmd.S ./build/krnlbuidcmd.mki ./build/krnlbuidcmd.mk
|
|
|
[root@instance-fj5pftdp Cosmos]# grep -i '\-O2' -r .
|
|
|
[root@instance-fj5pftdp Cosmos]#
|
|
|
|
|
|
```
|
|
|
|
|
|
使用sed命令批量去掉ld的`-s`参数,代码如下:
|
|
|
|
|
|
```bash
|
|
|
[root@instance-fj5pftdp Cosmos]# sed -i 's/-s / /g' ./initldr/build/krnlbuidcmd.mh ./script/krnlbuidcmd.S ./build/krnlbuidcmd.mki ./build/krnlbuidcmd.mk
|
|
|
[root@instance-fj5pftdp Cosmos]# grep '\-s ' -r .
|
|
|
|
|
|
```
|
|
|
|
|
|
完成上面的操作以后,编译选项就修改好了。
|
|
|
|
|
|
### 编译生成“带调试符号的elf文件"
|
|
|
|
|
|
我们修复bug和修改编译选项后,执行`make`就可以编译出带有调试符号的elf文件,如下图:这里的“not stripped”就表示文件带有调试符号。
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/bf/14/bfd6156949406c0ayy682753389b4114.png?wh=1147x226)
|
|
|
|
|
|
这里有两个要点,我特别说明一下。
|
|
|
|
|
|
1.Cosmos.elf:当需要调试“内核代码”时,可以在GDB中执行`symbol-file ./initldr/build/Cosmos.elf`加载调试符号。
|
|
|
|
|
|
2.initldrkrl.elf:当需要调试“二级加载器代码”时,可以在GDB中执行`symbol-file ./initldr/build/initldrkrl.elf`加载调试符号。
|
|
|
|
|
|
### 重新制作hd.img
|
|
|
|
|
|
最后一步,我们需要重新制作hd.img,这样VBox或者QEMU就能运行我们重新生成的Cosmos内核。
|
|
|
|
|
|
整个过程很简单,分两步。首先生成Cosmos.eki,这里需要注意的是,font.fnt等资源文件要拷贝过来。
|
|
|
|
|
|
```bash
|
|
|
[root@instance-fj5pftdp build]# pwd
|
|
|
/root/cosmos/lesson25~26/Cosmos/initldr/build
|
|
|
[root@instance-fj5pftdp build]# cp ../../build/Cosmos.bin ./
|
|
|
[root@instance-fj5pftdp build]# cp ../../release/font.fnt ../../release/logo.bmp ../../release/background.bmp ./
|
|
|
[root@instance-fj5pftdp build]# ./lmoskrlimg -m k -lhf initldrimh.bin -o Cosmos.eki -f initldrkrl.bin initldrsve.bin Cosmos.bin background.bmp font.fnt logo.bmp
|
|
|
文件数:6
|
|
|
映像文件大小:5169152
|
|
|
|
|
|
```
|
|
|
|
|
|
然后更新hd.img,替换其中的Cosmos.eki。
|
|
|
|
|
|
```bash
|
|
|
[root@instance-fj5pftdp build]# pwd
|
|
|
/root/cosmos/lesson25~26/Cosmos/initldr/build
|
|
|
[root@instance-fj5pftdp build]# mount ../../hd.img /tmp/
|
|
|
[root@instance-fj5pftdp build]# cp Cosmos.eki /tmp/boot/
|
|
|
cp:是否覆盖"/tmp/boot/Cosmos.eki"? y
|
|
|
[root@instance-fj5pftdp build]# umount /tmp/
|
|
|
[root@instance-fj5pftdp build]#
|
|
|
|
|
|
```
|
|
|
|
|
|
完成上面的操作以后,hd.img就制作好了。现在我们可以用hd.img和之前生成的elf文件来调试代码。
|
|
|
|
|
|
### 打包传输hd.img到mac
|
|
|
|
|
|
因为我是在云上购买的Linux虚拟机上调试Mac上QEMU运行的Cosmos内核,所以我需要把Linux上制作的hd.img传输到Mac。你可以根据自己的实际情况设置传输地址。
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/be/f1/be17d89e3a927ae0e4a4b70252c043f1.png?wh=1518x206)
|
|
|
|
|
|
## 如何通过动态调试验证grub镜像文件的加载过程
|
|
|
|
|
|
动态调试也好,汇编代码也罢,其实都是为我们分析问题和解决问题服务的。对于调试不太熟悉的小伙伴也别有太大心理负担,一回生、二回熟嘛,咱们多试试就有手感了。
|
|
|
|
|
|
接下来,我就给你分享个比较简单的案例,你只需要看到几行汇编代码,就能解决一些学习中的小疑问。
|
|
|
|
|
|
在正式讲解这个调试案例之前,我先交代下问题背景。在学习课程中的“初始化”部分时,我有两个疑问:
|
|
|
|
|
|
1.代码从grub到Cosmos项目时,第一条指令是什么?这条指令被加载到哪里执行?
|
|
|
|
|
|
2.此时CPU是实模式还是保护模式?
|
|
|
|
|
|
为了解决这两个疑问,我开始了自己的探索之旅。
|
|
|
|
|
|
### 分析过程
|
|
|
|
|
|
```bash
|
|
|
[root@instance-fj5pftdp Cosmos]# od -tx4 ./initldr/build/Cosmos.eki | head -3
|
|
|
0000000 909066eb 1badb002 00010003 e4514ffb
|
|
|
0000020 04000004 04000000 00000000 00000000
|
|
|
0000040 04000068 90909090 e85250d6 00000000
|
|
|
|
|
|
```
|
|
|
|
|
|
根据 [11 | 设置工作模式与环境(中):建造二级引导器](https://time.geekbang.org/column/article/380507)课程中说的GRUB头结构,结合上面的Cosmos.eki文件头信息,我们很容易就能知道,`_start`符号地址是`0x04000000`,`_entry`符号地址是`0x04000068`。
|
|
|
|
|
|
所以,可以猜测:grub程序会加载cosmos.eki到`0x04000000`位置,然后跳到`0x04000000`执行,再从`0x04000000` jmp 到`0x04000068`。
|
|
|
|
|
|
我们可以使用GDB调试验证是否符合这个猜测,调试代码如下:
|
|
|
|
|
|
```bash
|
|
|
[root@instance-fj5pftdp Cosmos]# gdb -silent
|
|
|
(gdb) target remote :1234
|
|
|
Remote debugging using :1234
|
|
|
0x0000000000008851 in ?? ()
|
|
|
(gdb) b *0x04000000
|
|
|
Breakpoint 1 at 0x4000000
|
|
|
(gdb) b *0x04000068
|
|
|
Breakpoint 2 at 0x4000068
|
|
|
(gdb) c
|
|
|
Continuing.
|
|
|
|
|
|
Breakpoint 1, 0x0000000004000068 in ?? ()
|
|
|
(gdb) x /3i $rip // 和imginithead.asm文件内容可以对应上
|
|
|
=> 0x4000068: cli
|
|
|
0x4000069: in al,0x70
|
|
|
0x400006b: or al,0x80
|
|
|
(gdb) x /10x 0x4000000 // 和cosmos.eki文件头可以对应上
|
|
|
0x4000000: 0x909066eb 0x1badb002 0x00010003 0xe4514ffb
|
|
|
0x4000010: 0x04000004 0x04000000 0x00000000 0x00000000
|
|
|
0x4000020: 0x04000068 0x90909090 0xe85250d6 0x00000000
|
|
|
(gdb) info r cr0
|
|
|
cr0 0x11 [ PE ET ]
|
|
|
|
|
|
```
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/43/bb/43dbcfee9357075722330b464125b1bb.png?wh=1920x807)
|
|
|
|
|
|
通过GDB可以看到,程序不是在`0x04000000`断点暂停,而是直接在`0x04000068` 断点暂停,说明第一条指令不是\_start符号位置而是\_entry符号位置。到\_entry时,cr0的pe=1,这表明此时保护模式已经打开了。怎么样?是不是挺方便的?
|
|
|
|
|
|
经过前面的调试,我得到了最后的结论:第一条指令是\_entry符号位置,地址是`0x04000068`。到`0x04000068`这一条指令时,CPU已经是保护模式了。
|
|
|
|
|
|
我的分享到这里就告一段落啦。为了照顾刚入门的同学,我再提供两个参考资料。关于GDB的使用,你可以参考 [100个GDB小技巧](https://wizardforcel.gitbooks.io/100-gdb-tips/content/index.html)。关于QEMU、GCC、ld等命令参数的含义,你可以参考 [man手册](https://www.mankier.com/1/qemu)。
|
|
|
|
|
|
希望这篇加餐对你有所启发,如果你有什么好的学习方法,不妨也在留言区多多分享,让我们一起学习进步。
|
|
|
|