李述铜老师的手写操作系统,复习笔记。
我们看看execve( const char *name, char * const *argv, char * const *env ) 函数,name函数是需要运行函数的文件名,第二个参数是运行文件需要的参数,env是环境变量,储存着地址。
execve函数的作用就是覆盖当前程序的代码,eip重新指向新函数的地址入口。
和fork函数一样,这个函数都需要操作系统在内核状态执行。我们看系统调用的sys_fork()函数。
1、创建新的页表
//创建新的页表
uint32_t old_page_dir=current_task->tss.cr3;
uint32_t new_page_dir=memory_create_uvm();
if(new_page_dir==0){
goto execve_fail;
}
这里memory_create_uvm,就是用地址分配器新建一个一级页表,然后把0--0x80000000地址段的一级页表指向操作系统的二级页表,这块代码是共享的。
uint32_t memory_create_uvm (void){
//为进程创建一级页表,返回新的一级页表地址,出错返回0
pde_t * pde_paddr =(pde_t *)addr_alloc_page(&paddr_alloc,1);
if(!pde_paddr){
//创建页表失败
return 0;
}
//清空数据
kernel_memset((void *)pde_paddr,0,MEM_PAGE_SIZE);
//将操作系统的数据搬过来
// 这个是取操作系统最大内存的页表项
uint32_t user_pde_start=pde_index(MEMORY_TASK_BASE);
for(int i=0;i<user_pde_start;i++){
pde_paddr[i].v=kernel_page_dir[i].v;
}
return (uint32_t )pde_paddr;
};
2、加载elf文件
//加载elf文件
uint32_t entry=load_elf_file(current_task,name,new_page_dir);
if(entry==0){
goto execve_fail;
}
模拟的就是操作系统加载文件,因为这里还没有实现文件系统,就利用编译器,放在操作系统的内存里,然后再加载。
这个函数就是读取二进制elf头,返回的是函数的起始地址。
static uint32_t load_elf_file (task_t * task, const char * name, uint32_t page_dir){
//将elf文件加载到内存中,返回入口地址
Elf32_Ehdr elf_hdr;
Elf32_Phdr elf_phdr;
// 以只读方式打开
int file = sys_open(name, 0); // todo: flags暂时用0替代
if (file < 0) {
log_printf("open file failed.%s", name);
goto load_failed;
}
// 先读取文件头
int cnt = sys_read(file, (char *)&elf_hdr, sizeof(Elf32_Ehdr));
if (cnt < sizeof(Elf32_Ehdr)) {
log_printf("elf hdr too small. size=%d", cnt);
goto load_failed;
}
// 做点必要性的检查。当然可以再做其它检查
if ((elf_hdr.e_ident[0] != ELF_MAGIC) || (elf_hdr.e_ident[1] != 'E')
|| (elf_hdr.e_ident[2] != 'L') || (elf_hdr.e_ident[3] != 'F')) {
log_printf("check elf indent failed.");
goto load_failed;
}
// 必须是可执行文件和针对386处理器的类型,且有入口
if ((elf_hdr.e_type != ET_EXEC) || (elf_hdr.e_machine != ET_386) || (elf_hdr.e_entry == 0)) {
log_printf("check elf type or entry failed.");
goto load_failed;
}
// 必须有程序头部
if ((elf_hdr.e_phentsize == 0) || (elf_hdr.e_phoff == 0)) {
log_printf("none programe header");
goto load_failed;
}
// 然后从中加载程序头,将内容拷贝到相应的位置
uint32_t e_phoff = elf_hdr.e_phoff;
for (int i = 0; i < elf_hdr.e_phnum; i++, e_phoff += elf_hdr.e_phentsize) {
if (sys_lseek(file, e_phoff, 0) < 0) {
log_printf("read file failed");
goto load_failed;
}
// 读取程序头后解析,这里不用读取到新进程的页表中,因为只是临时使用下
cnt = sys_read(file, (char *)&elf_phdr, sizeof(Elf32_Phdr));
if (cnt < sizeof(Elf32_Phdr)) {
log_printf("read file failed");
goto load_failed;
}
// 简单做一些检查,如有必要,可自行加更多
// 主要判断是否是可加载的类型,并且要求加载的地址必须是用户空间
if ((elf_phdr.p_type != PT_LOAD) || (elf_phdr.p_vaddr < MEMORY_TASK_BASE)) {
continue;
}
// 加载当前程序头
int err = load_phdr(file, &elf_phdr, page_dir);
if (err < 0) {
log_printf("load program hdr failed");
goto load_failed;
}
// 简单起见,不检查了,以最后的地址为bss的地址
task->heap_start = elf_phdr.p_vaddr + elf_phdr.p_memsz;
task->heap_end = task->heap_start;
}
sys_close(file);
return elf_hdr.e_entry;
load_failed:
if (file >= 0) {
sys_close(file);
}
return 0;
}
3、为进程创建栈空间
//为进程创建新的栈空间
uint32_t stack_top = MEM_TASK_STACK_TOP - MEM_TASK_ARG_SIZE; // 预留一部分参数空间
uint32_t err=memory_alloc_for_page_dir(new_page_dir,MEM_TASK_STACK_TOP - MEM_TASK_STACK_SIZE,MEM_TASK_STACK_SIZE,PTE_P | PTE_U | PTE_W);
if(err==0){
goto execve_fail;
}
进程需要有自己的栈空间的
4、复制程序运行的参数
// 复制参数,写入到栈顶的后边
int argc = strings_count(argv);
int err1 = copy_args((char *)stack_top, new_page_dir, argc, argv);
if (err1 < 0) {
goto execve_fail;
}
这里的有一个难点,就是两个进程页表不一样,不能通过虚拟地址直接复制,需要通过页表,获取当前的物理地址。
static int copy_args (char * to, uint32_t page_dir, int argc, char **argv) {
// 在stack_top中依次写入argc, argv指针,参数字符串
task_args_t task_args;
task_args.argc = argc;
task_args.argv = (char **)(to + sizeof(task_args_t));
// 复制各项参数, 跳过task_args和参数表
// 各argv参数写入的内存空间
char * dest_arg = to + sizeof(task_args_t) + sizeof(char *) * (argc); // 留出结束符
// argv表
char ** dest_argv_tb = (char **)memory_get_paddr(page_dir, (uint32_t)(to + sizeof(task_args_t)));
ASSERT(dest_argv_tb != 0);
for (int i = 0; i < argc; i++) {
char * from = argv[i];
// 不能用kernel_strcpy,因为to和argv不在一个页表里
int len = kernel_strlen(from) + 1; // 包含结束符
int err = memory_copy_uvm_data((uint32_t)dest_arg, page_dir, (uint32_t)from, len);
ASSERT(err >= 0);
// 关联ar
dest_argv_tb[i] = dest_arg;
// 记录下位置后,复制的位置前移
dest_arg += len;
}
// 写入task_args
return memory_copy_uvm_data((uint32_t)to, page_dir, (uint32_t)&task_args, sizeof(task_args_t));
}
5、修改新进程的寄存器
//修改新进程的寄存器
syscall_frame_t * frame = (syscall_frame_t *)(current_task->tss.esp0 - sizeof(syscall_frame_t));
frame->eip = entry;
frame->eax = frame->ebx = frame->ecx = frame->edx = 0;
frame->esi = frame->edi = frame->ebp = 0;
frame->eflags = EFLAGS_DEFAULT| EFLAGS_IF; // 段寄存器无需修改
// 内核栈不用设置,保持不变,后面调用memory_destroy_uvm并不会销毁内核栈的映射。
// 但用户栈需要更改, 同样要加上调用门的参数压栈空间
frame->esp = stack_top - sizeof(uint32_t)*SYSCALL_PARAM_COUNT;
6、设置新的cr3 页表
//设置新的cr3 页表
current_task->tss.cr3=new_page_dir;
mmu_set_page_dir(new_page_dir);
//释放旧页表
memory_destroy_uvm(old_page_dir);
return 0;
这是释放用户的页表
void memory_destroy_uvm (uint32_t page_dir){
//释放用户的页表
uint32_t user_pde_index=pde_index(MEMORY_TASK_BASE);
pde_t *user_pde=(pde_t*)page_dir+user_pde_index;
//0x80000000之后的,就是进程自己的页表
for(int i=user_pde_index;i<PDE_CNT;i++,user_pde++){
if(!user_pde->present){
continue;
}
pte_t *user_pte=(pte_t*)pde_paddr(user_pde);
for(int j=0;j<PTE_CNT;j++){
if(!user_pde->present){
continue;
}
addr_free_page(&paddr_alloc, pte_paddr(user_pte), 1);
}
addr_free_page(&paddr_alloc, (uint32_t)pde_paddr(user_pde), 1);
}
// 页目录表
addr_free_page(&paddr_alloc, page_dir, 1);
};