xv6学习笔记(3):中断处理和系统调用

1. tvinit函数

这个函数位于main函数内

表明了就是设置idt表

void
tvinit(void)
{
int i; for(i = 0; i < 256; i++)
SETGATE(idt[i], 0, SEG_KCODE<<3, vectors[i], 0);
SETGATE(idt[T_SYSCALL], 1, SEG_KCODE<<3, vectors[T_SYSCALL], DPL_USER); initlock(&tickslock, "time");
}

1. SETGATE函数

这里的setgate是一个宏定义是用来设置idt表的

#define SETGATE(gate, istrap, sel, off, d)                \
{ \
(gate).off_15_0 = (uint)(off) & 0xffff; \
(gate).cs = (sel); \
(gate).args = 0; \
(gate).rsv1 = 0; \
(gate).type = (istrap) ? STS_TG32 : STS_IG32; \
(gate).s = 0; \
(gate).dpl = (d); \
(gate).p = 1; \
(gate).off_31_16 = (uint)(off) >> 16; \
}

下面是函数参数的说明

Sel : 表示对于中断处理程序代码所在段的段选择子

off:表示中断处理程序代码的段内偏移

(gate).gd_off_15_0 : 存储偏移值的低16位

(gate).gd_off_31_16 : 存储偏移值的高16位

(gate).gd_sel : 存储段选择子

(gate).gd_dpl : dpl 表示该段对应的

熟悉了这些之后参考intel的开发手册找一下istrap的值,这里注意系统调用的dpl = 3不然我们无法从用户模式进去

这里只要按照上述宏定义的格式书写就好,而且这里的中断处理函数我们都不用关心怎么实现,只用给他一个占位符。

可以发现这里就是这是IDT表格了

2. idtinit函数

void
idtinit(void)
{
lidt(idt, sizeof(idt));
}

这里就是调用lidt函数

static inline void
lidt(struct gatedesc *p, int size)
{
volatile ushort pd[3]; pd[0] = size-1;
pd[1] = (uint)p;
pd[2] = (uint)p >> 16; asm volatile("lidt (%0)" : : "r" (pd));
}

这个函数最后会调用lidt这个汇编代码

而lidt这个汇编代码做的事情就是把pd加载到GDTR。

也就是有对应的IDT表的基地址 和 IDT表的大小

CS寄存器存储的是内核代码段的段编号SEG_KCODE,offset部分存储的是vector[i]的地址。在XV6系统中,所有的vector[i]地址均指向trapasm.S中的alltraps函数。

2. XV6中断处理过程

1. 中断例子

当XV6的遇到中断志龙,首先CPU硬件会发现这个错误,触发中断处理机制。在中断处理机制中,硬件会执行如下步骤:下面的过程我们成为保护现场xv6官方文档

  1. 从IDT 中获得第 n 个描述符,n 就是 int 的参数。
  2. 检查CS的域 CPL <= DPL,DPL 是描述符中记录的特权级。
  3. 如果目标段选择符的 PL < CPL,就在 CPU 内部的寄存器中保存ESP和SS的值。
  4. 从一个任务段描述符中加载SS和ESP。
  5. 将SS压栈。
  6. 将ESP压栈。
  7. 将EFLAGS压栈。
  8. 将CS压栈。
  9. 将EIP压栈。
  10. 清除EFLAGS的一些位。
  11. 设置CS和EIP为描述符中的值。

此时,由于CS已经被设置为描述符中的值(SEG_KCODE),所以此时已经进入了内核态,并且EIP指向了trapasm.S中alltraps函数的开头。在alltrap函数中,系统将用户寄存器压栈,构建Trap Frame,并且设置数据寄存器段为内核数据段,然后跳转到trap.c中的trap函数。

alltraps继续压入寄存器保存现场,得到trapframe结构体,trapframe结构体如图所示,其中oesp没有用处,这是pushal指令统一压栈的。

.globl alltraps
alltraps:
# Build trap frame.
pushl %ds
pushl %es
pushl %fs
pushl %gs
pushal

这里的pushal就是压入所有通用寄存器

在这之后重新设置段寄存器,进入内核态,压入当前栈esp,然后调用C函数trap处理中断,在trap返回时,弹出esp

# Set up data segments.
movw $(SEG_KDATA<<3), %ax
movw %ax, %ds
movw %ax, %es # Call trap(tf), where tf=%esp
pushl %esp
call trap

