Lab1 实验报告
Thinking
Thinking 1.1
请阅读 附录中的编译链接详解,尝试分别使用实验环境中的原生 x86 工具链(gcc、 ld、 readelf、 objdump 等)和 MIPS 交叉编译工具链(带有 mips-linux-gnu-前缀),重复其中的编译和解析过程,观察相应的结果,并解释其中向 objdump 传入的参数的含义。
创建 C 源文件
{.line-numbers}1 2 3 4 5 6
| #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
|
{.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
|
{.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-endian
ELF文件的解析程序(32位这一点从readelf.c
中频繁出现的32中也可以看到),故hello
文件可以解析。而我们编写的readelf
解析程序本身则是64位的小端序ELF文件,故其不能解析自身。从Makefile
文件中也可以看到,readelf
默认生成了64位文件,而hello
通过-m32
选项生成了32位文件。
Thinking 1.3
在理论课上我们了解到, MIPS 体系结构上电时,启动入口地址为 0xBFC00000(其实启动入口地址是根据具体型号而定的,由硬件逻辑确定,也有可能不是这个地址,但一定是一个确定的地址),但实验操作系统的内核入口并没有放在上电启动地址,而是按照内存布局图放置。思考为什么这样放置内核还能保证内核入口被正确跳转到?(提示:思考实验中启动过程的两阶段分别由谁执行。)
计算机上电后,由bootloader
开始启动过程。
在stage1
,运行在ROM
或FLASH
中的程序完成硬件的初始化,并为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
|
.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; p_char+=1;
|
因此,需要注意计算节头表地址时,应当加上(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
来输出负号。
心得体会
本次实验难度并不大,也没有很高的工作量,只要跟着指导书一步步来,都可以轻松完成。但如果仅仅以完成任务为导向来进行这次实验,显然是学不到什么有效的东西。在按照提示补全代码之外,更需要大量地阅读相关源码,了解本次实验的全流程,多追问几个为什么,才能真正意义上懂得操作系统的相关内容。