Lab2
我们会在Lab2中实现一个真实的操作系统内核的调试,我们采用的是[xv6-public](https://github.com/mit-pdos/xv6-public)的代码
预备知识(工具篇)
需要编译这个操作系统内核,我们需要了解一些qemu和一些关于link的知识。
qemu(我们采用的版本是qemu-system-i386)
QEMU是一款开源的硬件虚拟化软件,它允许用户在单个物理机器上创建和运行多个虚拟机(VM)。它可以模拟各种硬件平台,为运行不同操作系统和应用程序提供一个隔离的VM环境。QEMU被广泛用于开发和测试,同时也用于生产环境中部署虚拟服务器。
PS:就是一个提供硬件虚拟化的软件和VMware之类的相同,个人没研究过VMware的全部功能。不知道能不能支持numa
下面请自行阅读Makefile,对于不知道的参数使用man指令来进行阅读。
下面的代码提供了一个操作系统的多种启动方式
qemu: fs.img xv6.img
$(QEMU) -serial mon:stdio $(QEMUOPTS)
qemu-memfs: xv6memfs.img
$(QEMU) -drive file=xv6memfs.img,index=0,media=disk,format=raw -smp $(CPUS) -m 256
qemu-nox: fs.img xv6.img
$(QEMU) -nographic $(QEMUOPTS)
.gdbinit: .gdbinit.tmpl
sed "s/localhost:1234/localhost:$(GDBPORT)/" < $^ > $@
qemu-gdb: fs.img xv6.img .gdbinit
@echo "*** Now run 'gdb'." 1>&2
$(QEMU) -serial mon:stdio $(QEMUOPTS) -S $(QEMUGDB)
qemu-nox-gdb: fs.img xv6.img .gdbinit
@echo "*** Now run 'gdb'." 1>&2
$(QEMU) -nographic $(QEMUOPTS) -S $(QEMUGDB)
使用.gdbinit(建议掌握)(这一段是ChatGPT生成的)
PS:如果不使用.gdbinit,调试的枯燥程度很难想象的
gdb 提供了一种机制来自动加载预定义的命令,这通常通过一个名为 .gdbinit 的文件完成。当 gdb 启动时,它会在当前目录下查找 .gdbinit 文件,并执行其中的命令。这对于设置调试会话的环境非常有用,比如定义自定义命令、设置断点或配置显示选项等。
用法示例
下面是一个 .gdbinit 文件的简单示例:
# .gdbinit
# 设置断点
break main
# 设置调试格式
set print pretty
# 定义自定义命令
define hello
echo Hello, GDB!\\n
end
在上述示例中:
break main: 在 main 函数上设置一个断点。
set print pretty: 设置打印输出格式为“pretty”,以便更易于阅读。
define ... end: 定义一个自定义命令,这里定义了一个名为 hello 的命令,它将输出 "Hello, GDB!"。
注意
出于安全原因,gdb 默认不会自动加载当前目录中的 .gdbinit 文件。要允许加载,请在 /home/
更多命令和用法
run: 带参数启动你的程序。
info registers: 显示寄存器的当前状态。
x/...: 用于内存检查。
bt: 显示调用堆栈。
使用 .gdbinit 的优点
自动设置:可以自动设置断点、监视点等。
环境配置:可以预先定义一些格式和显示的配置。
自定义命令:可以创建自定义命令以简化复杂的调试流程。
通过合理利用 .gdbinit,你可以提高调试的效率和准确性,特别是在复杂项目中。你可以将常用的命令和设置放在 .gdbinit 文件中,这样在每次启动 gdb 时都会自动加载这些预定义的设置和命令,省去了手动设置的麻烦。
预备知识(知识篇)
编译过程(结合gcc指令的介绍)(在这里我们着重讨论的是ld的过程)
预处理(Preprocessing):
在GCC中,预处理步骤可以通过 E 标志单独执行。
命令示例:gcc -E source.c。
这将只执行预处理步骤,处理如#include、#define等指令,并输出扩展的源代码。
编译(Compilation):
编译步骤将预处理后的代码转换为汇编代码。
在GCC中,这可以通过 S 标志来执行。
命令示例:gcc -S source.c。
这将生成一个汇编文件(通常是.s扩展名)。
汇编(Assembly):
GCC将汇编代码转换为机器代码(目标文件)。
通常这一步是自动进行的,但也可以单独执行。
命令示例:gcc -c source.s,这将生成一个目标文件(.o扩展名)。
链接(Linking):
最后,GCC的链接器(通常是ld)将一个或多个目标文件与所需的库文件链接在一起,生成可执行文件。
命令示例:gcc source.o -o program。
这里,source.o是目标文件,program是最终生成的可执行文件。
综合命令:
通常,上述步骤可以通过单个GCC命令一次性完成:gcc source.c -o program。
这个命令将执行全部的编译步骤:预处理、编译、汇编和链接,最终生成名为program的可执行文件。
GCC还有许多其他选项和功能,例如优化选项(如-O2)、调试信息生成(如-g)等,可以根据具体需求进行选择和使用。
链接器脚本
GNU手册
链接器脚本定义了程序的内存布局,指定了不同段(如文本段、数据段、bss段等)在可执行文件中的位置和地址。这对于操作系统这样需要精确控制内存布局的程序来说非常重要。
下面的代码也正好能说明2G user + 2G kernel的分配
我们会在后面的例子中分析xv6的链接器脚本(kernel.ld)
/* Simple linker script for the JOS kernel.
See the GNU ld 'info' manual ("info ld") to learn the syntax. */
OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(_start)
SECTIONS
{
/* Link the kernel at this address: "." means the current address */
/* Must be equal to KERNLINK */
. = 0x80100000;
.text : AT(0x100000) {
*(.text .stub .text.* .gnu.linkonce.t.*)
}
PROVIDE(etext = .); /* Define the 'etext' symbol to this value */
.rodata : {
*(.rodata .rodata.* .gnu.linkonce.r.*)
}
/* Include debugging information in kernel memory */
.stab : {
PROVIDE(__STAB_BEGIN__ = .);
*(.stab);
PROVIDE(__STAB_END__ = .);
}
.stabstr : {
PROVIDE(__STABSTR_BEGIN__ = .);
*(.stabstr);
PROVIDE(__STABSTR_END__ = .);
}
/* Adjust the address for the data segment to the next page */
. = ALIGN(0x1000);
/* Conventionally, Unix linkers provide pseudo-symbols
* etext, edata, and end, at the end of the text, data, and bss.
* For the kernel mapping, we need the address at the beginning
* of the data section, but that's not one of the conventional
* symbols, because the convention started before there was a
* read-only rodata section between text and data. */
PROVIDE(data = .);
/* The data segment */
.data : {
*(.data)
}
PROVIDE(edata = .);
.bss : {
*(.bss)
}
PROVIDE(end = .);
/DISCARD/ : {
*(.eh_frame .note.GNU-stack)
}
}
启动调试
启动的分类
冷启动,是指计算机在关机状态下按 POWER 键启动,又叫硬件启动,比如开机,这种启动方式在启动之前计算机处于断电状态,像内存这种需要加电维持的存储部件里面的内容都丢失了,加电开机那一刻里面的值都是随机的,操作系统会对其进行初始化。
热启动是在加电的情况下启动,又叫软件启动,比如重启,这种启动方式在启动之前和启动之后电没断过,内存等存储部件里面的值不会改变,但毕竟是启动过程,操作系统会对其进行初始化。
PS:不知道大家有没有遇到电脑卡死的情况,这种情况下一般不是长按开机键,进行reset。在世纪初左右的电脑,reset键和开机键是分开的。
BIOS->MBR->Bootloader->OS->Multiprocessor
在这引入启动的过程,然后我们正式开始实验,我们这里不考虑多核启动的问题
编译运行
建议先把CPU数设置为1,然后进行调试,简化启动流程。当然可以在Makefile中进行修改(建议修改Makefile减少后续的操作)
make
make qemu
make CPU=1 qemu-gdb
如何启动调试
在一个窗口中输入make qemu-gdb,另外一个相同目录下的窗口中启动gdb即可。
手册的重要性
PS:RTFM,STFW
手册规定了计算机所有可能的行为,计算机永远不会错,所以当我们想要学习这种人定的机器时,我们应该使用手册。
如果大家在后面的实验中遇到了问题,应该积极的RTFM和STFW
Intel® 64 and IA-32 Architectures Software Developer’s Manual
intel 32位和64位开发手册.pdf
理论过程
BIOS的作用:
将硬盘(通常引导设备就是硬盘)最开始那个扇区 MBR 加载到0x7c00
自检,然后对一些硬件设备做简单的初始化
构建中断向量表加载中断服务程序
MBR的作用:
MBR 的代码在分区表中寻找可以引导存在操作系统的分区,也就是寻找标记为 0x80 的活动分区,然后加载该活动分区的引导块,再执行其中的操作系统引导程序 Bootloader。
Bootloader的作用:
主要作用就是将操作系统加载到内存里面。操作系统也是一个程序,需要加载到内存里面才能运行
还做了一些其他事情,比如进入保护模式,开启分页机制,建立内存的映射等等
OS的作用:
操作系统内核加载到内存之后,就做一些初始化工作建立好工作环境,比如各个硬件的初始化,重新设置 GDT,IDT 等等初始的操作。初始化启动其他处理器(如果有多个处理器的话)。这里不细说,也不好叙述。
Multiprocessor:(拓展知识)
先启动一个 CPU,用它作为基础启动其他的处理器。先启动的这个 CPU 称作 BSP(BootStrap Processor),其他处理器叫做 AP(Application Processor)。
BIOS
在启动的瞬间就会完成CS和IP的初始化:CS = 0xf000, IP = 0xfff0
操作系统在开机时处于实地址模式
刚启动的时候计算机处于实模式,实模式地址下总线只用了20位,只有2^20=1M的寻址空间,换句话说只用了内存的低1M,这时候分页机制还没建立,CPU就是运行在实际的物理地址上。(实模式)
如何访问实地址模式下的内存:
在实模式下,物理地址(即实际RAM中的地址)是通过将CS左移4位(或者说乘以16)并与IP相加来计算的。下面是计算公式:
Physical Address=CS * 16+ IP
根据CS = 0xf000, IP = 0xfff0,可以得到
address =0xf000 << 4 + 0xfff0=0xffff0
LAB1:
使用gdb来查看0xffff0的语句是什么?并且说明BIOS在内存中的位置。
MBR过程
介绍:MBR(Master Boot Record)是位于存储设备(如硬盘或SSD)最开始部分的一个特殊区域,具有重要的启动和分区表信息。MBR的大小通常为512字节。
bootasm.S 和 bootmain.c 两文件联合在一起编译成二进制文件 bootblock 放在磁盘最开始的那个扇区,然后被 BIOS 加载到0x7c00处,从0x7c00处开始执行。
使用file指令查看文件的属性
file bootblock
bootblock: DOS/MBR boot sector
查看Makefile
bootblock: bootasm.S bootmain.c
$(CC) $(CFLAGS) -fno-pic -O -nostdinc -I. -c bootmain.c
$(CC) $(CFLAGS) -fno-pic -nostdinc -I. -c bootas1m.S
$(LD) $(LDFLAGS) -N -e start **-Ttext 0x7C00** -o bootblock.o bootasm.o bootmain.o
$(OBJDUMP) -S bootblock.o > bootblock.asm
$(OBJCOPY) -S -O binary -j .text bootblock.o bootblock
./sign.pl bootblock
PS:这里我就不多做解释了,如果感兴趣,我会给出一个相似的实验,写一个能直接再硬件上运行的程序(拓展补充)
LAB2:
使用gdb进行内存追踪,来查看具体是BIOS的哪一段代码加载了MBR。
LAB3:
阅读bootasm.S,给出A20线如何开启
给出操作系统如何切换到保护模式
拓展补充
BIOS VS UEFI
写一个能直接再硬件上运行的程序(拓展补充)
实验任务
要求:
需要有自己信息的截图,gdb需要输出自定义字符串(带学号和姓名),需要高亮关键调试信息并进行解释
(gdb) printf "学号-姓名"
关于2-3需要给出详细解释,具体到函数层面。(解释每个代码段的功能)
2-1 LAB:
使用gdb来查看地址0xffff0的语句是什么?并且说明BIOS在内存中的位置。
2-2 LAB:
使用gdb进行内存追踪,来查看具体是BIOS的哪一段代码加载了MBR。
2-3 LAB:
阅读bootasm.S,给出A20线如何开启
给出操作系统如何切换到保护模式的,结合代码说明,最好能结合手册说明。