摘要:其中的set_intr_gate用來初始化硬件相關的調用門,其對應的中斷門處理函數在irq_entries_start中定義,它位於arch/x86/entry/entry_64.S中:。硬件中斷相關IDT的初始化也是在Linux啓動時完成,在start_kernel中通過調用init_IRQ完成,我們來看一下:。

通過閱讀本文,您可以瞭解到: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寄存器裏存放的偏移量,真正的邏輯地址就有了。

我們先看中斷描述符的定義:

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

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

一圖以蔽之:

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雲計算

360技術公衆號

技術乾貨|一手資訊|精彩活動

掃碼關注我們

相關文章