操作系统实验(1):系统软件启动过程

实验目的

操作系统是一个软件,也需要通过某种机制加载并运行它。在这里我们将通过另外一个更加简单的软件(bootloader)来完成这些工作。为此,我们需要完成一个能够切换到x86的保护模式并显示字符的bootloader,为启动操作系统ucore做准备。lab1提供了一个非常小的bootloader和ucore OS,整个bootloader执行代码小于512个字节,这样才能放到硬盘的主引导扇区中。通过分析和实现这个bootloader和ucore OS,读者可以了解到:

  • 计算机原理
    • CPU的编址与寻址: 基于分段机制的内存管理
    • CPU的中断机制
    • 外设:串口/并口/CGA,时钟,硬盘
  • Bootloader软件
    • 编译运行bootloader的过程
    • 调试bootloader的方法
    • PC启动bootloader的过程
    • ELF执行文件的格式和加载
    • 外设访问:读硬盘,在CGA上显示字符串
  • ucore OS软件

- 编译运行ucore OS的过程
- ucore OS的启动过程
- 调试ucore OS的方法
- 函数调用关系:在汇编级了解函数调用栈的结构和处理过程
- 中断管理:与软件相关的中断处理
- 外设管理:时钟

代码结构及基本说明

lab1目录下的文件结构是这样的:

  • boot/
    • asm.h
    • bootasm.S
    • bootmain.c
  • kern/
    • kern/debug/
      • assert.h
      • kdebug.c
      • kdebug.h
      • kmonitor.h
      • panic.c
      • stab.h
    • kern/driver/
      • clock.c
      • clock.h
      • console.c
      • console.h
      • intr.c
      • intr.h
      • kbdreg.h
      • picirq.c
      • picirq.h
    • kern/init/
      • init.c
    • kern/libs/
      • readline.c
      • stdio.c
    • kern/mm/
      • memlayout.h
      • mmu.h
      • pmm.c
      • pmm.h
    • kern/trap/
      • trap.c
      • trap.h
      • trapentry.S
      • vectors.S
  • libs/
    • defs.h
    • elf.h
    • error.h
    • printfmt.h
    • stdarg.h
    • stdio.h
    • string.c
    • string.h
    • x86.h
  • tools/
    • function.mk
    • gdbinit
    • grade.sh
    • kernel.ld
    • sign.c
    • vector.c
  • Makefile

实验内容

练习1:理解通过make生成执行文件的过程

1.1

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

首先列出tools/function.mk文件中的内容,其中定义了一些Makefile中用到的函数。

OBJPREFIX    := __objs_

.SECONDEXPANSION:
# -------------------- function begin --------------------

# list all files in some directories: (#directories, #types)
listf = $(filter $(if $(2),$(addprefix %.,$(2)),%),\
          $(wildcard $(addsuffix $(SLASH)*,$(1))))

# get .o obj files: (#files[, packet])
toobj = $(addprefix $(OBJDIR)$(SLASH)$(if $(2),$(2)$(SLASH)),\
        $(addsuffix .o,$(basename $(1))))

# get .d dependency files: (#files[, packet])
todep = $(patsubst %.o,%.d,$(call toobj,$(1),$(2)))

totarget = $(addprefix $(BINDIR)$(SLASH),$(1))

# change $(name) to $(OBJPREFIX)$(name): (#names)
packetname = $(if $(1),$(addprefix $(OBJPREFIX),$(1)),$(OBJPREFIX))

# cc compile template, generate rule for dep, obj: (file, cc[, flags, dir])
define cc_template
$$(call todep,$(1),$(4)): $(1) | $$$$(dir $$$$@)
    @$(2) -I$$(dir $(1)) $(3) -MM $$< -MT "$$(patsubst %.d,%.o,$$@) $$@"> $$@
$$(call toobj,$(1),$(4)): $(1) | $$$$(dir $$$$@)
    @echo + cc $$<
    $(V)$(2) -I$$(dir $(1)) $(3) -c $$< -o $$@
ALLOBJS += $$(call toobj,$(1),$(4))
endef

# compile file: (#files, cc[, flags, dir])
define do_cc_compile
$$(foreach f,$(1),$$(eval $$(call cc_template,$$(f),$(2),$(3),$(4))))
endef

