Post

ChCore Lab2 Report

SJTU ChCore Lab2 思考题个人解答

ChCore Lab2 Report

实验记录

数据结构 - 嵌入式节点

ChCore 在实现链表(list)和红黑树(rbtree)均采用了嵌入式节点的方式,即节点信息与数据信息分离,节点信息作为成员变量嵌入到数据结构体中

e.g.

1
2
3
4
5
6
7
8
struct list_head {
    struct list_head *next, *prev;
};

struct data {
    int value;
    struct list_head list; // 嵌入式节点
}

由于在进行链表操作时,只能操作嵌入式节点,为了获取数据结构体的地址,ChCore 使用了 container_of 宏,该宏根据节点成员名 field 和目标类型 type 计算出数据结构体的相对编译量,并根据节点地址 ptr 还原出数据结构体地址

1
2
#define container_of(ptr, type, field) \
    ((type *)((void *)ptr - (void *)&(((type *)(0))->field)))

分离的优点是很明显的,在实际遍历链表时,只需要操作嵌入式节点即可,不需要关注数据结构体的具体类型,一定程度上可以简化代码并提高可读性

分离也存在缺点,首先是增加了代码复杂度,container_of 宏的实现比较复杂,不易理解(甚至是未定义行为 hhh);其次是遍历过程中也丢失了一些类型信息,程序员更难发现一些潜在的错误

物理内存管理 - Buddy & Slab

ChCore 的物理内存管理模型采用了 Buddy 和 Slab 相结合的方式, Buddy 负责大块内存,Slab 负责小块内存

虚拟内存管理 - 多级页表

ChCore 采用了与 AArch64 相似的四级页表实现虚拟内存管理

map

map_range_in_pgtbl_common 函数中,我们在调用 get_next_ptp 时设置 alloc 为真以自动申请页表页

需要注意:在处理 L3 即页表时,继续使用 alloc 为真会申请页表页,但是 L3 页表项只能映射物理页,不能再指向下一级页表,因此需要单独处理,否则会造成内存泄露

挑战题:重构内核页表

内核启动时,构建的内核页表是一个粗粒度的页表,使用了 2MB 和 1GB 的大页映射内核空间,我们需要将内核页表重构为细粒度的页表

小品环节


网上很多解法对物理页的权限设置并不正确,特别容易导致以下情况:VA 能被正确翻译,但由于页表项的权限设置不正确,触发了缺页异常,由于在这里内核的 irq 尚未初始化,导致内核在 0x200 处陷入异常处理死循环

为了处理权限问题,在触发异常后,需要检查 FAR_EL1ELR_EL1ESR_EL1MAIR_EL1SCTLR_EL1 寄存器,并结合新旧页表的权限位,分析具体的原因

然后,经过痛苦的调试(整整两天),你会发现:似乎 Lab2 中的页表项权限位与 Lab1 并不完全相同. (e.g., 0xffffff00006ab320 在 boot_ttbr1_l0 中的 AP 为 0, 为 L2-BLOCK 粒度; 而 0xffffff000009844c 的 AP 为 2, 为 L3-PAGE 粒度),这怎么跟 Lab1 说的不一样啊 QAQ。

