笔记03.5 - Lab 1:Jos内核

在保护模式下线性地址 = GDT 表项中段基址 + 偏移地址,而实际上指针的值通常就是偏移地址。在 Boot Loader 完成了将内核可执行文件加载到内存中的工作后,将用 ((void (*)(void)) (ELFHDR->e_entry))(); 这一句代码执行指令的跳转。内核程序在 kern/entry.S 中设置 CR0_PG 标识符,虚存硬件开始将线性地址映射为物理地址。在标识符设置之前,由于没有开启分页,逻辑地址通过段映射得到的线性地址都被当作物理地址。开启分页后,内核程序使用 [KERNBASE, KERNBASE+4MB) 的线性地址,该地址范围会被映射到物理地址 [0, 4MB),线性地址 [0, 4MB) 也会映射到物理地址 [0, 4MB) ,其中 KERNBASE = 0xF0000000。

也就是说,boot/boot.S切换到保护模式之后,开始了逻辑地址到线性地址的转换,但是没有开启分页,线性地址被当为物理地址,所以一般使用的是低地址。kern/entry.S开启分页之后,开始在保护模式下使用高线性地址,因此需要加载页表。此时还没开始内存管理,因此页表是手工静态文件。

部分内核代码解析

首先来看看 kern/entry.S 文件。

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
/* See COPYRIGHT for copyright information. */

#include <inc/mmu.h>
#include <inc/memlayout.h>

# Shift Right Logical
#define SRL(val, shamt) (((val) >> (shamt)) & ~(-1 << (32 - (shamt))))


###################################################################
# The kernel (this code) is linked at address ~(KERNBASE + 1 Meg),
# but the bootloader loads it at address ~1 Meg.
#
# RELOC(x) maps a symbol x from its link address to its actual
# location in physical memory (its load address).
###################################################################

#define RELOC(x) ((x) - KERNBASE)

#define MULTIBOOT_HEADER_MAGIC (0x1BADB002)
#define MULTIBOOT_HEADER_FLAGS (0)
#define CHECKSUM (-(MULTIBOOT_HEADER_MAGIC + MULTIBOOT_HEADER_FLAGS))

###################################################################
# entry point
###################################################################

.text

# The Multiboot header
.align 4
.long MULTIBOOT_HEADER_MAGIC
.long MULTIBOOT_HEADER_FLAGS
.long CHECKSUM

# '_start' specifies the ELF entry point. Since we haven't set up
# virtual memory when the bootloader enters this code, we need the
# bootloader to jump to the *physical* address of the entry point.
.globl _start
_start = RELOC(entry)

.globl entry
entry:
movw $0x1234,0x472 # warm boot

# We haven't set up virtual memory yet, so we're running from
# the physical address the boot loader loaded the kernel at: 1MB
# (plus a few bytes). However, the C code is linked to run at
# KERNBASE+1MB. Hence, we set up a trivial page directory that
# translates virtual addresses [KERNBASE, KERNBASE+4MB) to
# physical addresses [0, 4MB). This 4MB region will be
# sufficient until we set up our real page table in mem_init
# in lab 2.

# 注意到内核代码的执行是从KERNBASE+1MB开始的,而不是KERNBASE!!!
# 由于虚拟内存机制还没建立,所以在kern/entrypgdir.c里面手写了4MB的页表,
# 把[0,4MB)物理地址同时映射到线性地址[0, 4MB)和[KERNBASE, KERNBASE+4MB)中

# Load the physical address of entry_pgdir into cr3. entry_pgdir
# is defined in entrypgdir.c.
# 此时还没开启分页,需要RELOC将线性地址转化为物理地址
movl $(RELOC(entry_pgdir)), %eax
movl %eax, %cr3
# Turn on paging.
movl %cr0, %eax
orl $(CR0_PE|CR0_PG|CR0_WP), %eax
movl %eax, %cr0

# Now paging is enabled, but we're still running at a low EIP
# (why is this okay?). Jump up above KERNBASE before entering
# C code.
# 线性地址[0, 4MB)和[KERNBASE, KERNBASE+4MB)同时映射到[0,4MB)物理地址
# 如果不映射线性地址[0, 4MB),则low eip的线性地址不在可访问线性地址范围内,将出现非法访问
mov $relocated, %eax
jmp *%eax # jmp 之后eip将会加上KERNBASE !!!
relocated:

# Clear the frame pointer register (EBP)
# so that once we get into debugging C code,
# stack backtraces will be terminated properly.
movl $0x0,%ebp # nuke frame pointer

# 可以看到在这里定义了两个全局变量 bootstack 和 bootstacktop, bootstack 标识了内存中
# 的一个位置,表示从这里开始的 KSTKSIZE 个字节的区域都是属于这个临时堆栈的
# (KSTKSIZE 在 inc/memlayout.h 中定义为 32k),而 bootstacktop 则指向的是这段区域后的第
# 一个字节,由于刚开始的时候堆栈是空的,所以栈顶便是 bootstacktop 所指向的位置,于是
# 程序便将 bootstacktop 的值赋给了 esp 寄存器。该位置位于.data节内。

