ChCore Lab1 旧稿

当前博文已经废弃,请看最新版的:https://www.cnblogs.com/kangyupl/p/chcore_lab1.html

本文为上海交大ipads研究所陈海波老师等人所著的《现代操作系统:原理与实现》的课程实验(LAB)的学习笔记。练习题

实验链接:好大学慕课的第十六章,链接以后可能会更换。

课程视频&PPT:SE315 / 2020 / Welcome

先说感受:虽然不是实验的设计和引导不是很完美,但做起来还是蛮爽的。

环境配置

能用虚拟机的请直接用讲义里给的虚拟机。想自己配环境的话......太遭罪了!实在是太遭罪了!我用的Ubuntu18的WSL,结果发现需要手动安装一堆依赖。人与人的体质不能一概而论,我在极端愤怒的情况下直接卸载了Ubuntu18,换成了Ubuntu20,然后发现还要装Docker,于是又是一个小时搭进去了......

练习1

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

ARM汇编我也是头一次接触,给的全英文的参考指南读起来确实有点费劲儿。建议找去本中文的ARM汇编书翻翻,有问题就多百度。

练习2

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

0x0000000000080000 in ?? ()
(gdb) where
#0  0x0000000000080000 in _start ()
Backtrace stopped: not enough registers or memory available to unwind further

第一个函数为_start(),地址为0x0000000000080000

练习3-1

结合readelf -S build/kernel.img读取符号表与练习 2 中的GDB 调试信息,请找出请找出build/kernel.image入口定义在哪个文件中。

~/chcore$ 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
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

观察到init段的地址为0x0000000000080000,恰为上一题_start()。在源码里全局搜索可知,该函数位于boot/start.S中。

练习3-2

继续借助单步调试追踪程序的执行过程,思考一个问题:目前本实验中支持的内核是单核版本的内核,然而在 Raspi3 上电后,所有处理器会同时启动。结合boot/start.S中的启动代码,并说明挂起其他处理器的控制流。

对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	/* mpidr_el1中记录了当前PE的cpuid */
	and	x8, x8,	#0xFF	/* 保留低8位 */
	cbz	x8, primary		/* 若为0,则为首个PE,跳转到primary */

  /* hang all secondary processors before we intorduce multi-processors */
secondary_hang:
	bl secondary_hang	/* 若不为0,则为非首个PE,进入死循环来挂起 */

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	/* 读入数组boot_cpu_stack地址,init_c.c中有定义 */
	add 	x0, x0, #0x1000		/* 栈由高地址向低地址增长,故用加法,相当于给栈分配了4096字节 */
	mov 	sp, x0				/* 设置栈指针寄存器 */

	bl 	init_c	/* 调用函数init_c,init_c.c中定义 */

	/* Should never be here */
	b	.
END_FUNC(_start)

可知是通过mpidr_el1寄存器的值来判断当前PE的cpuid,若为0则为首个PE,正常执行后续代码;若不为0,则非首个PE,跳到一个死循环函数中来进行挂起。

练习4

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

~/chcore$ 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相同,其他段的VMA和LMA都有0xffffff00000的偏差。这是因为init段中存放的是bootloader的代码,其他段存放的时内核代码。bootloader开始运行时仍处于实模式,既不支持虚拟内存,也无法访问0xffffff00000级别的内存区域,寻址的时候用的是LMA。bootloader在运行过程中将切换到保护模式,并完成内核代码从低地址段到高地址段的映射。故进入内核后VMA变成了上图中的数值,寻址的时候也是用的VMA。

练习5

以不同的进制打印数字的功能(例如 8、10、16)尚未实现,请在kernel/common/printk.c中 填 充printk_write_num以 完善printk的功能。

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;
	int t, neg = 0, pc = 0;
	unsigned long long u = i;

	if (i == 0) {
		print_buf[0] = '0';
		print_buf[1] = '\0';
		return prints(out, print_buf, width, flags);
	}

	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
	s=print_buf+PRINT_BUF_LEN;
	*s='\0';
	while(u>0){
		s--;
		t=u%base;
		if(t<=9){
			*s=t+'0';
		}
		else {
			if(letbase)
				*s=t-10+'a';
			else
				*s=t-10+'A';
		}
		u/=base;
	}

	if (neg) {
		if (width && (flags & PAD_ZERO)) {
			simple_outputchar(out, '-');
			++pc;
			--width;
		} else {
			*--s = '-';
		}
	}

	return pc + prints(out, s, width, flags);
}

练习6

内核栈初始化(即初始化 SP 和 FP)的代码位于哪个函数?内核栈在内存中位于哪里?内核如何为栈保留空间?

