os-lab1实验报告


Lab1实验报告

思考题

Thinking 1.1

请阅读 附录中的编译链接详解,尝试分别使用实验环境中的原生 x86 工具 链(gcc、ld、readelf、objdump 等)和 MIPS 交叉编译工具链(带有 mips-linux-gnu前缀),重复其中的编译和解析过程,观察相应的结果,并解释其中向 objdump 传入的参数 的含义

首先是gcc的几个重要的选项

-E                       Preprocess only; do not compile, assemble or link.      
-S                       Compile only; do not assemble or link.                   
-c                       Compile and assemble, but do not link.

objdump的几个重要的选项

-D, --disassemble-all    Display assembler contents of all sections
	 --disassemble=<sym>  Display assembler contents from <sym>
-S,  --source             Intermix source code with disassembly
	 --source-comment[=<txt>] Prefix lines of source code with <txt>

image-20240327155605072

使用不同的gcc,如使用MIPS交叉编译工具链的gcc,得到的文件类型是不一样的

image-20240327161230861

那么对比具体内容呢,我们分别使用只进行预处理后的文本进行比较

image-20240327161546394

发现不相同的地方还是很多的。

在进行编译后发现得到的文本类型也不相同

image-20240327161721152

那么显然使用diff后区别会非常大,毕竟一个x86,一个mips

这里有意思的一点是

image-20240327162651923

如果我使用x86的objdump去反汇编用交叉编译器得到的文件会报错。

这里我也有点不懂,objdump不是支持多个架构吗,难道说在什么架构确定了objdump对硬件的接口,只是说objdump有不同硬件架构的的接口吗.

因为不支持mips,樂

Thinking 1.2

思考下述问题:

  • 尝试使用我们编写的 readelf 程序,解析之前在 target 目录下生成的内核 ELF 文 件。

  • 也许你会发现我们编写的 readelf 程序是不能解析 readelf 文件本身的,而我们刚 才介绍的系统工具 readelf 则可以解析,这是为什么呢?(提示:尝试使用 readelf -h,并阅读 tools/readelf 目录下的 Makefile,观察 readelf 与 hello 的不同)

image-20240327163822161

关于为什么不能够解析自己,首先我们查看一下readelf和hello的文件头

git@22373058 ~/2/t/readelf (lab2)> readelf -h readelf
ELF 头:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              DYN (Position-Independent Executable file)
  系统架构:                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口点地址:               0x1180
  程序头起点:          64 (bytes into file)
  Start of section headers:          14488 (bytes into file)
  标志:             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         13
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 30
git@22373058 ~/2/t/readelf (lab2)> readelf -h hello
ELF 头:
  Magic:   7f 45 4c 46 01 01 01 03 00 00 00 00 00 00 00 00
  类别:                              ELF32
  数据:                              2 补码,小端序 (little endian)
  Version:                           1 (current)
  OS/ABI:                            UNIX - GNU
  ABI 版本:                          0
  类型:                              EXEC (可执行文件)
  系统架构:                          Intel 80386
  版本:                              0x1
  入口点地址:               0x8049600
  程序头起点:          52 (bytes into file)
  Start of section headers:          746280 (bytes into file)
  标志:             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         8
  Size of section headers:           40 (bytes)
  Number of section headers:         35
  Section header string table index: 34

可以看到readelf的ELF类别是ELF64,而hello的ELF的类别是ELF32。可以知道我们的readelf只能解析ELF32类别的ELF文件,我们可以自己手动得到一个ELF32的myReadelf使得可以解析自身

在Makefile中做如下修改

myReadelf:
	$(CC) -c main.c -m32
	$(CC) -c readelf.c -m32
	$(CC) main.o readelf.o -o $@ -m32 -static -g

发现可以解析自身了~

image-20240327173712186

Thinking 1.3

在理论课上我们了解到,MIPS 体系结构上电时,启动入口地址为 0xBFC00000 (其实启动入口地址是根据具体型号而定的,由硬件逻辑确定,也有可能不是这个地址,但 一定是一个确定的地址),但实验操作系统的内核入口并没有放在上电启动地址,而是按照 内存布局图放置。思考为什么这样放置内核还能保证内核入口被正确跳转到? (提示:思考实验中启动过程的两阶段分别由谁执行。)

