“操作系统实验-基于uCore OS”实验报告

lab1:启动操作系统

为了这门课做了不知道多少准备工作啊。。。。。感慨一下,OS真不愧是计算机科学的一座可望不可即的高山,而且是越来越高越来越陡,让人望而却步啊。。。

课程报告有些长,我还是分成几部分来,就按照实验指导书那样,练习一,练习二这样来分类:

练习一

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

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

学习思路:阅读下 lab1中makefile的内容,这里的第一问题是说系统镜像文件Ucore.img是怎么生成的?那么按照我的理解,有存在必有起源,既然会生成这个镜像,肯定也就有对应的源头,什么东西生成这个镜像?然后确定了头尾后,再来分析中间过程的具体实现。从实验提供的源码看来,主要就是分析好makefile中的内容。

首先看一下makefile中关于生成Ucore.img的命令:

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

第一行主要是定义变量,用UCOREIMG这个变量表示$(call totarget,ucore.img),为了便于理解就相当于二者等价(暂时忽略:=定义时扩展这个概念,因为自己也搞不明白4种定义变量的方式到底具体差别在哪);

而后主要看主体规则: (UCOREIMG):(UCOREIMG): (kernel) (bootblock)分别是目标:前置条件,所以需要用到两个依赖文件,分别就是(bootblock) 分别是目标:前置条件,所以需要用到两个依赖文件,分别就是(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
copy

这里我们先看(V),里面的Vmakefile文件开头定义了是一个全局变量,V:=@,所以这里的(V),里面的V在makefile文件开头定义了是一个全局变量,V := @,所以这里的(V)等价于(@)也就是代表了@(@)也就是代表了@,@代表的是目标也就是$(UCOREIMG),而后就是三句执行的命令,dd命令在Linux内的作用是用指定大小的块拷贝一个文件,并在拷贝的同时进行指定的转换。具体参数如下:

----------------------------参数------------------------------------------------

1     if=文件名:输入文件名,缺省为标准输入。即指定源文件。


2    ibs=bytes:一次读入bytes个字节,即指定一个块大小为bytes个字节。
          -obs=bytes:一次输出bytes个字节,即指定一个块大小为bytes个字节。
          - bs=bytes:同时设置读入/输出的块大小为bytes个字节。


3     cbs=bytes:一次转换bytes个字节,即指定转换缓冲区大小。

4      skip=blocks:从输入文件开头跳过blocks个块后再开始复制。

5      seek=blocks:从输出文件开头跳过blocks个块后再开始复制。
-注意:通常只用当输出文件是磁盘或磁带时才有效,即备份到磁盘或磁带时才有效。

6   count=blocks:仅拷贝blocks个块,块大小等于ibs指定的字节数。

7   conv=conversion:用指定的参数转换文件。
  - ascii:转换ebcdic为ascii
  - ebcdic:转换ascii为ebcdic
  -ibm:转换ascii为alternateebcdic
  -block:把每一行转换为长度为cbs,不足部分用空格填充
  -unblock:使每一行的长度都为cbs,不足部分用空格填充
  -lcase:把大写字符转换为小写字符
  -ucase:把小写字符转换为大写字符
  -swab:交换输入的每对字节
  -noerror:出错时不停止
  -notrunc:不截短输出文件
  -sync:将每个输入块填充到ibs个字节,不足部分用空(NUL)字符补齐。
copy

也就是相当于:

$(V)dd if=/dev/zero of=$@ count=10000 
就是将/dev/zero下面的块文件拷贝到$@这个地方
也就是$(UCOREIMG)所在的空间,然后指定了拷贝10000个block。
这里要特别说明下/dev/zero代表的意义,/dev/zero,是一个输入设备,你可用它来初始化文件。该设备无穷尽地提供0,可以使用任何你需要的数目——设备提供的要多的多。他可以用于向设备或文件写入字符串0。所以这个指令相当于初始化ucore.img所要用的空间,整个通式相当于
dd if=源文件位置 of=目标文件位置;
copy
$(V)dd if=$(bootblock) of=$@ conv=notrunc 
这条指令相当于将$(bootblack)的内容拷贝到$(UCOREIMG)这里,且选项conv=notrunc我们查询可得知是表示不截短输出文件地输出;
copy
$(V)dd if=$(kernel) of=$@ seek=1 conv=notrunc 
同样,相当于将$(kernel)的内容拷贝到$(UCOREIMG)这里,且选项内容是不截短输出文件地输出以及从输出文件开头跳过1个块后再开始复制。
这里之所以要用到seek=1的原因就是因为前面第二条指令bootblock已经用到了一个块的空间了,为了不覆盖掉其内容,所以要选择跳过这个块区域的内容进行复制。
copy
其余的一条指令$(call create_target,ucore.img)
也就是相当于调用函数,具体后面分析。
而且UCOREIMG:= $(call totarget,ucore.img)
所代表的具体意义也后续讲解;
copy

好了,总算分析完ucore.img的大致生成过程,依赖于两个文件分别是bootblock还有kernel。下面我们继续分析这两个依赖文件的makefile代码:

首先是kernel的代码:

--------------------------include kernel/user----------------------------

INCLUDE    += libs/

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

LIBDIR    += libs

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

这一部分主要应该是包含相应需要用到的头文件,第一条指令代表的是将libs/这个目录下的内容追加到INCLUDE这个变量的内容后面,相当于加上了libs/的内容到INCLUDE中,这个INCLUDE代表了一个变量,在makefile的87行include tools/function.mk 代表了包含的文件,粗略浏览了下function.mk的内容,发现这个makefile用到的多数函数都放在了这个文件之中,具体就不一一介绍了。具体用到的时候再进行展开讲解好了。

接着看第二条指令,addprefix这个函数的内容如下:

$(addprefix <prefix>,<names...> )
copy
名称:加前缀函数——addprefix。
 
功能:把前缀<prefix>加到<names>中的每个单词前面。 

返回:返回加过前缀的文件名序列。 

示例:$(addprefixsrc/,footer)返回值是“src/foosrc/bar”。
copy

所以第二条指令的内容是将-I libs/追加到变量CFLAGS后,也就是直接指定了头文件的路径位置。 这个CFLAGS变量在makefile中是指一个makefile中的隐式规则里会用到的常见预定义变量,是C编译器的选项,具体内容如下:

CFLAGS: 指定头文件(.h文件)的路径,如
CFLAGS=-I/usr/include -I/path/include。

同样地,安装一个包时会在安装路径下建立一个include目录,当安装过程中出现问题时,试着把以前安装的包的include目录加入到该变量中来。
copy

第三条就是直接追求到变量后,我们主要看看下面那个函数调用,此处得介绍一下关于call内置函数的内容:

call 函数是唯一一个可以用来创建新的参数化的函数。你可以写一个非常复杂的表达式,这个表达式中,你可以定义许多参数,然后你可以用call函数来向这个表达式传递参 数。其语法是:

$(call <expression>,<parm1>,<parm2>,<parm3>...)
copy

当make执行这个函数时,参数中的变量,如(1)(1),(2),$(3)等,会被参数,,依次取代。而的返回值就是call函数的返回值。

在此处的这个call是一个嵌套函数,内部有一个call外部再加一个call
内部的call主要完成的事是将LIBDIR(即libs)和libs传递进listf_cc这个表达式

而外部的call则将listf_cc传递给add_files_cc

通过整个makefile的查询可以得到listf_cc = $(call listf,$(1),$(CTYPE))

所以遇到call可以这么理解相当于把后面的值传递给前面的表达式,这里最重要的就是涉及到了一个在function.mk中定义的变量

listf = $(filter $(if $(2),$(addprefix %.,$(2)),%),\$(wildcard $(addsuffix $(SLASH)*,$(1))))

所以这里的双层嵌套相当于将libs传递给这个flist。
copy

总结:这部分的功能主要是将相关的内核需要调用到的头文件都包含进来。主要就是在libs的目录里面用到的库函数文件。


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

这部分的KINCLUDE和KSRCDIR两个变量的作用就是将kern目录也就是内核文件添加进makefile工程中

而KCFLAGS的作用是将-I选项加到KINCLUDE的所有目录下

关于-I选项的作用:

-I DIR
当包含其他 makefile 文件时,可利用该选项指定搜索目录,也就是指定目录下(如tmp)的makefile(或者其他名字)

在当前Makefile中要有这样一句:include makefile

然后makefile -I tmp时就会在在tmp下找Makefile并把里边的内容添加到当前目录下的Makefile中

简单点理解就是如果其他目录下有相应的makefile文件,那这个-I相当于把多个目录中的makefile文件的内容全部串联起来使用。
copy

而后嵌套调用call,将这些内核文件全部加到工程中。

而KOBJS的作用是将libs的内容追加到read_packet这个变量中

这个变量通过function.mk可以查到它的源头关系文件

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

foreach类似于for循环,最终的作用是在kernel libs这个目录中的所有文件的前面都加上一个gcc编译的选项__objs_

总结后个人理解这个指令的作用是目标文件依次通过foreach传输到工程中。

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

这部分就是比较容易看的makefile语法了,作用就是生成kernel对象

第一条定义了kernel变量是call了一个kernel到totarget中

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

也就是把kernel追加给totarget

紧接着是kernel的依赖文件tools/kernel.ld以及KOBJS变量的内容

之后是规则内容了:

首先是第一条

@echo + ld $@

等价于@echo + ld $(kernel)
copy

相当于将kernel所指的内容做链接。

其次是第二条等价于

@ld  -m $(shell $(LD) -V | grep elf_i386 2>/dev/null) -nostdlib -T tools/kernel.ld -o tools/kernel.ld $(KOBJS)
copy

作用其实就是在shell下面执行gcc的相关链接操作以及生成相关的目标文件,具体的相关参数就不一一列举了。反正大概就是这个意思。

后面的指令是对生成的目标文件进行汇编操作

这里主要看create_target这个在function.mk中定义的函数

可以具体参考下function.mk的内容

其主要内容是完成相关的gcc -o编译。这里附上其中用到的eval函数的内容:

函数原型 $(eval text)
copy

它的意思是 text 的内容将作为makefile的一部分而被make解析和执行。

比如这样一个makefile:

$(eval xd:xd.c a.c)

将会产生一个这样的编译

cc   xd.c a.c -o xd
copy

到此处,我们就大致分析完了kernel的生成过程

总结一下,基本这个kernel的生成依赖文件主要是tools/kernel.ld这个链接文件,前两部分主要是一些变量的定义和引用,具体可以看上面的内容,估计是有些地方可能有误解,希望有大神帮我指点指点。

接下来我们继续分析另一个重要的依赖文件bootblock:

-----------------create bootblack--------------------

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,bootblack)


