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位。
其次,在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);
}