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那樣。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM