assembly 为什么切换到长模式会导致VGA文本输出行为异常?

aiazj4mn  于 2023-06-06  发布在  其他
关注(0)|答案(1)|浏览(277)

我正在开发一个引导x86_64机器的小型演示程序。在早期的init(真实的模式)中,我通过int 10 h设置了videomode 3。然后,我在0xb 8000处写入内存Map文本。我的第二阶段已经是高级C代码了。这在32位的保护模式和分页模式下工作得很好。
我更改了引导加载程序,使其也启用PAE,然后设置LME,然后跳转到第二阶段(然后已经编译为x86_64)。这就是我的展示崩溃的地方,我不知道发生了什么。我一直在调试小样本,并有一些即使在64位模式下也能可靠工作的东西:

for (uint32_t i = 0xb8000; i < 0xb8000 + (25 * 80 * 2); i += 2) {
        *((volatile uint16_t*)i) = 0x0741;
    }

正如预期的那样,这将使屏幕充满所有“A”。下面是生成的程序集:

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>

但是,当我将代码更改为:

volatile uint16_t *screen_base = (volatile uint16_t*)0xb8000;
    for (uint32_t i = 0; i < 25 * 80; i++) {
        screen_base[i] = 0x0741;
    }

它停止工作;它输出粉红色的控制字符(表示“0x 07”是字符,“0x 41”是颜色代码),但甚至不填充整个屏幕(右下端的最后两个字符不填充)。下面是生成的程序集:

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>

奇怪的是,我可以通过修改指向0xb 8003的指针来掩盖这个问题,但这显然是不正确的。我不知道这是怎么回事,有人知道会发生什么吗?

roejwanj

roejwanj1#

asm在调试版本中是fairly painful to follow,但看起来很正常。您是否100%确定您在jmp far或任何设置了L位的CS描述符之后处于64位模式?因为有确凿的证据表明你不是。
32位模式下的0x48dec eax的操作码(而不是雷克斯.W前缀),看起来它可能解释了-3字节的偏移量。add %rdx, %rax变为dec %eax; add %edx, %eax。之前,在莱亚之前有一个dec %eax,它使dec %eax加倍,在mov -0x10(%ebp),%eax存储之前。
您的版本通过将uint32_t转换为指针来避免任何雷克斯前缀。请注意,这些指令都不使用64位操作数大小、R8-R15或BPL-DIL,因此它们都不以机器码中的404F字节开头。(除了最初的mov %rsp, %rbp,但EAX在那一点上并不活跃;对EAX的下一次访问是只写的。)
所以这是相当强有力的证据,表明CPU没有处于完整的64位模式。使用Bochs单步切换到长模式,并检查您实际处于什么模式。和单步执行指令中的不工作代码;您将看到48字节解码为单独的指令。(你也可以在QEMU + GDB中这样做; GDB可能不确定CPU处于什么模式,但通过陷阱标志TF单步执行将反映CPU实际正在执行的操作。
顺便说一句,GCC调试版本更喜欢首先使用EAX/RAX来计算任何表达式,可能是因为它是返回值寄存器。如果GCC碰巧选择了不同的寄存器,那么递减EAX也就没什么关系了。但你肯定会在某个时候遇到问题,例如。当GCC使用RAX时,因为它是返回值寄存器,或者当它试图使用带有0x40雷克斯前缀的DIL字节寄存器时(32位模式下为inc eax)。

相关问题