本文为阅读上海交大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深入到了操作系统的启动环节,让我明白了在通电之后操作系统是如何激活各个模块的,确实很有收获。