操作系统设计人员一般会将硬件初始化的相关工作作为“bootloader”程序放在非易失存储器中,而将操作系统内核放在磁盘中。从操作系统的角度来看,bootloader是为了正确找到内核并加载执行。同时这个启动过程分为两个阶段。

在stage1中,进行初始化硬件设备,同时加载stage2到RAM空间,并且设置堆栈最后跳转到stage2的入口函数。

在stage2中,初始化这一阶段需要用到的硬件设备以及其他功能,然后将内核镜像从存储器读到RAM中,并为内核设置启动参数,最后将CPU指令寄存器的内容设置为内核入口函数的地址,并将控制权转交给系统内核,这时就已经保证了内核地址被正确跳转到。

在我们的qemu中,在stage2阶段:

由 Linker Script (控制加载地址)完成。Linker Script 可以控制各节的加载地址。我们在 kernel.lds 中设置了程序各个生成地址:

SECTIONS {
	. = 0x80010000;
	.text : { *(.text) }
	.data : { *(.data) }
	.bss : { *(.bss) }
	bss_end = .;
	. = 0x80400000;
	end = . ;
}

通过这个控制,生成的程序各个 section 的位置就被调整到了我们所指定的地址上。 segment 是由 section组合而成的,section 的地址被调整了,那么最终 segment 的地址也会相应地被调整。 至此,我们通过 lds 文件控制各段(包括内核)被加载到我们预期的位置。与此同时kernel.lds规定了 ENTRY(_start) ,即把内核入口定为 _start 这个函数。

我们通过对 /init/start.S 中 _start 函数的设置,就可以正确的跳转至 mips_init 函数:

EXPORT(_start)
.set at
.set reorder
	mtc0    zero, CP0_STATUS
	li	sp, 0x80400000
	jal mips_init

难点分析

QEMU模拟器

qemu(Quick Emulator)是一个硬件模拟器,刚开始接触的话我感觉还是会不太懂得这个的运行流程。

通过观察Makefile文件可以看到一些QUME的功劳

QEMU_FLAGS              += -cpu 4Kc -m 64 -nographic -M malta \

可以看到QUME为我们指定了使用的使用的CPU模型为MIPS 4Kc,同时-m 64指定了虚拟机内存为64MB,-nographic指定QEMU以无图形界面模式运行,-M malta指定QEMU使用"MIPS Malta"模拟器模型,这是一种MIPS架构的开发板。

同时与QEMU一起出现的交叉编译也很有意思

比如mips-kubyx-gnu-gccgcc的区别,不同架构的编译器的使用还是很有意思的。

QUME源代码

操作系统内核启动流程

这里主要是指操作系统引导内核启动的过程。

首先计算机的存储部分主要包括ROM和RAM,RAM是易失存储器,刚开始RAM应该是空的,而ROM的内存又是有限的,因此操作系统需要在ROM中找到将内核部分的

这个过程的主要目的是将内核定位并加载到主存中,那么操作系统适配在不同的架构中启动的过程主要包括以下几个流程。

  1. 打开电源,加载BIOS
  2. BIOS进行开机自检,其中启动程序会对计算机的硬件进行初始化,同时会对计算机的主存、磁盘、I/O设备进行检查。
  3. 加载引导程序。BIOS或这UEFI会根据Boot Sequence的内容,同时将控制权交给启动顺序第一位的存储设备(这个如果本机装了多个操作系统比较明显,在我们进入BIOS后可以选择进入哪个操作系统,实际上就是CPU知道有哪几个操作系统,然后选择操作系统并将相应位置的扇区内容进行加载,根据 老师上课说的一切管理都需要数据结构可以推断出,CPU中有某个数据结构专门存储了操作系统类型、扇区地址和扇区大小等数据),然后CPU将存储设备中的引导扇区的内容加载到内存中。
  4. 加载MBR,CPU可以知道在哪个主分区寻找操作系统。
  5. 扫描硬盘分区表,并加载硬盘活动分区。MBR包含活动分区和非活动分区,找到活动分区后,加载活动分区并将控制权交给活动分区。
  6. 加载PBR,读取活动分区的第一个分区(分区引导记录PBR),寻找并激活分区根目录下用于引导操作系统的程序(启动管理器)
  7. 加载启动管理器
  8. 加载操作系统。将操作系统的初始化程序加载到内存中执行。

