Contents

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的大致结构,并对操作系统底层的一些实现方式有了初步了了解,这些也是学习后续课程的重要基础。

原创说明

本报告所有内容均为原创。