免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 7455 | 回复: 1

[FreeBSD] freebsd9.2-内核如何实现IA32中断处理 [复制链接]

论坛徽章:
0
发表于 2014-07-05 16:40 |显示全部楼层
本帖最后由 71v5 于 2014-07-05 17:13 编辑

先简单描述一下中断和异常相关的基本概念:
中断是一种机制,这种机制使I/O设备能够打断cpu当前正在进行的计算,这种机制使cpu能够同时进行计算和I/O操作。

异常,当cpu在执行指令的时候探测到一个错误条件,就会产生一个异常,比如被零除,访问的数据当前不在内存中等。

不管是中断还是异常,内核是通过中断向量号来识别的,cpu将中断向量号做为中断描述表的索引,相应的表项中包含了
中断处理程序或者异常处理程序的地址。

这里将不讨论I/O设备产生的中断,因为其牵扯的内容多且有点复杂,留作以后分析。

因为异常是cpu内部识别的,所以其对应的中断向量号是固定的,处理期间中断对应的中断向量号也是固定的,将从这些
中断和异常里面摘取几个典型简要分析一下,看看在能处理异常和中断前,freebsd9.2所完成的初始化工作以及
IA32提供的中断处理机制。



[IA32:高级可编程中断控制器(APIC)]-对于APIC,下面简单介绍一下相关概念以及用来发送处理器间中断的寄存器,详细信息
可以参考"Intel 64 and IA-32 ArchitecturesSoftware Developer’s Manual Volume 3A"第十章,将处理器间中断简称为IPI。

在IA32中,APIC分为local APIC和I/O APIC两种:
local APIC,主要用来完成:
1:从cpu的处理器引脚,或者外部中断源,或者外部的I/O APIC接收中断,并将中断发送给处理器核心进行处理。
2:在SMP系统中,local APIC从系统总线上接收其它cpu发送的IPI消息或者将IPI消息通过系统总线发送给其它cpu,IPI消息
   也可以用来在系统中的cpu间分发中断。

I/O APIC:主要用来接收I/O设备产生的中断,并且将中断转换为中断消息发送给local APIC,关于I/O 1PIC将不会去深入分析,
          知道其作用就行;作为一个实例,大家可以参考82093AA I/O APIC这个中断控制器芯片的详细说明。

local APIC和I/O APIC的关系图如下所示:
apic-io.jpg
local APIC包含了一组寄存器,在local APIC能正确工作之前,要初始化这些寄存器,其中有些寄存器是只读的,在SMP系统的配置
中,这组寄存器被映射到一个大小为4KB的连续物理地址空间中,起始物理地址为FEE00000H,当然也可以通过修改IA32_APIC_BASE MSR
这个寄存器来更改起始物理地址。

下面简单描述一下两个local APIC寄存器:Local APIC ID Register和Interrupt Command Register。

[Local APIC ID Register]:
在加电时,系统硬件会给每个local APIC分配一个唯一的APIC ID,系统硬件基于系统拓扑以及编码插槽位置信息等获取一个APIC ID。
在SMP系统中,BIOS和操作系统使用local APIC ID 作为cpu的id,和前面描述的logical cpu id不同,该寄存器如下图所示。
apic-id.jpg

[Interrupt Command Register]-该寄存器是64bit,主要用来发送IPI,为了发送一个IPI,必须要正确的构造这个寄存器,向低32位
执行一个写操作,将导致一个IPI被发送,寄存器详细格式如下图所示,正确构造这个寄存器就是用正确的值来初始化其中的字段:
icr.jpg
只关心下面的字段:
Vector:中断向量号。

Delivery Mode:指定了IPI发送类型,Fixed之外的类型大部分在多处理器启动阶段使用,用来启动AP等。

Delivery Status:指示IPI的发送状态。0:IPI已经成功发送出去;1:还没有完成IPI的发送。

Destination:IPI的目的地,只有当Destination Shorthand字段为00时,才使用,此时该字段的值为目的cpu的local APIC ID。