# add files to packet: (#files, cc[, flags, packet, dir])
define do_add_files_to_packet
__temp_packet__ := $(call packetname,$(4))
ifeq ($$(origin $$(__temp_packet__)),undefined)
$$(__temp_packet__) :=
endif
__temp_objs__ := $(call toobj,$(1),$(5))
$$(foreach f,$(1),$$(eval $$(call cc_template,$$(f),$(2),$(3),$(5))))
$$(__temp_packet__) += $$(__temp_objs__)
endef

# add objs to packet: (#objs, packet)
define do_add_objs_to_packet
__temp_packet__ := $(call packetname,$(2))
ifeq ($$(origin $$(__temp_packet__)),undefined)
$$(__temp_packet__) :=
endif
$$(__temp_packet__) += $(1)
endef

# add packets and objs to target (target, #packes, #objs[, cc, flags])
define do_create_target
__temp_target__ = $(call totarget,$(1))
__temp_objs__ = $$(foreach p,$(call packetname,$(2)),$$($$(p))) $(3)
TARGETS += $$(__temp_target__)
ifneq ($(4),)
$$(__temp_target__): $$(__temp_objs__) | $$$$(dir $$$$@)
    $(V)$(4) $(5) $$^ -o $$@
else
$$(__temp_target__): $$(__temp_objs__) | $$$$(dir $$$$@)
endif
endef

# finish all
define do_finish_all
ALLDEPS = $$(ALLOBJS:.o=.d)
$$(sort $$(dir $$(ALLOBJS)) $(BINDIR)$(SLASH) $(OBJDIR)$(SLASH)):
    @$(MKDIR) $$@
endef

# --------------------  function end  --------------------
# compile file: (#files, cc[, flags, dir])
cc_compile = $(eval $(call do_cc_compile,$(1),$(2),$(3),$(4)))

# add files to packet: (#files, cc[, flags, packet, dir])
add_files = $(eval $(call do_add_files_to_packet,$(1),$(2),$(3),$(4),$(5)))

# add objs to packet: (#objs, packet)
add_objs = $(eval $(call do_add_objs_to_packet,$(1),$(2)))

# add packets and objs to target (target, #packes, #objs, cc, [, flags])
create_target = $(eval $(call do_create_target,$(1),$(2),$(3),$(4),$(5)))

read_packet = $(foreach p,$(call packetname,$(1)),$($(p)))

add_dependency = $(eval $(1): $(2))

finish_all = $(eval $(call do_finish_all))

下面首先给出Makefile文件的内容。

以下部分定义了一些常量、函数、编译选项等。具体可以看英文注释。

PROJ    := challenge
EMPTY    :=
SPACE    := $(EMPTY) $(EMPTY)
SLASH    := /

V       := @
#need llvm/cang-3.5+
#USELLVM := 1
# try to infer the correct GCCPREFX
ifndef GCCPREFIX
GCCPREFIX := $(shell if i386-elf-objdump -i 2>&1 | grep '^elf32-i386$$' >/dev/null 2>&1; \
    then echo 'i386-elf-'; \
    elif objdump -i 2>&1 | grep 'elf32-i386' >/dev/null 2>&1; \
    then echo ''; \
    else echo "***" 1>&2; \
    echo "*** Error: Couldn't find an i386-elf version of GCC/binutils." 1>&2; \
    echo "*** Is the directory with i386-elf-gcc in your PATH?" 1>&2; \
    echo "*** If your i386-elf toolchain is installed with a command" 1>&2; \
    echo "*** prefix other than 'i386-elf-', set your GCCPREFIX" 1>&2; \
    echo "*** environment variable to that prefix and run 'make' again." 1>&2; \
    echo "*** To turn off this error, run 'gmake GCCPREFIX= ...'." 1>&2; \
    echo "***" 1>&2; exit 1; fi)
endif

# try to infer the correct QEMU
ifndef QEMU
QEMU := $(shell if which qemu-system-i386 > /dev/null; \
    then echo 'qemu-system-i386'; exit; \
    elif which i386-elf-qemu > /dev/null; \
    then echo 'i386-elf-qemu'; exit; \
    elif which qemu > /dev/null; \
    then echo 'qemu'; exit; \
    else \
    echo "***" 1>&2; \
    echo "*** Error: Couldn't find a working QEMU executable." 1>&2; \
    echo "*** Is the directory containing the qemu binary in your PATH" 1>&2; \
    echo "***" 1>&2; exit 1; fi)
endif

# eliminate default suffix rules
.SUFFIXES: .c .S .h

# delete target files if there is an error (or make is interrupted)
.DELETE_ON_ERROR:

