ARM:禁用 MMU 并更新 PC

ARM: Disabling MMU and updating PC

简而言之,出于调试目的,我想关闭 Linux 上下文(从内核内部)中的所有 MMU(和缓存)操作,只是为了 运行 一些测试。明确地说,我不打算在那之后我的系统仍然可以运行。

关于我的设置:我目前正在摆弄集成了 Cortex A5 的 Freescale Vybrid (VF610) 及其低功耗模式。由于我正在试验一些 可疑的本地 内存损坏,而芯片处于 "Low Power Stop" 模式并且我的 DDR3 处于自刷新状态,我试图一点一点地移动操作,现在执行所有 suspend/resume 步骤而不实际执行 WFI。因为在这条指令之前我 运行 有地址转换,之后没有(它本质上是一个重置),我想 "simulate" 通过 "manually" 关闭 MMU。

(我 目前 没有 JTAG 或任何其他调试访问我的芯片。我通过 MMC/TFTP/NFS 加载它,并用 LED 调试它。)

到目前为止我尝试过的:

    /* disable the Icache, Dcache and branch prediction */
    mrc     p15, 0, r6, c1, c0, 0
    ldr r7, =0x1804
    bic r6, r6, r7
    mcr     p15, 0, r6, c1, c0, 0
    isb

    /* disable the MMU and TEX */
    bic r7, r6, r7
    isb
    mcr p15, 0, r6, c1, c0, 0   @ turn on MMU, I-cache, etc
    mrc p15, 0, r6, c0, c0, 0   @ read id reg
    isb
    dsb
    dmb

以及具有相同效果的其他变体。

我观察到的:

在 MMU 块之前,我可以点亮一个 LED(3 个汇编指令,没有分支,没有什么花哨的,也没有访问我的 DDR,它已经处于自刷新状态 - GPIO 端口的虚拟地址存储在在那之前注册)。

在 MMU 块之后,我无法再尝试物理地址或虚拟地址。

我认为问题可能与我的电脑有关,它保留了一个过时的虚拟地址。查看内核其他地方的工作方式,但反过来(即启用翻译时):

    ldr r3, =cpu_resume_after_mmu

    instr_sync
    mcr p15, 0, r0, c1, c0, 0   @ turn on MMU, I-cache, etc
    mrc p15, 0, r0, c0, c0, 0   @ read id reg
    instr_sync

    mov r0, r0
    mov r0, r0
    ret r3          @ jump to virtual address
ENDPROC(cpu_resume_mmu)
    .popsection
cpu_resume_after_mmu:

(来自arch/arm/kernel/sleep.S, cpu_resume_mmu)

我想知道这 2 条指令的延迟与什么有关,以及记录在何处。我在这个问题上一无所获。我试过类似的东西,但没有成功:

    adr lr, BSYM(phys_block)

    /* disable the Icache, Dcache and branch prediction */
    mrc     p15, 0, r6, c1, c0, 0
    ldr r7, =0x1804
    bic r6, r6, r7
    mcr     p15, 0, r6, c1, c0, 0
    isb

    /* disable the MMU and TEX */
    bic r7, r6, r7
    isb
    mcr p15, 0, r6, c1, c0, 0   @ turn on MMU, I-cache, etc
    mrc p15, 0, r6, c0, c0, 0   @ read id reg
    isb
    dsb
    msb

    mov r0, r0
    mov r0, r0
    ret lr

phys_block:
    blue_light
    loop

感谢任何有线索或指点的人!

由于 Jacen 和 dwelch 都通过评论(各自)提供了我需要的答案,为了清楚起见,我将在这里回答我自己的问题:

诀窍是简单地添加身份映射 from/to 进行转换的页面,允许我们使用 "physical"(尽管实际上是虚拟的)PC 跳转到它,然后禁用 MMU。

这是最终代码(有点具体,但已注释):

    /* Duplicate mapping to here */

    mrc p15, 0, r4, c2, c0, 0 // Get TTRB0
    ldr r10, =0x00003fff
    bic r4, r10 // Extract page table physical base address
    orr r4, #0xc0000000 // Nastily "translate" it to the virtual one

    /*
     * Here r8 holds vf_suspend's physical address. I had no way of
     * doing this more "locally", since both physical and virtual
     * space for my code are runtime-allocated.
     */

    add lr, r8, #(phys_block-vf_suspend) // -> phys_block physical address 

    lsr r9, lr, #20 // SECTION_SHIFT     -> Page index
    add r7, r4, r9, lsl #2 // PMD_ORDER  -> Entry address
    ldr r10, =0x00000c0e // Flags
    orr r9, r10, r9, lsl #20 // SECTION_SHIFT   -> Entry value
    str r9, [r7] // Write entry

    ret lr  // Jump / transition to virtual addressing

phys_block:
    /* disable the MMU and TEX */
    isb
    mrc     p15, 0, r6, c1, c0, 0
    ldr r7, =0x10000001
    bic r6, r6, r7
    mcr p15, 0, r6, c1, c0, 0   @ turn on MMU, I-cache, etc
    mrc p15, 0, r6, c0, c0, 0   @ read id reg
    isb
    dsb
    dmb

    /* disable the Icache, Dcache and branch prediction */
    mrc     p15, 0, r6, c1, c0, 0
    ldr r7, =0x1804
    bic r6, r6, r7
    mcr     p15, 0, r6, c1, c0, 0
    isb

    // Done !

为了解决问题的 "what this 2-instruction delay is" 部分,与 /arch/arm 的大部分内容一样,它主要只是遗留下来的废话*.

早在任何类型的屏障指令之前的日子里,你必须考虑这样一个事实,即在你切换 MMU 时,管道包含在切换之前已经获取和解码的指令,所以有任何像如果地址 space 在它执行时已经改变,那么其中的分支或内存访问将出现可怕的错误。 ARMv4 Architecture Reference Manual 做出了精彩的陈述 "The correct code sequence for enabling and disabling the MMU is IMPLEMENTATION DEFINED" - 实际上,这主要意味着您知道您的管道有 3 个阶段长,因此插入两个 NOP 以安全地填充它。或者 took full advantage of the fact 做一些可怕的事情,比如安排直接跳转到翻译后的 VA,而不通过恒等映射(哎呀!)。

来自对旧微体系结构手册的有趣拖网,3 NOPs are needed for StrongARM (compared to 2 for the 3-stage ARM7 pipeline), and reading CP15 with a data dependency on the result is the recommended self-synchronising sequence for XScale,这解释了对主 ID 寄存器显然毫无意义的读取。

然而,在现代的东西(ARMv6 或更高版本)上,none 应该是必需的,因为你已经构建了障碍,所以你只需按下开关然后发出一个 isb 来刷新管道,这就是 instr_sync 宏在为此类架构构建时扩展到的内容。

* 或 Linux "works on everything" 方法的一个很好的例子,取决于您的观点...