Destination Shorthand:使用速记符号指定IPI的目的地,编码如下:
00:IPI目的地在Destination字段中指定。
01:IPI的目的地仅包括发送此IPI的cpu。
10:IPI的目的地为系统中全部cpu,包括发送此IPI的cpu。
11:IPI的目的地为系统中除发送此IPI的cpu以外的cpu。


[freebsd9.2:怎么访问local APIC寄存器组]:
  1. /*************************************************************************************************************
  2. * typedef struct LAPIC lapic_t;

  3.    lapic:一个虚拟线性地址,通过该地址可以访问local APIC寄存器组。
  4.    lapic_paddr:local APIC寄存器组的物理地址。
  5.    volatile lapic_t *lapic;
  6.    vm_paddr_t lapic_paddr;

  7.    在freebsd9.2中,通过变量lapic访问local APIC寄存器组。

  8.    关于变量lapic的初始化过程,大概说明一下,需要一点ACPI规范方面的知识,ACPI规范定义了一个
  9.    名为Multiple APIC Description Table的表,该表包含了一个32bit的物理地址(这里假设为apic_phyaddr),
  10.    系统中所有cpu都可以通过apic_phyaddr访问自己的local APIC寄存器组。在系统初始化的最初阶段,初始化函数
  11.    apic_init除了获取系统中每个cpu的local APIC ID外,还要获取Multiple APIC Description Table的物理地址
  12.    并将这个物理地址保存到变量madt_physaddr中;之后的初始化函数lapic_init会从Multiple APIC Description Table
  13.    获取apic_phyaddr,将apic_phyaddr赋值给lapic_paddr,同时建立一个到内核地址空间的映射,将apic_phyaddr开始
  14.    的这段物理内存映射到内核地址空间,返回的内核虚拟线性地址保存到lapic变量中。

  15.    关于struct LAPIC类型,这里只列出两个成员,描述Local APIC ID Register和Interrupt Command Register:
  16. ********************************************************************/
  17.    127        struct LAPIC {
  18. 。。。。。。。。。。。。。。。。。。。。。。。。。。
  19.    130                u_int32_t id;                PAD3;
  20. 。。。。。。。。。。。。。。。。。。。。。。。。。。
  21.    176                u_int32_t icr_lo;        PAD3;
  22.    177                u_int32_t icr_hi;        PAD3;
  23. 。。。。。。。。。。。。。。。。。。。。。。。。。。
  24.    192        };
复制代码
[IA32:中断向量号,中断描述表(IDT)]:
中断向量号:
为了帮助处理中断和异常,IA32给需要由处理器特殊处理的异常和中断条件分配了唯一的标识号,这个唯一的标识号就是中断向量号;
中断向量号的范围是0-255,IA32将0-31之间的中断向量号保留给其定义的内部异常和中断。

中断描述符表(IDT):
IDT可以看成是一个门描述符(大小为8Byte)的数组,cpu使用中断向量号作为IDT的索引,数组元素数目为256,IDT中的门描述符的格式和GDT
中段描述符的格式基本相同,只不过其中某些字段取值不一样,IDT中门描述符的标准格式如下图所示:
门描述符.jpg
这里简单描述下面三个字段:
Segment Selector:
中断处理程序或者异常处理程序所在段的Segment Selector,因为中断处理程序或者异常处理程序一般都位于内核地址空间中,
所以在freebsd9.2中,描述符中该字段都设置为KCSEL,即内核代码段的Segment Selector。

Offset:
中断处理程序或者异常处理程序的地址。

DPL:
门描述符的特权级;根据IA32的保护机制,如果中断处理程序或者异常处理程序在一个较低特权级(数值大)的代码段中,那么cpu将不会执行
一个控制转移,试图违背这个保护机制的话,就会产生一个general-protection exception;如果中断或者异常是通过INT n指令产生的,
那么当前当前CS段寄存器中RPL中的值一定要小于或等于门描述符中DPL字段中的值,否则就会产生一个general-protection exception。