# define compiler and flags
ifndef  USELLVM
HOSTCC        := gcc
HOSTCFLAGS    := -g -Wall -O2
CC        := $(GCCPREFIX)gcc
CFLAGS    := -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc $(DEFS)
CFLAGS    += $(shell $(CC) -fno-stack-protector -E -x c /dev/null >/dev/null 2>&1 && echo -fno-stack-protector)
else
HOSTCC        := clang
HOSTCFLAGS    := -g -Wall -O2
CC        := clang
CFLAGS    := -fno-builtin -Wall -g -m32 -mno-sse -nostdinc $(DEFS)
CFLAGS    += $(shell $(CC) -fno-stack-protector -E -x c /dev/null >/dev/null 2>&1 && echo -fno-stack-protector)
endif

CTYPE    := c S

LD      := $(GCCPREFIX)ld
LDFLAGS    := -m $(shell $(LD) -V | grep elf_i386 2>/dev/null)
LDFLAGS    += -nostdlib

OBJCOPY := $(GCCPREFIX)objcopy
OBJDUMP := $(GCCPREFIX)objdump

COPY    := cp
MKDIR   := mkdir -p
MV        := mv
RM        := rm -f
AWK        := awk
SED        := sed
SH        := sh
TR        := tr
TOUCH    := touch -c

OBJDIR    := obj
BINDIR    := bin

ALLOBJS    :=
ALLDEPS    :=
TARGETS    :=

include tools/function.mk

listf_cc = $(call listf,$(1),$(CTYPE))

# for cc
add_files_cc = $(call add_files,$(1),$(CC),$(CFLAGS) $(3),$(2),$(4))
create_target_cc = $(call create_target,$(1),$(2),$(3),$(CC),$(CFLAGS))

# for hostcc
add_files_host = $(call add_files,$(1),$(HOSTCC),$(HOSTCFLAGS),$(2),$(3))
create_target_host = $(call create_target,$(1),$(2),$(3),$(HOSTCC),$(HOSTCFLAGS))

cgtype = $(patsubst %.$(2),%.$(3),$(1))
objfile = $(call toobj,$(1))
asmfile = $(call cgtype,$(call toobj,$(1)),o,asm)
outfile = $(call cgtype,$(call toobj,$(1)),o,out)
symfile = $(call cgtype,$(call toobj,$(1)),o,sym)

# for match pattern
match = $(shell echo $(2) | $(AWK) '{for(i=1;i<=NF;i++){if(match("$(1)","^"$$(i)"$$")){exit 1;}}}'; echo $$?)

# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# include kernel/user

INCLUDE    += libs/

CFLAGS    += $(addprefix -I,$(INCLUDE))

LIBDIR    += libs

$(call add_files_cc,$(call listf_cc,$(LIBDIR)),libs,)

以下命令编译出kernel/文件夹下的全部源文件,并链接为bin/kernel

# -------------------------------------------------------------------
# kernel

# kernel中头文件目录
KINCLUDE    += kern/debug/ \
               kern/driver/ \
               kern/trap/ \
               kern/mm/

# kernel的源代码目录
KSRCDIR        += kern/init \
               kern/libs \
               kern/debug \
               kern/driver \
               kern/trap \
               kern/mm

# 在编译选项中添加头文件包含目录
KCFLAGS        += $(addprefix -I,$(KINCLUDE))

# 调用function.mk中的add_files_cc函数,将kernel的全部源文件编译,将源文件和编译生成的OBJ文件加入kernel包(packet)中
$(call add_files_cc,$(call listf_cc,$(KSRCDIR)),kernel,$(KCFLAGS))

# 将KOBJS定义为编译生成的.o文件列表(大概??)
KOBJS    = $(call read_packet,kernel libs)

# create kernel target
# (在kernel前面加上bin/目录名)
kernel = $(call totarget,kernel)

# kernel目标依赖于tools/kernel.ld文件
$(kernel): tools/kernel.ld

# kernel目标依赖于编译生成的OBJ文件
$(kernel): $(KOBJS)
        # 输出"+ ld bin/kernel"到控制台
    @echo + ld $@
    # 即命令"ld -m    elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel  obj/kern/init/init.o ... obj/libs/printfmt.o"
    $(V)$(LD) $(LDFLAGS) -T tools/kernel.ld -o $@ $(KOBJS)
    # 将OBJ文件全部反编译为汇编文件
    @$(OBJDUMP) -S $@ > $(call asmfile,kernel)
    # 输出OBJ文件对应的符号表
    @$(OBJDUMP) -t $@ | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(call symfile,kernel)

