65.9K
CodeProject 正在变化。 阅读更多。
Home

深入了解 CPU:原始多核编程

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (43投票s)

2015年3月25日

CPOL

7分钟阅读

viewsIcon

62183

downloadIcon

620

从 DOS 触发所有核心

Github 链接:https://github.com/WindowsNT/asm 现在包含 VS 解决方案,并支持自动编译/ISO 生成/Bochs 运行。

臭名昭著的三部曲:第三部分

在我之前两篇关于 CPU 内部原理虚拟化的硬核文章中,我解释了各种 CPU 内部机制,但没有说明多处理器是如何工作的。这里有一小段可运行的(尽管很粗糙)代码,可以帮助你了解多核处理。

在我的下一篇“底层乱炖 (Low Level M3ss)”文章中,我用它来实现一个 DOS 多核接口。

4x1 = 1x4

基本上,这很简单。每个 CPU 都有自己的一套寄存器和模式,只有内存是它们之间共享的。这意味着,为了将 i7 的 8 个核心都置于长模式,我们必须为每个核心执行完全相同的过程,因为每个核心都有自己的寄存器组、GDT、LDT 等。因此,我们能够在一个 CPU 上启动并保持在实模式,同时将另一个 CPU 引导至长模式。

虚拟化中也会发生同样的情况。在我的虚拟化文章中,我解释了如何将 CPU 置于此状态。为了将整个机器置于虚拟化状态,每个 CPU 都必须被引导进入虚拟化,这意味着要为所有 CPU 设置 VMCS、VMX 区域等。

正如你已经知道的,CPU 从 0xFFFF:0xFFF0 开始启动,但这仅对第一个 CPU 成立;所有其他 CPU 都保持“睡眠”状态,直到被唤醒,这种特殊状态称为等待 SIPI (Wait-for-SIPI)。主 CPU 通过发送一个包含启动地址的 SIPI(启动处理器间中断)来唤醒其他 CPU。之后,还有其他处理器间中断用于 CPU 之间的通信。

因此,在我正在开发的一个非常奇怪的驱动程序中,我应该能够从 Windows 中取出一个处理器,让它回到实模式,然后虚拟化所有其余的 CPU,以创建一个内部调试器。哈!没那么容易,是吧。

准备派对

因为多核编程与 CPU 模式无关,你可以在任何模式下进行。实际上,你可以在实模式下进行,但我们需要访问的内存在 1MB 以上,这意味着你必须进入保护模式。本文的更新版本兼容 Bochs (ACPI 1.0) 和 Vmware (ACPI 2.0+),用 FASM 编写,并进入非实模式(unreal mode)以访问这些结构。我的代码设置了一个带有 Live CD 的 FreeDos 安装,你可以直接从 Visual Studio 启动。对 VirtualBox 的支持正在进行中。

 

ACPI 和 APIC

所有这些事情都是由 APIC(高级可编程中断控制器)完成的。它基本上是内存中的一组表,控制器会检查这些表,并根据我们对表寄存器(内存偏移)的修改做出反应。你可以通过搜索 ACPI(高级配置与电源接口)找到更多关于 APIC 的信息。在验证我们确实有 APIC(通过 CPUID 参数 1,然后检查 EDX 的第 9 位)之后,我们首先要做的是找到 ACPI 在内存中的位置。ACPI 位于以下位置之一:

  • 在一个地方,其真实模式段指针存储在内存地址 040E(我本人从未在那里见过)。
  • 在 BIOS 内存中,物理地址介于 0xE0000 和 0xFFFFF 之间。

通过搜索 ACPI,我们将通过其 8 字节签名 0x2052545020445352 来定位它。如果在内存中找不到这个签名,那么我们就没有 ACPI,因此也没有多个 CPU 核心。

正如在 RSDP 中所述,这仅仅是一个更大结构的签名。我们可能有 ACPI 1.0 或 ACPI 2.0,我们会保存结构数据以备后用。每个 ACPI 表都有一个校验和,一个 ACPI 表中所有字节的总和必须是一个低字节为零的值。

// Yes I indent braces. Sue me :P
int ChecksumValid(unsigned char* addr,int cnt)
    {
    unsigned long a1 = 0;
    for(int i = 0 ; i < cnt ; i++)
        {
        a1 += *(addr + i);
        }
    if((a1 & 0xFF) == 0)
        return 1;
    return 0;
    }

找到 RSDP 后,我们从其字段中获取起始 ACPI 表在内存中的地址。起始表包含指向所有其他表的指针。这个物理地址超过 1MB(实际上,它是一个 64 位地址,但通常位于低 4GB 区域,以允许 32 位系统工作),因此只能从保护模式访问,或者在我们的小程序中,从非实模式访问。有许多 ACPI 表,我们只对其中的几个感兴趣。

