Linux中断一网打尽(2) - IDT及中断处理的实现
女主宣言
通过阅读本文,您可以了解到: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及中断处理的实现相关推荐
- Linux中断一网打尽(1) — 中断及其初始化
女主宣言 通过本文您可以了解到:Linux 中断是什么,如何分类,能干什么?Linux 中断在计算机启动各阶段是如何初始化的? PS:丰富的一线技术.多元化的表现形式,尽在"360云计算&q ...
- linux中断系统那些事之----中断处理过程【转】
转自:http://blog.csdn.net/xiaojsj111/article/details/14129661 以外部中断irq为例来说明,当外部硬件产生中断时,linux的处理过程.首先先说 ...
- 32获取外部中断状态_Linux中断一网打尽(1) — 中断及其初始化
1 中断是什么 既然叫中断, 那我们首先就会想到这个中断是中断谁?想一想计算机最核心的部分是什么?没错, CPU, 计算机上绝大部分的计算都在CPU中完成,因此这个中断也就是中断CPU当前的运行,让C ...
- Linux 中断所有知识点
目录 Linux 中断管理机制 GIC 硬件原理 GIC v3中断类别 GIC v3 组成 中断路由 中断状态机 中断处理流程 GIC 驱动 设备树 初始化 中断的映射 数据结构 中断控制器注册 ir ...
- 吐血整理 | 肝翻 Linux 中断所有知识点
Linux 中断管理机制 GIC 硬件原理 GIC,Generic Interrupt Controller.是ARM公司提供的一个通用的中断控制器.主要作用为:接受硬件中断信号,并经过一定处理后,分 ...
- Linux中断技术、门描述符、IDT(中断描述符表)、异常控制技术总结归类
相关学习资料 <深入理解计算机系统(原书第2版)>.pdf http://zh.wikipedia.org/zh/%E4%B8%AD%E6%96%B7 独辟蹊径品内核:Linux内核源代码 ...
- Linux 中断之中断处理浅析
1. 中断的概念 中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的 CPU 暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续 ...
- linux 中断 c语言程序,linux驱动之中断处理过程C程序部分
当发生中断之后,linux系统在汇编阶段经过一系列跳转,最终跳转到asm_do_IRQ()函数,开始C程序阶段的处理.在汇编阶段,程序已经计算出发生中断的中断号irq,这个关键参数最终传递给asm_d ...
- Linux中断子系统(三)之GIC中断处理过程
Linux中断子系统(三)之GIC中断处理过程 备注: 1. Kernel版本:5.4 2. 使用工具:Source Insight 4.0 3. 参考博客: Linux中断子系统(一)中 ...
最新文章
- extjs4.0的高级组件grid补充01选择模式selection和表格特性feature
- python中哪个函数能生成集合_神奇的python系列11:函数之生成器,列表推导式
- 从菜鸟到专家的五步编程语言学习法
- 获取今天,昨天,本周,上周,本月,上月时间
- 层次聚类分析代码_你知道如何聚类吗?层次聚类与聚类树
- 史上最全的女人坐月子注意事項
- usb连接不上 艾德克斯电源_工程师,USB与SPI之间如何通信?什么芯片方案可以实现...
- 计算机基础(二):嵌入式驱动、图像处理知识设备小结
- 基于SSM实现后勤报修系统
- 获取B站SESSDATA及解决403
- 抖音搬运新技术秒上热门,爆抖神器,效果惊人
- 【C语言】size与strlen的区别解析
- python调用函数库_python调用操作系统的库函数
- 007-aven-assembly-plugin和maven-jar-plugin打包,java启动命令
- bttnserv.exe
- pdf书籍资源共享_书籍和更多内容已获许可使用知识共享
- 在windows下编译erlang内建函数(nif)的dll文件
- 保姆式RecyclerView下拉刷新、上拉加载更多Kotlin
- 出圈问题(java)-----n个人围成一圈,数到key或者key的倍数,出圈,问剩下的最后一个人原来的位置是多少?
- Cadence创建异形焊盘教程(详细操作)
热门文章
- linux报错之no space left on device问题分析
- centos 7 源码方式安装mysql5.6
- 诗与远方:无题(七十一)- 雨季来了
- hash表、java中的hashMap/hashSet
- SQL解析引擎Apache Calcite
- html嵌入excel_第5天 | 16天搞定前端,html布局,表格和大块头
- 跨路由器 网段访问rtsp_实验演示:三层交换机与路由器对接
- error:Name node is in safe mode.
- spyder中绘图无法显示负号_Python绘图--时序图
- CentOS7.4中搭建lnmp环境