ucore OS lab: bootloader启动ucore (3)

分析bootloader进入保护模式的过程

BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。

1.为何开启A20,以及如何开启A20

为了向下兼容早期8086 PC(只能访问1M内存空间), IBM决定在PC AT计算机系统上加个硬件逻辑,来模仿回绕特征(寻址超过1M,则又从0开始,相当于实际内存地址由取模操作得到),于是出现了A20 Gate。他们的方法就是把A20地址线控制和8042键盘控制器的一个输出进行AND操作,这样来控制A20地址线的打开(使能)和关闭(屏蔽\禁止)。当80386处于保护模式时,需要将A20开启,才能访问全部有效内存空间。

关于A20 Gate的更多描述以及如何操作8042键盘控制器,可见:
https://chyyuu.gitbooks.io/ucore_os_docs/content/lab1/lab1_appendix_a20.html

以下为bootloader中由实模式进入保护模式的相关代码,在此之前将各个数据段初始化好,然后开启A20 :

.code16                        # Assemble for 16-bit mode
    cli                        # Disable interrupts
    cld                        # String operations increment
    # Set up the important data segment registers (DS, ES, SS).
    xorw %ax, %ax              # Segment number zero
    movw %ax, %ds              # -> Data Segment
    movw %ax, %es              # -> Extra Segment
    movw %ax, %ss              # -> Stack Segment
seta20.1:
    inb $0x64, %al             # Wait for not busy(8042 input buffer empty).
    testb $0x2, %al
    jnz seta20.1

    movb $0xd1, %al            # 0xd1 -> port 0x64
    outb %al, $0x64            # 0xd1 means: write data to 8042's P2 port

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

2.如何初始化GDT表

lgdt gdtdesc

lgdt命令将如下描述的gdt表的大小和地址载入到GDTR寄存器中。

# 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

3.如何使能和进入保护模式

经过上面的准备,
将CR0中的保护模式允许位(PE)置1,就启动了保护模式.

    movl %cr0, %eax
    orl $CR0_PE_ON, %eax
    movl %eax, %cr0

接着跳转到保护模式32位下的代码段,

ljmp $PROT_MODE_CSEG, $protcseg

完成段选择子和堆栈的设置后,进入 bootmain 方法

.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

ucore OS lab: bootloader启动ucore (1)

理解通过make生成执行文件的过程

1.操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)

通过 make V= 命令可看build过程中,make执行了哪些命令。

出现最多的编译命令,如以下形式 (除源文件和目标文件外,其他参数差不多):

gcc -Ikern/init/ -march=i686 -fno-builtin -fno-PIC -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

通过源文件和目标文件进行逐步分析:

  • 编译 kern/init/init.c ,生成 obj/kern/init/init.o
  • 编译 kern/libs 下的 stdio.c readline.c, 在obj/kern/libs 目录下生成对应的.o文件
  • 编译 kern/debug 下的panic.c kdebug.c kmonitor.c, 在obj/kern/debug 目录下生成对应.o文件
  • 编译 kern/driver 下的 clock.c console.c picirq.c intr.c, 同上在对应目录生成.o文件
  • 编译 kern/trap 下的trap.c vectors.S trapentry.S, 在对应目录生成.o文件
  • 编译 kern/mm 下的pmm.c 文件,生成 obj/kern/mm/pmm.o
  • 编译 libs 下的 string.c printfmy.c 文件, 在 obj/libs下生成对应.o文件
  • ld命令指定tools目录下的kernel.ld作为link的script,将上述.o文件链接生成 bin/kernel
  • 编译 tools/sign.c, 生成sign.o并进一步得到 bin/sign
  • 编译 boot目录下的 bootasm.S 和 bootmain.c编译生成相应.o文件,两个文件链接生成 obj/bootblock.o, 最后通过sign工具生成 bin/bootblock
  • 通过 dd 命令将bin目录下的bootloader和kernel写到 ucore.img

对应到 makefile 文件中:

# create ucore.img

UCOREIMG    := $(call totarget,ucore.img)

$(UCOREIMG): $(kernel) $(bootblock)
    $(V)dd if=/dev/zero of=$@ count=10000
    $(V)dd if=$(bootblock) of=$@ conv=notrunc
    $(V)dd if=$(kernel) of=$@ seek=1 conv=notrunc

$(call create_target,ucore.img)

totarget函数的作用是给参数加上路径前缀: \
totarget = $(addprefix $(BINDIR)$(SLASH),$(1))

if: 输入文件 \
of:输出文件 \
count:拷贝的块数\
seek = 1:从输出文件跳过1个块数再开始拷贝 \
conv = notrunc : 输入文件比输出文件小时,不截断输出文件 \
dd命令参考链接:https://www.runoob.com/linux/linux-comm-dd.html

从上可知, ucore.img 的生成需要 bootblock 和 kernel 两个二进制文件。

接下来是 bootblock 对应的makefile部分 :

# create bootblock
bootfiles = $(call listf_cc,boot)
$(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),$(CFLAGS) -Os -nostdinc))

bootblock = $(call totarget,bootblock)

$(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign)
    @echo + ld $@
    $(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 $^ -o $(call toobj,bootblock)
    @$(OBJDUMP) -S $(call objfile,bootblock) > $(call asmfile,bootblock)
    @$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock)
    @$(call totarget,sign) $(call outfile,bootblock) $(bootblock)

$(call create_target,bootblock)

其中,
bootfiles = $(call listf_cc,boot)

