ChCore 实验1:机器启动

发布时间 2023-03-26 16:57:58作者: _vv123

实验指导书地址:https://ipads.se.sjtu.edu.cn/courses/os/labs/lab1.pdf

1.基本知识

1.1 熟悉 AArch64 汇编

练习 1

浏览《ARM 指令集参考指南》的 A1、 A3 和 D 部分,以熟悉 ARM ISA。
请做好阅读笔记,如果之前学习 x86-64 的汇编,请写下与 x86-64 相比
的一些差异。

Solution

https://www.cnblogs.com/vv123/p/17234414.html

文档:Arm® Instruction Set Reference Guide (sjtu.edu.cn)

书籍:《ARM64体系结构与编程实践》

与x86的区别:

  • 指令条数少,长度固定(32bits),各种指令的使用频率和执行时间相近。x86与此相反。
  • ARM只用LDR、STR指令操作内存,其它指令均是寄存器上的操作;而x86指令对访问内存不加限制。
  • ARM具有更多的通用寄存器。等等。

1.2 构建 ChCore 内核

1.3 QEMU 与 GDB

练习2

启动带调试的 QEMU,使用 GDB 的where命令来跟踪入口(第一个函数)及 bootloader 的地址。

Solution

之前没用过GDB(悲),先看https://wangjunstf.github.io/2021/06/12/gdb-ru-men-jiao-cheng/ 了解一下
where命令和bt的作用是一样的,用于显示当前程序的函数调用堆栈,即当前执行点在哪个函数中,该函数被哪个函数调用,以此类推,直到到达 main 函数。它打印出堆栈中每个函数的函数名、参数和返回地址等信息。
image
执行where可以发现入口函数是_start(),地址为0x0000000000080000

2.内核的引导与加载

Raspi3 从闪存(SD 卡)中加载.img镜像中的 bootloader 并执行。 bootloader 包括两个功能:

  1. bootloader 通过函数arm64_elX_to_el1将处理器的异常级别从其他级别切换到 EL1。《ARM 指令集参考指南》的 A3.2 节简要描述了异常级别。
  2. bootloader 初始化引导 UART,页表和 MMU。然后, bootloader 跳转到实际的内核。在后续的实验中将会描述内存结构。

bootloader 的源文件由一个汇编文件boot/start.S和一个 C 文件boot/init_c.c组成。

2.1 编译与可执行文件

在编译并链接诸如 ChCore 内核的可执行文件时,编译器会将每个 C 文件(.c)和汇编文件(.S)编译成为目标文件(objective file)(.o)。它是用二进制格式编码的机器指令编写的,但是由于文件内的符号地址等信息未完全生成,因此不能直接被运行。然后,链接器将所有已编译的目标文件链接(即在文件中填充其他文件中符号的地址等)成一个可执行目标文件(executable objective file) ,例如build/kernel.img,这个文件中是硬件可以运行的二进制机器指令组成的。可执行目标文件的常见格式是可执行和可链接格式(Executable and Linkable Format, ELF) 二进制文件。ELF 可执行文件以 ELF 头部(ELF header) 开始,后跟几个程序段(program section)。 ELF 头部记录文件的结构,每个程序段都是一个连续的二进制块,(硬件或软件)加载器将它们作为代码或数据加载到指定地址的内存中并开始执行。

以build/kernel.img文件为例,可以通过以下命令看到完整的 ELF 头部信息:
image

通过以下命令,看到build/kernel.img包含的程序段:
image

现在对一些主要的程序段进行一些解释:

  • .init: 保存 bootloader 的代码和数据。这个特殊的段在CMakefiles.txt中定义。所有其余的程序段都是真正的 ChCore 内核。
  • .text:保存内核程序代码,是由一条条的机器指令组成的。
  • .data:保存初始化的全局变量或静态变量数据。定义在函数内部的局部非静态变量不在该段中存储。
  • .rodata:保存只读数据,包括一些不可修改的常量数据,例如全局常量、 char *str = "apple"中的字符串常量等。然而,如果使用char str2[] = "apple",那么此时该字符串是动态地存在栈上的。
  • .bss:记录未初始化的全局或静态变量,例如int a。由于在运行期间未初始化的全局变量被初始化为 0,因此链接器只记录地址和大小,而不是占用实际空间。

除上面列出的部分外,其他大多数段都包含调试信息,通常包含在可执行文件中,而不是加载到内存中。

练习3

