Unable to access any addresses after enabling MMU in QEMU on arm64

364 Views Asked by At

I am trying to set up page tables in EL1. Eventually I want to configure different access rights for text, stack, data, rodata and enable MTE as well but for now I am stuck at simply enabling the MMU with a flatmap for all relevant memory. I have attached the files for a MWE.

I am using newlib's CRT0 that calls my _cpu_init_hook (in test.S) and then later main.

In _cpu_init_hook I disable all but one core (if need be), make sure we enter EL1, enable FP register accesses, setup interrupt vectors. This function returns to the CRT0 and later calls main that calls my setup_mmu() function in mmu.c.

For the page tables I use 64kB granules and two levels (2 and 3). Level 2 simply points to the one block of l3 entries that are generated by add_pgtbl_entries(). Currently, this is just called once for the whole memory range and produces 9 entries covering 0x40000000 to 0x4008FFFF. All pages are supposed to be RWX for now but nevertheless an exception is triggered immediately when the MMU is enabled by writing to sctlr_el1. Apparently the MMU is not able or allowed to fetch the next instruction (isb).

0x0000000040001db4 <+276>:  msr sctlr_el1, x1
0x0000000040001db8 <+280>:  isb

I have compiled QEMU with MMU debugging (DEBUG_TLB) enabled and it shows the same information gdb shows after the exception happened: It's a prefetch abort (ESR 0x21/0x86000006) at 0x40001db8.

I have played around with a few bits but due to the complexity of the architecture I am pretty sure I cannot not find the problem by accident this way... any ideas?

Sorry for the rather long MWE but I don't think the question can be answered without code...

test.S

.text

hang:
  wfi
  b hang
 
// Macro to define ISRs spaced-out 2^7=128 bytes
.macro isr label
  .align 7
  b \label
.endm

// Align to 2k
.align 11
.global  _vectors
_vectors:
isr sp0_sync
isr sp0_irq
isr sp0_fiq
isr sp0_serror
isr spx_sync
isr spx_irq
isr spx_fiq
isr spx_serror

// Actual ISRs
sp0_sync:
sp0_irq:
sp0_fiq:
sp0_serror:

spx_sync:
spx_irq:
spx_fiq:
spx_serror:
  b hang

stop_all_but_cpu0:
  str x30, [sp, -8]!
  mrs x1, mpidr_el1
  // check for uniprocessor system
  ubfx x2, x1, 30, 1
  cbnz x2, done
  // check affinity level 0 for core ID, let only #0 continue
  ands x2, x1, 0xff
  cbnz x2, hang
done:
  ldr x30, [sp], 8
  ret

enter_el1:
  // Use a shared stack for all ELs
  mov x0, sp
  msr sp_el0, x0
  msr spsel, xzr

  // Determine current EL and act accordingly
  mrs x0, CurrentEL
  cmp x0, #2<<2  // the actual EL is stored in bits [2:3]
  bhi hang
  beq hang
  ret

