练习1:加载应用程序并执行(需要编码)
do_execv
函数调用load_icode
(位于kern/process/proc.c
中)来加载并解析一个处于内存中的ELF执行文件格式的应用程序,建立相应的用户内存空间来放置应用程序的代码段、数据段等,且要设置好proc_struct
结构中的成员变量trapframe
中的内容,确保在执行此进程后,能够从应用程序设定的起始执行地址开始执行。需设置正确的trapframe
内容。
load_icode
函数的主要工作就是给用户进程建立一个能够让用户进程正常运行的用户环境。此函数有一百多行,完成了如下重要工作:
- 调用
mm_create
函数来申请进程的内存管理数据结构mm所需内存空间,并对mm进行初始化;- 调用
setup_pgdir
来申请一个页目录表所需的一个页大小的内存空间,并把描述ucore内核虚空间映射的内核页表(boot_pgdir所指)的内容拷贝到此新目录表中,最后让mm->pgdir指向此页目录表,这就是进程新的页目录表了,且能够正确映射内核虚空间;- 根据应用程序执行码的起始位置来解析此ELF格式的执行程序,并调用
mm_map
函数根据ELF格式的执行程序说明的各个段(代码段、数据段、BSS段等)的起始位置和大小建立对应的vma结构,并把vma插入到mm结构中,从而表明了用户进程的合法用户态虚拟地址空间;- 调用根据执行程序各个段的大小分配物理内存空间,并根据执行程序各个段的起始位置确定虚拟地址,并在页表中建立好物理地址和虚拟地址的映射关系,然后把执行程序各个段的内容拷贝到相应的内核虚拟地址中,至此应用程序执行码和数据已经根据编译时设定地址放置到虚拟内存中了;
- 需要给用户进程设置用户栈,为此调用
mm_mmap
函数建立用户栈的vma结构,明确用户栈的位置在用户虚空间的顶端,大小为256个页,即1MB,并分配一定数量的物理内存且建立好栈的虚地址<–>物理地址映射关系;- 至此,进程内的内存管理vma和mm数据结构已经建立完成,于是把
mm->pgdir
赋值到cr3寄存器中,即更新了用户进程的虚拟内存空间,此时的initproc已经被hello的代码和数据覆盖,成为了第一个用户进程,但此时这个用户进程的执行现场还没建立好;- 先清空进程的中断帧,再重新设置进程的中断帧,使得在执行中断返回指令“iret”后,能够让CPU转到用户态特权级,并回到用户态内存空间,使用用户态的代码段、数据段和堆栈,且能够跳转到用户进程的第一条指令执行,并确保在用户态能够响应中断;
从指导书中的load_icode
介绍中提到了几个函数:
// mm_create - alloc a mm_struct & initialize it.
struct mm_struct *
mm_create(void) {
struct mm_struct *mm = kmalloc(sizeof(struct mm_struct));
if (mm != NULL) {
list_init(&(mm->mmap_list));
mm->mmap_cache = NULL;
mm->pgdir = NULL;
mm->map_count = 0;
if (swap_init_ok) swap_init_mm(mm);
else mm->sm_priv = NULL;
set_mm_count(mm, 0);
lock_init(&(mm->mm_lock));
}
return mm;
}
// setup_pgdir - alloc one page as PDT
static int
setup_pgdir(struct mm_struct *mm) {
struct Page *page;
if ((page = alloc_page()) == NULL) {
return -E_NO_MEM;
}
pde_t *pgdir = page2kva(page);
memcpy(pgdir, boot_pgdir, PGSIZE);
pgdir[PDX(VPT)] = PADDR(pgdir) | PTE_P | PTE_W;
mm->pgdir = pgdir;
return 0;
}
int
mm_map(struct mm_struct *mm, uintptr_t addr, size_t len, uint32_t vm_flags,
struct vma_struct **vma_store) {
uintptr_t start = ROUNDDOWN(addr, PGSIZE), end = ROUNDUP(addr + len, PGSIZE);
if (!USER_ACCESS(start, end)) {
return -E_INVAL;
}
assert(mm != NULL);
int ret = -E_INVAL;
struct vma_struct *vma;
if ((vma = find_vma(mm, start)) != NULL && end > vma->vm_start) {
goto out;
}
ret = -E_NO_MEM;
if ((vma = vma_create(start, end, vm_flags)) == NULL) {
goto out;
}
insert_vma_struct(mm, vma);
if (vma_store != NULL) {
*vma_store = vma;
}
ret = 0;
out:
return ret;
}
do_execv
的主要工作流程:
- 首先为加载新的执行码做好用户态内存空间清空准备。如果mm不为NULL,则设置页表为内核空间页表,且进一步判断mm的引用计数减1后是否为0,如果为0,则表明没有进程再需要此进程所占用的内存空间,为此将根据mm中的记录,释放进程所占用户空间内存和进程页表本身所占空间。最后把当前进程的mm内存管理指针为空。由于此处的initproc是内核线程,所以mm为NULL,整个处理都不会做。
- 接下来的一步是加载应用程序执行码到当前进程的新创建的用户态虚拟空间中。这里涉及到读ELF格式的文件,申请内存空间,建立用户态虚存空间,加载应用程序执行码等。
load_icode
函数完成了整个复杂的工作。
所以do_execv
函数主要做的工作就是先回收自身所占用户空间,然后调用load_icode
,用新的程序覆盖内存空间,形成一个执行新程序的新进程。
do_execv
函数:
// do_execve - call exit_mmap(mm)&put_pgdir(mm) to reclaim memory space of current process
// - call load_icode to setup new memory space accroding binary prog.
int
do_execve(const char *name, size_t len, unsigned char *binary, size_t size) {
struct mm_struct *mm = current->mm;
if (!user_mem_check(mm, (uintptr_t)name, len, 0)) {
return -E_INVAL;
}
if (len > PROC_NAME_LEN) {
len = PROC_NAME_LEN;
}
char local_name[PROC_NAME_LEN + 1];
memset(local_name, 0, sizeof(local_name));
memcpy(local_name, name, len);
//清空空间
if (mm != NULL) {
lcr3(boot_cr3);
if (mm_count_dec(mm) == 0) {
exit_mmap(mm);//清空内存管理部分和对应页表
put_pgdir(mm);//清空页表
mm_destroy(mm);//清空内存
}
current->mm = NULL;
}
int ret;
//向清空的空间填充新内容,调用load_icode
if ((ret = load_icode(binary, size)) != 0) {
goto execve_exit;
}
set_proc_name(current, local_name);
return 0;
execve_exit:
do_exit(ret);
panic("already exit: %e.\n", ret);
}
load_icode
函数的主要工作就是给用户进程建立一个能够让用户进程正常运行的用户环境
load_icode
:
/* load_icode - 加载二进制程序的内容(ELF格式)作为当前进程的新内容
* @binary: 二进制程序内容的内存地址
* @size: 二进制程序内容的大小
*/
static int
load_icode(unsigned char *binary, size_t size) {
if (current->mm != NULL) {
panic("load_icode: current->mm must be empty.\n");
}
//当前进程必须为空,这样才能加载到内存
int ret = -E_NO_MEM;
struct mm_struct *mm;
//(1) 为当前进程创建一块内存
//a:使用了mm_create函数
if ((mm = mm_create()) == NULL) {
//申请内存
goto bad_mm;
}
//(2) create a new PDT, and mm->pgdir= kernel virtual addr of PDT
//b:使用了setup_pgdir函数
if (setup_pgdir(mm) != 0) {
goto bad_pgdir_cleanup_mm;
}
//(3) copy TEXT/DATA section, build BSS parts in binary to memory space of process
struct Page *page;//申请一个页
//(3.1) get the file header of the bianry program (ELF format)
struct elfhdr *elf = (struct elfhdr *)binary;//获取ELF格式文件的表头
//(3.2) get the entry of the program section headers of the bianry program (ELF format)
struct proghdr *ph = (struct proghdr *)(binary + elf->e_phoff);
//(3.3) This program is valid?
//判断是否合法
if (elf->e_magic != ELF_MAGIC) {
ret = -E_INVAL_ELF;//ELF文件不合法
goto bad_elf_cleanup_pgdir;//返回
}
uint32_t vm_flags, perm;
struct proghdr *ph_end = ph + elf->e_phnum;
for (; ph < ph_end; ph ++) {
//遍历每一个程序段
//(3.4) find every program section headers
//找到每一个程序段的头部
if (ph->p_type != ELF_PT_LOAD) {