-----------------bootblock---------------------
copy

前面三条指令主要是变量的相关定义,bootfiles主要调用了两个函数关系,实现了将boot目录下的文件加上了.c或者.S的后缀

第二条makefile的功能是将boot下的文件与GCC的编译模版规则进行比对,查看到底是否有语法错误

而第三条是将bin 这个前缀加到boot目录文件前参与编译工作。

接着是主要的部分,bootblock的依赖文件主要这里分成了两个,两个之一都可以满足条件

前一个的作用是调用了toobj函数,实现了在.o文件前加obj 前缀的功能

而第二个是在依赖文件sign前加bin。所以此处还应该涉及到另一个bootblock的依赖文件sign,下面会一并分析。

生成规则的第一条是输出链接文件,第二条是准备将处理后的bootblock加载到内存地址0x7c00处开始运行

这里就是负责加载bootloader的功能部分了,接下去的指令分别代表了汇编和链接功能生成可执行的二进制执行文件

而后调用了create_target函数生成了bootblock。按照操作系统的原理解释,这个bootblock的大小应该是小于512byte的。

接下来顺便分析一下sign这个依赖文件的makefile:

$(call add_files_host,tools/sign.c,sign,sign)
$(call create_target_host,sign,sign)
copy

主要只有两行,简单解释下它们的功能