struct ACPISDTHeader 
  {
  char Signature[4];
  unsigned long Length;
  unsigned char Revision;
  unsigned char Checksum;
  char OEMID[6];
  char OEMTableID[8];
  unsigned long OEMRevision;
  unsigned long CreatorID;
  unsigned long CreatorRevision;
  };

所有 ACPI 表都以这个结构作为头部开始,Length 成员告诉我们该结构所占的总字节数。对于起始表,其余信息是所支持表的 32 位(或 ACPI 2 的 64 位)地址列表。

我有多少个 CPU?

这是最简单的部分。你必须在内存中找到“MADT” ACPI 表,然后我将内存传递给 DumpMadt。请注意,MADT 也告诉我们本地 APIC 的地址(默认情况下总是在物理地址 0xFEE00000)。

每个 CPU 都有自己的本地 APIC。这个 APIC 处理该 CPU 的中断。它包含各种东西,例如一个本地向量表 (LVT),它是本地中断(如时钟)到实际中断向量的转换。还有一个 I/O APIC,它提供多处理器管理。MADT 也告诉我们 I/O APIC 的地址,默认情况下也在物理地址 0xFEC00000。这两个位置都可以通过设置 MSR 来更改,但在我们的程序中,我们将让它们保持默认值。

请注意,CPU 不知道你有多少内存。即使你只有 4MB 的 RAM,本地 APIC 地址仍然在物理地址 0xFEE00000。

检查 MADT 将为我们提供以上所有信息。

配置本地 APIC

为了准备 APIC 来管理中断,我们必须启用索引为 0xF0 的“伪中断向量寄存器”(Spurious Interrupt Vector Register)。

之后,我们就可以发送 IPI 了。IPI(处理器间中断)是通过使用本地 APIC 的中断命令寄存器发送的。它由两个 32 位寄存器组成,一个在偏移量 0x300,另一个在偏移量 0x310(所有本地 APIC 寄存器都按 16 字节对齐)。

  • 我们首先写入的是偏移量为 0x310 的寄存器,它在 24 - 27 位包含了我们想要发送中断的目标处理器的本地 APIC ID。
  • 偏移量为 0x300 的寄存器具有以下结构:
struct R300
    {
    unsigned char VectorNumber; // Starting page for SIPI
    unsigned char DestinationMode:3; //  0 normal, 1 low, 2 SMI, 4 NMI, 5 Init, 6 SIPI 
    unsigned char DestinationModeType:1; // 0 for physical 1 for logical
    unsigned char DeliveryStatus:1; // 0 - message delivered
    unsigned char R1:1;
    unsigned char InitDeAssertClear:1; 
    unsigned char InitDeAssertSet:1;
    unsigned char R2:2;
    unsigned char DestinationType:2; // 0 normal, 1 send to me, 2 send to all, 3 send to all except me
    unsigned char R3:12;
    };    

写入寄存器 0x300 将实际发送 IPI(这就是为什么你必须先写入 0x310)。请注意,如果 DestinationType 不为 0,则寄存器 0x310 中的目标地址将被忽略。在 Windows 下,IPI 是以 IRQL 级别 29 发送的。

为了唤醒处理器,我们发送两个特殊的 IPI。第一个是“Init” IPI,DestinationMode 为 5,它存储了 CPU 的启动地址。记住,CPU 是在实模式下启动的。因为处理器在实模式下启动,我们必须给它一个实模式内存地址,存储在 VectorNumber 中。第二个 IPI 是 SIPI,DestinationMode 为 6,它启动 CPU。按照惯例,会发送两次 SIPI,中间有延迟。

由于启动地址必须是 4096 字节对齐的,我的代码将 ASM 源码中的代码转移到硬编码的地址 0x80000,以此作为快速解决方案。

最后,你需要向“中断结束”(本地 Apic + 0xB0)写入值 0,以表示你可以发送另一个中断了。

线程安全

哈!正如你所能猜到的,没有一个 DOS 函数是线程安全的。这意味着,要从其他 CPU 调用 DOS,你必须执行适当的同步。我的代码创建了一个快速互斥锁,允许一个线程成功调用 int 21h。

结论

如你所见,这并不是很难。问题在于如何将所有这些东西同步起来。更多内容将在下一篇文章中介绍!

历史

  • 2018年12月26日:更新,仅包含 FASM 代码并支持 Bochs
  • 2015年3月27日:修正一些拼写错误和 INIT IPI
  • 2015年3月25日:首次发布
© . All rights reserved.