使用逆向工具分析 mmu.o,得到逆向后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
void init_kernel_pt() {
    vaddr_t va;
    paddr_t pa;
    
    for (va = 0; va < SIZE_2M; va += SIZE_4K) {
        // int64_t pte = va | 0x60000000000f13;
        u64 idx = GET_L3_INDEX(va);
        pte_t pte;
        pte.pte = 0;

        pte.l3_page.UXN = 1;
        pte.l3_page.PXN = 1;
        pte.l3_page.nG = 1;
        pte.l3_page.AF = 1;
        pte.l3_page.SH = INNER_SHAREABLE;
        pte.l3_page.attr_index = NORMAL_MEMORY;
        pte.l3_page.pfn = va >> PAGE_SHIFT;
        pte.l3_page.is_valid = 1;
        pte.l3_page.is_page = 1;
        pte.l3_page.AP = AARCH64_MMU_ATTR_PAGE_AP_HIGH_RW_EL0_RW;

        if (img_start <= va && init_end > va
                && (boot_cpu_stack > va || (boot_cpu_stack + 0x4000) <= va)) {
            pte.l3_page.PXN = 0;
            pte.l3_page.AP = AARCH64_MMU_ATTR_PAGE_AP_HIGH_RO_EL0_RO;
        }
        boot_ttbr0_l3[idx] = pte.pte;
    }
    
    for (va = SIZE_2M; va < PERIPHERAL_BASE; va += SIZE_2M) {
        // boot_ttbr0_l2[GET_L2_INDEX(va)] = va | 0x40 0000 0000 0f11;
        u64 idx = GET_L2_INDEX(va);
        pte_t pte;
        pte.pte = 0;

        pte.l2_block.pfn = idx;
        pte.l2_block.PXN = 1;
        pte.l2_block.attr_index = NORMAL_MEMORY;
        pte.l2_block.SH = INNER_SHAREABLE;
        pte.l2_block.AF = 1;
        pte.l2_block.nG = 1;
        pte.l2_block.is_valid = 1;
        pte.l3_page.AP = AARCH64_MMU_ATTR_PAGE_AP_HIGH_RW_EL0_RW;

        boot_ttbr0_l2[idx] = pte.pte;
    }
    
    for (va = PERIPHERAL_BASE; va < PERIPHERAL_END; va += SIZE_2M) {
        // boot_ttbr0_l2[GET_L2_INDEX(va)] = va | 0x40000000000c01;
        u64 idx = GET_L2_INDEX(va);
        pte_t pte;
        pte.pte = 0;

        pte.l2_block.pfn = idx;
        pte.l2_block.PXN = 1;
        pte.l2_block.attr_index = DEVICE_MEMORY;
        pte.l2_block.AF = 1;
        pte.l2_block.nG = 1;
        pte.l2_block.is_valid = 1;
        pte.l3_page.AP = AARCH64_MMU_ATTR_PAGE_AP_HIGH_RW_EL0_RW;

        boot_ttbr0_l2[idx] = pte.pte;
    }
    
    BUG_ON((u64)(&_text_end) >= KERNEL_VADDR + SIZE_2M);
    BUG_ON((u64)(&_text_start) % SIZE_4K != 0 || (u64)(&_text_end) % SIZE_4K != 0);
    
   for (pa = 0; pa < SIZE_2M; pa += SIZE_4K) {
       va = pa + KERNEL_VADDR;
       uint64_t idx = GET_L3_INDEX(va);
       pte_t pte;
       pte.pte = 0;

       pte.l3_page.UXN = 1;
       pte.l3_page.PXN = 1;
       pte.l3_page.AF = 1;
       pte.l3_page.SH = INNER_SHAREABLE;
       pte.l3_page.attr_index = NORMAL_MEMORY;
       pte.l3_page.pfn = pa >> PAGE_SHIFT;
       pte.l3_page.is_valid = 1;
       pte.l3_page.is_page = 1;
       pte.l3_page.AP = AARCH64_MMU_ATTR_PAGE_AP_HIGH_RW_EL0_RW;
       
       if (_text_start <= va && _text_end > va) {
           pte.l3_page.PXN = 0;
           pte.l3_page.AP = AARCH64_MMU_ATTR_PAGE_AP_HIGH_RO_EL0_RO;
       }
       boot_ttbr1_l3[idx] = pte.pte;
   }
   
   for (; pa < PERIPHERAL_BASE; pa += SIZE_2M) {
       va = pa + KERNEL_VADDR;
       u64 idx = GET_L2_INDEX(va);
       pte_t pte;
       pte.pte = 0;

       pte.l2_block.pfn = idx;
       pte.l2_block.UXN = 1;
       pte.l2_block.attr_index = NORMAL_MEMORY;
       pte.l2_block.SH = INNER_SHAREABLE;
       pte.l2_block.AF = 1;
       pte.l2_block.is_valid = 1;
       pte.l2_block.AP = AARCH64_MMU_ATTR_PAGE_AP_HIGH_RW_EL0_RW;

       boot_ttbr1_l2[idx] = pte.pte;
   }
   
   for (pa = PERIPHERAL_BASE; pa < PERIPHERAL_END; pa += SIZE_2M) {
       va = pa + KERNEL_VADDR;
       u64 idx = GET_L2_INDEX(va);
       pte_t pte;
       pte.pte = 0;

       pte.l2_block.pfn = idx;
       pte.l2_block.UXN = 1;
       pte.l2_block.attr_index = DEVICE_MEMORY;
       pte.l2_block.AF = 1;
       pte.l2_block.is_valid = 1;
       pte.l2_block.AP = AARCH64_MMU_ATTR_PAGE_AP_HIGH_RW_EL0_RW;

       boot_ttbr1_l2[idx] = pte.pte;
   }

   pa = 0x40000000UL;
   va = pa + KERNEL_VADDR;
   u64 idx = GET_L1_INDEX(va);
   pte_t pte;
   pte.pte = 0;
   
   pte.l1_block.pfn = idx;
   pte.l1_block.UXN = 1;
   pte.l1_block.attr_index = DEVICE_MEMORY;
   pte.l1_block.AF = 1;
   pte.l1_block.is_valid = 1;
   pte.l1_block.AP = AARCH64_MMU_ATTR_PAGE_AP_HIGH_RW_EL0_RW;

   boot_ttbr0_l1[idx] = pte.pte;
}