第一条主要是将tools/sign下的文件加载到makefile工程中,主要用到了一系列在function.mk中定义的函数,具体可以查看

而第二条指令是调用了create_target_host函数:

create_target_host = $(call create_target,$(1),$(2),$(3),$(HOSTCC),$(HOSTCFLAGS))
copy

这里的HOSTCC还有HOSTCFLAGS分别是GCC编译器的相关选项clang还有-g -Wall -O2,嵌套调用了create_target并且加上了这两个选项,具体可以参考GNU手册内容。

至此,大致分析完了ucore.img的生成过程,这其中还有很多很多需要补充的

但是说实在的,独自一个人实在是分析makefile很够呛

所以希望有高手指点指点,如果有不对的地方或者出错的步骤,请指点下本人,感激不尽!!(PS:学makefile的语法真是够累!!!这种语法比汇编还恶心!)

接着是简要分析下其他部分的makefile的内容

首先看第一部分也就是ifndef GCCPREFIX开始的,这个部分主要实现的是检测GCC的版本是否提供了i386-elf-gcc的编译功能

其中用到了if...elif....then的makefile语法,具体可以看百度解释。

第二部分的作用也差不多,主要是检测qemu-system-i386等的模拟器功能是否正常提供,若是无法正常提供则输出报错信息。