# 将kernel包和OBJ文件添加到目标依赖
$(call create_target,kernel)

以下命令编译出boot/文件夹下的全部源文件并链接到bin/bootblock

# -------------------------------------------------------------------

# create bootblock
# bootfiles为boot/文件夹下的全部文件列表
bootfiles = $(call listf_cc,boot)
# 编译boot/文件夹下的全部文件
$(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),$(CFLAGS) -Os -nostdinc))

# (在bootblock前面加上bin/目录名)
bootblock = $(call totarget,bootblock)

# bootblock目标的依赖项为源文件对应的OBJ文件和bin/sign
$(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign)
        # 输出"+ ld bin/bootblock"到控制台
        @echo + ld $@
        # 将OBJ文件链接为bin/bootblock
    $(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 $^ -o $(call toobj,bootblock)
    # 将bin/bootblock文件反编译
    @$(OBJDUMP) -S $(call objfile,bootblock) > $(call asmfile,bootblock)
    # 将bin/bootblock转换成bin/bootblock.out二进制文件
    @$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock)
    # 用链接出的bin/sign工具将bin/bootblock.out再转换回bin/bootblock二进制文件
    @$(call totarget,sign) $(call outfile,bootblock) $(bootblock)

# 将bootblock包添加到bootblock目标
$(call create_target,bootblock)

以下代码编译tools/sign.c文件并链接到bin/sign

# -------------------------------------------------------------------

# create 'sign' tools
# 将tools/sign.c编译到OBJ文件,将源文件和中间文件添加到sign包
$(call add_files_host,tools/sign.c,sign,sign)
# 将sign包添加到sign目标
$(call create_target_host,sign,sign)

以下代码创建bin/ucore.img并将bin/bootblockbin/kernel依次拷贝进去:

# -------------------------------------------------------------------

# create ucore.img
# 在ucore.img前面加上bin/
UCOREIMG    := $(call totarget,ucore.img)

$(UCOREIMG): $(kernel) $(bootblock)
        # 创建一个大小为10000字节的空白文件
    $(V)dd if=/dev/zero of=$@ count=10000
    # 向上述文件中拷贝bin/bootblock
    $(V)dd if=$(bootblock) of=$@ conv=notrunc
    # 向上述文件中继续拷贝bin/kernel
    $(V)dd if=$(kernel) of=$@ seek=1 conv=notrunc

# 将ucore.img包添加到ucore.img目标
$(call create_target,ucore.img)

以下代码用于处理评分:

# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

$(call finish_all)

IGNORE_ALLDEPS    = clean \
                  dist-clean \
                  grade \
                  touch \
                  print-.+ \
                  handin

ifeq ($(call match,$(MAKECMDGOALS),$(IGNORE_ALLDEPS)),0)
-include $(ALLDEPS)
endif

# files for grade script

TARGETS: $(TARGETS)

.DEFAULT_GOAL := TARGETS

.PHONY: qemu qemu-nox debug debug-nox
qemu-mon: $(UCOREIMG)
    $(V)$(QEMU)  -no-reboot -monitor stdio -hda $< -serial null
qemu: $(UCOREIMG)
    $(V)$(QEMU) -no-reboot -parallel stdio -hda $< -serial null
log: $(UCOREIMG)
    $(V)$(QEMU) -no-reboot -d int,cpu_reset  -D q.log -parallel stdio -hda $< -serial null
qemu-nox: $(UCOREIMG)
    $(V)$(QEMU)   -no-reboot -serial mon:stdio -hda $< -nographic
TERMINAL        :=gnome-terminal
debug: $(UCOREIMG)
    $(V)$(QEMU) -S -s -parallel stdio -hda $< -serial null &
    $(V)sleep 2
    $(V)$(TERMINAL) -e "gdb -q -tui -x tools/gdbinit"

debug-nox: $(UCOREIMG)
    $(V)$(QEMU) -S -s -serial mon:stdio -hda $< -nographic &
    $(V)sleep 2
    $(V)$(TERMINAL) -e "gdb -q -x tools/gdbinit"

.PHONY: grade touch

GRADE_GDB_IN    := .gdb.in
GRADE_QEMU_OUT    := .qemu.out
HANDIN            := proj$(PROJ)-handin.tar.gz

TOUCH_FILES        := kern/trap/trap.c

