通過閱讀本文您可以了解到:
- IDT是什么 ;
- IDT如何被初始化;
- 什么是門;
- 傳統系統調用是如何實現的;
- 硬件中斷的實現;
如何設置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的分段式內存管理,我們這里不詳細展開,有興趣的同學可以自行學習。如下簡述之:
我們知道CPU只認識邏輯地址,邏輯地址經分段處理轉換成線性地址,線性地址經分頁處理最終轉換成物理地址,這樣就可以從內存中讀取了;
邏輯地址你可以簡單認為就是CPU執行代碼時從CS(代碼段寄存器) : IP (指令計數寄存器)中加載的代碼,實際上通過CS可以得到邏輯地址的基地址,再加上IP這個相對于基地址的偏移量,就得到真正的邏輯地址;
CS寄存器16位,它不會包含真正的基地址,它一般被稱為
段選擇子
,包括一個index索引,指向GDT
或LDT
的一項;一個指示位,指示index索引是屬于GDT
還是LDT
; 還有CPL
, 表明當前代碼運行權限;GDT
: 全局描述符表,每一項記錄著相應的段基址,段大小,段的訪問權限DPL
等,到這里終于可以獲取到段基地址了,再加上之前IP
寄存器里存放的偏移量,真正的邏輯地址就有了。-
附上簡圖:
idt2.jpg
-
我們先看中斷描述符的定義:
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));
其中:
offset_high
,offset_middle
和offset_low
合起來就是中斷處理函數地址的偏移量;segment
就是相應的段選擇子,根據它在GDT
中查找可以最終獲取到段基地址;-
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
; -
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
的各個字段。
傳統系統調用的實現
這里所說的傳統系統調用主要指舊的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
寄存器。
一圖以蔽之
硬件中斷的實現
硬件中斷的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
,因此另一個重點就是這個中斷描述符的設置了,它可以單開一篇文章來講,我們暫不詳述了。