(1)结合readelf -S build/kernel.img读取符号表与练习2中的GDB调试信息,请找出请找出build/kernel.image入口定义在哪个文件中。
(2)继续借助单步调试追踪程序的执行过程,思考一个问题:目前本实验中支持的内核是单核版本的内核,然而在 Raspi3 上电后,所有处理器会同时启动。结合boot/start.S中的启动代码,并说明挂起其他处理器的控制流。

Solution

虚拟机自带了VSCode,我们打开chcore目录,大致浏览一下文件结构
image
练习2给出BootLoader的入口,以及build/kernel.img的elf头部给出.init的地址,均为0x0000000000080000

全局搜索80000,可以发现boot/image.h中定义的TEXT_OFFSET 值为0x80000。
image
全局搜索TEXT_OFFSET,发现在scripts/linker-aarch64.lds.in中。
image

首先可以看到这两行

. = TEXT_OFFSET
img_start = .

按字面意思理解,image的启动地址在TEXT_OFFSET,这与我们得到的结论相符

${init_object}又是什么?我们继续搜索,发现在CMakeList.txt中
image
可见依次执行Start.s,mmu.c,tools.S,init_c.c,uart.c,这与https://www.cnblogs.com/vv123/p/17234414.html 中提到的操作系统启动过程相符。由此可以判断build/kernel.imag的入口定义在Start.S文件中。

好吧,第(2)问似乎已经给出了(1)的答案。。。
boot/Start.S的内容如下

#include <common/asm.h>

.extern arm64_elX_to_el1
.extern boot_cpu_stack
.extern secondary_boot_flag
.extern clear_bss_flag
.extern init_c

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)

分析这段汇编程序,含义是将mpidr_el1的值传送给x8,然后保留x8的低8位,如果x8的低8位为零则执行后面的primary段。否则"hang all secondary processors before we intorduce multi-processors"
由此可见,chcore是通过判断mpidr_el1低8位是否为0来判断当前启动的是主CPU还是应该挂起的其他CPU。

2.2 内核的加载与执行

ELF 文件的加载(load) 与执行(execute) 是启动一个程序的两个重要的步骤:

  1. 加载是指将程序的 ELF 文件按照链接规则从存储器(如 ROM)上按照每个段的加载内存地址(Load Memory Address, LMA) 拷贝到内存上指定的地址
  2. 执行需要将 ELF 文件中的段“放到”(可通过拷贝或页表映射等方式) 虚拟内存地址(Virtual Memory Address, VMA),然后开始真正执行ELF文件中的代码。

大多数情况下,一个段 LMA 和 VMA 是相同的。
通过objdump也可以查看 ELF 文件中每一个段的 LMA 和 VMA:

chcore$ objdump -h build/kernel.img

练习 4

查看build/kernel.img的objdump信息。比较每一个段中的 VMA 和 LMA 是否相同,为什么?在 VMA 和 LMA 不同的情况下,内核是如何将该段的地址从 LMA 变为 VMA?
提示:从每一个段的加载和运行情况进行分析

Solution

objdump的执行结果如下

os@ubuntu:~/chcore-lab$ objdump -h build/kernel.img

build/kernel.img:     文件格式 elf64-little

节:
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和.comment的VMA和LMA相同,.text,.rodata,.bss的VMA都比LMA多了ffffff0000000000。
全局搜索ffffff0000000000,发现它在熟悉的boot/image.h中,被定义为 KERNEL_VADDR

#pragma once

#define SZ_16K			0x4000
#define SZ_64K                  0x10000

#define KERNEL_VADDR		0xffffff0000000000
#define TEXT_OFFSET		0x80000

再次搜索KERNEL_VADDR,回到了熟悉的链接脚本文件scripts/linker-aarch64.lds.in

#include "../boot/image.h"

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

    . = ALIGN(SZ_16K);

    init_end = ABSOLUTE(.);

    .text KERNEL_VADDR + init_end : AT(init_end) {
        *(.text*)
    }

    . = ALIGN(SZ_64K);
    .data : {
        *(.data*)
    }
    . = ALIGN(SZ_64K);

    .rodata : {
        *(.rodata*)
    }
    _edata = . - KERNEL_VADDR;

    _bss_start = . - KERNEL_VADDR;
    .bss : {
        *(.bss*)
    }
    _bss_end = . - KERNEL_VADDR;
    . = ALIGN(SZ_64K);
    img_end = . - KERNEL_VADDR;
}

为了看懂上面这些,还需要学习一下链接脚本的相关知识... https://www.cnblogs.com/jianhua1992/p/16852784.html
可以明显看出.text,.rodata,.bss
究其原因,猜测init和.comment