64 位处理器上的 32 位(文章写于 2005 年)






4.74/5 (8投票s)
构建您自己的 64 位 Windows 扩展器
前言
这篇文章写于 2005 年 10 月,但从未发表。 这是提交给 Doctor Dobbs Journal 并经过编辑的最终草稿。在此期间,我提交了三篇文章,其中两篇如下链接所示已发表。然而,这篇文章从未发表,鉴于 DDJ 现已关闭,它将永远不会被发表。考虑到这篇文章已经写好,尽管其背景已是 10 年前,我决定在此发布,仅作存档之用。
我没有修改过这篇文章,自 2005 年 10 月初次撰写以来。有几点我会写得不同,但考虑到这篇文章不是为今天而写的,我已将其保持不变。阅读这篇文章的视角是,当时我们使用的是单核处理器,其中一些是 x64 兼容的。但是,即使它们兼容 x64,它们仍然与 Windows XP 32 位等操作系统一起使用。这篇文章是一个有趣的“假设”场景,将 16 位和 32 位的 DOS 扩展程序与 32 位到 64 位的“Windows 扩展程序”的可能性进行了比较。
引言
不久前,大多数 PC 还在 32 位处理器上运行 16 位操作系统。那时,MSDOS 盛行,“Ralf Brown”和“DPMI”是熟悉的名字,每个人都知道端口 3DAh 用于垂直回扫。当然,也有一些人使用 OS/2 或某种 UNIX 风格的 32 位操作系统。至于开发者,他们通常必须针对 16 位操作系统开发应用程序,从而失去了从 32 位中获得的优势。DOS 扩展程序——允许进入保护模式并让应用程序在没有 66h 指令前缀的情况下利用 32 位指令的库——解决了这个问题。它们还允许您访问高达 4GB 的内存,具体取决于您使用的扩展程序的特性。最受欢迎的 DOS 扩展程序是 PROT(参见 Al Williams 的“Roll Your Own DOS Extender”,DDJ,1990 年 10 月)、DOS4GW(Microsoft 的 32 位扩展)、Pharlap 的 DOS Extender 和 Trans PMODE(它与 Watcom 的 C/C++ 编译器集成得很好)。大多数扩展程序只是使用 DOS 保护模式中断 (DPMI),它通过中断抽象了保护模式的实现。它们还提供了一种执行 16 位 BIOS 中断的便捷方法,这样您就不需要自己实现切换视频模式等功能。其他一些扩展程序则自行实现了 32 位保护模式,而有些则只是进行了“大实模式”(也称为“不真实模式”)。如今,我们面临着类似的局面,即 64 位兼容机器运行 32 位操作系统。诚然,这一次我们不必等待太久,更强大的主流操作系统就会出现。尽管如此,我们还是希望能够利用 64 位处理器的功能。为此,我在本文中提供了一个驱动程序,它扩展了 Windows 以利用 CPU 的 64 位功能,这在很大程度上类似于 DOS 扩展程序为 16 位和 32 位系统所做的那样。这个 Windows 驱动程序将处理器置于 64 位模式,让您可以运行 64 位应用程序。该驱动程序会保存操作系统,以便您可以将其恢复到原始状态。诚然,这个驱动程序很简单。例如,它不执行任何调度。尽管如此,它允许您在不安装新操作系统的情况下,通过 Windows XP 利用处理器的强大功能。
什么是长模式?
启用本机 64 位功能的处理器模式称为“长模式”。它有两个子模式:本机 64 位模式和兼容模式。本机 64 位模式允许执行 64 位指令并定义新的行为。兼容模式执行 32 位和 16 位应用程序,类似于保护模式。除了简单地扩展位数之外,保护模式和长模式之间存在差异。这些可以在 AMD64 处理器架构手册(http://www.amd.com/)中找到。在此,我将简要概述一些更有趣的差异。在撰写本文时,我还没有将 AMD64 与 Intel 的 EM64T 进行比较。
无虚拟 8086 模式。当处理器处于长模式时,AMD64 不支持 V86 模式。V86 是一种模式,允许操作系统在保护模式环境中隔离和调度为实模式编写的应用程序。简而言之,这是处理器对 16 位旧应用程序虚拟化的支持。当处理器处于保护模式时,它仍然支持 V86 作为子模式。同样,当处理器处于长模式时,它只支持两个子模式——本机 64 位和兼容模式。在本机 64 位模式下,选择器引用长模式描述符并执行 64 位指令。在兼容模式下,选择器引用旧描述符并执行 16 位或 32 位指令。
必须启用分页。保护模式架构不需要您实现分页,这使得 DOS 扩展程序的实现非常简单。长模式并非如此,它不仅需要分页,而且实际上将分页作为保护模式的物理地址扩展 (PAE) 的扩展来实现。
无分段。在 Native-64-bit Long mode 中,描述符中指定的基地址不被使用。在此模型中,虚拟地址就是线性地址。在分页转换之前,虚拟地址没有中间转换。当设置了长模式位时,描述符的大部分实际上并未被使用。如果描述符中的长模式位未设置,则系统处于兼容模式,并且描述符的解释方式与在保护模式下相同。
更多通用寄存器。有八个新寄存器,R8 到 R15,它们有 8 位、16 位、32 位和 64 位形式。当处理器处于 Native-64-bit 模式时,这些寄存器可用。
PAE 分页体系结构。物理地址扩展中使用的体系结构是长模式下分页实现的基础。在编写 Windows 扩展程序之前,您需要理解这些。图 1 显示了 32 位保护模式下 4K 分页模型的图形概述。
处理器实际上支持 2MB、4MB 和 4KB 的分页实现,它们也可以混合使用。软件中使用的虚拟地址首先会添加到操作系统维护的描述符表的基地址。最终结果是线性地址,然后进行分页转换。线性地址中的位会索引到操作系统实现的几个表中。CR3 寄存器是指向进程中第一个页的指针。在非 PAE 分页中,此寄存器仅指向一个页目录。PAE 的实现引入了一个名为“页目录指针”的第三个表,该表包含四个指针,每个指针引用一个完整的页目录。页目录本身是一个表,在 4K 分页实现中引用页表。页表中的条目引用了线性地址的最后一位索引的物理内存的 4KB 基址。
图 2 显示了 4K 分页模式下的完整细分。PAE 的实现允许操作系统访问高达 36 位的物理内存页。然而,虚拟地址空间并未增加,仍为 32 位。这使得处理器可以在多个进程之间使用更多的物理内存,因为它们各自拥有独立的虚拟地址空间。然后,处理器可以利用具有超过 4GB RAM 的系统来优化分页。历史重演,操作系统现在可以实现内存的“视图”。这允许应用程序映射超过 4GB 的地址空间。这让我们回到了 XMS/EMS 的时代,尽管对于应用程序来说,不需要超过 4GB 的物理内存来实现此功能,因为操作系统可以轻易地“欺骗”。在 64 位体系结构中,物理地址扩展通过引入页映射级别 4 表得到扩展。该表允许索引其他页目录指针表。
图 4 说明了长模式下与新分页相关的虚拟地址。分页位实际上是相同的,除了 PDP 扩展到位 38 和 PML4 的添加以及符号扩展。当子模式为 Native 64 时,描述符表在计算线性地址时也不起作用。符号扩展仅用作表中的负索引,以便地址上的数学运算可以导致正确的映射。符号扩展要么是全 1,要么是全 0。
概念架构
再次强调,我的主要目标是创建一个基本的 64 位 Windows 扩展程序。在用户模式下运行的应用程序将简单地向驱动程序发送一个 IOCTL,其中包含一个包含 64 位代码的缓冲区。然后,驱动程序会转换当前操作系统,然后执行此代码。代码完成后,它会恢复操作系统并完成 IRP。第一步是编写一个基本的驱动程序外壳和应用程序。第二阶段是为项目设置阶段,并在每个阶段进行一次基本测试,以验证代码是否正常工作。由于我只有一台 AMD64,我用于测试的机器是运行 Windows XP 的 Athlon64 3200+。大部分项目都超出了操作系统的正常范围。这意味着如果出现问题,机器很可能会重启。在没有任何硬件工具的情况下,您需要实现自己的调试器。这可以是通过写入屏幕、逐步添加代码,甚至选择性启用和大量的设置调试消息。以下是工作项的简短列表以及我如何验证每个阶段都已通过:
保存和恢复操作系统。起点是保存操作系统状态并尝试恢复它。这将有助于验证代码的正确性,并且相对容易做到。状态的保存侧重于我们的代码在转换过程中可能更改的任何内容。这包括描述符表、控制寄存器和选择器。
创建全局描述符表。全局描述符表是唯一必需的表。可以通过简单地禁用中断来绕过中断表。为了构建描述符表,我查阅了 AMD64 架构手册,并为旧模式和长模式描述符创建了结构。我需要同时创建两者,并且不知道在从保护模式切换到长模式时是需要一个 GDT 还是两个 GDT。事实证明,在切换到长模式时可以使用一个 GDT。您可以使用此相同的 GDT 并用您的旧式和新式描述符填充它。这还需要实现旧式描述符表的镜像并创建我自己的。然后,测试就是简单地加载描述符表并设置选择器。设置和重置会导致选择器的隐藏部分被刷新并从新 GDT 重新加载。使用 LGDT 汇编指令加载描述符表,并使用 SGDT 指令将其存储到内存位置。
实现页表。在保护模式下有两种分页实现——旧式分页和 PAE 机制。为分页实现的 कोड 应镜像操作系统使用的方法。我测试的操作系统正在使用 PAE,因此我无法测试我的旧式分页实现。页表的测试只是交换出 CR3 并交换进我自己的。然而,要验证测试,您需要重置 CR4 中的全局位。处理器允许操作系统通过在页表中使用全局位来提示要缓存哪些页面。如果您执行的代码位于全局页面上,当您交换 CR3 时,即使您的页表不正确,您很可能也不会崩溃。解决方案是在 CR4 中取消设置此位,从而在提供新页面之前禁用全局页面缓存。您还可以通过写入内存位置来验证不同的页表是否正常工作,并在以后使用调试器进行验证。
禁用分页。在进入 64 位模式之前需要禁用分页,这需要创建同一映射页。当虚拟地址等于物理地址时,创建同一映射页。这允许在不将当前指令传输到随机内存位置的情况下启用或禁用分页。此测试将简单地加载我们的页表,因为我们对映射有更多控制,然后完全禁用分页。这将有助于验证我们的分页是否正常工作;但是,它们可能需要一些技巧来实现。分页通过 CR0 的高位进行禁用或启用。在 Windows 中,驱动程序内存位于非常高的地址空间,因此您需要跳转到代码中的另一个地址,或者您可以使用负数映射您的描述符基地址。
设置堆栈。我决定在我设置并跳转到 64 位模式时拥有自己的堆栈。然后需要正确映射堆栈,并且可以通过将值推入堆栈、恢复旧堆栈和操作系统,最后在调试器中验证内存位置来轻松测试。我通常尝试将 0DEADBEEFh 写入内存位置,并在之后验证它已被标记。
切换到 64 位。这是令人兴奋的,在您验证 64 位代码本身是否正常工作之前,您想测试兼容模式。这是您进入长模式时处理器最初开始的模式。通过 CPU 的扩展功能寄存器完成到 64 位模式的切换。在为长模式进行设置时,我进行了一项优化。这可以在代码中看到;然而,我只是重用了所有页表。同样,64 位实现只是 32 位 PAE 的扩展。我需要做一个增强:创建我自己的页目录指针表副本。在旧式模式下,位 1 是保留的,必须为 0;然而,在 64 位模式下,它可以是 1 以表示读/写。这些不是兼容的实现,只需要一位就可以重启机器。
执行 64 位代码。我提供了一些您可以执行的示例 64 位应用程序(电子版)。要运行它们,请键入 *mutinyapp <file.bin>*。当前驱动程序不支持未使用物理地址扩展的系统,因此您必须确保已启动到此模式。这可以通过编辑您的 BOOT.INI 来使用 /PAE 开关来实现。示例代码使用 0D8000000h,这是我的显卡为其视频显示指定线性帧缓冲区的地址。驱动程序映射了此地址和标准 VGA 视频位置供应用程序使用。标准 VGA 位置是 0A0000h,它镜像 0D8000000h 的前 64KB,但仅限于屏幕的可见区域。我的显卡涉及一个“跨距”(也称为“步长”),任何使用过 Direct Draw 的人都明白这意味着什么。本质上,视频内存不是每行线性的,并且每行比实际视频模式显示行长。这意味着当您到达一行的末尾时,您需要加上跨距才能进入下一个可见扫描线。我发现我的显卡上的总行匹配显示卡的最大分辨率。您可以通过显示驱动程序的设备管理器中的资源找到您显卡上的视频位置。您可以使用此信息来找出要为您的特定卡映射内存的位置,并且可以更改驱动程序来使用它。要构建应用程序,您只需确保任何需要的内存都已映射。如果您需要增加分配的内存池,请在驱动程序中进行。您还需要使用 RMDHR 应用程序删除任何可执行文件头,因为此驱动程序会执行原始的 64 位汇编代码。您可以通过 BUILD.BAT 文件中的示例了解如何构建应用程序。这个概念实际上有一个真实的实现——虚拟化。这个概念可以应用于允许在 32 位操作系统上运行的 64 位操作系统的执行和虚拟化!
增强功能
您可以在此概念验证实现中进行大量增强,首先是 64 位中断描述符表。这对键盘支持来说将是一个重要的增强。它还将允许您实现自己的调试器,这有助于查找/修复问题并提高系统的稳定性。
代码中有个地方我称之为“无人区”。在无人区,如果发生错误,您的计算机就会重启。在当前实现中,整个转换和程序的执行都在无人区。在您开始转换操作系统之前,操作系统内核调试器仍然可以使用。实现自己的调试器将限制无人区的范围,从而允许查找和调试问题。目标是只有一个小部分代码处于这种灰色执行区域。
原型肯定需要更多的测试和增强。这需要对其他系统进行测试,甚至可能实现非 PAE 支持。当前切换到 64 位模式的代码由操作系统控制,我不会尝试在它位于页面边界上时进行转换。理想的实现是在同一映射页跨越物理非线性边界时进行分配或尝试分配。同一映射页需要物理上连续,以防止故障。如果驱动程序无法切换到 64 位模式,它应该知道。还可以添加多处理器支持,以停止并利用两个处理器处于 64 位模式。
增强功能可以扩展到实现自己的 64 位操作系统虚拟机。引擎也可以仅仅用于编写和测试您的 256 字节 64 位演示效果。
写于 2005 年 10 月