當前博文已經廢棄,請看最新版的: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那樣。