LLM 解释
在 DOS 环境下通过 BIOS 中断 INT 15h, AH=87h 访问 1MB 以上的扩展内存(Extended Memory),需要严格遵循全局描述符表(GDT)的格式要求。原帖中尝试读取线性地址 3FF40000(即接近 1GB 的物理内存位置,通常属于 ACPI 表所在的区域)时遇到了错误或返回值为 0 的问题。
以下是对原代码实现逻辑的纠正与技术背景补充:
1. INT 15h, AH=87h 的工作机制
该功能允许实模式下的程序在不进入保护模式的情况下,由 BIOS 切换到临时保护模式来移动数据。它需要调用者准备一个包含 6 个描述符的 GDT 结构:
-
空描述符 (Dummy): 8 字节。
-
GDT 自身描述符 (GDT Alias): 8 字节。
-
源地址描述符 (Source): 8 字节,指向待读取的内存。
-
目标地址描述符 (Destination): 8 字节,指向实模式下的缓冲区。
-
BIOS 代码段描述符: 8 字节。
-
堆栈段描述符: 8 字节。
原代码中 TheBuffer 的偏移逻辑和字节填充存在多处错误。
2. GDT 描述符格式纠正
8 字节描述符的物理地址分布如下:
- 第 0-1 字节:段界限 (Limit) 的低 16 位。
- 第 2-4 字节:物理基地址 (Base) 的低 24 位。
- 第 5 字节:访问权限位(通常设为
0x93,表示可读写的数据段)。
- 第 6 字节:段界限的高 4 位及属性。
- 第 7 字节:物理基地址的高 8 位。
3. 代码中的错误分析
-
物理地址计算错误:实模式下的缓冲区地址不能简单地通过
FP_SEG 和 FP_OFF 直接拆分填入。必须将其转换为 24 位或 32 位的线性物理地址(Segment << 4 + Offset)。
-
GDT 填充位置错误:
INT 15h, AH=87h 规定寄存器 ES:SI 指向 GDT 的起始地址。源描述符位于 ES:SI + 0x10,目标描述符位于 ES:SI + 0x18。原代码中填充 TheBuffer 的下标处理不规范,且未正确处理基地址的高位字节。
-
环境依赖:在 2010 年左右的硬件上,
3FF40000 这样的高位地址可能位于 BIOS 系统区或保留内存中。如果该区域被芯片组屏蔽、属于不可读的内存映射 I/O(MMIO),或者 A20 地址线未开启(尽管此中断通常会自动处理 A20),则无法读到有效值。
4. 正确的实现逻辑示例
若要从线性地址 target_phys_addr 读取数据到实模式缓冲区 Buffer,关键步骤如下:
#include <dos.h>
#include <stdio.h>
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef unsigned long DWORD;
// 必须 8 字节对齐以保证兼容性
BYTE gdt[48];
void build_descriptor(BYTE* desc, DWORD base, WORD limit) {
desc[0] = limit & 0xFF; // Limit 0:15
desc[1] = (limit >> 8) & 0xFF;
desc[2] = base & 0xFF; // Base 0:15
desc[3] = (base >> 8) & 0xFF;
desc[4] = (base >> 16) & 0xFF; // Base 16:23
desc[5] = 0x93; // Access: Present, Ring 0, Data, Expand-up, Writable
desc[6] = (limit >> 16) & 0x0F; // Limit 16:19 + Attributes
desc[7] = (base >> 24) & 0xFF; // Base 24:31
}
int fetch_extended_memory(DWORD src_phys, void far* dest_ptr, WORD bytes_count) {
union REGS in, out;
struct SREGS sregs;
DWORD dest_phys;
// 转换实模式远指针为物理地址
dest_phys = ((DWORD)FP_SEG(dest_ptr) << 4) + FP_OFF(dest_ptr);
// 清空 GDT
for(int i=0; i<48; i++) gdt[i] = 0;
// 源描述符 (Index 2, Offset 0x10)
build_descriptor(&gdt[0x10], src_phys, bytes_count - 1);
// 目标描述符 (Index 3, Offset 0x18)
build_descriptor(&gdt[0x18], dest_phys, bytes_count - 1);
in.h.ah = 0x87;
in.x.cx = bytes_count / 2; // 以字为单位
in.x.si = FP_OFF(gdt);
sregs.es = FP_SEG(gdt);
int86x(0x15, &in, &out, &sregs);
return (out.x.cflag == 0); // 返回 1 表示成功
}
5. 关于 FDAPM 与 ACPI 关机
FDAPM 是 FreeDOS 环境下的电源管理工具。它之所以能实现关机,是因为它能够解析 ACPI 表(通过在高端内存搜索 RSD PTR 签名),定位到 FADT 描述表,并获取 PM1a_CNT_BLK 寄存器的 IO 端口地址。关机操作通常是向该端口写入一个特定的 SLP_TYP 值并置位 SLP_EN 位。
原帖中读取 3FF40000 失败的原因可能是:
-
地址不对:ACPI 表的物理地址并非固定,必须通过扫描内存中的
RSDP 结构来获取。
-
数据单位:
INT 15h, AH=87h 的 CX 寄存器是以字(Word)为单位计算的,如果 CX 设置错误,读取长度会异常。
-
内存可见性:部分笔记本电脑或服务器的 ACPI 区域位于 4GB 地址空间的顶端,实模式下的 24 位地址限制可能无法覆盖这些区域。虽然
AH=87h 在 386 以后的 BIOS 中支持 32 位线性地址(即 4GB 范围),但某些 16 位 C 编译器的库函数可能未正确处理超出 16MB(24位地址限制)以上的地址填充。