MOSPI-ChCore lab (1)

由于👴觉得👴学校的操作系统讲了个🔨,慕名而来学习上交的 MOSPI 课程。银杏书看完之后👴发现👴学校的OS确实讲了个🔨。我直接当场来一段圣经吟唱:

那个额西电操作系统嗷,不会写教材可以不写,害特么在弄你那个管程,来我教你啊,看好了啊。首先 M.A.L.H. 原则,看懂了吗,然后开讲虚拟内存,哎我就不虚拟,我就讲那个空闲链表。哎,再扎个多线程,看到没,线程上下文切换了。我特么直接三段系统调度(短期,中期,长期),然后我直接~就一个多核调度,我就调度到IPC,进程现在已经可以通信了啊!别怪我没有教好你,进程通信了之后干什么,憋特么讲你那破几把处理机了。看好啊,讲出锁(嬉皮笑脸),讲出信号量直接就扔到互斥资源身上,就疯狂的进入他的临界区。然后我再一个,文件系统!加三段系统虚拟化(CPU虚拟化、内存虚拟化、IO虚拟化),全部吃满,完成强杀,你唛璧你懂个der,讲寄吧OS,我爱你。

圣经原文:拖更云的鹰佐教学


本系列为 ChCore lab 实验报告。 Lab源码:https://gitee.com/ipads-lab/chcore-lab MOSPI在线网站:https://ipads.se.sjtu.edu.cn/mospi/

实验环境

需要docker和qemu,docker不赘述。linux下安装qemu: sudo apt-get install qemu-system-arm 安装完成之后查看版本号:

1
2
3
$ qemu-system-aarch64 --version
QEMU emulator version 4.2.0
Copyright (c) 2003-2019 Fabrice Bellard and the QEMU Project developers

5个实验在源码仓库分别以5个分支存在。 git clone -b即可。

内核构建和调试:

  • 用docker交叉编译内核:make build
  • 启动qemu:make qemu
    • 这里遇到报错: Unable to init server: Could not connect: Connection refused gtk initialization failed
    • 解决方法:修改 Makefile ,在QEMUOPTS参数后加-nographic
  • 启动qemu:make qemu-gdb
    • 将监听1234端口以供gdb远程调用
    • 退出:ctrl+a,然后按x。
    • 如果意外退出,要杀死进程:kill $(ps -ef | grep qemu | grep 1234 | awk '{print $2}')
  • 在另一个终端启动gdb调试:make gdb
    • 这里可能需要安装gdb-multiarch:sudo apt-get install gdb-multiarch

可以看到,本项目中 Makefile 主要是封装了一些命令。


Lab1

练习3-加载入口定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
root@lastyear:~/chcore-lab# readelf -S build/kernel.img
There are 9 section headers, starting at offset 0x20cd8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] init              PROGBITS         0000000000080000  00010000
       000000000000b5b0  0000000000000008 WAX       0     0     4096
  [ 2] .text             PROGBITS         ffffff000008c000  0001c000
       00000000000011dc  0000000000000000  AX       0     0     8
  [ 3] .rodata           PROGBITS         ffffff0000090000  00020000
       00000000000000f8  0000000000000001 AMS       0     0     8
  [ 4] .bss              NOBITS           ffffff0000090100  000200f8
       0000000000008000  0000000000000000  WA       0     0     16
  [ 5] .comment          PROGBITS         0000000000000000  000200f8
       0000000000000032  0000000000000001  MS       0     0     1
  [ 6] .symtab           SYMTAB           0000000000000000  00020130
       0000000000000858  0000000000000018           7    46     8
  [ 7] .strtab           STRTAB           0000000000000000  00020988
       000000000000030f  0000000000000000           0     0     1
  [ 8] .shstrtab         STRTAB           0000000000000000  00020c97
       000000000000003c  0000000000000000           0     0     1

看到init段的起始地址是0x80000,和readelf -h中的 Entry point address 一致,也和 GDB 刚进入时where的输出一致。

1
2
3
0x0000000000080000 in ?? ()
(gdb) where
#0  0x0000000000080000 in _start ()

下面寻找_start的定义,在CMakeLists.txt中找到_start,

1
2
3
4
5
6
7
set_property(
    TARGET kernel.img
    APPEND_STRING
    PROPERTY
        LINK_FLAGS
        "-T ${CMAKE_CURRENT_BINARY_DIR}/${link_script} -e _start"
)

这里为kernel.img指定了链接器脚本(-T)和入口函数(-e)。

于是跟随link_script:

1
2
set(link_script "linker.lds")
configure_file("./scripts/linker-aarch64.lds.in" "linker.lds.S")

