女主宣言

通过阅读本文,您可以了解到:IDT是什么,它如何被初始化,什么是门,传统系统调用是如何实现的,以及硬件中断的实现。

PS:丰富的一线技术、多元化的表现形式,尽在“360云计算”,点关注哦!

1

如何设置IDT

IDT 中断描述符表定义

中断描述符表简单来说说是定义了发生中断/异常时,CPU按这张表中定义的行为来处理对应的中断/异常。

#define IDT_ENTRIES         256
gate_desc idt_table[IDT_ENTRIES] __page_aligned_bss;

从上面我们可以知道,其包含了256项,它是一个gate_desc的数据,其下标0-256就表示中断向量,gate_desc我们在下面马上介绍。

中断描述符项定义

当中断发生,cpu获取到中断向量后,查找IDT中断描述符表得到相应的中断描述符,再根据中断描述符记录的信息来作权限判断,运行级别转换,最终调用相应的中断处理程序。这里涉及到Linux kernel的分段式内存管理,我们这里不详细展开,有兴趣的同学可以自行学习。如下简述之:

1. 我们知道CPU只认识逻辑地址,逻辑地址经分段处理转换成线性地址,线性地址经分页处理最终转换成物理地址,这样就可以从内存中读取了;

2. 逻辑地址你可以简单认为就是CPU执行代码时从CS(代码段寄存器) :IP (指令计数寄存器)中加载的代码,实际上通过CS可以得到逻辑地址的基地址,再加上IP这个相对于基地址的偏移量,就得到真正的逻辑地址;

3. CS寄存器16位,它不会包含真正的基地址,它一般被称为段选择子,包括一个index索引,指向GDT或 LDT的一项;一个指示位,指示index索引是属于GDT还是LDT; 还有CPL, 表明当前代码运行权限;

4. GDT: 全局描述符表,每一项记录着相应的段基址,段大小,段的访问权限DPL等,到这里终于可以获取到段基地址了,再加上之前IP寄存器里存放的偏移量,真正的逻辑地址就有了。

我们先看中断描述符的定义:

struct gate_struct {u16     offset_low;u16     segment;struct idt_bits bits;u16     offset_middle;
#ifdef CONFIG_X86_64u32     offset_high;u32     reserved;
#endif
} __attribute__((packed));

其中:

1. offset_high,offset_middle和offset_low合起来就是中断处理函数地址的偏移量;

2. segment就是相应的段选择子,根据它在GDT中查找可以最终获取到段基地址;

3. bits是该中断描述符的一些属性值:

struct idt_bits {u16     ist : 3,zero    : 5,type    : 5,dpl : 2,p   : 1;
} __attribute__((packed));

ist表示此中断处理函数是使用pre-cpu的中断栈,还是使用IST的中断栈;

type表示所中断是何种类型,目前有以下四种:

enum {GATE_INTERRUPT = 0xE, //中断门GATE_TRAP = 0xF, // 陷入门GATE_CALL = 0xC, // 调用门GATE_TASK = 0x5, // 任务门
};

门的概念这里主要用作权限控制,我们从一个区域进到另一个区域需要通过一扇门,有门禁权限才可以通过,因此 dpl就是这个权限,实际中我们一般称为RPL;

我们后面会通过一个例子来讲一下CPL,RPL和DPL三者之间的关系。

IDT中断描述符本身的存储

IDT 中断描述符表的物理地址存储在IDTR寄存器中,这个寄存器存储了IDT的基地址和长度。查询时,从 IDTR 拿到 base address ,加上向量号 * IDT entry size,即可以定位到对应的表项(gate)。

设置IDT

  • 设置中断门类型的IDT描述符:

static void set_intr_gate(unsigned int n, const void *addr)
{struct idt_data data;BUG_ON(n > 0xFF);memset(&data, 0, sizeof(data));data.vector = n; // 中断向量data.addr   = addr; // 中断处理函数的地址data.segment    = __KERNEL_CS; // 段选择子data.bits.type  = GATE_INTERRUPT; // 类型data.bits.p = 1;idt_setup_from_table(idt_table, &data, 1, false);
}

上面的函数主要是填充好idt_data,然后调用idt_setup_from_table;

static void
idt_setup_from_table(gate_desc *idt, const struct idt_data *t, int size, bool sys)
{gate_desc desc;for (; size > 0; t++, size--) {idt_init_desc(&desc, t);write_idt_entry(idt, t->vector, &desc);if (sys)set_bit(t->vector, system_vectors);}
}

首先使用 idt_data结构来填充中断描述符变量idt_init_desc, 然后将这个中断描述符变量copy进idt_table。

看,就是这么简单~~~

  • gate_desc的多种初始化方法:

    因为gate_desc是通过ida_dat填充的,所以这里关键是idt_data的初始化,我们详细看一下:

