实验指导书地址: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 函数。它打印出堆栈中每个函数的函数名、参数和返回地址等信息。
执行where可以发现入口函数是_start(),地址为0x0000000000080000
2.内核的引导与加载
Raspi3 从闪存(SD 卡)中加载.img镜像中的 bootloader 并执行。 bootloader 包括两个功能:
- bootloader 通过函数arm64_elX_to_el1将处理器的异常级别从其他级别切换到 EL1。《ARM 指令集参考指南》的 A3.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 头部信息:
通过以下命令,看到build/kernel.img包含的程序段:
现在对一些主要的程序段进行一些解释:
- .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目录,大致浏览一下文件结构
练习2给出BootLoader的入口,以及build/kernel.img的elf头部给出.init的地址,均为0x0000000000080000
全局搜索80000,可以发现boot/image.h中定义的TEXT_OFFSET 值为0x80000。
全局搜索TEXT_OFFSET,发现在scripts/linker-aarch64.lds.in中。
首先可以看到这两行
. = TEXT_OFFSET
img_start = .
按字面意思理解,image的启动地址在TEXT_OFFSET,这与我们得到的结论相符
${init_object}
又是什么?我们继续搜索,发现在CMakeList.txt中
可见依次执行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) 是启动一个程序的两个重要的步骤:
- 加载是指将程序的 ELF 文件按照链接规则从存储器(如 ROM)上按照每个段的加载内存地址(Load Memory Address, LMA) 拷贝到内存上指定的地址
- 执行需要将 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