ucoreOS lab1 实验报告

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

列出本实验各练习中对应的OS原理的知识点,并说明本实验中的实现部分如何对应和体现了原理中的基本概念和关键知识点

在此练习中,大家需要通过静态分析代码来了解:

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

    进入lab1目录下运行make,会在bin文件夹下生成ucore.img

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    moocos-> pwd
    /home/moocos/Desktop/ucore_lab/labcodes/lab1
    [~/Desktop/ucore_lab/labcodes/lab1]
    moocos-> ls
    boot kern libs Makefile q.log tools
    [~/Desktop/ucore_lab/labcodes/lab1]
    moocos-> make
    + cc kern/init/init.c
    kern/init/init.c:95:1: warning: ‘lab1_switch_test’ defined but not used [-Wunused -function]
    lab1_switch_test(void) {
    ^
    + cc kern/libs/readline.c
    + cc kern/libs/stdio.c
    + cc kern/debug/kdebug.c
    kern/debug/kdebug.c:251:1: warning: ‘read_eip’ defined but not used [-Wunused-fun ction]
    read_eip(void) {
    ^
    + cc kern/debug/kmonitor.c
    + cc kern/debug/panic.c
    + cc kern/driver/clock.c
    + cc kern/driver/console.c
    + cc kern/driver/intr.c
    + cc kern/driver/picirq.c
    + cc kern/trap/trap.c
    kern/trap/trap.c:14:13: warning: ‘print_ticks’ defined but not used [-Wunused-fun ction]
    static void print_ticks() {
    ^
    kern/trap/trap.c:30:26: warning: ‘idt_pd’ defined but not used [-Wunused-variable ]
    static struct pseudodesc idt_pd = {
    ^
    + cc kern/trap/trapentry.S
    + cc kern/trap/vectors.S
    + cc kern/mm/pmm.c
    + cc libs/printfmt.c
    + cc libs/string.c
    + ld bin/kernel
    + cc boot/bootasm.S
    + cc boot/bootmain.c
    + cc tools/sign.c
    + ld bin/bootblock
    'obj/bootblock.out' size: 472 bytes
    build 512 bytes boot sector: 'bin/bootblock' success!
    10000+0 records in
    10000+0 records out
    5120000 bytes (5.1 MB) copied, 0.0179732 s, 285 MB/s
    1+0 records in
    1+0 records out
    512 bytes (512 B) copied, 0.000302065 s, 1.7 MB/s
    138+1 records in
    138+1 records out
    70775 bytes (71 kB) copied, 0.000541628 s, 131 MB/s
    [~/Desktop/ucore_lab/labcodes/lab1]
    moocos-> ls
    bin boot kern libs Makefile obj q.log tools
    [~/Desktop/ucore_lab/labcodes/lab1]
    moocos-> ls bin/
    bootblock kernel sign ucore.img
    moocos-> ls obj/
    boot bootblock.o kern kernel.sym sign
    bootblock.asm bootblock.out kernel.asm libs
    [~/Desktop/ucore_lab/labcodes/lab1]
    moocos-> cd ..
    [~/Desktop/ucore_lab/labcodes]
    moocos-> diff -r lab1 ~/moocos/ucore_lab/labcodes/lab1 #比较文件
    Only in lab1: bin
    Only in lab1: obj

    从上面diff命令可以看到,唯一不同于原文件的是生成了bin文件夹和obj文件夹

查看Makefile内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
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

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)

$(call create_target,kernel)

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

# 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)

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

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

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

# 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)

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

$(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

删除binobj文件夹 ,或者用make clean清除编译文件,运行下面命令得到make的详细信息

1
$ make "V="
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
   [~/Desktop/ucore_lab/labcodes/lab1]
moocos-> ls
bin boot kern libs Makefile obj q.log tools
[~/Desktop/ucore_lab/labcodes/lab1]
moocos-> rm -rf bin/ obj/
[~/Desktop/ucore_lab/labcodes/lab1]
moocos-> ls
boot kern libs Makefile q.log tools
[~/Desktop/ucore_lab/labcodes/lab1]
moocos-> make "V="
+ 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
kern/init/init.c:95:1: warning: ‘lab1_switch_test’ defined but not used [-Wunused-function]
lab1_switch_test(void) {
^
+ 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
+ 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
+ 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
kern/debug/kdebug.c:251:1: warning: ‘read_eip’ defined but not used [-Wunused-function]
read_eip(void) {
^
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
kern/trap/trap.c:14:13: warning: ‘print_ticks’ defined but not used [-Wunused-function]
static void print_ticks() {
^
kern/trap/trap.c:30:26: warning: ‘idt_pd’ defined but not used [-Wunused-variable]
static struct pseudodesc idt_pd = {
^
+ 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
+ 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
+ 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
+ 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
+ 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
+ ld bin/kernel
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
+ 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
+ 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
+ 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
+ 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: 472 bytes
build 512 bytes boot sector: 'bin/bootblock' success!
dd if=/dev/zero of=bin/ucore.img count=10000
10000+0 records in
10000+0 records out
5120000 bytes (5.1 MB) copied, 0.0160073 s, 320 MB/s
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
1+0 records in
1+0 records out
512 bytes (512 B) copied, 0.000253753 s, 2.0 MB/s
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
138+1 records in
138+1 records out
70775 bytes (71 kB) copied, 0.000466247 s, 152 MB/s
[~/Desktop/ucore_lab/labcodes/lab1]
moocos-> ls
bin boot kern libs Makefile obj q.log tools

可以看到,ucore.img 通过make的最后三个dd命令生成:

image-20200307223319152

对应Makefile文件命令为

image-20200307225339592

第一个dd表示在bin文件夹下创建ucore.img,其大小为10000个块。

第二个dd表示将/bin/bootblock的内容复制到第一个块,其中noerror选项意味着如果发生错误,程序也将继续运行。

第三个dd表示从第二个块开始复制/bin/kernel中的内容,seek表示跳过第一个块开始写。

因此首先要先生成bootlockkernel文件

kernel生成:

image-20200307225636354

image-20200307230243874

对应Makefile文件命令为

image-20200307234603343

gcc命令参数解释
-I 添加搜索头文件的路径
-fno-builtin 不进行builtin函数的优化
-Wall 编译后显示所有警告
-ggdb 生成可供gdb使用的调试信息
-m32 生成适用于32位环境的代码
-gstabs 生成stabs格式的调试信息
-nostdinc 不使用标准库
-fno-stack-protector 不生成用于检测缓冲区溢出的代码
-Os 为减小代码大小而进行优化
-c只激活预处理,编译,和汇编,也就是他只把程序做成obj文件

由图可知,要生成文件kernel,需要将obj/kern文件夹下的所有.c文件编译生成.o文件,有如下文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

然后用ld命令将这些.o文件生成一个可执行文件

1
2
   + ld bin/bootblock
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

ld命令参数解释
-m elf_i386表示模拟指定的连接器为elf_i386
-T tools/kernel.ld表示使用tools/kernel.ld作为链接器脚本。此脚本将替换ld的默认链接器脚本(而不是添加到其中)。
-o bin/kernel表示将输出文件在bin文件夹下,文件名为kernel
后面跟着的.o文件是所要转化的文件

bootlock生成
image-20200307234212505

对应的Makefile命令为

image-20200307234724465

可以看到,要生成bootlock,需要先生成bootams.o, bootmian.c, sign.c

gcc命令参数解释
-I 添加搜索头文件的路径
-fno-builtin 不进行builtin函数的优化
-Wall 编译后显示所有警告
-ggdb 生成可供gdb使用的调试信息
-m32 生成适用于32位环境的代码
-gstabs 生成stabs格式的调试信息
-nostdinc 不使用标准库
-fno-stack-protector 不生成用于检测缓冲区溢出的代码
-Os 为减小代码大小而进行优化
-c只激活预处理,编译,和汇编,也就是他只把程序做成obj文件

生成bootfile对应的源码由宏定义批量实现

image-20200308000103617

生成sign对应的Makefile源码为

image-20200307235431536

最后用ld命令生成

1
2
   + 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

ld命令参数解释
-m elf_i386表示模拟指定的连接器为elf_i386
-nostdlib 表示不连接系统标准启动文件和标准库文件,只把指定的文件传递给连接器。这个选项常用于编译内核、bootloader等程序,它们不需要启动文件、标准库文件。
-N 设置代码段和数据段均可读写
-e 指定入口
start 代码段开始于
-Ttext 制定代码段开始位置
-o obj/bootblock.o表示将输出文件在obj文件夹下,文件名为bootblock

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

    查看sign.c文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    moocos-> ls tools/
    function.mk gdbinit grade.sh kernel.ld sign.c vector.c
    [~/Desktop/ucore_lab/labcodes/lab1]
    moocos-> cat tools/sign.c
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    #include <sys/stat.h>

    int
    main(int argc, char *argv[]) {
    struct stat st;
    if (argc != 3) {
    fprintf(stderr, "Usage: <input filename> <output filename>\n");
    return -1;
    }
    if (stat(argv[1], &st) != 0) {
    fprintf(stderr, "Error opening file '%s': %s\n", argv[1], strerror(errno));
    return -1;
    }
    printf("'%s' size: %lld bytes\n", argv[1], (long long)st.st_size);
    if (st.st_size > 510) {
    fprintf(stderr, "%lld >> 510!!\n", (long long)st.st_size);
    return -1;
    }
    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;
    }

    得知一个被系统认为是符合规范的硬盘主引导扇区的特征有以下几点:
    - 磁盘主引导扇区只有512字节
    - 磁盘最后两个字节为0x55AA

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

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

  1. 从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。

  2. 在初始化位置0x7c00设置实地址断点,测试断点正常。

  3. 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。

  4. 自己找一个bootloader或内核中的代码位置,设置断点并进行测试。

练习2.1

首先修改文件

1
moocos-> vim tools/gdbinit

增加set architecture i8086

1
2
3
4
5
file bin/kernel
set architecture i8086 #增添语句
target remote :1234
break kern_init
continue

在GUI界面lab1目录下,执行如下语句

1
make debug

ssh环境下执行会报错

image-20200308015334271

之后使用si命令可使BIOS单步执行

image-20200308015709679

在gdb中执行x /2i $pc 来看BIOS的代码

1
x /2i $pc #显示当前eip处的汇编指令

image-20200308015905107

练习2.2

在 tools/gdbinit中加入中断

1
2
3
4
5
target remote :1234     //连接qemu,此时qemu会进入停止状态,听从gdb的命令
set architecture i8086 //设置当前调试的CPU是8086
b *0x7c00 //在0x7c00处设置断点。此地址是bootloader入口点地址,可看boot/bootasm.S的start地址处
c //continue简称,表示继续执行
x/10i $pc //显示当前eip处的汇编指令

重新make debug得到

image-20200308022217191

1
2
3
4
5
6
7
8
9
10
=> 0x7c00:      cli    
0x7c01: cld
0x7c02: xor %ax,%ax
0x7c04: mov %ax,%ds
0x7c06: mov %ax,%es
0x7c08: mov %ax,%ss
0x7c0a: in $0x64,%al
0x7c0c: test $0x2,%al
0x7c0e: jne 0x7c0a
0x7c10: mov $0xd1,%al

练习2.3

查看bootasm.S

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
moocos-> cat bootasm.S -n
1 #include <asm.h>
2

3 # Start the CPU: switch to 32-bit protected mode, jump into C.
4 # The BIOS loads this code from the first sector of the hard disk into
5 # memory at physical address 0x7c00 and starts executing in real mode
6 # with %cs=0 %ip=7c00.
7
8 .set PROT_MODE_CSEG, 0x8 # kernel code segment selector
9 .set PROT_MODE_DSEG, 0x10 # kernel data segment selector
10 .set CR0_PE_ON, 0x1 # protected mode enable flag
11
12 # start address should be 0:7c00, in real mode, the beginning address of the running bootloader
13 .globl start
14 start:
15 .code16 # Assemble for 16-bit mode
16 cli # Disable interrupts
17 cld # String operations increment
18
19 # Set up the important data segment registers (DS, ES, SS).
20 xorw %ax, %ax # Segment number zero
21 movw %ax, %ds # -> Data Segment
22 movw %ax, %es # -> Extra Segment
23 movw %ax, %ss # -> Stack Segment
24
25 # Enable A20:
26 # For backwards compatibility with the earliest PCs, physical
27 # address line 20 is tied low, so that addresses higher than
28 # 1MB wrap around to zero by default. This code undoes this.
29 seta20.1:
30 inb $0x64, %al # Wait for not busy(8042 input buffer empty).
31 testb $0x2, %al
32 jnz seta20.1
33
34 movb $0xd1, %al # 0xd1 -> port 0x64
35 outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port
36
37 seta20.2:
38 inb $0x64, %al # Wait for not busy(8042 input buffer empty).
39 testb $0x2, %al
40 jnz seta20.2
41
42 movb $0xdf, %al # 0xdf -> port 0x60
43 outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
44
45 # Switch from real to protected mode, using a bootstrap GDT
46 # and segment translation that makes virtual addresses
47 # identical to physical addresses, so that the
48 # effective memory map does not change during the switch.
49 lgdt gdtdesc
50 movl %cr0, %eax
51 orl $CR0_PE_ON, %eax
52 movl %eax, %cr0
53
54 # Jump to next instruction, but in 32-bit code segment.
55 # Switches processor into 32-bit mode.
56 ljmp $PROT_MODE_CSEG, $protcseg
57
58 .code32 # Assemble for 32-bit mode
59 protcseg:
60 # Set up the protected-mode data segment registers
61 movw $PROT_MODE_DSEG, %ax # Our data segment selector
62 movw %ax, %ds # -> DS: Data Segment
63 movw %ax, %es # -> ES: Extra Segment
64 movw %ax, %fs # -> FS
65 movw %ax, %gs # -> GS
66 movw %ax, %ss # -> SS: Stack Segment
67
68 # Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
69 movl $0x0, %ebp
70 movl $start, %esp
71 call bootmain
72
73 # If bootmain returns (it shouldn't), loop.
74 spin:
75 jmp spin
76
77 # Bootstrap GDT
78 .p2align 2 # force 4 byte alignment
79 gdt:
80 SEG_NULLASM # null seg
81 SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel
82 SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel
83
84 gdtdesc:
85 .word 0x17 # sizeof(gdt) - 1
86 .long gdt # address gdt

bootblock.asm的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
moocos-> cat ./obj/bootblock.asm -n
1
2 obj/bootblock.o: file format elf32-i386
3
4
5 Disassembly of section .text:
6
7 00007c00 <start>:
8
9 # start address should be 0:7c00, in real mode, the beginning address of the running bootloader
10 .globl start
11 start:
12 .code16 # Assemble for 16-bit mode
13 cli # Disable interrupts
14 7c00: fa cli
15 cld # String operations increment
16 7c01: fc cld
17
18 # Set up the important data segment registers (DS, ES, SS).
19 xorw %ax, %ax # Segment number zero
20 7c02: 31 c0 xor %eax,%eax
21 movw %ax, %ds # -> Data Segment
22 7c04: 8e d8 mov %eax,%ds
23 movw %ax, %es # -> Extra Segment
24 7c06: 8e c0 mov %eax,%es
25 movw %ax, %ss # -> Stack Segment
26 7c08: 8e d0 mov %eax,%ss
27
28 00007c0a <seta20.1>:
29 # Enable A20:
30 # For backwards compatibility with the earliest PCs, physical
31 # address line 20 is tied low, so that addresses higher than
32 # 1MB wrap around to zero by default. This code undoes this.
33 seta20.1:
34 inb $0x64, %al # Wait for not busy(8042 input buffer empty).
35 7c0a: e4 64 in $0x64,%al
36 testb $0x2, %al
37 7c0c: a8 02 test $0x2,%al
38 jnz seta20.1
39 7c0e: 75 fa jne 7c0a <seta20.1>
40
41 movb $0xd1, %al # 0xd1 -> port 0x64
42 7c10: b0 d1 mov $0xd1,%al
43 outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port
44 7c12: e6 64 out %al,$0x64
45
46 00007c14 <seta20.2>:
47
48 seta20.2:
49 inb $0x64, %al # Wait for not busy(8042 input buffer empty).
50 7c14: e4 64 in $0x64,%al
51 testb $0x2, %al
52 7c16: a8 02 test $0x2,%al
53 jnz seta20.2
54 7c18: 75 fa jne 7c14 <seta20.2>
55
56 movb $0xdf, %al # 0xdf -> port 0x60
57 7c1a: b0 df mov $0xdf,%al
58 outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
59 7c1c: e6 60 out %al,$0x60
60
61 # Switch from real to protected mode, using a bootstrap GDT
62 # and segment translation that makes virtual addresses
63 # identical to physical addresses, so that the
64 # effective memory map does not change during the switch.
65 lgdt gdtdesc
66 7c1e: 0f 01 16 lgdtl (%esi)
67 7c21: 6c insb (%dx),%es:(%edi)
68 7c22: 7c 0f jl 7c33 <protcseg+0x1>
69 movl %cr0, %eax
70 7c24: 20 c0 and %al,%al
71 orl $CR0_PE_ON, %eax
72 7c26: 66 83 c8 01 or $0x1,%ax
73 movl %eax, %cr0
74 7c2a: 0f 22 c0 mov %eax,%cr0
75
76 # Jump to next instruction, but in 32-bit code segment.
77 # Switches processor into 32-bit mode.
78 ljmp $PROT_MODE_CSEG, $protcseg
79 7c2d: ea 32 7c 08 00 66 b8 ljmp $0xb866,$0x87c32
80
81 00007c32 <protcseg>:
82
83 .code32 # Assemble for 32-bit mode
84 protcseg:
85 # Set up the protected-mode data segment registers
86 movw $PROT_MODE_DSEG, %ax # Our data segment selector
87 7c32: 66 b8 10 00 mov $0x10,%ax
88 movw %ax, %ds # -> DS: Data Segment
89 7c36: 8e d8 mov %eax,%ds
90 movw %ax, %es # -> ES: Extra Segment
91 7c38: 8e c0 mov %eax,%es
92 movw %ax, %fs # -> FS
93 7c3a: 8e e0 mov %eax,%fs
94 movw %ax, %gs # -> GS
95 7c3c: 8e e8 mov %eax,%gs
96 movw %ax, %ss # -> SS: Stack Segment
97 7c3e: 8e d0 mov %eax,%ss
98
99 # Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
100 movl $0x0, %ebp
101 7c40: bd 00 00 00 00 mov $0x0,%ebp
102 movl $start, %esp
103 7c45: bc 00 7c 00 00 mov $0x7c00,%esp
104 call bootmain
105 7c4a: e8 82 00 00 00 call 7cd1 <bootmain>
106
107 00007c4f <spin>:
108
109 # If bootmain returns (it shouldn't), loop.
110 spin:
111 jmp spin
112 7c4f: eb fe jmp 7c4f <spin>
113 7c51: 8d 76 00 lea 0x0(%esi),%esi
114
115 00007c54 <gdt>:
116 ...
117 7c5c: ff (bad)
118 7c5d: ff 00 incl (%eax)
119 7c5f: 00 00 add %al,(%eax)
120 7c61: 9a cf 00 ff ff 00 00 lcall $0x0,$0xffff00cf
121 7c68: 00 92 cf 00 17 00 add %dl,0x1700cf(%edx)
122
123 00007c6c <gdtdesc>:
124 7c6c: 17 pop %ss
125 7c6d: 00 54 7c 00 add %dl,0x0(%esp,%edi,2)
126 ...
127
128 00007c72 <readsect>:
129 /* do nothing */;
130 }
131
132 /* readsect - read a single sector at @secno into @dst */
133 static void
134 readsect(void *dst, uint32_t secno) {
135 7c72: 55 push %ebp
136 7c73: 89 d1 mov %edx,%ecx
137 7c75: 89 e5 mov %esp,%ebp
138 static inline void ltr(uint16_t sel) __attribute__((always_inline));
139
140 static inline uint8_t
141 inb(uint16_t port) {
142 uint8_t data;
143 asm volatile ("inb %1, %0" : "=a" (data) : "d" (port));
144 7c77: ba f7 01 00 00 mov $0x1f7,%edx
145 7c7c: 57 push %edi
146 7c7d: 89 c7 mov %eax,%edi
147 7c7f: ec in (%dx),%al
148 #define ELFHDR ((struct elfhdr *)0x10000) // scratch space
149
150 /* waitdisk - wait for disk ready */
151 static void
152 waitdisk(void) {
153 while ((inb(0x1F7) & 0xC0) != 0x40)
154 7c80: 83 e0 c0 and $0xffffffc0,%eax
155 7c83: 3c 40 cmp $0x40,%al
156 7c85: 75 f8 jne 7c7f <readsect+0xd>
157 : "memory", "cc");
158 }
159
160 static inline void
161 outb(uint16_t port, uint8_t data) {
162 asm volatile ("outb %0, %1" :: "a" (data), "d" (port));
163 7c87: ba f2 01 00 00 mov $0x1f2,%edx
164 7c8c: b0 01 mov $0x1,%al
165 7c8e: ee out %al,(%dx)
166 7c8f: 0f b6 c1 movzbl %cl,%eax
167 7c92: b2 f3 mov $0xf3,%dl
168 7c94: ee out %al,(%dx)
169 7c95: 0f b6 c5 movzbl %ch,%eax
170 7c98: b2 f4 mov $0xf4,%dl
171 7c9a: ee out %al,(%dx)
172 waitdisk();
173
174 outb(0x1F2, 1); // count = 1
175 outb(0x1F3, secno & 0xFF);
176 outb(0x1F4, (secno >> 8) & 0xFF);
177 outb(0x1F5, (secno >> 16) & 0xFF);
178 7c9b: 89 c8 mov %ecx,%eax
179 7c9d: b2 f5 mov $0xf5,%dl
180 7c9f: c1 e8 10 shr $0x10,%eax
181 7ca2: 0f b6 c0 movzbl %al,%eax
182 7ca5: ee out %al,(%dx)
183 outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
184 7ca6: c1 e9 18 shr $0x18,%ecx
185 7ca9: b2 f6 mov $0xf6,%dl
186 7cab: 88 c8 mov %cl,%al
187 7cad: 83 e0 0f and $0xf,%eax
188 7cb0: 83 c8 e0 or $0xffffffe0,%eax
189 7cb3: ee out %al,(%dx)
190 7cb4: b0 20 mov $0x20,%al
191 7cb6: b2 f7 mov $0xf7,%dl
192 7cb8: ee out %al,(%dx)
193 static inline void ltr(uint16_t sel) __attribute__((always_inline));
194
195 static inline uint8_t
196 inb(uint16_t port) {
197 uint8_t data;
198 asm volatile ("inb %1, %0" : "=a" (data) : "d" (port));
199 7cb9: ec in (%dx),%al
200 #define ELFHDR ((struct elfhdr *)0x10000) // scratch space
201
202 /* waitdisk - wait for disk ready */
203 static void
204 waitdisk(void) {
205 while ((inb(0x1F7) & 0xC0) != 0x40)
206 7cba: 83 e0 c0 and $0xffffffc0,%eax
207 7cbd: 3c 40 cmp $0x40,%al
208 7cbf: 75 f8 jne 7cb9 <readsect+0x47>
209 return data;
210 }
211
212 static inline void
213 insl(uint32_t port, void *addr, int cnt) {
214 asm volatile (
215 7cc1: b9 80 00 00 00 mov $0x80,%ecx
216 7cc6: ba f0 01 00 00 mov $0x1f0,%edx
217 7ccb: fc cld
218 7ccc: f2 6d repnz insl (%dx),%es:(%edi)
219 // wait for disk to be ready
220 waitdisk();
221
222 // read a sector
223 insl(0x1F0, dst, SECTSIZE / 4);
224 }
225 7cce: 5f pop %edi
226 7ccf: 5d pop %ebp
227 7cd0: c3 ret
228
229 00007cd1 <bootmain>:
230 }
231 }
232
233 /* bootmain - the entry of bootloader */
234 void
235 bootmain(void) {
236 7cd1: 55 push %ebp
237 7cd2: 89 e5 mov %esp,%ebp
238 7cd4: 57 push %edi
239 7cd5: 56 push %esi
240 7cd6: 53 push %ebx
241
242 // round down to sector boundary
243 va -= offset % SECTSIZE;
244
245 // translate from bytes to sectors; kernel starts at sector 1
246 uint32_t secno = (offset / SECTSIZE) + 1;
247 7cd7: bb 01 00 00 00 mov $0x1,%ebx
248 }
249 }
250
251 /* bootmain - the entry of bootloader */
252 void
253 bootmain(void) {
254 7cdc: 83 ec 1c sub $0x1c,%esp
255 7cdf: 8d 43 7f lea 0x7f(%ebx),%eax
256
257 // If this is too slow, we could read lots of sectors at a time.
258 // We'd write more to memory than asked, but it doesn't matter --
259 // we load in increasing order.
260 for (; va < end_va; va += SECTSIZE, secno ++) {
261 readsect((void *)va, secno);
262 7ce2: 89 da mov %ebx,%edx
263 7ce4: c1 e0 09 shl $0x9,%eax
264 uint32_t secno = (offset / SECTSIZE) + 1;
265
266 // If this is too slow, we could read lots of sectors at a time.
267 // We'd write more to memory than asked, but it doesn't matter --
268 // we load in increasing order.
269 for (; va < end_va; va += SECTSIZE, secno ++) {
270 7ce7: 43 inc %ebx
271 readsect((void *)va, secno);
272 7ce8: e8 85 ff ff ff call 7c72 <readsect>
273 uint32_t secno = (offset / SECTSIZE) + 1;
274
275 // If this is too slow, we could read lots of sectors at a time.
276 // We'd write more to memory than asked, but it doesn't matter --
277 // we load in increasing order.
278 for (; va < end_va; va += SECTSIZE, secno ++) {
279 7ced: 83 fb 09 cmp $0x9,%ebx
280 7cf0: 75 ed jne 7cdf <bootmain+0xe>
281 bootmain(void) {
282 // read the 1st page off disk
283 readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
284
285 // is this a valid ELF?
286 if (ELFHDR->e_magic != ELF_MAGIC) {
287 7cf2: 81 3d 00 00 01 00 7f cmpl $0x464c457f,0x10000
288 7cf9: 45 4c 46
289 7cfc: 75 6a jne 7d68 <bootmain+0x97>
290 }
291
292 struct proghdr *ph, *eph;
293
294 // load each program segment (ignores ph flags)
295 ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
296 7cfe: a1 1c 00 01 00 mov 0x1001c,%eax
297 7d03: 8d 98 00 00 01 00 lea 0x10000(%eax),%ebx
298 eph = ph + ELFHDR->e_phnum;
299 7d09: 0f b7 05 2c 00 01 00 movzwl 0x1002c,%eax
300 7d10: c1 e0 05 shl $0x5,%eax
301 7d13: 01 d8 add %ebx,%eax
302 7d15: 89 45 e4 mov %eax,-0x1c(%ebp)
303 for (; ph < eph; ph ++) {
304 7d18: 3b 5d e4 cmp -0x1c(%ebp),%ebx
305 7d1b: 73 3f jae 7d5c <bootmain+0x8b>
306 readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
307 7d1d: 8b 73 08 mov 0x8(%ebx),%esi
308 * readseg - read @count bytes at @offset from kernel into virtual address @va,
309 * might copy more than asked.
310 * */
311 static void
312 readseg(uintptr_t va, uint32_t count, uint32_t offset) {
313 uintptr_t end_va = va + count;
314 7d20: 8b 43 14 mov 0x14(%ebx),%eax
315
316 // load each program segment (ignores ph flags)
317 ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
318 eph = ph + ELFHDR->e_phnum;
319 for (; ph < eph; ph ++) {
320 readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
321 7d23: 8b 4b 04 mov 0x4(%ebx),%ecx
322 7d26: 81 e6 ff ff ff 00 and $0xffffff,%esi
323 * readseg - read @count bytes at @offset from kernel into virtual address @va,
324 * might copy more than asked.
325 * */
326 static void
327 readseg(uintptr_t va, uint32_t count, uint32_t offset) {
328 uintptr_t end_va = va + count;
329 7d2c: 01 f0 add %esi,%eax
330 7d2e: 89 45 e0 mov %eax,-0x20(%ebp)
331
332 // round down to sector boundary
333 va -= offset % SECTSIZE;
334 7d31: 89 c8 mov %ecx,%eax
335 7d33: 25 ff 01 00 00 and $0x1ff,%eax
336
337 // translate from bytes to sectors; kernel starts at sector 1
338 uint32_t secno = (offset / SECTSIZE) + 1;
339 7d38: c1 e9 09 shr $0x9,%ecx
340 static void
341 readseg(uintptr_t va, uint32_t count, uint32_t offset) {
342 uintptr_t end_va = va + count;
343
344 // round down to sector boundary
345 va -= offset % SECTSIZE;
346 7d3b: 29 c6 sub %eax,%esi
347
348 // translate from bytes to sectors; kernel starts at sector 1
349 uint32_t secno = (offset / SECTSIZE) + 1;
350 7d3d: 8d 79 01 lea 0x1(%ecx),%edi
351
352 // If this is too slow, we could read lots of sectors at a time.
353 // We'd write more to memory than asked, but it doesn't matter --
354 // we load in increasing order.
355 for (; va < end_va; va += SECTSIZE, secno ++) {
356 7d40: 3b 75 e0 cmp -0x20(%ebp),%esi
357 7d43: 73 12 jae 7d57 <bootmain+0x86>
358 readsect((void *)va, secno);
359 7d45: 89 fa mov %edi,%edx
360 7d47: 89 f0 mov %esi,%eax
361 7d49: e8 24 ff ff ff call 7c72 <readsect>
362 uint32_t secno = (offset / SECTSIZE) + 1;
363
364 // If this is too slow, we could read lots of sectors at a time.
365 // We'd write more to memory than asked, but it doesn't matter --
366 // we load in increasing order.
367 for (; va < end_va; va += SECTSIZE, secno ++) {
368 7d4e: 81 c6 00 02 00 00 add $0x200,%esi
369 7d54: 47 inc %edi
370 7d55: eb e9 jmp 7d40 <bootmain+0x6f>
371 struct proghdr *ph, *eph;
372
373 // load each program segment (ignores ph flags)
374 ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
375 eph = ph + ELFHDR->e_phnum;
376 for (; ph < eph; ph ++) {
377 7d57: 83 c3 20 add $0x20,%ebx
378 7d5a: eb bc jmp 7d18 <bootmain+0x47>
379 readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
380 }
381
382 // call the entry point from the ELF header
383 // note: does not return
384 ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
385 7d5c: a1 18 00 01 00 mov 0x10018,%eax
386 7d61: 25 ff ff ff 00 and $0xffffff,%eax
387 7d66: ff d0 call *%eax
388 asm volatile ("outb %0, %1" :: "a" (data), "d" (port));
389 }
390
391 static inline void
392 outw(uint16_t port, uint16_t data) {
393 asm volatile ("outw %0, %1" :: "a" (data), "d" (port));
394 7d68: b8 00 8a ff ff mov $0xffff8a00,%eax
395 7d6d: 89 c2 mov %eax,%edx
396 7d6f: 66 ef out %ax,(%dx)
397 7d71: b8 00 8e ff ff mov $0xffff8e00,%eax
398 7d76: 66 ef out %ax,(%dx)
399 7d78: eb fe jmp 7d78 <bootmain+0xa7>

在调用qemu 时增加-d in_asm -D q.log 参数,便可以将运行的汇编指令保存在q.log 中。

改写Makefile文件第220行

1
2
3
4
debug: $(UCOREIMG)
$(V)$(TERMINAL) -e "$(QEMU) -S -s -d in_asm -D $(BINDIR)/q.log -parallel stdio -hda $< -serial null"
$(V)sleep 2
$(V)$(TERMINAL) -e "gdb -q -tui -x tools/gdbinit"

重新make debug

gdb中运行命令

1
2
b *0x7c4a
c

得到0x7c00到bootmain函数入口前(0x7c4a)的debug的汇编指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
----------------
IN:
0x00007c00: cli

----------------
IN:
0x00007c00: cli

----------------
IN:
0x00007c01: cld
0x00007c02: xor %ax,%ax
0x00007c04: mov %ax,%ds
0x00007c06: mov %ax,%es
0x00007c08: mov %ax,%ss

----------------
IN:
0x00007c0a: in $0x64,%al

----------------
IN:
0x00007c0c: test $0x2,%al
0x00007c0e: jne 0x7c0a

----------------
IN:
0x00007c10: mov $0xd1,%al
0x00007c12: out %al,$0x64
0x00007c14: in $0x64,%al
0x00007c16: test $0x2,%al
0x00007c18: jne 0x7c14

----------------
IN:
0x00007c1a: mov $0xdf,%al
0x00007c1c: out %al,$0x60
0x00007c1e: lgdtw 0x7c6c
0x00007c23: mov %cr0,%eax
0x00007c26: or $0x1,%eax
0x00007c2a: mov %eax,%cr0

----------------
IN:
0x00007c2d: ljmp $0x8,$0x7c32

----------------
IN:
0x00007c32: mov $0x10,%ax
0x00007c36: mov %eax,%ds

----------------
IN:
0x00007c38: mov %eax,%es

----------------
IN:
0x00007c3a: mov %eax,%fs
0x00007c3c: mov %eax,%gs
0x00007c3e: mov %eax,%ss

----------------
IN:
0x00007c40: mov $0x0,%ebp

----------------
IN:
0x00007c45: mov $0x7c00,%esp
0x00007c4a: call 0x7cd1

可见三者对应且一致。

练习2.4

设置0x7cd7为下一个断点

得到bootmain对应的汇编码

1
2
3
4
5
6
7
8
----------------
IN:
0x00007cd1: push %ebp
0x00007cd2: mov %esp,%ebp
0x00007cd4: push %edi
0x00007cd5: push %esi
0x00007cd6: push %ebx
0x00007cd7: mov $0x1,%ebx

对应bootasm.S源码

1
71      call bootmain

查看bootblock.asm的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
233  /* bootmain - the entry of bootloader */
234 void
235 bootmain(void) {
236 7cd1: 55 push %ebp
237 7cd2: 89 e5 mov %esp,%ebp
238 7cd4: 57 push %edi
239 7cd5: 56 push %esi
240 7cd6: 53 push %ebx
241
242 // round down to sector boundary
243 va -= offset % SECTSIZE;
244
245 // translate from bytes to sectors; kernel starts at sector 1
246 uint32_t secno = (offset / SECTSIZE) + 1;
247 7cd7: bb 01 00 00 00 mov $0x1,%ebx
248 }

三者对应

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

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

提示:需要阅读**小节“保护模式和分段机制”**和lab1/boot/bootasm.S源码,了解如何从实模式切换到保护模式,需要了解:

  • 为何开启A20,以及如何开启A20

  • 如何初始化GDT表

  • 如何使能和进入保护模式

bootasm.S 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
moocos-> cat boot/bootasm.S -n
1 #include <asm.h>
2 #导入asm.h头文件,用于定义GDT,GDT是保护模式使用的全局段描述表,其中存储者段描述符
3 # Start the CPU: switch to 32-bit protected mode, jump into C.
4 # The BIOS loads this code from the first sector of the hard disk into
5 # memory at physical address 0x7c00 and starts executing in real mode
6 # with %cs=0 %ip=7c00.
7 # 此处注释说明了此段代码的目的是启动保护模式,转入C。计算机加电后,由BIOS将bootasm.S生成的可执行代码从硬盘的第一个扇区复制到内存中的物理地址0x7c00处,并开始执行。此时系统处于实模式。可用内存不多于1M。
8 .set PROT_MODE_CSEG, 0x8 # kernel code segment selector
9 .set PROT_MODE_DSEG, 0x10 # kernel data segment selector
# 这两个段选择子提供了gdt中代码段和数据段的索引
10 .set CR0_PE_ON, 0x1 # protected mode enable flag
# 此变量是开启A20地址线的标志,为1是开启保护模式
11
12 # start address should be 0:7c00, in real mode, the beginning address of the running bootloader
13 .globl start
14 start:
#此处相当于C语言的main函数,是BIOS调用程序的执行入口
15 .code16 # Assemble for 16-bit mode
16 cli # Disable interrupts
17 cld # String operations increment #使方向标志位复位
18
19 # Set up the important data segment registers (DS, ES, SS).
20 xorw %ax, %ax # Segment number zero #将ax清零
21 movw %ax, %ds # -> Data Segment
22 movw %ax, %es # -> Extra Segment
23 movw %ax, %ss # -> Stack Segment
24 #上面三段是汇编常写的三段代码,将段选择子清零
25 # Enable A20:
26 # For backwards compatibility with the earliest PCs, physical
27 # address line 20 is tied low, so that addresses higher than
28 # 1MB wrap around to zero by default. This code undoes this.
#为了与最早的PC向后兼容,物理地址线20设置为低电平,因此高于1MB的地址默认情况下会回零。 此代码撤消了此操作。
# 接下来是激活保护模式
#由于历史原因A20地址位由键盘控制器芯片8042管理。所以要给8042发命令激活A20,8042有两个IO端口:0x60和0x64, 激活流程位: 发送0xd1命令到0x64端口 --> 发送0xdf到0x60
29 seta20.1:
30 inb $0x64, %al # Wait for not busy(8042 input buffer empty). #读入数据
31 testb $0x2, %al
32 jnz seta20.1
33 #发送命令之前,要等待键盘输入缓冲区为空,这通过8042的状态寄存器的第2bit来观察,而状态寄存器的值可以读0x64端口得到。上面的指令的意思就是,如果状态寄存器的第2位为1,就跳到seta20.1符号处执行,知道第2位为0,代表缓冲区为空
34 movb $0xd1, %al # 0xd1 -> port 0x64
35 outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port
36 # 将数据0xd1写入0x64 IO端口
37 seta20.2:
38 inb $0x64, %al # Wait for not busy(8042 input buffer empty).
39 testb $0x2, %al
40 jnz seta20.2
41
42 movb $0xdf, %al # 0xdf -> port 0x60
43 outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
44 #此处A20激活成功
45 # Switch from real to protected mode, using a bootstrap GDT
46 # and segment translation that makes virtual addresses
47 # identical to physical addresses, so that the
48 # effective memory map does not change during the switch.
#转入保护模式,这里需要指定一个临时的GDT,来翻译逻辑(虚拟)地址。这里使用的GDT通过gdtdesc段定义。它翻译得到的物理地址和虚拟地址相同,所以转换过程中内存映射不会改变
49 lgdt gdtdesc #载入gdt
50 movl %cr0, %eax
51 orl $CR0_PE_ON, %eax
52 movl %eax, %cr0
53 #打开保护模式标志位,开启保护模式,cr0寄存器的第0位就是这个开关,通过CR0_PE_ON或cr0寄存器,将第0位置1
54 # Jump to next instruction, but in 32-bit code segment.
55 # Switches processor into 32-bit mode.
56 ljmp $PROT_MODE_CSEG, $protcseg
57 #开保护模式之后就要使用逻辑地址。
58 .code32 # Assemble for 32-bit mode
59 protcseg:
60 # Set up the protected-mode data segment registers
61 movw $PROT_MODE_DSEG, %ax # Our data segment selector
62 movw %ax, %ds # -> DS: Data Segment
63 movw %ax, %es # -> ES: Extra Segment
64 movw %ax, %fs # -> FS
65 movw %ax, %gs # -> GS
66 movw %ax, %ss # -> SS: Stack Segment
67 #设置段寄存器,并建立堆栈。
68 # Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
69 movl $0x0, %ebp
70 movl $start, %esp
71 call bootmain
72#栈顶设定在start处,也就是地址0x7c00处,call函数将返回地址入栈,将控制权交给bootmain主方法
73 # If bootmain returns (it shouldn't), loop.
74 spin:
75 jmp spin #死循环
76
77 # Bootstrap GDT
78 .p2align 2 # force 4 byte alignment
79 gdt:
80 SEG_NULLASM # null seg
81 SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel
82 SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel
83
84 gdtdesc:
85 .word 0x17 # sizeof(gdt) - 1
86 .long gdt # address gdt

asm.h的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
moocos-> cat boot/asm.h -n
1 #ifndef __BOOT_ASM_H__
2 #define __BOOT_ASM_H__
3
4 /* Assembler macros to create x86 segments */
5
6 /* Normal segment */
7 #define SEG_NULLASM \
8 .word 0, 0; \
9 .byte 0, 0, 0, 0
10
11 #define SEG_ASM(type,base,lim) \
12 .word (((lim) >> 12) & 0xffff), ((base) & 0xffff); \
13 .byte (((base) >> 16) & 0xff), (0x90 | (type)), \
14 (0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)
15
16
17 /* Application segment type bits */
18 #define STA_X 0x8 // Executable segment
19 #define STA_E 0x4 // Expand down (non-executable segments)
20 #define STA_C 0x4 // Conforming code segment (executable only)
21 #define STA_W 0x2 // Writeable (non-executable segments)
22 #define STA_R 0x2 // Readable (executable segments)
23 #define STA_A 0x1 // Accessed
24
25 #endif /* !__BOOT_ASM_H__ */
26

练习3.1

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

当A20地址线控制禁止时,程序就像运行在8086上,1MB以上的地址是不可访问的,只能访问奇数MB的不连续的地址。为了使能所有地址位的寻址能力,必须向键盘控制器8082发送一个命令,键盘控制器8042会将A20线置于高电位,使全部32条地址线可用,实现访问4GB内存。

激活方法:要给8042发命令激活A20,8042有两个IO端口:0x60和0x64, 激活流程位: 发送0xd1命令到0x64端口 --> 发送0xdf到0x60

练习3.2

如何初始化GDT表

**全局描述符表(GDT)**的是提供内存保护。在80286之前的处理器中只有实模式,所有程序都可访问任意内存。GDT是保护模式下限制非法内存访问的一种方式。

直接载入gpt

1
lgdt gdtdesc #载入gdt

练习3.3

如何使能和进入保护模式

通过CR0_PE_ON或cr0寄存器,将第0位置1

1
2
3
50      movl %cr0, %eax 
51 orl $CR0_PE_ON, %eax
52 movl %eax, %cr0

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

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

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

提示:可阅读“硬盘访问概述”,“ELF执行文件格式概述”这两小节。

bootmain.c源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
moocos-> cat boot/bootmain.c -n
1 #include <defs.h>
2 #include <x86.h>
3 #include <elf.h>
4
5 /* *************************************************** **********************
6 这是一个非常简单的引导加载程序,它的唯一工作就是引导
7 *来自第一个IDE硬盘的ELF内核映像。
8 *
9 *磁盘布局
10 * *该程序(bootasm.S和bootmain.c)是引导加载程序。
11 *应该存储在磁盘的第一个扇区中。
12 *
13 * *第二个扇区开始保存内核映像。
14 *
15 * *内核映像必须为ELF格式。
16 *
17 *启动步骤
18 * *当CPU启动时,它将BIOS加载到内存中并执行
19 *
20 * * BIOS初始化设备,中断例程集以及
21 *读取引导设备的第一个扇区(例如,硬盘驱动器)
22 *进入内存并跳转到它。
23 *
24 * *假设此引导加载程序存储在磁盘的第一个扇区中
25 *硬盘,此代码接管...
26 *
27 * *控制从bootasm.S开始-设置保护模式,
28 *和一个堆栈,然后运行C代码,然后调用bootmain()
29 *
该文件中的30 * * bootmain()接管,读取内核并跳转到该内核。
31 * */
32
33 #define SECTSIZE 512
34 #define ELFHDR ((struct elfhdr *)0x10000) //暂存空间
35
36 /* waitdisk-等待磁盘就绪 */
37 static void
38 waitdisk(void) {
39 while ((inb(0x1F7) & 0xC0) != 0x40)
40 /* do nothing */;
41 }
42
43 /* readsect-将@secno的单个扇区读入@dst */
44 static void
45 readsect(void *dst, uint32_t secno) {
46 //等待磁盘准备好
47 waitdisk();
48
49 outb(0x1F2, 1); // count = 1
50 outb(0x1F3, secno & 0xFF);
51 outb(0x1F4, (secno >> 8) & 0xFF);
52 outb(0x1F5, (secno >> 16) & 0xFF);
53 outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
54 outb(0x1F7, 0x20); // cmd 0x20 - read 读取扇区
55
56 //等待磁盘准备好
57 waitdisk();
58
59 //读一个扇区
60 insl(0x1F0, dst, SECTSIZE / 4);
61 }
62
63 /* *
64 * readseg-从内核将@count处的@count字节读取到虚拟地址@va中,
65 *可能会复印超过要求的数量。
66 * */
67 static void
68 readseg(uintptr_t va, uint32_t count, uint32_t offset) {
69 uintptr_t end_va = va + count;
70
71 //向下舍入到扇区边界
72 va -= offset % SECTSIZE;
73
74 //从字节转换为扇区;内核从扇区1开始
75 uint32_t secno = (offset / SECTSIZE) + 1;
76 //如果太慢,我们可以一次读取很多扇区。
78 //我们向内存中写入的内容超出了要求,但这没关系-
79 //我们按升序加载。
80 for (; va < end_va; va += SECTSIZE, secno ++) {
81 readsect((void *)va, secno);
82 }
83 }
84
85 /* bootmain-引导加载程序的条目*/
86 void
87 bootmain(void) {
88 // 首先读取ELF的头部
89 readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
90
91 // 通过储存在头部的幻数判断是否是合法的ELF文件
92 if (ELFHDR->e_magic != ELF_MAGIC) {
93 goto bad;
94 }
95
96 struct proghdr *ph, *eph;
97
98 // ELF头部有描述ELF文件应加载到内存什么位置的描述表, 先将描述表的头地址存在ph
99 ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
100 eph = ph + ELFHDR->e_phnum;
// 按照描述表将ELF文件中数据载入内存
101 for (; ph < eph; ph ++) {
102 readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
103 }
// ELF文件0x1000位置后面的0xd1ec比特被载入内存0x00100000
// ELF文件0xf000位置后面的0x1d20比特被载入内存0x0010e000
104
105 // 根据ELF头部储存的入口信息,找到内核的入口
106 //注意:不返回 --死循环
107 ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
108
109 bad:
110 outw(0x8A00, 0x8A00);
111 outw(0x8A00, 0x8E00);
112
113 /* do nothing */
114 while (1);
115 }
116

练习4.1

bootloader如何读取硬盘扇区的?

读取扇区代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
36  /* waitdisk-等待磁盘就绪 */
37 static void
38 waitdisk(void) {
39 while ((inb(0x1F7) & 0xC0) != 0x40)
40 /* do nothing */;
41 }
42
43 /* readsect-将@secno的单个扇区读入@dst */
44 static void
45 readsect(void *dst, uint32_t secno) {
46 //等待磁盘准备好
47 waitdisk();
48
49 outb(0x1F2, 1); // count = 1
50 outb(0x1F3, secno & 0xFF);
51 outb(0x1F4, (secno >> 8) & 0xFF);
52 outb(0x1F5, (secno >> 16) & 0xFF);
53 outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
54 outb(0x1F7, 0x20); // cmd 0x20 - read 读取扇区
55
56 //等待磁盘准备好
57 waitdisk();
58
59 //读一个扇区
60 insl(0x1F0, dst, SECTSIZE / 4);
61 }

磁盘IO各个端口作用

IO地址 功能
0x1f0 读数据,当0x1f7不为忙状态时,可以读。
0x1f2 要读写的扇区数,每次读写前,你需要表明你要读写几个扇区。最小是1个扇区
0x1f3 如果是LBA模式,就是LBA参数的0-7位
0x1f4 如果是LBA模式,就是LBA参数的8-15位
0x1f5 如果是LBA模式,就是LBA参数的16-23位
0x1f6 第0~3位:如果是LBA模式就是24-27位 第4位:为0主盘;为1从盘
0x1f7 状态和命令寄存器。操作时先给命令,再读取,如果不是忙状态就从0x1f0端口读数据
  1. 待硬盘空闲。waitdisk的函数实现只有一行:while ((inb(0x1F7) & 0xC0) != 0x40),0xC0=1100 0000 b,0x40=0100 0000b,意思是不断查询读0x1F7寄存器的最高两位,直到最高位为0、次高位为1(这个状态应该意味着磁盘空闲)才返回。
  2. 硬盘空闲后,发出读取扇区的命令。对应的命令字为0x20,放在0x1F7寄存器中;读取的扇区数为1,放在0x1F2寄存器中;读取的扇区起始编号共28位,分成4部分依次放在0x1F3~0x1F6寄存器中。
  3. 发出命令后,再次等待硬盘空闲。
  4. 硬盘再次空闲后,开始从0x1F0寄存器中读数据。注意insl的作用是"That function will read cnt dwords from the input port specified by port into the supplied output array addr.",是以dword即4字节为单位的,因此这里SECTIZE需要除以4

bootblock.asm中的insl函数定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*包含3个输入参数,port代表端口号,addr代表这个扇区存放在主存中的起始地址,cnt则代表读取的次数*/
212 static inline void
213 insl(uint32_t port, void *addr, int cnt) {
214 asm volatile (
215 7cc1: b9 80 00 00 00 mov $0x80,%ecx
216 7cc6: ba f0 01 00 00 mov $0x1f0,%edx
217 7ccb: fc cld
218 7ccc: f2 6d repnz insl (%dx),%es:(%edi)
219 // wait for disk to be ready
220 waitdisk();
221
222 // read a sector
223 insl(0x1F0, dst, SECTSIZE / 4);
224 }

练习4.2

bootloader是如何加载ELF格式的OS?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 85  /* bootmain-引导加载程序的条目*/
86 void
87 bootmain(void) {
88 // 首先读取ELF的头部
89 readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
90
91 // 通过储存在头部的幻数判断是否是合法的ELF文件
92 if (ELFHDR->e_magic != ELF_MAGIC) {
93 goto bad;
94 }
95
96 struct proghdr *ph, *eph;
97
98 // ELF头部有描述ELF文件应加载到内存什么位置的描述表, 先将描述表的头地址存在ph
99 ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
100 eph = ph + ELFHDR->e_phnum;
// 按照描述表将ELF文件中数据载入内存
101 for (; ph < eph; ph ++) {
102 readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
103 }
// ELF文件0x1000位置后面的0xd1ec比特被载入内存0x00100000
// ELF文件0xf000位置后面的0x1d20比特被载入内存0x0010e000
104
105 // 根据ELF头部储存的入口信息,找到内核的入口
106 //注意:不返回 --死循环
107 ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
108
109 bad:
110 outw(0x8A00, 0x8A00);
111 outw(0x8A00, 0x8E00);
112
113 /* do nothing */
114 while (1);
115 }
116

练习5:实现函数调用堆栈跟踪函数

我们需要在lab1中完成kdebug.c中函数print_stackframe的实现,可以通过函数print_stackframe来跟踪函数调用堆栈中记录的返回地址。在如果能够正确实现此函数,可在lab1中执行 “make qemu”后,在qemu模拟器中得到类似如下的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
……
ebp:0x00007b28 eip:0x00100992 args:0x00010094 0x00010094 0x00007b58 0x00100096
kern/debug/kdebug.c:305: print_stackframe+22
ebp:0x00007b38 eip:0x00100c79 args:0x00000000 0x00000000 0x00000000 0x00007ba8
kern/debug/kmonitor.c:125: mon_backtrace+10
ebp:0x00007b58 eip:0x00100096 args:0x00000000 0x00007b80 0xffff0000 0x00007b84
kern/init/init.c:48: grade_backtrace2+33
ebp:0x00007b78 eip:0x001000bf args:0x00000000 0xffff0000 0x00007ba4 0x00000029
kern/init/init.c:53: grade_backtrace1+38
ebp:0x00007b98 eip:0x001000dd args:0x00000000 0x00100000 0xffff0000 0x0000001d
kern/init/init.c:58: grade_backtrace0+23
ebp:0x00007bb8 eip:0x00100102 args:0x0010353c 0x00103520 0x00001308 0x00000000
kern/init/init.c:63: grade_backtrace+34
ebp:0x00007be8 eip:0x00100059 args:0x00000000 0x00000000 0x00000000 0x00007c53
kern/init/init.c:28: kern_init+88
ebp:0x00007bf8 eip:0x00007d73 args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8
<unknow>: -- 0x00007d72 –
……

请完成实验,看看输出是否与上述显示大致一致,并解释最后一行各个数值的含义。

提示:可阅读小节“函数堆栈”,了解编译器如何建立函数调用关系的。在完成lab1编译后,查看lab1/obj/bootblock.asm,了解bootloader源码与机器码的语句和地址等的对应关系;查看lab1/obj/kernel.asm,了解 ucore OS源码与机器码的语句和地址等的对应关系。

要求完成函数kern/debug/kdebug.c::print_stackframe的实现,提交改进后源代码包(可以编译执行),并在实验报告中简要说明实现过程,并写出对上述问题的回答。

当运行make qemu得到如下:

1
2
3
4
5
6
7
8
9
10
moocos-> make qemu
(THU.CST) os is loading ...

Special kernel symbols:
entry 0x00100000 (phys)
etext 0x00103209 (phys)
edata 0x0010da16 (phys)
end 0x0010ed20 (phys)
Kernel executable memory footprint: 60KB
++ setup timer interrupts

bootblock.asm源码

查看kern/debug/kdebug.c::print_stackframe

image-20200311151416667

按照注释来做

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void
print_stackframe(void) {
uint32_t ebp =read_ebp(), eip = read_eip();
int i;
for (i = 0; ebp != 0 && i < STACKFRAME_DEPTH; i ++){
cprintf("ebp:0x%08x eip:0x%08x ", ebp, eip);
uint32_t *args = (uint32_t *)ebp + 2;
cprintf("args: 0x%08x 0x%08x 0x%08x 0x%08x", args[0], args[1], args[2], args[3]);
cprintf("\n");
print_debuginfo(eip - 1);
eip = ((uint32_t *)ebp)[1];
ebp = ((uint32_t *)ebp)[0];
}
}

中cprintf表示控制台(console)输出

重新make qemu后

image-20200311154602152

最后一行是

1
ebp:0x00007bf8 eip:0x00007d68 args: 0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8

共有ebp,eip和args三类参数

函数调用栈

函数开头

1
2
pushl   %ebp
movl %esp , %ebp

函数调用栈

这两条汇编指令的含义是:首先将ebp寄存器入栈,然后将栈顶指针esp赋值给ebp。“mov ebp esp”这条指令表面上看是用esp覆盖ebp原来的值,其实不然。因为给ebp赋值之前,原ebp值已经被压栈(位于栈顶),而新的ebp又恰恰指向栈顶。此时ebp寄存器就已经处于一个非常重要的地位,该寄存器中存储着栈中的一个地址(原ebp入栈后的栈顶),从该地址为基准,向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数局部变量值,而该地址处又存储着上一层函数调用时的ebp值。

一般而言,EBP 基址指针,是保存调用者函数的地址,总是指向函数栈栈底,ESP被调函数的指针,总是指向函数栈栈顶。ss:[ebp+4]处为返回地址,ss:[ebp+8]处为第一个参数值(最后一个入栈的参数值,此处假设其占用4字节内存),ss:[ebp-4]处为第一个局部变量,ss:[ebp]处为上一层ebp值。

image-20200311170344228

最后一行输出的ebp为0x00007bf8,eip为0x00007d68,这是因为bootloader被加载到了0x00007c00地址处,在执行到 [bootasm.S](#bootasm.S 源码) 最后"call bootmain"指令时,首先将返回地址压栈,再将当前ebp压栈,所以此时esp为0x00007bf8。ss:ebp+4指向caller调用时的eip在bootmain函数入口处,有mov %esp %ebp指令,故bootmain中ebp为0x00007bf8。

1
2
289      7cfc:       75 6a                   jne    7d68 <bootmain+0x97>
394 7d68: b8 00 8a ff ff mov $0xffff8a00,%eax

ss:ebp+4指向caller调用时的eip,可以看到其地址。

一般来说,args存放的4个dword是对应4个输入参数的值。但这里比较特殊,由于bootmain函数调用时并没传递任何输入参数,并且栈顶的位置恰好在bootloader第一条指令存放的地址的上面,而args恰好是ebp寄存器指向的栈顶往上第2~5个单元,因此args存放的就是bootloader指令的前16个字节。可以对比obj/bootblock.asm文件来验证(其字节序为小端字节序)

1
2
3
4
5
6
7
8
9
14      7c00:       fa                      cli
16 7c01: fc cld
20 7c02: 31 c0 xor %eax,%eax
22 7c04: 8e d8 mov %eax,%ds
24 7c06: 8e c0 mov %eax,%es
26 7c08: 8e d0 mov %eax,%ss
35 7c0a: e4 64 in $0x64,%al
37 7c0c: a8 02 test $0x2,%al
39 7c0e: 75 fa jne 7c0a <seta20.1>

练习6:完善中断初始化和处理

请完成编码工作和回答如下问题:

  1. 中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
  2. 请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。
  3. 请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。

【注意】除了系统调用中断(T_SYSCALL)使用陷阱门描述符且权限为用户态权限以外,其它中断均使用特权级(DPL)为0的中断门描述符,权限为内核态权限;而ucore的应用程序处于特权级3,需要采用`int 0x80`指令操作(这种方式称为软中断,软件中断,Tra中断,在lab5会碰到)来发出系统调用请求,并要能实现从特权级3到特权级0的转换,所以系统调用中断(T_SYSCALL)所对应的中断门描述符中的特权级(DPL)需要设置为3。

要求完成问题2和问题3 提出的相关函数实现,提交改进后的源代码包(可以编译执行),并在实验报告中简要说明实现过程,并写出对问题1的回答。完成这问题2和3要求的部分代码后,运行整个系统,可以看到大约每1秒会输出一次”100 ticks”,而按下的键也会在屏幕上显示。

练习6.1

中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?

中断向量表一个表项占用8字节,其中2-3字节是段选择子,0-1字节和6-7字节拼成位移,
两者联合便是中断处理程序的入口地址。

练习6.2

kern/trap/trap.c的片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
34  /* idt_init - initialize IDT to each of the entry points in kern/trap/vectors.S */
35 void
36 idt_init(void) {
37 /* LAB1 YOUR CODE : STEP 2 */
38 /* (1) Where are the entry addrs of each Interrupt Service Routine (ISR)?
39 * All ISR's entry addrs are stored in __vectors. where is uintptr_t __vectors[] ?
40 * __vectors[] is in kern/trap/vector.S which is produced by tools/vector.c
41 * (try "make" command in lab1, then you will find vector.S in kern/trap DIR)
42 * You can use "extern uintptr_t __vectors[];" to define this extern variable which will be used later.
43 * (2) Now you should setup the entries of ISR in Interrupt Description Table (IDT).
44 * Can you see idt[256] in this file? Yes, it's IDT! you can use SETGATE macro to setup each item of IDT
45 * (3) After setup the contents of IDT, you will let CPU know where is the IDT by using 'lidt' instruction.
46 * You don't know the meaning of this instruction? just google it! and check the libs/x86.h to know more.
47 * Notice: the argument of lidt is idt_pd. try to find it!
48 */
49 }

kern/mm/mmu.h中的SETGATE宏

1
2
3
4
5
6
7
8
9
10
11
71  #define SETGATE(gate, istrap, sel, off, dpl) {            \
72 (gate).gd_off_15_0 = (uint32_t)(off) & 0xffff; \
73 (gate).gd_ss = (sel); \
74 (gate).gd_args = 0; \
75 (gate).gd_rsv1 = 0; \
76 (gate).gd_type = (istrap) ? STS_TG32 : STS_IG32; \
77 (gate).gd_s = 0; \
78 (gate).gd_dpl = (dpl); \
79 (gate).gd_p = 1; \
80 (gate).gd_off_31_16 = (uint32_t)(off) >> 16; \
81 }

补全函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
extern uintptr_t __vectors[];
int i;
for(i = 0; i < 256; i ++){
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
}
SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER);
lidt(&idt_pd);
/*
传入的第一个参数gate是中断的描述符表
传入的第二个参数istrap用来判断是中断还是trap
传入的第三个参数sel的作用是进行段的选择
传入的第四个参数off表示偏移
传入的第五个参数dpl表示这个中断的优先级
*/

练习6.3

请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。

提示片段

1
2
3
4
5
6
/* 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!
*/

补充如下

1
2
3
ticks++;
if(ticks%TICK_NUM == 0)//每次时钟中断之后ticks就会加一 当加到TICK_NUM次数时 打印并重新开始
print_ticks();//前面有定义 打印字符串

运行make qemu显示如下

image-20200311180818859

扩展练习 Challenge 1

扩展proj4,增加syscall功能,即增加一用户态函数(可执行一特定系统调用:获得时钟计数值),当内核初始完毕后,可从内核态返回到用户态的函数,而用户态的函数又通过系统调用得到内核态的服务。

提示: 规范一下 challenge 的流程。

kern_init 调用 switch_test,该函数如下:

1
2
3
4
5
6
7
8
9
10
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();
}

switchto函数建议通过 中断处理的方式实现。主要要完成的代码是在 trap 里面处理 T_SWITCH_TO* 中断,并设置好返回的状态。

在 lab1 里面完成代码以后,执行 make grade 应该能够评测结果是否正确。

Solution:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
kern/init/init.c
static void lab1_switch_to_user(void) {
//LAB1 CHALLENGE 1 : TODO
/*--------------------------------------------------------
"sub $0x8, %%esp \n"
让 SS 和 ESP 这两个寄存器 有机会 POP 出时 更新 SS 和 ESP
因为 从内核态进入中断 它的特权级没有改变 是不会 push 进 SS 和 ESP的 但是我们又需要通过 POP SS 和 ESP 去修改它们
进入 T_SWITCH_TOU(120) 中断
将原来的栈顶指针还给esp栈底指针
--------------------------------------------------------*/
asm volatile (
"sub $0x8, %%esp \n"
"int %0 \n"
"movl %%ebp, %%esp"
:
: "i"(T_SWITCH_TOU)
);
}
static void lab1_switch_to_kernel(void) {
//LAB1 CHALLENGE 1 : TOD
/*--------------------------------------------------------
进入 T_SWITCH_TOK(121) 中断
将原来的栈顶指针还给esp栈底指针
--------------------------------------------------------*/
asm volatile (
"int %0 \n"
"movl %%ebp, %%esp \n"
:
: "i"(T_SWITCH_TOK)
);
}



kern/trap/trap.c :

/* 定义临时指针 */
struct trapframe switchk2u, *switchu2k;

static void trap_dispatch(struct trapframe *tf)
//通过"改造"一个中断 来进入我们想进入的用户态或者内核态
case T_SWITCH_TOU:
if (tf->tf_cs != USER_CS) {
switchk2u = *tf;
switchk2u.tf_cs = USER_CS;
switchk2u.tf_ds = switchk2u.tf_es = switchk2u.tf_ss = USER_DS;
switchk2u.tf_eflags |= FL_IOPL_MASK; // IOPL 改为 0
switchk2u.tf_esp = (uint32_t)tf + sizeof(struct trapframe) - 8; // tf->esp的位置
// iret 回到用户栈
*((uint32_t *)tf - 1) = (uint32_t)&switchk2u;
}
break;
case T_SWITCH_TOK:
if (tf->tf_cs != KERNEL_CS) {
tf->tf_cs = KERNEL_CS;
tf->tf_ds = tf->tf_es = KERNEL_DS;
tf->tf_eflags &= ~FL_IOPL_MASK;
switchu2k = (struct trapframe *)(tf->tf_esp - (sizeof(struct trapframe) - 8));
memmove(switchu2k, tf, sizeof(struct trapframe) - 8);
*((uint32_t *)tf - 1) = (uint32_t)switchu2k;
}
break;

去掉42行注释

image-20200311193059728

运行make grade

image-20200311194238755

莫名其妙的满分。。。

扩展练习 Challenge 2

用键盘实现用户模式内核模式切换。具体目标是:“键盘输入3时切换到用户模式,键盘输入0时切换到内核模式”。 基本思路是借鉴软中断(syscall功能)的代码,并且把trap.c中软中断处理的设置语句拿过来。

注意:

1.关于调试工具,不建议用lab1_print_cur_status()来显示,要注意到寄存器的值要在中断完成后tranentry.S里面iret结束的时候才写回,所以再trap.c里面不好观察,建议用print_trapframe(tf)

2.关于内联汇编,最开始调试的时候,参数容易出现错误,可能的错误代码如下

1
2
3
4
asm volatile ( "sub $0x8, %%esp \n"
"int %0 \n"
"movl %%ebp, %%esp"
: )

要去掉参数int %0 \n这一行

3.软中断是利用了临时栈来处理的,所以有压栈和出栈的汇编语句。硬件中断本身就在内核态了,直接处理就可以了。

Solution:

补充代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kern/trap/trap.c
// 173行
case IRQ_OFFSET + IRQ_KBD:
c = cons_getc();
cprintf("kbd [%03d] %c\n", c, c);
if (c == '0') {
if (tf->tf_cs != KERNEL_CS) {
tf->tf_cs = KERNEL_CS;
tf->tf_ds = tf->tf_ss = tf->tf_es = KERNEL_DS;
tf->tf_eflags &= ~FL_IOPL_MASK;
}
print_trapframe(tf);
}
if (c == '3') {
if (tf->tf_cs != USER_CS) {
tf->tf_cs = USER_CS;
tf->tf_ds = tf->tf_ss = tf->tf_es = USER_DS;
tf->tf_eflags |= FL_IOPL_MASK;
}
print_trapframe(tf);
}
break;

make qemu

键盘输入3时切换到用户模式

image-20200311203258304

键盘输入0时切换到内核模式

image-20200311203152425