初始化的代码位于boot/start.S中

	/* Prepare stack pointer and jump to C. */
	adr 	x0, boot_cpu_stack	/* 读入数组boot_cpu_stack地址,init_c.c中有定义 */
	add 	x0, x0, #0x1000		/* 栈由高地址向低地址增长,故用加法,相当于给栈分配了4096字节 */
	mov 	sp, x0				/* 设置栈指针寄存器 */

在boot/init_c.c中可找到boot_cpu_stack的定义,是一个定义好的4*4096字节的二维全局数组,每个CPU用其中的一维。

char boot_cpu_stack[PLAT_CPU_NUMBER][INIT_STACK_SIZE] ALIGN(16);

内核栈初始化的行为就是让SP指向boot_cpu_stack[0]的第4096字节处。因为栈是由高地址向低地址增长,所以第4096字节前的空间即为留给内核栈的空间。

练习7

为了熟悉 AArch64 上的函数调用惯例,请在kernel/main.c中通过GDB 找到stack_test函数的地址,在该处设置一个断点,并检查在内核启动后的每次调用情况。每个stack_test递归嵌套级别将多少个 64位值压入堆栈,这些值是什么含义?

首先要确认要确认CMakeLists.txt中是Debug模式,如果是在Release模式下会因为代码逻辑优化导致部分执行逻辑与预期不符。

set(CMAKE_BUILD_TYPE "Debug")  # "Release" or "Debug"

stack_test()打个断点,然后一顿观察。顺便看看反汇编码。

(gdb) b stack_test
Breakpoint 1 at 0xffffff000008c030: file ../kernel/main.c, line 27.
(gdb) c
Continuing.

Thread 1 hit Breakpoint 1, stack_test (x=5) at ../kernel/main.c:27
27      ../kernel/main.c: No such file or directory.
(gdb) x/10g $x29
0xffffff00000920e0 <kernel_stack+8128>: 0xffffff0000092100      0xffffff000008c0c0
0xffffff00000920f0 <kernel_stack+8144>: 0x0000000000000000      0x00000000ffffffc0
0xffffff0000092100 <kernel_stack+8160>: 0x00000000000887e0      0xffffff000008c018
0xffffff0000092110 <kernel_stack+8176>: 0x0000000000000000      0x00000000000873c8
0xffffff0000092120 <kernel_stack+8192>: 0x0000000000000000      0x0000000000000000
(gdb) c
Continuing.

Thread 1 hit Breakpoint 1, stack_test (x=4) at ../kernel/main.c:27
27      in ../kernel/main.c
(gdb) x/10g $x29
0xffffff00000920c0 <kernel_stack+8096>: 0xffffff00000920e0      0xffffff000008c070
0xffffff00000920d0 <kernel_stack+8112>: 0x0000000000000005      0x00000000ffffffc0
0xffffff00000920e0 <kernel_stack+8128>: 0xffffff0000092100      0xffffff000008c0c0
0xffffff00000920f0 <kernel_stack+8144>: 0x0000000000000000      0x00000000ffffffc0
0xffffff0000092100 <kernel_stack+8160>: 0x00000000000887e0      0xffffff000008c018
(gdb) c
Continuing.

Thread 1 hit Breakpoint 1, stack_test (x=3) at ../kernel/main.c:27
27      in ../kernel/main.c
(gdb) x/10g $x29
0xffffff00000920a0 <kernel_stack+8064>: 0xffffff00000920c0      0xffffff000008c070
0xffffff00000920b0 <kernel_stack+8080>: 0x0000000000000004      0x00000000ffffffc0
0xffffff00000920c0 <kernel_stack+8096>: 0xffffff00000920e0      0xffffff000008c070
0xffffff00000920d0 <kernel_stack+8112>: 0x0000000000000005      0x00000000ffffffc0
0xffffff00000920e0 <kernel_stack+8128>: 0xffffff0000092100      0xffffff000008c0c0
(gdb) c
Continuing.

Thread 1 hit Breakpoint 1, stack_test (x=2) at ../kernel/main.c:27
27      in ../kernel/main.c
(gdb) x/10g $x29
0xffffff0000092080 <kernel_stack+8032>: 0xffffff00000920a0      0xffffff000008c070
0xffffff0000092090 <kernel_stack+8048>: 0x0000000000000003      0x00000000ffffffc0
0xffffff00000920a0 <kernel_stack+8064>: 0xffffff00000920c0      0xffffff000008c070
0xffffff00000920b0 <kernel_stack+8080>: 0x0000000000000004      0x00000000ffffffc0
0xffffff00000920c0 <kernel_stack+8096>: 0xffffff00000920e0      0xffffff000008c070
(gdb) c
Continuing.