// This function is called by newlib's crt0.
// x0 contains the stack base (quad-aligned)
.global _cpu_init_hook
.type _cpu_init_hook, "function"
_cpu_init_hook:
  str x30, [sp, #-8]!
  mov x19, x0 // save stack base for later

  # make sure only cpu0 continues beyond here
  bl stop_all_but_cpu0

  # make sure we are in el1
  bl enter_el1

  # disable trapping accessing FP registers in EL1 and EL0
  mov x0, #(0x3 << 20)
  msr cpacr_el1, x0

  # set the interrupt vector base addresses to our ISRs
  adr x0, _vectors
  msr vbar_el1, x0

  # synchronize pipeline
  isb

  mov x0, x19
#   bl setup_mmu

  ldr x30, [sp], 8
  ret

.global main
main:
  mov x1, x0
  bl setup_mmu
  b main

.global _exit
_exit:
  b _exit

.bss
.align 17
.global _pgtbl_start
_pgtbl_start:
  .space 2*64*1024

mmu.c

#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

uint64_t *add_pgtbl_entries(uint64_t *tbl_base, uintptr_t start, uintptr_t end, uint64_t attrs, int granule) {
  for (uintptr_t addr = start; addr < end; addr+=granule, tbl_base++) {
    *tbl_base = addr | attrs;
    printf("tbl_base (%p) = 0x%lx = 0x%lx | 0x%lx \n", tbl_base, *tbl_base, addr, attrs);
  }
  return tbl_base;
}

void setup_mmu(uintptr_t sp) {
  extern uint64_t _pgtbl_start;
  uint64_t *pgtbl_start = &_pgtbl_start;
  size_t tbl_size = 64*1024;
  size_t tbl_size_words = tbl_size/8;
  memset(pgtbl_start, 0, 2*tbl_size); // Zero out two tables
  pgtbl_start[0] = (uintptr_t)(pgtbl_start+tbl_size_words) | 0x3; // lvl2 entry: point to (final) lvl3 table

  printf("pgtbl_start[0] (%p)=0x%lx\n", &pgtbl_start[0], pgtbl_start[0]);
  /* Attribute bits:
   *  - [59]: PXNTable: 
   *  - [54]: UXN: unpriv execute never
   *  - [53]: PXN: priv execute never
   *  - [52]: contiguous
   *  - [50]: guarded page (BTI)
   *  - [10]: access flag, if 0 then accesses to respective addresses trap
   *  - [9:8]: shareable: 00 non, 10 outer, 11 inner
   *  - [7:6]: access permissions: 00 r/w EL1+, 01 r/w, 10 r/o EL1+, 11 r/o
   *  - [5]: NS: non-secure bit
   *  - [4:2]: AttrIndx into MAIR_ELn
   *  - [1]: is page, (RES1 in lvl3)
   *  - [0]: valid
   */
  //  !PXN, inner, r/w EL1+, non-secure, MAIR[0]
  uint64_t rwx_page = (0UL<<53)|(1<<10)|(3<<8)|(0<<6)|(1<<5)|(0<<2)|(3<<0);

  uintptr_t init_start = 0x40000000;
  extern uintptr_t _end;

  uint64_t *cur_off = pgtbl_start+tbl_size_words;
  cur_off = add_pgtbl_entries(cur_off, init_start, (uintptr_t)&_end, rwx_page, 64*1024);

  uint64_t mair = 0xFF;
  uint64_t tcr = 0x807520; // IPS=3b/4GB, EPD1=1, TG0=1, SH0=3, ORGN0=1, IRGN0=1, T0SZ=64k
  // uint64_t tcr = 0x007520; // IPS=3b/4GB, EPD1=0, TG0=1, SH0=3, ORGN0=1, IRGN0=1, T0SZ=64k
  // uint64_t tcr = 0x200807518; // IPS=40b/1TB, EPD1=1, TG0=1, SH0=3, ORGN0=1, IRGN0=1, T0SZ=64k
  __asm__ volatile (
    "msr ttbr0_el1, %[pgtbl_start] \n\t"
    "msr mair_el1, %[mair] \n\t"
    "msr tcr_el1, %[tcr] \n\t"
    "isb \n\t"
    "dsb sy \n\t"
    : // Outputs
    : // Inputs
      [pgtbl_start]"r"(pgtbl_start),
      [mair]"r"(mair),
      [tcr]"r"(tcr)
    : // Clobbers
  );

  // flush TLB, enable MMU
  uint64_t sctlr = 0x1005; // I, C, !A, MMU
  // uint64_t sctlr = 0x1; // MMU
  __asm__ volatile (
    "tlbi vmalle1 \n\t"
    "dsb sy \n\t"
    "isb \n\t"
    "msr sctlr_el1, %[sctlr] \n\t"
    "isb \n\t"
    "at s1e1r, %[init_start] \n\t"
    "mrs %[init_start], PAR_EL1 \n\t"
    : // Outputs
      [init_start]"+r"(init_start)
    : // Inputs
      [sctlr]"r"(sctlr)
    : // Clobbers
  );
}

Makefile

    gdb: test
        aarch64-none-elf-gdb "$^" \
            -ex 'target extended-remote | \
                qemu-system-aarch64 -nographic -monitor none -serial none -semihosting-config enable=on,target=gdb -gdb stdio -S \
                    -machine virt -cpu max \
                    -D qemu.log \
                    -d mmu,int \
                    -kernel $^' \
            -ex 'br exit' \
            -ex 'br hang' \
            ;

    test: test.S mmu.c
        aarch64-none-elf-gcc -Wl,-Map=test.map -ggdb3 -Wl,--section-start=.init=0x40000000 -specs=rdimon.specs  $^ -o $@
1

There are 1 best solutions below

0
Siguza On

Your level 2 translation table entry is wrong.

One L2 entry spans 0x2000 L3 entries à 64KiB each, giving you 512MiB, or 0x20000000 bytes per L2 entry.

0x20000000 is less than 0x40000000, so everything you're currently mapping is between addresses 0x0 and 0x20000000.

Put your L2 entry in pgtbl_start[2], not pgtbl_start[0].