trap函数是通过tf->trapno来进行逻辑分支处理的。下面介绍一下系统调用的处理。

系统调用

当tr->trapno是 T_SYSCALL的时候,内核调用syscall函数。

if(tf->trapno == T_SYSCALL){
if(myproc()->killed)
exit();
myproc()->tf = tf;
syscall();
if(myproc()->killed)
exit();
return;
}

这是syscalls的对应数组嗷

extern int sys_chdir(void);
extern int sys_close(void);
extern int sys_dup(void);
extern int sys_exec(void);
extern int sys_exit(void);
extern int sys_fork(void);
extern int sys_fstat(void);
extern int sys_getpid(void);
extern int sys_kill(void);
extern int sys_link(void);
extern int sys_mkdir(void);
extern int sys_mknod(void);
extern int sys_open(void);
extern int sys_pipe(void);
extern int sys_read(void);
extern int sys_sbrk(void);
extern int sys_sleep(void);
extern int sys_unlink(void);
extern int sys_wait(void);
extern int sys_write(void);
extern int sys_uptime(void); static int (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
[SYS_wait] sys_wait,
[SYS_pipe] sys_pipe,
[SYS_read] sys_read,
[SYS_kill] sys_kill,
[SYS_exec] sys_exec,
[SYS_fstat] sys_fstat,
[SYS_chdir] sys_chdir,
[SYS_dup] sys_dup,
[SYS_getpid] sys_getpid,
[SYS_sbrk] sys_sbrk,
[SYS_sleep] sys_sleep,
[SYS_uptime] sys_uptime,
[SYS_open] sys_open,
[SYS_write] sys_write,
[SYS_mknod] sys_mknod,
[SYS_unlink] sys_unlink,
[SYS_link] sys_link,
[SYS_mkdir] sys_mkdir,
[SYS_close] sys_close,
};

这里的systemcall函数利用eax寄存器获得系统调用号。最后的返回值也利用eax寄存器返回

如果系统调用号合理的话,返回值就是对应系统调用函数产生的返回值

void
syscall(void)
{
int num;
struct proc *curproc = myproc(); num = curproc->tf->eax;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
curproc->tf->eax = syscalls[num]();
} else {
cprintf("%d %s: unknown sys call %d\n",
curproc->pid, curproc->name, num);
curproc->tf->eax = -1;
}
}

下面是对于除0的处理。

    if(myproc() == 0 || (tf->cs&3) == 0){
// In kernel, it must be our mistake.
cprintf("unexpected trap %d from cpu %d eip %x (cr2=0x%x)\n",
tf->trapno, cpuid(), tf->eip, rcr2());
panic("trap");
}
// In user space, assume process misbehaved.
cprintf("pid %d %s: trap %d err %d on cpu %d "
"eip 0x%x addr 0x%x--kill proc\n",
myproc()->pid, myproc()->name, tf->trapno,
tf->err, cpuid(), tf->eip, rcr2());
myproc()->killed = 1;

根据触发中断的是内核态还是用户进程,执行不同的处理。如果是用户进程出错了,那么系统会杀死这个用户进程;如果是内核进程出错了,那么在输出一段错误信息后,整个系统进入死循环。

如果是一个可以修复的错误,比如页错误,那么系统会在处理完后返回trap()函数进入trapret()函数,在这个函数中恢复进程的执行上下文,让整个系统返回到触发中断的位置和状态。

2. 系统调用全过程

首先在文件user.h中存储了提供的系统调用,这里以exec这个系统调用为例,考察在用户态执行的整个流程。

// system calls
int fork(void);
int exit(void) __attribute__((noreturn));
int wait(void);
int pipe(int*);
int write(int, const void*, int);
int read(int, void*, int);
int close(int);
int kill(int);
// .......

1. 考虑系统调用号如何传递

这里需要去看一下usys.S和反汇编一下usys.o

1. 首先看去看usys.S

可以发现这里定义了一个宏定义就是根据传递过来的系统调用名称把系统调用号传递到%eax寄存器中

随后触发int中断陷入内核态