可以看到,确实与 Lab1 说的不一样(掀桌),Lab2 中将虚拟内存设置如下:

  • 低半区:
    1. [0x00000000, 0x00200000): 4KB 页,大部分 RW 但不可执行,只有 image 和 init 段(剔除 CPU 栈区)是 RO 且 EL1 可执行
    2. [0x00200000, 0x3F000000): 2MB 页,RW,EL1 不可执行,EL0 可执行
    3. [0x3F000000, 0x3FFFFFFF]: 2MB 页,设备内存,RW,EL1 不可执行,EL0 可执行
  • 高半区 (请自动加上内核基址):
    1. [0x00000000, 0x00200000): 4KB 页,大部分 RW 但不可执行,只有 text 段是 RO 且 EL1 可执行
    2. [0x00200000, 0x3F000000): 2MB 页,RW,EL1 可执行,EL0 不可执行
    3. [0x3F000000, 0x3FFFFFFF]: 2MB 页,设备内存,RW,EL1 可执行,EL0 不可执行
    4. [0x40000000, 0xFFFFFFFF]: 1GB 页,设备内存,RW,EL1 可执行,EL0 不可执行

进一步摸查代码中的几个段:

  • text 段为 0xffffff0000090000 - 0xffffff00000aa000
  • img_start: 0x80000
  • init_end: 0x90000
  • boot_cpu_stack: 0x8b000 - 0x8f000

至此,经过一个周末的反复折磨,我们终于摸清了 Lab2 的虚拟内存布局是怎么一回事了,可以安心地进行内核页表重构了

main 中,我们能看到一段映射 CPU 0 内核栈页表的代码:

1
2
3
4
5
6
7
8
9
void main(paddr_t boot_flag, void *info) {
    ...
	/* Mapping KSTACK into kernel page table. */
	map_range_in_pgtbl_kernel((void*)((unsigned long)boot_ttbr1_l0 + KBASE), 
		KSTACKx_ADDR(0),
		(unsigned long)(cpu_stacks[0]) - KBASE, 
		CPU_STACK_SIZE, VMR_READ | VMR_WRITE);
    ...
}

我们需要做的就是将这一段前,新建一个页表,完成映射,然后通过 msr 指令将新的页表基址写入 ttbr1_el1 寄存器. 注意构建新的页表后,每个 CPU 的 KSTACK,都使用新的页表基址而非 boot_ttbr1_l0

1
2
3
4
5
6
7
8
9
10
11
12
13
void main(paddr_t boot_flag, void *info) {
    ...
	/* Mapping KSTACK into kernel page table. */
	map_range_in_pgtbl_kernel((void*)((unsigned long)boot_ttbr1_l0 + KBASE), 
			KSTACKx_ADDR(0),
			(unsigned long)(cpu_stacks[0]) - KBASE, 
			CPU_STACK_SIZE, VMR_READ | VMR_WRITE);

    /* Remap kernel space to thinner granularity */
	remap_kernel_pt();
    kinfo("[ChCore] remap kernel page table finished\n");
    ...
}

remap_kernel_pt 函数的具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#define DATA_START      (0x0UL)
#define DATA_END        (0x001FFFFFUL)
#define TEXTSEC_START   (virt_to_phys(&_text_start))
#define TEXTSEC_END     (virt_to_phys(&_text_end))
#define TEXT_START      (0x00200000UL)
#define TEXT_END        (0x3EFFFFFFUL)
#define DEVICE_START    (0x3F000000UL)
#define PHYSMEM_END     (0xFFFFFFFFUL)

#define KMAP(pgtbl, p_start, p_end, flags) \
    map_range_in_pgtbl_kernel((void *)(pgtbl), \
        phys_to_virt(p_start), p_start, p_end - p_start, flags \
    )
    
vaddr_t volatile pgtbl;

void remap_kernel_pt() {
    // construct new page table
    pgtbl = (vaddr_t)get_pages(0);
    memset((void *)pgtbl, 0, PAGE_SIZE);
    // CPU 0 KSTACK SECTION
    map_range_in_pgtbl_kernel((void *)pgtbl, 
        KSTACKx_ADDR(0), virt_to_phys(cpu_stacks[0]), CPU_STACK_SIZE, 
        VMR_READ | VMR_WRITE);
    // DATA SECTION
    /* 0x90000 - 0xaa000 is the READ ONLY and EXECUTABLE section */
    KMAP(pgtbl, DATA_START,     TEXTSEC_START,  VMR_READ | VMR_WRITE);
    KMAP(pgtbl, TEXTSEC_START,  TEXTSEC_END,    VMR_READ | VMR_EXEC);
    KMAP(pgtbl, TEXTSEC_END,    DATA_END,       VMR_READ | VMR_WRITE);
    // TEXT SECTION (0x00200000 - 0x3f000000)
    KMAP(pgtbl, TEXT_START,     TEXT_END,       VMR_READ | VMR_WRITE | VMR_EXEC);
    // DEVICE SECTION (0x3f000000 - 0xffffffff)
    KMAP(pgtbl, DEVICE_START,   PHYSMEM_END,     VMR_DEVICE | VMR_READ | VMR_WRITE | VMR_EXEC);
    // write pa to register
    dsb(ishst);
    isb();
    asm volatile("msr ttbr1_el1, %0" : : "r" (virt_to_phys(pgtbl)));
    flush_tlb_all();
}