Interrupt Descriptor Table Register(IDTR)-用来访问IDT,lidt指令用来加载IDTR,格式如下:
  1. Interrupt Descriptor Table Register(IDTR)-用来访问IDT,lidt指令用来加载IDTR,格式如下:
  2. bit47                                                                 bit0
  3. ***************************************************************************
  4. *                                            *                            *
  5. *   32-bit Linear Base Address               *   16-Bit Table Limit       *
  6. ***************************************************************************
  7. Base Address指定了IDT的线性基地址,Table Limit指定了IDT的大小字(节数)。
复制代码
[IDTR和IDT的关系如下图所示]:
IDT.png


[freebsd9.2:建立IDT]-相关数据结构:

[struct  gate_descriptor]-用来描述IDT中的表项:
  1. /****************************************************************************
  2. *  #define        NIDT        256
  3. *
  4. *  static struct gate_descriptor idt0[NIDT];
  5.     struct gate_descriptor *idt = &idt0[0];
  6.   
  7.     数组idt0就是IDT,变量idt是为了方便setidt函数的操作。
  8.     因为IA32中IDT的最大数目为256,所以这里数组idt0跟IA32保持一致。

  9.     对于struct  gate_descriptor对象中的成员,和上面图中所示的门描述
  10.     中的字段一一对应,这里忽略成员gd_stkcpy,gd_xx,因为这两个成员
  11.     在门描述符对应的字段没有使用,一般设置为0:

  12.     gd_looffset:中断或异常处理程序的低16bit。

  13.     gd_selector:中断或异常处理程序所在段的segment selector。

  14.     gd_type:门描述符的类型,一般情况下为中断门或者陷阱门,当通过
  15.              中断门访问中断或异常处理程序时,cpu清EFLAGS寄存器中的
  16.              IF标志,而通过陷阱门访问中断或异常处理程序时,不会
  17.              清这个标志。

  18.     gd_dpl:门描述符的特权级,一般情况下,这个字段都设置为0;有
  19.             一个例外,就是系统调用处理异常,该异常的中断向量号为0x80,
  20.             IDT中该向量号对应的门描述符类型是一个陷阱门,而且门描述符
  21.             的DPL字段的值为3,这样在用户态,才能够正常执行一个int 0x80
  22.             软中断指令陷入内核。

  23.     gd_p:段描述符是否在内存中,始终设置为1。

  24.     gd_hioffset:中断或异常处理程序的高16bit。

  25.     和GDT不同,freebsd9.2中只有一个IDT。
  26. ********************************************************/
  27.     88  struct  gate_descriptor {
  28.     89          unsigned gd_looffset:16 ;       /* gate offset (lsb) */
  29.     90          unsigned gd_selector:16 ;       /* gate segment selector */
  30.     91          unsigned gd_stkcpy:5 ;          /* number of stack wds to cpy */
  31.     92          unsigned gd_xx:3 ;              /* unused */
  32.     93          unsigned gd_type:5 ;            /* segment type */
  33.     94          unsigned gd_dpl:2 ;             /* segment descriptor priority level */
  34.     95          unsigned gd_p:1 ;               /* segment descriptor present */
  35.     96          unsigned gd_hioffset:16 ;       /* gate offset (msb) */
  36.     97  } ;
复制代码
[struct region_descriptor]-加载IDTR使用:
  1. /**************************************************************************************
  2. * struct region_descriptor r_idt;
  3.   
  4.    struct region_descriptor类型的数据对象只在使用lidt指令加载IDTR时使用:
  5.    rd_limit:IDT的大小。
  6.    rd_base:IDT的线性基地址。

  7.    在初始化函数init386中,会对r_idt的成员做如下设置,在这之前已经调用函数setidt建立了
  8.    部分异常号对应的门描述符:
  9.    
  10.    r_idt.rd_limit = sizeof(idt0) - 1;
  11.    r_idt.rd_base = (int) idt;
  12.    lidt(&r_idt);
  13.    lidt指令加载BSP的IDTR。
  14. *****************************************/
  15.    164        struct region_descriptor {
  16.    165                unsigned rd_limit:16;                /* segment extent */
  17.    166                unsigned rd_base:32 __packed;        /* base address  */
  18.    167        };
