ChCore Lab1 机器启动

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

电子书目录: 《现代操作系统:原理与实现》
PPT及课程地址:SE315 /2021 /Welcome

Exercise one

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

简单总结了后面会用到的几条指令,其实只需要后面看汇编代码的时候,把《ARM指令集参考指南》当字典查一查即可。

bl:Branch with Link

mrs: Move System Register allows the PE to read an AArch64 System register into a general-purpose register.

stp:Store Pair of Registers calculates an address from a base register value and an immediate offset, andstores two 32-bit words or two 64-bit doublewords to the calculated address, from two registers.

Exercise two

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

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

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

Exercise three

请找出请找出build/kernel.image入口定义在哪个文件中。结合boot/start.S中的启动代码,说明挂起其他处理器的控制流。

init              PROGBITS         0000000000080000  00010000

可见.init 段的地址是 080000,和上面Bootloader的地址相同,可以发现入口被定义在 start.S

BEGIN_FUNC(_start)
	mrs	x8, mpidr_el1 //记录此时运行的cup id
	and	x8, x8,	#0xFF
	cbz	x8, primary //如果为0,证明是主cpu,跳转到primary

  /* hang all secondary processors before we intorduce multi-processors */
secondary_hang:
	bl secondary_hang // 如果不是零,则为其他CPU,进入死循环挂起

Exercise four

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

Sections:
Idx Name          Size      VMA               LMA               
  0 init          0000b5b0  0000000000080000  0000000000080000  
  1 .text         000011dc  ffffff000008c000  000000000008c000  
  2 .rodata       000000f8  ffffff0000090000  0000000000090000  
  3 .bss          00008000  ffffff0000090100  0000000000090100  
  4 .comment      00000032  0000000000000000  0000000000000000

可见.init段的VMA和LMA相同,而其他段VMA和LMA则有0xffffff00的偏差。

LMA(Load Memory Address): 将可执行文件从硬盘装载到内存,存入的地址即LMA

VMA(Virtual Memory Address): 当启用MMU之后,系统中就有了虚拟地址和物理地址。程序运行前,要将程序内容拷贝到相应的地址再运行。程序运行时的地址,就是VMA

因此在多数情况下,VMA和LMA是相等的,即程序被装载内存的某个地址,就在这个地址执行。

.init段执行时,MMU还没有启动,没有虚拟地址,因此LMA和VMA相同。
而其他段则是kernel代码,操作系统kernel一般运行在虚拟地址的高位,但一开始bootloader运行时处于实模式,无法访问0xffffff0000000000以上的内存区域,因此将代码装载入此时的LMA。当进入保护模式后,再将对应的内核代码映射到高地址段,即VMA,再运行。

Exercise five

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

对输入的数字进行进制转换

//进制转换
	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;
	}

Exercise six

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

在start.S文件中可以找到对sp的相关定义

/* Prepare stack pointer and jump to C. */
	adr 	x0, boot_cpu_stack  //读入boot_cpu_stack地址
	add 	x0, x0, #0x1000			//将地址指向0x1000,即4096字节
	mov 	sp, x0							//将地址传递给栈指针sp

在init.c中定义了boot_cpu_stack

#define INIT_STACK_SIZE 0x1000
char boot_cpu_stack[PLAT_CPU_NUMBER][INIT_STACK_SIZE] ALIGN(16);

内核初始化即将sp指向cpu_stack的4096字节处。因为栈从高地址向低地址增长,因此前4096字节即是分配给栈的。

Exercise seven

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

在gdb中对stack_test打入断点,开始运行。
因为在ARM中栈指针SP寄存器为sp,帧指针FP寄存器为x29,而返回地址保存在链接寄存器LR中,即x30,因此观察反汇编代码

(gdb) b stack_test
Breakpoint 1 at 0xffffff000008c03c
(gdb) c
Continuing.

