切换至Long Mode后VGA文本输出异常的原因排查
问题背景
我正在开发一个x86_64架构的启动Demo:
- 实模式阶段通过
int 10h设置视频模式3,向0xb8000地址写入内容正常 - 32位保护模式(带分页)下的C代码第二阶段运行完全正常
- 切换到启用PAE和LME的x86_64模式后,显示功能出现异常
正常工作的代码
以下代码在64位模式下能正常将屏幕填满'A'字符:
for (uint32_t i = 0xb8000; i < 0xb8000 + (25 * 80 * 2); i += 2) { *((volatile uint16_t*)i) = 0x0741; }
对应的汇编代码:
000000000000843f <main>: 843f: f3 0f 1e fa endbr64 8443: 55 push %rbp 8444: 48 89 e5 mov %rsp,%rbp 8447: c7 45 fc 00 80 0b 00 movl $0xb8000,-0x4(%rbp) 844e: eb 0c jmp 845c <main+0x1d> 8450: 8b 45 fc mov -0x4(%rbp),%eax 8453: 66 c7 00 41 07 movw $0x741,(%rax) 8458: 83 45 fc 02 addl $0x2,-0x4(%rbp) 845c: 81 7d fc 9f 8f 0b 00 cmpl $0xb8f9f,-0x4(%rbp) 8463: 76 eb jbe 8450 <main+0x11> 8465: 90 nop 8466: eb fd jmp 8465 <main+0x26>
功能失效的代码
修改为指针索引形式后,功能异常:屏幕显示粉色控制字符(说明0x07被当作字符、0x41被当作颜色代码),且右下角最后两个字符未填充:
volatile uint16_t *screen_base = (volatile uint16_t*)0xb8000; for (uint32_t i = 0; i < 25 * 80; i++) { screen_base[i] = 0x0741; }
对应的汇编代码:
000000000000843f <main>: 843f: f3 0f 1e fa endbr64 8443: 55 push %rbp 8444: 48 89 e5 mov %rsp,%rbp 8447: 48 c7 45 f0 00 80 0b movq $0xb8000,-0x10(%rbp) 844e: 00 844f: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 8456: eb 17 jmp 846f <main+0x30> 8458: 8b 45 fc mov -0x4(%rbp),%eax 845b: 48 8d 14 00 lea (%rax,%rax,1),%rdx 845f: 48 8b 45 f0 mov -0x10(%rbp),%rax 8463: 48 01 d0 add %rdx,%rax 8466: 66 c7 00 41 07 movw $0x741,(%rax) 846b: 83 45 fc 01 addl $0x1,-0x4(%rbp) 846f: 81 7d fc cf 07 00 00 cmpl $0x7cf,-0x4(%rbp) 8476: 76 e0 jbe 8458 <main+0x19> 8478: 90 nop 8479: eb fd jmp 8478 <main+0x39>
尝试将指针改为0xb8003能掩盖问题,但这显然不是正确解法,求问题根源。
1. 核心问题:分页映射的地址对齐/字节序错误
VGA文本模式的内存布局为:每个字符占2字节,低地址字节是ASCII字符,高地址字节是颜色属性。你写入的0x0741按小端序存储应为0x41(字符'A')在低地址、0x07(灰底黑字)在高地址,这是正确的格式。
失效代码出现字符与属性反转的现象,说明实际写入内存的字节顺序被颠倒了——0x07在低地址(被当作字符)、0x41在高地址(被当作颜色)。结合两种写法的差异,问题出在64位模式下分页表对对齐的16位访问的映射错误:
- 第一种写法中,循环后期会出现未对齐的16位访问(最后一次写入地址
0xb8f9f,为奇数地址),x86 CPU会将其拆分为两次独立的字节访问,而字节访问的映射是正确的,因此最终能填满屏幕。 - 第二种写法的所有访问都是对齐的16位操作,触发了分页映射中的字节反转错误,导致写入的字节顺序颠倒;同时,对齐访问无法覆盖VGA内存的最后一个字节,造成右下角两个字符未填充。
2. 次要问题:分页表的地址偏移
你提到将指针改为0xb8003能掩盖问题,这进一步验证了地址偏移的存在:0xb8003是奇数地址,写入时会触发未对齐访问,绕过分页映射中对齐访问的错误处理,从而得到正确的字节顺序。
解决步骤
检查分页表配置:
- 确保虚拟地址
0x00000000000b8000到0x00000000000b8fff与物理地址是1:1映射,没有人为添加偏移或字节反转规则。 - VGA内存属于内存映射I/O,需将其分页属性设置为未缓存(UC),避免缓存机制干扰写入操作。
- 确保虚拟地址
验证字节访问的正确性:
尝试用字节级写入替代16位写入,确认是否能正常工作:volatile uint8_t *screen_base = (volatile uint8_t*)0xb8000; for (uint32_t i = 0; i < 25 * 80; i++) { screen_base[2*i] = 'A'; // 写入字符字节 screen_base[2*i+1] = 0x07; // 写入属性字节 }如果这段代码正常,说明问题确实出在对齐16位访问的映射配置上。
修正64位模式的初始化流程:
检查PAE和LME的启用代码,确保CR4寄存器的PAE位、EFER寄存器的LME位设置正确,且分页表的各级条目(PML4、PDPT、PD、PT)均正确指向VGA物理内存。
内容的提问来源于stack exchange,提问作者performancematters