#include "syscall.h"
#include "traps.h" #define SYSCALL(name) \
.globl name; \
name: \
movl $SYS_ ## name, %eax; \
int $T_SYSCALL; \
ret SYSCALL(fork)
SYSCALL(exit)
SYSCALL(wait)
SYSCALL(pipe)
SYSCALL(read)
SYSCALL(write)
SYSCALL(close)
SYSCALL(kill)
SYSCALL(exec)
SYSCALL(open)
SYSCALL(mknod)
SYSCALL(unlink)
SYSCALL(fstat)
SYSCALL(link)
SYSCALL(mkdir)
SYSCALL(chdir)
SYSCALL(dup)
SYSCALL(getpid)
SYSCALL(sbrk)
SYSCALL(sleep)
SYSCALL(uptime)
2. 在看usys.o

我们这里反汇编一下usys.o

以fork为例子它把系统调用号1传递给了eax寄存器

3.执行系统调用函数

随后在syscall.c中到syscall函数

在这里利用系统调用号获取对应的系统调用函数

void
syscall(void)
{
int num;
struct proc *curproc = myproc(); num = curproc->tf->eax;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
curproc->tf->eax = syscalls[num]();
} else {
cprintf("%d %s: unknown sys call %d\n",
curproc->pid, curproc->name, num);
curproc->tf->eax = -1;
}
}

exec为例子就是执行这个函数sys_exec进行系统调用处理

2. 系统调用函数执行

经过上面的一顿分析,最后exec系统调用会进入这里进行执行

int
sys_exec(void)
{
char *path, *argv[MAXARG];
int i;
uint uargv, uarg; if(argstr(0, &path) < 0 || argint(1, (int*)&uargv) < 0){
return -1;
}
memset(argv, 0, sizeof(argv));
for(i=0;; i++){
if(i >= NELEM(argv))
return -1;
if(fetchint(uargv+4*i, (int*)&uarg) < 0)
return -1;
if(uarg == 0){
argv[i] = 0;
break;
}
if(fetchstr(uarg, &argv[i]) < 0)
return -1;
}
return exec(path, argv);
}

对于exec而言,exec需要一个可执行文件的路径和需要执行的参数。而获取参数和路径的函数下面来介绍一下

1. argstr函数

可以发现这个函数调用了argint函数以及fetchstr()函数

这里的(myproc()->tf->esp) + 4 + 4*n就是获取上述栈帧里存储的第几个参数

Eg: n = 0 时候就说获取edi寄存器的参数我们以exec为例子第一个参数使用edi寄存器传递的因此就是获取可执行文件的路径的地址

而真正的字符串还要利用fetchstr函数获取

int
argstr(int n, char **pp)
{
int addr;
if(argint(n, &addr) < 0)
return -1;
return fetchstr(addr, pp);
}
2. argint函数
int
argint(int n, int *ip)
{
return fetchint((myproc()->tf->esp) + 4 + 4*n, ip);
}
3. fetchint函数
// Fetch the int at addr from the current process.
int
fetchint(uint addr, int *ip)
{
struct proc *curproc = myproc(); if(addr >= curproc->sz || addr+4 > curproc->sz)
return -1;
*ip = *(int*)(addr);
return 0;
}
4. fetchstr函数
int
fetchstr(uint addr, char **pp)
{
char *s, *ep;
struct proc *curproc = myproc(); if(addr >= curproc->sz)
return -1;
*pp = (char*)addr;
ep = (char*)curproc->sz;
for(s = *pp; s < ep; s++){
if(*s == 0)
return s - *pp;
}
return -1;
}

3. 真正系统调用的执行

而构建好参数之后最后sys_exec实际上会调用exec(path, argv);函数

而exec函数还是比较复杂的这里简单分析一下即可。

  1. 根据提供的path获取文件信息读入到inode
  2. 然后把inode信息解析到elf头中
int
exec(char *path, char **argv)
{
char *s, *last;
int i, off;
uint argc, sz, sp, ustack[3+MAXARG+1];
struct elfhdr elf;
struct inode *ip;
struct proghdr ph;
pde_t *pgdir, *oldpgdir;
struct proc *curproc = myproc(); begin_op(); if((ip = namei(path)) == 0){
end_op();
cprintf("exec: fail\n");
return -1;
}
ilock(ip);
pgdir = 0; // Check ELF header
if(readi(ip, (char*)&elf, 0, sizeof(elf)) != sizeof(elf))
goto bad;
if(elf.magic != ELF_MAGIC)
goto bad;
  1. 这里会给每一个进程分配一个内核页表然后在返回用户空间之前把它copy到用户空间
  2. 然后按照elf段把它分配memory然后加载到内存,分配和加载分别通过allocuvmloaduvm函数实现
  if((pgdir = setupkvm()) == 0)
goto bad; // Load program into memory.
sz = 0;
for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
if(readi(ip, (char*)&ph, off, sizeof(ph)) != sizeof(ph))
goto bad;
if(ph.type != ELF_PROG_LOAD)
continue;
if(ph.memsz < ph.filesz)
goto bad;
if(ph.vaddr + ph.memsz < ph.vaddr)
goto bad;
if((sz = allocuvm(pgdir, sz, ph.vaddr + ph.memsz)) == 0)
goto bad;
if(ph.vaddr % PGSIZE != 0)
goto bad;
if(loaduvm(pgdir, (char*)ph.vaddr, ip, ph.off, ph.filesz) < 0)
goto bad;
}