复制代码
[函数setidt]-建立IDT中的门描述符:
  1. /*************************************************************************************
  2. *  函数setidt比较简单,基本是一些赋值操作,下面描述一下参数:
  3.     idx: 中断向量号。
  4.     func:中断或异常处理程序地址。
  5.     type:要建立门描述符的类型。
  6.     dpl: 要建立门描述符的特权级       
  7.     selec:中断或异常处理程序所在段的segment selector

  8.     该函数就是使用参数idx作为IDT的索引,确定相应的表项,然后使用参数建立相应的
  9.     门描述符。
  10. *****************************************/
  11.   1919        void
  12.   1920        setidt(idx, func, typ, dpl, selec)
  13.   1921                int idx;
  14.   1922                inthand_t *func;
  15.   1923                int typ;
  16.   1924                int dpl;
  17.   1925                int selec;
  18.   1926        {
  19.   1927                struct gate_descriptor *ip;
  20.   1928       
  21.   1929                ip = idt + idx;
  22.   1930                ip->gd_looffset = (int)func;
  23.   1931                ip->gd_selector = selec;
  24.   1932                ip->gd_stkcpy = 0;
  25.   1933                ip->gd_xx = 0;
  26.   1934                ip->gd_type = typ;
  27.   1935                ip->gd_dpl = dpl;
  28.   1936                ip->gd_p = 1;
  29.   1937                ip->gd_hioffset = ((int)func)>>16 ;
  30.   1938        }
复制代码
有了前面描述做准备,下面看两个比较典型的中断或者异常如何建立门描述符的。

1:系统调用异常,对应的异常向量号是0x80,即在IDT中的索引为128,系统调用异常处理程序在初始化函数init386中注册到IDT中,如下所示:
  1.    系统调用异常对应的异常处理程序为Xint0x80_syscall,通过readelf读取内核符号表,该符号的值为c0f36640。
  2.    setidt(IDT_SYSCALL, &IDTVEC(int0x80_syscall), SDT_SYS386TGT, SEL_UPL,GSEL(GCODE_SEL, SEL_KPL));
  3.    
  4.    结合上面的setidt函数,相应的门描述符个字段取值如下:
  5.    idt[128].gd_looffset = 6640;// 系统调用异常处理程序地址低16位
  6.    idt[128].gd_selector = KCSEL;// 内核代码段的segment selector
  7.    idt[128].gd_stkcpy = 0;
  8.    idt[128].gd_xx = 0;
  9.    idt[128].gd_type = SDT_SYS386TGT;// 15 即为一个陷阱门
  10.    idt[128].gd_dpl = SEL_UPL;// 3,注意这里是最低特权级3,只有这样用户程序才能执行int 0x80发出一个软中断。
  11.    idt[128].gd_p = 1;
  12.    idt[128].gd_hioffset = c0f3 ;// 系统调用异常处理程序地址高16位
复制代码
2:中断向量号为IPI_BITMAP_VECTOR的中断,宏IPI_BITMAP_VECTOR的值为249,对应的中断处理程序在初始化函数cpu_mp_start中注册到IDT中,如下所示:
  1.    中断向量号为IPI_BITMAP_VECTOR的中断处理程序为Xipi_intr_bitmap_handler,通过readelf读取内核符号表,该符号的值为c0f36cb0。
  2.    setidt(IPI_BITMAP_VECTOR, IDTVEC(ipi_intr_bitmap_handler),SDT_SYS386IGT, SEL_KPL, GSEL(GCODE_SEL, SEL_KPL));

  3.    结合上面的setidt函数,相应的门描述符各个字段取值如下:
  4.    idt[249].gd_looffset = 6cb0;// 中断处理程序地址低16位
  5.    idt[249].gd_selector = KCSEL;// 内核代码段的segment selector
  6.    idt[249].gd_stkcpy = 0;
  7.    idt[249].gd_xx = 0;
  8.    idt[249].gd_type = SDT_SYS386IGT;// 14 即为一个中断门
  9.    idt[249].gd_dpl = SEL_KPL;// 0,注意这里是最高特权级0。
  10.    idt[249].gd_p = 1;
  11.    idt[249].gd_hioffset = c0f3 ;// 中断处理程序地址高16位
