Switching from 4MB to 4KB paging

Question about which tools to use, bugs, the best way to implement a function, etc should go here. Don't forget to see if your question is answered in the wiki first! When in doubt post here.
Post Reply
Thpertic
Member
Member
Posts: 56
Joined: Sun Sep 16, 2018 6:46 am

Switching from 4MB to 4KB paging

Post by Thpertic »

I have 4MB paging enabled from the bootloader to load my higher-half kernel at 0xC0000000.

I created two Page Tables where I identity page the first 4MB and the higher-half kernel.

Code: Select all

uint32_t *first4MBpt = kmalloc();
uint32_t virt = 0;
for (i = 0; i < 1024; i++, virt += PAGE_SIZE)
    first4MBpt[i] = virt | 0x000000003;

uint32_t *kernelPT = kmalloc();

// Mapping kernel space - should be from the 768th pde and the 256th pte (for KERNEL_VIRTUAL_BASE == 0xC00000000)
for (i = 0x100000, virt = KERNEL_VIRTUAL_BASE; virt <= (_half_maxStack + KERNEL_VIRTUAL_BASE); i += PAGE_SIZE, virt += PAGE_SIZE)
    kernelPT[PAGE_TABLE_INDEX(i)] = virt | 0x000000003;
Then I created a Page Directory to hold them and set as current.

Code: Select all

pageDirectory[0] = (uint32_t)(first4MBpt) | 0x000000003;
pageDirectory[PAGE_DIRECTORY_INDEX(KERNEL_VIRTUAL_BASE)] = (uint32_t)(kernelPT) | 0x000000003;

_cur_pageDirectory = pageDirectory;
Now I have to switch the type of paging from 4MB to 4KB and set the new Page Directory.
This is my entire .asm code:

Code: Select all

section .text 
; Needs a parameter: the physical address of the Page Directory
extern _cur_pageDirectory
global switch_to4kb

switch_to4kb:
    pusha

    ; Load PDBR (CR3), it must contains the physical address of the Page Directory
    mov ecx, [_cur_pageDirectory]
    mov cr3, ecx

    ; Reset PSE bit in CR4 to enable 4KB pages
    mov ecx, cr4
    and ecx, 0xffffffef
    mov cr4, ecx

    popa
    ret
CR3 get loaded, but then Bochs throws this error: bx_dbg_read_linear: physical memory read error (phy=0x00000000c000527a, lin=0x00000000c010527a).
The code is executed at that address, so what should I do?
I tried to only switch cr4 and it worked, but the new PD is not loaded so it's useless.

Can anyone help me? Thanks.
nullplan
Member
Member
Posts: 1798
Joined: Wed Aug 30, 2017 8:24 am

Re: Switching from 4MB to 4KB paging

Post by nullplan »

There is no need to disable PSE. You can keep your 4MB pages for the kernel around and use 4kB pages for everything else.

Also, you are mapping virtual addresses to virtual addresses. This will not work. You need to write the physical addresses of the kernel into the page tables. What bochs is saying here is, you tried to execute physical address 0xc000XXXX, which is usually an IO memory hole. So it complains about that.
Carpe diem!
Thpertic
Member
Member
Posts: 56
Joined: Sun Sep 16, 2018 6:46 am

Re: Switching from 4MB to 4KB paging

Post by Thpertic »

nullplan wrote:There is no need to disable PSE. You can keep your 4MB pages for the kernel around and use 4kB pages for everything else.
Can you please explain this further?
nullplan wrote:Also, you are mapping virtual addresses to virtual addresses. This will not work. You need to write the physical addresses of the kernel into the page tables.
Right, my bad!
nullplan wrote:What bochs is saying here is, you tried to execute physical address 0xc000XXXX, which is usually an IO memory hole. So it complains about that.
Ok, but EIP is executing there, so what should I do?
Octocontrabass
Member
Member
Posts: 5581
Joined: Mon Mar 25, 2013 7:01 pm

Re: Switching from 4MB to 4KB paging

Post by Octocontrabass »