MAKEOPTS        := --quiet --no-print-directory

grade:
    $(V)$(MAKE) $(MAKEOPTS) clean
    $(V)$(SH) tools/grade.sh

touch:
    $(V)$(foreach f,$(TOUCH_FILES),$(TOUCH) $(f))

print-%:
    @echo $($(shell echo $(patsubst print-%,%,$@) | $(TR) [a-z] [A-Z]))

.PHONY: clean dist-clean handin packall tags
clean:
    $(V)$(RM) $(GRADE_GDB_IN) $(GRADE_QEMU_OUT) cscope* tags
    -$(RM) -r $(OBJDIR) $(BINDIR)

dist-clean: clean
    -$(RM) $(HANDIN)

handin: packall
    @echo Please visit http://learn.tsinghua.edu.cn and upload $(HANDIN). Thanks!

packall: clean
    @$(RM) -f $(HANDIN)
    @tar -czf $(HANDIN) `find . -type f -o -type d | grep -v '^\.*$$' | grep -vF '$(HANDIN)'`

tags:
    @echo TAGS ALL
    $(V)rm -f cscope.files cscope.in.out cscope.out cscope.po.out tags
    $(V)find . -type f -name "*.[chS]" >cscope.files
    $(V)cscope -bq 
    $(V)ctags -L cscope.files

生成kernel所需全部OBJ文件的实际命令包括:

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
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
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
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
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
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
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
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
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
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
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
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
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
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
gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/  -c libs/string.c -o obj/libs/string.o
gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/  -c libs/printfmt.c -o obj/libs/printfmt.o

