@SovietPower
        
        2021-12-05T06:52:27.000000Z
        字数 7574
        阅读 1793
    OS
文件在userprog/process.c下。
32位CPU所含有的寄存器有: 
4个数据寄存器(EAX、EBX、ECX和EDX) 
2个变址和指针寄存器(ESI和EDI) 2个指针寄存器(ESP和EBP) 
6个段寄存器(ES、CS、SS、DS、FS和GS) 
1个指令指针寄存器(EIP) 1个标志寄存器(EFlags)
计算机需要对内存分段,以分配给不同的程序使用。在描述内存分段时,需要有如下段的信息:段的大小、段的起始地址、段的管理属性(禁止写入/禁止执行/系统专用等)。 
这些信息需要用8字节(64位)存储,但段寄存器只有16位,因此段寄存器中只存储段号(segment selector,段选择符),再由段号映射到存在内存中的GDT(全局描述符表)。
分类 
代码段寄存器CS(Code Segment):存放当前正在运行的程序代码所在段的段基址,表示当前使用的指令代码可以从该段寄存器指定的存储器段中取得,相应的偏移量则由IP提供。 
数据段寄存器DS(Data Segment):指出当前程序使用的数据所存放段的最低地址,即存放数据段的段基址。 
堆栈段寄存器SS(Stack Segment):指出当前堆栈的底部地址,即存放堆栈段的段基址。 
附加段寄存器ES(Extra Segment):指出当前程序使用附加数据段的段基址,该段是串操作指令中目的串所在的段。 
标志段寄存器FS(Flag Segment):附加段寄存器。 
全局段寄存器GS(Global Segment):附加段寄存器。
在userprog/gdt.h中。GDT为全局描述符表。
/* 段选择符. */#define SEL_UCSEG 0x1B /* 用户代码段选择符. */#define SEL_UDSEG 0x23 /* 用户数据段选择符. */#define SEL_TSS 0x28 /* 任务状态段,实现任务的挂起和恢复. */#define SEL_CNT 6 /* 段数. */
标志寄存器值。
/* EFLAGS Register. */#define FLAG_MBS 0x00000002 /* 必须设置. */#define FLAG_IF 0x00000200 /* 中断标志. */
中断帧,保存中断发生时进程的栈帧。
和任务状态段(Task State Segment, TSS)差不多?
任务状态段包含了多个字段,表示了管理任务的所有信息。和其它段一样,任务状态段描述符(TSS Descriptor)用于定义TSS。
struct intr_frame{/* Pushed by intr_entry in intr-stubs.S.保存中断程序的寄存器. */uint32_t edi; /* 保存 EDI. */uint32_t esi; /* 保存 ESI. */uint32_t ebp; /* 保存 EBP. */uint32_t esp_dummy; /* 未使用. */uint32_t ebx; /* 保存 EBX. */uint32_t edx; /* 保存 EDX. */uint32_t ecx; /* 保存 ECX. */uint32_t eax; /* 保存 EAX. */uint16_t gs, :16; /* 保存 全局段寄存器. */uint16_t fs, :16; /* 保存 标志段寄存器. */uint16_t es, :16; /* 保存 附加段寄存器. */uint16_t ds, :16; /* 保存 数据段寄存器. *//* Pushed by intrNN_stub in intr-stubs.S. */uint32_t vec_no; /* 中断向量标号. *//* Sometimes pushed by the CPU,otherwise for consistency pushed as 0 by intrNN_stub.The CPU puts it just under `eip', but we move it here. */uint32_t error_code; /* 错误码. *//* Pushed by intrNN_stub in intr-stubs.S.This frame pointer eases interpretation of backtraces. */void *frame_pointer; /* 保存 帧指针 EBP. *//* Pushed by the CPU.保存中断程序的寄存器. */void (*eip) (void); /* 指令寄存器的位置. */uint16_t cs, :16; /* 保存 代码段寄存器. */uint32_t eflags; /* 保存 CPU 标志符. */void *esp; /* 保存栈指针. */uint16_t ss, :16; /* 保存 堆栈段寄存器. */};
开始一个线程,用于运行名为file_name的用户程序。 
如果线程被成功创建,返回线程id,否则返回TID_ERROR。 
线程可能在该函数结束前就被调度运行。
tid_t process_execute (const char *file_name){char *fn_copy;tid_t tid;/* Make a copy of FILE_NAME.Otherwise there's a race between the caller and load(). */fn_copy = palloc_get_page (0);if (fn_copy == NULL)return TID_ERROR;strlcpy (fn_copy, file_name, PGSIZE); // 拷贝该参数,避免之后使用时被修改?/* Create a new thread to execute FILE_NAME. */tid = thread_create (file_name, PRI_DEFAULT, start_process, fn_copy); // 创建执行该程序的新线程if (tid == TID_ERROR)palloc_free_page (fn_copy);return tid;}
加载一个用户程序并使其开始运行。 
会初始化一个中断帧,调用load()加载程序,然后执行一个从中断的返回intr_exit()来开始该进程。
static void start_process (void *file_name_){char *file_name = file_name_;struct intr_frame if_;bool success;/* 初始化中断帧,并调用load()加载程序 */memset (&if_, 0, sizeof if_);if_.gs = if_.fs = if_.es = if_.ds = if_.ss = SEL_UDSEG;if_.cs = SEL_UCSEG;if_.eflags = FLAG_IF | FLAG_MBS;success = load (file_name, &if_.eip, &if_.esp);/* 加载失败,退出 */palloc_free_page (file_name);if (!success)thread_exit ();/* 通过模拟一个从中断的返回(intr_exit()),来开始这个用户进程。intr_exit() 需要中断帧的所有参数,所以只需将栈指针指向当前栈指,就可以直接跳到它。*/asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (&if_) : "memory");/* 确保成功跳转,而不是继续向下执行 */NOT_REACHED (); // PANIC ("executed an unreachable statement");}
等待指定进程退出,并返回其退出状态。 
若对应进程被内核终止,返回-1;如果指定的TID非法,或对应进程不是当前进程的子进程,或已经调用过了对应进程的process_wait(),不等待,立即返回-1。
该函数还未实现。
int process_wait (tid_t child_tid UNUSED);
加载指定页目录PD的物理地址,到CPU的页目录基址寄存器中,使PD对应的新的页表立刻被激活。 
若pd为空指针,则令pd = init_page_dir,即加载预定义的页目录(为只有内核可以映射的页目录,kernel-only page directory)。
void pagedir_activate (uint32_t *pd);
销毁指定页目录PD,释放它指向的所有页。
void pagedir_destroy (uint32_t *pd);
释放当前进程的页目录,并将当前页表切换到init_page_dir,为只有内核可映射的页目录。 
注意在切换页表前,必须先将当前进程cur->pagedir设为NULL,否则切换页表后可能被某个中断影响,又切换回去。在释放页表前,必须先切换页表,否则就是释放当前正在使用的页表。
void process_exit (void);
Intel的x86处理器是通过Ring级别来进行访问控制的,CPU运行级别共分4层,RING0, RING1, RING2, RING3。 
RING0层拥有最高的权限,RING3层拥有最低的权限。按照Intel原有的构想,应用程序工作在RING3层,只能访问RING3层的数据,操作系统工作在RING0层,可以访问所有层的数据,而其他驱动程序位于RING1、RING2层,每一层只能访问本层以及权限更低层的数据。如果普通应用程序企图执行RING0指令,则Windows会显示“非法指令”错误信息。 
Windows只使用其中的两个级别:RING0和RING3。
将TSS中的ring 0栈指针,指向当前进程栈的底部。 
不是很懂用途,将当前进程栈设为最高优先级?
void tss_update (void);
为当前进程的用户代码设置CPU? 
每次上下文切换,都需调用该函数。
void process_activate (void){struct thread *t = thread_current ();/* 激活线程的页表 */pagedir_activate (t->pagedir);/* 设置线程的内核栈,用于处理中断 */tss_update ();}
用于加载ELF二进制文件的定义。取自ELF手册(ELF specification)。
/* ELF types. See [ELF1] 1-2. */typedef uint32_t Elf32_Word, Elf32_Addr, Elf32_Off;typedef uint16_t Elf32_Half;/* For use with ELF types in printf(). */#define PE32Wx PRIx32 /* Print Elf32_Word in hexadecimal. */#define PE32Ax PRIx32 /* Print Elf32_Addr in hexadecimal. */#define PE32Ox PRIx32 /* Print Elf32_Off in hexadecimal. */#define PE32Hx PRIx16 /* Print Elf32_Half in hexadecimal. */
可执行文件的头部(可执行头部?Executable Header)。在每个ELF二进制文件的最开始都会有。
struct Elf32_Ehdr{unsigned char e_ident[16];Elf32_Half e_type;Elf32_Half e_machine;Elf32_Word e_version;Elf32_Addr e_entry;Elf32_Off e_phoff;Elf32_Off e_shoff;Elf32_Word e_flags;Elf32_Half e_ehsize;Elf32_Half e_phentsize;Elf32_Half e_phnum;Elf32_Half e_shentsize;Elf32_Half e_shnum;Elf32_Half e_shstrndx;};
程序的头部(Program Header)。 
会有e_phnum个,从文件的e_phoff处开始。
struct Elf32_Phdr{Elf32_Word p_type;Elf32_Off p_offset;Elf32_Addr p_vaddr;Elf32_Addr p_paddr;Elf32_Word p_filesz;Elf32_Word p_memsz;Elf32_Word p_flags;Elf32_Word p_align;};
p_type, p_flags的可能值。
/* Values for p_type. See [ELF1] 2-3. */#define PT_NULL 0 /* 忽略 */#define PT_LOAD 1 /* 可加载段 */#define PT_DYNAMIC 2 /* 动态链接信息 */#define PT_INTERP 3 /* 动态加载器的名称 */#define PT_NOTE 4 /* 辅助信息 */#define PT_SHLIB 5 /* 保留 */#define PT_PHDR 6 /* 程序头部表 */#define PT_STACK 0x6474e551 /* 堆栈段 *//* Flags for p_flags. See [ELF3] 2-3 and 2-4. */#define PF_X 1 /* 可执行 */#define PF_W 2 /* 可写 */#define PF_R 4 /* 可读 */
从file_name加载一个二进制文件到当前进程。并将可执行文件的入口点(Entry Point)保存到EIP中,将可执行文件的初始栈指针保存到ESP中。 
如果成功返回true,失败返回false。
函数会依次进行:为其创建、分配、激活新的页目录,打开该二进制文件,读并确认该文件的可执行头部,读该程序的头部,为其创建栈,设置EIP为其入口地址,关闭该文件。
bool load (const char *file_name, void (**eip) (void), void **esp);
判断一个程序头部phdr是否描述了一个file中有效的、可加载的段。如果是返回true,否则返回false。
phdr描述了一个file中有效的、可加载的段,必须满足以下条件:
phdr的p_offset和p_vaddr必须有相同的页偏移。
phdr必须指向file内部(其p_offset在file内)。
phdr的memsz大小不小于其p_filesz。
phdr的p_memsz非空(不为0)。
phdr的虚拟内存区域起始(phdr->p_vaddr)和终止位置(phdr->p_vaddr+phdr->p_memsz)都在用户地址空间内部。
phdr的虚拟内存区域不会超出内核的虚拟地址空间,即phdr->p_vaddr + phdr->p_memsz要大于等于phdr->p_vaddr。
不允许映射到页0,即phdr->p_vaddr要大于等于PGSIZE(页0的终止位置)。
static boolvalidate_segment (const struct Elf32_Phdr *phdr, struct file *file);
在地址upage处,加载文件file在ofs处的段。 
从upage开始的read_bytes个字节,会被设为file从ofs开始的对应内容;从upage+read_bytes开始的zero_bytes个字节,会被设为0。 
该函数初始化的这read_bytes+zero_bytes个字节,可写性需与writable一致,即writable为true时,这部分需对用户进程可写,否则只可读。 
如果上述内容成功实现,返回true,否则内存分配错误或硬盘读写错误,返回false。
函数实现大概为:每次分配一页内存,根据read_bytes和zero_bytes确定当前页是从file中复制还是设为0。
static boolload_segment (struct file *file, off_t ofs, uint8_t *upage,uint32_t read_bytes, uint32_t zero_bytes, bool writable);
创建一个最小大小(一页)的栈。成功返回true,失败返回false。 
通过在用户虚拟空间的最顶部,映射一个清0的页实现。
static bool setup_stack (void **esp);
在页表中,添加一个从用户虚拟地址upage到内核虚拟地址kpage的映射。 
若writable为true,用户进程可以修改对应页,否则对应页对用户只读。 
upage必须还未被映射。kpage或许应该是(should probably be?)一个从用户池中获取的页(通过palloc_get_page())。 
如果上述内容成功实现,返回true,否则upage已被映射或内存分配失败,返回false。
static boolinstall_page (void *upage, void *kpage, bool writable){struct thread *t = thread_current ();/* 确认对应虚拟地址还没映射到内核页,然后映射 */return (pagedir_get_page (t->pagedir, upage) == NULL&& pagedir_set_page (t->pagedir, upage, kpage, writable));}