之前装debian的时候经常会遇到找不到系统的问题,推断是第3步出现了问题,BIOS找不到磁盘中debian的启动分区在哪里。

Linker Script

这里是因为感觉对于Linker Script的语法不够理解

链接器脚本实际上就是是一个文本文件,使用链接器脚本编写一系列的命令,每个命令要么是一个关键字,后面可能跟有参数,要么是对符号的赋值。

  • 一个简单的Linker Script脚本

    实际上大多数的链接器脚本都是相对比较简单的,最简单脚本通常为

    SECTIONS
    {
    . = 0x10000;
    .text : {*(.text)}
    . = 0x80000000;
    .data : {*(.data)}
    .bss :{*(.bss)}
    }

    而其他复杂的语法包括
    一些简单的赋值符号

symbol = expression ;
symbol += expression ;
symbol -= expression ;
symbol *= expression ;
symbol /= expression ;
symbol <<= expression ;
symbol >>= expression ;
symbol &= expression ;
symbol |= expression ;

给出另外一个例子

floating_point = 0;
SECTIONS
{
  .text:
    {
      *(.text)
      .etext = .;
    }
    _bdata = (. + 3) & ~ 4;
    .data : { *(.data) }
}

这里的_bdata是一个符号,它的值是当前位置加3,然后向下取整4的结果。

printk

在本次实验中我们实现了一个简单的printk函数,其中很多关于C语言的一些用法还是很有难度的。

首先是梳理一下代码架构

  • 最外层是printk()函数,其用法与我们通常使用的printf()函数有很大的相似性,可能没有那么智能的正则匹配等功能
  • printk()函数中的主要功能由vprintfmt(outputk, NULL, fmt, ap)函数实现,其中outputk是一个函数指针,fmt是一个字符串,ap是一个va_list类型的变量,这个函数的主要功能是将fmt中的内容输出到串口
  • vprintfmt()中主要部分在解析fmt这个字符串,解析到%后判断其后面的内容,然后直接调用如num = va_arg(ap, int)来得到参数,然后根据读取到的%后的内容将相应的结果进行格式化输出,这里调用了三个主要的函数,分别是print_char(),print_str(),print_num(),这里print_char和print_str实质上就是直接调用out(data,&c,1)或者out(data, s, len),但是由于考虑到左对齐右对齐等需要补充一些空格的情况,因此这里的print_char和print_str函数还是有一些复杂的。而print_num()可以看到是直接将数字转化为字符串,然后调用out(data,buf,len)函数输出。

其中va_list和va_start,va_end这几个的含义也是很难理解的一部分,这里简单的说一下,va_list是一个指向参数的指针,va_start是将这个指针指向第一个参数,va_end是将这个指针指向最后一个参数。

  • va_list 变长参数表的变量类型
  • va_start(va_list, ap ,lastarg)用于初始化边长参数表的宏
  • va_arg(va_list, type)用于获取变长参数表中的参数。这里的类型有很多种,比如int,double,char等
  • va_end(va_list,ap) 结束使用边长参数表的宏

但是由于这个不太了解,还是太难啦!

实验体会

主要是关于printf中的几个细节有了更多的了解。但是我觉得对于其中的va_*那几个函数还是不太能够理解,就是一个变长参数表到底是个什么东西还是不太清楚,这也导致我extra没做出来,哭!

其中对于启动引导的学习让自己对于操作系统有了更深的认识,完成了从计算机组成原理中学习到的底层计算机硬件架构到操作系统的过渡,同时也对于活动分区等这些有了更深的认识,起码对于自己电脑上的多系统的了解更深了一分。

同时还有readelf也让自己对于文件的结构更加了解了,包括认识学习到了之前一直苦恼的一些有关.so文件缺失的报错,对于文件头这个数据结构也懂了很多!

总体而言还是觉得自己需要加强对于操作系统的学习啊!!C语言真是博大精深啊!


文章作者: hugo
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hugo !
  目录