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

2

12

If I want to access the VGA text buffer in X86 which is located at address 0xb8000:

uint16_t *VGA_buffer = (uint16_t*)0xb8000;

Then I index the variable VGA_buffer as a normal array, i.e., VGA_buffer[0], VGA_buffer[1], etc.

However, I read about memory map in x86, the addresses listed there are physical addresses.

My question is:

How does the CPU access this address? Does the CPU knows that any address written explicitly in the code is a physical address and shall not pass by address translation mechanisms (logical address --> virtual address --> to physical address)?

Thanks in advance.

Emmeram answered 1/9, 2017 at 9:31 Comment(6)
If you are in real mode there is no memory mapping. In any other operating system that allows you to access 0xb8000 nothing stops it from mapping a virtual address to the same physical address.Body
On a completely different not (not related to the actual question) you should mark device mapped memory as volatile since a compiler could conceivably apply optimizations that may be unaware the memory has other side effects.. I'd use volatile uint16_t *VGA_buffer = (uint16_t*)0xb8000; .In C99 or later and if VGA_buffer pointer won't change you can mark VGA_buffer pointer as const volatile uint16_t * const VGA_buffer = (uint16_t*)0xb8000;Liar
This is great, but I'm not in real mode; I'm in the protected mode. I'm trying to figure out how such an explicit address is handled in the address translation process even though the address in the aforementioned example is a physical address?Emmeram
Thanks a bunch Michael Petch for your great notes.Emmeram
Are you writing your own OS?Liar
Related: X86 Address Space Controller? explains that decoding different physical address to regular DRAM vs. MMIO vs. device memory is all done inside the CPU, by the "system agent" in Intel CPUs.Isomorphism
I
7

If you want to access a specific physical address while paging is enabled, map that physical address into virtual memory somewhere. If you're running under an existing OS, this is something you have to ask the OS to do for you.


How you ask the OS to do this for you is of course OS-specific.

For example, on Linux you could do this with an mmap() system call on /dev/mem, which is a special device file that gives access to the entire physical address space. See 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? (You need to be root, and even then it's just mapping memory, not running in kernel mode where you could run instructions like lidt).

This superuser answer mentions that Linux's CONFIG_STRICT_DEVMEM restricts it to only actual device memory, and is often enabled in real kernels.

So for example:

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 system calls, e.g. to blit a buffer into physical memory at a given position.

Instead of just uint16_t*, you might define a struct for text mode:

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

All load/store instructions will treat addresses as virtual. Even if the compiler wanted to do something different, it couldn't. x86 has no "store-physical" instruction that bypasses address-translation and paging permission checks.

Remember that the CPU runs machine code produced by the compiler. At that point, there's no distinction left between addresses that appeared as integer constants in the C source vs. addresses of string constants. (e.g. puts("Hello World"); might compile to mov edi,0x4005c4 / call puts).

e.g. look at how this function compiles:

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

In the compiler's asm output (from gcc -O3 for x86-64 Linux, on Godbolt), we see this:

    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

I passed it to puts just to illustrate that absolutely nothing is different in how a pointer that comes from an integer constant is handled. By the time we get to machine code (linker output), the label referring to the string-constant's address has been compiled to an immediate constant, just like the 0xB8000: disassembly output from the same compiler-explorer 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    

It's only after addresses are mapped to physical that the hardware checks to see whether it's regular DRAM, MMIO, or device memory. (This happens in the system agent on Intel CPUs, on chip in the CPU, but outside of an individual core).

And for DRAM it also checks what memory type is in use: WB (write-back), USWC (uncacheable speculative write-combining), or UC (uncacheable) or others. VGA memory is normally USWC, so writing to it one char at a time is slow, and so is reading it. Use movnt stores and movntdqa loads to efficiently access whole blocks.

Isomorphism answered 1/9, 2017 at 21:51 Comment(1)
Downvoter please explain. I think this answers all parts of the question sufficiently, even if the OP isn't using Linux. I explain how C compiles, and what that implies for running with paging enabled.Isomorphism
M
0

In traditional x86 systems the mapping of addresses to IO operations isn't handled in the CPU at all -- it is a function of the northbridge device, which sits between the CPU data and address busses and the memory devices. x86-type CPUs designed for small or embedded systems might integrate the northbridge functionality into the CPU, but it's still distinct functionality. The northbridge decodes the CPU's address lines, and carries out the corresponding IO operations, particularly for graphics adapters.

Melamine answered 1/9, 2017 at 9:45 Comment(4)
but memory translation happens in the CPU (using MMU) before going to the northbridge?Emmeram
CPUs have integrated the northbridge for over 10 years, at least since they started integrating the memory controller. See superuser.com/questions/1226197/x86-address-space-controller. And as the OP says, address translation happens inside the core before a request even checks L1D cache, so this totally doesn't answer the question. Especially since modern CPUs usually have VGA built-in, so VGA memory lives in the same memory chips as the rest of system RAM.Isomorphism
But, with the greatest respect, I'm not sure your response answers the question either -- not the specific point about the distinction between physical/virtual addresses. To say that the "system agent" does it doesn't really provide any more information than to say that the northbridge does it. The question how it does it still seems to me to be unresolved. I accept, however, that I might have just missed it, or that that my grasp of x86 internals is sufficiently vague that I might not understand a detailed response even if I were shown one.Melamine
Didn't see your response since you didn't @KevinBoone notify me. The 2nd last section of my answer explains why all C and user-space asm addresses are virtual addresses under a normal OS like Linux. The question seems to be asking if you can use physical addresses in C, and the answer is no. An explanation of HOW virtual->physical translation works doesn't seem to be necessary here. But since you ask, the TL:DR version is: Page tables set up by the OS have the mappings, and the CPU caches those translations in the TLB. See also https://mcmap.net/q/14089/-what-happens-after-a-l2-tlb-miss/224132Isomorphism

© 2022 - 2024 — McMap. All rights reserved.