xv6-Memory

简介

这个部分,我们来学习一下 xv6 是如何进行内存的管理的,了解最简单的管理方式,为后续学习 Linux 内存管理哪些打个小基础。

xv6 这儿有源文件 是一个 UNIX 风格的操作系统。后续的代码,我会按照顺序,对其进行拆分,和一些自己理解的说明,其实基本上内容都是在 xv6 的这本书里。

Page tables

x86 的指令操纵的都是虚拟地址(virtual address),而硬件 RAM 物理内存(physical address),则是每个存储空间有一个物理地址。x86 页表硬件将虚拟地址和物理地址进行一一映射。

x86 页表逻辑上是一个大小为 2^20 的数组,每个数组想被称为一个页表条目(page table entry, PTEs)。每个 PTE 包含 20-bit 物理页号(physical page number, PPN)和一些标志 flag。页表硬件将虚拟地址的高 20-bit 作为索引在页表中寻找一个 PTE,然后将高 20-bit 的地址替换为 PTE 中对应内存单元中保存的 PPN;同时,页表硬件将虚拟地址中的低 12-bit 直接复制到物理内存中的低 12-bit 的位置,然后和前边儿的 20-bit 组合起来形成一个地址,这个地址就是虚拟地址对应的实际的物理地址,然后硬件系统就可以通过该物理地址进行内存数据的访问。通过这样的步骤,就可以得到一个页大小 2^12(4096)bytes。

x86 page table hardware

如图所示,虚拟地址到物理地址的转换实际分为了两个步骤。页表在物理内存中保存为两级树的形式,树的根部是一个 4096 字节的页目录(page directory),包含 1024 个页表页(page table pages)。这些页表页都包含一个 1024 大小的 32-bit 的页表条目(PTEs)。

页表硬件会使用虚拟地址的高地址的 10-bits 在页目录表中选择条目,然后使用接下来的 10-bits 在二级页表中选择页表条目。如果页目录表(page directory entry)和页表条目(PTE)都没有的话,页表硬件会报错。

每个 PTE 会包含标志位,页表硬件通过这些标志位来知晓如何使用虚拟地址。

  • PTE_P:表明PTE是否存在,如果否会报错;
  • PTE_W:表明指令是否可以对页进行写操作,如果否,那么只允许读和取指令;
  • PTE_U:表明用户程序允许使用该页;

这些标志位的具体信息,可以在 mmu.h 中看到,这些 16 进制的数,转化为二进制过后就可以得到相应的标志位为 1。

1
2
3
4
5
// Page table/directory entry flags.
#define PTE_P 0x001 // Present
#define PTE_W 0x002 // Writeable
#define PTE_U 0x004 // User
#define PTE_PS 0x080 // Page Size

关于术语的一些注释。 物理内存是指DRAM中的存储单元。 物理内存的一个字节有一个地址,称为物理地址。 指令仅使用虚拟地址,分页硬件将其转换为物理地址,然后发送到 DRAM 硬件以读取或写入存储。 在这个级别的讨论中,没有虚拟内存之类的东西,只有虚拟地址。

Process address space

这个部分我们讨论 xv6 的进程地址空间。

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
# By convention, the _start symbol specifies the ELF entry point.
# Since we haven't set up virtual memory yet, our entry point is
# the physical address of 'entry'.
.globl _start
_start = V2P_WO(entry)

# Entering xv6 on boot processor, with paging off.
.globl entry
entry:
# Turn on page size extension for 4Mbyte pages
movl %cr4, %eax
orl $(CR4_PSE), %eax
movl %eax, %cr4
# Set page directory
movl $(V2P_WO(entrypgdir)), %eax
movl %eax, %cr3
# Turn on paging.
movl %cr0, %eax
orl $(CR0_PG|CR0_WP), %eax
movl %eax, %cr0

# Set up the stack pointer.
movl $(stack + KSTACKSIZE), %esp

# Jump to main(), and switch to executing at
# high addresses. The indirect call is needed because
# the assembler produces a PC-relative instruction
# for a direct jump.
mov $main, %eax
jmp *%eax

.comm stack, KSTACKSIZE

由entry创建的页表有足够的映射来允许内核的C代码开始运行。

1
2
3
4
5
6
7
8
9
10

// Allocate one page table for the machine for the kernel address
// space for scheduler processes.
void
kvmalloc(void)
{
kpgdir = setupkvm();
switchkvm();
}

