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

栏目: IT技术 · 发布时间: 4年前

内容简介:通过阅读本文,您可以了解到:IDT是什么,它如何被初始化,什么是门,传统系统调用是如何实现的,以及硬件中断的实现。中断描述符表简单来说说是定义了发生中断/异常时,CPU按这张表中定义的行为来处理对应的中断/异常。

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

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

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寄存器里存放的偏移量,真正的逻辑地址就有了。

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

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

struct gate_struct {

u16 offset_low;

u16 segment;

struct idt_bits bits;

u16 offset_middle;

#ifdef CONFIG_X86_64

u32 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)。

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

设置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_CLAC

pushl %eax /* pt_regs->orig_ax */


SAVE_ALL pt_regs_ax=$-ENOSYS switch_stacks=1 /* save rest */


TRACE_IRQS_OFF


movl %esp, %eax

call 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_EMULATION

ti->status |= TS_COMPAT;

#endif

if (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_EMULATION

regs->ax = ia32_sys_call_table[nr](regs);

#else

regs->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寄存器。

一图以蔽之:

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

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_REGS

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

jmp common_interrupt

.align 8

vector=vector+1

.endr

END(irq_entries_start)

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

UNWIND_HINT_IRET_REGS

pushq $(~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);

else

generic_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云计算

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

360技术公众号

技术干货|一手资讯|精彩活动

扫码关注我们


以上所述就是小编给大家介绍的《Linux中断一网打尽(2) - IDT及中断处理的实现》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

颠覆式成长

颠覆式成长

惠特尼•约翰逊 / 张瀚文 / 中信出版集团 / 2018-8 / 49.00

你可能想要标新立异、挑战自我,甚至抛弃安逸的事业; 你可能会从目前的行业或公司中跳槽,进入一个完全陌生的崭新领域, 这本书会让你认识到颠覆式成长的意义所在。 成功没有捷径,颠覆也会令人心生惧意,但是在职业发展与个人成长上的回报,会让你克服这种恐惧,让你不断尝试、不断精进。 S型曲线精进模型将帮助你预测自己创新的成长周期,洞悉颠覆自我过程中的心路历程,在变革与颠覆中从容应对,......一起来看看 《颠覆式成长》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换