算了这里先看一下分配和加载分别是如何做的‘

allocuvm函数

这个函数就是逐页为每一段分配页表并做对应的映射。

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;
}

loaduvm函数

这里加载到了内核的高地址区域(说实话现在还不懂为啥要这样做。后面慢慢来吧

// Load a program segment into pgdir.  addr must be page-aligned
// and the pages from addr to addr+sz must already be mapped.
int
loaduvm(pde_t *pgdir, char *addr, struct inode *ip, uint offset, uint sz)
{
uint i, pa, n;
pte_t *pte; if((uint) addr % PGSIZE != 0)
panic("loaduvm: addr must be page aligned");
for(i = 0; i < sz; i += PGSIZE){
if((pte = walkpgdir(pgdir, addr+i, 0)) == 0)
panic("loaduvm: address should exist");
pa = PTE_ADDR(*pte);
if(sz - i < PGSIZE)
n = sz - i;
else
n = PGSIZE;
if(readi(ip, P2V(pa), offset+i, n) != n)
return -1;
}
return 0;
}
  1. 这里用来构建参数
  2. 然后为返回用户空间做准备
  3. 这里把curproc->tf->eip = elf.entry;这样就设置好了所需要执行函数的入口地址
 iunlockput(ip);
end_op();
ip = 0; // Allocate two pages at the next page boundary.
// Make the first inaccessible. Use the second as the user stack.
sz = PGROUNDUP(sz);
if((sz = allocuvm(pgdir, sz, sz + 2*PGSIZE)) == 0)
goto bad;
clearpteu(pgdir, (char*)(sz - 2*PGSIZE));
sp = sz; // Push argument strings, prepare rest of stack in ustack.
for(argc = 0; argv[argc]; argc++) {
if(argc >= MAXARG)
goto bad;
sp = (sp - (strlen(argv[argc]) + 1)) & ~3;
if(copyout(pgdir, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
goto bad;
ustack[3+argc] = sp;
}
ustack[3+argc] = 0; ustack[0] = 0xffffffff; // fake return PC
ustack[1] = argc;
ustack[2] = sp - (argc+1)*4; // argv pointer sp -= (3+argc+1) * 4;
if(copyout(pgdir, sp, ustack, (3+argc+1)*4) < 0)
goto bad; // Save program name for debugging.
for(last=s=path; *s; s++)
if(*s == '/')
last = s+1;
safestrcpy(curproc->name, last, sizeof(curproc->name)); // Commit to the user image.
oldpgdir = curproc->pgdir;
curproc->pgdir = pgdir;
curproc->sz = sz;
curproc->tf->eip = elf.entry; // main
curproc->tf->esp = sp;
switchuvm(curproc);
freevm(oldpgdir);
return 0; bad:
if(pgdir)
freevm(pgdir);
if(ip){
iunlockput(ip);
end_op();
}
return -1;
}

参考

博客1

博客2

xv6中文文档

xv6学习笔记(3):中断处理和系统调用的更多相关文章

  1. xv6学习笔记(4) : 进程调度

    xv6学习笔记(4) : 进程 xv6所有程序都是单进程.单线程程序.要明白这个概念才好继续往下看 1. XV6中进程相关的数据结构 在XV6中,与进程有关的数据结构如下 // Per-process ...

  2. XV6学习笔记(1) : 启动与加载

    XV6学习笔记(1) 1. 启动与加载 首先我们先来分析pc的启动.其实这个都是老生常谈了,但是还是很重要的(也不知道面试官考不考这玩意), 1. 启动的第一件事-bios 首先启动的第一件事就是运行 ...

  3. XV6学习笔记(2) :内存管理

    XV6学习笔记(2) :内存管理 在学习笔记1中,完成了对于pc启动和加载的过程.目前已经可以开始在c语言代码中运行了,而当前已经开启了分页模式,不过是两个4mb的大的内存页,而没有开启小的内存页.接 ...

  4. xv6学习笔记(5) : 锁与管道与多cpu

    xv6学习笔记(5) : 锁与管道与多cpu 1. xv6锁结构 1. xv6操作系统要求在内核临界区操作时中断必须关闭. 如果此时中断开启,那么可能会出现以下死锁情况: 进程A在内核态运行并拿下了p ...

  5. 20135202闫佳歆--week4 系统调用(上)--学习笔记

    此为个人笔记存档 week 4 系统调用(上) 一.用户态.内核态和中断处理过程 用户通过库函数与系统调用联系起来. 1.内核态 在高执行级别下,代码可以执行特权指令,访问任意的物理地址. 2.用户态 ...

  6. 20135202闫佳歆--week5 系统调用(下)--学习笔记

    此为个人笔记存档 week 5 系统调用(下) 一.给MenuOS增加time和time-asm命令 这里老师示范的时候是已经做好的了: rm menu -rf 强制删除 git clone http ...

  7. 《Linux内核分析》第八周学习笔记

    <Linux内核分析>第八周学习笔记 进程的切换和系统的一般执行过程 郭垚 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163 ...

  8. 《Linux内核分析》第五周学习笔记

    <Linux内核分析>第五周学习笔记 扒开系统调用的三层皮(下) 郭垚 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.c ...

  9. 《Linux内核分析》第四周学习笔记

    <Linux内核分析>第四周学习笔记 扒开系统调用的三层皮(上) 郭垚 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.c ...

随机推荐

  1. 嵌入式Linux会议LinuxCon欧洲的时间表公布

    From: http://linuxgizmos.com/embedded-linux-conference-and-linuxcon-europe-schedules-posted/ Linux基金 ...

  2. Linux 命令行通配符及转义符的实现

    我们想对一类文件批量操作,例如批量查看硬盘文件属性,那么正常命令会是: [root@linuxprobe ~]# ls /dev/sda [root@linuxprobe ~]# ls /dev/sd ...

  3. Robotframework学习笔记之一Common Resource导入的Library库显示红色(导入失败)

    第一次使用Robotframework,所以也遇到了很多的坑,导入项目后 ,一些自带的库显示红色,导入失败!(ps:自带的库也显示红色) Ride日志如下(Tools--view ride log): ...

  4. 二进制方式搭建Kubernetes集群

    环境准备: 演练暂时用单节点一台master和一台node节点来进行部署搭建(kubernetes 1.19版本) 角色 IP 组件 master 10.129.246.114 kube-apiser ...

  5. Java | 集合(Collection)和迭代器(Iterator)

    集合(Collection) 集合就是Java中提供的一种 空器,可以用来存储多个数据. 集合和数组都是一个容器,它们有什么区别呢? 数组的长度是固定的,集合的长度是可变的. 数组中存储的是同一类型的 ...

  6. (精)题解 guP2860 [USACO06JAN]冗余路径Redundant Paths

    (写题解不容易,来我的博客玩玩咯qwq~) 该题考察的知识点是边双连通分量 边双连通分量即一个无向图中,去掉一条边后仍互相连通的极大子图.(单独的一个点也可能是一个边双连通分量) 换言之,一个边双连通 ...

  7. CTF-Streamgame2-writeup

    Streamgame2 题目信息: 附件: streamgame2.py from flag import flag assert flag.startswith("flag{") ...

  8. asp.net web.config数据库连接字符串加密与解密

    在WEB网站开发过程中,如果我们将数据库连接字符串封装到.DLL文件中,将会给数据库和程序的迁移带来麻烦,因为万一服务器地址或者数据库发生变更,那么我们就不得不修改源程序并重新将其编译.最好的解决方法 ...

  9. 备战-Java IO

    备战-Java IO 君如载酒须尽醉,醉来不复思天涯. 简介:备战-Java IO. 一.概述 Java 的 I/O 大概可以分成以下几类: 磁盘操作:File 字节操作:InputStream 和 ...

  10. HelloWord!

    HelloWorld 1.新建一个文件夹,存放代码 2.新建一个java文件后缀为.java Hello.java (注意后缀 如系统没打开显示后缀需要打开) 3.编写HelloWorld代码 (建议 ...