@SovietPower
2021-12-05T14:52:27.000000Z
字数 7574
阅读 1274
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 bool
validate_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 bool
load_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 bool
install_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));
}