Lab1 实验报告

BUAA 2023 Operating System Lab1

(一)思考题

Thinking 1.1

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

#只编译不链接
gcc -c hello.o hello.c 
mips-linux-gnu-gcc -c hello.c
#链接成可执行文件
gcc -o hello hello.o 
mips-linux-gnu-gcc hello hello.o

接下来分别使用objdump -DS hello.o objdump -DS hello,可以看到第一个指令中的输出中有一个call的指令,但是call的地址为0,如下:

17:   e8 00 00 00 00          call   1c <main+0x1c>

在第二个指令中,call的位置已经被填上了一个确定的地址。使用mips-linux-gnu-gcc编译同理,但是反汇编时需要使用mips-linux-gnu-objdump来解析,因为相当于给定了体系结构,才能做解析,在尝试中发现使用objdump不能反汇编通过mips-linux-gnu-gcc得到的.o或者可执行文件。

objdump(mips-linux-gnu-gcc)

-D --disassemble-all 

-S --source 尽可能反汇编出源代码

Thinking 1.2

思考下述问题:

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

解析mos文件结果如下:

# ./readelf ../../target/mos
0:0x0
1:0x80010000
2:0x80011d40
3:0x80011d58
4:0x80011d70
5:0x0
6:0x0
7:0x0
8:0x0
9:0x0
10:0x0
11:0x0
12:0x0
13:0x0
14:0x0
15:0x0
16:0x0
17:0x0

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

首先,使用readelf -h readelf指令我们可以看见系统工具readelf的ELF头,同时使用readelf -h hello指令查看hello文件的ELF头。通过对比,可以发现这两个文件的开头都有一段以7f 45 4c 46开头的Magic number,但是第五对数字不同,分别为02和01,代表着两个文件分别是64位和32位。

image-20230310221241061

其次,在Makefile中hello使用了-m32编译选项,代表着将hello编译为32为可执行文件,而readelf是64位的,我们编写的readelf是针对32位ELF文件解析的,所以不能解析64位的readelf文件本身

Thinking 1.3

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

载入内存阶段,上电之后,会有Linker Script将各个节引导到预先设定的正确的位置上,段是由节组合而成的,节的地址被调整了,那么最终段的地址也会相应地被调整。此时内核的新入口就已经确定。

执行阶段,会从ENTRY(_start) 来得到程序入口为_start,这样内核就能被正确的跳转到。

(二)难点分析

本次实验的难点首先在于概念的辨析,比如第一题的e_shentsize,代表的是节头表入口大小,也就是节头表每一个表所占的空间大小,然而节头表中的sh_size,代表的是这个节中数据的大小,如果未装入数据,sh_size的大小是0,而e_shentsize是确定且固定不变的

第二个难点是在于printk函数中参数类型的判断,printk函数的第三个参数传入的是一个unsigned long,所以需要提前判断符号,设置neg_flag,将原来的有符号数字转化为它的绝对值形式。

第三个难点在于C语言指针相关的问题,在这次实验中,经常使用void表示泛型,如果将该地址给到另外类型的指针时,需要强制类型转化。*同时对于指针的加操作,每次加1实际内存所增加的字节数是该指针指向对象类型的size大小。

(三)实验体会

  • 在这次实验中我体会到C语言的灵活精妙之处,我们需要更多的学习C语言的使用。
  • 一个简单的printf函数,最终的落脚点还是对于内存空间的写入,我体会到操作系统,从顶层到底层的纵深感。

知识积累

Makefile中的一些常见符号

$@

在bash脚本中,**$@**代表输入的参数,相当于一个参数表

#test.sh
cnt=0
for i in$@do 
    echo “Number of $cnt parametre is: $i(( cnt++ ))
done

如果我们输入命令./test.sh hello world "hello world",回得到以下输出:

Number of 1 parametre is: hello
Number of 2 parametre is: world
Number of 3 parametre is: hello world

然而在Makefile中,**$@**表示规则中的目标文件集。在模式规则中,如果有多个目标,那么,”$@”就是匹配于目标中模式定义的集合,即下面Makefile文件中的target。

$(@D)

**$(@D)**表示”$@“的目录部分(不以斜杠作为结尾) ,如果”**$@”**值是”dir/foo.o”,那么”$(@D)“就是”dir”,而如果”$@”中没有包含斜杠的话,其值就是”.”(当前目录) 。

$(@F)

表示”$@“的文件部分,如果”$@“值是”dir/foo.o”,那么”$(@F)”就是”foo.o”,”$(@F)”相

当于函数”$(notdir $@)”

$^

$^代表所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那个这个变量

会去除重复的依赖目标,只保留一份。所谓依赖项就是Makefile文件中的components。

target: components
	command 1
	command 2
	……
$?

**$?**表示所有比目标新的依赖目标的集合。以空格分隔。即更新后的依赖项。

$<

$<依赖目标中的第一个目标名字。如果依赖目标是以模式(即”%”)定义的,那么**”$<”**将是符合模式的一系列的文件集。注意,其是一个一个取出来的。

示例

下面给出两个例子:

main : main.o  test.o  test1.o  test2.o
	gcc  -o  $@  $^

指令代表把所有的o文件编译生成可执行的main文件,$^代表所以的依赖文件集合(main.o test.o test1.o test2.o),@代表目标文件

%.o : %.c
	gcc  -c  $<  -o  $@

把所以的c文件编译生成对应的o文件,$<代表每次取的c文件,$@代表每次c文件对应的目标文件

补档

Lab1-exam

这次的lab1-exam比较正常,注意变量名不要弄错即可。

Lab1-extra

使用make test lab=1_sprintf && make run测试

题目要求我们完成**sprintf(char *buf, const char *fmt, …)方法,即将我们的输入放到缓冲区中,记住,是放到缓冲区中。这里隐含了有许多个参数需要因此放进去的情况**。

我们可以仿照printk()函数的形式,依次构架sprint()和outputk1()方法,下面是我写的方法,乍一看,好像很对的样子,但是我忽略了一个问题,outputk1()会在vprintk()中多次调用,因为有多个参数,outputk1()的索引i是从从0开始,这样相当于输入一个参数就覆盖上一个参数,可能没有输出

void outputk1(char *data, const char *buf, size_t len) {
  for (int i = 0; i < len; i++) {
    data[i] = buf[i];
  }
    data[len]='\0';
}
int sprintf(char *buf, const char *fmt, ...) {
  va_list ap;
  va_start(ap, fmt);
  vprintfmt(outputk1, buf, fmt, ap);
  va_end(ap);
  return strlen(buf);
}

修改方式也很简单,只要每次将data的指针移到最后一位,即移动到它的’\0’的位置,为下一个可能的参数连接做准备,对于一个新的字符串总是从上一次结果的末尾开始继续添加。其实这个更像是strcat()的实现。

对于buf的初始化一定要有,因为后面会用到strlen(),要求不能为NULL。

void outputk1(char *data, const char *buf, size_t len) {
  	int l=strlen(data);
    for (int i = 0; i < len; i++) {
    data[i+l] = buf[i];
  }
    data[l+len]='\0';
}
int sprintf(char *buf, const char *fmt, ...) {
  *buf=0;
  va_list ap;
  va_start(ap, fmt);
  vprintfmt(outputk1, buf, fmt, ap);
  va_end(ap);
  return strlen(buf);
}
Search by:GoogleBingBaidu