X86 CPU 如何将地址转换为 VGA 文本缓冲区等 IO?

How does X86 CPU translate an address to an IO such as VGA text buffer?

如果我想访问位于地址 0xb8000:

的 X86 中的 VGA 文本缓冲区
uint16_t *VGA_buffer = (uint16_t*)0xb8000;

然后我将变量 VGA_buffer 索引为普通数组,即 VGA_buffer[0]VGA_buffer[1]

但是,我读到memory map in x86,那里列出的地址是物理地址。

我的问题是:

CPU如何访问这个地址? CPU 是否知道代码中显式写入的任何地址都是物理地址并且不应通过地址转换机制(逻辑地址 --> 虚拟地址 --> 到物理地址)传递?

提前致谢。

在传统的 x86 系统中,地址到 IO 操作的映射根本不在 CPU 中处理——它是位于 CPU 数据之间的北桥设备的一个功能和地址总线和存储设备。为小型或嵌入式系统设计的 x86 类型 CPU 可能会将北桥功能集成到 CPU 中,但它仍然是不同的功能。北桥解码CPU的地址线,并执行相应的IO操作,特别是对于图形适配器。

如果您想在启用分页时访问特定的物理地址,将该物理地址映射到某处的虚拟内存中。如果您 运行 在现有的 OS 下,这是您必须要求 OS 为您做的事情。


你如何要求 OS 为你做这件事当然是 OS 特定的。

例如,在 Linux 上,您可以通过对 /dev/memmmap() 系统调用来执行此操作,这是一个特殊的设备文件可以访问整个物理地址 space。请参阅 the mem(4) man page. Anything you do with /dev/mem is actually handled by the kernel device driver functions; it's just an API for letting you map physical memory. See also How does mmap'ing /dev/mem work despite being from unprivileged mode?(您需要是 root,即使那样它也只是映射内存,而不是 运行 在内核模式下您可以 运行 指令,如 lidt)。

This superuser answer 提到 Linux 的 CONFIG_STRICT_DEVMEM 将其限制为仅实际设备内存,并且通常在真实内核中启用。

例如:

int fd = open("/dev/mem", O_RDWR);
volatile uint16_t *vgabase = mmap(NULL, 256 * 1024,
                             PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0xb8000);
close(fd);
// TODO: error checking on system-call return values.
// Run  strace ./a.out to see what happens (not recommended with an X server running...)

vgabase[1] = 'a' + (0x07<<8);  // lightgrey-on-black

http://wiki.osdev.org/VGA_Hardware#Video_Memory_Layout says that VGA memory is up to 256kiB, so I mapped it all. Note that the 0xb8000 is used as an offset into /dev/mem. That's how you tell the kernel which physical memory you want to map. You can also use /dev/mem with read/write or pread/pwrite 系统调用,例如在给定位置将缓冲区写入物理内存。

而不只是 uint16_t*,您可以为文本模式定义一个结构:

struct vgatext_char {
    char c;
    union {  // anonymous union so you can do .fg or .color
      struct {uint8_t fg:4,
                      bg:4;
      };
      uint8_t color;
    };
};
// you might want to use this instead of uint16_t, 
// or with an anonymous union of this and uint16_t.

Does the CPU knows that any address written explicitly in the code is a physical address and shall not pass by address translation mechanisms

所有 load/store 指令都将地址视为虚拟地址。 即使编译器想要做一些不同的事情,它也做不到。 x86 没有 "store-physical" 绕过地址转换和分页权限检查的指令。

记住 CPU 运行s 机器代码是由编译器生成的。在这一点上,在 C 源代码中显示为整数常量的地址与字符串常量地址之间没有区别。 (例如 puts("Hello World"); 可能会编译为 mov edi,0x4005c4 / call puts)。

例如看看这个函数是如何编译的:

#include <stdio.h>
int foo() {
    puts("hello world");
    char *p = 0xb8000;
    puts(p);
    return 0;
}

在编译器的 asm 输出(from gcc -O3 for x86-64 Linux,在 Godbolt 上),我们看到:

    sub     rsp, 8

    mov     edi, OFFSET FLAT:.LC0   # address of the string constant
    call    puts
    mov     edi, 753664             # 0xB8000
    call    puts

    xor     eax, eax          # return 0;
    add     rsp, 8
    ret

我将它传递给 puts 只是为了说明在处理来自整数常量的指针方面绝对没有什么不同。当我们到达机器代码(linker 输出)时,引用字符串常量地址的标签已被编译为立即常量,就像 0xB8000: 来自同一编译器的反汇编输出-探索者link:

 sub    rsp,0x8

 mov    edi,0x4005d4             # address of the string constant
 call   400410 <puts@plt>
 mov    edi,0xb8000
 call   400410 <puts@plt>

 xor    eax,eax
 add    rsp,0x8
 ret    

只有在地址映射到物理地址后,硬件才会检查它是常规 DRAM、MMIO 还是设备内存。 (这发生在英特尔 CPU 的 system agent 中,在 CPU 的芯片上,但在单个内核之外)。

对于 DRAM,它还会检查正在使用的内存类型:WB(回写)、USWC(不可缓存的推测写入组合)或 UC(不可缓存的)或其他。 VGA 内存通常是 USWC,因此一次写入一个字符很慢,读取它也是如此。使用 movnt 存储和 movntdqa 加载来有效地访问整个块。