You can have a mixture of 4kB and 4MB pages. For each page directory entry, bit 7 determines whether the entry points to a single 4MB page or a page table containing 1024 4kB pages.
nullplan
Member
Member
Posts: 1798
Joined: Wed Aug 30, 2017 8:24 am

Re: Switching from 4MB to 4KB paging

Post by nullplan »

Thpertic wrote:
nullplan wrote:What bochs is saying here is, you tried to execute physical address 0xc000XXXX, which is usually an IO memory hole. So it complains about that.
Ok, but EIP is executing there, so what should I do?
Don't mix up physical and virtual addresses. EIP is set to 0xc000XXXX, which is a virtual address. The physical backing store is, I presume, at the 1MB line, right? So there is no issue. Just, you should never map the physical address 0xc000XXXX as executable and then run it. Unless the BIOS told you there is RAM there. And you loaded something there to be executed.
Carpe diem!
Thpertic
Member
Member
Posts: 56
Joined: Sun Sep 16, 2018 6:46 am

Re: Switching from 4MB to 4KB paging

Post by Thpertic »

Octocontrabass wrote:You can have a mixture of 4kB and 4MB pages. For each page directory entry, bit 7 determines whether the entry points to a single 4MB page or a page table containing 1024 4kB pages.
This will be useful, thank you!
nullplan wrote:Don't mix up physical and virtual addresses. EIP is set to 0xc000XXXX, which is a virtual address. The physical backing store is, I presume, at the 1MB line, right? So there is no issue. Just, you should never map the physical address 0xc000XXXX as executable and then run it. Unless the BIOS told you there is RAM there. And you loaded something there to be executed.
The kernel is executing there because I set it up in the higher-half but it's mapped to 1MB physical, I'm not sure what do you mean.
linguofreak
Member
Member
Posts: 510
Joined: Wed Mar 09, 2011 3:55 am

Re: Switching from 4MB to 4KB paging

Post by linguofreak »

Thpertic wrote:
nullplan wrote:Don't mix up physical and virtual addresses. EIP is set to 0xc000XXXX, which is a virtual address. The physical backing store is, I presume, at the 1MB line, right? So there is no issue. Just, you should never map the physical address 0xc000XXXX as executable and then run it. Unless the BIOS told you there is RAM there. And you loaded something there to be executed.
The kernel is executing there because I set it up in the higher-half but it's mapped to 1MB physical, I'm not sure what do you mean.
The error from Bochs indicates that your code tried reading or executing from 0xc000xxxx *physical*. Your higher half setup has your code at 1MB physical, which is mapped at 0xc000xxxx *virtual*.

So the mapping that you meant to have is:

Code: Select all

virtual          physical
0-1MB ---->   0-1MB
0xc0000xxxx ---> 1MB
But you also (or perhaps instead) have a mapping:

Code: Select all

virtual           physical
????????    ---> 0xc000xxxx 
There often isn't any RAM at physical 0xc000xxxx, either because the computer has less than 3 GiB of RAM, or because the motherboard chipset maps some bit of hardware there, so it isn't safe to use physical addresses that high until you've asked the firmware for a list of usable addresses.
Thpertic
Member
Member
Posts: 56
Joined: Sun Sep 16, 2018 6:46 am

Re: Switching from 4MB to 4KB paging

Post by Thpertic »

I think I may have fixed it, but it still triple faults. The kernel mapping starts from 0xC0000000 + 0x100000 using this macro to find the exact index:

Code: Select all

#define PAGE_TABLE_INDEX(x) (((x) >> 12) & 0x3ff)

Code: Select all

for (i = KERNEL_VIRTUAL_BASE + 0x100000, addr = 0x100000; (uint32_t)i <= _end_addr; i += PAGE_SIZE, addr += PAGE_SIZE)
        kernelPT[PAGE_TABLE_INDEX(i)] = addr | 0x000000003;
Then, I also changed something in the switch assembly, now it just invalidate the TLB and load the new page directory:

Code: Select all

pusha
; Invalidate the TLB
mov ecx, cr3
invlpg [ecx]

