一个bootloader
可以切换到X86保护模式
能够读磁盘并加载ELF执行文件格式
显示字符
一个OS
可以处理时钟中断
显示字符
项目组成
lab1的整体目录结构如下所示:
其中一些比较重要的文件说明如下:
bootloader部分
boot/bootasm.S :定义并实现了bootloader最先执行的函数start,此函数进行了一定的初始化,完成了从实模式到保护模式的转换,并调用bootmain.c中的bootmain函数。
boot/bootmain.c:定义并实现了bootmain函数实现了通过屏幕、串口和并口显示字符串。bootmain函数加载ucore操作系统到内存,然后跳转到ucore的入口处执行。
boot/asm.h:是bootasm.S汇编文件所需要的头文件,主要是一些与X86保护模式的段访问方式相关的宏定义。
ucore操作系统部分
系统初始化部分:
kern/init/init.c:ucore操作系统的初始化启动代码
内存管理部分:
kern/mm/memlayout.h:ucore操作系统有关段管理(段描述符编号、段号等)的一些宏定义
kern/mm/mmu.h:ucore操作系统有关X86 MMU等硬件相关的定义,包括EFLAGS寄存器中各位的含义,应用/系统段类型,中断门描述符定义,段描述符定义,任务状态段定义,NULL段声明的宏SEG_NULL, 特定段声明的宏SEG,设置中 断门描述符的宏SETGATE(在练习6中会用到)
kern/mm/pmm.[ch]:设定了ucore操作系统在段机制中要用到的全局变量:任务状态段ts,全局描述符表 gdt[],加载全局描述符表寄存器的函数lgdt,临时的内核栈stack0;以及对全局描述符表和任务状态段的初始化函数gdt_init
外设驱动部分:
kern/driver/intr.[ch]:实现了通过设置CPU的eflags来屏蔽和使能中断的函数;
kern/driver/picirq.[ch]:实现了对中断控制器8259A的初始化和使能操作;
kern/driver/clock.[ch]:实现了对时钟控制器8253的初始化操作;- kern/driver/console. [ch]:实现了对串口和键盘的中断方式的处理操作;
中断处理部分:
kern/trap/vectors.S:包括256个中断服务例程的入口地址和第一步初步处理实现。注意,此文件是由tools/vector.c在编译ucore期间动态生成的;
kern/trap/trapentry.S:紧接着第一步初步处理后,进一步完成第二步初步处理;并且有恢复中断上下文的处理,即中断处理完毕后的返回准备工作;
kern/trap/trap.[ch]:紧接着第二步初步处理后,继续完成具体的各种中断处理操作;
内核调试部分:
kern/debug/kdebug.[ch]:提供源码和二进制对应关系的查询功能,用于显示调用栈关系。其中补全print_stackframe函数是需要完成的练习。其他实现部分不必深究。
kern/debug/kmonitor.[ch]:实现提供动态分析命令的kernel monitor,便于在ucore出现bug或问题后,能够进入kernel monitor中,查看当前调用关系。实现部分不必深究。
kern/debug/panic.c | assert.h:提供了panic函数和assert宏,便于在发现错误后,调用kernel monitor。大家可在编程实验中充分利用assert宏和panic函数,提高查找错误的效率。
公共库部分
libs/defs.h:包含一些无符号整型的缩写定义。
Libs/x86.h:一些用GNU C嵌入式汇编实现的C函数(由于使用了inline关键字,所以可以理解为宏)。
工具部分
Makefile和function.mk:指导make完成整个软件项目的编译,清除等工作。
sign.c:一个C语言小程序,是辅助工具,用于生成一个符合规范的硬盘主引导扇区。
tools/vector.c:生成vectors.S,此文件包含了中断向量处理的统一实现。
编译方法
首先下载lab1.tar.bz2,然后解压lab1.tar.bz2。在lab1目录下执行make,可以生成ucore.img(生成于bin目录下)。ucore.img是一个包含了bootloader或OS的硬盘镜像,通过执行如下命令可在硬件虚拟环境 qemu中运行bootloader或OS:
$ make qemu 则可以得到如下显示界面*(仅供参考)*
练习1
理解通过make生成执行文件的过程。
问题
操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
补充材料
如何调试Makefile
当执行make时,一般只会显示输出,不会显示make到底执行了哪些命令。
如想了解make执行了哪些命令,可以执行:(记住,V是大写)
$ make “V=” 要获取更多有关make的信息,可上网查询,并请执行
$ man make 实验过程
问题1
操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
首先,进入~/ucore_os_lab-master/labcodes_answer/lab1_result目录下
执行make,并查看详情
$ make “V=”
观察输出结果
(1)通过GCC编译器将Kernel目录下的.c文件编译成OBJ目录下的.o文件。
为了生成ucore.img,首先需要生成bootblock、kernel
为了生成bootblock,首先需要生成bootasm.o、bootmain.o、sign
递归下去
为了生成kernel,首先需要 kernel.ld init.o readline.o stdio.o kdebug.o
递归下去
问题2
一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
我们可以查看tools下的sign.c文件
从sign.c的代码来看,一个磁盘主引导扇区只有512字节。且第510个(倒数第二个)字节是0x55,第511个(倒数第一个)字节是0xAA。
练习2
使用qemu执行并调试lab1中的软件。
实验要求
从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
在初始化位置0x7c00设置实地址断点,测试断点正常。
从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和bootblock.asm进行比较。
自己找一个bootloader或内核中的代码位置,设置断点并进行测试。
提示:参考附录“启动后第一条执行的指令”,可了解更详细的解释,以及如何单步调试和
查看BIOS代码。
提示:查看 labcodes_answer/lab1_result/tools/lab1init 文件,用如下命令试试如何调试
bootloader第一条指令:
$ cd labcodes_answer/lab1_result/ $ make lab1-mon
补充材料
我们主要通过硬件模拟器qemu来进行各种实验。在实验的过程中我们可能会遇上各种各样的问题,调试是必要的。qemu支持使用gdb进行的强大而方便的调试。所以用好qemu和gdb是完成各种实验的基本要素。
默认的gdb需要进行一些额外的配置才进行qemu的调试任务。qemu和gdb之间使用网络端口1234进行通讯。在打开qemu进行模拟之后,执行gdb并输入
target remote localhost:1234 即可连接qemu,此时qemu会进入停止状态,听从gdb的命令。
另外,我们可能需要qemu在一开始便进入等待模式,则我们不再使用make qemu开始系统的运行,而使用make debug来完成这项工作。这样qemu便不会在gdb尚未连接的时候擅自运行了。
gdb的地址断点
在gdb命令行中,使用b *[地址]便可以在指定内存地址设置断点,当qemu中的cpu执行到指定地址时,便会将控制权交给gdb。
关于代码的反汇编
有可能gdb无法正确获取当前qemu执行的汇编指令,通过如下配置可以在每次gdb命令行前强制反汇编当前的指令,在gdb命令行或配置文件中添加:
define hook-stop x/i $pc end 即可
gdb的单步命令
在gdb中,有next, nexti, step, stepi等指令来单步调试程序,他们功能各不相同,区别在于单步的“跨度”上。
next 单步到程序源代码的下一行,不进入函数。 nexti 单步一条机器指令,不进入函数。 step 单步到下一个不同的源代码行(包括进入函数)。 stepi 单步一条机器指令。 实验过程
问题1
从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
直接看截图吧,就是玩玩gdb那几个命令而已
这是直接拿答案的代码了,拿题目的是这样:
(1)进入~/moocos/ucore_lab/labcodes/lab1/bin,即ucore.img所在文件夹。
(2)输入指令:
qemu -S -s -hda ucore.img -monitor stdio
(3)打开gdb。
(4)输入命令,使gdb与qemu通过1234端口进行通信,qemu会停止状态听从gbd命令。
target remote 127.0.0.1:1234
(5)输入命令,单步跟踪。
si
问题2
在初始化位置0x7c00设置实地址断点,测试断点正常。
(1)输入命令,设置断点。
b *0x7c00
(2)continue后,测试断点。
c
x/2i $pc
运行结果:
(gdb) target remote 127.0.0.1:1234 Remote debugging using 127.0.0.1:1234 0x0000fff0 in ?? () (gdb) b *0x7c00 Breakpoint 1 at 0x7c00 (gdb) c Continuing. Breakpoint 1, 0x00007c00 in ?? () (gdb) x /2i $pc =》 0x7c00: cli 0x7c01: cld
问题3
从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和bootblock.asm进行比较。
使用meld对比bootasm.S和bootlock.asm的代码
meld /home/moocos/moocos/ucore_lab/labcodes/lab1/boot/bootasm.S /home/moocos/moocos/ucore_lab/labcodes/lab1/obj/bootblock.asm
看就完了,是一样的
问题4
自己找一个bootloader或内核中的代码位置,设置断点并进行测试。
(略)
补充
在进入练习3前,再复习一下bootloader做了什么:
切换到保护模式,启用分段机制 (修改A20地址线)
读磁盘中ELF执行文件格式的ucore操作系统到内存
显示字符串信息
把控制权交给ucore操作系统
保护模式下,有两个段表:GDT(Global Descriptor Table)和LDT(Local Descriptor Table),每一张段表可以包含8192 (2^13)个描述符,因而最多可以同时存在2 * 2^13 = 2^14 个段。虽然保护模式下可以有这么多段,逻辑地址空间看起来很大,但实际上段并不能扩展物理地址空间,很大程度上各个段的地址空间是相互重叠的。所谓的64TB(2^(14+32) =246)逻辑地址空间是一个理论值,没有实际意义。在32位保护模式下,真正的物理空间仍然只有232字节那么大。(注:在ucore lab中只用到了GDT,没有用LDT。)
练习3
分析bootloader进入保护模式的过程。
实验要求
BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。
提示:需要阅读小节**“保护模式和分段机制”**和lab1/boot/bootasm.S源码,了解如何从实模式切换到保护模式,需要了解:
为何开启A20,以及如何开启A20
如何初始化GDT表
如何使能和进入保护模式
实验过程
打开lab1/boot/bootasm.S
清理环境:关中断,重要的段寄存器清零
开启A20(为了与最早的PC兼容,物理地址线绑定在了低20位,因此高于1MB的地址默认情况下会回零。 此代码撤消了此操作),通过将键盘控制器上的A20线置于高电位,全部32条地址线可用,可以访问4G的内存空间。
设备和芯片的I/O端口操作实现,其实没有复杂的东西在里边 ,I/O端口操作主要是看一堆文档,把整个X86架构的PC机所有I/O端口记住,并记住它们每一个数据寄存器、命令寄存器等操作访问标准(也可以称之协议) ; 记住之后,整个过程中就是按标准使用I/O指令。
in, out(只能与DX,AX,AL寄存器结合使用) ;
下面的实现是提供给C使用,语法太晦涩,所以直接使用汇编实现。
inb 从I/O端口读取一个字节(BYTE, HALF-WORD) ;
outb 向I/O端口写入一个字节(BYTE, HALF-WORD);
inw 从I/O端口读取一个字(WORD,即两个字节);
outw 向I/O端口写入一个字(WORD,即两个字节) ;
初始化GDT(全局描述表,Global Descriptor Table),一个简单的GDT表和其描述符已经静态储存在引导区中,载入即可
进入保护模式:通过将cr0寄存器PE位(第0位)置1便开启了保护模式;通过长跳转更新cs的基地址;设置段寄存器,并建立堆栈(BIOS数据区:0x0000-0x7c00)
call bootmain 使用引导程序GDT和段转换,使虚拟地址与物理地址相同,从而从实模式切换到保护模式,从而使有效内存映射在切换期间不会改变。
一个bootloader
可以切换到X86保护模式
能够读磁盘并加载ELF执行文件格式
显示字符
一个OS
可以处理时钟中断
显示字符
项目组成
lab1的整体目录结构如下所示:
其中一些比较重要的文件说明如下:
bootloader部分
boot/bootasm.S :定义并实现了bootloader最先执行的函数start,此函数进行了一定的初始化,完成了从实模式到保护模式的转换,并调用bootmain.c中的bootmain函数。
boot/bootmain.c:定义并实现了bootmain函数实现了通过屏幕、串口和并口显示字符串。bootmain函数加载ucore操作系统到内存,然后跳转到ucore的入口处执行。
boot/asm.h:是bootasm.S汇编文件所需要的头文件,主要是一些与X86保护模式的段访问方式相关的宏定义。
ucore操作系统部分
系统初始化部分:
kern/init/init.c:ucore操作系统的初始化启动代码
内存管理部分:
kern/mm/memlayout.h:ucore操作系统有关段管理(段描述符编号、段号等)的一些宏定义
kern/mm/mmu.h:ucore操作系统有关X86 MMU等硬件相关的定义,包括EFLAGS寄存器中各位的含义,应用/系统段类型,中断门描述符定义,段描述符定义,任务状态段定义,NULL段声明的宏SEG_NULL, 特定段声明的宏SEG,设置中 断门描述符的宏SETGATE(在练习6中会用到)
kern/mm/pmm.[ch]:设定了ucore操作系统在段机制中要用到的全局变量:任务状态段ts,全局描述符表 gdt[],加载全局描述符表寄存器的函数lgdt,临时的内核栈stack0;以及对全局描述符表和任务状态段的初始化函数gdt_init
外设驱动部分:
kern/driver/intr.[ch]:实现了通过设置CPU的eflags来屏蔽和使能中断的函数;
kern/driver/picirq.[ch]:实现了对中断控制器8259A的初始化和使能操作;
kern/driver/clock.[ch]:实现了对时钟控制器8253的初始化操作;- kern/driver/console. [ch]:实现了对串口和键盘的中断方式的处理操作;
中断处理部分:
kern/trap/vectors.S:包括256个中断服务例程的入口地址和第一步初步处理实现。注意,此文件是由tools/vector.c在编译ucore期间动态生成的;
kern/trap/trapentry.S:紧接着第一步初步处理后,进一步完成第二步初步处理;并且有恢复中断上下文的处理,即中断处理完毕后的返回准备工作;
kern/trap/trap.[ch]:紧接着第二步初步处理后,继续完成具体的各种中断处理操作;
内核调试部分:
kern/debug/kdebug.[ch]:提供源码和二进制对应关系的查询功能,用于显示调用栈关系。其中补全print_stackframe函数是需要完成的练习。其他实现部分不必深究。
kern/debug/kmonitor.[ch]:实现提供动态分析命令的kernel monitor,便于在ucore出现bug或问题后,能够进入kernel monitor中,查看当前调用关系。实现部分不必深究。
kern/debug/panic.c | assert.h:提供了panic函数和assert宏,便于在发现错误后,调用kernel monitor。大家可在编程实验中充分利用assert宏和panic函数,提高查找错误的效率。
公共库部分
libs/defs.h:包含一些无符号整型的缩写定义。
Libs/x86.h:一些用GNU C嵌入式汇编实现的C函数(由于使用了inline关键字,所以可以理解为宏)。
工具部分
Makefile和function.mk:指导make完成整个软件项目的编译,清除等工作。
sign.c:一个C语言小程序,是辅助工具,用于生成一个符合规范的硬盘主引导扇区。
tools/vector.c:生成vectors.S,此文件包含了中断向量处理的统一实现。
编译方法
首先下载lab1.tar.bz2,然后解压lab1.tar.bz2。在lab1目录下执行make,可以生成ucore.img(生成于bin目录下)。ucore.img是一个包含了bootloader或OS的硬盘镜像,通过执行如下命令可在硬件虚拟环境 qemu中运行bootloader或OS:
$ make qemu 则可以得到如下显示界面*(仅供参考)*
练习1
理解通过make生成执行文件的过程。
问题
操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
补充材料
如何调试Makefile
当执行make时,一般只会显示输出,不会显示make到底执行了哪些命令。
如想了解make执行了哪些命令,可以执行:(记住,V是大写)
$ make “V=” 要获取更多有关make的信息,可上网查询,并请执行
$ man make 实验过程
问题1
操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
首先,进入~/ucore_os_lab-master/labcodes_answer/lab1_result目录下
执行make,并查看详情
$ make “V=”
观察输出结果
(1)通过GCC编译器将Kernel目录下的.c文件编译成OBJ目录下的.o文件。
为了生成ucore.img,首先需要生成bootblock、kernel
为了生成bootblock,首先需要生成bootasm.o、bootmain.o、sign
递归下去
为了生成kernel,首先需要 kernel.ld init.o readline.o stdio.o kdebug.o
递归下去
问题2
一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
我们可以查看tools下的sign.c文件
从sign.c的代码来看,一个磁盘主引导扇区只有512字节。且第510个(倒数第二个)字节是0x55,第511个(倒数第一个)字节是0xAA。
练习2
使用qemu执行并调试lab1中的软件。
实验要求
从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
在初始化位置0x7c00设置实地址断点,测试断点正常。
从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和bootblock.asm进行比较。
自己找一个bootloader或内核中的代码位置,设置断点并进行测试。
提示:参考附录“启动后第一条执行的指令”,可了解更详细的解释,以及如何单步调试和
查看BIOS代码。
提示:查看 labcodes_answer/lab1_result/tools/lab1init 文件,用如下命令试试如何调试
bootloader第一条指令:
$ cd labcodes_answer/lab1_result/ $ make lab1-mon
补充材料
我们主要通过硬件模拟器qemu来进行各种实验。在实验的过程中我们可能会遇上各种各样的问题,调试是必要的。qemu支持使用gdb进行的强大而方便的调试。所以用好qemu和gdb是完成各种实验的基本要素。
默认的gdb需要进行一些额外的配置才进行qemu的调试任务。qemu和gdb之间使用网络端口1234进行通讯。在打开qemu进行模拟之后,执行gdb并输入
target remote localhost:1234 即可连接qemu,此时qemu会进入停止状态,听从gdb的命令。
另外,我们可能需要qemu在一开始便进入等待模式,则我们不再使用make qemu开始系统的运行,而使用make debug来完成这项工作。这样qemu便不会在gdb尚未连接的时候擅自运行了。
gdb的地址断点
在gdb命令行中,使用b *[地址]便可以在指定内存地址设置断点,当qemu中的cpu执行到指定地址时,便会将控制权交给gdb。
关于代码的反汇编
有可能gdb无法正确获取当前qemu执行的汇编指令,通过如下配置可以在每次gdb命令行前强制反汇编当前的指令,在gdb命令行或配置文件中添加:
define hook-stop x/i $pc end 即可
gdb的单步命令
在gdb中,有next, nexti, step, stepi等指令来单步调试程序,他们功能各不相同,区别在于单步的“跨度”上。
next 单步到程序源代码的下一行,不进入函数。 nexti 单步一条机器指令,不进入函数。 step 单步到下一个不同的源代码行(包括进入函数)。 stepi 单步一条机器指令。 实验过程
问题1
从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
直接看截图吧,就是玩玩gdb那几个命令而已
这是直接拿答案的代码了,拿题目的是这样:
(1)进入~/moocos/ucore_lab/labcodes/lab1/bin,即ucore.img所在文件夹。
(2)输入指令:
qemu -S -s -hda ucore.img -monitor stdio
(3)打开gdb。
(4)输入命令,使gdb与qemu通过1234端口进行通信,qemu会停止状态听从gbd命令。
target remote 127.0.0.1:1234
(5)输入命令,单步跟踪。
si
问题2
在初始化位置0x7c00设置实地址断点,测试断点正常。
(1)输入命令,设置断点。
b *0x7c00
(2)continue后,测试断点。
c
x/2i $pc
运行结果:
(gdb) target remote 127.0.0.1:1234 Remote debugging using 127.0.0.1:1234 0x0000fff0 in ?? () (gdb) b *0x7c00 Breakpoint 1 at 0x7c00 (gdb) c Continuing. Breakpoint 1, 0x00007c00 in ?? () (gdb) x /2i $pc =》 0x7c00: cli 0x7c01: cld
问题3
从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和bootblock.asm进行比较。
使用meld对比bootasm.S和bootlock.asm的代码
meld /home/moocos/moocos/ucore_lab/labcodes/lab1/boot/bootasm.S /home/moocos/moocos/ucore_lab/labcodes/lab1/obj/bootblock.asm
看就完了,是一样的
问题4
自己找一个bootloader或内核中的代码位置,设置断点并进行测试。
(略)
补充
在进入练习3前,再复习一下bootloader做了什么:
切换到保护模式,启用分段机制 (修改A20地址线)
读磁盘中ELF执行文件格式的ucore操作系统到内存
显示字符串信息
把控制权交给ucore操作系统
保护模式下,有两个段表:GDT(Global Descriptor Table)和LDT(Local Descriptor Table),每一张段表可以包含8192 (2^13)个描述符,因而最多可以同时存在2 * 2^13 = 2^14 个段。虽然保护模式下可以有这么多段,逻辑地址空间看起来很大,但实际上段并不能扩展物理地址空间,很大程度上各个段的地址空间是相互重叠的。所谓的64TB(2^(14+32) =246)逻辑地址空间是一个理论值,没有实际意义。在32位保护模式下,真正的物理空间仍然只有232字节那么大。(注:在ucore lab中只用到了GDT,没有用LDT。)
练习3
分析bootloader进入保护模式的过程。
实验要求
BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。
提示:需要阅读小节**“保护模式和分段机制”**和lab1/boot/bootasm.S源码,了解如何从实模式切换到保护模式,需要了解:
为何开启A20,以及如何开启A20
如何初始化GDT表
如何使能和进入保护模式
实验过程
打开lab1/boot/bootasm.S
清理环境:关中断,重要的段寄存器清零
开启A20(为了与最早的PC兼容,物理地址线绑定在了低20位,因此高于1MB的地址默认情况下会回零。 此代码撤消了此操作),通过将键盘控制器上的A20线置于高电位,全部32条地址线可用,可以访问4G的内存空间。
设备和芯片的I/O端口操作实现,其实没有复杂的东西在里边 ,I/O端口操作主要是看一堆文档,把整个X86架构的PC机所有I/O端口记住,并记住它们每一个数据寄存器、命令寄存器等操作访问标准(也可以称之协议) ; 记住之后,整个过程中就是按标准使用I/O指令。
in, out(只能与DX,AX,AL寄存器结合使用) ;
下面的实现是提供给C使用,语法太晦涩,所以直接使用汇编实现。
inb 从I/O端口读取一个字节(BYTE, HALF-WORD) ;
outb 向I/O端口写入一个字节(BYTE, HALF-WORD);
inw 从I/O端口读取一个字(WORD,即两个字节);
outw 向I/O端口写入一个字(WORD,即两个字节) ;
初始化GDT(全局描述表,Global Descriptor Table),一个简单的GDT表和其描述符已经静态储存在引导区中,载入即可
进入保护模式:通过将cr0寄存器PE位(第0位)置1便开启了保护模式;通过长跳转更新cs的基地址;设置段寄存器,并建立堆栈(BIOS数据区:0x0000-0x7c00)
call bootmain 使用引导程序GDT和段转换,使虚拟地址与物理地址相同,从而从实模式切换到保护模式,从而使有效内存映射在切换期间不会改变。
举报