Как процессор X86 переводит адрес в IO, например, в текстовый буфер VGA?

Если я хочу получить доступ к текстовому буферу VGA в X86, который находится по адресу 0xb8000:

uint16_t *VGA_buffer = (uint16_t*)0xb8000;

Затем я индексирую переменную VGA_buffer как обычный массив, т.е. VGA_buffer[0], VGA_buffer[1] и т.д.

Тем не менее, я читал о карте памяти в x86, там перечислены физические адреса.

Мой вопрос:

Как процессор получает доступ к этому адресу? Знает ли CPU, что любой адрес, явно указанный в коде, является физическим адресом и не должен проходить через механизмы преобразования адресов (логический адрес → виртуальный адрес → на физический адрес)?

Спасибо заранее.

Ответ 1

Если вы хотите получить доступ к определенному физическому адресу во время подкачки, сопоставить этот физический адрес в виртуальной памяти где-нибудь. Если вы работаете под существующей ОС, это то, что вы должны попросить ОС сделать для вас.


Как вы просите ОС сделать это для вас, конечно, зависит от ОС.

Например, в Linux вы можете сделать это с помощью системного вызова mmap() на /dev/mem, который является специальным файлом устройства, предоставляющим доступ ко всему физическому адресному пространству. См. справочную страницу mem(4). Все, что вы делаете с /dev/mem, фактически обрабатывается функциями драйвера устройства ядра; это просто API, позволяющий вам отображать физическую память. См. Также Как работает mmap'ing/dev/mem, несмотря на то, что он из непривилегированного режима? (вам нужно быть root, и даже тогда он просто отображает память, а не работает в в котором вы могли бы запускать команды типа lidt).

В ответе суперпользователя упоминается, что 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 говорит, что VGA-память до 256киB, поэтому я отобразил все. Обратите внимание, что 0xb8000 используется как смещение в /dev/mem. То, как вы указываете ядру, какую физическую память вы хотите отобразить. Вы также можете использовать /dev/mem с чтением/записью или 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.

Знает ли CPU, что любой адрес, явно указанный в коде, является физическим адресом и не должен проходить через механизмы преобразования адресов.

Все инструкции загрузки/хранения будут обрабатывать адреса как виртуальные. Даже если компилятор хотел сделать что-то другое, он не смог. x86 не имеет инструкции "store-physical", которая обходит проверки разрешений адресов и подкачки.

Помните, что CPU запускает машинный код, созданный компилятором. В этот момент не было различий между адресами, которые появлялись как целочисленные константы в источнике 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 компилятора (из gcc -O3 для 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, чтобы проиллюстрировать, что абсолютно ничего не изменилось в том, как обрабатывается указатель, который исходит из целочисленной константы. К тому времени, когда мы получаем машинный код (вывод компоновщика), метка, ссылающаяся на адрес строковой константы, была скомпилирована с непосредственной константой, как и вывод 0xb8000: disassembly из той же самой ссылки для компилятора-проводника:

 sub    rsp,0x8

 mov    edi,0x4005d4             # address of the string constant
 call   400410 <[email protected]>
 mov    edi,0xb8000
 call   400410 <[email protected]>

 xor    eax,eax
 add    rsp,0x8
 ret    

Это только после того, как адреса сопоставлены с физическим, что аппаратное обеспечение проверяет, является ли оно регулярным DRAM, MMIO или память устройства. (Это происходит в системном агенте на процессорах Intel, на чипе в CPU, но вне отдельного ядра).

И для DRAM он также проверяет, какой тип памяти используется: WB (обратная связь), USWC (несовместимое умозрительное объединение записи) или UC (несовместимый) или другие. VGA-память обычно USWC, поэтому запись в нее одного char в то же время медленная, и поэтому читает ее. Используйте movnt магазины и movntdqa для эффективного доступа к целым блокам.

Ответ 2

В традиционных системах x86 сопоставление адресов с операциями ввода-вывода вообще не обрабатывается в ЦПУ - это функция устройства северного моста, которая находится между данными процессора и адресными шинами и устройствами памяти. Процессоры x86-типа, предназначенные для небольших или встраиваемых систем, могут интегрировать функциональность северного моста в CPU, но она все еще отличается функциональностью. Северный мост декодирует адресные строки процессора и выполняет соответствующие операции ввода-вывода, особенно для графических адаптеров.