# Set the stack pointer
movl $(bootstacktop),%esp

# now to C code
call i386_init

# Should never get here, but in case we do, just spin.
spin: jmp spin


.data
###################################################################
# boot stack
###################################################################
.p2align PGSHIFT # force page alignment
.globl bootstack
bootstack:
.space KSTKSIZE
.globl bootstacktop
bootstacktop:

在初始化堆栈指针后,程序调用了 i386_init 函数,这个函数是在 kern/init.c 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void
i386_init(void)
{
extern char edata[], end[];

// 在完成任何事之前,先完成 ELF 的加载过程。清除未初始化的全局数据 (BSS) 节,确保所有 static/global 变量置零。
// 可以看到两个外部字符数组变量 edata 和 end,其中 edata 表示的是 bss 节在内存中开始的位置,而 end 则是表示内核可执行程序在内存中结束的位置。由 2.1 节中对 ELF 文件的讲解我们可以知道 bss 节是文件在内存中的最后一部分,于是 edata 与 end 之间的部分便是 bss 节的部分,我们又知道 bss 节的内容是未初始化的变量,而这些变量是默认为零的,所以在一开始的时候程序要用 memset(edata, 0, end - edata)这句代码将这些变量都置为零。
memset(edata, 0, end - edata);

// 初始化控制台,有显存的初始化、键盘的初始化之类的
// Can't call cprintf until after we do this!
cons_init();
// 测试输出
cprintf("6828 decimal is %o octal!\n", 6828);
// 通过堆栈来对函数调用进行回溯(lab 1 only)
test_backtrace(5);

// Drop into the kernel monitor.
// 无限循环的调用了 monitor 函数,这个函数的原型在 kern/monitor.c 中,它的功能是提示用户输入命令与操作系统进行交互。
while (1)
monitor(NULL);
}

定义在 kern/monitor.c 中的 runcmd 函数:

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
static int
runcmd(char *buf, struct Trapframe *tf)
{
int argc;
char *argv[MAXARGS];
int i;

// Parse the command buffer into whitespace-separated arguments
argc = 0;
argv[argc] = 0;
while (1) {
// gobble whitespace
while (*buf && strchr(WHITESPACE, *buf))
*buf++ = 0; //把所有空格字符都置为空字符
if (*buf == 0)
break;// 命令结束

// save and scan past next arg
if (argc == MAXARGS-1) {
cprintf("Too many arguments (max %d)\n", MAXARGS);
return 0;// 参数个数超过最大个数的限制
}
argv[argc++] = buf;// 指向相应的字符串
while (*buf && !strchr(WHITESPACE, *buf))
buf++; // 跳过非空格的字符
}
argv[argc] = 0;

// Lookup and invoke the command
if (argc == 0)
return 0;
for (i = 0; i < NCOMMANDS; i++) {
if (strcmp(argv[0], commands[i].name) == 0)
return commands[i].func(argc, argv, tf);
}
cprintf("Unknown command '%s'\n", argv[0]);
return 0;
}

程序让指针指向了每个子字符串并且把命令字符串中的空格都换成了空字符, 因为用户在输入命令的时候, 命令名和参数之间, 参数和参数之间都是由空格相隔开的,这样处理后每个子字符串的结尾便都是一个空字符,便可以方便之后读取这个字符串,就如下图所示:

堆栈分析

先进后出是堆栈的特点,内存中栈顶在内存的低地址处,而栈底是在高地址处。栈底是固定的,栈顶是可以变化的。假设我们需要堆栈出栈一个字的数据,即 4 个字节,这个时候我们需要当前 esp 指向位置开始的 4 个字节读出来,并且在这之后把 esp 加 4。当需要进栈一个字的数据时,将 esp 减 4,并把这个字存放在 esp 指向位置开始的 4 个字节处。


关键的寄存器: eip 存储当前执行指令的下一条指令在内存中的偏移地址, esp 存储指向栈顶的指针,而 ebp 则是存储指向当前函数需要使用的参数的指针。在程序中,如果需要调用一个函数,首先(1)会将函数需要的参数进栈,然后(2)将 eip 中的一个字进栈,也就是下一条指令在内存中的位置,这样在函数调用结束后便可以通过堆栈中的 eip 值返回调用函数的程序(CALL将下一条指令的CS:EIP压入堆栈,但真实的情况是,下一条指令的地址压入堆栈,但EIP装入跳转函数的地址)。而在一进入调用函数的时候,第一件事便是(3)将 ebp 进栈(调用本函数的过程的栈指针),然后将当前的 esp 的值赋给 ebp。(4)ebp 以下部分一般作为临时数据区(esp 下调),包含本函数要调用的过程的参数的空间。
当前函数的 ebp 设置在 3 和 4 之间的地址,因此 0x0[%ebp] 可以读出上个函数的栈指针, 0x4[%ebp] 可以读出返回地址 %eip, 0x8[%ebp] 可以读出第一个参数……

显示 Gitment 评论