Thread 1 hit Breakpoint 1, stack_test (x=1) at ../kernel/main.c:27
27      in ../kernel/main.c
(gdb) x/10g $x29
0xffffff0000092060 <kernel_stack+8000>: 0xffffff0000092080      0xffffff000008c070
0xffffff0000092070 <kernel_stack+8016>: 0x0000000000000002      0x00000000ffffffc0
0xffffff0000092080 <kernel_stack+8032>: 0xffffff00000920a0      0xffffff000008c070
0xffffff0000092090 <kernel_stack+8048>: 0x0000000000000003      0x00000000ffffffc0
0xffffff00000920a0 <kernel_stack+8064>: 0xffffff00000920c0      0xffffff000008c070
(gdb) c
Continuing.

Thread 1 hit Breakpoint 1, stack_test (x=0) at ../kernel/main.c:27
27      in ../kernel/main.c
(gdb) x/10g $x29
0xffffff0000092040 <kernel_stack+7968>: 0xffffff0000092060      0xffffff000008c070
0xffffff0000092050 <kernel_stack+7984>: 0x0000000000000001      0x00000000ffffffc0
0xffffff0000092060 <kernel_stack+8000>: 0xffffff0000092080      0xffffff000008c070
0xffffff0000092070 <kernel_stack+8016>: 0x0000000000000002      0x00000000ffffffc0
0xffffff0000092080 <kernel_stack+8032>: 0xffffff00000920a0      0xffffff000008c070
(gdb) x/30i stack_test
   0xffffff000008c020 <stack_test>:     stp     x29, x30, [sp, #-32]!	//SP,LR压栈
   0xffffff000008c024 <stack_test+4>:   mov     x29, sp					//更新SP
   0xffffff000008c028 <stack_test+8>:   str     x19, [sp, #16]			//函数参数压栈
   0xffffff000008c02c <stack_test+12>:  mov     x19, x0
=> 0xffffff000008c030 <stack_test+16>:  mov     x1, x0
   0xffffff000008c034 <stack_test+20>:  adrp    x0, 0xffffff0000090000
   0xffffff000008c038 <stack_test+24>:  add     x0, x0, #0x0
   0xffffff000008c03c <stack_test+28>:  bl      0xffffff000008dd68 <printk>
   0xffffff000008c040 <stack_test+32>:  cmp     x19, #0x0
   0xffffff000008c044 <stack_test+36>:  b.gt    0xffffff000008c068 <stack_test+72>
   0xffffff000008c048 <stack_test+40>:  bl      0xffffff000008c0d0 <stack_backtrace>
   0xffffff000008c04c <stack_test+44>:  mov     x1, x19
   0xffffff000008c050 <stack_test+48>:  adrp    x0, 0xffffff0000090000
   0xffffff000008c054 <stack_test+52>:  add     x0, x0, #0x20
   0xffffff000008c058 <stack_test+56>:  bl      0xffffff000008dd68 <printk>
   0xffffff000008c05c <stack_test+60>:  ldr     x19, [sp, #16]			//读取函数参数
   0xffffff000008c060 <stack_test+64>:  ldp     x29, x30, [sp], #32		//读取SP,LR
   0xffffff000008c064 <stack_test+68>:  ret
   0xffffff000008c068 <stack_test+72>:  sub     x0, x19, #0x1
   0xffffff000008c06c <stack_test+76>:  bl      0xffffff000008c020 <stack_test>
   0xffffff000008c070 <stack_test+80>:  b       0xffffff000008c04c <stack_test+44>

注意SP和FP的区别,SP指当前函数的栈顶,FP指当前函数的栈底。刚进入一个函数时栈底和栈顶是相同的,随着各种临时变量的定义SP逐渐增长,在调用子函数时父函数的在调用时的SP值又要作为子函数的FP值使用。

所以stack_test()压入的值就好理解了,FP处的内存值是父函数的FP值,FP+8处的值是当前函数的LR值,即保存在链接寄存器(Link Register,LR)中的返回地址。FP-16处则是函数的参数。至于FP-8处为啥老是0x00000000ffffffc0,都是printk()函数干的,跟stack_test()无关。放到整体的内存图里可以很清楚的观察出规律来。

(gdb) x/40x $x29
0xffffff0000092000 <kernel_stack+7904>: 0xffffff0000092040      0xffffff000008c04c
0xffffff0000092010 <kernel_stack+7920>: 0x0000000000000000      0x0000000000000000
0xffffff0000092020 <kernel_stack+7936>: 0x0000000000000000      0x0000000000000000
0xffffff0000092030 <kernel_stack+7952>: 0x0000000000000000      0x0000000000000000
0xffffff0000092040 <kernel_stack+7968>: 0xffffff0000092060      0xffffff000008c070
0xffffff0000092050 <kernel_stack+7984>: 0x0000000000000001      0x00000000ffffffc0
0xffffff0000092060 <kernel_stack+8000>: 0xffffff0000092080      0xffffff000008c070
0xffffff0000092070 <kernel_stack+8016>: 0x0000000000000002      0x00000000ffffffc0
0xffffff0000092080 <kernel_stack+8032>: 0xffffff00000920a0      0xffffff000008c070
0xffffff0000092090 <kernel_stack+8048>: 0x0000000000000003      0x00000000ffffffc0
0xffffff00000920a0 <kernel_stack+8064>: 0xffffff00000920c0      0xffffff000008c070
0xffffff00000920b0 <kernel_stack+8080>: 0x0000000000000004      0x00000000ffffffc0
0xffffff00000920c0 <kernel_stack+8096>: 0xffffff00000920e0      0xffffff000008c070
0xffffff00000920d0 <kernel_stack+8112>: 0x0000000000000005      0x00000000ffffffc0
0xffffff00000920e0 <kernel_stack+8128>: 0xffffff0000092100      0xffffff000008c0c0
0xffffff00000920f0 <kernel_stack+8144>: 0x0000000000000000      0x00000000ffffffc0
0xffffff0000092100 <kernel_stack+8160>: 0x00000000000887e0      0xffffff000008c018
0xffffff0000092110 <kernel_stack+8176>: 0x0000000000000000      0x00000000000873c8
0xffffff0000092120 <kernel_stack+8192>: 0x0000000000000000      0x0000000000000000
0xffffff0000092130 <kernel_stack+8208>: 0x0000000000000000      0x0000000000000000

练习8

在 AArch64 中,返回地址(保存在x30寄存器),帧指针(保存在x29寄存器)和参数由寄存器传递。但是,当调用者函数(caller function)调用被调用者函数(callee fcuntion)时,为了复用这些寄存器,这些寄存器中原来的值是如何被存在栈中的?请使用示意图表示,回溯函数所需的信息(如 SP、FP、LR、参数、部分寄存器值等)在栈中具体保存的位置在哪?

根据上面的分析很容易画出图来,我这里是用ASCIIFlow画的文字图。

|           |
|           | ^
|           | |
+-----------+ | Low Address
|Arg1       | |
+-----------+
|Other Data |
+-----------+
|Father's FP| ------+
+-----------+       |
|LR         |       |
+-----------+       |
|......     |       |
|           |       |
|           |       |
|......     |       |
+-----------+       |
|Arg1       |       |
+-----------+       |
|Other Data |       |
+-----------+       |
|Father's FP| <-----+
+-----------+
|LR         |  ^
+-----------+  | High Address
|           |  |
|           |  |

练习9

使用与示例相同的格式, 在kernel/monitor.c中实现stack_backtrace。为了忽略编译器优化等级的影响,只需要考虑stack_test的情况,我们已经强制了这个函数编译优化等级。

这一题需要仔细地阅读文档,弄清楚输出的格式。主要逻辑就是在练习7里的分析基础上在函数栈里进行递归,直到FP变成0时终止递归。

需要注意递归时不输出stack_backtrace(),而是从调用stack_backtrace()的函数开始输出。

__attribute__ ((optimize("O1")))
int stack_backtrace()
{
	printk("Stack backtrace:\n");

	// Your code here.
	u64* fp=(u64*)(*(u64*)read_fp());	// 输出的FP为调用stack_backtrace的函数的FP,故加一层间接访问
	while(*(fp) != 0){	// 递归到没有父函数时停止
        // 地址为FP+8处的值为当前函数LR,地址为FP处的值为父函数的FP,FP的值就是当前函数的FP
		printk("LR %lx FP %lx Args ",*(fp+1),fp);	
		u64* p=fp-2; // 地址为FP-16处开始的值为当前函数的参数列表
		for(int k=5;k>0;k--){
			printk("%d ",*p);
			p++;
		}
		printk("\n");
		fp = (u64*) *fp; // 沿着FP递归访问
	}

	return 0;
}

后记

既然发售当天买的花钱买的第一版,有些感受还是得谈谈嘛。在课本的致谢中可以看到ipads研究所的许多前辈合作完成的,但就目前的初版而言内容充实度实在不能称为一本操作系统的教材,如果不是配合陈老师的课看的话很多地方根本就读不懂。虽然陈老师把课程的视频、讲义、配套实验都开源了,甚至还设了专门论坛,这一套下来肯定对得起书的价格,但就实际体验而言还是差点儿意思。比如如果汇编和组成原理学的不太好,又不会用linux的话做实验的时候根本就无从下手。毕竟是刚出的东西,再发展几年可能会变得更容易上手一些,希望后期可以发展到清华的ucore那样。

posted @ 2020-11-06 22:59  康宇PL  阅读(1045)  评论(1编辑  收藏  举报