/* Interrupt gate
中断门,DPL = 0
只能从内核调用
*/
#define INTG(_vector, _addr)              \G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL0, __KERNEL_CS)/* System interrupt gate
系统中断门,DPL = 3
可以从用户态调用,比如系统调用
*/
#define SYSG(_vector, _addr)              \G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL3, __KERNEL_CS)/** Interrupt gate with interrupt stack. The _ist index is the index in* the tss.ist[] array, but for the descriptor it needs to start at 1.中断门, DPL = 0只能从内核态调用,使用TSS.IST[]作为中断栈 */
#define ISTG(_vector, _addr, _ist)            \G(_vector, _addr, _ist + 1, GATE_INTERRUPT, DPL0, __KERNEL_CS)/* Task gate
任务门, DPL = 0
只能作内核态调用
*/
#define TSKG(_vector, _gdt)               \G(_vector, NULL, DEFAULT_STACK, GATE_TASK, DPL0, _gdt << 3)

我们再来看下G这个宏的实现:

#define G(_vector, _addr, _ist, _type, _dpl, _segment)    \{                       \.vector     = _vector,      \.bits.ist   = _ist,         \.bits.type  = _type,        \.bits.dpl   = _dpl,         \.bits.p     = 1,            \.addr       = _addr,        \.segment    = _segment,     \}

实际上就是填充idt_data的各个字段。

2

传统系统调用的实现

这里所说的传统系统调用主要指旧的32位系统使用 int 0x80软件中断来进入内核态,实现的系统调用。因为这种传统系统调用方式需要进入内核后作权限验证,还要切换内核栈后作大量压栈方式,调用结束后清理栈作恢复,两个字太慢,后来CPU从硬件上支持快速系统调用sysenter/sysexit, 再后来又发展到syscall/sysret, 这两种都不需要通过中断方式进入内核态,而是直接转换到内核态,速度快了很多。

传统系统调用相关IDT的设置

Linux系统启动过程中内核压解后最终都调用到start_kernel, 在这里会调用trap_init, 然后又会调用idt_setup_traps:

void __init idt_setup_traps(void)
{idt_setup_from_table(idt_table, def_idts, ARRAY_SIZE(def_idts), true);
}

我们来看这里的def_idts的定义:

static const __initconst struct idt_data def_idts[] = {....
#if defined(CONFIG_IA32_EMULATION)SYSG(IA32_SYSCALL_VECTOR,   entry_INT80_compat),
#elif defined(CONFIG_X86_32)SYSG(IA32_SYSCALL_VECTOR,   entry_INT80_32),
#endif
};

上面的SYSG(IA32_SYSCALL_VECTOR, entry_INT80_32)就是设置系统调用的异常中断处理程序,其中  #define IA32_SYSCALL_VECTOR 0x80

再看一下SYSG的定义:

#define SYSG(_vector, _addr)                \G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL3, __KERNEL_CS)

它初始化一个中断门,权限是DPL3, 因此从用户态是允许发起系统调用的。

我们调用系统调用,不大可能自已手写汇编代码,都是通过glibc来调用,基本流程是保存参数到寄存器,然后保存系统调用向量号到eax寄存器,然后调用int 0x80进入内核态,切换到内核栈,将用户态时的ss/sp/eflags/cs/ip/error code依次压入内核栈。

entry_INT80_32系统调用对应的中断处理程序:

ENTRY(entry_INT80_32)ASM_CLACpushl   %eax            /* pt_regs->orig_ax */SAVE_ALL pt_regs_ax=$-ENOSYS switch_stacks=1    /* save rest */TRACE_IRQS_OFFmovl    %esp, %eaxcall    do_int80_syscall_32
.Lsyscall_32_done:
...
.Lirq_return:INTERRUPT_RETURN...
ENDPROC(entry_INT80_32)

我们略去了中间的一些细节部分,可以看到首先将中断向量号压栈,再保存所有当前的寄存器值到pt_regs, 保存当前栈指针到%eax寄存器,最后再调用 do_int80_syscall_32, 这个函数中就会执行具体的中断处理,然后INTERRUPT_RETURN恢复栈,作好返回用户态的准备。

do_int80_syscall_32调用 do_syscall_32_irqs_on,我们看一下其实现:

static __always_inline void do_syscall_32_irqs_on(struct pt_regs *regs)
{struct thread_info *ti = current_thread_info();unsigned int nr = (unsigned int)regs->orig_ax;#ifdef CONFIG_IA32_EMULATIONti->status |= TS_COMPAT;#endifif (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY) {nr = syscall_trace_enter(regs);}if (likely(nr < IA32_NR_syscalls)) {nr = array_index_nospec(nr, IA32_NR_syscalls);#ifdef CONFIG_IA32_EMULATIONregs->ax = ia32_sys_call_table[nr](regs);#elseregs->ax = ia32_sys_call_table[nr]((unsigned int)regs->bx, (unsigned int)regs->cx,(unsigned int)regs->dx, (unsigned int)regs->si,(unsigned int)regs->di, (unsigned int)regs->bp);#endif /* CONFIG_IA32_EMULATION */}syscall_return_slowpath(regs);}

通过中断向量号nr从ia32_sys_call_table中断向量表中索引到具体的中断处理函数然后调用之,其结果最终合存入%eax寄存器。

一图以蔽之:

3

硬件中断的实现

硬件中断的IDT初始化和调用流程

这里我们不讲解具体的代码细节,只关注流程 。

硬件中断相关IDT的初始化也是在Linux启动时完成,在start_kernel中通过调用init_IRQ完成,我们来看一下:

void __init init_IRQ(void)
{int i;for (i = 0; i < nr_legacy_irqs(); i++)per_cpu(vector_irq, 0)[ISA_IRQ_VECTOR(i)] = irq_to_desc(i);BUG_ON(irq_init_percpu_irqstack(smp_processor_id()));x86_init.irqs.intr_init(); // 即调用  native_init_IRQ
}void __init native_init_IRQ(void)
{/* Execute any quirks before the call gates are initialised: */x86_init.irqs.pre_vector_init();idt_setup_apic_and_irq_gates();lapic_assign_system_vectors();if (!acpi_ioapic && !of_ioapic && nr_legacy_irqs())setup_irq(2, &irq2);
}

重点在于idt_setup_apic_and_irq_gates:

 */
void __init idt_setup_apic_and_irq_gates(void)
{int i = FIRST_EXTERNAL_VECTOR;void *entry;idt_setup_from_table(idt_table, apic_idts, ARRAY_SIZE(apic_idts), true);for_each_clear_bit_from(i, system_vectors, FIRST_SYSTEM_VECTOR) {entry = irq_entries_start + 8 * (i - FIRST_EXTERNAL_VECTOR);set_intr_gate(i, entry);}
}

其中的set_intr_gate用来初始化硬件相关的调用门,其对应的中断门处理函数在irq_entries_start中定义,它位于arch/x86/entry/entry_64.S中:

    .align 8
ENTRY(irq_entries_start)vector=FIRST_EXTERNAL_VECTOR.rept (FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR)UNWIND_HINT_IRET_REGSpushq   $(~vector+0x80)         /* Note: always in signed byte range */jmp common_interrupt.align  8vector=vector+1.endr
END(irq_entries_start)

这段汇编实现对不大熟悉汇编的同学可能看起来有点晕,其实很简单它相当于填充一个中断处理函数的数组,填充多少次呢? (FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR)这就是次数,数组的每一项都是一个函数:

    UNWIND_HINT_IRET_REGSpushq   $(~vector+0x80)         /* Note: always in signed byte range */jmp common_interrupt

即先将中断号压栈,然后跳转到common_interrupt执行,可以看到这个common_interrupt是硬件中断的通用处理函数,它里面最主要的就是调用do_IRQ:

__visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
{struct pt_regs *old_regs = set_irq_regs(regs);struct irq_desc * desc;/* high bit used in ret_from_ code  */unsigned vector = ~regs->orig_ax;entering_irq();/* entering_irq() tells RCU that we're not quiescent.  Check it. */RCU_LOCKDEP_WARN(!rcu_is_watching(), "IRQ failed to wake up RCU");desc = __this_cpu_read(vector_irq[vector]);if (likely(!IS_ERR_OR_NULL(desc))) {if (IS_ENABLED(CONFIG_X86_32))handle_irq(desc, regs);elsegeneric_handle_irq_desc(desc);} else {ack_APIC_irq();if (desc == VECTOR_UNUSED) {pr_emerg_ratelimited("%s: %d.%d No irq handler for vector\n",__func__, smp_processor_id(),vector);} else {__this_cpu_write(vector_irq[vector], VECTOR_UNUSED);}}exiting_irq();set_irq_regs(old_regs);return 1;
}

首先根据中断向量号获取到对应的中断描述符irq_desc, 然后调用generic_handle_irq来处理:

static inline void generic_handle_irq_desc(struct irq_desc *desc)
{desc->handle_irq(desc);
}

这里最终会调用到中断描述符的handle_irq,因此另一个重点就是这个中断描述符的设置了,它可以单开一篇文章来讲,我们暂不详述了。

360云计算

由360云平台团队打造的技术分享公众号,内容涉及数据库、大数据、微服务、容器、AIOps、IoT等众多技术领域,通过夯实的技术积累和丰富的一线实战经验,为你带来最有料的技术分享

Linux中断一网打尽(2) - IDT及中断处理的实现相关推荐

  1. Linux中断一网打尽(1) — 中断及其初始化

    女主宣言 通过本文您可以了解到:Linux 中断是什么,如何分类,能干什么?Linux 中断在计算机启动各阶段是如何初始化的? PS:丰富的一线技术.多元化的表现形式,尽在"360云计算&q ...

  2. linux中断系统那些事之----中断处理过程【转】

    转自:http://blog.csdn.net/xiaojsj111/article/details/14129661 以外部中断irq为例来说明,当外部硬件产生中断时,linux的处理过程.首先先说 ...

  3. 32获取外部中断状态_Linux中断一网打尽(1) — 中断及其初始化

    1 中断是什么 既然叫中断, 那我们首先就会想到这个中断是中断谁?想一想计算机最核心的部分是什么?没错, CPU, 计算机上绝大部分的计算都在CPU中完成,因此这个中断也就是中断CPU当前的运行,让C ...

  4. Linux 中断所有知识点

    目录 Linux 中断管理机制 GIC 硬件原理 GIC v3中断类别 GIC v3 组成 中断路由 中断状态机 中断处理流程 GIC 驱动 设备树 初始化 中断的映射 数据结构 中断控制器注册 ir ...

  5. 吐血整理 | 肝翻 Linux 中断所有知识点

    Linux 中断管理机制 GIC 硬件原理 GIC,Generic Interrupt Controller.是ARM公司提供的一个通用的中断控制器.主要作用为:接受硬件中断信号,并经过一定处理后,分 ...

  6. Linux中断技术、门描述符、IDT(中断描述符表)、异常控制技术总结归类

    相关学习资料 <深入理解计算机系统(原书第2版)>.pdf http://zh.wikipedia.org/zh/%E4%B8%AD%E6%96%B7 独辟蹊径品内核:Linux内核源代码 ...

  7. Linux 中断之中断处理浅析

    1. 中断的概念 中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的 CPU 暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续 ...

  8. linux 中断 c语言程序,linux驱动之中断处理过程C程序部分

    当发生中断之后,linux系统在汇编阶段经过一系列跳转,最终跳转到asm_do_IRQ()函数,开始C程序阶段的处理.在汇编阶段,程序已经计算出发生中断的中断号irq,这个关键参数最终传递给asm_d ...

  9. Linux中断子系统(三)之GIC中断处理过程

    Linux中断子系统(三)之GIC中断处理过程 备注:   1. Kernel版本:5.4   2. 使用工具:Source Insight 4.0   3. 参考博客: Linux中断子系统(一)中 ...

最新文章

  1. extjs4.0的高级组件grid补充01选择模式selection和表格特性feature
  2. python中哪个函数能生成集合_神奇的python系列11:函数之生成器,列表推导式
  3. 从菜鸟到专家的五步编程语言学习法
  4. 获取今天,昨天,本周,上周,本月,上月时间
  5. 层次聚类分析代码_你知道如何聚类吗?层次聚类与聚类树
  6. 史上最全的女人坐月子注意事項
  7. usb连接不上 艾德克斯电源_工程师,USB与SPI之间如何通信?什么芯片方案可以实现...
  8. 计算机基础(二):嵌入式驱动、图像处理知识设备小结
  9. 基于SSM实现后勤报修系统
  10. 获取B站SESSDATA及解决403
  11. 抖音搬运新技术秒上热门,爆抖神器,效果惊人
  12. 【C语言】size与strlen的区别解析
  13. python调用函数库_python调用操作系统的库函数
  14. 007-aven-assembly-plugin和maven-jar-plugin打包,java启动命令
  15. bttnserv.exe
  16. pdf书籍资源共享_书籍和更多内容已获许可使用知识共享
  17. 在windows下编译erlang内建函数(nif)的dll文件
  18. 保姆式RecyclerView下拉刷新、上拉加载更多Kotlin
  19. 出圈问题(java)-----n个人围成一圈,数到key或者key的倍数,出圈,问剩下的最后一个人原来的位置是多少?
  20. Cadence创建异形焊盘教程(详细操作)

热门文章

  1. linux报错之no space left on device问题分析
  2. centos 7 源码方式安装mysql5.6
  3. 诗与远方:无题(七十一)- 雨季来了
  4. hash表、java中的hashMap/hashSet
  5. SQL解析引擎Apache Calcite
  6. html嵌入excel_第5天 | 16天搞定前端,html布局,表格和大块头
  7. 跨路由器 网段访问rtsp_实验演示:三层交换机与路由器对接
  8. error:Name node is in safe mode.
  9. spyder中绘图无法显示负号_Python绘图--时序图
  10. CentOS7.4中搭建lnmp环境