; Load PDBR (CR3), it must contains the physical address of the Page Directory
mov ecx, [_cur_pageDirectory]
mov cr3, ecx

popa
ret
The Bochs debugger doesn't complain anymore about the physical-to-virtual address mapping, but still reboots.
Can you help me?
nullplan
Member
Member
Posts: 1798
Joined: Wed Aug 30, 2017 8:24 am

Re: Switching from 4MB to 4KB paging

Post by nullplan »

Thpertic wrote:

Code: Select all

#define PAGE_TABLE_INDEX(x) (((x) >> 12) & 0x3ff)

Code: Select all

for (i = KERNEL_VIRTUAL_BASE + 0x100000, addr = 0x100000; (uint32_t)i <= _end_addr; i += PAGE_SIZE, addr += PAGE_SIZE)
        kernelPT[PAGE_TABLE_INDEX(i)] = addr | 0x000000003;
Looks about right.
Thpertic wrote:Then, I also changed something in the switch assembly, now it just invalidate the TLB and load the new page directory:

Code: Select all

pusha
; Invalidate the TLB
mov ecx, cr3
invlpg [ecx]

; Load PDBR (CR3), it must contains the physical address of the Page Directory
mov ecx, [_cur_pageDirectory]
mov cr3, ecx

popa
ret
What did I just say? Don't mix up physical and virtual addresses. CR3 contains a physical address. "invlpg" requires a virtual one.

This isn't necessary anyway, since loading CR3 already causes a TLB flush (except in global pages, but that is a feature you aren't using yet). The pusha and popa aren't necessary either if you'd just follow the ABI. In your case, you only clobber ecx, which is volatile, so you don't need to save it.

Also, CR3 contains the physical address of the page directory. And the page directory must contain the physical addresses of the page tables. So, what you need to do is:

Code: Select all

pageDirectory[0] = v2p(first4MBpt) | 3;
pageDirectory[PAGE_DIRECTORY_INDEX(KERNEL_VIRTUAL_BASE)] = v2p(kernelPT) | 0x000000003;
loadPageDir(v2p(pageDirectory));
And then think how you get from virtual to physical address. I don't know where your kmalloc() is allocating from. Maybe something like

Code: Select all

uintptr_t v2p(void* addr) {
  uintptr_t x = (uintptr_t)addr;
  if (x - KERNEL_VIRTUAL_BASE < (uintptr_t)_end_linmap - KERNEL_VIRTUAL_BASE)
    return x - KERNEL_VIRTUAL_BASE;
  uint32_t pde = _cur_pageDirectory[PAGE_DIRECTORY_INDEX(x)];
  assert(pde & 1);
  if (pde & 128)
    return (pde & 0xffc00000) + (x & 0x003fffff);
  uint32_t *pt = p2v(pde & 0xfffff000);
  assert(pt);
  uint32_t pte = pt[PAGE_TABLE_INDEX(x)];
  assert(pte & 1);
  return (pte & 0xfffff000) + (x & 0x00000fff);
}

void *p2v(uint32_t paddr) {
  if (paddr < _end_linmap - KERNEL_VIRTUAL_BASE)
    return (void*)(paddr + KERNEL_VIRTUAL_BASE);
  return 0; //XXX: Or panic instead? Are the page tables always mapped?
}
This depends on assertions. I suggest you panic if an assertion fails (stop executing, write a crashlog to console, stop all other cores).

This also depends on a new global variable, _end_linmap, which determines the current end of the linear mapping from 1MB to 3GB. Basically, this is where the cat chases its own tail: You need virtual and physical addresses of the page tables always to hand, and can't look it up in the page tables. If you only have a physical address, you need a simple way to find the corresponding virtual address.

This is usually accomplished by putting the paging structures inside the area that is linearly mapped. Then there is always a very simple relationship between physical and virtual address for the page tables. Alternatively, another linear area can be set aside. Problem is that it constrains the possible locations of the page tables in physical address space. Thus, you might run into the problem that you are out of space for such things, even if there'd be room for other stuff (linear mapping area is full and can't be expanded since either the physical or virtual memory following it is already in use).