参数及其意义:(gcc官方文档

  • -I:添加包含目录
  • -fno-builtin:只接受以“__builtin_”开头的名称的内建函数
  • -Wall:开启全部警告提示
  • -ggdb:生成GDB需要的调试信息
  • -m32:为32位环境生成代码,int、long和指针都是32位
  • -gstab:生成stab格式的调试信息,仅用于gdb
  • -nostdinc:不扫描标准系统头文件,只在-I指令指定的目录中扫描
  • -fno-stack-protector:生成用于检查栈溢出的额外代码,如果发生错误,则打印错误信息并退出
  • -c:编译源文件但不进行链接
  • -o:结果的输出文件

链接生成kernel二进制文件的命令为:

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

参数及其意义:(ld参数说明

  • -m elf_i386:使用elf_i386模拟器
  • -nostdlib:只查找命令行中明确给出的库目录,不查找链接器脚本中给出的(即使链接器脚本是在命令行中给出的)
  • -T tools/kernel.ld:将tools/kernel.ld作为链接器脚本
  • -o bin/kernel:输出到bin/kernel文件

生成bootblock和sign工具所需全部OBJ文件的实际命令包括:

gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o
gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o
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

参数及其意义(重复的参数不再列出):

  • -Os:对输出文件大小进行优化,开启全部不增加代码大小的-O2优化
  • -g:以操作系统原生格式输出调试信息,gdb可以处理这一信息
  • -O2:进行大部分不以空间换时间的优化

链接生成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!(这两行是sign的输出)

参数及其意义:

  • -N:将文字和数据部分置为可读写,不将数据section置为与页对齐, 不链接共享库
  • -e start:将start符号置为程序起始点
  • -Ttext 0x7C00:链接时将".bss"、".data"或".text"置于绝对地址0x7C00

生成ucore.img的命令为:

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

参数及其意义:(dd命令说明

  • if:输入
  • of:输出
  • count=10000:只拷贝输入的10000块
  • conv=notrunc:不截短输出文件
  • seek=1:从输出文件开头跳过1个块后再开始复制

1.2

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

摘录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]);

从中可以发现,符合规范的系统主引导扇区的要求是:

  • 大小为512字节
  • 最后两个字节为0x55AA

练习2:使用qemu执行并调试lab1中的软件

为了熟悉使用qemu和gdb进行的调试工作,我们进行如下的小练习:

  1. 从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
  2. 在初始化位置0x7c00设置实地址断点,测试断点正常。
  3. 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。
  4. 自己找一个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 单步一条机器指令。

调试过程

首先在tools/gdbinit中添加set architecture i386一行,然后进入lab1/根目录下,输入make debug,得到如下界面:

02_01_start_shell.png

这样就打开了QEMU界面,并打开了一个新的命令行窗口,分成上下两个部分:上半部分的代码预览和下面的gdb。不过只有代码能滚动,gdb部分不能滚动有些难受。好像有打开成两个窗口的方法。

然后开始调试,在kernel/init/init.c:kern_init函数处有一个自动生成的断点,程序停止在了这里。继续单步执行几条,打印出的内容如下:

02_02_single_step.png

可以看出,开始执行的几条指令为:

push   %ebp
mov    %esp,%ebp
sub    $0x18,%esp
mov    $0x10fd20,%edx
02_03_break_at_start.png

然后在0x7C00地址处打上断点,并进行单步调试,得到bootloader的代码,与原boot/bootasm.S对比如下表:

bootasm.S 调试时输出的反汇编代码 说明
cell-content cell-content
start: - 位于0x7C00的开始符号
cli cli 禁用中断
cld cld 将方向标志位DS复位
xorw %ax, %ax xor %eax, %eax 设置数据段寄存器,下同
movw %ax, %ds mov %eax, %ds
movw %ax, %es mov %eax, %es
movw %ax, %ss mov %eax, %ss
seta20.1: - 打开A20
inb $0x64, %al in $0x64, %al 等待端口空闲的读指令
testb $0x2, %al test $0x2, %al
jnz seta20.1 jne 0x7c0a 把符号替换成了实际地址
movb $0xd1, %al mov $0xd1, %al
outb %al, $0x64 out %al, $0x64 等待端口空闲的写指令
seta20.2: - 符号
inb $0x64, %al in $0x64, %al 等待
testb $0x2, %al test $0x2, %al
jnz seta20.2 jne 0x7c0a 跳转到seta20.2符号
- mov $0xd1, %al 跳转之后重复执行了一遍
- out %al, $0x64
- in $0x64, %al
- test $0x2, %al
- jne 0x7c14
movb $0xdf, %al move $0xd1, %al
outb %al, $0x60 out %al $0x60
lgdt gdtdesc lgdtl (%esi) 加载线性地址
movl %cr0, %eax mov %cr0, %eax
orl $CR0_PE_ON, %eax or $0x1, %ax
movl %eax, %cr0 mov %eax, %cr0
ljmp $PROT_MODE_CSEG, $protcseg ljmp $0xb866, $0x87c32
protcseg: - -
movw $PROT_MODE_DSEG, %ax mov $0x10, %ax
movw %ax, %ds mov %eax, %ds
movw %ax, %es mov %eax, %es
movw %ax, %fs mov %eax, %fs
movw %ax, %gs mov %eax, %gs
movw %ax, %ss mov %eax, %ss
movl $0x0, %ebp mov $0x0, %ebp
movl $start, %esp mov $0x7c00, %esp
call bootmain call 0x7d0d

最后,在kernel的pmm.cpmm_init函数处打上断点,继续执行:

02_04_break_in_kernel.png

练习3:分析bootloader进入保护模式的过程

BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。

首先打开bootasm.S。

常量设置

#include <asm.h>

# 启动CPU:切换到32位保护模式,跳转到C代码。
# BIOS从硬盘的第一个扇区中,把代码
# 加载到内存中物理地址为0x7c00的位置,并在实模式下开始运行
# 其中%cs=0,%ip=7c00(cs+ip=PC)

.set PROT_MODE_CSEG,        0x8                     # kernel代码段选择器
.set PROT_MODE_DSEG,        0x10                    # kernel数据段选择器
.set CR0_PE_ON,             0x1                     # 保护模式开启

1. 关中断,将段寄存器置0

# start符号的地址应该为0x7c00,在实模式下,这是bootload的起始地址
.globl start
start:
.code16                                             # 以16位模式进行汇编
    cli                                             # 禁用中断
    cld                                             # 将方向标志位DS复位

    # 设置重要的数据段寄存器 (DS, ES, SS).
    xorw %ax, %ax                                   # 段号为0
    movw %ax, %ds                                   # -> 数据段 = 0
    movw %ax, %es                                   # -> extra段 = 0
    movw %ax, %ss                                   # -> 栈段 = 0

2. 开启A20

为了使 80286 和 8086/8088 在 real mode 下的行为一致,即:在 80286 下也产生 wraparound 现象。IBM 想出了古怪的方法:当 80286 运行在 real mode 时,将 A20 地址线(第 21 条 address bus)置为 0 ,这样使得 80286 在 real mode 下第 21 条 address line 无效,从而人为造成了 wraparound 现象。

在 OS 的 boot 阶段一般都要做打开 A20 gate 操作,虽然现在 A20 gate 缺省为开的。

打开 A20 gate 的方法最原始的是给 keyboard controller 8042 发送 A20 gate enable 命令字,就是上面所说的 0xDF 命令。摘自【x86 & x64 沉思录】 之 2. A20 地址线

    # 开启A20:
    # 为了与最早的PC兼容,
    # 物理地址线20置为低位,这样>1MB的地址
    # 就会变成0。以下代码打开A20。
seta20.1:
    inb $0x64, %al                                  # 等待8042端口不忙
    testb $0x2, %al
    jnz seta20.1

    movb $0xd1, %al                                 # 0xd1 -> 0x64端口(I8402 写端口)
    outb %al, $0x64                                 # 将向0x64发送命令

seta20.2:
    inb $0x64, %al                                  # 等待8042端口不忙
    testb $0x2, %al
    jnz seta20.2

    movb $0xdf, %al                                 # 0xdf -> 0x60端口
    outb %al, $0x60                                 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1将P2

3. 从实模式切换到保护模式

    # GDT从实模式切换到保护模式,使用GDT(全局描述表,Global Descriptor Table)
    # 和段变换,使得虚拟地址
    # 和物理地址相同,这样,
    # 切换过程中不会改变有效内存映射
    lgdt gdtdesc   # 加载GDT表
    movl %cr0, %eax  # 将CR0的保护允许位PE(Protedted Enable)置1,开启保护模式
    orl $CR0_PE_ON, %eax
    movl %eax, %cr0

    # 跳转到处于32位代码块中的下一条指令
    # Switches processor into 32-bit mode.
    ljmp $PROT_MODE_CSEG, $protcseg

4. 重置其他段寄存器

.code32                                             # 32位模式汇编代码
protcseg:
    # 设置保护模式的数据段寄存器
    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

5. 设置栈帧指针并调用C代码

    # Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)设置栈指针并调用C代码。栈区是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

练习4:分析bootloader加载ELF格式的OS的过程

通过阅读bootmain.c,了解bootloader如何加载ELF文件。通过分析源代码和通过qemu来运行并调试bootloader&OS,

  • bootloader如何读取硬盘扇区的?
  • bootloader是如何加载ELF格式的OS?

首先拿出bootmain.c。

代码开头的注释是这样的:

这是一个巨简单的bootloader,它唯一的任务就是从第一个硬盘中启动ELF格式的操作系统内核映像。
磁盘格式:这个程序(bootasm.S和bootmain.c)是bootloader,它应该被存储在磁盘的第一个扇区内;第二个扇区之后存储的是映像,必须是ELF格式的。
BOOT步骤:当CPU启动的时候,它把BIOS读入内存中并执行它。BIOS对设备进行初始化,设置中断程序,并将启动设备(比如硬盘)的第一个扇区读入内存,跳转到这一部分。假设bootloader就存储在硬盘的第一个扇区中,那么它就开始工作了。bootasm.S中的代码先开始执行,它开启保护态,并设置C代码能够运行的栈,最后调用本文件中的bootmain()函数;bootmain()函数将kernel读入内存并跳转到它。

源代码说明

此部分参考了http://www.read.seas.harvard.edu/~kohler/class/cs111-s05/notes/notes2.pdf

1. 一些常量定义:

#define SECTSIZE        512  // 扇区大小为512
#define ELFHDR          ((struct elfhdr *)0x10000)      // scratch space

2. waitdisk函数:等待硬盘准备好

我们接下来需要的就是把硬盘中从第二个扇区开始的数据读入进来。0x1F0-0x1F7是内存中的特殊片段,我们可以用inb指令向这些位置写入数据用以向磁盘说明我们要读的数据。

static void
waitdisk(void) {
    while ((inb(0x1F7) & 0xC0) != 0x40)  // 等待磁盘准备好
        /* do nothing */;
}

3. readsect函数:读入一个扇区

dst是存储数据的位置,secno是扇区的编号。

static void
readsect(void *dst, uint32_t secno) {
    // wait for disk to be ready
    waitdisk();

    outb(0x1F2, 1);                         // 读入扇区的个数为1
    outb(0x1F3, secno & 0xFF);   // 28位偏移量的第0-7位
    outb(0x1F4, (secno >> 8) & 0xFF);  // 28位偏移量的第8-15位
    outb(0x1F5, (secno >> 16) & 0xFF);  // 28位偏移量的第16-23位
    outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);  // 28位偏移量的24-27位;第28位置为0(磁盘0);29-31位必须为1
    outb(0x1F7, 0x20);                      // 命令0x20:读扇区

    // 等待磁盘准备好
    waitdisk();

    // read a sector
    insl(0x1F0, dst, SECTSIZE / 4);  // 获得128个“长字”(4字节)大小的数据,即512字节
}

