多核处理器编程入门

本文将提供一个简单的代码示例,帮助理解多核处理的基本概念。

在下一篇“低级M3ss”文章中,将使用这些知识来实现一个DOS多核接口。

多核处理器的基本概念

每个CPU都有自己的一组寄存器和模式,但它们共享内存。这意味着,如果想将一个i7的8个核心都设置为长模式,需要对每个核心执行相同的过程,因为每个核心都有自己的寄存器集、GDT、LDT等。因此,可以在一个CPU上启动真实模式,而将另一个CPU设置为长模式。

虚拟化中也是如此。在虚拟化文章中,解释了如何将CPU设置为这种状态,为了使整个机器处于虚拟化状态,必须将每个CPU都设置为虚拟化状态,这意味着为它们设置VMCS、VMX区域等。

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

因此,在正在创建的一个非常奇怪的驱动程序中,应该能够从Windows中取出一个处理器,将其恢复到真实模式,然后将其余的CPU虚拟化,以创建一个内部调试器。哈哈!这并不容易,对吧。

准备工作

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

ACPI和APIC

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

  • 在内存地址040E处存储真实模式段指针的地方(从未在那里见过它)。
  • 在物理地址0xE0000和0xFFFFF之间的BIOS内存中。

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

正如在RSDP中所述,这仅仅是一个更大结构的签名。可能有ACPI 1.0或ACPI 2.0,将保存结构数据以供进一步使用。每个ACPI表都有一个校验和,ACPI表中所有字节的总和必须是一个低字节等于零的值:

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管理中断,必须启用“Spurious Interrupt Vector Register”,索引为0xF0:

之后,准备发送IPI。IPI(处理器间中断)是通过使用本地APIC的中断命令寄存器发送的。这包括两个32位寄存器,一个位于偏移量0x300,一个位于偏移量0x310(所有本地APIC寄存器都对齐到16字节):

struct R300 { unsigned char VectorNumber; unsigned char DestinationMode:3; unsigned char DestinationModeType:1; unsigned char DeliveryStatus:1; unsigned char R1:1; unsigned char InitDeAssertClear:1; unsigned char InitDeAssertSet:1; unsigned char R2:2; unsigned char DestinationType:2; unsigned char R3:12; };

首先写入0x310寄存器,它包含想要发送中断的处理器的本地APIC。0x300寄存器具有以下结构:

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

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

由于起始地址必须对齐到4096,代码将代码从ASM源代码传输到硬编码地址0x80000,作为快速解决方案。

最后,需要写入"End of Interrupt"(Local Apic + 0xB0)的值为0,以指示可以发送另一个中断。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485