当前博文已经废弃,请看最新版的: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那样。