本次实验主要实现了MOS操作系统的启动,了解了ELF文件的结构
Thinking 1.1
在阅读附录中的编译链接详解以及本章内容后,尝试分别使用实验环境中的原生 x86 工具链(gcc、ld、readelf、objdump 等)和 MIPS 交叉编译工具链(带有mips-linux-gnu- 前缀,如 mips-linux-gnu-gcc、mips-linux-gnu-ld),重复其中的编译和解析过程,观察相应的结果,并解释其中向objdump传入的参数的含义。
| 参数 | 含义 |
| —- | ———————————————————— |
| -d | 对文件中包含指令的节进行反汇编,将机器码转换为汇编指令,仅处理含实际指令的节(如 .text 节) |
| -D | 反汇编文件中的所有节,不局限于包含指令的节,会对文件里每个节都进行反汇编操作 |
| -S | 尽可能将反汇编代码与源代码混合显示,编译时需用 -g
选项生成调试信息,以便关联汇编指令和对应源代码行 |
| -C | 对 C++ 符号进行名称修饰还原,C++ 编译器为支持函数重载和命名空间等特性会对函数名和类名进行名称修饰,该参数可还原 |
| -l | 将反汇编信息与文件名和行号关联起来。如果文件是使用调试信息编译的,此选项会显示出每个汇编指令对应的源代码文件和行号 |
| -j | 指定要反汇编的节。后面需跟节名,例如 -j .text 表示只反汇编 .text 节 |
git@23373387:~/23373387 (lab1)$ vim hello.c
git@23373387:~/23373387 (lab1)$ cat hello.c
#include <stdio.h>
int main() {
printf("Hello world!\n");
return 0;
}
git@23373387:~/23373387 (lab1)$ gcc -c hello.c
git@23373387:~/23373387 (lab1)$ objdump -DS hello.o
hello.o: 文件格式 elf64-x86-64
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
git@23373387:~/23373387 (lab1)$ gcc hello.c -o hello
git@23373387:~/23373387 (lab1)$ objdump -DS hello
hello: 文件格式 elf64-x86-64
0000000000001149 <main>:
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 48 89 e5 mov %rsp,%rbp
1151: 48 8d 05 ac 0e 00 00 lea 0xeac(%rip),%rax # 2004 <_IO_stdin_used+0x4>
1158: 48 89 c7 mov %rax,%rdi
115b: e8 f0 fe ff ff call 1050 <puts@plt>
1160: b8 00 00 00 00 mov $0x0,%eax
1165: 5d pop %rbp
1166: c3 ret
git@23373387:~/23373387 (lab1)$ mips-linux-gnu-gcc -c hello.c
git@23373387:~/23373387 (lab1)$ mips-linux-gnu-objdump -DS hello.o
hello.o: 文件格式 elf32-tradbigmips
00000000 <main>:
0: 27bdffe0 addiu sp,sp,-32
4: afbf001c sw ra,28(sp)
8: afbe0018 sw s8,24(sp)
c: 03a0f025 move s8,sp
10: 3c1c0000 lui gp,0x0
14: 279c0000 addiu gp,gp,0
18: afbc0010 sw gp,16(sp)
1c: 3c020000 lui v0,0x0
20: 24440000 addiu a0,v0,0
24: 8f820000 lw v0,0(gp)
28: 0040c825 move t9,v0
2c: 0320f809 jalr t9
30: 00000000 nop
34: 8fdc0010 lw gp,16(s8)
38: 00001025 move v0,zero
3c: 03c0e825 move sp,s8
40: 8fbf001c lw ra,28(sp)
44: 8fbe0018 lw s8,24(sp)
48: 27bd0020 addiu sp,sp,32
4c: 03e00008 jr ra
50: 00000000 nop
...
git@23373387:~/23373387 (lab1)$ mips-linux-gnu-gcc hello.c -o hello
git@23373387:~/23373387 (lab1)$ mips-linux-gnu-objdump -DS hello
hello: 文件格式 elf32-tradbigmips
00400650 <main>:
400650: 27bdffe0 addiu sp,sp,-32
400654: afbf001c sw ra,28(sp)
400658: afbe0018 sw s8,24(sp)
40065c: 03a0f025 move s8,sp
400660: 3c1c0043 lui gp,0x43
400664: 279c8010 addiu gp,gp,-32752
400668: afbc0010 sw gp,16(sp)
40066c: 3c020040 lui v0,0x40
400670: 24440720 addiu a0,v0,1824
400674: 8f828024 lw v0,-32732(gp)
400678: 0040c825 move t9,v0
40067c: 0320f809 jalr t9
400680: 00000000 nop
400684: 8fdc0010 lw gp,16(s8)
400688: 00001025 move v0,zero
40068c: 03c0e825 move sp,s8
400690: 8fbf001c lw ra,28(sp)
400694: 8fbe0018 lw s8,24(sp)
400698: 27bd0020 addiu sp,sp,32
40069c: 03e00008 jr ra
4006a0: 00000000 nop
...
Thinking 1.2
思考下述问题:
- 尝试使用我们编写的readelf程序,解析之前在target目录下生成的内核ELF文件。
- 也许你会发现我们编写的readelf程序是不能解析readelf 文件本身的,而我们刚才介绍的系统工具 readelf 则可以解析,这是为什么呢?(提示:尝试使用readelf -h,并阅读tools/readelf 目录下的 Makefile,观察 readelf 与 hello 的不同)
git@23373387:~/23373387/tools/readelf (lab1)$ ./readelf ~/23373387/target/mos
0:0x0
1:0x80020000
2:0x80021950
3:0x80021968
4:0x80021980
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
18:0x0
git@23373387:~/23373387/tools/readelf (lab1)$ ./readelf ./readelf
git@23373387:~/23373387/tools/readelf (lab1)$ readelf -h ~/23373387/target/mos
ELF 头:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
类别: ELF32
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: EXEC (可执行文件)
系统架构: MIPS R3000
版本: 0x1
入口点地址: 0x80021500
程序头起点: 52 (bytes into file)
Start of section headers: 20344 (bytes into file)
标志: 0x50001001, noreorder, o32, mips32
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 4
Size of section headers: 40 (bytes)
Number of section headers: 19
Section header string table index: 18
git@23373387:~/23373387/tools/readelf (lab1)$ 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@23373387:~/23373387/tools/readelf (lab1)$ cat Makefile
%.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 的不同在于,hello文件被编译为32位,而 readelf 程序是64位的。readelf.c 代码中仅使用了 Elf32_Ehdr 和 Elf32_Shdr 数据结构,这意味着它只能处理 32 位的 ELF 文件,所以无法解析自身的主要原因是 32 位与 64 位的 ELF 格式不兼容
Thinking 1.3
在理论课上我们了解到,MIPS体系结构上电时,启动入口地址为0xBFC00000 (其实启动入口地址是根据具体型号而定的,由硬件逻辑确定,也有可能不是这个地址,但 一定是一个确定的地址),但实验操作系统的内核入口并没有放在上电启动地址,而是按照内存布局图放置。思考为什么这样放置内核还能保证内核入口被正确跳转到? (提示:思考实验中启动过程的两阶段分别由谁执行。)
启动流程分为两个阶段:加载内核到内存,跳转到内核的入口
- 由 Linker Script 把内核加载到内存,Linker Script 可以控制各节的加载地址,通过在 kernel.lds 中定义程序各节的生成地址,把各段程序(包括内核)被加载到我们预期的位置
- 与此同时,kernel.lds 定义了 ENTRY(_start) ,把内核入口定为 _start 这个函数。我们通过对 /init/start.S 中 _start 函数的设置,就可以正确的跳转至 mips_init 函数,完成内核入口的跳转
实验难点
-
在本次实验中,需要阅读和理解大量C语言代码,大量带有指针的内容,而且提示都用英文书写,我读代码比较困难,而且整个仓库的内容繁多,补全代码时,代码之间的逻辑关系很复杂,需要仔细分析
-
以往写代码都是在智能的IDE中,而本次实验的代码全程在命令行中进行编程和调试,没有语法和拼写的检查,感觉有些不适应
-
需要深入理解MIPS系统的内存结构,根据各区域的特征,分析内核文件应当被放在什么位置
-
在补全printk函数时,需要处理变长参数列表的情况,当函数参数列表末尾有省略号时,该函数即有变长的参数表,在stdarg.h 头文件中为处理变长参数表定义了一组宏和变量类型如下:
-
va_list,变长参数表的变量类型
-
va_start(va_list ap, lastarg),用于初始化变长参数表的宏 ( lastarg 为该函数最后一个命名的形式参数)
-
va_arg(va_list ap, 类型),用于取变长参数表下一个参数的宏
-
实验感想
-
经过本次实验,使我比较浅显的认识到 MIPS 操作系统的启动的一些步骤,对操作系统代码执行流程有一个大致的基础的了解
-
对于具有复杂逻辑关系的代码,可以使用强大的终端复用工具tmux,分屏查看代码
-
虽然在有详细提示的情况下,补全代码并不太困难,但彻底弄清楚其他的文件、宏定义、函数,了解各个文件、函数之间的层次关系,对于后续的实验是相当重要的