然而,main 立即通过调用 kvmalloc 更改为新的页表,因为内核有一个更详细的计划来描述进程地址空间。

每个进程有一个单独的页表,并且 xv6 告知页表硬件,当 xv6 切换进程的时候切换页表。

Layout of the virtual address space of a process and the layout of the physical address space. Note that if a machine has more than 2 Gbyte of physical memory, xv6 can use only the memory that fits between KERNBASE and 0xFE00000

如图所示,一个进程的用户内存从虚拟地址 0 开始,一直增长到 KERNBASE 的位置,允许一个进程寻址最多 2G。

memlayout.h 中,有描述 xv6 的内存布局,和一些将虚拟地址转化到虚拟地址的宏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Memory layout

#define EXTMEM 0x100000 // Start of extended memory
#define PHYSTOP 0xE000000 // Top physical memory
#define DEVSPACE 0xFE000000 // Other devices are at high addresses

// Key addresses for address space layout (see kmap in vm.c for layout)
#define KERNBASE 0x80000000 // First kernel virtual address
#define KERNLINK (KERNBASE+EXTMEM) // Address where kernel is linked

#define V2P(a) (((uint) (a)) - KERNBASE)
#define P2V(a) ((void *)(((char *) (a)) + KERNBASE))

#define V2P_WO(x) ((x) - KERNBASE) // same as V2P, but without casts
#define P2V_WO(x) ((x) + KERNBASE) // same as P2V, but without casts

当一个进程向 xv6 申请内存的时候,xv6 首先寻找空闲的物理页(physical page),然后添加 PTEs 到进程的页表,该 PTEs 指向新的物理页。xv6 设置 PTE_U PTE_W PTE_P 标志位在这些 PTEs中。绝大部分进程不会使用完所有的用户进程空间,xv6 让 未使用的PTEs的 PTE_P 清零。不同的进程页表将用户地址转换为不同的物理内存,所以每个进程都有私有的用户内存。

xv6 包含了内核在每个进程的页表中运行所需的所有映射; 这些映射都出现在 KERNBASE 之上。它将虚拟地址 KERNBASE:KERNBASE+PHYSTOP 映射到 0:PHYSTOP。 这种映射的原因之一是内核可以使用自己的指令和数据。 另一个原因是内核有时需要能够写入物理内存的给定页面,例如在创建页表页面时; 让每个物理页都出现在可预测的虚拟地址处使这变得很方便。 这种安排的一个缺陷是 xv6 无法使用超过 2 GB 的物理内存,因为地址空间的内核部分是 2 GB。 因此,xv6 要求 PHYSTOP 小于 2 GB,即使计算机具有超过 2 GB 的物理内存。

一些使用内存映射 I/O 的设备出现在从 0xFE000000 开始的物理地址处,因此 xv6 页表包括它们的直接映射。 因此,PHYSTOP 必须小于 2 GB - 16 MB(对于设备内存)。

xv6 不会将高于 KERNBASE 的 PTEs 中设置 PTE_U 标志,所以只有内核可以使用他们。

让每个进程的页表都包含用户内存和整个内核的映射,在系统调用和中断期间从用户代码切换到内核代码时很方便:这种切换不需要页表切换。 大多数情况下,内核没有自己的页表; 它几乎总是借用某个进程的页表。(其实这种管理方式是不安全的,Linux 现在都会维护内核页表,用户态切换带内核态的时候,也会从用户页表切换到内核页表)

回顾一下,xv6 确保每个进程只能使用自己的内存。 而且,每个进程都将其内存视为具有从零开始的连续虚拟地址,而进程的物理内存可以是不连续的。 xv6 通过仅在引用进程自己的内存的虚拟地址的 PTE 上设置 PTE_U 位来实现第一个。 它使用页表的能力来实现第二个,将连续的虚拟地址转换为恰好分配给进程的任何物理页。

Code:createing an address space

main 调用 kvmalloc 创建并切换到具有内核运行所需的 KERNBASE 以上映射的页表。 大部分工作发生在 setupkvm 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Set up kernel part of a page table.
pde_t*
setupkvm(void)
{
pde_t *pgdir;
struct kmap *k;

if((pgdir = (pde_t*)kalloc()) == 0)
return 0;
memset(pgdir, 0, PGSIZE);
if (P2V(PHYSTOP) > (void*)DEVSPACE)
panic("PHYSTOP too high");
for(k = kmap; k < &kmap[NELEM(kmap)]; k++)
if(mappages(pgdir, k->virt, k->phys_end - k->phys_start,
(uint)k->phys_start, k->perm) < 0) {
freevm(pgdir);
return 0;
}
return pgdir;
}

