x86_64 IDT 在 Linux 内核中的 CPU 之间共享吗?

问题描述 投票:0回答:1

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
linux linux-kernel x86-64 cpu-architecture interrupt
1个回答
0
投票

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。

© www.soinside.com 2019 - 2024. All rights reserved.