define compiler and flags
copy

这部分是定义相关的变量,具体就不一一展开了。对照makefile的语法是可以很好理解的。

for cc和for host相当于是声明了相关的函数调用,主要看include tools/function.mk这个,这条指令相当于c的include头文件。

files for grade script
copy

这部分的指令主要涉及了一些make的其他操作,比如clean,还有qemu和debug等功能,具体也不一一展开了。

之后就是第二个问题

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

关于这个问题,个人理解符合规范的意思应该是指首先硬盘需要有指定的只读扇区的内容

比如一般来说我们是把硬盘上的第一个扇区512byte大小的内容全部拷贝到内存中0x7c00这个地址去运行,所以这个bootloader的大小就应该小于512byte

其次主引导扇区内的内容一般主要是结合BIOS完成系统初始化的功能,主要是涉及到一些环境配置以及初始化硬件设备

建立系统的内存空间映射图,从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核准备好正确的环境

最终引导加载程序把操作系统内核映像加载到RAM中,并将系统控制权传递给它

所以其实有一个疑问,那么这个引导扇区的内容岂不是很容易被修改?然后对系统进行随意的篡改和破坏?毕竟在此之前OS还是没有加载的

这个问题需要去了解下,还是说这个bootloader一般是只读的?而且它完成的具体功能具体是什么?这个需要往后结合分析bootasm.S和bootmain.c研究研究。

注释:通常引导扇区的内容在os启动保护模式后,由于段页式的保护机制,这时候这部分引导扇区的内容是禁止访问的了

可以根据Linux的bootsect.s以及setup.s还有head.s这几个模板进行一些理解,这时候我们发现

整个过程中的确如果能够在内核加载启动保护机制之前,这引导扇区的代码确实是可以进行修改的(处于实模式下)

但是问题是如果进行了修改,那么os启动后也会检测到异常的存在,甚至会崩溃。具体需要进一步了解代码
copy

练习二

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

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

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

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

先按部就班,看1的要求,这里提到说要从CPU加电执行的第一条指令开始,单步跟踪BIOS的执行

那么在i386中这个第一条指令的地址应该就是0xFFFFFFF0H,所以应该使用QEMU的GDB

根据教程的步骤,截图中可以看到确实断点设置在了kern_init函数的入口处,由于要用到GDB的调试工具,这里给出一些常用的操作指令和说明:

GDB:
一般会用到的Debug命令:
$ gcc –g gdb.c -o testgdb
使用gdb调试:
$ gdb testgdb <---------- 启动gdb
.......此处省略一万行

键入 l命令相当于list命令,从第一行开始列出源码:
$ gdb testgdb
.......此处省略一万行

(gdb) l

(gdb) break 16 <-------------------- 设置断点,在源程序第16行处。
Breakpoint 1 at 0x804836a: file test.c, line 16.
(gdb) break func <-------------------- 设置断点,在函数func()入口处。
Breakpoint 2 at 0x804832e: file test.c, line 5.
(gdb) info break <-------------------- 查看断点信息。
Num Type           Disp Enb Address    What
1   breakpoint     keep y   0x0804836a in main at test.c:16
2   breakpoint     keep y   0x0804832e in func at test.c:5
(gdb) r <--------------------- 运行程序,run命令简写
Starting program: /home/shiyanlou/testgdb

Breakpoint 1, main () at test.c:16 <---------- 在断点处停住。

(gdb) n <--------------------- 单条语句执行,next命令简写。

(gdb) c     <--------------------- 继续运行程序,continue命令简写。

(gdb) p I    <—————————— 打印变量i的值,print命令简写。

(gdb) bt     <—————————— 查看函数堆栈。

(gdb) finish <—————————— 退出函数。

(gdb) c <—————————— 继续运行。


Program exited with code 027. <————程序退出,调试结束。
(gdb) q     <—————————— 退出gdb。
copy

执行n后可以看到

之后我们在0x7c00这里进行break

未完待续...........

最新评论
暂无评论~