4. readseg函数

从内核的offset处读count个字节到虚拟地址va中。复制的内容可能比count个字节多。

static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
    uintptr_t end_va = va + count;

    // 向下舍入到扇区边界
    va -= offset % SECTSIZE;

    // 从字节转换到扇区;kernel开始于扇区1
    uint32_t secno = (offset / SECTSIZE) + 1;

    // 如果这个函数太慢,则可以同时读多个扇区。
    // 我们会写得超出内存边界,但这并不重要:
    // 因为是以内存递增次序加载的。
    for (; va < end_va; va += SECTSIZE, secno ++) {
        readsect((void *)va, secno);
    }
}

5. bootmain函数:bootloader的入口函数

首先把libs/elf.h中的定义摘录如下:

ifndef __LIBS_ELF_H__
#define __LIBS_ELF_H__

#include <defs.h>

#define ELF_MAGIC    0x464C457FU            // 在小端下为"\x7FELF"

/* file header */
struct elfhdr {
    uint32_t e_magic;     // 必须与ELF_MAGIC相等
    uint8_t e_elf[12];
    uint16_t e_type;      // 1=relocatable, 2=executable, 3=shared object, 4=core image
    uint16_t e_machine;   // 3=x86, 4=68K, etc.
    uint32_t e_version;   // file version, always 1
    uint32_t e_entry;     // entry point if executable
    uint32_t e_phoff;     // file position of program header or 0
    uint32_t e_shoff;     // file position of section header or 0
    uint32_t e_flags;     // architecture-specific flags, usually 0
    uint16_t e_ehsize;    // size of this elf header
    uint16_t e_phentsize; // size of an entry in program header
    uint16_t e_phnum;     // number of entries in program header or 0
    uint16_t e_shentsize; // size of an entry in section header
    uint16_t e_shnum;     // number of entries in section header or 0
    uint16_t e_shstrndx;  // section number that contains section name strings
};

