TLDR:
Q1:Intel x86_64 架构是否有每个 CPU idtr?如果是这样,那么 IDT 应该加载 N 次,其中 N 是 CPU 的数量?我的意思是针对每个 CPU,而不是针对一个 CPU N 次。
Q2:我发现 x86_64 上的 IDT 在 CPU 之间共享,而 Linux 中的注释却相反(x86_64 有每个 CPU IDT 表),哪个位置是正确的?
冗长的描述
我正在研究 Linux 中的 IDT(中断描述符表)设置,我在 arch/x86/include/asm/irq_vectors.h:
中发现了这样的注释/*
* Linux IRQ vector layout.
*
* There are 256 IDT entries (per CPU - each entry is 8 bytes) which can
* be defined by Linux. They are used as a jump table by the CPU when a
* given vector is triggered - by a CPU-external, CPU-internal or
* software-triggered event.
*
* Linux sets the kernel code address each entry jumps to early during
* bootup, and never changes them. This is the general layout of the
* IDT entries:
*
* Vectors 0 ... 31 : system traps and exceptions - hardcoded events
* Vectors 32 ... 127 : device interrupts
* Vector 128 : legacy int80 syscall interface
* Vectors 129 ... LOCAL_TIMER_VECTOR-1
* Vectors LOCAL_TIMER_VECTOR ... 255 : special interrupts
*
* 64-bit x86 has per CPU IDT tables, 32-bit has one shared IDT table.
*
* This file enumerates the exact layout of them:
*/
IDT的布局是可以理解的,但是有一行让我很困惑:
64-bit x86 has per CPU IDT tables, 32-bit has one shared IDT table.
造成混淆的原因如下:据我所知“main”(不是 IVT/early IDT)IDT 加载于:
void __init idt_setup_apic_and_irq_gates(void)
{
/* Prepare interrupt gates and idt_descr */
...
/* Map IDT into CPU entry area and reload it. */
idt_map_in_cea();
load_idt(&idt_descr);
...
}
所以,看看
idt_map_in_cea
:
static void __init idt_map_in_cea(void)
{
/*
* Set the IDT descriptor to a fixed read-only location in the cpu
* entry area, so that the "sidt" instruction will not leak the
* location of the kernel, and to defend the IDT against arbitrary
* memory write vulnerabilities.
*/
cea_set_pte(CPU_ENTRY_AREA_RO_IDT_VADDR, __pa_symbol(idt_table),
PAGE_KERNEL_RO);
idt_descr.address = CPU_ENTRY_AREA_RO_IDT;
}
这里我看到IDT被映射到
CPU_ENTRY_AREA_RO_IDT
,它等于fffffe0000000000
(根据linux虚拟内存映射,这确实是CPU入口区域的开始),然后使用lidt
中的
load_idt()
加载它功能。
首先,我认为“因为这是虚拟地址,所以应该有不同的页表,因此会有不同的 IDT 物理实例”,但是使用 sidt
(实际上是
store_idt
函数)转储 idtr给出了这个虚拟地址(
fffffe0000000000
) 正如预期的那样,但是遍历页表 for_each_online_cpu
给出了相同的物理地址。使用gdb/QEMU
我发现这个地址是正确的并且对应于idt_table
符号(在idt_map_in_cea
之后加载的实际IDT表)。这实际上让我感到困惑,我可以看到IDT在CPU之间共享,是这样还是我错过了什么?
此外,当我将 IDT 复制到我自己的 LKM 内存,然后使用
lidt
(实际上是 load_idt
函数)遍历页表重新加载它时,for_each_online_cpu
给出了相同的 LKM 地址,即使我没有执行 lidt
每个CPU为所有CPU加载新的IDT。
编辑1:
我使用
smp_call_function_single
来获取特殊CPU的IDTR:
static void smp_get_idtr(void *info)
{
struct desc_ptr *idt_ptr = info;
store_idt(idt_ptr);
}
static void idtr_per_cpu_show(void)
{
int cpu;
for_each_online_cpu(cpu) {
struct desc_ptr *idt_ptr = kzalloc(...);
/* ... */
smp_call_function_single(cpu, smp_get_idtr, idt_ptr, 1);
/* print address/base */
kfree(idt_ptr);
}
}
编辑2:
我发现,我使用与上面相同的方案来加载新复制的IDT,因此所有CPU都有相同的表(是的,我是一个懒惰的人,只是复制粘贴了该函数)。我修复了这个问题,在重新加载之前,我仍然可以看到初始 IDT 的相同物理地址,但是一旦我在 CPU 0 上使用自己的副本重新加载表,那么我就拥有了自己的 IDT 地址,仅适用于该 CPU,其他人仍然具有初始物理和虚拟地址地址(例如,如下所示的 4 插槽虚拟机):
CPU 0:
base = 0xffff8b3c484dc000
limit = 0xfff
phys = 0x0x00000001084dc000
CPU 1:
base = 0xfffffe0000000000
limit = 0xfff
phys = 0x0x000000007659b000
CPU 2:
base = 0xfffffe0000000000
limit = 0xfff
phys = 0x0x000000007659b000
CPU 3:
base = 0xfffffe0000000000
limit = 0xfff
phys = 0x0x000000007659b000
IDT 在 CPU 之间共享
在
start_secondary
中辅助 CPU 的初始化阶段,会调用 cpu_init_exception_handling
:
/*
* Activate a secondary processor.
*/
static void notrace start_secondary(void *unused)
{
/* ... */
cpu_init_exception_handling();
/* ... */
此时,“主”IDT 已为 CPU 0 设置,但
idt_table
尚未更改,因此 cpu_init_exception_handling
加载该 IDT 的地址:
/*
* Setup everything needed to handle exceptions from the IDT, including the IST
* exceptions which use paranoid_entry().
*/
void cpu_init_exception_handling(void)
{
/* ... */
/* Finally load the IDT */
load_current_idt();
}
扩展为:
void load_current_idt(void)
{
/* ... */
load_idt(&idt_descr);
}
并且由于 CPU 0 初始化
idt_descr
未更改且 IDT 不可写,因此它会为其余 CPU 加载相同的 IDT。