On AMD64 you don't have this problem, since all of physical address space can be mapped at once.

Oh, and also:

Code: Select all

uint32_t loadPageDir(uint32_t); /*loads CR3 with argument and returns old CR3. */

Code: Select all

loadPageDir:
  mov ecx, [esp + 4]
  mov eax, cr3
  mov cr3, ecx
  ret
HTH.
Carpe diem!
Thpertic
Member
Member
Posts: 56
Joined: Sun Sep 16, 2018 6:46 am

Re: Switching from 4MB to 4KB paging

Post by Thpertic »

Thank you, this is helping!

I only have a few questions: since kmalloc already returns physical page-aligned addresses, can I skip the translation step, at least for v2p()? And can you please explain the use of _end_linmap further?
nullplan
Member
Member
Posts: 1798
Joined: Wed Aug 30, 2017 8:24 am

Re: Switching from 4MB to 4KB paging

Post by nullplan »

Wait, so kmalloc() returns a physical address? Then you need to map the page returned into virtual address space before you can access it. Physical addresses are no pointers, you cannot dereference them directly. So if you can take the return value of kmalloc() and write in it, then it is a virtual address. I generally try to keep them apart by passing physical addresses as "paddr_t", and virtual addresses as "void*". For x86, you could do:

Code: Select all

#ifdef CONFIG_PAE
typedef uint64_t paddr_t;
#else
typedef uint32_t paddr_t;
#endif
One more thing that is easier on AMD64: PAE is always enabled there.

Regarding _end_linmap: The idea is that you reserve "some" space behind the kernel for things where you have to tell the physical address from the virtual one quickly / without page tables. This is mostly just all the page tables. If you need a physical address for a device driver, those already have paging set up.

So, how much memory to reserve here? Well, IBM's recommendation for the PowerPC page table was to use 1/512 of the memory size, which is as good of an estimate as any. So you could do something like:

Code: Select all

uint32_t _end_linmap;
void init_linmap(void) {
  _end_linmap = ((size_t)_end_kernel + (count_ram_size() >> 9) + 4095) & -4096;

  /* now trim _end_linmap back if the physical memory area the kernel is in ends before the current _end_linmap */
  kmalloc_mark_used((uint32_t)_end_kernel - KERNEL_VIRTUAL_BASE, _end_linmap - KERNEL_VIRTUAL_BASE);
  for (uint32_t addr = (uint32_t)_end_kernel; addr < _end_linmap; addr += 4096)
    mmap((void*)addr, addr - KERNEL_VIRTUAL_BASE, PROT_READ | PROT_WRITE | PROT_KERNEL);
}
Memory for page tables is then exclusively allocated from that area. If you run out of space you can start swapping things out. You are going to have to save the virtual memory mappings of each process in your own data structure (usually a binary tree). So what you can do is, you can just leave the things you currently don't need out of the page tables. If a page fault happens, and the address was really mapped, throw out the least recently used page tables till you have the space to add this memory mapping. Which is actually easier on x86, since you definitely will have a page directory table, so you could only ever be missing a page table. So only a single page to free and then allocate again.
Carpe diem!
Thpertic
Member
Member
Posts: 56
Joined: Sun Sep 16, 2018 6:46 am

Re: Switching from 4MB to 4KB paging

Post by Thpertic »

Okay, so I have to map pages those pages.

Does kmalloc_mark_used() maps pages? Or you just map them through mmap()?
nullplan wrote:  /* now trim _end_linmap back if the physical memory area the kernel is in ends before the current _end_linmap */
  kmalloc_mark_used((uint32_t)_end_kernel - KERNEL_VIRTUAL_BASE, _end_linmap - KERNEL_VIRTUAL_BASE);
  for (uint32_t addr = (uint32_t)_end_kernel; addr < _end_linmap; addr += 4096)
    mmap((void*)addr, addr - KERNEL_VIRTUAL_BASE, PROT_READ | PROT_WRITE | PROT_KERNEL);
Would it be worse if I don't use _end_linmap at all and just map where kmalloc() returns with the proper function?
Post Reply