操作系统Lab3 User environments
本次 lab 的目标是创建进程运行环境。在本实验中,(1)将实现运行受保护的用户模式环境(即“进程”)所需的基本内核功能(2)将完善JOS内核,以设置数据结构来跟踪用户环境,创建单个用户环境,将程序映像加载到其中并开始运行(3)还将使JOS内核能够处理用户环境发出的任何系统调用并处理它引起的任何其他异常。
满分代码,附有详细注释
传送门:https://download.csdn.net/download/qhaaha/13741545
PartA 代码实现
练习1:修改kern/pmap.c中的mem_init(),完成envs数组的分配与映射。
看到这部分代码善良的Ta已经帮忙完成,去看一下具体干了什么:
mem_init()函数的主要工作是为内核初始化地址空间,其中一个很重要的函数就是boot_alloc(uint32_t n),其作用为分配连续的物理内存页以容纳n个字节,并通过与从链接器处得到的内存末尾指针end对比,判断是否超出限制,如果超出发出一个panic。
所以我们要完成的代码(Ta帮忙给出)就是分配包含NENV个env类型元素的内存页并初始化之:
练习2:完成kern/env.c中几个与环境有关的函数
1.
env_init():初始化envs中所有环境,并把它们放入env_free_list中。
参数初始化要求看注释,注意这么一句话
所以这里倒序遍历方便向链表中添加元素并且结束时env_free_list正好结束在头指针的位置。直接上代码:
2.
env_setup_vm(Env *e)这个函数为环境e初始化内核部分的虚拟内存映射。关于虚拟内存映射lab2中已经详尽提过,这里花一些篇幅再复习一下:(地址映射的内容,可跳过,直接移步这个练习的代码,其实也就一行:
)
由于linux中段基址总是0,所以线性地址总是等于虚拟地址,页表(两级页表结构分为页目录(page directory)和页表(page table))完成了虚拟地址到物理地址的映射,结构如下图。
虚拟地址空间再inc/memlayout.h有详尽的定义:从高地址到低地址列出主要部分
~~~~注意这里是分界线
注意上面我画出的分界线,可以粗略的理解为UTOP上面的是内核的区域,就是说所有env在UTOP上面区域的虚拟内存应该是一样的,所以这个函数要做的事情就明朗了,就是将这一部分虚拟地址,按照内核页表映射到对应的物理地址上。
还要注意这句话:
意思是一个物理页被UTOP之上的虚存区域映射到,不需要使得pp_ref++,这是合理的因为所有env的UTOP之上的虚存实际上都是按照内核页表映射的,pp_ref统计的是用户部分页表的映射数,这在lab2中已经说到过。但是env_pgdir作为一个页是例外,它的被映射数必须加1,为了在env_free时不出错。
代码善良的Ta又帮助我们完成过了,还是读一遍看一下做了哪些工作。
首先调用了page_alloc函数为env的页目录分配了一行,page_alloc(lab2)定义在pmap.c中,作用是从空闲页page_free_list中取出一页,返回之,并更新空闲页链表如果没有空闲页了返回NULL。
然后就是Ta写的部分了:
这一页的计数器加1,然后将这一页作为env的页目录。注意看到第二行使用了KADDR(page2pa§),所以说e->env_pgdir实际存入了什么?p是一个PageInfo* 型的指针,通过page2pa转化为物理地址,再通过KADDR转化为内核页表逆映射后的内核看到的虚拟地址。
这是大致的转化关系图,至于内核页表为什么能完成从物理地址的“逆映射”,在lab2中有叙述:实际上,为了方便内核实现这种逆映射,从线性地址0xf0000000开始,直接映射到了从0开始的整个物理内存,这JOS注释中称这种做法非常的“magic”。
也就是说e->env_pgdir实际保存的是内核看到的该env_pgdir的虚拟地址,为什么这么做呢?因为env_pgdir在内核区域的高线性地址,在这个区域内env_pgdir和内核页表是一样的,都会映射到正确的pgdir物理地址上。换言之,对于一个env,在你还没有获得其页目录env_pgdir的物理地址时,你就可以通过它固有的内核区域页表找到它的页目录,之后再借助页目录完成用户区域虚拟地址的映射。
再补充说明一下为什么env_pgdir在内核区域的高线性地址,原因很简单且已经提到过:从线性地址0xf0000000开始,直接映射到了从0开始的整个物理内存。
至于是怎么分配的这一页,就是复习lab2了:这里的page p通过page_alloc分配,是从page_free_list中取出的页,也就是pages[]中的页。而pages不是实际的页,而是通过boot_alloc分配的PageInfo*型数组,从end(虚拟内存内核bss段结束的地方),pages[i]是可以简单映射到物理地址 i << PGSHIFT处的。
比较绕,上图:
回到函数中,借助memcopy我们已经完成了页目录内核部分的复制,现在关心的是UVPT部分了。lab2说到过内核页表对这部分的处理:
在页目录中线性地址UVPT到UVPT+ PTSIZE这一部分的指针并没有指向其对应的页表,而是指向页目录本身。要想理解这么做的妙处,首先要明白以下几点:
1.一般程序能直接使用到的,都是逻辑地址(本实验中也即线性地址),真正的物理地址对于用户程序(和对内核的大部分也希望如此)是不可见的。
2.当一个进程使用一个逻辑(线性)地址时,操作系统根据该进程的页表自动实现到物理地址的转换。
在这种情况下,页表条目对于使用线性地址的程序而言,是难以访问的到的,因为它无法使用页表的物理地址来访问它,所有地址会被按照线性地址的方式来翻译。
而这种指针自指的方法解决了这个问题,可以看到,虚拟地址uvpt[N],就会被页表自动翻译为页目录中第N张页表的首地址。其实现的原理为:声明uvpt为32位型的指针,uvpt首先被翻译为directory数组的首地址,其每个条目为32位,代表一个页表指针。这样,uvpt[N]就自然而然地被翻译为了第N个条目的值,也就是N号页表的地址!
这样我们就实现了用线性地址来访问页表了。当然,还可以用uvpt[uvpt]来访问“uvpt号页表“—也就是页目录!uvpt[uvpt]实际上对应的线性地址为:uvpt + (uvpt >> 10),这里代码注释里有一个错误,它右移了12位,应该再乘以4。附上图:
同理,我们需要把env_pgdir中这一部分的pde设置为该env的页目录本身:
这样第二个函数就完成了,花了很大篇幅回顾内存映射,不过磨刀不误砍柴工,后面用起来就方便多了。
3.
region_alloc(struct Env *e, void *va, size_t len) 为环境e 开辟len 个byte大小的物理空间,并将va虚拟地址开始的len长度大小的空间和物理空间建立映射关系。
前面两个函数已经完成了环境的初始化和为环境分配初始的页表。由于我们知道“操作系统给每个进程(这里的环境)以独享内存空间的假象“,所以接下来实现环境e从虚拟地址到物理地址的映射是顺理成章的。
如果这块虚拟地址已经被映射到了物理地址,就跳过,借助page_lookup完成判断:
在处理边界条件之后,通过调用page_insert建立虚拟地址与物理地址的映射。
这里调用了pmap.c中几个page处理函数,我不在这篇报告里展开写了, lab2中有写~
4.
load_icode(struct Env *e, uint8_t *binary)函数将ELF格式的二进制映像binary加载到环境e中,这是一个很有挑战的函数,但是Ta又又帮我完成咯!还是老规矩去读一下:
按照Hints的提示,注意到以下几点:
(1)该函数要做的核心部分是将ELF文件中的每个段ph,从binary + ph->p_offset开始的ph->p_filesz 个字节复制到虚拟内存ph->p_va处,其中p_va是pgramhdr定义中规定的段的第一个字节所对应的虚拟内存位置。
(2)ELF中的e_phoff和e_phnum分别指明了这个ELF文件中第一个段的位置(ph的开始)和总共的段数(也就是program header 的数量)。
(3)在开始加载内存之前要先切换页表,从kern_pgdir到env_pgdir:
(回答了后面的问题B.1)
其中页表的切换用到了inc/x86.c中定义的内联汇编(后面有专门说到):
static inline void
lcr3(uint32_t val)
{
asm volatile(“movl %0,%%cr3” : : “r” (val));
}
注意到lcr3这个函数告诉编译器将控制寄存器cr3的值设为环境e页表的物理地址,我查到了cr3寄存器正是页目录基址寄存器,用于保存页目录表的物理地址。
之后就完成对(2)每个段的拷贝,需要拷贝的内容和目的地虚拟地址如(1)所述。
注意只有p_type 为 ELF_PROG_LOAD的段才需要复制。
最后为栈空间分配一页:
5.
env_create(uint8_t *binary, enum EnvType type)作用是创建环境,并将binary中的代码载入。这个函数很简单,按照提示调用前面已定义好的env_alloc和load_icode就可以了。
6.
env_run(struct Env *e)真正切换环境的函数,首先看到Hint里有写此函数将从e->env_tf加载程序状态,向前翻在load_icode函数中已经正确设置入口地址:
然后就按照提示的步骤完成就好了,注意要切换页表和按照保存在trapframe中的值恢复寄存器:
练习2目前已经完成了,总结一下这个练习中完成的函数的调用关系:
env_create()
–>env_alloc()
–>env_setup_vm()
–>load_icode()
–>region_alloc()
同时env_create函数与上层函数i386_init的关系实验介绍中已经给出:
• start (kern/entry.S)
• i386_init (kern/init.c)
o cons_init
o mem_init
o env_init
o trap_init (still incomplete at this point)
o env_create
o env_run
env_pop_tf
练习3:阅读题,略过。
练习4:
这一部分是编写处理异常和中断部分的代码。主要用到的函数在kern/trap.c和kern/trapentry.s中。实验提示中有这样一段话:Each exception or interrupt should have its own handler in trapentry.S and trap_init() should initialize the IDT with the addresses of these handlers. Each of the handlers should build a struct Trapframe (see inc/trap.h) on the stack and call trap() (in trap.c) with a pointer to the Trapframe.它提示我这部分工作主要是:将异常和中断与它们的处理函数建立连接,每个处理函数都会在完成一系列准备工作后调用陷入内核的函数trap()——trap()只具有一个类型为trapframe的参数。我的工作就从阅读kern/trap.c中的trap()函数展开。
在阅读这部分代码之前,需要先做一些准备工作:
(1)内联汇编
asm [ volatile ] (
assembler template
[ : output operands ] /* optional /
[ : input operands ] / optional /
[ : list of clobbered registers ] / optional */
);
JOS代码中基本都使用了asm volatile 的关键字,volatile 是指表示不需要gcc对下面的汇编代码做任何优化。之后括号中在汇编指令之后跟着至多3个冒号,他们之后的内容分别表示:输出、输入、以及不包含在输入输出中的在操作中可能被改变了的寄存器列表(用于提醒编译器)。
例子:
asm volatile(“inw %w1,%0” : “=a” (data) : “d” (port));
参数”=a”是约束:“=”是output operand字段特有的约束,表示该操作数是只写的(write-only);“a”表示先将命令执行结果输出至%eax,然后再由寄存器%eax更新位于内存中的out_var。”d”也类似。常见的约束”r”表示任意选择寄存器(由编译器)。
(2)trapframe
trapframe定义在inc/trap.h中,在inc/env.h中对环境的定义里就出现了trapframe:
struct Env {
struct Trapframe env_tf; // Saved registers
…
它中断、自陷、异常进入内核后,在堆栈上形成的一种数据结构,保存了关键寄存器。
具体来讲,在x86体系下,上述三种机制都会触发相同的硬件操作,完成部分保护现场的任务,通过将部分寄存器值压入堆栈来实现保护现场。如果触发中断(由于系统调用、中断、异常具有相同的处理机制,所以全以中断代称)前处于内核态,则直接在当前栈中保护现场;如果处于用户态,则根据任务栈描述符得到新的内核栈并压入用户态的ss和esp。在硬件完成操作后,栈中会得到下图数据。
(参考自https://blog.csdn.net/qq_25426415/article/details/54647862)
初步阅读
做完准备活动就开始从trap.c的主体-trap(tf)读起,大致过程如下。
(1)cld使得方向状态位DF复位,注意这里内联汇编第三个冒号后的“cc”提醒编译器可能被cld指令修改。
(2)断言(一个叫assert的宏用于在断言失败时抛出异常)中断已经被禁止。这一步暂时还不太懂是为了什么,先存疑。
(3)然后是用户态陷入内核的处理。这里要做的是将curenv(当前环境)中的env_tf修改为从trap传进来的参数tf;然后是这样的一步tf = &curenv->env_tf,他给的注释为“The trapframe on the stack should be ignored from here on.”意思是在程序调用栈上的trapframe从现在起要被忽略了。不大懂为什么,是防止这个地址的值被修改吗?为什么要保护本来地址上的值?这些放到后面再来探索吧。
之后是trap_dispatch(tf),看来是分配给“下级干活儿”。
之后就处理完了,继续run:
trap()函数在哪里被使用了呢?trapentry.S中用汇编代码调用了trap,并在此之前完成了参数入栈。xv6简单地将所有的中断处理程序指向alltraps,由alltraps来负责具体的处理。在调用alltraps之前,xv6统一压入errnum和trapnum来区分是256情况中的哪种。
代码完成
看完trap()发生的大致过程,就需要完成trap()的准备过程了。首先要做的是将中断与处理程序handler建立映射。这是在trap_init()中完成的。这是我们要完成的一部分代码,先看几个用到的数据结构和函数:
(1)idt是我们的中断描述符表。
(2)idt的每个表项由8个字节组成,其中的每个表项叫做一个门描述符(Gate Descriptor), “门”的含义是指当中断发生时必须先访问这些“门”,能够“开门”(即将要进行的处理需通过特权检查,符合设定的权限等约束)后,然后才能进入相应的处理程序。而门描述符则描述了“门”的属性(如特权级、段内偏移量等)。Gatedesc的定义可以在inc/mmu.h中看到:
(3)同样在mmu.h中,定义了一个叫做SETGATE的宏,用来初始化一个门:
(4)每个异常或中断都应在trapentry.S中具有自己的处理程序。trap_init()应使用这些处理程序的地址初始化IDT。 每个处理程序都应在栈上构建一个Trapframe,并以指向Trapframe的指针作为参数调用trap()。 然后trap()处理异常/中断或将其分派到特定的处理函数。
那就从trapentry.S开始吧,首先看到它定义了两个宏TRAPHANDLER(name, num)和TRAPHANDLER_NOEC(name, num) ,注释说的很清楚,这两个宏都是将名叫name的处理函数和num号的中断绑定,不同之处是是否传入了错误码这个参数。那就利用这两个宏定义我们处理函数的接口:
然后我们需要构造.alltraps,它是从handler到trap的桥梁,主要工作是准备好trapframe并将指向trapframe的指针作为参数调用trap。
注意注释里这句话:You shouldn’t call a TRAPHANDLER function from C, but you may
need to declare one in C (for instance, to get a function pointer during IDT setup).
所以还要去trap.c里面声明函数入口。
回到kern/trap.c中,在trap_init函数中用前面说到的这个SETGETE宏初始化IDT就ok了:
练习5和练习6
这两个练习都是修改之前说到的“trap函数的下级”—trap_dispatch,使得可以完成缺页中断和调试中断的处理,这部分很简单,直接在trap_dispatch里面添加两个case就可以了。
练习7
这个练习是完成系统调用syscall部分的编写,由于在前面的部分IDT中已经为syscall分配好了处理函数,现在直接进入kern/syscall.c,完成syscall()函数就可以了。注意到syscall.c中除了syscall这个函数之外其他都不在其头文件syscall.h中,那就可以推断前面帮忙实现的函数都是用来实现syscall的“武器”,换句话说是syscall中要用到的。这样看来难度就不大了,我需要完成的工作就是根据系统调用类型的不同传一传参数,至于参数的顺序什么的,参考系统调用对用户程序的接口—lib/syscall.c就可以了。(完整的过程是:用户程序通过调用lib中的syscall函数(其实是一个内联汇编int中断指令)陷入内核,后kern/syscall.c完成系统调用。)
syscall函数完成后,还需要完成sys_cputs(const char *s, size_t len),如其名字所言就是打印len个的字符到控制台。
后面的sys_show_environments补充写了用户程序完成系统调用的机制。
练习8
这个练习是理解并完善用户程序的引导过程。用户程序首先在lib/entry.S中运行,完成一系列设置后call libmain,进入libmain函数中。
libmain函数可以看成是一般的用户程序的引导函数,在这里我只需要修改一行,按他所说:
thisenv = envs;使全局指针thisenv指向我们前面用来保存环境的envs。
之后它根据命令行参数调用对应的用户程序(如果是从内核调用的用户程序,不含有命令行参数,不过不用担心,entry.S已经完成了工作,见下图)
用户程序执行结束后,libmain会显式调用exit优雅地终止。
完成这个练习之后,重新make qemu引导内核,就会看到这样的输出:
红色部分执行了hello.c中的代码,之后可以看到exit“优雅地退出”。
后面的练习9,10没有要求,我看了看是关于缺页中断和内存的,那就之后再回来看吧!
回顾–完整测试过程
为了回顾已经完成的工作,我从内核引导开始gdb了一遍理清楚进程的运行方向。大致过程列出来:
(1)从地址为(FFFF : 0)的BIOS程序开始, BIOS将执行某些系统检测,并在物理地址0处开始初始化中断向量。 此后,它将可启动设备的第一个扇区(磁盘引导扇区,512字节)读入绝对内存地址 0x7C00 处,也就是kern/boot.S的中的内容,并跳转到这个地方。
(2)从boot.S的开始完成操作系统内核引导,一系列操作之后,将内核代码段等加载到物理地址0x10000开始的一段,之后调用根据ELF头中的entry调用内核的entry.S。
(3)由于在此之前还没有虚拟内存的映射,为了entry.S完成一部分关于初始虚存转换的设置后调用i386_init。
(4)i386_init开始执行我们编写的C代码,完成内存、环境、中断等的初始化,之后调用我们唯一的用户程序—hello。
整个过程中代码运行方向:
BIOS -> boot -> kern/entry -> i386_init
补充练习
lab任务还要求我们完成一个展示现有环境的SYS_show_environments系统调用,在此之前先补充一下用户程序完成系统调用的过程。
很多操作,在操作系统设计之初是不希望给予用户直接的访问权限的,就比如说我们要写的这个打印所有env的信息,env的信息不希望能被用户程序直接访问;但同时又有很多不希望直接被使用的操作是用户程序必须的,比如说常见的对控制台的输入输出等。所以,用户程序如果想使用到这些受保护的行为,就需要进入内核态完成操作,实际上就是通过系统调用(system call)实现的。
为了方便,库函数就应运而生。JOS给出的代码目录lib中的函数,其实是可以近似的看作现实编程中用到的C标准库的,他为用户程序提供了接口访问内核变量,使用定义在内核中的函数。在lib中定义的函数按照对应的参数,通过内联汇编(前面有写)的方式,借助int触发一个软件中断陷入内核,完成处理程序。
所以用户程序只需要包含库,库中定义的syscall会设置参数陷入内核完成操作(中断),中断完成后,内核调用iret(后面题目会说到)回到用户程序继续执行。可以用几张图看一下:
以hello.c为例,包含库头。
定义在lib/syscall.c中的系统调用,可以看到使用汇编指令陷入内核。
kern/trap.c中断操作完成后,回到重新回到用户程序
注意看到env_run的最后调用了env_pop_tf(&(e->env_tf)),后者有这样的操作
恢复了寄存器,恢复上下文回到用户env。
理清思路后开始coding
首先inc/syscall.h的enum中加上这个名字
然后在kern/syscall.c中完成这个调用的处理程序,也就是打印呗,遍历envs不是free的打印出来想要的信息,不必多说了,如斯:
再去把他作为kern/syscall.c中syscall函数的一个情况
再去lib/syscall.c中写这个函数,其实lib中的这个函数就是设置好参数,调用lib
中的syscall,直接陷入内核就可以了。同时这个函数除了调用名也不需要参数别的参数。
然后就完事了!现在hello中可以调用这个函数来打印已有env了,我是添加在这里:
看一下运行效果,可以看到因为只有hello这一个env,只打印了这一条
运行截图
代码部分到这里就结束了,make grade截图:
PartB问题回答
1. JOS 中, 内核态页表基地址定义在哪个变量中? 页表基地址存在哪个寄存器中?
JOS 中如何切换页表基地址空间?
内核态页表基地址:kern_pgdir
页表基地址:e->env_pgdir , 运行时存在寄存器cr3中
切换页表基地址空间
lcr3内联汇编: asm volatile(“movl %0,%%cr3” : : “r” (val));(详见load_icode部分代码说明)
2. iret 指令的功能和作用是什么? kernel stack 的栈顶在在哪里定义? Exception 陷入 kernel 的时候,esp, eip, eax, ebx 分别是谁(processer,jos kernel)保存的?
(1)IRET(interrupt return)中断返回,中断服务程序的最后一条指令。IRET指令将推入堆栈的段地址和偏移地址弹出,使程序返回到原来发生中断的地方。其作用是从中断中恢复中断前的状态,具体作用有如下三点:
1.恢复IP(instruction pointer):(IP)←((SP)+1:(SP)),(SP)←(SP)+2
2.恢复CS(code segment):(CS)←((SP)+1:(SP)),(SP)←(SP)+2
3.恢复中断前的PSW(program status word),即恢复中断前的标志寄存器的状态。
(FR)←((SP)+1:(SP)),(SP)←(SP)+2
4.恢复ESP(返回权限发生变化)
5.恢复SS(返回权限发生变化)
(参考:https://www.cnblogs.com/chuijingjing/p/9319406.html)
(2)kern stack的栈顶KSTACKTOP,定义在inc/memlayout.h,其值 = KERNBASE,是线性地址0xf0000000处。
(3)这道题不很确定,trapentry.S中alltrap这里用pushal压入了一些寄存器
手册上是这样写的:
那按理说题目中的eax,ebx,esp都应该是内核负责保存的,但是又查到一些文章中写:运行级别提升时需切换堆栈,因此CPU会多压入用户态的堆栈指针esp。
那只能姑且回答:eax,ebx是kernel保存,eip是processer保存,esp被kernel和processer保存。
(参考:
https://blog.csdn.net/zhuichao001/article/details/5686058
https://blog.csdn.net/lovelycheng/article/details/78359955?utm_source=blogxgwz7
https://blog.csdn.net/lovelycheng/article/details/78359955?utm_source=blogxgwz7 )
3. IDT 和 GDT 存储的信息分别是什么?
GDT:存储全局段描述符,整个系统中只有一张(对照于Windows中的LDT——局部描述符表)。具体来讲:
(1)C语言程序中用到的指针,使用的都是逻辑地址,实际上为段内偏移量,为了转化为线性地址,还需要加上该程序所在的段的段基址。段基址保存在哪里,就是段描述符中。段描述符有64位,包含段基址、限长、访问权限信息。
(2)在实模式下,使用16-bit段寄存器CS(Code Segment),DS(Data Segment),SS(Stack Segment)来指定段,CPU将段寄存器中的数值向左偏移4-bit,放到20-bit的地址线上就成为20-bit的基地址。
(3)对于32位或是64位的系统,如果我们直接通过一个64-bit段描述符来引用一个段的时候,就必须使用一个64-bit长的段寄存器装入这个段描述符。但Intel为了保持向后兼容,将段寄存器仍然规定为16-bit。为了解决这一问题,需要把这些长度为64-bit的段描述符放入一个数组中,而将段寄存器中的值作为下标索引来间接引用(事实上,是将段寄存器中的高13-bit的内容作为索引)。这个全局的数组就是GDT——段描述符表。
(4)32位汇编中16位段寄存器(CS、DS、ES、SS、FS、GS)中不再存放段基址,而是段描述符在段描述符表中的索引值,这个索引值也叫段选择符(段选择子)。
总而言之,把所有段描述符按顺序组织成线性表放在内存,这就是段描述符表GDT。GDT包含系统使用的以及全局的代码段、数据段、堆栈段和特殊数据段描述符,事实上,在Linux下,即便是进程独有的段,其段描述符也是放在GDT中的,Linux省略了局部描述符表LDT。
由xxxx:yyyyyyyy(段选择器:偏移量)进行寻址的过程:
① 先从GDTR寄存器中获得GDT基址。
② 然后再GDT中以段选择符高13位位置索引值得到段描述符。
③ 段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址yyyyyyyy才得到最后的线性地址。
(参考:https://blog.csdn.net/Six_666A/article/details/80634972)
同时还有一点值得注意,看lab2中有这样的描述
也就是说,JOS把GDT中所有的段基址都为0,也就是逻辑地址(段偏移量)就是线性地址,这一点前面已经提到过。
IDT:中断描述符表,同GDT存储段描述符一样,IDT存储的每一项为中断描述符,又称门描述符,主要含有一个指针(段选择符+段偏移量)——用于指定中断处理程序地址,以及一些其他标志位——用于区分中断门、陷阱门、任务门,以及确定中断处理的privilege level。
x86将中断编号1~256,相当于GDT中的段选择子,用来索引中断描述符表。
参考资料
https://blog.csdn.net/qq_25426415/article/details/54647862
https://www.cnblogs.com/chuijingjing/p/9319406.html
https://blog.csdn.net/zhuichao001/article/details/5686058
https://blog.csdn.net/lovelycheng/article/details/78359955?utm_source=blogxgwz7
https://blog.csdn.net/lovelycheng/article/details/78359955?utm_source=blogxgwz7
https://blog.csdn.net/Six_666A/article/details/80634972
https://blog.csdn.net/cinmyheart/article/details/40023965