/* program section header */
struct proghdr {
    uint32_t p_type;   // loadable code or data, dynamic linking info,etc.
    uint32_t p_offset; // file offset of segment
    uint32_t p_va;     // virtual address to map segment
    uint32_t p_pa;     // physical address, not used
    uint32_t p_filesz; // size of segment in file
    uint32_t p_memsz;  // size of segment in memory (bigger if contains bss)
    uint32_t p_flags;  // read/write/execute bits
    uint32_t p_align;  // required alignment, invariably hardware page size
};

#endif /* !__LIBS_ELF_H__ */
void
bootmain(void) {
    // 从磁盘读出kernel映像的第一页(8个扇区?)到0x10000开始的空间,作为ELF文件读入
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

    // 是一个合法的ELF文件吗?
    if (ELFHDR->e_magic != ELF_MAGIC) {
        goto bad;
    }

    struct proghdr *ph, *eph;

    // load each program segment (ignores ph flags)
    ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);  // ph为program header的位置
    eph = ph + ELFHDR->e_phnum;  // program header中一共有多少项
    for (; ph < eph; ph ++) {
        readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);  // 虚拟地址的后8位,段的大小,段偏移量:读入对应的扇区
    }

    // 从ELF头中调用入口点,不返回
    ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

bad:  // ELF文件不合法
    outw(0x8A00, 0x8A00);
    outw(0x8A00, 0x8E00);

    /* do nothing */
    while (1);
}

步骤分析

读取硬盘扇区的步骤为:

  • 等待硬盘准备好
  • 向内存中0x1F2-0x1F7区域中写入数据,说明需要读取的扇区的信息
  • 等待硬盘准备好
  • 从0x1F0处读入一个扇区的数据

加载kernel的步骤为:

  • 读取磁盘上的1页(8个扇区),得到ELF头
  • 判断是否为合法ELF文件
  • 从ELF头中获得程序头的位置,从中获得每段的信息
  • 分别读取每段的信息
  • 读入每段对应的扇区并保存
  • 跳转到ELF文件的入口点


新增一则回应

除非特别注明,本页内容采用以下授权方式: Creative Commons Attribution-ShareAlike 3.0 License