From 882a5a0dc06add8f91b747e8032e044708a32318 Mon Sep 17 00:00:00 2001 From: Paul Mackerras Date: Thu, 23 Apr 2020 15:33:36 +1000 Subject: [PATCH] tests: Add a test for the MMU radix page table walks This adds tests to check that the MMU and dTLB are translating addresses and checking permissions correctly. We use a simple 2-level radix tree. The radix tree maps 2GB of address space and has a 1024-entry page directory pointing to 512-entry page table pages. Signed-off-by: Paul Mackerras --- tests/mmu/Makefile | 3 + tests/mmu/head.S | 112 +++++++++ tests/mmu/mmu.c | 468 +++++++++++++++++++++++++++++++++++++ tests/mmu/powerpc.lds | 13 ++ tests/test_mmu.bin | Bin 0 -> 12304 bytes tests/test_mmu.console_out | 10 + tests/update_console_tests | 2 +- 7 files changed, 607 insertions(+), 1 deletion(-) create mode 100644 tests/mmu/Makefile create mode 100644 tests/mmu/head.S create mode 100644 tests/mmu/mmu.c create mode 100644 tests/mmu/powerpc.lds create mode 100755 tests/test_mmu.bin create mode 100644 tests/test_mmu.console_out diff --git a/tests/mmu/Makefile b/tests/mmu/Makefile new file mode 100644 index 0000000..84f7ff2 --- /dev/null +++ b/tests/mmu/Makefile @@ -0,0 +1,3 @@ +TEST=mmu + +include ../Makefile.test diff --git a/tests/mmu/head.S b/tests/mmu/head.S new file mode 100644 index 0000000..3627cff --- /dev/null +++ b/tests/mmu/head.S @@ -0,0 +1,112 @@ +/* Copyright 2013-2014 IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define STACK_TOP 0x4000 + +/* Load an immediate 64-bit value into a register */ +#define LOAD_IMM64(r, e) \ + lis r,(e)@highest; \ + ori r,r,(e)@higher; \ + rldicr r,r, 32, 31; \ + oris r,r, (e)@h; \ + ori r,r, (e)@l; + + .section ".head","ax" + + /* + * Microwatt currently enters in LE mode at 0x0, so we don't need to + * do any endian fix ups + */ + . = 0 +.global _start +_start: + b boot_entry + +.global boot_entry +boot_entry: + /* setup stack */ + LOAD_IMM64(%r1, STACK_TOP - 0x100) + LOAD_IMM64(%r12, main) + mtctr %r12 + bctrl + attn // terminate on exit + b . + + /* Read a location with translation on */ + .globl test_read +test_read: + mfmsr %r9 + ori %r8,%r9,0x10 /* set MSR_DR */ + mtmsrd %r8,0 + mr %r6,%r3 + li %r3,0 + ld %r5,0(%r6) + li %r3,1 + /* land here if DSI occurred */ + mtmsrd %r9,0 + std %r5,0(%r4) + blr + + /* Write a location with translation on */ + .globl test_write +test_write: + mfmsr %r9 + ori %r8,%r9,0x10 /* set MSR_DR */ + mtmsrd %r8,0 + mr %r6,%r3 + li %r3,0 + std %r4,0(%r6) + li %r3,1 + /* land here if DSI occurred */ + mtmsrd %r9,0 + blr + +#define EXCEPTION(nr) \ + .= nr ;\ + attn + + /* DSI vector - skip the failing instruction + the next one */ + . = 0x300 + mtsprg0 %r10 + mfsrr0 %r10 + addi %r10,%r10,8 + mtsrr0 %r10 + rfid + + /* More exception stubs */ + EXCEPTION(0x380) + EXCEPTION(0x400) + EXCEPTION(0x480) + EXCEPTION(0x500) + EXCEPTION(0x600) + EXCEPTION(0x700) + EXCEPTION(0x800) + EXCEPTION(0x900) + EXCEPTION(0x980) + EXCEPTION(0xa00) + EXCEPTION(0xb00) + EXCEPTION(0xc00) + EXCEPTION(0xd00) + EXCEPTION(0xe00) + EXCEPTION(0xe20) + EXCEPTION(0xe40) + EXCEPTION(0xe60) + EXCEPTION(0xe80) + EXCEPTION(0xf00) + EXCEPTION(0xf20) + EXCEPTION(0xf40) + EXCEPTION(0xf60) + EXCEPTION(0xf80) diff --git a/tests/mmu/mmu.c b/tests/mmu/mmu.c new file mode 100644 index 0000000..0a717c7 --- /dev/null +++ b/tests/mmu/mmu.c @@ -0,0 +1,468 @@ +#include +#include +#include + +#include "console.h" + +extern int test_read(long *addr, long *ret, long init); +extern int test_write(long *addr, long val); + +static inline void do_tlbie(unsigned long rb, unsigned long rs) +{ + __asm__ volatile("tlbie %0,%1" : : "r" (rb), "r" (rs) : "memory"); +} + +static inline unsigned long mfspr(int sprnum) +{ + long val; + + __asm__ volatile("mfspr %0,%1" : "=r" (val) : "i" (sprnum)); + return val; +} + +static inline void mtspr(int sprnum, unsigned long val) +{ + __asm__ volatile("mtspr %0,%1" : : "i" (sprnum), "r" (val)); +} + +static inline void store_pte(unsigned long *p, unsigned long pte) +{ + __asm__ volatile("stdbrx %1,0,%0" : : "r" (p), "r" (pte) : "memory"); +} + +void print_string(const char *str) +{ + for (; *str; ++str) + putchar(*str); +} + +void print_hex(unsigned long val) +{ + int i, x; + + for (i = 60; i >= 0; i -= 4) { + x = (val >> i) & 0xf; + if (x >= 10) + putchar(x + 'a' - 10); + else + putchar(x + '0'); + } +} + +// i < 100 +void print_test_number(int i) +{ + print_string("test "); + putchar(48 + i/10); + putchar(48 + i%10); + putchar(':'); +} + +#define CACHE_LINE_SIZE 64 + +void zero_memory(void *ptr, unsigned long nbytes) +{ + unsigned long nb, i, nl; + void *p; + + for (; nbytes != 0; nbytes -= nb, ptr += nb) { + nb = -((unsigned long)ptr) & (CACHE_LINE_SIZE - 1); + if (nb == 0 && nbytes >= CACHE_LINE_SIZE) { + nl = nbytes / CACHE_LINE_SIZE; + p = ptr; + for (i = 0; i < nl; ++i) { + __asm__ volatile("dcbz 0,%0" : : "r" (p) : "memory"); + p += CACHE_LINE_SIZE; + } + nb = nl * CACHE_LINE_SIZE; + } else { + if (nb > nbytes) + nb = nbytes; + for (i = 0; i < nb; ++i) + ((unsigned char *)ptr)[i] = 0; + } + } +} + +#define PERM_EX 0x001 +#define PERM_WR 0x002 +#define PERM_RD 0x004 +#define PERM_PRIV 0x008 +#define ATTR_NC 0x020 +#define CHG 0x080 +#define REF 0x100 + +#define DFLT_PERM (PERM_WR | PERM_RD | REF | CHG) + +/* + * Set up an MMU translation tree using memory starting at the 64k point. + * We use 2 levels, mapping 2GB (the minimum size possible), with a + * 8kB PGD level pointing to 4kB PTE pages. + */ +unsigned long *pgdir = (unsigned long *) 0x10000; +unsigned long free_ptr = 0x12000; +void *eas_mapped[4]; +int neas_mapped; + +void init_mmu(void) +{ + zero_memory(pgdir, 1024 * sizeof(unsigned long)); + /* RTS = 0 (2GB address space), RPDS = 10 (1024-entry top level) */ + mtspr(720, (unsigned long) pgdir | 10); + do_tlbie(0xc00, 0); /* invalidate all TLB entries */ +} + +static unsigned long *read_pgd(unsigned long i) +{ + unsigned long ret; + + __asm__ volatile("ldbrx %0,%1,%2" : "=r" (ret) : "b" (pgdir), + "r" (i * sizeof(unsigned long))); + return (unsigned long *) (ret & 0x00ffffffffffff00); +} + +void map(void *ea, void *pa, unsigned long perm_attr) +{ + unsigned long epn = (unsigned long) ea >> 12; + unsigned long i, j; + unsigned long *ptep; + + i = (epn >> 9) & 0x3ff; + j = epn & 0x1ff; + if (pgdir[i] == 0) { + zero_memory((void *)free_ptr, 512 * sizeof(unsigned long)); + store_pte(&pgdir[i], 0x8000000000000000 | free_ptr | 9); + free_ptr += 512 * sizeof(unsigned long); + } + ptep = read_pgd(i); + store_pte(&ptep[j], 0xc000000000000000 | ((unsigned long)pa & 0x00fffffffffff000) | perm_attr); + eas_mapped[neas_mapped++] = ea; +} + +void unmap(void *ea) +{ + unsigned long epn = (unsigned long) ea >> 12; + unsigned long i, j; + unsigned long *ptep; + + i = (epn >> 9) & 0x3ff; + j = epn & 0x1ff; + if (pgdir[i] == 0) + return; + ptep = read_pgd(i); + ptep[j] = 0; + do_tlbie(((unsigned long)ea & ~0xfff), 0); +} + +void unmap_all(void) +{ + int i; + + for (i = 0; i < neas_mapped; ++i) + unmap(eas_mapped[i]); + neas_mapped = 0; +} + +int mmu_test_1(void) +{ + long *ptr = (long *) 0x123000; + long val; + + /* this should fail */ + if (test_read(ptr, &val, 0xdeadbeefd00d)) + return 1; + /* dest reg of load should be unchanged */ + if (val != 0xdeadbeefd00d) + return 2; + /* DAR and DSISR should be set correctly */ + if (mfspr(19) != (long) ptr || mfspr(18) != 0x40000000) + return 3; + return 0; +} + +int mmu_test_2(void) +{ + long *mem = (long *) 0x4000; + long *ptr = (long *) 0x124000; + long *ptr2 = (long *) 0x1124000; + long val; + + /* create PTE */ + map(ptr, mem, DFLT_PERM); + /* initialize the memory content */ + mem[33] = 0xbadc0ffee; + /* this should succeed and be a cache miss */ + if (!test_read(&ptr[33], &val, 0xdeadbeefd00d)) + return 1; + /* dest reg of load should have the value written */ + if (val != 0xbadc0ffee) + return 2; + /* load a second TLB entry in the same set as the first */ + map(ptr2, mem, DFLT_PERM); + /* this should succeed and be a cache hit */ + if (!test_read(&ptr2[33], &val, 0xdeadbeefd00d)) + return 3; + /* dest reg of load should have the value written */ + if (val != 0xbadc0ffee) + return 4; + /* check that the first entry still works */ + if (!test_read(&ptr[33], &val, 0xdeadbeefd00d)) + return 5; + if (val != 0xbadc0ffee) + return 6; + return 0; +} + +int mmu_test_3(void) +{ + long *mem = (long *) 0x5000; + long *ptr = (long *) 0x149000; + long val; + + /* create PTE */ + map(ptr, mem, DFLT_PERM); + /* initialize the memory content */ + mem[45] = 0xfee1800d4ea; + /* this should succeed and be a cache miss */ + if (!test_read(&ptr[45], &val, 0xdeadbeefd0d0)) + return 1; + /* dest reg of load should have the value written */ + if (val != 0xfee1800d4ea) + return 2; + /* remove the PTE */ + unmap(ptr); + /* this should fail */ + if (test_read(&ptr[45], &val, 0xdeadbeefd0d0)) + return 3; + /* dest reg of load should be unchanged */ + if (val != 0xdeadbeefd0d0) + return 4; + /* DAR and DSISR should be set correctly */ + if (mfspr(19) != (long) &ptr[45] || mfspr(18) != 0x40000000) + return 5; + return 0; +} + +int mmu_test_4(void) +{ + long *mem = (long *) 0x6000; + long *ptr = (long *) 0x10a000; + long *ptr2 = (long *) 0x110a000; + long val; + + /* create PTE */ + map(ptr, mem, DFLT_PERM); + /* initialize the memory content */ + mem[27] = 0xf00f00f00f00; + /* this should succeed and be a cache miss */ + if (!test_write(&ptr[27], 0xe44badc0ffee)) + return 1; + /* memory should now have the value written */ + if (mem[27] != 0xe44badc0ffee) + return 2; + /* load a second TLB entry in the same set as the first */ + map(ptr2, mem, DFLT_PERM); + /* this should succeed and be a cache hit */ + if (!test_write(&ptr2[27], 0x6e11ae)) + return 3; + /* memory should have the value written */ + if (mem[27] != 0x6e11ae) + return 4; + /* check that the first entry still exists */ + /* (assumes TLB is 2-way associative or more) */ + if (!test_read(&ptr[27], &val, 0xdeadbeefd00d)) + return 5; + if (val != 0x6e11ae) + return 6; + return 0; +} + +int mmu_test_5(void) +{ + long *mem = (long *) 0x7ffd; + long *ptr = (long *) 0x39fffd; + long val; + + /* create PTE */ + map(ptr, mem, DFLT_PERM); + /* this should fail */ + if (test_read(ptr, &val, 0xdeadbeef0dd0)) + return 1; + /* dest reg of load should be unchanged */ + if (val != 0xdeadbeef0dd0) + return 2; + /* DAR and DSISR should be set correctly */ + if (mfspr(19) != ((long)ptr & ~0xfff) + 0x1000 || mfspr(18) != 0x40000000) + return 3; + return 0; +} + +int mmu_test_6(void) +{ + long *mem = (long *) 0x7ffd; + long *ptr = (long *) 0x39fffd; + + /* create PTE */ + map(ptr, mem, DFLT_PERM); + /* initialize memory */ + *mem = 0x123456789abcdef0; + /* this should fail */ + if (test_write(ptr, 0xdeadbeef0dd0)) + return 1; + /* DAR and DSISR should be set correctly */ + if (mfspr(19) != ((long)ptr & ~0xfff) + 0x1000 || mfspr(18) != 0x42000000) + return 2; + return 0; +} + +int mmu_test_7(void) +{ + long *mem = (long *) 0x4000; + long *ptr = (long *) 0x124000; + long val; + + *mem = 0x123456789abcdef0; + /* create PTE without R or C */ + map(ptr, mem, PERM_RD | PERM_WR); + /* this should fail */ + if (test_read(ptr, &val, 0xdeadd00dbeef)) + return 1; + /* dest reg of load should be unchanged */ + if (val != 0xdeadd00dbeef) + return 2; + /* DAR and DSISR should be set correctly */ + if (mfspr(19) != (long) ptr || mfspr(18) != 0x00040000) + return 3; + /* this should fail */ + if (test_write(ptr, 0xdeadbeef0dd0)) + return 4; + /* DAR and DSISR should be set correctly */ + if (mfspr(19) != (long)ptr || mfspr(18) != 0x02040000) + return 5; + /* memory should be unchanged */ + if (*mem != 0x123456789abcdef0) + return 6; + return 0; +} + +int mmu_test_8(void) +{ + long *mem = (long *) 0x4000; + long *ptr = (long *) 0x124000; + long val; + + *mem = 0x123456789abcdef0; + /* create PTE with R but not C */ + map(ptr, mem, REF | PERM_RD | PERM_WR); + /* this should succeed */ + if (!test_read(ptr, &val, 0xdeadd00dbeef)) + return 1; + /* this should fail */ + if (test_write(ptr, 0xdeadbeef0dd1)) + return 2; + /* DAR and DSISR should be set correctly */ + if (mfspr(19) != (long)ptr || mfspr(18) != 0x02040000) + return 3; + /* memory should be unchanged */ + if (*mem != 0x123456789abcdef0) + return 4; + return 0; +} + +int mmu_test_9(void) +{ + long *mem = (long *) 0x4000; + long *ptr = (long *) 0x124000; + long val; + + *mem = 0x123456789abcdef0; + /* create PTE without read or write permission */ + map(ptr, mem, REF); + /* this should fail */ + if (test_read(ptr, &val, 0xdeadd00dbeef)) + return 1; + /* dest reg of load should be unchanged */ + if (val != 0xdeadd00dbeef) + return 2; + /* DAR and DSISR should be set correctly */ + if (mfspr(19) != (long) ptr || mfspr(18) != 0x08000000) + return 3; + /* this should fail */ + if (test_write(ptr, 0xdeadbeef0dd1)) + return 4; + /* DAR and DSISR should be set correctly */ + if (mfspr(19) != (long)ptr || mfspr(18) != 0x0a000000) + return 5; + /* memory should be unchanged */ + if (*mem != 0x123456789abcdef0) + return 6; + return 0; +} + +int mmu_test_10(void) +{ + long *mem = (long *) 0x4000; + long *ptr = (long *) 0x124000; + long val; + + *mem = 0x123456789abcdef0; + /* create PTE with read but not write permission */ + map(ptr, mem, REF | PERM_RD); + /* this should succeed */ + if (!test_read(ptr, &val, 0xdeadd00dbeef)) + return 1; + /* this should fail */ + if (test_write(ptr, 0xdeadbeef0dd1)) + return 2; + /* DAR and DSISR should be set correctly */ + if (mfspr(19) != (long)ptr || mfspr(18) != 0x0a000000) + return 3; + /* memory should be unchanged */ + if (*mem != 0x123456789abcdef0) + return 4; + return 0; +} + +int fail = 0; + +void do_test(int num, int (*test)(void)) +{ + int ret; + + mtspr(18, 0); + mtspr(19, 0); + unmap_all(); + print_test_number(num); + ret = test(); + if (ret == 0) { + print_string("PASS\r\n"); + } else { + fail = 1; + print_string("FAIL "); + putchar(ret + '0'); + print_string(" DAR="); + print_hex(mfspr(19)); + print_string(" DSISR="); + print_hex(mfspr(18)); + print_string("\r\n"); + } +} + +int main(void) +{ + potato_uart_init(); + init_mmu(); + + do_test(1, mmu_test_1); + do_test(2, mmu_test_2); + do_test(3, mmu_test_3); + do_test(4, mmu_test_4); + do_test(5, mmu_test_5); + do_test(6, mmu_test_6); + do_test(7, mmu_test_7); + do_test(8, mmu_test_8); + do_test(9, mmu_test_9); + do_test(10, mmu_test_10); + + return fail; +} diff --git a/tests/mmu/powerpc.lds b/tests/mmu/powerpc.lds new file mode 100644 index 0000000..c4bff13 --- /dev/null +++ b/tests/mmu/powerpc.lds @@ -0,0 +1,13 @@ +SECTIONS +{ + _start = .; + . = 0; + .head : { + KEEP(*(.head)) + } + . = 0x1000; + .text : { *(.text) } + . = 0x3000; + .data : { *(.data) } + .bss : { *(.bss) } +} diff --git a/tests/test_mmu.bin b/tests/test_mmu.bin new file mode 100755 index 0000000000000000000000000000000000000000..961e2df8a24786b52775933f1f9573d2beea9c7e GIT binary patch literal 12304 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