进入内核后,上篇内容已经将汇编部分讲完了,接下来讲解 C 语言部分,即 i386_init,在这段代码中会初始化控制台和内存,随后进入一个无限循环,等待用户的命令。例如用户输入 help 会触发相应的函数,控制台输出相应的内容。
i386_init
下面是 i386_init 对应的代码,随后是对这段代码的详细解释:
void
i386_init(void)
{
extern char edata[], end[];
// 在执行其他任何操作之前,完成ELF加载过程。
// 清除程序的未初始化全局数据(BSS)段。
// 这确保了所有静态/全局变量都从零开始。
memset(edata, 0, end - edata);
// 初始化控制台。
// 在执行这个操作之前,不能调用cprintf函数!
cons_init();
mem_init();
// 进入内核监视器。
// 无限循环,等待监视器的命令。
while (1) {
monitor(NULL);
}
}
-
extern char edata[], end[];
:这两个变量是由链接器提供的,表示程序的数据段(包括初始化的全局变量)和 BSS 段(包括未初始化的全局变量)的结束地址。edata
指向数据段的结束地址,end
指向 BSS 段的结束地址。 -
memset(edata, 0, end - edata);
:这行代码将 BSS 段的所有字节设置为 0。这是因为 C 语言规定,未初始化的全局变量和静态变量应该被初始化为 0。这里的end - edata
计算的是 BSS 段的大小。
下面是 edata 和 end 这两个变量在链接器中的定义:
.bss : {
PROVIDE(edata = .);
*(.bss)
PROVIDE(end = .);
BYTE(0)
}
这段代码定义了一个名为 .bss
的段,这个段用于存储程序的未初始化的全局变量和静态变量。
PROVIDE(edata = .);
这行代码定义了一个符号 edata
,并将其设置为当前位置(.
表示当前位置)。在链接器脚本中,.
代表当前输出段的位置计数器,也就是当前已经输出到的地址。因此,edata
的值就是 .bss
段的开始地址。
*(.bss)
这行代码将所有输入文件中的 .bss
段合并到输出文件的 .bss
段中。*
是通配符,表示所有的文件,.bss
表示 .bss
段。
PROVIDE(end = .);
这行代码定义了一个符号 end
,并将其设置为当前位置。因为这行代码在 *(.bss)
之后,所以 end
的值就是 .bss
段的结束地址。
BYTE(0)
这行代码在 .bss
段的末尾添加了一个字节的空间,并将其初始化为 0。这是为了确保 .bss
段在内存中的实际大小至少为一个字节,即使在源代码中没有任何未初始化的全局变量或静态变量。
总的来说,这段代码的作用是设置 .bss
段的开始和结束地址,并将所有输入文件中的 .bss
段合并到一起。
-
cons_init();
:这个函数用于初始化控制台。在这个函数被调用之前,不能使用cprintf
函数,因为cprintf
函数依赖于控制台的初始化。 -
mem_init();
:这个函数用于初始化内存管理系统。在这个函数被调用之后,操作系统就可以正常地分配和释放内存了。下一章回详细讲解这部分实现细节。 -
while (1) { monitor(NULL); }
:这是一个无限循环,它会不断地调用monitor
函数。monitor
函数是一个简单的命令行界面,它会等待用户输入命令,然后执行相应的操作。这个无限循环保证了,即使monitor
函数返回,操作系统也不会退出,而是继续等待下一个命令。
内存布局
下面展现了一个进程在内存中的布局,结合内存布局讲解 BSS 段和数据段。
高地址 ---> .----------------------.
| Environment |
|----------------------|
| | 在栈上声明的函数和变量
| STACK | 栈上的空间被基指针(base pointer)指示。
base pointer -> | - - - - - - - - - - -|
| | |
| v |
: :
. . 栈向未使用的空间方向增长,而堆向上增长。
. 空 .
. .
. . (这里可能会发生其他内存映射,如动态库,不同的内存分配方式)
. .
: :
| ^ |
| | |
brk point -> | - - - - - - - - - - -| 堆上声明动态内存
| HEAP |
| |
|----------------------|
| BSS | 未初始化数据 (BSS)
|----------------------|
| Data | 初始化数据 (DS)
|----------------------|
| Text | 二进制代码
低地址 ----> '----------------------'
其中数据段和 BSS 段是程序内存布局的两个重要部分。
-
数据段:数据段主要用于存储程序的全局变量和静态变量。这些变量在程序编译时就已经确定了初始值。例如,如果你在程序中定义了一个全局变量
int g_var = 10;
,那么这个变量就会被存储在数据段中,其初始值为 10。 -
BSS 段:BSS 段用于存储未初始化的全局变量和静态变量。在 C 语言中,如果全局变量和静态变量在声明时没有显式初始化,那么它们会被自动初始化为 0。这些变量就会被存储在 BSS 段中。例如,如果你在程序中定义了一个全局变量
int g_var;
,那么这个变量就会被存储在 BSS 段中,其初始值为 0。
这两个段的主要区别在于,数据段中的变量有初始值,而 BSS 段中的变量没有初始值。在程序加载到内存时,操作系统会为数据段和 BSS 段分配内存空间,并将数据段的变量初始化为预设的值,将 BSS 段的变量初始化为 0。
堆栈的布局
在 x86 架构中,esp(堆栈指针寄存器)和 ebp(基指针寄存器)是两个非常重要的寄存器,它们在函数调用和堆栈操作中起着关键的作用。
-
esp(堆栈指针寄存器):esp 寄存器指向当前堆栈的顶部。当我们向堆栈中压入数据时,esp 的值会减小(因为在 x86 架构中,堆栈是向下增长的),当我们从堆栈中弹出数据时,esp 的值会增大。因此,esp 总是指向当前堆栈帧的顶部。
-
ebp(基指针寄存器):ebp 寄存器通常用作帧指针,指向当前堆栈帧的底部。在函数调用时,ebp 的值会被压入堆栈,然后 esp 的当前值会被复制到 ebp,这样 ebp 就指向了新的堆栈帧的底部。在函数返回时,ebp 的值会被恢复,指向上一个堆栈帧的底部。
这两个寄存器的使用使得我们可以在堆栈中有效地定位和访问数据。例如,我们可以通过 ebp 来访问函数的参数和局部变量(它们都存储在堆栈中),而 esp 则用于管理堆栈的增长和缩小。
下面表示在 x86 架构中函数调用时,堆栈的布局。
+---------------------+
| Function Call |
|---------------------|
| ... |
| Previous Frame | <- ebp points here (bottom of the current stack frame)
| Local Variables |
| ... |
| |
|---------------------|
| Return Address |
|---------------------|
| Parameters |
| ... |
| |
|---------------------|
| Old EBP |
|---------------------|
| Local Data |
| ... |
| |
|---------------------|
| ... |
| |
|---------------------|
| ... |
| |
|---------------------|
| New Stack Frame |
| |
|---------------------|
| ... |
| Previous ESP Value | <- esp points here (top of the current stack frame)
+---------------------+
这样的结构使得函数调用和返回可以方便地在堆栈上进行,以及访问局部变量和控制程序的流程。
在 x86 架构中,当一个函数被调用时,它会将当前的 ebp 寄存器的值(也就是上一个函数的 ebp 值)保存(push)到堆栈中,然后将 esp(堆栈指针)寄存器的值复制到 ebp 寄存器,这样 ebp 就指向了新的堆栈帧的底部。因此,堆栈中保存的 ebp 值实际上形成了一个链表,每一个 ebp 值都指向了上一个函数的堆栈帧。 因此,我们可以通过从当前的 ebp 值开始,逐个追踪保存在堆栈中的 ebp 值,来回溯整个堆栈,这就是所谓的"通过跟踪保存的 ebp 指针链回溯堆栈"。这种技术常常被用于调试和错误诊断,即 backtrace 。例如,当程序崩溃时,我们可以通过回溯堆栈来找出导致崩溃的函数调用序列。
ebp(基指针寄存器)本身不是一个链表。然而,在函数调用过程中,ebp 寄存器的值会被保存在堆栈中,这样就形成了一个链式结构,我们可以通过这个链式结构回溯函数调用栈。这就是为什么有时候我们会说"ebp 指针链"或者"ebp 链"。但实际上,这是一种比喻,用来描述 ebp 在函数调用和返回过程中的行为。
总结
接下来讲解内存管理,即上面提及的 mem_init 部分。