listf_cc 函数的作用是从 boot/* 中过滤出所有 .c 和 .S 文件,即 bootmain.c 和 bootasm.S, 在makefile中截取了如下相关描述:

CTYPE   := c S
listf_cc = $(call listf,$(1),$(CTYPE))  
# list all files in some directories: (#directories, #types) 
listf = $(filter $(if $(2),$(addprefix %.,$(2)),%),\
          $(wildcard $(addsuffix $(SLASH)*,$(1)))) 
# wildcard会展开后面的通配符,即返回符合 boot/*的所有内容,且以空格分隔

$(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),$(CFLAGS) -Os -nostdinc)) ' \
通过这条命令,从 bootfiles 循环取出源文件名,会批量生成gcc编译命令,具体的过程较为复杂。最后得到 bootasm.o 和 bootmain.o。

实际命令:


gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs \
    -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc \
    -c boot/bootasm.S -o obj/boot/bootasm.o

其中关键的参数为:(直接从lab1_result拿过来的,因为的确好多不认识...,有些还难查)
    -ggdb  生成可供gdb使用的调试信息。这样才能用qemu+gdb来调试bootloader or ucore。
    -m32  生成适用于32位环境的代码。我们用的模拟硬件是32bit的80386,所以ucore也要是32位的软件。
    -gstabs  生成stabs格式的调试信息。这样要ucore的monitor可以显示出便于开发者阅读的函数调用栈信息
    -nostdinc  不使用标准库。标准库是给应用程序用的,我们是编译ucore内核,OS内核是提供服务的,所以所有的服务要自给自足。
    -fno-stack-protector  不生成用于检测缓冲区溢出的代码。这是for 应用程序的,我们是编译内核,ucore内核好像还用不到此功能。
    -Os  为减小代码大小而进行优化。根据硬件spec,主引导扇区只有512字节,我们写的简单bootloader的最终大小不能大于510字节。
    -I<dir>  添加搜索头文件的路径

gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc    -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o

新出现参数:      
    -fno-builtin  除非用__builtin_前缀,否则不进行builtin函数的优化

下面一段就是将生成的 bootasm.o , bootmain.o 和 sign 进行连接,得到二进制程序 bootblock 的 makefile 部分:

$(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign)
    @echo + ld $@
    $(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 $^ -o $(call toobj,bootblock)
    @$(OBJDUMP) -S $(call objfile,bootblock) > $(call asmfile,bootblock)
    @$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock)
    @$(call totarget,sign) $(call outfile,bootblock) $(bootblock)

实际命令:

#首先生成bootlock.o
ld -m    elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
 其中关键的参数为:
    -m <emulation>  模拟为i386上的连接器
    -nostdlib  不使用标准库
    -N  设置代码段和数据段均可读写
    -e <entry>  指定入口
    -Ttext  制定代码段开始位置

#拷贝二进制代码bootblock.o到bootblock.out
 objcopy -S -O binary obj/bootblock.o obj/bootblock.out
    其中关键的参数为:
        -S  移除所有符号和重定位信息
        -O <bfdname>  指定输出格式

#使用sign工具处理bootblock.out,生成bootblock
  bin/sign obj/bootblock.out bin/bootblock

从上可见 bootblock的生成还需要 sign。实验指导书中解释这是一个C语言小程序,是辅助工具,用于生成一个符合规范的硬盘主引导扇区。
这是生成sign的 makefile 部分:

# create 'sign' tools
$(call add_files_host,tools/sign.c,sign,sign)
$(call create_target_host,sign,sign)

实际命令:

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

下面是生成 kernel 的 makefile 代码:

# kernel

KINCLUDE    += kern/debug/ \
               kern/driver/ \
               kern/trap/ \
               kern/mm/

KSRCDIR     += kern/init \
               kern/libs \
               kern/debug \
               kern/driver \
               kern/trap \
               kern/mm

KCFLAGS     += $(addprefix -I,$(KINCLUDE))

$(call add_files_cc,$(call listf_cc,$(KSRCDIR)),kernel,$(KCFLAGS))

KOBJS   = $(call read_packet,kernel libs)

# create kernel target
kernel = $(call totarget,kernel)

$(kernel): tools/kernel.ld

$(kernel): $(KOBJS)
    @echo + ld $@
    $(V)$(LD) $(LDFLAGS) -T tools/kernel.ld -o $@ $(KOBJS)
    @$(OBJDUMP) -S $@ > $(call asmfile,kernel)
    @$(OBJDUMP) -t $@ | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(call symfile,kernel)

KINCLUDE: 头文件搜索目录 \
KSRCDIR: 源文件目录 \
KOBJS: 包含了所有连接需要的.o文件 \
KCFLAGS: gcc编译命令参数

$(call add_files_cc, $(call listf_cc,$(KSRCDIR)), kernel, $(KCFLAGS))

此条命令批量地生成编译命令,这里生成 .o 文件的编译命令类似,以init.o为例(即开头所述的那条)

gcc -Ikern/init/ -march=i686 -fno-builtin -fno-PIC -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

link 的实际命令:

ld -m    elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel  obj/kern/init/init.o obj/kern/libs/stdio.o obj/kern/libs/readline.o obj/kern/debug/panic.o obj/kern/debug/kdebug.o obj/kern/debug/kmonitor.o obj/kern/driver/clock.o obj/kern/driver/console.o obj/kern/driver/picirq.o obj/kern/driver/intr.o obj/kern/trap/trap.o obj/kern/trap/vectors.o obj/kern/trap/trapentry.o obj/kern/mm/pmm.o  obj/libs/string.o obj/libs/printfmt.o

# -T 后指定link的script

当bootblock 和 kernel 都准备好后,即可用 dd 装到 ucore.img 中了。

makefile教程参考 : https://seisman.github.io/how-to-write-makefile/index.html

2.一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

前面提到sign是一个c语言辅助工具,用于生成一个符合规范的硬盘主引导扇区。从sign.c中可发现主引导扇区是512字节大小,第511个字节是0x55,第512个字节是0xAA.