OS-Lab1

Foolish-Han Lv2

Lab1 实验报告

Thinking

Thinking 1.1

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

创建 C 源文件

{.line-numbers}
1
2
3
4
5
6
// hello.c
#include<stdio.h>
int main(){
printf("Hello,world!\n");
return 0;
}

只进行预处理

1
gcc -o pre_hello -E hello.c

只编译不链接,获得hello.o的反汇编文件

1
2
gcc -c hello.c
objdump -DS hello.o > disass_hello_o
{.line-numbers}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

hello.o: 文件格式 elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # f <main+0xf>
f: 48 89 c7 mov %rax,%rdi
12: e8 00 00 00 00 call 17 <main+0x17>
17: b8 00 00 00 00 mov $0x0,%eax
1c: 5d pop %rbp
1d: c3 ret
...

正常编译,获得可执行文件hello的反汇编文件

1
2
gcc -o hello hello.c
objdump -DS > disass_hello
{.line-numbers}
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
...
Disassembly of section .plt:

0000000000001020 <.plt>:
1020: ff 35 9a 2f 00 00 push 0x2f9a(%rip) # 3fc0 <_GLOBAL_OFFSET_TABLE_+0x8>
1026: f2 ff 25 9b 2f 00 00 bnd jmp *0x2f9b(%rip) # 3fc8 <_GLOBAL_OFFSET_TABLE_+0x10>
102d: 0f 1f 00 nopl (%rax)
1030: f3 0f 1e fa endbr64
1034: 68 00 00 00 00 push $0x0
1039: f2 e9 e1 ff ff ff bnd jmp 1020 <_init+0x20>
103f: 90 nop

Disassembly of section .plt.got:

0000000000001040 <__cxa_finalize@plt>:
1040: f3 0f 1e fa endbr64
1044: f2 ff 25 ad 2f 00 00 bnd jmp *0x2fad(%rip) # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
104b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)

Disassembly of section .plt.sec:

0000000000001050 <puts@plt>:
1050: f3 0f 1e fa endbr64
1054: f2 ff 25 75 2f 00 00 bnd jmp *0x2f75(%rip) # 3fd0 <puts@GLIBC_2.2.5>
105b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)

Disassembly of section .text:

0000000000001060 <_start>:
1060: f3 0f 1e fa endbr64
1064: 31 ed xor %ebp,%ebp
1066: 49 89 d1 mov %rdx,%r9
1069: 5e pop %rsi
106a: 48 89 e2 mov %rsp,%rdx
106d: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
1071: 50 push %rax
1072: 54 push %rsp
1073: 45 31 c0 xor %r8d,%r8d
1076: 31 c9 xor %ecx,%ecx
1078: 48 8d 3d ca 00 00 00 lea 0xca(%rip),%rdi # 1149 <main>
107f: ff 15 53 2f 00 00 call *0x2f53(%rip) # 3fd8 <__libc_start_main@GLIBC_2.34>
1085: f4 hlt
1086: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1)
108d: 00 00 00
...

objdump

1
2
3
4
5
6
7
8
NAME
objdump - display information from object files

SYNOPSIS
objdump [-D|--disassemble-all]
[-S|--source]
-D: 反汇编所有 section
-S: 尽可能反汇编出源代码

Thinking 1.2

思考下述问题:

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

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

解析mos文件

./readelf ~/22371236/target/mos

{.line-numbers}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0x0
0x80020000
0x800218d0
0x800218e8
0x80021900
0x0
0x0
0x0
0x0
0x0
0x0
0x0
0x0
0x0
0x0
0x0
0x0
1
readelf -h hello
{.line-numebrs}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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: 746252 (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
1
readelf -h readelf
{.line-numbers}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
{.line-numbers}
1
2
3
4
5
6
7
8
9
10
11
12
13
%.o: %.c
$(CC) -c $<

.PHONY: clean

readelf: main.o readelf.o
$(CC) $^ -o $@

hello: hello.c
$(CC) $^ -o $@ -m32 -static -g

clean:
rm -f *.o readelf hello

通过系统工具readelf可以看到,hello是32位的小端序ELF文件。而我们编写的正是32-bit little-endianELF文件的解析程序(32位这一点从readelf.c中频繁出现的32中也可以看到),故hello文件可以解析。而我们编写的readelf解析程序本身则是64位的小端序ELF文件,故其不能解析自身。从Makefile文件中也可以看到,readelf默认生成了64位文件,而hello通过-m32选项生成了32位文件。

Thinking 1.3

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

计算机上电后,由bootloader开始启动过程。

stage1,运行在ROMFLASH中的程序完成硬件的初始化,并为stage2初始化RAM,将stage2的程序载入到RAM中,并设置堆栈跳转到stage2的入口。

stage2,运行在RAM中的程序继续初始化本阶段所需的硬件设备,载入内核和根文件系统,设置内核的启动参数,最后跳转到内核入口。

我们的实验在QEMU中完成,QEMU 支持加载 ELF 格式内核,所以启动流程被简化为加载内核到内存,之后跳转到内核的入口,启动就完成了。

{.line-numbers}
1
2
3
4
5
6
7
8
9
10
11
12
OUTPUT_ARCH(mips)
ENTRY(_start)
SECTIONS {
. = 0x80020000;
.text : { *(.text) }
.data : { *(.data) }
bss_start = .;
.bss : { *(.bss) }
bss_end = .;
. = 0x80400000;
end = . ;
}

我们在kernel.lds中,设置了各节加载到指定的位置,通过ENTRY(_start)设置程序入口为_start

{.line-numbers}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <asm/asm.h>
#include <mmu.h>

.text
EXPORT(_start)
.set at
.set reorder
la v0, bss_start
la v1, bss_end
clear_bss_loop:
beq v0, v1, clear_bss_done
sb zero, 0(v0)
addiu v0, v0, 1
j clear_bss_loop
clear_bss_done:
mtc0 zero, CP0_STATUS
lui sp, 0x8040
jal mips_init

同时,我们在start.S中,设置了_start函数跳转到内核启动入口的位置。

难点分析

Exercise 1.1

C语言指针的偏移量单位与指针的类型有关

{.line-numbers}
1
2
3
4
int p_int[]={1,2,3,4};
char p_char[]={'a','b','c','d'};
p_int+=1;//偏移四个字节(int 大小),指向2
p_char+=1;//偏移一个字节(char 大小),指向'b'

因此,需要注意计算节头表地址时,应当加上(void*),避免指针以ELF文件的大小为单位进行移动。

sh_table=(void*)ehdr+ehdr->e_shoff;

Exercise 1.2

“.”是一个特殊符号,用来做定位计数器,它根据输出节的大小增长。在 SECTIONS
命令开始的时候,它的值为 0。通过设置“.”即可设置接下来的节的起始地址。“*”是一个通
配符,匹配所有的相应的节。

Exercise 1.3

include/mmu.h中找到栈空间的位置,设置好栈指针,跳转到mips_init即可。

Exercise 1.4

需要注意的是,print_num函数中把输入的数字都当作非负数处理。因此,在输出负数时,需要传入其绝对值,同时设置neg_flag来输出负号。

心得体会

本次实验难度并不大,也没有很高的工作量,只要跟着指导书一步步来,都可以轻松完成。但如果仅仅以完成任务为导向来进行这次实验,显然是学不到什么有效的东西。在按照提示补全代码之外,更需要大量地阅读相关源码,了解本次实验的全流程,多追问几个为什么,才能真正意义上懂得操作系统的相关内容。