Thread 1 hit Breakpoint 1, 0xffffff000008c03c in stack_test ()
(gdb) x/10g $x29
0xffffff00000920d0 <kernel_stack+8144>:	-1099511029760	-1099511054124
0xffffff00000920e0 <kernel_stack+8160>:	0	4294967232
0xffffff00000920f0 <kernel_stack+8176>:	0	-1099511054312
0xffffff0000092100 <kernel_stack+8192>:	0	0
0xffffff0000092110 <kernel_stack+8208>:	0	0
(gdb) c
Continuing.

Thread 1 hit Breakpoint 1, 0xffffff000008c03c in stack_test ()
(gdb) x/10g $x29
0xffffff00000920b0 <kernel_stack+8112>:	-1099511029760	-1099511054224
0xffffff00000920c0 <kernel_stack+8128>:	5	4294967232
0xffffff00000920d0 <kernel_stack+8144>:	-1099511029760	-1099511054124
0xffffff00000920e0 <kernel_stack+8160>:	0	4294967232
0xffffff00000920f0 <kernel_stack+8176>:	0	-1099511054312
(gdb) c
Continuing.

Thread 1 hit Breakpoint 1, 0xffffff000008c03c in stack_test ()

...

(gdb) x/30i stack_test
   0xffffff000008c020 <stack_test>:	stp	x29, x30, [sp, #-32]!  //将FP,LR压栈
   0xffffff000008c024 <stack_test+4>:	mov	x29, sp //将FP更新为当前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	0xffffff000008c620 <printk>
   0xffffff000008c040 <stack_test+32>:	cmp	x19, #0x0
   0xffffff000008c044 <stack_test+36>:
    b.gt	0xffffff000008c068 <stack_test+72>
   0xffffff000008c048 <stack_test+40>:
    bl	0xffffff000008c0dc <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	0xffffff000008c620 <printk>
   0xffffff000008c05c <stack_test+60>:	ldr	x19, [sp, #16]
   0xffffff000008c060 <stack_test+64>:	ldp	x29, x30, [sp], #32 //读取FP,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>:	mov	x1, x19

因此,当调用stack_test时,SP首先将之前函数的返回地址LR压栈,再将之前的栈底FP压栈,然后压入函数的局部变量,调用printk,因此会压入四个64位地址。

Exercise eight

回溯函数所需的信息(如SP、FP、LR、参数、部分寄存器值等)在栈中具体保存的位置在哪?请画出AArch64函数调用的栈帧示意图。

AArch64函数调用的栈帧示意图如下图所示

|           | High Address
|           |
|           |             +
+-----------+             |
|LR         |             |
+-----------+             |
|Father's FP| <-------+   |
+-----------+         |   |
|other Data |         |   |
+-----------+         |   |
|Local Arg  |         |   |
+-----------+         |   |
|......     |         |   |
|           |         |   |
|           |         |   |
|......     |         |   |
+-----------+         |   |
|LR         |         |   |
+-----------+         |   v
|Father's FP| --------+
+-----------+
|other Data |
+-----------+
|Local Arg  |
+-----------+ Low Address
|......     |
|           |

Exercise nine

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

实现stack_backtrace如下,当FP指向的地址中的值位0时,递归终止:

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

	u64* fp=(u64*)(*(u64*)read_fp());	// 输出的FP为调用stack_backtrace的函数的FP,故加一层间接访问
	while(true){
        // 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");
		if(*fp == 0) //此时没有父函数,递归终止
			break;
		fp = (u64*) *fp; // FP是一个地址,沿着FP递归访问
	}

	return 0;
}

总结

计算机是如何从通上电,到唤醒操作系统,开启登录界面的我一直留有疑惑,这是在大多数有关操作系统的书中都不会涉及的内容。而lab1深入到了操作系统的启动环节,让我明白了在通电之后操作系统是如何激活各个模块的,确实很有收获。

上一篇:uiscrollview 事件冲突


下一篇:Java Lab1 Problem5--EBU4201 Lab1 答案与解析