进入脚本linker-aarch64.lds.in:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include "../boot/image.h"

SECTIONS
{
    . = TEXT_OFFSET;
    img_start = .;
    init : {
        ${init_object}
    }
// ...

其中init段指定了加载init_object,它表示bootloader的所有目标文件集合。其定义回到CmakeLists.txt:

1
2
3
4
5
6
7
set(init_object
        "${BINARY_KERNEL_IMG_PATH}/${BOOTLOADER_PATH}/start.S.o
        ${BINARY_KERNEL_IMG_PATH}/${BOOTLOADER_PATH}/mmu.c.o
        ${BINARY_KERNEL_IMG_PATH}/${BOOTLOADER_PATH}/tools.S.o
        ${BINARY_KERNEL_IMG_PATH}/${BOOTLOADER_PATH}/init_c.c.o
        ${BINARY_KERNEL_IMG_PATH}/${BOOTLOADER_PATH}/uart.c.o"
    )

可发现/boot/start.S定义了_start。

下面继续寻找地址,在链接器脚本引用了image.h,其中有TEXT_OFFSET的定义:

1
2
3
4
5
6
7
#pragma once

#define SZ_16K                  0x4000
#define SZ_64K                  0x10000

#define KERNEL_VADDR            0xffffff0000000000
#define TEXT_OFFSET             0x80000

一切终于串起来了:

  • CMakeLists.txt:是CMake的脚本文件。 CMake 是跨平台的C/C++建构工具。
    • 作用:
      • 指定源文件集合init_object
      • 定义链接器脚本link_script
      • 指定入口函数_start并指定链接器脚本
      • 最终生成kernel.img
    • //最近看到的挺好的CMake教程:https://www.bilibili.com/video/BV1rR4y1E7n9
  • linker-aarch64.lds.in:lds是链接器脚本文件,负责控制输出的ELF文件的细节。
    • 作用:指定了起始地址0x80000

练习3-多处理器挂起

start.S中注释的很明白了,通过检查mpidr_el1寄存器来判断 cpuid ,如果不是0则进入死循环。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
BEGIN_FUNC(_start)
        mrs     x8, mpidr_el1
        and     x8, x8, #0xFF
        cbz     x8, primary
  /* hang all secondary processors before we intorduce multi-processors */
secondary_hang:
        bl secondary_hang
primary:
        /* Turn to el1 from other exception levels. */
        bl      arm64_elX_to_el1
        /* Prepare stack pointer and jump to C. */
        adr     x0, boot_cpu_stack
        add     x0, x0, #0x1000
        mov     sp, x0
        bl      init_c
        /* Should never be here */
        b       .
END_FUNC(_start)

练习4-LMA和VMA

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
root@lastyear:~/chcore-lab# objdump -h build/kernel.img

build/kernel.img:     file format elf64-little

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 init          0000b5b0  0000000000080000  0000000000080000  00010000  2**12
                  CONTENTS, ALLOC, LOAD, CODE
  1 .text         000011dc  ffffff000008c000  000000000008c000  0001c000  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  2 .rodata       000000f8  ffffff0000090000  0000000000090000  00020000  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .bss          00008000  ffffff0000090100  0000000000090100  000200f8  2**4
                  ALLOC
  4 .comment      00000032  0000000000000000  0000000000000000  000200f8  2**0
                  CONTENTS, READONLY

可以发现只有init段的VMA和LMA相同。其赋值还是回到lds脚本:

 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
SECTIONS
{
    . = TEXT_OFFSET;
    img_start = .;
    init : { //init段VMA==VMA
        ${init_object}
    }
    . = ALIGN(SZ_16K); // 对齐16k
    init_end = ABSOLUTE(.); // init段结束
    // KERNEL_VADDR在image.h定义为0xffffff0000000000
    .text KERNEL_VADDR + init_end : AT(init_end) { // AT指定LMA
        *(.text*)
    } // .text段:VMA = KERNEL_VADDR + init_end; LMA = init_end
    // 后面的段,全部按顺序对齐并递增,此时VMA和LMA已经不同,故后面的段也全都不同
    . = ALIGN(SZ_64K);
    .data : {
        *(.data*)
    }
    . = ALIGN(SZ_64K);
    .rodata : {
        *(.rodata*)
    }
    _edata = . - KERNEL_VADDR; // 这些外部变量指的是LMA,则减去虚拟地址头
    _bss_start = . - KERNEL_VADDR;
    .bss : {
        *(.bss*)
    }
    _bss_end = . - KERNEL_VADDR;
    . = ALIGN(SZ_64K);
    img_end = . - KERNEL_VADDR;
}

回答问题:

  • 为什么LMA和VMA不同

    • VMA是对应虚拟内存的地址,但在内核启动时还处于物理地址模式,VMA可能超出物理内存范围。所以只能先加载,再映射到虚拟地址。
    • 为什么内核段的VMA要映射到高位,应该是一种惯例。
    • 为什么bootloader不用VMA,因为他负责初始化页表,他不能用,也没有必要。
  • LMA到VMA在何时转换

    • 由上一问可知,页表初始化之后便可转换为VMA。

练习5-c语言进制转换

从后往前取余即可。

练习6-函数栈

start.S中赋值了sp:

1
2
3
4
        /* Prepare stack pointer and jump to C. */
        adr     x0, boot_cpu_stack
        add     x0, x0, #0x1000
        mov     sp, x0 /* sp = boot_cpu_stack + 0x1000 */

boot_cpu_stack在init.c

1
2
#define INIT_STACK_SIZE 0x1000
char boot_cpu_stack[PLAT_CPU_NUMBER][INIT_STACK_SIZE] ALIGN(16);

由于PLAT_CPU_NUMBER被定义为4,故boot_cpu_stack大小为4*4096,可供四个CPU使用。sp初始化后指向第一个4069,也就是第一个cpu内核栈的最高位。初始化时,fp=sp。

但这是bootloader的栈。后续进入内核后,会重新分配内核栈,参见head.S:

1
2
3
4
5
6
7
8
BEGIN_FUNC(start_kernel)
    mov     x3, #0
    msr     TPIDR_EL1, x3
    ldr     x2, =kernel_stack
    add     x2, x2, KERNEL_STACK_SIZE
    mov     sp, x2 
    bl      main
END_FUNC(start_kernel)

于是内核栈的定义在start_kernel函数。

有关内核栈的位置,因为kernel_stack是全局数组,且未初始化,因而位于.bss。同时没有其他未初始化变量,因此首地址在.bss + KERNEL_STACK_SIZE。

通过readelf得到.bss的VMA为0xffffff0000090100,KERNEL_STACK_SIZE为0x2000,进入gdb调试可以验证

1
2
gef➤  x/g $sp
0xffffff0000092100 <kernel_stack+8192>: 0x0

练习7-调用惯例

先看stack_test函数。这里gdb安装了gef插件:

 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
gef➤ b stack_test
Breakpoint 1 at 0xffffff000008c020
gef➤ disas
Dump of assembler code for function stack_test:
=> 0xffffff000008c020 <+0>:     stp     x29, x30, [sp, #-32]! /* FP、LR 入栈 */
   0xffffff000008c024 <+4>:     mov     x29, sp
   0xffffff000008c028 <+8>:     str     x19, [sp, #16] /* x 入栈 */
   0xffffff000008c02c <+12>:    mov     x19, x0
   0xffffff000008c030 <+16>:    mov     x1, x0
   0xffffff000008c034 <+20>:    adrp    x0, 0xffffff0000090000 # 计算偏移
   0xffffff000008c038 <+24>:    add     x0, x0, #0x0
   0xffffff000008c03c <+28>:    bl      0xffffff000008c620 <printk>
   0xffffff000008c040 <+32>:    cmp     x19, #0x0
   0xffffff000008c044 <+36>:    b.gt    0xffffff000008c068 <stack_test+72> # greater than /* 递归 */
   0xffffff000008c048 <+40>:    bl      0xffffff000008c0dc <stack_backtrace> 
   0xffffff000008c04c <+44>:    mov     x1, x19
   0xffffff000008c050 <+48>:    adrp    x0, 0xffffff0000090000
   0xffffff000008c054 <+52>:    add     x0, x0, #0x20
   0xffffff000008c058 <+56>:    bl      0xffffff000008c620 <printk> 
   0xffffff000008c05c <+60>:    ldr     x19, [sp, #16] # x19 = sp + 16 /* x 出栈 */
   0xffffff000008c060 <+64>:    ldp     x29, x30, [sp], #32 # load pair  /* FP、LR 出栈 */
   0xffffff000008c064 <+68>:    ret
   0xffffff000008c068 <+72>:    sub     x0, x19, #0x1
   0xffffff000008c06c <+76>:    bl      0xffffff000008c020 <stack_test>
   0xffffff000008c070 <+80>:    mov     x1, x19
   0xffffff000008c074 <+84>:    adrp    x0, 0xffffff0000090000
   0xffffff000008c078 <+88>:    add     x0, x0, #0x20
   0xffffff000008c07c <+92>:    bl      0xffffff000008c620 <printk>
   0xffffff000008c080 <+96>:    ldr     x19, [sp, #16]
   0xffffff000008c084 <+100>:   ldp     x29, x30, [sp], #32
   0xffffff000008c088 <+104>:   ret
End of assembler dump.

运行,观察栈的变化,这里省略部分输出:

 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
46
47
48
49
50
51
52
53
54
55
56
57
gef➤  c
───────────────────────────────────────── registers ────
$x0  : 0x0000000000000005  # 这一层函数的输入值
$x19 : 0x0000000000000000  # 上一层函数的返回值
$x29 : 0xffffff00000920f0                # FP
$x30 : 0xffffff000008c0d4  → <main+72>   # LR
$sp  : 0xffffff00000920f0 
───────────────────────────────────────────── stack ────
0xffffff00000920f0│+0x0000: 0x0000000000000000
0xffffff00000920f8│+0x0008: 0xffffff000008c018 # 栈头,可能是栈初始化的数据结构
──────────────────────────────────────────── trace ────
[#0] 0xffffff000008c020 → stack_test()
[#1] 0xffffff000008c0d4 → main()
───────────────────────────────────────────────────────
gef➤  c
───────────────────────────────────────── registers ────
$x0  : 0x0000000000000004 
$x19 : 0x0000000000000005 
$x29 : 0xffffff00000920d0  
$x30 : 0xffffff000008c070  #→  <stack_test+80>
$sp  : 0xffffff00000920d0  →  0xffffff00000920f0  
───────────────────────────────────────────── stack ────
0xffffff00000920d0│+0x0000: 0xffffff00000920f0 ─┐  # FP
0xffffff00000920d8│+0x0008: 0xffffff000008c0d4  │  # LR
0xffffff00000920e0│+0x0010: 0x0000000000000000  │
0xffffff00000920e8│+0x0018: 0x00000000ffffffc0  │
0xffffff00000920f0│+0x0020: 0x0000000000000000 ◄┘ 
0xffffff00000920f8│+0x0028: 0xffffff000008c018  
───────────────────────────────────────────── trace ────
[#0] 0xffffff000008c020 → stack_test()
[#1] 0xffffff000008c070 → stack_test()
[#2] 0xffffff000008c0d4 → main()
────────────────────────────────────────────────────────
gef➤  c
───────────────────────────────────────── registers ────
$x0  : 0x0000000000000003  
$x19 : 0x0000000000000004  
$x29 : 0xffffff00000920b0  →  0xffffff00000920d0  →  0xffffff00000920f0 
$x30 : 0xffffff000008c070  
$sp  : 0xffffff00000920b0  →  0xffffff00000920d0  →  0xffffff00000920f0 
──────────────────────────────────────────── stack ────
0xffffff00000920b0│+0x0000: 0xffffff00000920d0 ─┐ # [#1] 
0xffffff00000920b8│+0x0008: 0xffffff000008c070  │
0xffffff00000920c0│+0x0010: 0x0000000000000005  │  
0xffffff00000920c8│+0x0018: 0x00000000ffffffc0  │  
0xffffff00000920d0│+0x0020: 0xffffff00000920f0 ◄┘ # [#2]
0xffffff00000920d8│+0x0028: 0xffffff000008c0d4  │
0xffffff00000920e0│+0x0010: 0x0000000000000000  │
0xffffff00000920e8│+0x0018: 0x00000000ffffffc0  │
0xffffff00000920f0│+0x0020: 0x0000000000000000 ◄┘ # [#3]
0xffffff00000920f8│+0x0028: 0xffffff000008c018  
─────────────────────────────────────────── trace ────
[#0] 0xffffff000008c020 → stack_test()
[#1] 0xffffff000008c070 → stack_test()
[#2] 0xffffff000008c070 → stack_test()
[#3] 0xffffff000008c0d4 → main()
──────────────────────────────────────────────────────

可以看到每次递归调用压栈4个64位字,分别是:上一层FP,LR,参数x和0x00000000ffffffc0。最后一个64位字用途未知。

练习9-backtrace

提供read_fp()接口,我们知道fp永远指向父函数的fp,故递归调用即可。

1
2
3
4
5
	u64* fp = (u64*) *((u64*)read_fp()); // 双层指针,因为第一层是本函数
	while(fp != 0) { 
        printk("LR %lx FP %lx Args %d %d %d %d %d\n", *(fp + 1), fp, *(fp - 2), *(fp - 1), *(fp), *(fp + 1), *(fp + 2)); //为什么5个参数是fp-2到fp+2?样例只包括一个参数,只要出现fp+2就能测试通过
        fp = (u64*) *fp; //下一层
	}

满分通过,懒得贴图了。


看到大佬写的,瞬间不想写了,寄。 https://www.cnblogs.com/kangyupl/p/chcore_lab1.html

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy