这篇文章讲解如何将程序加载到虚拟内存中?文件是以 ELF 格式组织起来的,接下来先讲解 ELF 格式。
ELF 文件格式
这段代码是关于 ELF (Executable and Linkable Format) 文件格式的定义。ELF 是一种常见的二进制文件格式,用于存储程序或库。在这个文件中,定义了一些结构体和常量,用于解析和处理 ELF 文件。
-
struct Elf
:这个结构体代表了 ELF 文件的头部信息。包括魔数、类型、机器类型、版本、入口点地址、程序头部偏移、节头部偏移、标志、头部大小等信息。 -
struct Proghdr
:这个结构体代表了 ELF 文件的程序头部信息。包括类型、偏移、虚拟地址、物理地址、文件大小、内存大小、标志、对齐等信息。 -
struct Secthdr
:这个结构体代表了 ELF 文件的节头部信息。包括名称、类型、标志、地址、偏移、大小、链接、信息、地址对齐、条目大小等信息。 -
ELF_PROG_LOAD
、ELF_PROG_FLAG_EXEC
、ELF_PROG_FLAG_WRITE
、ELF_PROG_FLAG_READ
等常量:这些常量用于解析和处理 ELF 文件的程序头部信息。 -
ELF_SHT_NULL
、ELF_SHT_PROGBITS
、ELF_SHT_SYMTAB
、ELF_SHT_STRTAB
等常量:这些常量用于解析和处理 ELF 文件的节头部信息。
这些定义是操作系统加载和运行程序的基础,通过这些定义,操作系统可以正确地解析 ELF 文件,将程序加载到内存中,并执行。
ELF 文件在内存中的表示
在内存中,ELF 文件的加载会将其各个部分放置到相应的地址空间中。以下是一个简化的 ELF 文件在内存中的文本图形化表示:
+-------------------------+
| ELF File Layout |
+-------------------------+
| |
| ELF Header |
| |
+-------------------------+ <-- 0x08048000 (ELF默认起始地址)
| Program Headers |
| |
+-------------------------+
| Section Headers |
| |
+-------------------------+
| Section 1 (.text) |
| |
+-------------------------+
| Section 2 (.data) |
| |
+-------------------------+
| Section 3 (.bss) |
| |
+-------------------------+
| Section 4 (.rodata) |
| |
+-------------------------+
| Dynamic Section |
| |
+-------------------------+
| String Table |
| |
+-------------------------+
| Symbol Table |
| |
+-------------------------+
| Relocation Table |
| |
+-------------------------+
| Stack (grows down) |
| |
+-------------------------+ <-- Bottom of Memory
解释:
-
ELF Header 包含有关整个 ELF 文件的信息,例如入口点地址、程序头和节头的偏移量等。ELF Header 在内存中的地址通常是 ELF 文件默认起始地址(0x08048000)。
-
Program Headers 包含指向各个段的信息,如代码段、数据段等。每个 Program Header 指定了在内存中的加载位置、大小等信息。
-
Section Headers 包含有关各个节的信息,如代码节、数据节等。每个 Section Header 指定了在内存中的加载位置、大小等信息。
-
各个 Section 包括代码、数据、bss(未初始化数据)、只读数据等。
-
Dynamic Section 包含动态链接信息,用于运行时动态链接和共享库加载。
-
String Table 包含节名、符号名等字符串。
-
Symbol Table 包含符号信息,如函数名、变量名等。
-
Relocation Table 包含需要在加载时进行重定位的信息,以修正代码和数据的引用地址。
-
Stack 是程序运行时使用的堆栈,通常从内存底部开始向上增长。
请注意,实际情况可能更加复杂,具体取决于 ELF 文件的结构和加载方式。
加载 ELF 到进程中
load_icode
函数是用于加载 ELF 二进制文件到新创建的进程中的函数。它接受两个参数:一个是新创建的进程的指针,另一个是指向 ELF 二进制文件的指针。
-
首先,函数会检查 ELF 文件的魔数是否正确。如果不正确,函数会触发 panic。
-
然后,函数会获取 ELF 文件的程序头部信息。程序头部信息描述了如何将 ELF 文件加载到内存中。
-
接着,函数会遍历所有的程序头部信息。对于每一个类型为
ELF_PROG_LOAD
的程序头部,函数会分配相应的内存,并将 ELF 文件的相应部分加载到内存中。如果程序头部的内存大小大于文件大小,函数会将多余的部分清零。 -
加载完所有的程序段后,函数会设置进程的入口点地址。这个地址是程序开始执行的地方。
-
最后,函数会为程序的初始堆栈分配一个页面。这个页面位于虚拟地址
USTACKTOP - PGSIZE
。
这个函数是操作系统加载和运行程序的关键步骤,它将一个程序加载到新的进程中,并设置好进程的状态,使得操作系统可以开始运行这个程序。
程序头部组成
下面是程序头部信息的文本图形化表示:
+-----------------------+
| Program Header 0 | <- ph
+-----------------------+
| p_type |
| p_offset |
| p_va |
| p_pa |
| p_filesz |
| p_memsz |
| p_flags |
| p_align |
+-----------------------+
| Program Header 1 |
+-----------------------+
| p_type |
| p_offset |
| p_va |
| p_pa |
| p_filesz |
| p_memsz |
| p_flags |
| p_align |
+-----------------------+
| ... |
+-----------------------+
| Program Header n | <- eph
+-----------------------+
| p_type |
| p_offset |
| p_va |
| p_pa |
| p_filesz |
| p_memsz |
| p_flags |
| p_align |
+-----------------------+
上述表示形式中,ph
到 eph
之间的每一项代表一个程序头部。每个程序头部包含了描述如何将 ELF 文件加载到内存中的信息。根据实际情况,n
的值可能会不同,具体取决于 ELF 文件中的程序头部数量。
加载程序段
下面这段代码是从 ELF (Executable and Linkable Format) 文件中加载程序段的部分。它遍历 ELF 文件的所有程序头部,只加载类型为 ELF_PROG_LOAD
的段。
struct Proghdr *ph, *eph;
ph = (struct Proghdr *)(binary + elf_hdr->e_phoff);
eph = ph + elf_hdr->e_phnum;
// Load each program segment
for (; ph < eph; ph++) {
if (ph->p_type != ELF_PROG_LOAD) {
continue;
}
// Allocate memory for the segment
region_alloc(e, (void *)ph->p_va, ph->p_memsz);
// Load the segment
memcpy((void *)ph->p_va, binary + ph->p_offset, ph->p_filesz);
// Zero the remaining part
if (ph->p_memsz > ph->p_filesz) {
memset((void *)(ph->p_va + ph->p_filesz), 0, ph->p_memsz - ph->p_filesz);
}
}
-
region_alloc(e, (void *)ph->p_va, ph->p_memsz);
:这行代码为程序段分配内存。ph->p_va
是程序段应该被加载到的虚拟地址,ph->p_memsz
是程序段在内存中的大小。 -
memcpy((void *)ph->p_va, binary + ph->p_offset, ph->p_filesz);
:这行代码将程序段加载到内存中。binary + ph->p_offset
是程序段在 ELF 文件中的位置,ph->p_filesz
是程序段在 ELF 文件中的大小。 -
memset((void *)(ph->p_va + ph->p_filesz), 0, ph->p_memsz - ph->p_filesz);
:如果程序段的内存大小大于文件大小,这行代码将多余的部分清零。
总的来说,这段代码将 ELF 文件的每个程序段加载到指定的虚拟地址中,并将未使用的部分清零。
为进程分配物理内存
这段代码的功能是为进程分配一定长度的物理内存,并将其映射到进程的虚拟地址空间中。这段代码并不会对映射的页面进行零初始化或其他初始化操作。页面应该可以被用户和内核写入。如果任何分配尝试失败,代码将会触发 panic。
这段代码的主要步骤如下:
-
计算需要分配的页面数量,这是通过将请求的长度(len)除以页面大小(PGSIZE)并向上取整得到的。
-
对于每个需要分配的页面,执行以下操作:
- 使用
page_alloc
函数分配一个新的页面。如果分配失败,触发 panic。 - 使用
page_insert
函数将新分配的页面映射到进程的虚拟地址空间中。映射的虚拟地址由参数va
指定,每个新页面的虚拟地址比前一个页面的虚拟地址高一个页面大小(PGSIZE)。如果映射失败,触发 panic。
- 使用
接下来实现如何为进程的虚拟地址空间分配并映射新的物理内存。这在进程需要更多内存来存储数据或代码时非常有用。例如,当进程调用 malloc
函数来请求更多堆内存时,或者当进程需要加载新的代码段时,就可能需要调用这段代码。
static void
region_alloc(struct Env *e, void *va, size_t len)
{
uintptr_t start = ROUNDDOWN((uintptr_t)va, PGSIZE);
uintptr_t end = ROUNDUP((uintptr_t)va + len, PGSIZE);
uintptr_t i;
struct PageInfo *pp;
for (i = start; i < end; i += PGSIZE) {
if (!(pp = page_alloc(0))) {
panic("region_alloc: out of memory");
}
if (page_insert(e->env_pgdir, pp, (void *)i, PTE_W|PTE_U) < 0) {
panic("region_alloc: page_insert failed");
}
}
}
启动进程
下面这段代码是在操作系统中执行上下文切换的函数,从当前环境 curenv
切换到环境 e
。如果这是对 env_run
的第一次调用,curenv
是 NULL
。
void
env_run(struct Env *e)
{
// 如果这是一个上下文切换(即正在运行一个新的环境)
if (curenv && curenv->env_status == ENV_RUNNING) {
// 将当前环境(如果有)的状态设置回ENV_RUNNABLE,如果它当前的状态是ENV_RUNNING
curenv->env_status = ENV_RUNNABLE;
}
// 将'curenv'设置为新的环境
curenv = e;
// 将新环境的状态设置为ENV_RUNNING
curenv->env_status = ENV_RUNNING;
// 更新新环境的'env_runs'计数器
curenv->env_runs++;
// 使用lcr3()切换到新环境的地址空间
lcr3(PADDR(curenv->env_pgdir));
// 使用env_pop_tf()恢复环境的寄存器并在环境中进入用户模式
env_pop_tf(&curenv->env_tf);
}
步骤 1:如果这是一个上下文切换(即正在运行一个新的环境):
- 如果当前环境存在且其状态为
ENV_RUNNING
,则将其状态设置回ENV_RUNNABLE
。 - 将
curenv
设置为新的环境。 - 将新环境的状态设置为
ENV_RUNNING
。 - 更新新环境的
env_runs
计数器。 - 使用
lcr3()
切换到新环境的地址空间。
步骤 2:使用 env_pop_tf()
恢复环境的寄存器并在环境中进入用户模式。
提示:此函数从 e->env_tf
加载新环境的状态。回顾你之前写的代码,确保你已经将 e->env_tf
的相关部分设置为合理的值。
这行代码的作用是切换到新环境的地址空间。lcr3()
是一个用于加载页目录基址寄存器(CR3)的函数,它接受一个物理地址作为参数。在 x86 架构中,CR3 寄存器存储了当前活动页目录的物理地址。当 CR3 的值改变时,处理器会清空或部分清空其转换缓冲区(TLB),这是一种硬件缓存,用于加速虚拟地址到物理地址的转换。
在这个上下文中,PADDR(curenv->env_pgdir)
计算出新环境的页目录的物理地址,然后 lcr3()
将这个地址加载到 CR3 寄存器中。这样,处理器就会开始使用新环境的页目录,从而实现了到新环境的地址空间的切换。这是在操作系统中进行进程或线程上下文切换的关键步骤之一。
总结
这篇文章主要讲解了如何将程序加载到虚拟内存中。首先介绍了 ELF 文件格式,包括其头部信息、程序头部信息和节头部信息等。然后详细解释了如何将 ELF 文件的各个部分加载到内存中,包括为进程分配物理内存、加载程序段和设置进程的入口点等步骤。最后讲解了如何在操作系统中执行上下文切换,从当前环境切换到新的环境。