理解通过make生成执行文件的过程:
1. 操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
2. 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
[解答]
Q1:
运行命令:
cd /moocos/ucore_lab/labcodes_answer/lab1_result //进入文件夹
mooc->make clean //将生成的文件删除
mooc->make V= > mpro.txt //将makefile的信息重定位到mpro.txt
打开mpro.txt进行分析
可以从复杂的生成过程中,提取出几条关键的命令:
命令1:
在132,3541条命令中,其大致格式为:
+ cc xx.c
gcc ... -o xxx.o
其含义为将源代码.c文件编译成目标文件.o文件,例如:
+ cc kern/init/init.c
gcc -Ikern/init/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/init/init.c -o obj/kern/init/init.o
其含义为将源代码init.c转换成目标文件init.o
命令2:
在42~45行中,其大致格式为:
+ ld
ld xxx.o
其含义为将目标文件.o文件转换成执行程序,例如:
+ ld bin/bootblock
ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
'obj/bootblock.out' size: 488 bytes
build 512 bytes boot sector: 'bin/bootblock' success!
其含义为生成bootloader程序
命令3:
在46~48行,其大致格式为:
dd xxx
其含义为将执行程序放入虚拟硬盘,例如:
dd if=/dev/zero of=bin/ucore.img count=10000
其含义为将bootloader放入虚拟硬盘(此处为ucore.img)中,qemu未来会基于虚拟硬盘中的数据执行相应代码。
接下来详细解释每一条命令和相关参数:
#将源代码kern/init/init.c编译为中间文件
+ cc kern/init/init.c
#-Wall 生成所有警告信息 -ggdb 尽可能的生成gdb可以使用的调试信息 -m32 -gstabs以stabs格式声称调试信
#息,但是不包括gdb调试信息 -nostdinc 使编译器不再系统默认的头文件目录中找头文件 -fno-stack-#protector 停用栈溢出保护
gcc -Ikern/init/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/init/init.c -o obj/kern/init/init.o
#将源代码readline.c编译为中间文件
+ cc kern/libs/readline.c
gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/readline.c -o obj/kern/libs/readline.o
#将源代码stdio.c编译为中间文件
+ cc kern/libs/stdio.c
gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/stdio.c -o obj/kern/libs/stdio.o
#将kdebug.c编译为中间文件
+ cc kern/debug/kdebug.c
gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kdebug.c -o obj/kern/debug/kdebug.o
#将kmonitor.c编译为中间文件
+ cc kern/debug/kmonitor.c
gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kmonitor.c -o obj/kern/debug/kmonitor.o
#将panic.c编译为中间文件
+ cc kern/debug/panic.c
gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/panic.c -o obj/kern/debug/panic.o
#将clock.c编译为中间文件
+ cc kern/driver/clock.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/clock.c -o obj/kern/driver/clock.o
#将console.c编译为中间文件
+ cc kern/driver/console.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/console.c -o obj/kern/driver/console.o
#将intr.c编译为中间文件
+ cc kern/driver/intr.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/intr.c -o obj/kern/driver/intr.o
#将picirq.c编译为中间文件
+ cc kern/driver/picirq.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/picirq.c -o obj/kern/driver/picirq.o
#将trap.c编译为中间文件
+ cc kern/trap/trap.c
gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trap.c -o obj/kern/trap/trap.o
#将trapentry.S汇编为中间文件
+ cc kern/trap/trapentry.S
gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trapentry.S -o obj/kern/trap/trapentry.o
#将vector.S汇编为中间文件
+ cc kern/trap/vectors.S
gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/vectors.S -o obj/kern/trap/vectors.o
#将pmm.c编译为中间文件
+ cc kern/mm/pmm.c
gcc -Ikern/mm/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/mm/pmm.c -o obj/kern/mm/pmm.o
#将printfmt.c编译为中间文件
+ cc libs/printfmt.c
gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -c libs/printfmt.c -o obj/libs/printfmt.o
#将string.c编译为中间文件
+ cc libs/string.c
gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -c libs/string.c -o obj/libs/string.o
#链接生成可执行文件Kernel
+ ld bin/kernel
#参数介绍:-m 模拟指定的连接器 -nostdlib 不用标准库
ld -m elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel obj/kern/init/init.o obj/kern/libs/readline.o obj/kern/libs/stdio.o obj/kern/debug/kdebug.o obj/kern/debug/kmonitor.o obj/kern/debug/panic.o obj/kern/driver/clock.o obj/kern/driver/console.o obj/kern/driver/intr.o obj/kern/driver/picirq.o obj/kern/trap/trap.o obj/kern/trap/trapentry.o obj/kern/trap/vectors.o obj/kern/mm/pmm.o obj/libs/printfmt.o obj/libs/string.o
#将bootasm.S汇编成中间文件
+ cc boot/bootasm.S
gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o
#将bootmain.c编译成中间文件
+ cc boot/bootmain.c
gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o
#将sign.c编译成中间文件,再生成可执行文件sign
+ cc tools/sign.c
gcc -Itools/ -g -Wall -O2 -c tools/sign.c -o obj/sign/tools/sign.o
gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign
#链接生成可执行文件bootblock
+ ld bin/bootblock
#参数介绍 -N 指定读取/写入文本段
ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
'obj/bootblock.out' size: 488 bytes
build 512 bytes boot sector: 'bin/bootblock' success!
dd if=/dev/zero of=bin/ucore.img count=10000
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
Q2:
符合规范的硬盘主引导扇区即大小为512bytes,且最后两个字节为0x55AA的一个硬盘扇区。
打开tools/sign.c
文件,可得到以下内容:
char buf[512];
memset(buf, 0, sizeof(buf));
FILE *ifp = fopen(argv[1], "rb");
int size = fread(buf, 1, st.st_size, ifp);
if (size != st.st_size) {
fprintf(stderr, "read '%s' error, size is %d.\n", argv[1], size);
return -1;
}
fclose(ifp);
buf[510] = 0x55;
buf[511] = 0xAA;
FILE *ofp = fopen(argv[2], "wb+");
size = fwrite(buf, 1, 512, ofp);
if (size != 512) {
fprintf(stderr, "write '%s' error, size is %d.\n", argv[2], size);
return -1;
}
fclose(ofp);
printf("build 512 bytes boot sector: '%s' success!\n", argv[2]);
return 0;
由代码可得,可执行文件的大小不小于510bytes,在511和512字节分别放0x55和0xAA。
补充资料:
主引导记录(MBR),也被称为主引导扇区,是计算机开机以后访问硬盘时所必须要读取的第一个扇区。在深入讨论主引导扇区内部结构的时候,有时也将其开头的446字节内容特指为”主引导 记录“(MBR),其后是4个16字节的”磁盘分区表“(DPT)以及2字节的结束标志(55AA)。
在执行MBR的引导程序时,会验证MBR扇区最后两个字节是否为“55AA”,如果是“55AA”,那么系统才会继续执行下面的程序;如果不是“55AA”,则程序认为这是一个非法的MBR,那么程序将停止执行,同时会在屏幕上列出错误信息。
使用qemu执行并调试lab1中的软件:
1. 从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
2. 在初始化位置0x7c00设置实地址断点,测试断点正常。
3. 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和bootblock.asm进行比较。
4. 自己找一个bootloader或内核中的代码位置,设置断点并进行测试。
/tools/gdbinit
文件,得到以下内容:file obj/bootblock.o
target remote :1234
break bootmain
continue
通过查阅手册可以推测,以上的指令与执行的操作相关。
为方便调试的展开(如实现单步跟踪、设置断点),将gdbinit
修改为如下内容:
target remote :1234
set architecture i8086
通过以上配置可以在每次gdb命令行前强制反汇编当前的指令。
从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
由于是单步跟踪,故需要将continue删除。因为continue
是连续执行的意思。
在lab1的目录下执行make debug
:
可以看到gdb停在第一条指令处,如果查看当前执行的指令的具体内容,需要执行以下操作:
获取当前的$cs
和eip
$cs = 0xf000
$eip = 0xfff0
转换为实地址模式下的地址为:
$cs << 4 | $eip = 0xffff0
如此转换的原因是,在实模式下,内存寻址方式和8086相同,由16位寄存器的内容乘以16(10H)当做段基地址,加上16位偏移地址形成20位物理地址。代码存放寄存器CS(Code Segment)存放正在运行的程序代码所在段的段基址,表示当前使用的指令代码可以从该寄存器指定的存储器段中取得,相应的偏移量则由IP提供。
执行命令x/i 0xffff0
,可得到
=> 0xffff0: ljmp $0xf000,$0xe05b
这就是CPU加电后执行的第一条指令。
执行si
即可单步调试。
在初始化位置0x7c00设置实地址断点,测试断点正常。
执行命令(对于绝对地址,需要添加*将其作为地址):
b *0x7c00
从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和bootblock.asm进行比较。
单步跟踪反汇编后得到的代码为:
查看bootasm.S和bootblock.asm文件,可以得到:
可以看出,三个文件的结果是一致的。
自己找一个bootloader或内核中的代码位置,设置断点并进行测试。
bootloader的起始位置是0x7c00
,故设置断点为0x7c02
来进行测试。
b *0x7c02
continue
x/2i $pc
分析bootloader进入保护模式的过程。
BIOS将通过读取硬盘主引导扇区到内存,并转跳到内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。
根据提示,我们首先查看lab1/boot/bootasm.S
源码,并补充注释:
#include
#asm.h文件中包含一些宏定义,用于定义gdt,gdt是保护模式使用的全局段描述符表,其中存储段描述符。
# Start the CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.
#启动CPU:切换到32位保护模式,跳转到C函数
#BIOS将代码从硬盘的第一扇区加载到物理内存0x7c00处并在实模式下开始执行
#cs的内容为0 ip的内容为0x7c00
.set PROT_MODE_CSEG, 0x8 # kernel code segment selector
#提供gdt中代码段的索引
.set PROT_MODE_DSEG, 0x10 # kernel data segment selector
#提供gdt中数据段的索引
.set CR0_PE_ON, 0x1 # protected mode enable flag
#保护模式使能标志,为1是开启保护模式
# start address should be 0:7c00, in real mode, the beginning address of the running bootloader
#开始地址为0x7c00 在实模式中,这个地址为bootloader开始的地址
.globl start
start:
#相当于C语言中的main函数,BIOS调用程序时从这里开始执行
.code16 # Assemble for 16-bit mode
#以下代码在实模式下执行,告诉编译器使用16位模式编译
cli # Disable interrupts
#关闭中断
cld # String operations increment
#设置字符串操作是递增方向
# Set up the important data segment registers (DS, ES, SS).
#设置重要的数据段寄存器
xorw %ax, %ax # Segment number zero
#清零ax 相当于movw $0,%ax
#xorw比mov reg,0的优势是:
1. 比mov reg,0有更小的代码大小
2. 避免对以后的代码进行部分注册惩罚
3. 不使用执行单元,节省电源并释放执行资源
4. 较小的uop(无即时数据)会在UOP缓存线中留下空间,以便在需要时获取附近的指令
5. 不会用尽物理寄存器文件中的条目
movw %ax, %ds # -> Data Segment
#设置ds为0
movw %ax, %es # -> Extra Segment
#设置es为0
movw %ax, %ss # -> Stack Segment
#设置ss为0
# Enable A20:
# For backwards compatibility with the earliest PCs, physical
# address line 20 is tied low, so that addresses higher than
# 1MB wrap around to zero by default. This code undoes this.
#使能A20,为向下兼容早期PC,物理地址第20位调低,高于1MB的地址回到0
#A20地址由键盘控制器芯片042管理,所以给8042发命令激活A20
#8042有两个IO端口:0x60和0x64
#激活流程:发送0xd1命令到0x64端口 --> 发送0xdf到0x60
seta20.1:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.1
#发送命令前等待输入缓冲区为空,缓冲区是否为空,通过8042的状态寄存器的第2位观察(testb $0x2,%al)
#如果状态寄存器的第2位为1,则跳转到seta20.1符号循环执行
#如果状态寄存器的第2位为0,则代表缓冲区为空
movb $0xd1, %al # 0xd1 -> port 0x64
outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port
#发送0xd1到0x64端口
seta20.2:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.2
movb $0xdf, %al # 0xdf -> port 0x60
outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
#此处操作与上述一致,发送0xdf到0x60端口
# Switch from real to protected mode, using a bootstrap GDT
# and segment translation that makes virtual addresses
# identical to physical addresses, so that the
# effective memory map does not change during the switch.
#从实模式切换到保护模式,在切换过程中使用bootstrap GDT和段翻译将虚拟地址映射到物理地址,以使内存映射不会改变。
lgdt gdtdesc
#载入gdt
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
#打开保护模式标志位,相当于按下保护模式的开关。
# Jump to next instruction, but in 32-bit code segment.
# Switches processor into 32-bit mode.
#进入保护模式
ljmp $PROT_MODE_CSEG, $protcseg
.code32 # Assemble for 32-bit mode
protcseg:
# Set up the protected-mode data segment registers
movw $PROT_MODE_DSEG, %ax # Our data segment selector
movw %ax, %ds # -> DS: Data Segment
movw %ax, %es # -> ES: Extra Segment
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
movw %ax, %ss # -> SS: Stack Segment
# Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
movl $0x0, %ebp
movl $start, %esp
call bootmain
# If bootmain returns (it shouldn't), loop.
spin:
jmp spin
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:
SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
下面,我们回答以下子问题来加深对问题的理解:
为何开启A20,以及如何开启A20
8088/8086只有20位地址线,PC的寻址结构是segment:offset,segment和offset都是16位寄存器,最大值为0x0ffff,换算成物理地址是segment左移4位,再加上offset,所以segment:offset能表达的寻址空间最大为0ffefh,即1088KB,超过20位地址线的寻址能力,早期由于内存较小,即使超过影响也不大,如寻址100000h时可以访问00000h,但是之后出现大的内存,如80286的地址线达到24位,此时如果超过,则多出来的接近64k的存储器成为可能,为了保持向下兼容,于是出现A20 Gate。
PC机再设计上在第21条地址线(A20)做了一个开关,开关打开时,这条地址线和其他地址线一样可以使用,关闭时A20始终为0,则系统只能访问x.?M的内存,x一定是一个偶数,但是在实模式下如果要访问高端内存区或者在保护模式下32位地址线一定要都有效,这两种情况下A20都要打开。
那么如何开启A20呢?工程师使用8042键盘控制器来控制A20 Gate,理论上说,只要操作8042芯片的输出端口(64h)的bit1就可以控制A20,但是当我准备向8042的输入缓冲区写入数据时,可能还有其他数据没有处理,所以首先禁止键盘操作,等数据缓冲区中没有数据后,再去操作8042打开或者关闭A20。
打开A20的步骤如下:
1.关闭中断;
2.等待8042 Input buffer为空;
3.发送禁止键盘操作命令;
4.等待8042 Input buffer为空;
5.发送读取8042 Output Port命令;
6.等待8042 Output buffer有数据;
7.读取8042 Output buffer,并保存得到的字节;
8.等待8042 Input buffer为空;
9.发送Write 8042 Output Port命令到8042 Input buffer;
10.等待8042 Input buffer为空;
11.将从8042 Output Port得到的字节的第2位置1(或清0),然后写入8042 Input buffer;
12.等待,直到8042 Input buffer为空为止;
13.发送允许键盘操作命令到8042 Input buffer;
14. 打开中断。
如何初始化GDT表
全局描述符表(GDT)是一个用于界定不同内存区域的特征的数据结构,存在于内存,全局描述符的条目描述及规定不同段的各种特征,包括基地址、大小和访问等特权。
全局描述符表用于内存地址的转换,访问GDT需要segment selector(索引用于查找GDT的条目)和segment offset。
GDT提供内存保护,在80286之前的处理器中只有实模式,所有程序都可访问任意内存,GDT是保护模式下限制非法内存访问的一种方式。
查看lab1/boot/bootasm.S
,底部有初始化GDT的代码
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:
SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
如代码所示,初始化GDT需要初始化内核代码段描述符和内核数据段描述符。
如何使能和进入保护模式
进入“保护模式”我们需要打开开关(控制寄存器),X86的控制寄存器一共有4个:CR0、CR1、CR2、CR3,进入“保护模式”的开关在CR0上。CR0包含6个预定义标志,0位是保护允许位PE,用于启动保护模式,如果PE位为1,则保护模式启动,如果PE=0,则在实模式下运行。
对应代码为:
movl %cr0, %eax #用通用寄存器保存当前CR0寄存器的值
orl $CR0_PE_ON, %eax #CR0_PE_ON为宏定义值为0x00000001,或操作后将eax中的第0位设置为1
movl %eax, %cr0 #将eax中的值保存到cr0寄存器
通过长跳转,更新CS寄存器的基地址,代码如下:
ljmp $PROT_MODE_CSEG, $protcseg
设置段寄存器,并建立堆栈,代码如下:
.code32 # Assemble for 32-bit mode
protcseg:
# Set up the protected-mode data segment registers
movw $PROT_MODE_DSEG, %ax # Our data segment selector
movw %ax, %ds # -> DS: Data Segment
movw %ax, %es # -> ES: Extra Segment
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
movw %ax, %ss # -> SS: Stack Segment
# Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
movl $0x0, %ebp
movl $start, %esp
转换到保护模式,生成主方法,代码如下:
call bootmain
下面回答本题的问题:分析bootloader进入保护模式的过程。
步骤1,BIOS加载硬盘第一扇区到物理内存0x7c00
处,并且开始执行bootloader。
步骤2,CLI屏蔽中断,中断屏蔽是通过写CPU提供的中断屏蔽寄存器来完成的;CLD使DF复位,DF为方向标志(DF=0,地址向高地址增加;DF=1,地址向低地址减小)。
步骤3,设置寄存器ax,ds,es,ss为0。
步骤4,激活A20,激活步骤见1中打开A20的步骤
步骤5,从实模式转换到保护模式,为保护内存不被非法访问,初始化全局描述符表使得虚拟地址和物理地址匹配可以相互转换。
步骤6,将CR0的第0号设置为1,进入保护模式;指令跳转由代码段跳到protcseg位置;设置保护模式下的数据寄存器;设置堆栈寄存器并调用bootmain函数。
分析bootloader加载ELF格式的OS的过程。
查看bootmain.c
文件,得到的结果如下(已添加注释):
#include
#include
#include
/* *********************************************************************
* This a dirt simple boot loader, whose sole job is to boot
* an ELF kernel image from the first IDE hard disk.
*
* DISK LAYOUT
* * This program(bootasm.S and bootmain.c) is the bootloader.
* It should be stored in the first sector of the disk.
*
* * The 2nd sector onward holds the kernel image.
*
* * The kernel image must be in ELF format.
*
* BOOT UP STEPS
* * when the CPU boots it loads the BIOS into memory and executes it
*
* * the BIOS intializes devices, sets of the interrupt routines, and
* reads the first sector of the boot device(e.g., hard-drive)
* into memory and jumps to it.
*
* * Assuming this boot loader is stored in the first sector of the
* hard-drive, this code takes over...
*
* * control starts in bootasm.S -- which sets up protected mode,
* and a stack so C code then run, then calls bootmain()
*
* * bootmain() in this file takes over, reads in the kernel and jumps to it.
* */
/**
* 这是一个简单的引导加载程序,唯一的工作就是引导来自第一个IDE硬盘的ELF内核映像
* 磁盘布局
* 这个程序(bootasm.S和bootmain.c)是引导加载程序应该存储在磁盘的第一扇区。
* 第二个扇区包含内核镜像,内核镜像应为ELF格式
* 开机步骤:
* 当CPU启动,它将BIOS从硬盘加载到内存并执行BIOS
* BIOS初始化设备,设置中断并读取启动设备的第一扇区,进入内存并跳转到它(启动bootloader)。
* 控制启动bootasm.S并设置保护模式和一个堆栈、C代码然后运行、调用bootmain()
**/
unsigned int SECTSIZE = 512 ;
struct elfhdr * ELFHDR = ((struct elfhdr *)0x10000) ; // scratch space
/* waitdisk - wait for disk ready */
static void
waitdisk(void) {
while ((inb(0x1F7) & 0xC0) != 0x40)
/* do nothing */;
}
/* readsect - read a single sector at @secno into @dst */
static void
readsect(void *dst, uint32_t secno) {
// wait for disk to be ready
waitdisk();
outb(0x1F2, 1); // count = 1
outb(0x1F3, secno & 0xFF);
outb(0x1F4, (secno >> 8) & 0xFF);
outb(0x1F5, (secno >> 16) & 0xFF);
outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
outb(0x1F7, 0x20); // cmd 0x20 - read sectors
// wait for disk to be ready
waitdisk();
// read a sector
insl(0x1F0, dst, SECTSIZE / 4);
}
/* *
* readseg - read @count bytes at @offset from kernel into virtual address @va,
* might copy more than asked.
* */
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
uintptr_t end_va = va + count;
// round down to sector boundary
va -= offset % SECTSIZE;
// translate from bytes to sectors; kernel starts at sector 1
uint32_t secno = (offset / SECTSIZE) + 1;
// If this is too slow, we could read lots of sectors at a time.
// We'd write more to memory than asked, but it doesn't matter --
// we load in increasing order.
for (; va < end_va; va += SECTSIZE, secno ++) {
readsect((void *)va, secno);
}
}
/* bootmain - the entry of bootloader */
void
bootmain(void) {
// read the 1st page off disk
// 读取ELF的头部
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
// is this a valid ELF?
//通过检查ELFHDR中的幻数e_magic来判断是否是ELF文件
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
// load each program segment (ignores ph flags)
// 按照描述符将ELF文件中数据载入内存
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
// call the entry point from the ELF header
// note: does not return
// 根据ELF头部存储的入口信息,找到内核的入口
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
/* do nothing */
while (1);
}
bootmain将内核从硬盘加载到内存中,并把控制权交给内核。
bootloader如何读取硬盘扇区?
对应的代码如下:
/* readsect - read a single sector at @secno into @dst */
static void
readsect(void *dst, uint32_t secno) {
// wait for disk to be ready
// 等待磁盘准备完成
waitdisk();
outb(0x1F2, 1); // count = 1 设置读取扇区的数目为1
outb(0x1F3, secno & 0xFF);
outb(0x1F4, (secno >> 8) & 0xFF);
outb(0x1F5, (secno >> 16) & 0xFF);
outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
//联合制定扇区号
outb(0x1F7, 0x20); // cmd 0x20 - read sectors
// wait for disk to be ready
waitdisk();
//将扇区内容加载到内存中虚拟地址
// read a sector
insl(0x1F0, dst, SECTSIZE / 4); //读扇区
}
其步骤解释如下:
waitdisk
,其实现为while ((inb(0x1F7) & 0xC0) != 0x40)
,意思是不断查询读0x1f7寄存器的最高两位,直到最高位为0、次高位为1(磁盘空闲)才返回。outb(0x1F7, 0x20);
为读取命令,读取的扇区编号放在0x1F3~0x1F6寄存器中。bootloader如何加载ELF格式的OS?
对应代码如下:
/* bootmain - the entry of bootloader */
void
bootmain(void) {
// read the 1st page off disk
// 读取ELF的头部
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
// is this a valid ELF?
//通过检查ELFHDR中的幻数e_magic来判断是否是ELF文件
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
// load each program segment (ignores ph flags)
// 按照描述符将ELF文件中数据载入内存
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
// call the entry point from the ELF header
// note: does not return
// 根据ELF头部存储的入口信息,找到内核的入口
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
/* do nothing */
while (1);
}
其步骤解释如下:
0x10000
,并在这里强制转换成elfhdr
实现函数调用堆栈跟踪函数:
我们需要在lab1中完成kdebug.c中函数print_stackframe的实现,可以通过函数print_stackframe来跟踪函数调用堆栈中记录的返回地址。
打开lab1/kern/debug/kdebug.c
,定位到print_stackframe
函数,得到以下内容:
void
print_stackframe(void) {
/* LAB1 YOUR CODE : STEP 1 */
/* (1) call read_ebp() to get the value of ebp. the type is (uint32_t);
* (2) call read_eip() to get the value of eip. the type is (uint32_t);
* (3) from 0 .. STACKFRAME_DEPTH
* (3.1) printf value of ebp, eip
* (3.2) (uint32_t)calling arguments [0..4] = the contents in address (unit32_t)ebp +2 [0..4]
* (3.3) cprintf("\n");
* (3.4) call print_debuginfo(eip-1) to print the C calling function name and line number, etc.
* (3.5) popup a calling stackframe
* NOTICE: the calling funciton's return addr eip = ss:[ebp+4]
* the calling funciton's ebp = ss:[ebp]
*/
}
上述内容大致含义为:
1. 调用read_ebp函数获取ebp的值
2. 调用read_eip函数获取eip的值
3.
3.1 打印ebp和eip的值
3.2 打印参数
3.3 打印换行符
3.4 调用print_debuginfo,参数为eip-1
3.5 将stackframe弹出栈
注意:返回地址eip = ebp+4
ebp = ss:[ebp]
根据翻译补充函数,得到:
void
print_stackframe(void) {
uint32_t ebp = read_ebp();
uint32_t eip = read_eip();
int i,j;
for(i; i<STACKFRAME_DEPTH; i++)
{
cprintf("ebp:%#010x eip:%#010x args:",ebp,eip);
uint32_t *args = (uint32_t *)(ebp+8);
for (j=0;j<4;j++) {
cprintf("0x%08x ",args[j]);
}
cprintf("\n");
print_debuginfo(eip-1);
eip = *(uint32_t *)(ebp+4);
ebp = *(uint32_t *)(ebp);
}
}
验证:
两者相似
补充资料:
如何打印带0x的8位数据?
int a = 7;
printf("%#010x\n",a); //givens 0x00000007
printf("0x%08x\n",a); //givens 0x00000007
printf("%#08x\n",a); //givens 0x000007
#代表的是在字符串前面加上0x,010表示输出10个bit,x是输出16进制。也可以写成0x%08x
两者的区别在于%#010x的是包含0x输出10个bit,0x%08x是不连0x输出8个bit
指针和整数相加?
指针*p和整数n相加的结果:p+n*sizeof(p的类型),如此题中获取args的地址,可以表示为(uint32_t *)(ebp+8)
或者(uint32_t *)ebp+2
uint32_t ebp = read_ebp(), eip = read_eip();
int i, j;
for (i = 0; ebp != 0 && i < STACKFRAME_DEPTH; i ++) {
cprintf("ebp:0x%08x eip:0x%08x args:", ebp, eip);
uint32_t *args = (uint32_t *)ebp + 2;
for (j = 0; j < 4; j ++) {
cprintf("0x%08x ", args[j]);
}
cprintf("\n");
print_debuginfo(eip - 1);
eip = ((uint32_t *)ebp)[1];
ebp = ((uint32_t *)ebp)[0];
}
完善中断初始化和处理
中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
中断向量表在./kern/mm/mmu.h
中定义,打开该文件,得到:
由上图可以看出中断描述符表中一个表项占(16+16+5+3+4+1+2+1+16)\8=8
个字节,其中段选择子和偏移地址用来代表中断处理程序的入口地址,具体表现为:通过段选择子查找相应段描述符得到代码段的基址,再用基址加上偏移地址得到中断处理程序入口地址。段选择子segment selector在1631位,偏移地址低16位在描述符015位(即首16位),偏移地址高16位在49~64位(即末尾16位)。
请编程完善kern/trap/trap.c
中对中断向量表进行初始化的函数idt_init
。在idt_init
函数中,依次对所有中断入口进行初始化。使用mmu.h
中的SETGATE宏,填充idt
数组内容。每个中断的入口由tools/vectors.c
生成,使用trap.c
中声明的vectors数组即可。
打开kern/trap/trap.c
文件,查看idt_init
函数
/* idt_init - initialize IDT to each of the entry points in kern/trap/vectors.S */
void
idt_init(void) {
/* LAB1 YOUR CODE : STEP 2 */
/* (1) Where are the entry addrs of each Interrupt Service Routine (ISR)?
* All ISR's entry addrs are stored in __vectors. where is uintptr_t __vectors[] ?
* __vectors[] is in kern/trap/vector.S which is produced by tools/vector.c
* (try "make" command in lab1, then you will find vector.S in kern/trap DIR)
* You can use "extern uintptr_t __vectors[];" to define this extern variable which will be used later.
* (2) Now you should setup the entries of ISR in Interrupt Description Table (IDT).
* Can you see idt[256] in this file? Yes, it's IDT! you can use SETGATE macro to setup each item of IDT
* (3) After setup the contents of IDT, you will let CPU know where is the IDT by using 'lidt' instruction.
* You don't know the meaning of this instruction? just google it! and check the libs/x86.h to know more.
* Notice: the argument of lidt is idt_pd. try to find it!
*/
}
其翻译如下:
/* 对kern/trap/vector.S中的每个入口进行初始化 */
void
idt_init(void) {
/**
(1)每个中断服务的地址在哪?所有ISR的地址存储在__vectors中,__vectors在kern/trap/vector.S中,该文件由 tools/vector.c生成
(2)现在建立在IDT中的ISR。idt[256]就是我们要建立的IDT,我们可以使用SETGATE macro来设置IDT的每一项
(3) 设置IDT后,我需要使用lidt指令让CPU知道我在哪里使用IDT。
**/
}
查看kern/mm
文件,得到:
/* *
* Set up a normal interrupt/trap gate descriptor
* - istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate
* - sel: Code segment selector for interrupt/trap handler
* - off: Offset in code segment for interrupt/trap handler
* - dpl: Descriptor Privilege Level - the privilege level required
* for software to invoke this interrupt/trap gate explicitly
* using an int instruction.
* */
#define SETGATE(gate, istrap, sel, off, dpl) { \
(gate).gd_off_15_0 = (uint32_t)(off) & 0xffff; \
(gate).gd_ss = (sel); \
(gate).gd_args = 0; \
(gate).gd_rsv1 = 0; \
(gate).gd_type = (istrap) ? STS_TG32 : STS_IG32; \
(gate).gd_s = 0; \
(gate).gd_dpl = (dpl); \
(gate).gd_p = 1; \
(gate).gd_off_31_16 = (uint32_t)(off) >> 16; \
}
翻译如下:
istrap:1--陷阱门 0--中断门
sel: 代码段选择子
off: 代码段偏移值
dpl: 设置特权等级
在《中断与异常》中可知,在保护模式下,最多会存在256个Interrupt/Exception Vectors。范围[0,31]内的32个向量被异常Exception和NMI使用,范围[32,255]内的向量被保留给用户定义的Interrupts
实现idt_init
代码如下:
void
idt_init(void) {
extern uintptr_t __vectors[];
int sum = sizeof(idt) / sizeof(struct gatedesc);
int i;
for(i = 0; i < sum; i++)
{
if(i <= 31) {
/*在/kern/mm/memlayout.h中*/
SETGATE(idt[i],1,GD_KTEXT,__vectors[i],DPL_KERNEL);
}else {
SETGATE(idt[i],0,GD_KTEXT,__vectors[i],DPL_KERNEL);
}
}
lidt(&idt_pd);
}
补充资料:
extern和头文件的区别
头文件里存放函数、变量、类的声明而不是定义,否则多次引用这个头文件就会多次定义。extern添加在变量前,表示声明一个全局变量;extern不添加则表示定义一个全局变量,如果有赋值,则都表示定义变量。对于函数,定义的时候用extern说明函数可以被外部引用,声明的时候用extern说明这是一个声明。
uintptr_t是什么?
在C中,不能对指针执行按位操作,因此先将指针转换为uintptr类型,然后执行按位操作。
请编程完善trap.
c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字"100 ticks"。
打开/kern/trap
文件,查看trap_dispatch
函数,即需要补充的函数:
case IRQ_OFFSET + IRQ_TIMER:
/* LAB1 YOUR CODE : STEP 3 */
/* handle the timer interrupt */
/* (1) After a timer interrupt, you should record this event using a global variable (increase it), such as ticks in kern/driver/clock.c
* (2) Every TICK_NUM cycle, you can print some info using a funciton, such as print_ticks().
* (3) Too Simple? Yes, I think so!
*/
break;
处理时间中断
1. 在一个时间中断后,你应该记录这个使用一个个全局变量记录这个事件,比如说kern/driver/clock.c中的ticks
2. 每个TICK_NUM循环,你应该调用print_ticks()打印一些信息
补充代码如下:
case IRQ_OFFSET + IRQ_TIMER:
/* LAB1 YOUR CODE : STEP 3 */
/* handle the timer interrupt */
/* (1) After a timer interrupt, you should record this event using a global variable (increase it), such as ticks in kern/driver/clock.c
* (2) Every TICK_NUM cycle, you can print some info using a funciton, such as print_ticks().
* (3) Too Simple? Yes, I think so!
*/
ticks++;//在kern/driver/clock.c中可以看到定义
if(ticks == TICK_NUM)
{
ticks = 0;
print_ticks();
}
break;
结果如下:
扩展proj4,增加`syscall`功能,即增加一用户态函数(可执行一特定系统调用:获得时钟计数值),当内核初始完毕后,可从内核态返回到用户态的函数,而用户态的函数又通过系统调用得到内核态的服务。
题目的含义为实现内核态和用户态之间的切换。题目中建议通过中断处理的方式实现,并会调用下面的test
函数。
static void
switch_test(void) {
print_cur_status(); // print 当前 cs/ss/ds 等寄存器状态
cprintf("+++ switch to user mode +++\n");
switch_to_user(); // switch to user mode
print_cur_status();
cprintf("+++ switch to kernel mode +++\n");
switch_to_kernel(); // switch to kernel mode
print_cur_status();
}
从内核态切换到用户态(switch_to_user)
实现代码如下:
void switch_to_user(void) {
asm volatile {
//为trapframe预留用户栈ss、esp的空间
"sub $0x8,%%esp \n"
//完成从内核态进入中断向用户态转换
"int %0 \n"
//转换到用户态后将函数处于刚进入函数执行的状态
"movl %%ebp,%%esp"
:
:"i"(T_SWITCH_TOU)
};
}
case T_SWITCH_TOU:
if(tf->tf_cs != USER_CS) {
//将CS设置为用户代码段
tf->tf_cs = USER_CS;
//将DS、ES、SS设置为用户数据段
tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS
//设置eflags 确保ucore可以在用户模式下使用IO
tf->tf_eflags |= FL_IOPL_MASK;
}
break;
补充资料:
int和iret指令的使用
int指令的格式为:int n,n为中断类型码,功能是引发中断过程。CPU执行int n指令相当于引发一个n号中断的中断过程。中断例程可以直接返回dos,也可以像子程序一样返回到中断产生的地方,这时需要使用iret——中断返回指令。iret与int相对,它会恢复原始的CS和IP值,并恢复标志寄存器的值。
寄存器的作用
ss:栈段
从用户态到内核态(switch to kernel)
指令在用户态执行时,也会进行中断处理。但是中断处理程序都是在内核态,在执行的过程中需要将用户栈转换为内核栈,具体步骤如下:
1. 从当前进程的描述符中提取内核栈的ss0和esp0的信息。
2. 将SS设置为ss0,esp设置为esp0,同时压入用户栈的ss和esp,待中断返回时,从内核栈转为用户栈。
设置一个用户态可以使用的中断
当用户态代码使用int指令调用中断处理程序时,要进行权限检查,只有该中断描述符的DPL>=CPL时,才可顺利进行中断调用。因此在IDT初始化时,将T_SWITCH_TOK对应的中断描述符DPL设置为用户态。
SETGATE(idt[T_SWITCH_TOK],0,GD_KTEXT,__vectors[T_SWITICH_TOK],DPL_USER)
用户态触发中断
void switch_to_kernel(){
asm volatile(
"int %0\n"
"movl %%ebp,%%esp\n"
:
:"i"(T_SWITCH_TOK)
);
}
case T_SWITCH_TOK:
if(tf->tf_cs != KERNEL_CS) {
tf->tf_cs = KERNEL_CS;
tf->tf_ds = tf->tf_es = tf->tf_ss = KERNEL_DS;
tf->tf_eflags &= ~FL_IOPL_MASK;
}
做扩展实验之前:
做扩展实验之后:
用键盘实现用户模式内核模式切换。具体目标是:“键盘输入3时切换到用户模式,键盘输入0时切换到内核模式”。
./kern/init/init.c
中的switch_to_user
、switch_to_kernel
两个函数添加到trap.c
文件trap_dispatch
中的case IRQ_OFFSET+IRQ_KBD
中调用这两个函数,即键盘按下0时切换到内核,打印出switch to kernel 000
,键盘按下3时切换到用户模式,打印出switch to user
。实现函数如下:
case IRQ_OFFSET + IRQ_KBD:
c = cons_getc();
if ( c == '3' ) {
cprintf("switch to user\n");
lab1_switch_to_user();
print_trapframe(tf);
lab1_switch_to_kernel();
}else if ( c == '0' ) {
cprintf("switch to kernel 000\n");
lab1_switch_to_kernel();
print_trapframe(tf);
}
cprintf("kbd [%03d] %c\n", c, c);
break;
使用图形化界面Meld Diff Viewer
,可以查看出以下不同:
主要是地址、格式的表现形式不同,两种方式都可以,具体内容在实验代码部分已经说明。
左侧(我的)代码将中断向量表分为两个部分,右侧没有区分。
右侧定义一个trapframe(switchk2u)作为中间变量
与OS原理中相应的知识点
CPU的编制与寻址:基于分段机制的内存管理
如果我们将整个地址空间放入物理内存,那么栈和堆之间的空间没有被进程使用,却依然占用实际的物理内存。因此,分段机制应运而生,分段机制使得操作系统能够将不同的段放到不同的物理内存区域,从而避免了虚拟地址空间中的未使用部分占用物理内存。
CPU的中断机制
CPU利用时间中断重新获得控制权,在实验中每隔100个时钟周期,就会打印100ticks。
函数调用关系:在汇编级了解函数调用栈的结构和处理过程
受限直接执行
直接执行的明显优势是快速。但是在CPU上运行会带来一个问题——如果进程希望执行某种受限操作(如向磁盘发出I/O请求或获得更多系统资源)时,就不能简单允许,如果这样做,一个进程就可以读取或写入整个磁盘,这样所有的保护就会失效。因此,采用的方法是引入一种新的处理模式——用户模式。
外设:串口/并口/CGA,时钟,硬盘
编译运行bootloader的过程
调试bootloader的方法
PC启动bootloader的方法
ELF执行文件的格式和加载
外设访问:读硬盘,在CGA上显示字符串
编译运行ucore OS的过程
ucore OS的启动过程
调试ucore OS的方法
中断管理:与软件相关的中断处理
外设管理:时钟
未对应的知识点