由于👴觉得👴学校的操作系统讲了个🔨,慕名而来学习上交的 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文件的细节。
练习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在何时转换
练习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