# chcore-lab-0 **Repository Path**: otal/chcore-lab ## Basic Information - **Project Name**: chcore-lab-0 - **Description**: 银杏书实验chcore-lab - **Primary Language**: C - **License**: MulanPSL-1.0 - **Default Branch**: lab1 - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 3 - **Created**: 2022-10-09 - **Last Updated**: 2022-10-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # ChCore Lab 1 experiment note 此实验是来自[《现代操作系统:原理与实现》](https://ipads.se.sjtu.edu.cn/mospi/)。 相比于6.828,此实验有虚拟实验平台,可以不用自己配环境。实验平台链接:[实验 1:ChCore操作系统-机器启动](https://www.educoder.net/shixuns/7fwuk3lh/challenges) 如果希望自己配环境做实验的话,移步官方实验配置:[ChCore实验环境配置](https://ipads.se.sjtu.edu.cn/courses/os/labs/start.html) **声明**:本人不能保证此答案的正确性,有部分原因是问题不清晰,另一部分原因是给的代码注释较少,有些代码由于个人能力无法读懂,所以推测部分我会标注清楚。希望大家能指正我的错误。 ## 练习1 > 浏览《ARM 指令集参考指南》的 A1、A3 和 D 部分,以熟悉 ARM ISA。请做好阅读笔记,如果之前学习 x86-64 的汇编,请写下与 x86-64 相比的一些差异。 具体看知乎专栏里的ARM汇编相关的文章 ## 练习2 > 启动带调试的 QEMU,使用 GDB 的where命令来跟踪入口(第一个函数)及 bootloader 的地址。 ```shell 0x0000000000080000 in ?? () (gdb) where #0 0x0000000000080000 in _start () ``` 可以看到入口为`0x0000000000080000`并且函数为`_start`。通过查找可知`_start`函数在`boot/start.S`文件中。 (吐槽一下平台判断正确的方式有点离谱,一直没过测试) ## 练习3 > 结合`readelf -S build/kernel.img`读取符号表与练习 2 中的GDB 调试信息,请找出请找出`build/kernel.image`入口定义在哪个文件中。 ```shell $ 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 ``` 同时根据练习2获得的信息可以得出:程序入口在0x80000。在chcore-lab文件夹查找入口定义,可以看到在`boot/image.h`文件中有定义:`#define TEXT_OFFSET 0x80000`。 接着查找可以看到在`scripts/linker-aarch64.lds.in`文件中,`TEXT_OFFSET`被使用了。 `linker-aarch64.lds.in`文件就是链接器脚本。链接器脚本是一个或多个输入文件合成一个输出文件。(链接器脚本规则参考[1]) ```assembly #include "../boot/image.h" SECTIONS { . = TEXT_OFFSET; // 把定位器符号置为0x80000 img_start = .; // 赋值语句 init : { ${init_object} // 将init_object中所有文件合并成一个init段 } ...... } ``` 其中`init_object`在`CMakeLists.txt`文件定义: ```cmake 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" ) ``` 在文章[1]中,可知设置进程入口的方式有: > 1. ld命令行的-e选项 > 2. 连接脚本的ENTRY(SYMBOL)命令 > 3. 如果定义了start 符号, 使用start符号值 > 4. 如果存在 .text section , 使用.text section的第一字节的位置值 > 5. 使用值0 通过搜索可以找到在`CMakeLists.txt`文件中有: ```cmake set_property( TARGET kernel.img APPEND_STRING PROPERTY LINK_FLAGS "-T ${CMAKE_CURRENT_BINARY_DIR}/${link_script} -e _start" ) ``` 此条语句设置了编译时使用自定义链接器脚本并且设置程序入口为`_start`。 > 继续借助单步调试追踪程序的执行过程,思考一个问题:目前本实验中支持的内核是单核版本的内核,然而在 Raspi3 上电后,所有处理器会同时启动。结合`boot/start.S`中的启动代码,并说明挂起其他处理器的控制流。 使用GDB调试时,断点会不断在1.1-1.4线程中切换,只有1.1线程会往下走,由此可知只有1.1线程所在的处理器在运行,其他处理器都被挂起。从调试信息中可以看到挂起的线程都是在`_start()`函数中挂起。在`boot/start.S`文件中可以看到注释: ```assembly BEGIN_FUNC(_start) ...... 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 ...... END_FUNC(_start) ``` 根据注释可知:挂起操作就是让其他处理器一直死循环。 ## 练习4 > 查看build/kernel.img的objdump信息。比较每一个段中的VMA和LMA是否相同? ```shell $ objdump -h build/kernel.img build/kernel.img: file format elf64-little section: 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相同。.text段、.bss段和.rodata段VMA和LMA不同。 VMA和LMA的设置主要在链接器脚本中:`SECTIONS`命令告诉ld如何把输入文件的sections映射到输出文件的各个section:如何将输入section合为输出section;如何把输出section放入虚拟内存地址(VMA)和加载内存地址(LMA)。 输出section描述具有如下格式: ```assembly SECTION [ADDRESS] [(TYPE)] : [AT(LMA)] { OUTPUT-SECTION-COMMAND OUTPUT-SECTION-COMMAND ... } [>REGION] [AT>LMA_REGION] [:PHDR :PHDR ...] [=FILLEXP] ``` `[]`内的内容为可选选项,`SECTION`为section的名字。其中`[ADDRESS]`指定了VMA,`[AT(LMA)]`指定了LMA[2]。 现在来查看`linker-aarch64.lds.in`文件: ```assembly #include "../boot/image.h" SECTIONS { . = TEXT_OFFSET; img_start = .; init : { ${init_object} } . = ALIGN(SZ_16K); init_end = ABSOLUTE(.); // KERNEL_VADDR在image.h定义为0xffffff0000000000 .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; } ``` 可以很明显地看到init段没有设置VMA和LMA,所以链接器将VMA设为定位符号的值,并且默认VMA=LMA。而.text段设置了不同的VMA和LMA。而设置VMA会更改定位符号的值,使得后面的.bss段和.rodata段的VMA和LMA不同。 > 为什么VMA和LMA不同? 根据lab 1实验文档可以得知init段就是bootloader,主要的作用就是: - 将处理器的异常级别从其他级别切换到EL1 - 初始化引导UART,页表和 MMU - 最后跳转到实际的内核 可以知道在init段运行时,MMU还未初始化,还处于实模式。所以不能使用虚拟地址,因为虚拟地址可能超出了物理地址的范围。而bootloader之后,内核就映射到虚拟地址,而内核段映射到高位应该是一种惯例。 > 在VMA和LMA不同的情况下,内核是如何将该段的地址从LMA变为VMA?提示:从每一个段的加载和运行情况进行分析 LMA变为VMA的方式就是虚拟地址映射到物理地址的过程。具体内容可以看银杏书第4章。 ## 练习5 > 以不同的进制打印数字的功能(例如 8、10、16)尚未实现,请在`kernel/common/printk.c`中填充`printk_write_num`以完善printk的功能。 找到`kernel/common/printk.c`文件,分析一下代码: ```c static int printk_write_num(char **out, long long i, int base, int sign, int width, int flags, int letbase) { char print_buf[PRINT_BUF_LEN]; char *s, c; int t, neg = 0, pc = 0; unsigned long long u = i, num; if (i == 0) { print_buf[0] = '0'; print_buf[1] = '\0'; return prints(out, print_buf, width, flags); // 将print_buf中的所有字符用simple_outputchar函数打印 } if (sign && base == 10 && i < 0) { neg = 1; u = -i; } // TODO: fill your code here // store the digitals in the buffer `print_buf`: // 1. the last postion of this buffer must be '\0' // 2. the format is only decided by `base` and `letbase` here if (neg) { // 如果数字是负数,那么需要添加‘-’ if (width && (flags & PAD_ZERO)) { // 如果要求打印的数字长度不为0或需要填充0去满足要求长度 simple_outputchar(out, '-'); // 用simple_outputchar函数打印打印字符‘-’ ++pc; --width; } else { *--s = '-'; // 因为不需要填充,所以字符‘-’直接放在数字前面 } } return pc + prints(out, s, width, flags); } ``` `simple_outputchar`函数: ```c static void simple_outputchar(char **str, char c) { if (str) { **str = c; ++(*str); } else { uart_send(c); } } ``` 此函数的功能是通过串口发送字符到`AUX_MU_IO_REG`或将字符复制到str指定的位置。`AUX_MU_IO_REG`应该是树莓派和外设沟通的UART[4],但是现在不能确定,之后再看看。 ## 练习6 > 内核栈初始化(即初始化SP和FP)的代码位于哪个函数? 全局搜索stack可以找到两个宏变量:`kernel_stack`和`boot_cpu_stack`。这两个都是栈,不同的是`boot_cpu_stack`栈是在bootloader中使用的栈,此栈是在start函数中被初始化。`kernel_stack`则是在内核启动(`start_kernel`函数中)时被初始化。所以内核栈初始化在`start_kernel`函数中: ```assembly 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) ``` `ldr x2, =kernel_stack`为ldr伪指令,功能是将`kernel_stack`的地址放入x2寄存器中。 > 内核栈在内存中位于哪里?内核如何为栈保留空间? 以下为猜测: `kernel_stack`定义在`main.c`文件中,是全局变量并且未初始化,所以按照编译规则应该放在.bss段。再回到之前的链接器脚本(`linker-aarch64.lds.in`)中,可以发现.bss段的地址是`. - KERNEL_VADDR`。 根据机器加载程序的方式,.bss段有全局变量的信息,在加载时就会开辟相应的空间给全局变量。 ## 练习7 > 为了熟悉AArch64上的函数调用惯例,请在`kernel/main.c`中通过GDB找到`stack_test`函数的地址,在该处设置一个断点,并检查在内核启动后的每次调用情况。每个stack_test递归嵌套级别将多少个64位值压入堆栈,这些值是什么含义? 我将gdb输出导出到[experiment7-gdb.txt](experiment7-gdb.txt)文件中(这里说一下:实验文档中打印内存的方式和下面的输出结果不同,应该使用`x /10xg $x29`才是输出16进制的结果[5])。可以看到一共有6个输出结果,说明`stack_test`函数调用了6次。每次将4个64位值压入栈。 下面分析gdb到最后一个断点时的输出和`stack_test`函数: ```shell 0xffffff0000092050 : 0xffffff0000092070 0xffffff000008c070 0xffffff0000092060 : 0x0000000000000002 0x00000000ffffffc0 0xffffff0000092070 : 0xffffff0000092090 0xffffff000008c070 0xffffff0000092080 : 0x0000000000000003 0x00000000ffffffc0 0xffffff0000092090 : 0xffffff00000920b0 0xffffff000008c070 0xffffff00000920a0 : 0x0000000000000004 0x00000000ffffffc0 0xffffff00000920b0 : 0xffffff00000920d0 0xffffff000008c070 0xffffff00000920c0 : 0x0000000000000005 0x00000000ffffffc0 0xffffff00000920d0 : 0xffffff00000920f0 0xffffff000008c0d4 0xffffff00000920e0 : 0x0000000000000000 0x00000000ffffffc0 0xffffff00000920f0 : 0x0000000000000000 0xffffff000008c018 0xffffff0000092100 : 0x0000000000000000 0x0000000000000000 ``` 每两行一组,是一次函数调用的入栈结果。从结果看父函数的栈底地址和调用时的参数都被保存了。仅仅根据这些数据无法推断出来值的意思。 现在来查看一下`stack_test`函数: ```c void stack_test(long x) { kinfo("entering stack_test %d\n", x); if (x > 0) stack_test(x - 1); else stack_backtrace(); kinfo("leaving stack_test %d\n", x); } ``` 和其编译后的汇编语言: ```assembly 0xffffff000008c020 : stp x29, x30, [sp,#-32]! /* FP、LR 入栈 */ 0xffffff000008c024 : mov x29, sp /* 更新FP */ 0xffffff000008c028 : str x19, [sp,#16] /* 在栈空间中保存x19寄存器 */ 0xffffff000008c02c : mov x19, x0 0xffffff000008c030 : mov x1, x0 0xffffff000008c034 : adrp x0, 0xffffff0000090000 0xffffff000008c038 : add x0, x0, #0x0 0xffffff000008c03c : bl 0xffffff000008c638 0xffffff000008c040 : cmp x19, #0x0 0xffffff000008c044 : b.gt 0xffffff000008c068 0xffffff000008c048 : bl 0xffffff000008c0dc 0xffffff000008c04c : mov x1, x19 0xffffff000008c050 : adrp x0, 0xffffff0000090000 0xffffff000008c054 : add x0, x0, #0x20 0xffffff000008c058 : bl 0xffffff000008c638 0xffffff000008c05c : ldr x19, [sp,#16] /* 恢复x19寄存器 */ 0xffffff000008c060 : ldp x29, x30, [sp],#32 /* FP、LR 出栈 */ 0xffffff000008c064 : ret 0xffffff000008c068 : sub x0, x19, #0x1 0xffffff000008c06c : bl 0xffffff000008c020 0xffffff000008c070 : mov x1, x19 /* 递归调用时的返回地址 */ 0xffffff000008c074 : adrp x0, 0xffffff0000090000 0xffffff000008c078 : add x0, x0, #0x20 0xffffff000008c07c : bl 0xffffff000008c638 0xffffff000008c080 : ldr x19, [sp,#16] /* 恢复x19寄存器 */ 0xffffff000008c084 : ldp x29, x30, [sp],#32 /* FP、LR 出栈 */ 0xffffff000008c088 : ret ``` 从练习7介绍中可知x30为返回地址、x29为帧指针。那么现在就可以来分析了: ```shell 0xffffff0000092050 : 0xffffff0000092070 0xffffff000008c070 0xffffff0000092060 : 0x0000000000000002 0x00000000ffffffc0 ``` 0xffffff0000092070为帧指针(FP寄存器),0xffffff000008c070为返回地址(LR寄存器),返回到``,0x0000000000000002为保存的x19寄存器(由[6]可知,x19-x28是需要子寄存器保存以便返回的时候恢复现场),0x00000000ffffffc0为当前函数栈空间保存的数据(具体是什么暂时说不清楚)。 ## 练习8 > 在AArch64中,返回地址(保存在x30寄存器),帧指针(保存在x29寄存器)和参数由寄存器传递。但是,当调用者函数(caller function)调用被调用者函数(callee fcuntion)时,为了复用这些寄存器,这些寄存器中原来的值是如何被存在栈中的?请使用示意图表示,回溯函数所需的信息(如SP、FP、LR、参数、部分寄存器值等)在栈中具体保存的位置在哪? 由练习7中的汇编代码和[6]可知,回溯函数所需的信息中只有FP、LR、部分寄存器保一定存在栈中,参数可能存在栈中(取决于参数的大小和多少)。保存方法: - SP值:通过入栈和出栈FP和LR来更改和恢复值的 - 参数:参数总数不超过8个或所有参数大小不超过64字节,那么参数就保存在x0-x7寄存器中,多出来的参数放入栈中。 - FP、LR:下图所属栈顶 - 寄存器:下图所示save area ![stack](img/lab1-8.png) ## 练习9 > 使用与示例相同的格式,在`kernel/monitor.c`中实现`stack_backtrace`。为了忽略编译器优化等级的影响,只需要考虑`stack_test`的情况,我们已经强制了这个函数编译优化等级。 由练习7可知,通过FP就能回溯到上一个函数的FP,直到回溯到FP为0时到头,然后根据练习8的图片可以知道LR和参数相对于FP的位置。 > **挑战**:请思考,如果考虑更多情况(例如,多个参数)时,应当如何进行回溯操作 个人认为此题目有问题,ARM在调用函数时参数是放在x0-x7中的,所以不一定能被保存在栈中。`stack_test`能保存是因为在`stack_test`函数中使用了x19作为临时寄存器来计算被调用函数的参数x,并且x19是需要被调用函数来保存现场以便返回时恢复寄存器值[6],所以在栈中会保存参数x。 ## Reference [1] [LD 文件:规则详解_申小白的博客-CSDN博客_ld文件](https://blog.csdn.net/shenjin_s/article/details/88712249) [2] [SECTIONS (biscuitos.github.io)](https://biscuitos.github.io/blog/LD-SECTIONS/) [3] [linux内存管理1 基础知识_lgjjeff的博客-CSDN博客_ttbr0_el1](https://blog.csdn.net/lgjjeff/article/details/93336516) [4] [raspberrypi/uart04.c at master · dwelch67/raspberrypi · GitHub](https://github.com/dwelch67/raspberrypi/blob/master/uart04/uart04.c) [5] [gdb中x的用法_公众号:程序芯世界的博客-CSDN博客_gdb x](https://blog.csdn.net/baidu_24256693/article/details/47298513) [6] [Procedure Call Standard for the Arm® 64-bit Architecture (AArch64)](https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst#machine-registers)