至此,长达四天的调试之旅终于结束了(悲)

还有小品环节?


完成所有挑战后,实际测试中发现,当开启 PM USAGE TEST 后,内核会莫名陷入无限缺页异常调用直到暴栈

甚至更进一步,当 DEBUG 开关开启时,内核正常工作,但是当关闭 DEBUG 开关时,内核就会陷入无限缺页异常调用直到暴栈

毁灭吧,我不会了 QAQ

这里是后期

经过摸查,发现不同编译开关会导致 text 段的变化,因此需要使用 boot.h 中的 _text_start_text_end 来动态获取 text 段的边界, 也就是上面代码中的 TEXTSEC_START1TEXTSEC_END 的由来。

缺页处理

在 Lab 2 中,我们并不关注异常初步处理及转发的内容,只关注缺页异常的处理本身

ChCore 中,一个进程的虚拟空间 (vmspace) 是由多段虚拟内存段 (VMR/VMA, vmregion) 构成,每一段虚拟内存段都对应一个物理内存对象(PMO),PMO 记录了物理地址的相关信息

因此缺页异常的处理过程应该是:

  1. 根据异常发生的虚拟地址,找到对应的虚拟内存段
  2. 根据虚拟内存段,找到对应的物理内存对象并找到需要映射的物理页
  3. 处理缺页异常,更新页表

缺页处理主要针对 PMO_SHMPMO_ANONYM 两种类型的 PMO,这两种类型的 PMO 都是根据访问按需分配物理页的

挑战题:RSS 监控

RSS: Resident Set Size, 常驻集大小,用来统计 map 映射中实际的物理内存大小

在处理缺页异常时,需要更新进程对应的 RSS 信息,涉及对 map, unmaphandle_pgfault 函数的修改

思考题

[1. ]

为了简单起见,在 ChCore 实验 Lab1 中没有为内核页表使用细粒度的映射,而是直接沿用了启动时的粗粒度页表,请思考这样做有什么问题。

只看低 8 位,Lab1 中内核页 0x00000000 - 0x40000000 以 2MB 的粒度统一映射,0x40000000 - 0x80000000 以 1GB 的粒度统一映射, 这么做的好处是简单,节省内存,同时提高 TLB 命中率

但是可以发现,实际上 0x00000000 - 0x40000000 总共 1GB 的空间中,是由 512 块 2MB 的页组成的,而 0x40000000 - 0x80000000 总共 1GB 的空间中,是由 1 块 1GB 的页组成的。

  • 一方面,大的物理块通常不会用满,导致内部碎片,浪费内存
  • 另一方面,粗粒度的页表缺少详细的权限控制,降低了内存访问的安全性
  • 此外,粗粒度的块进行拆分和合并的开销较大,降低了内存分配的灵活性

[2. ]

阅读 Arm Architecture Reference Manual,思考要在操作系统中支持写时拷贝(Copy-on-Write,CoW)需要配置页表描述符的哪个/哪些字段,并在发生页错误时如何处理。(在完成第三部分后,你也可以阅读页错误处理的相关代码,观察 ChCore 是如何支持 Cow 的)

AP 控制虚拟页在不同特权级别下的读写权限,为了支持写时拷贝,需要控制对应虚拟页为只读 (Read Only)。

触发 CoW 的 page fault 的大致流程如下:

  1. 检查 ESR_EL1 寄存器内容
  2. 如果是 WXN(禁止写),根据虚拟地址找到对应的 VMR
  3. 检查 VMR 的权限设置,如果是一个只读 VMR,且 VMR 允许写时复制,则触发 CoW,进行 perm fault 处理
    1. 获取出错地址和对应物理页,并获取物理页对应的内核页
    2. 分配一个新页并在记录在 VMR 中
    3. 将内核页复制到新的页
    4. 更新 VMR 的权限
    5. 更新页表表项权限和对应物理页号
    6. 清空用户虚拟页的 TLB
This post is licensed under CC BY 4.0 by the author.

Trending Tags