复制代码
下面看看再将控制传递到异常处理程序或者中断处理程序之前,cpu硬件单元所做的处理,这里以收到一个中断向量号为IPI_BITMAP_VECTOR的IPI为例,假设
此时正在运行线程intr_thread,并且cpu处于用户态,其内核栈如下所示,在进行线程切换时,TSS的tss_esp0字段已经设置为线程intr_thread内核栈的栈顶,
TSS的tss_ss0成员为KDSEL即内核数据段的segment selector:
  1. ******************************* 内核栈顶部 高地址方向
  2. *                             *
  3. *   struct pcb对象            *
  4. *                             *
  5. *                             *
  6. ******************************* <--struct thread的td_pcb成员指向这里
  7. *                             *
  8. *   16bytes for vm            *        
  9. *******************************  TSS.esp0指向这里,第6步执行完后                        
  10. *   SS                        *
  11. *******************************
  12. *   ESP                       *
  13. ******************************* 第7步执行完后,ESP指向这里
  14. *   EFLAGS                    *
  15. *******************************
  16. *   CS                        *
  17. *******************************
  18. *   EIP                       *     
  19. ******************************* 第8步执行完后,ESP指向这里
  20. *                             *
  21. *                             *
  22. *                             *
  23. *                             *
  24. *                             *  
  25. *                             *
  26. *                             *
  27. *******************************------ 内核栈底部 低地址方向----
复制代码
那么cpu硬件单元将做:
1:
获取对应的中断向量号,这里为249。

2:
从IDTR中获取IDT的基地址,读IDT的第249项,这里就为数组元素idt[249]的内容,即中断向量号249对应的门描述符。

3:
从GDTR中获取GDT的基地址,因为门描述符idt[249]中的Segment Selector为KCSEL,所以这里读取内核代码段对应
的段描述符。

4
使用第3步获取的段描述符中DPL字段在TSS中选择一个新栈(segment selector和栈顶指针);因为此时中断处理程序Xipi_intr_bitmap_handler在内核代码段中,
内核代码段段描述中DPL字段的值为0,所以此时就选择tss_esp0和tss_ss0。

5:
在cpu内部临时保存当前ESP和SS寄存器的值,这两个寄存器的值为线程intr_thred的用户态硬件上下文的一部分。

6:
用tss_esp0加载ESP寄存器,tss_ss0加载SS寄存器。期间cpu硬件单元还要进行很多的权限检查,一旦违例,就会产生一个异常。

7:
将第5步临时保存的ESP和SS寄存器的值压入栈中。

8:
将寄存器EFLAGS,CS,EIP的值压入栈中,这三个寄存器的值也为线程intr_thread的用户态硬件上下文的一部分。

9:
用内核代码段的Segment Selector加载CS寄存器,用第2步获取的门描述符中offset字段的值加载EIP寄存器,这里将用
KCSEL加载CS寄存器,用中断处理程序Xipi_intr_bitmap_handler的地址c0f36cb0加载EIP寄存器。

第9步执行完后,因为CS和EIP寄存器已被更改,所以将开始执行中断向量号为IPI_BITMAP_VECTOR对应的中断处理程序。

论坛徽章:
16
CU十二周年纪念徽章
日期:2013-10-24 15:41:3415-16赛季CBA联赛之广东
日期:2015-12-23 21:21:55青铜圣斗士
日期:2015-12-05 10:35:30黄金圣斗士
日期:2015-11-26 20:42:16神斗士
日期:2015-11-19 12:47:50每日论坛发贴之星
日期:2015-11-18 06:20:00程序设计版块每日发帖之星
日期:2015-11-18 06:20:002015亚冠之城南
日期:2015-11-10 19:10:492015亚冠之萨济拖拉机
日期:2015-10-28 18:47:282015亚冠之柏太阳神
日期:2015-08-30 17:21:492015亚冠之山东鲁能
日期:2015-07-07 18:48:39摩羯座
日期:2014-08-29 23:01:42
发表于 2014-07-06 00:21 |显示全部楼层
感谢楼主分享!good!
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP