OS-Lab1 实验报告
思考题
Thinking 1.1
在阅读附录中的编译链接详解以及本章内容后,尝试分别使用实验环境中的原生x86 工具链(gcc、ld、readelf、objdump 等)和MIPS 交叉编译工具链(带有mips-linux-gnu- 前缀,如mips-linux-gnu-gcc、mips-linux-gnu-ld),重复其中的编译和解析过程,观察相应的结果,并解释其中向objdump 传入的参数的含义。
编写一个简单的hello.c,只使用printf输出Hello, World,并执行:
gcc -save-temps hello.c -o hello_x86
mips-linux-gnu-gcc -save-temps hello.c -o hello_mips查看不同的汇编文件,可以看到:
; hello_x86-hello.s
.file "hello.c"
.text
.section .rodata
.LC0:
.string "Hello World!"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
leaq .LC0(%rip), %rax
movq %rax, %rdi
movl $0, %eax
call printf@PLT
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:# hello_mips-hello.s
.file 1 "hello.c"
.section .mdebug.abi32
.previous
.nan legacy
.module fp=xx
.module nooddspreg
.module arch=mips32r2
.abicalls
.text
.rdata
.align 2
$LC0:
.ascii "Hello World!\000"
.text
.align 2
.globl main
.set nomips16
.set nomicromips
.ent main
.type main, @function
main:
.frame $fp,32,$31 # vars= 0, regs= 2/0, args= 16, gp= 8
.mask 0xc0000000,-4
.fmask 0x00000000,0
.set noreorder
.set nomacro
addiu $sp,$sp,-32
sw $31,28($sp)
sw $fp,24($sp)
move $fp,$sp
lui $28,%hi(__gnu_local_gp)
addiu $28,$28,%lo(__gnu_local_gp)
.cprestore 16
lui $2,%hi($LC0)
addiu $4,$2,%lo($LC0)
lw $2,%call16(printf)($28)
move $25,$2
.reloc 1f,R_MIPS_JALR,printf
1: jalr $25
nop
lw $28,16($fp)
move $2,$0
move $sp,$fp
lw $31,28($sp)
lw $fp,24($sp)
addiu $sp,$sp,32
jr $31
nop
.set macro
.set reorder
.end main
.size main, .-main
.ident "GCC: (Ubuntu 12.3.0-17ubuntu1) 12.3.0"
.section .note.GNU-stack,"",@progbits可以看到,在x86平台编译得到的汇编为x86汇编,而交叉编译得到的是我们熟悉的mips汇编。
使用readelf观察文件头,可以看到;
$ readelf -h hello_x86
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
入口点地址: 0x1060
程序头起点: 64 (bytes into file)
Start of section headers: 13976 (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
$ readelf -h hello_mips
ELF 头:
Magic: 7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00
类别: ELF32
数据: 2 补码,大端序 (big endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: EXEC (可执行文件)
系统架构: MIPS R3000
版本: 0x1
入口点地址: 0x400520
程序头起点: 52 (bytes into file)
Start of section headers: 67616 (bytes into file)
标志: 0x70001007, noreorder, pic, cpic, o32, mips32r2
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 11
Size of section headers: 40 (bytes)
Number of section headers: 32
Section header string table index: 31观察发现,二者不仅系统架构不同,就连大小端存储、入口点位置均是不同的。这也是不同的架构的区别。(MIPS支持大小端存储,可见我们的实验环境使用大端存储)
使用objdump反汇编并输出section的摘要信息,可以看到:
$ objdump -h hello_x86
hello_x86: 文件格式 elf64-x86-64
节:
Idx Name Size VMA LMA File off Algn
0 .interp 0000001c 0000000000000318 0000000000000318 00000318 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .note.gnu.property 00000030 0000000000000338 0000000000000338 00000338 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .note.gnu.build-id 00000024 0000000000000368 0000000000000368 00000368 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .note.ABI-tag 00000020 000000000000038c 000000000000038c 0000038c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .gnu.hash 00000024 00000000000003b0 00000000000003b0 000003b0 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .dynsym 000000a8 00000000000003d8 00000000000003d8 000003d8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .dynstr 0000008f 0000000000000480 0000000000000480 00000480 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
7 .gnu.version 0000000e 0000000000000510 0000000000000510 00000510 2**1
CONTENTS, ALLOC, LOAD, READONLY, DATA
8 .gnu.version_r 00000030 0000000000000520 0000000000000520 00000520 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
9 .rela.dyn 000000c0 0000000000000550 0000000000000550 00000550 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
10 .rela.plt 00000018 0000000000000610 0000000000000610 00000610 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
11 .init 0000001b 0000000000001000 0000000000001000 00001000 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
12 .plt 00000020 0000000000001020 0000000000001020 00001020 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
13 .plt.got 00000010 0000000000001040 0000000000001040 00001040 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
14 .plt.sec 00000010 0000000000001050 0000000000001050 00001050 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
15 .text 0000010c 0000000000001060 0000000000001060 00001060 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
16 .fini 0000000d 000000000000116c 000000000000116c 0000116c 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
17 .rodata 00000011 0000000000002000 0000000000002000 00002000 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
18 .eh_frame_hdr 00000034 0000000000002014 0000000000002014 00002014 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
19 .eh_frame 000000ac 0000000000002048 0000000000002048 00002048 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
20 .init_array 00000008 0000000000003db8 0000000000003db8 00002db8 2**3
CONTENTS, ALLOC, LOAD, DATA
21 .fini_array 00000008 0000000000003dc0 0000000000003dc0 00002dc0 2**3
CONTENTS, ALLOC, LOAD, DATA
22 .dynamic 000001f0 0000000000003dc8 0000000000003dc8 00002dc8 2**3
CONTENTS, ALLOC, LOAD, DATA
23 .got 00000048 0000000000003fb8 0000000000003fb8 00002fb8 2**3
CONTENTS, ALLOC, LOAD, DATA
24 .data 00000010 0000000000004000 0000000000004000 00003000 2**3
CONTENTS, ALLOC, LOAD, DATA
25 .bss 00000008 0000000000004010 0000000000004010 00003010 2**0
ALLOC
26 .comment 0000002b 0000000000000000 0000000000000000 00003010 2**0
CONTENTS, READONLY
$ mips-linux-gnu-objdump -h hello_mips
hello_mips: 文件格式 elf32-tradbigmips
节:
Idx Name Size VMA LMA File off Algn
0 .interp 0000000d 00400194 00400194 00000194 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .MIPS.abiflags 00000018 004001a8 004001a8 000001a8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA, LINK_ONCE_SAME_SIZE
2 .reginfo 00000018 004001c0 004001c0 000001c0 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA, LINK_ONCE_SAME_SIZE
3 .note.gnu.build-id 00000024 004001d8 004001d8 000001d8 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .note.ABI-tag 00000020 004001fc 004001fc 000001fc 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .dynamic 00000100 0040021c 0040021c 0000021c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .hash 0000003c 0040031c 0040031c 0000031c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
7 .dynsym 000000a0 00400358 00400358 00000358 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
8 .dynstr 000000a8 004003f8 004003f8 000003f8 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
9 .gnu.version 00000014 004004a0 004004a0 000004a0 2**1
CONTENTS, ALLOC, LOAD, READONLY, DATA
10 .gnu.version_r 00000030 004004b4 004004b4 000004b4 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
11 .init 0000003c 004004e4 004004e4 000004e4 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
12 .text 00000190 00400520 00400520 00000520 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
13 .MIPS.stubs 00000030 004006b0 004006b0 000006b0 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
14 .fini 00000024 004006e0 004006e0 000006e0 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
15 .rodata 00000020 00400710 00400710 00000710 2**4
CONTENTS, ALLOC, LOAD, READONLY, DATA
16 .eh_frame 00000004 00400730 00400730 00000730 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
17 .init_array 00000004 0041fff8 0041fff8 0000fff8 2**2
CONTENTS, ALLOC, LOAD, DATA
18 .fini_array 00000004 0041fffc 0041fffc 0000fffc 2**2
CONTENTS, ALLOC, LOAD, DATA
19 .data 00000010 00420000 00420000 00010000 2**4
CONTENTS, ALLOC, LOAD, DATA
20 .rld_map 00000004 00420010 00420010 00010010 2**2
CONTENTS, ALLOC, LOAD, DATA
21 .got 00000020 00420020 00420020 00010020 2**4
CONTENTS, ALLOC, LOAD, DATA
22 .sdata 00000004 00420040 00420040 00010040 2**2
CONTENTS, ALLOC, LOAD, DATA
23 .bss 00000010 00420050 00420050 00010044 2**4
ALLOC
24 .comment 00000026 00000000 00000000 00010044 2**0
CONTENTS, READONLY
25 .pdr 00000020 00000000 00000000 0001006c 2**2
CONTENTS, READONLY
26 .gnu.attributes 00000010 00000000 00000000 0001008c 2**0
CONTENTS, READONLY
27 .mdebug.abi32 00000000 00000000 00000000 0001009c 2**0
CONTENTS, READONLY也就是说,不同的架构对于section的组织方式是不同的。
常见的objdump参数如下:
| 参数 | 全称 | 含义 |
|---|---|---|
| -d | –disassemble | 反汇编 |
| -D | –disassemble-all | 反汇编全部(包括数据段) |
| -s | –source | 汇编与源代码混合显示 |
| -h | –section-headers | 输出各个Section的摘要信息 |
| -x | –all-headers | 显示所有可用的头信息 |
Thinking 1.2
尝试使用我们编写的readelf 程序,解析之前在target 目录下生成的内核ELF 文件。 也许你会发现我们编写的readelf 程序是不能解析readelf 文件本身的,而我们刚才介绍的系统工具readelf 则可以解析,这是为什么呢?(提示:尝试使用readelf-h,并阅读tools/readelf 目录下的Makefile,观察readelf 与hello 的不同)
$ cd tools/readelf && make && ./readelf ../../target/mos
cc -c main.c
cc -c readelf.c
cc main.o readelf.o -o readelf
0:0x0
1:0x80020000
2:0x80022120
3:0x80022138
4:0x80022150
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使用readelf -h ./readelf可以发现,本文件是在x86平台上编译的,其位宽、端序等信息均与mips不同,而我们的readelf是按照mips的标准编写的,因此无法用我们自己的readelf去输出我们自己的readelf文件本身。观察Makefile,可以发现,在编译hello时,加入了-m32(32位编译)这一选项,因此可以推断,决定我们的readelf是否能够解析的核心因素是位宽。阅读代码可以发现,尽管不同平台的e_ident和e_machine等信息均有所不同,但这些我们并未访问,真正对我们有影响的是不同位宽导致的偏移量等变量的大小不同,这与我们的推断一致。
Thinking 1.3
在理论课上我们了解到,MIPS 体系结构上电时,启动入口地址为0xBFC00000(其实启动入口地址是根据具体型号而定的,由硬件逻辑确定,也有可能不是这个地址,但一定是一个确定的地址),但实验操作系统的内核入口并没有放在上电启动地址,而是按照内存布局图放置。思考为什么这样放置内核还能保证内核入口被正确跳转到?(提示:思考实验中启动过程的两阶段分别由谁执行。)
机器上电时,PC会跳到一个出厂固定的地址,这个地址处存放了bootloader,bootloader负责解析内核elf文件头,获取预设的入口地址,并将内核镜像加载到内存的对应位置,并通过指令跳转到该地址。这样的两个阶段保证了入口地址不与硬件出厂设定强绑定,以最大限度保证操作系统的灵活性。
难点分析
理解ELF文件
理解ELF文件,核心在于理解段、节只是对整个文件(除去文件头、段头表、节头表)的全部内容的不同划分方式。理解了这一点,我们就可以理解,在解析elf的过程中,我们需要做的只是根据文件头中的信息定位段头表、节头表,再根据段头表、节头表的信息定位到我们想找的段、节,并对其进行解析。当然,除去定位需要的大小、偏移量等信息,elf.h中还存储了许多其他的信息,比如魔数、架构、位宽、端序等,这使用在系统的readelf的过程中可以理解。
printk的实现
+---------------------------------------------------------+
| printk(fmt, ...) |
| 作用: 初始化 va_list, 准备将参数传递给格式化引擎 |
+------------+--------------------------------------------+
|
v
+---------------------------------------------------------+
| vprintfmt(output_callback, data, fmt, ap) |
| 解析字符串并交给回调函数 |
+------------+--------------------------------------------+
|
| (每产生一个字符就调用一次回调)
v
+---------------------------------------------------------+
| 回调函数: 或者你代码里的 outputk |
| 作用: 接收 vprintfmt 传来的字符,调用底层硬件驱动接口 |
+------------+--------------------------------------------+
|
v
+---------------------------------------------------------+
| 系统函数: printchar |
| 作用: 向屏幕输出 |
+------------+--------------------------------------------+可以看到,printk的核心在于回调函数的使用。printk调用vprintfmt去解析字符串得到最终需要输出的信息,并将其传给printk用于输出。这一设计保证了兼容不同的输出方式的可扩展性(只需写回调函数即可),而无需对整个操作进行重载。
变长参数
C语言在<stdarg.h>中定义了变长参数。va_list是变长参数的类型,va_start(va_list ap, lastarg)用于初始化变长参数列表ap,其中lastarg是最后一个固定参数。va_arg(ap, type)返回下一个类型为type的参数,va_end(ap)用于释放变长参数。可以认为,变长参数就是一个指针,最开始指向最后一个固定参数,每次获取下一个参数时就讲指针向后移动指定类型的长度。
实验体会
本次实验,我深入理解了操作系统的启动流程,掌握了可执行文件elf的大致结构,并对操作系统底层的一些实现方式有了初步了了解,这些也是学习后续课程的重要基础。
原创说明
本报告所有内容均为原创。