它首先分配一个内存页来保存页目录。 然后它调用mappages 来安装内核所需的翻译,这些映射在 kmap 数组中进行了描述。 这些翻译包括内核的指令和数据、直至 PHYSTOP 的物理内存以及实际上是 I/O 设备的内存范围。 setupkvm 不会为用户内存安装任何映射; 这将在稍后发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Create PTEs for virtual addresses starting at va that refer to
// physical addresses starting at pa. va and size might not
// be page-aligned.
static int
mappages(pde_t *pgdir, void *va, uint size, uint pa, int perm)
{
char *a, *last;
pte_t *pte;

a = (char*)PGROUNDDOWN((uint)va);
last = (char*)PGROUNDDOWN(((uint)va) + size - 1);
for(;;){
if((pte = walkpgdir(pgdir, a, 1)) == 0)
return -1;
if(*pte & PTE_P)
panic("remap");
*pte = pa | perm | PTE_P;
if(a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
return 0;
}

mappages 将一系列虚拟地址到对应的物理地址范围的映射安装到页表中。 它以页面大小间隔为范围内的每个虚拟地址单独执行此操作。 对于每个要映射的虚拟地址,mappages 调用 walkpgdir 来查找该地址的 PTE 地址。 然后它初始化 PTE 以保存相关的物理页号、期望的权限 (PTE_W 和/或 PTE_U) 以及 PTE_P 以将 PTE 标记为有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Return the address of the PTE in page table pgdir
// that corresponds to virtual address va. If alloc!=0,
// create any required page table pages.
static pte_t *
walkpgdir(pde_t *pgdir, const void *va, int alloc)
{
pde_t *pde;
pte_t *pgtab;

pde = &pgdir[PDX(va)];
if(*pde & PTE_P){
pgtab = (pte_t*)P2V(PTE_ADDR(*pde));
} else {
if(!alloc || (pgtab = (pte_t*)kalloc()) == 0)
return 0;
// Make sure all those PTE_P bits are zero.
memset(pgtab, 0, PGSIZE);
// The permissions here are overly generous, but they can
// be further restricted by the permissions in the page table
// entries, if necessary.
*pde = V2P(pgtab) | PTE_P | PTE_W | PTE_U;
}
return &pgtab[PTX(va)];
}

walkpgdir 模仿 x86 分页硬件在 PTE 中查找虚拟地址的操作。 walkpgdir 使用虚拟地址的高 10 位来查找页目录条目。 如果页目录项不存在,则所需的页表页尚未分配; 如果设置了 alloc 参数,walkpgdir 会分配它并将其物理地址放入页目录中。 最后,它使用虚拟地址的接下来的 10 位来查找页表页中 PTE 的地址。

Physical memory allocation

这个部分看一看物理内存的分配。

内核必须在运行时为页表、进程用户内存、内核堆栈和管道缓冲区分配和释放物理内存。

xv6 使用 kernel 结尾到 PHYSTOP 之间的物理内存,来为运行过程中进行内存分配。每一次分配,都是按照 4096-byte 大小的页进行。并且通过链表来跟踪每个页的使用情况。

物理内存分配包括从链表中删除一个页,释放物理内存则是添加空闲页到链表。

存在一个引导问题:必须映射所有物理内存以便分配器初始化空闲列表,但是使用这些映射创建页表涉及分配页表页面。 xv6 通过在进入期间使用单独的页面分配器来解决这个问题,该分配器在内核数据段末尾之后分配内存。 该分配器不支持释放,并且受到 entrypgdir 中 4 MB 映射的限制,但这足以分配第一个内核页表。

Code:Physical memory allocator

分配器的数据结构是可用于分配的物理内存页的空闲列表。

1
2
3
4
5
6
7
8
9
10
struct run {
struct run *next;
};

struct {
struct spinlock lock;
int use_lock;
struct run *freelist;
} kmem;

每个空闲页面的列表元素都是一个struct run。分配器从哪里获取内存来保存该数据结构? 它将每个空闲页面的运行结构存储在空闲页面本身中,因为那里没有存储任何其他内容。 空闲列表受自旋锁保护。 链表和锁包装在一个结构中,以明确锁保护结构中的字段。

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
// Bootstrap processor starts running C code here.
// Allocate a real stack and switch to it, first
// doing some setup required for memory allocator to work.
int
main(void)
{
kinit1(end, P2V(4*1024*1024)); // phys page allocator
kvmalloc(); // kernel page table
mpinit(); // detect other processors
lapicinit(); // interrupt controller
seginit(); // segment descriptors
picinit(); // disable pic
ioapicinit(); // another interrupt controller
consoleinit(); // console hardware
uartinit(); // serial port
pinit(); // process table
tvinit(); // trap vectors
binit(); // buffer cache
fileinit(); // file table
ideinit(); // disk
startothers(); // start other processors
kinit2(P2V(4*1024*1024), P2V(PHYSTOP)); // must come after startothers()
userinit(); // first user process
mpmain(); // finish this processor's setup
}

函数 main 调用 kinit1 和 kinit2 来初始化分配器。 进行两次调用的原因是,对于 main 的大部分调用来说,不能使用锁或超过 4 MB 的内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Initialization happens in two phases.
// 1. main() calls kinit1() while still using entrypgdir to place just
// the pages mapped by entrypgdir on free list.
// 2. main() calls kinit2() with the rest of the physical pages
// after installing a full page table that maps them on all cores.
void
kinit1(void *vstart, void *vend)
{
initlock(&kmem.lock, "kmem");
kmem.use_lock = 0;
freerange(vstart, vend);
}

void
kinit2(void *vstart, void *vend)
{
freerange(vstart, vend);
kmem.use_lock = 1;
}

kinit1 为前 4 MB 建立无锁的分配,kinit2 则是对剩下的更多的内存添加锁服务。

main 函数应该决定多少物理内存是可用的,但是在 x86 上比较困难。取而代之的,它假设一台电脑拥有 224 MB(PHYSTOP)的物理内存,并且使用所有的 kernel 结束位置到 PHYSTOP 的物理内存作为初始空闲内存池。

1
2
3
4
5
6
7
8
void
freerange(void *vstart, void *vend)
{
char *p;
p = (char*)PGROUNDUP((uint)vstart);
for(; p + PGSIZE <= (char*)vend; p += PGSIZE)
kfree(p);
}

kinit1 和 kinit2 调用 freerange 接口添加内存到空闲链表,这个步骤是通过对每一个物理页调用 kfree 来实现。一个 PTE 只能指向一个对齐 4096-byte 的物理地址,所以会使用 PGROUNDUP 来保证对齐。分配器 allocator 初始是没有内存的,这些调用通过 kfree 准备好一些内存来给分配器使用。

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
//PAGEBREAK: 21
// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc(). (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(char *v)
{
struct run *r;

if((uint)v % PGSIZE || v < end || V2P(v) >= PHYSTOP)
panic("kfree");

// Fill with junk to catch dangling refs.
memset(v, 1, PGSIZE);

if(kmem.use_lock)
acquire(&kmem.lock);
r = (struct run*)v;
r->next = kmem.freelist;
kmem.freelist = r;
if(kmem.use_lock)
release(&kmem.lock);
}

kfree 功能将一个物理内存页进行释放,并且将其添加到空闲内存链表内。这个步骤是需要进行原子操作的,因此会使用原子锁进行控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
char*
kalloc(void)
{
struct run *r;

if(kmem.use_lock)
acquire(&kmem.lock);
r = kmem.freelist;
if(r)
kmem.freelist = r->next;
if(kmem.use_lock)
release(&kmem.lock);
return (char*)r;
}

kalloc 则是分配一个 4096-byte 大小的物理内存,并且返回一个指向该内存页初始地址的指针。如果返回 0 的话,则是没有内存可以分配了。

值得注意的是,分配器 allocator 通过虚拟地址映射来指向一个物理页,所以 kinit 使用 P2V(PHYSTOP) 来将物理地址 PHYSTOP 翻译成一个虚拟地址。分配器有时将地址视为整数以便对其进行算术(例如,遍历 kinit 中的所有页面),有时将地址用作读写内存的指针(例如,操作存储在每个页面中的运行结构); 地址的双重使用是分配器代码充满 C 类型转换的主要原因。 另一个原因是释放和分配本质上改变了内存的类型。

User part of an address space

Memory layout of a user process with its initial stack

如图所示,xv6 进程的用户内存空间分布。每一个用户进程都从地址 0 开始。地址空间的地步是用户程序的具体内容 text,data,stack。堆 heap 在栈 stack 的上方,进程可以通过 sbrk 来扩展堆的大小。值得注意的是,text,data,stack 在进程地址空间内是连续的,但是 xv6 可以自由的为这些部分使用非连续的物理页。例如,当 xv6 扩展进程的 heap 时,可以使用任何空闲的物理页来构建新的虚拟页,然后使用页表硬件,通过编程将虚拟页映射到物理页。这种灵活性是使用分页硬件的主要优点。

堆栈是一个页,并显示由 exec 创建的初始内容。 包含命令行参数的字符串以及指向它们的指针数组位于堆栈的最顶部。 就在其下方是允许程序在 main 处启动的值,就像函数调用 main(argc, argv) 刚刚启动一样。 为了保护堆栈从堆栈页增长,xv6 在堆栈的正下方放置了一个保护页。 保护页未映射,因此如果堆栈超出堆栈页,硬件将生成异常,因为它无法转换错误地址。 Linux等操作系统可能会为堆栈分配更多空间,以便它可以增长到超过一页。

Code:sbrk

sbrk 是一个系统调用,它可以为一个进程收缩或者增加内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
int
sys_sbrk(void)
{
int addr;
int n;

if(argint(0, &n) < 0)
return -1;
addr = myproc()->sz;
if(growproc(n) < 0)
return -1;
return addr;
}

该系统调用通过函数 growproc 来实现。如果 n 是正数,growproc 会分配一个或更多的物理页,并且映射他们到进程地址空间的顶部。如果 n 是负数,则会从进程地址空间的顶部取消一个或更多的页。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Grow current process's memory by n bytes.
// Return 0 on success, -1 on failure.
int
growproc(int n)
{
uint sz;
struct proc *curproc = myproc();

sz = curproc->sz;
if(n > 0){
if((sz = allocuvm(curproc->pgdir, sz, sz + n)) == 0)
return -1;
} else if(n < 0){
if((sz = deallocuvm(curproc->pgdir, sz, sz + n)) == 0)
return -1;
}
curproc->sz = sz;
switchuvm(curproc);
return 0;
}

为了进行这些更改,xv6 修改了进程的页表。 进程的页表存储在内存中,因此内核可以使用普通的赋值语句更新页表,这就是 allocuvm 和 deallocuvm 所做的事情。

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
// Allocate page tables and physical memory to grow process from oldsz to
// newsz, which need not be page aligned. Returns new size or 0 on error.
int
allocuvm(pde_t *pgdir, uint oldsz, uint newsz)
{
char *mem;
uint a;

if(newsz >= KERNBASE)
return 0;
if(newsz < oldsz)
return oldsz;

a = PGROUNDUP(oldsz);
for(; a < newsz; a += PGSIZE){
mem = kalloc();
if(mem == 0){
cprintf("allocuvm out of memory\n");
deallocuvm(pgdir, newsz, oldsz);
return 0;
}
memset(mem, 0, PGSIZE);
if(mappages(pgdir, (char*)a, PGSIZE, V2P(mem), PTE_W|PTE_U) < 0){
cprintf("allocuvm out of memory (2)\n");
deallocuvm(pgdir, newsz, oldsz);
kfree(mem);
return 0;
}
}
return newsz;
}

allocuvm 分配一个物理页,并且添加一个映射到页表内。

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
// Deallocate user pages to bring the process size from oldsz to
// newsz. oldsz and newsz need not be page-aligned, nor does newsz
// need to be less than oldsz. oldsz can be larger than the actual
// process size. Returns the new process size.
int
deallocuvm(pde_t *pgdir, uint oldsz, uint newsz)
{
pte_t *pte;
uint a, pa;

if(newsz >= oldsz)
return oldsz;

a = PGROUNDUP(newsz);
for(; a < oldsz; a += PGSIZE){
pte = walkpgdir(pgdir, (char*)a, 0);
if(!pte)
a = PGADDR(PDX(a) + 1, 0, 0) - PGSIZE;
else if((*pte & PTE_P) != 0){
pa = PTE_ADDR(*pte);
if(pa == 0)
panic("kfree");
char *v = P2V(pa);
kfree(v);
*pte = 0;
}
}
return newsz;
}

deallocuvm 删除页表中的映射关系。并且调整具体的内存大小的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Switch TSS and h/w page table to correspond to process p.
void
switchuvm(struct proc *p)
{
if(p == 0)
panic("switchuvm: no process");
if(p->kstack == 0)
panic("switchuvm: no kstack");
if(p->pgdir == 0)
panic("switchuvm: no pgdir");

pushcli();
mycpu()->gdt[SEG_TSS] = SEG16(STS_T32A, &mycpu()->ts,
sizeof(mycpu()->ts)-1, 0);
mycpu()->gdt[SEG_TSS].s = 0;
mycpu()->ts.ss0 = SEG_KDATA << 3;
mycpu()->ts.esp0 = (uint)p->kstack + KSTACKSIZE;
// setting IOPL=0 in eflags *and* iomb beyond the tss segment limit
// forbids I/O instructions (e.g., inb and outb) from user space
mycpu()->ts.iomb = (ushort) 0xFFFF;
ltr(SEG_TSS << 3);
lcr3(V2P(p->pgdir)); // switch to process's address space
popcli();
}

x86 硬件将页表条目缓存在 Translation Look-aside Buffer(TLB) 中,当 xv6 更改页表时,它必须使缓存的条目无效。如果它没有使缓存的条目无效,那么在稍后的某个时刻,TLB 可能会使用旧的映射,指向同时已分配给另一个进程的物理页,因此,进程可能能在其他进程的内存上修改。

1
2
3
4
5
static inline void
lcr3(uint val)
{
asm volatile("movl %0,%%cr3" : : "r" (val));
}

xv6 通过重新加载 cr3(保存当前页表地址的寄存器)来使过时的缓存条目无效。

Code:exec

exec 是创建地址空间的用户部分的系统调用。它从文件系统中存储的文件初始化地址空间的用户部分。

exec 使用 namei 打开命名二进制路径。然后,它读取 ELF 标头。 xv6 应用程序使用 elf 格式,在 elf.h 中定义。

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
// Format of an ELF executable file

#define ELF_MAGIC 0x464C457FU // "\x7FELF" in little endian

// File header
struct elfhdr {
uint magic; // must equal ELF_MAGIC
uchar elf[12];
ushort type;
ushort machine;
uint version;
uint entry;
uint phoff;
uint shoff;
uint flags;
ushort ehsize;
ushort phentsize;
ushort phnum;
ushort shentsize;
ushort shnum;
ushort shstrndx;
};

// Program section header
struct proghdr {
uint type;
uint off;
uint vaddr;
uint paddr;
uint filesz;
uint memsz;
uint flags;
uint align;
};

// Values for Proghdr type
#define ELF_PROG_LOAD 1

// Flag bits for Proghdr flags
#define ELF_PROG_FLAG_EXEC 1
#define ELF_PROG_FLAG_WRITE 2
#define ELF_PROG_FLAG_READ 4

ELF 二进制文件由 ELF 头 struct elfhdr 组成,后面跟着一系列程序段头 struct proghdr。 每个 proghdr 描述了必须加载到内存中的应用程序的一部分; xv6 程序只有一个程序节标头,但其他系统可能有单独的指令和数据节。

第一步是快速检查该文件是否可能包含 ELF 二进制文件。 ELF 二进制文件以四字节“幻数 ”0x7F、“E”、“L”、“F” 或 ELF_MAGIC 开头。 如果 ELF 标头具有正确的幻数,则 exec 会假定二进制文件格式良好。

exec 使用 setupkvm 分配一个没有用户映射的新页表,使用 allocuvm 为每个 ELF 段分配内存,并使用 loaduvm 将每个段加载到内存中。 allocuvm 检查请求的虚拟地址是否低于 KERNBASE。 loaduvm 使用 walkpgdir 查找已定位内存的物理地址,在该地址写入 ELF 段的每个页面,并使用 readi 从文件中读取。

/init(使用 exec 创建的第一个用户程序)的程序节标头如下所示:

1
2
3
4
5
6
7

# objdump -p _init
_init: file format elf32-i386
Program Header:
LOAD off 0x00000054 vaddr 0x00000000 paddr 0x00000000 align 2**2
filesz 0x000008c0 memsz 0x000008cc flags rwx


xv6-Memory
https://www.bencorn.com/2023/07/05/xv6-Memory/
作者
Bencorn
发布于
2023年7月5日
许可协议