kernel/head.S – v7_flush_dcache_louis:

  • compressed/head.S 에서 사용하였던 __armv7_mmu_cache_flush: 루틴과 거의 동일하다.
  • 데이터 캐시를 지우라고 요청하면 SoC 정보를 확인하여 지원하는 최종 캐시레벨을 확인한 후 L1 부터 해당 캐시 레벨까지 flush한다.
  • 해당 캐시 레벨에서는 index와 way를 사용하여 하나씩 삭제한다.

kernel_head.s분석12a

hierarchical cache

  • ARMv6 아키텍처까지는 ARM 아키텍처에서 L1 캐시만 지원하였었다.
  • ARMv7 아키텍처부터 다단계의 캐시를 지원하게되었다.
  • 다단계 캐시, ARM에서는 hierarchical cache 구조 라고 한다.

 

v7_flush_dcache_louis()

arch/arm/mm/cache-v7.S

 /*
 *     v7_flush_dcache_louis()
 *
 *     Flush the D-cache up to the Level of Unification Inner Shareable
 *
 *     Corrupted registers: r0-r7, r9-r11 (r6 only in Thumb mode)
 */

ENTRY(v7_flush_dcache_louis)
        dmb                                     @ ensure ordering with previous memory accesses
        mrc     p15, 1, r0, c0, c0, 1           @ read clidr, r0 = clidr
        ALT_SMP(ands    r3, r0, #(7 << 21))     @ extract LoUIS from clidr
        ALT_UP(ands     r3, r0, #(7 << 27))     @ extract LoUU from clidr
#ifdef CONFIG_ARM_ERRATA_643719
        ALT_SMP(mrceq   p15, 0, r2, c0, c0, 0)  @ read main ID register
        ALT_UP(reteq    lr)                     @ LoUU is zero, so nothing to do
        ldreq   r1, =0x410fc090                 @ ID of ARM Cortex A9 r0p?
        biceq   r2, r2, #0x0000000f             @ clear minor revision number
        teqeq   r2, r1                          @ test for errata affected core and if so...
        orreqs  r3, #(1 << 21)                  @   fix LoUIS value (and set flags state to 'ne')
#endif
        ALT_SMP(mov     r3, r3, lsr #20)        @ r3 = LoUIS * 2
        ALT_UP(mov      r3, r3, lsr #26)        @ r3 = LoUU * 2
        reteq   lr                              @ return if level == 0
        mov     r10, #0                         @ r10 (starting level) = 0
        b       flush_levels                    @ start flushing cache levels
ENDPROC(v7_flush_dcache_louis)
  • mrc     p15, 1, r0, c0, c0, 1
    • LoUU/LoUIS를 추출하기 위해 CLIDR을 읽어온다.
  • ALT_SMP(ands    r3, r0, #(7 << 21))
    • SMP 시스템에서 CLIDR의 LoUIS 필드를 추출해온다.
  • ERRATA_643719
    • 특정 프로세서의 CLIDR.LOUIS가 잘못 기록이 되어 있어서 이를 보정해주는 코드
    • Cortex-A9 r1p0 이전 버전에서 LoUIS 값이 1이 아닌 0으로 기록된 것을 잡아준다.
  • ALT_SMP(mov     r3, r3, lsr #20)
    • r3: 읽어온 값을 우측으로 쉬프트하여 LoUIS  x 2와 같은 값으로 만든다.
      • d-cache를 어느 캐시 레벨까지 flush할지 결정하기 위함.
  • reteq   lr
    • 읽어온 LoUIS가 0이면 d-cache의 flush를 포기하고 루틴을 빠져나간다.
  • mov     r10, #0
    • 시작 캐시 레벨을 0(L1)부터 준비한다.
  • b flush_levels
    • v7_flush_dcache_all() 루틴 중간에 있는 flush_levels 레이블을 같이 사용한다.

 

v7_flush_dcache_all()

/*
 *      v7_flush_dcache_all()
 *
 *      Flush the whole D-cache.
 *
 *      Corrupted registers: r0-r7, r9-r11 (r6 only in Thumb mode)
 *
 *      - mm    - mm_struct describing address space
 */
ENTRY(v7_flush_dcache_all)
        dmb                                     @ ensure ordering with previous memory accesses
        mrc     p15, 1, r0, c0, c0, 1           @ read clidr
        ands    r3, r0, #0x7000000              @ extract loc from clidr
        mov     r3, r3, lsr #23                 @ left align loc bit field
        beq     finished                        @ if loc is 0, then no need to clean
        mov     r10, #0                         @ start clean at cache level 0
flush_levels:
        add     r2, r10, r10, lsr #1            @ work out 3x current cache level
        mov     r1, r0, lsr r2                  @ extract cache type bits from clidr
        and     r1, r1, #7                      @ mask of the bits for current cache only
        cmp     r1, #2                          @ see what cache we have at this level
        blt     skip                            @ skip if no cache, or just i-cache
#ifdef CONFIG_PREEMPT
        save_and_disable_irqs_notrace r9        @ make cssr&csidr read atomic
#endif
        mcr     p15, 2, r10, c0, c0, 0          @ select current cache level in cssr
        isb                                     @ isb to sych the new cssr&csidr
        mrc     p15, 1, r1, c0, c0, 0           @ read the new csidr
#ifdef CONFIG_PREEMPT
        restore_irqs_notrace r9
#endif
        and     r2, r1, #7                      @ extract the length of the cache lines
        add     r2, r2, #4                      @ add 4 (line length offset)
        ldr     r4, =0x3ff
        ands    r4, r4, r1, lsr #3              @ find maximum number on the way size
        clz     r5, r4                          @ find bit position of way size increment
        ldr     r7, =0x7fff
        ands    r7, r7, r1, lsr #13             @ extract max number of the index size
loop1:
        mov     r9, r7                          @ create working copy of max index
loop2:
 ARM(   orr     r11, r10, r4, lsl r5    )       @ factor way and cache number into r11
 THUMB( lsl     r6, r4, r5              )
 THUMB( orr     r11, r10, r6            )       @ factor way and cache number into r11
 ARM(   orr     r11, r11, r9, lsl r2    )       @ factor index number into r11
 THUMB( lsl     r6, r9, r2              )
 THUMB( orr     r11, r11, r6            )       @ factor index number into r11
        mcr     p15, 0, r11, c7, c14, 2         @ clean & invalidate by set/way
        subs    r9, r9, #1                      @ decrement the index
        bge     loop2
        subs    r4, r4, #1                      @ decrement the way
        bge     loop1
skip:
        add     r10, r10, #2                    @ increment cache number
        cmp     r3, r10
        bgt     flush_levels
finished:
        mov     r10, #0                         @ swith back to cache level 0
        mcr     p15, 2, r10, c0, c0, 0          @ select current cache level in cssr
        dsb     st
        isb
        ret     lr
ENDPROC(v7_flush_dcache_all)
  • decompressed/head.S에서 d-cache를 flush한 로직과 거의 흡사하다.
    • way와 index 루프 순서만 기존과 바뀌었다.

kernel/head.S – __v7_ca7mp_setup:

  1. SMP 시스템에서 캐시 coherent가 disable되어 있으면 enable시킨다.
  2. d-cache를 모두 flush(invalidate + clean)한다.
  3. 현재 프로세스와 관련 있는 ERRATA(프로세스 에러 패치)를 적용한다.
  4. i-cache를 모두 invalidate한다.
  5. TLB cache를 모두 invalidate한다.
  6. TTBCR을 0으로 설정한다. (페이지테이블을 나누어 사용하지 않는다)
  7. TTBR1에 TTB_RGN_WBWA | TTB_S를 설정한다.
  8. PRRR에 =PRRR 값을 설정한다.
  9. NMRR에 =NMRR 값을 설정한다.

kernel_head.s분석11

__v7_ca7mp_setup:

/*
 *      __v7_setup
 *
 *      Initialise TLB, Caches, and MMU state ready to switch the MMU
 *      on.  Return in r0 the new CP15 C1 control register setting.
 *
 *      This should be able to cover all ARMv7 cores.
 *
 *      It is assumed that:
 *      - cache type register is implemented
 */
__v7_ca7mp_setup:
        mov     r10, #0
#ifdef CONFIG_SMP
        ALT_SMP(mrc     p15, 0, r0, c1, c0, 1)
        ALT_UP(mov      r0, #(1 << 6))          @ fake it for UP
        tst     r0, #(1 << 6)                   @ SMP/nAMP mode enabled?
        orreq   r0, r0, #(1 << 6)               @ Enable SMP/nAMP mode
        orreq   r0, r0, r10                     @ Enable CPU-specific SMP bits
        mcreq   p15, 0, r0, c1, c0, 1
#endif
        b       __v7_setup
  • ACTLR.SMP를 읽어 0으로 되어 있으면 1로 다시 바꾼다.
    • Enables coherent requests to the processor: 0=disable, 1=enable

 

__v7_setup:

__v7_setup:
        adr     r12, __v7_setup_stack           @ the local stack
        stmia   r12, {r0-r5, r7, r9, r11, lr}
        bl      v7_flush_dcache_louis
        ldmia   r12, {r0-r5, r7, r9, r11, lr}
  • adr     r12, __v7_setup_stack
    • r12=임시 스택용도로 사용할 공간 주소
  • stmia   r12, {r0-r5, r7, r9, r11, lr}
    • 레지스터들을 잠시 백업한다.
  • bl      v7_flush_dcache_louis
    • LoUIS 레벨까지의 d-cache를 flush한다.
  • 백업해 두었던 레지스터들을 복원하다.
        mrc     p15, 0, r0, c0, c0, 0           @ read main ID register
        and     r10, r0, #0xff000000            @ ARM?
        teq     r10, #0x41000000
        bne     3f
        and     r5, r0, #0x00f00000             @ variant
        and     r6, r0, #0x0000000f             @ revision
        orr     r6, r6, r5, lsr #20-4           @ combine variant and revision
        ubfx    r0, r0, #4, #12                 @ primary part number
  •  mrc     p15, 0, r0, c0, c0, 0
    • ERRATA 루틴들을 적용하기 위해 확인목적으로 read MIDR 한다.
    • MIDR(Main ID Register)
      • Implementer[31:24]: 0x41=ARM
      • Variant[23:20]: Major Revision Number
      • Architecture[19:16]: 0xf=ARMv7
      • Primary_Part_Number[15:4]: 0xc07=Cortex A7 MPCore
      • Revision[3:0]: Minor Revision Number
  • bne     3f
    • ARM이 아니면 3f로 jump
  • and     r5, r0, #0x00f00000
    • r5=MIDR.variant 부분만 추출
  • and     r6, r0, #0x0000000f
    • r6=MIDR.revision 부분만 추출
  • orr     r6, r6, r5, lsr #20-4
    • r5를 우측으로 16비트 쉬프트하여 r6와 합친다.
    • r6[7:4]=variant, r6[3:0]=revision
  • ubfx    r0, r0, #4, #12
    • r0=r0를 우측으로 4비트 쉬프트한 후 lsb 12비트만을 가져온다.
    • 결국 MIDR.Primary_Part_Number 부분만 추출한다.
    • rpi2: r0=0xc07
        /* Cortex-A8 Errata */
        ldr     r10, =0x00000c08                @ Cortex-A8 primary part number
        teq     r0, r10
        bne     2f
  • 다음은 Cortex-A8용 ERRATA이므로 Cortex-A8이 아니면 2f로 점프
#if defined(CONFIG_ARM_ERRATA_430973) && !defined(CONFIG_ARCH_MULTIPLATFORM)

        teq     r5, #0x00100000                 @ only present in r1p*
        mrceq   p15, 0, r10, c1, c0, 1          @ read aux control register
        orreq   r10, r10, #(1 << 6)             @ set IBE to 1
        mcreq   p15, 0, r10, c1, c0, 1          @ write aux control register
#endif
#ifdef CONFIG_ARM_ERRATA_458693
        teq     r6, #0x20                       @ only present in r2p0
        mrceq   p15, 0, r10, c1, c0, 1          @ read aux control register
        orreq   r10, r10, #(1 << 5)             @ set L1NEON to 1
        orreq   r10, r10, #(1 << 9)             @ set PLDNOP to 1
        mcreq   p15, 0, r10, c1, c0, 1          @ write aux control register
#endif
#ifdef CONFIG_ARM_ERRATA_460075
        teq     r6, #0x20                       @ only present in r2p0
        mrceq   p15, 1, r10, c9, c0, 2          @ read L2 cache aux ctrl register
        tsteq   r10, #1 << 22
        orreq   r10, r10, #(1 << 22)            @ set the Write Allocate disable bit
        mcreq   p15, 1, r10, c9, c0, 2          @ write the L2 cache aux ctrl register
#endif
        b       3f

 

        /* Cortex-A9 Errata */
2:      ldr     r10, =0x00000c09                @ Cortex-A9 primary part number
        teq     r0, r10
        bne     3f
  • 다음은 Cortex-A9용 ERRATA이므로 Cortex-A9이 아니면 3f로 점프
#ifdef CONFIG_ARM_ERRATA_742230
        cmp     r6, #0x22                       @ only present up to r2p2
        mrcle   p15, 0, r10, c15, c0, 1         @ read diagnostic register
        orrle   r10, r10, #1 << 4               @ set bit #4
        mcrle   p15, 0, r10, c15, c0, 1         @ write diagnostic register
#endif
#ifdef CONFIG_ARM_ERRATA_742231
        teq     r6, #0x20                       @ present in r2p0
        teqne   r6, #0x21                       @ present in r2p1
        teqne   r6, #0x22                       @ present in r2p2
        mrceq   p15, 0, r10, c15, c0, 1         @ read diagnostic register
        orreq   r10, r10, #1 << 12              @ set bit #12
        orreq   r10, r10, #1 << 22              @ set bit #22
        mcreq   p15, 0, r10, c15, c0, 1         @ write diagnostic register
#endif
#ifdef CONFIG_ARM_ERRATA_743622
        teq     r5, #0x00200000                 @ only present in r2p*
        mrceq   p15, 0, r10, c15, c0, 1         @ read diagnostic register
        orreq   r10, r10, #1 << 6               @ set bit #6
        mcreq   p15, 0, r10, c15, c0, 1         @ write diagnostic register
#endif
#if defined(CONFIG_ARM_ERRATA_751472) && defined(CONFIG_SMP)
        ALT_SMP(cmp r6, #0x30)                  @ present prior to r3p0
        ALT_UP_B(1f)
        mrclt   p15, 0, r10, c15, c0, 1         @ read diagnostic register
        orrlt   r10, r10, #1 << 11              @ set bit #11
        mcrlt   p15, 0, r10, c15, c0, 1         @ write diagnostic register
1:
#endif

 

        /* Cortex-A15 Errata */
3:      ldr     r10, =0x00000c0f                @ Cortex-A15 primary part number
        teq     r0, r10
        bne     4f
  • 다음은 Cortex-A15용 ERRATA이므로 Cortex-A15가 아니면 4f로 점프
#ifdef CONFIG_ARM_ERRATA_773022
        cmp     r6, #0x4                        @ only present up to r0p4
        mrcle   p15, 0, r10, c1, c0, 1          @ read aux control register
        orrle   r10, r10, #1 << 1               @ disable loop buffer
        mcrle   p15, 0, r10, c1, c0, 1          @ write aux control register
#endif

 

4:      mov     r10, #0
        mcr     p15, 0, r10, c7, c5, 0          @ I+BTB cache invalidate
#ifdef CONFIG_MMU
        mcr     p15, 0, r10, c8, c7, 0          @ invalidate I + D TLBs
        v7_ttb_setup r10, r4, r8, r5            @ TTBCR, TTBRx setup
        ldr     r5, =PRRR                       @ PRRR
        ldr     r6, =NMRR                       @ NMRR
        mcr     p15, 0, r5, c10, c2, 0          @ write PRRR
        mcr     p15, 0, r6, c10, c2, 1          @ write NMRR
#endif
        dsb                                     @ Complete invalidations
  • mcr     p15, 0, r10, c7, c5, 0
    • invalidate all i-cache to PoU (ICIALLU)
  • mcr     p15, 0, r10, c8, c7, 0
    • invalidate TLB cache all (TLBIALL)
  • v7_ttb_setup r10, r4, r8, r5
    • arch/arm/mm/proc-v7-2level.S
    • .macro      v7_ttb_setup, zero, ttbr0, ttbr1, tmp
      • zero: TTBCR에서 사용할 0값이 담겨 있어야 한다.
      • ttbr0 |= TTB_RGN_WBWA|TTB_S -> 기록하지 않음.
      • ttbr1 |= TTB_RGN_WBWA|TTB_S -> 실제기록까지함.
      • tmp: 사용하지 않는다.
    • r10: 0
    • r4, r8: 페이지테이블 물리주소 |= TTB_RGN_WBWA|TTB_S
  • mcr     p15, 0, r5, c10, c2, 0
    • r5: 0xff0a_81a8 (PRRR)
  • mcr     p15, 0, r6, c10, c2, 1
    • r6: 0x40e0_40e0(NMRR)
#ifndef CONFIG_ARM_THUMBEE
        mrc     p15, 0, r0, c0, c1, 0           @ read ID_PFR0 for ThumbEE
        and     r0, r0, #(0xf << 12)            @ ThumbEE enabled field
        teq     r0, #(1 << 12)                  @ check if ThumbEE is present
        bne     1f
        mov     r5, #0
        mcr     p14, 6, r5, c1, c0, 0           @ Initialize TEEHBR to 0
        mrc     p14, 6, r0, c0, c0, 0           @ load TEECR
        orr     r0, r0, #1                      @ set the 1st bit in order to
        mcr     p14, 6, r0, c0, c0, 0           @ stop userspace TEEHBR access
1:
#endif
  • mrc     p15, 0, r0, c0, c1, 0
    • read ID_PFR0.ThumbEE[15..12]: 12번비트가 1이면 ThumbEE가 지원됨.
  • teq     r0, #(1 << 12)
    • ThumbEE 비트가 1인 경우 z flag가 1이 된다. (xor)
  • bne     1f
    • 0이 아니면(ThumbEE가 없으면) jump
        adr     r5, v7_crval
        ldmia   r5, {r5, r6}
 ARM_BE8(orr    r6, r6, #1 << 25)               @ big-endian page tables
#ifdef CONFIG_SWP_EMULATE
        orr     r5, r5, #(1 << 10)              @ set SW bit in "clear"
        bic     r6, r6, #(1 << 10)              @ clear it in "mmuset"
#endif
        mrc     p15, 0, r0, c1, c0, 0           @ read control register
        bic     r0, r0, r5                      @ clear bits them
        orr     r0, r0, r6                      @ set them
 THUMB( orr     r0, r0, #1 << 30        )       @ Thumb exceptions
        ret     lr                              @ return to head.S:__ret
        .space 256
ENDPROC(__v7_setup)
  • adr     r5, v7_crval
    • v7_crval는 arch/arm/mm/proc-v7-2level.S에 있음
  • ldmia   r5, {r5, r6}
    • r5=0x2120c302 (for bit clear)
    • r6=0x10c03c7d (for orr)
  • mrc     p15, 0, r0, c1, c0, 0
    • SCTLR을 r0에 읽어 들인다.
  • bic     r0, r0, r5
    • r5로 bit clear
  • orr     r0, r0, r6
    • r6를 더한다
  • ret     lr
    • lr=__enable_mmu: 의 물리 주소가 담겨 있음.

 

__v7_setup_stack:

        .align  2
__v7_setup_stack:
        .space  4 * 11                          @ 11 registers

 

 

v7_ttb_setup 매크로

        /*
         * Macro for setting up the TTBRx and TTBCR registers.
         * - \ttb0 and \ttb1 updated with the corresponding flags.
         */

        .macro  v7_ttb_setup, zero, ttbr0, ttbr1, tmp 
        mcr     p15, 0, \zero, c2, c0, 2        @ TTB control register
        ALT_SMP(orr     \ttbr0, \ttbr0, #TTB_FLAGS_SMP)
        ALT_UP(orr      \ttbr0, \ttbr0, #TTB_FLAGS_UP)
        ALT_SMP(orr     \ttbr1, \ttbr1, #TTB_FLAGS_SMP)
        ALT_UP(orr      \ttbr1, \ttbr1, #TTB_FLAGS_UP)
        mcr     p15, 0, \ttbr1, c2, c0, 1       @ load TTB1
        .endm
  • mcr     p15, 0, \zero, c2, c0, 2
    • TTBCR에 zero(0)를 기록한다.
  • ALT_SMP:
    • ttbr0 |= (TTB_RGN_WBWA|TTB_S)
    • ttbr1 |= (TTB_RGN_WBWA|TTB_S)
  • mcr     p15, 0, \ttbr1, c2, c0, 1
    • TTBR1에 ttbr1을 기록한다.
    • global 커널 페이지 테이블용으로 백업해두고 커널에 진입할 때 마다 ttbr0에 복사하여 사용

 

v7_crval:

arch/arm/mm/proc-v7-2level.S

        /*   AT 
         *  TFR   EV X F   I D LR    S
         * .EEE ..EE PUI. .T.T 4RVI ZWRS BLDP WCAM
         * rxxx rrxx xxx0 0101 xxxx xxxx x111 xxxx < forced 
         *   01    0 110       0011 1100 .111 1101 < we want
         */
        .align  2
        .type   v7_crval, #object
v7_crval:
        crval   clear=0x2120c302, mmuset=0x10c03c7d, ucset=0x00c01c7c
  • clear
    • AFE, VE, FI, Bit[15], RR, Bit[9], Bit[8], A
  • mmuset
    • TRE, Bit[23], U, V, I, Z, SW, Bit[6], CP15BEN, Bit[4], Bit[3], C, M
  • ucset
    • Bit[23], U, I, Z, SW, Bit[6], CP15BEN, Bit[4], Bit[3], C
  • SCTLR
    • Bit[31]: 0
    • TE[30]: Thumb Exception enable. 0=ARM state, 1=Thumb state
    • AFE[29]: Access Flag Enable. 0=full range, 1=simplified model
    • TRE[28]: TEX remap enable.
    • NMFI[27]: Non-maskable FIQ support. 0=SW can mask, 1=SW can not mask
    • Bit[26]: 0
    • EE[25]: Exception Endianness. 0=Little-endian, 1=Big-endian
    • VE[24]: Interrupt Vectors Enable: 0=use FIQ and IRQ vectors. 1=impl.
    • Bit[23]: 1
    • U[22]: Alignment support on page (ARMv7에서 1)
    • FI[21]: Fast Interrupts configuration enable. 0=All performance features enabled, 1=low interrupt latency. (some performance feature is disabled)
    • UWXN[20]: Unprivileged Write permission implies PL1 XN (for VE)
    • WXN[19]: Write permission implies XN (for VE)
    • Bit[20:19]: 0
    • Bit[18]: 1
    • HA[17]: Hardware Access flag enable.
    • Bit[16]: 1
    • Bit[15]: 0
    • RR[14]: Round Robin select. 0=normal(random), 1=Predictable strategy. (RR)
    • V[13]: Vectors bit. 0=0x0000_0000, 1=0xFFFF_0000
    • I[12]: Instruction Cache enable.
    • Z[11]: Branch Prediction enable.
    • SW[10]: SWP and SWPB enable
    • Bit[9:8]: 0
    • B[7]: Endian support on page (ARMv7에서 0)
    • Bit[6]: 1
    • CP15BEN[5]: CP15 barrier enable
    • Bit[4:3]: 1
    • C[2]: Cache enable
    • A[1]: Alignment check enable
    • M[0]: MMU enable

sctlr

 

crval 매크로

        .macro  crval, clear, mmuset, ucset
#ifdef CONFIG_MMU
        .word   \clear
        .word   \mmuset
#else
        .word   \clear
        .word   \ucset
#endif
        .endm

 

참고

kernel/head.S – __create_page_table:

create_page_table의 주요 동작

  • 페이지 테이블(PT)을 초기화한다.
  • 페이지 테이블 엔트리(PTE)에 사용할 메모리 속성 정보인 mm_mmuflags를 프로세스 정보에서 가져온다.
  • __turn_mmu_on 루틴을 1:1 identity(VA=PA) mapping 한다.
  • 커널 코드 시작부터 커널 끝(_end)까지 매핑 한다.
    • 커널의 물리주소로 매핑하지 않고 가상주소로 매핑한다.
  • XIP  커널 이미지의 경우 ROM/Nor-Flash에서 실행되므로 이 영역도 매핑한다.
  • ATAGs/DTB 영역도 2개 섹션에 매핑한다.
    • DTB가 최대 1M 이므로 섹션에 걸쳐진다 해도 1M 페이지 섹션 2개면 충분하다.
    • ATAG가 0x100 위치에 있는 경우는 매핑하지 않는다.

kernel_head.s분석9a

 kernel/head.S – 페이지 테이블 구성 Diagram

커널 영역을 매핑할 때 kernel space에서 동작하게 해야 하므로 물리 메모리의 주소로 매핑하지 않고 커널의 가상 주소에 해당하는 주소로 매핑한다.

kernel_head.s분석10a

1:1 identity mapping

1:1 identity mapping은 가상주소를 물리주소로 변환한 주소가 같아야 할 때 필요하다. MMU를 on할 때 이러한 경우가 필요하다.

  • __turn_mmu_on 루틴을 수행할 때, 즉 MMU가 켜지기 전이므로 루틴은 물리메모리 주소로 동작 하고 있다.
  • MMU를 on 시킨 후 부터는 CPU가 물리주소(PA)를 얻기 위해 해당 주소에 해당하는 TLB 캐시를 조회한다.
  • TLB 캐시에 해당 가상 주소의 엔트리가 없는 경우 페이지테이블로부터 얻어낸다.
  • 결국 물리주소와 동일한 가상주소의 매핑이 없는 경우 페이지 fault가 발생하므로 이를 방지하기 위해 해당 루틴의 주소에 해당하는 영역의 매핑이 필요하다.
  • 해당 루틴의 매핑은 해당 루틴 영역(8개의 명령)에 대해 1:1 VA=PA 매핑이 필요하다.
  • 1:1 VA=PA 해당 매핑은 MMU를 on 한 후 루틴의 마지막에 커널 가상 주소로 점프하기 전까지 사용된다.

identity

 

create_page_table:

/*
 * Setup the initial page tables.  We only setup the barest
 * amount which are required to get the kernel running, which
 * generally means mapping in the kernel code.
 *
 * r8 = phys_offset, r9 = cpuid, r10 = procinfo
 *
 * Returns:
 *  r0, r3, r5-r7 corrupted
 *  r4 = page table (see ARCH_PGD_SHIFT in asm/memory.h)
 */
__create_page_tables:
        pgtbl   r4, r8                          @ page table address
  • r8에 물리메모리 시작 주소를 담고 pgtbl 매크로를 수행하면 r4에 페이지 테이블 시작 주소가 담긴다.
        /*
         * Clear the swapper page table
         */
        mov     r0, r4
        mov     r3, #0
        add     r6, r0, #PG_DIR_SIZE
1:      str     r3, [r0], #4
        str     r3, [r0], #4
        str     r3, [r0], #4
        str     r3, [r0], #4
        teq     r0, r6
        bne     1b
  • 페이지 테이블 엔트리(r4 ~ r6 주소까지, r0=카운터) 전체를 0으로 초기화한다.
#ifdef CONFIG_ARM_LPAE
	(..생략..)
#endif
  • LPAE를 사용하면 3단계 주소 변환 테이블을 사용하는데 이 루틴에서 PMD 테이블을 가리키는 PGD 테이블을 생성한다.
  • PGD 엔트리는 64비트이다.
	ldr     r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
  • r10: 프로세서 타입에 따른 정보
    • rpi2: __v7_ca7mp_proc_info 레이블
  • #PROCINFO_MM_MMUFLAGS: 8
  • r7 (rpi2):
    • PMD_TYPE_SECT     |           <- 섹션타입
    • PMD_SECT_AP_WRITE |   <- write
    • PMD_SECT_AP_READ  |    <- read
    • PMD_SECT_AF       |
    • PMD_FLAGS_SMP                 <- PMD_SECT_WBWA | PMD_SECT_S
        /*
         * Create identity mapping to cater for __enable_mmu.
         * This identity mapping will be removed by paging_init().
         */
        adr     r0, __turn_mmu_on_loc
        ldmia   r0, {r3, r5, r6}
        sub     r0, r0, r3                      @ virt->phys offset
        add     r5, r5, r0                      @ phys __turn_mmu_on
        add     r6, r6, r0                      @ phys __turn_mmu_on_end
        mov     r5, r5, lsr #SECTION_SHIFT
        mov     r6, r6, lsr #SECTION_SHIFT
  • __turn_mmu_on 함수의 매핑(1:1 VA=PA(identity mapping))
  • __turn_mmu_on ~ __turn_mmu_on_end에는 mmu를 켜서 가상주소의 커널로 넘어가는 코드가 담겨있는데 mmu를 켜기 전에 가상주소와 물리메모리의 주소가 같은 1:1 매핑(identity mapping)을 한다. CPU가 가상 커널 주소로 리턴(jump)하기 전까지는 물리메모리의 위치에 해당하는 가상주소가 동작중이므로 완전히 커널이 위치한 가상주소로 스위칭되기 전까지 1-2개의 섹션이 필요하여 매핑한다.
1:      orr     r3, r7, r5, lsl #SECTION_SHIFT  @ flags + kernel base
        str     r3, [r4, r5, lsl #PMD_ORDER]    @ identity mapping
        cmp     r5, r6
        addlo   r5, r5, #1                      @ next section
        blo     1b
  • orr     r3, r7, r5, lsl #SECTION_SHIFT
    • __turn_mmu_on이 시작하는 섹션(31..20비트)과 위에서 읽은 mm_mmuflags(19..0비트)를 orr 시켜 계산된 테이블 엔트리에 저장한다
  • r3: 페이지테이블+r5(__turn_mmu_on~__turn_mmu_on_end의 1M 단위의 인덱스*4)
        /*
         * Map our RAM from the start to the end of the kernel .bss section.
         */
        add     r0, r4, #PAGE_OFFSET >> (SECTION_SHIFT - PMD_ORDER)
        ldr     r6, =(_end - 1)
        orr     r3, r8, r7
        add     r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)
1:      str     r3, [r0], #1 << PMD_ORDER
        add     r3, r3, #1 << SECTION_SHIFT
        cmp     r0, r6
        bls     1b
  • 커널영역에 대한 페이지 테이블 매핑
  • r0: 페이지테이블 시작
    • 저장할 페이지 테이블 엔트리 주소로 4(8)바이트씩 증가하는 카운터
  • r4(물리페이지테이블주소) + 가상커널주소의 섹션 인덱스 값*4
    • rpi2: 처음 값은 0x0000_6000 부터 시작
  • r6: 페이지테이블 끝
    • 매핑할 커널의 마지막(.bss 섹션) 가상주소-1
    • 기존 r6에 대응하는 페이지 테이블 엔트리 주소로 바꿈.
  • r3: 기록할 섹션엔트리 값
    • = r8:phys_offset (물리메모리주소) + r7:mm_mmuflags
#ifdef CONFIG_XIP_KERNEL
        /*
         * Map the kernel image separately as it is not located in RAM.
         */
#define XIP_START XIP_VIRT_ADDR(CONFIG_XIP_PHYS_ADDR)
        mov     r3, pc
        mov     r3, r3, lsr #SECTION_SHIFT
        orr     r3, r7, r3, lsl #SECTION_SHIFT
        add     r0, r4,  #(XIP_START & 0xff000000) >> (SECTION_SHIFT - PMD_ORDER)
        str     r3, [r0, #((XIP_START & 0x00f00000) >> SECTION_SHIFT) << PMD_ORDER]!
        ldr     r6, =(_edata_loc - 1)
        add     r0, r0, #1 << PMD_ORDER
        add     r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)
1:      cmp     r0, r6
        add     r3, r3, #1 << SECTION_SHIFT
        strls   r3, [r0], #1 << PMD_ORDER
        bls     1b
#endif
  • XIP 커널영역(XIP_START ~ _edata_loc-1)을 매핑한다.
        /*
         * Then map boot params address in r2 if specified.
         * We map 2 sections in case the ATAGs/DTB crosses a section boundary.
         */
        mov     r0, r2, lsr #SECTION_SHIFT
        movs    r0, r0, lsl #SECTION_SHIFT
        subne   r3, r0, r8
        addne   r3, r3, #PAGE_OFFSET
        addne   r3, r4, r3, lsr #(SECTION_SHIFT - PMD_ORDER)
        orrne   r6, r7, r0
        strne   r6, [r3], #1 << PMD_ORDER
        addne   r6, r6, #1 << SECTION_SHIFT
        strne   r6, [r3]

#if defined(CONFIG_ARM_LPAE) && defined(CONFIG_CPU_ENDIAN_BE8)
        sub     r4, r4, #4                      @ Fixup page table pointer
                                                @ for 64-bit descriptors
#endif
  • ATAG/DTB 영역에 대해 2개의 섹션으로 매핑한다.
  • r0: r2(ATAG/DTB)주소의 섹션인덱스 -> 다시 주소로
  • r3: 대상에 대해 수정할 페이지테이블주소
    • r0주소에 대한 가상 주소값 계산
      • r3 = r0(ATAG/DTB 1M align된 주소) – r8(phys_offset) + PAGE_OFFSET
    • 페이지 테이블 주소로 계산
      • r4(물리페이지테이블주소) + r3(r0의 가상 주소)의 섹션 인덱스의 offset
  • r6: 엔트리 값
    • r7(mm_mmuflags) + r0(r2주소의 섹션인덱스)
  • r0가 0이되는 케이스(ATAG나 DTB가 주어지지 않은 경우)는 ne 조건의 문장이 수행되지 않아 매핑을 하지 않는다.
    • ATAG가 0x100에 위치하는 경우도 매핑하지 않는다.
  • DTB는 최대 1M이며, 최대 2개의 섹션이 필요 할 수 있으므로 1M 증가하여 한번 더 매핑한다
#ifdef CONFIG_DEBUG_LL
(..생략..)
#endif
  • 디버그 출력 옵션을 사용한 경우 디버깅용 io 입출력 주소에 대한 매핑이 필요하다.
#ifdef CONFIG_ARM_LPAE
        sub     r4, r4, #0x1000         @ point to the PGD table
        mov     r4, r4, lsr #ARCH_PGD_SHIFT
#endif
        ret     lr
ENDPROC(__create_page_tables)
  • 종료

pgtbl 매크로

        .macro  pgtbl, rd, phys
        add     \rd, \phys, #TEXT_OFFSET
        sub     \rd, \rd, #PG_DIR_SIZE
        .endm
  • rd = phys + #TEXT_OFFSET – #PG_DIR_SIZE
  • 페이지 테이블이 커널 바로 밑에 위치하기 때문에 물리메모리 시작 주소에서 커널 시작 OFFSET를 더해 커널 시작 물리 주소를 구하고 페이지 디렉토리만큼 빼면 페이지 디렉토리 시작 물리 주소가 담기게 된다.

__turn_mmu_on_loc:

        .ltorg
        .align
__turn_mmu_on_loc:
        .long   .
        .long   __turn_mmu_on
        .long   __turn_mmu_on_end
  • __turn_mmu_on 코드가 시작되는 주소와 __turn_mmu_on 코드가 끝나는 주소가 담겨있다.

 

 

kernel/head.S – __fixup_pv_table:

  • 2011년 1월 kernel v2.6.39-rc1에서 pv_table을 fixup 하는 방법을 구현하였다.
  • 2013년 7월 kernel v3.13-rc1에서 ARM 32bits 시스템에서 4G를 초과하는 물리메모리를 사용하는 시스템(LPAE)에서도 동작되도록 구현을 추가하였다.
  • .init.pv_table 섹션 영역에 주소 변환 함수(__virt_to_phys(), __phys_to_virt())를 사용하는 모든 커널 코드를 초기 커널 부팅 시 런타임에 모두 패치한다.
  • 가상 주소와 물리 주소가 1:1 direct mapping 되는 구간의 특성을 이용하기 때문에 kernel 영역의 lowmem 영역의 주소 변환에서만 사용가능하다.
  • 32bit ARM 프로세스에서 64비트 물리 주소를 사용하는 경우는 LPAE를 사용하여 구현한다.
    • 이 때 가상주소는 32bit, 물리 주소는 64비트를 사용하여 변환한다.
  • 런타임 시 가상주소보다 큰 주소를 가질 수 있는 물리 주소의 첫 시작 주소에서 가상 주소중 커널의 시작 주소를 뺀 값을 pv_offset 이라고한다.
    • LPAE 시스템에서 물리주소가 4G를 초과하는 경우가 있기 때문에 pv_offset도 64비트를 사용한다.
    • rpi2: 0xFFFF_FFFF_8000_0000
    • 물리 RAM 주소가 커널 가상 주소보다 작은 경우에는 pv_offset의 상위 32비트가 0xFFFF_FFFF(-1 음수 값)이다.
  • rpi2:
    • 실제 라즈베리안 설정에서는 CONFIG_PHYS_OFFSET=0으로 고정되어 있고 CONFIG_ARM_PATCH_VIRT_PHYS 또한 설정되어 있지 않아  fixup_pv_table 루틴은 동작하지 않지만 CONFIG_ARM_PATCH_VIRT_PHYS 옵션을 사용하는 경우를 가정하여 알아본다.
    • bcm2709_defconfig로 빌드하면 CONFIG_ARM_PATCH_VIRT_PHYS 옵션을 사용한다.

 

다양한 주소 변환 함수

pv_table8

  • pv_table과 관련된 함수는 __virt_to_phys() 및 __phys_to_virt() 함수이다.
    • __virt_to_phys()
      • 가상 주소를 물리 주소로 바꾸는 inline 함수
    • __phys_to_virt()
      • 물리 주소를 가상 주소로 바꾸는 inline 함수
  • pv_offset를 사용하여 주소 변환을 하는 경우
    • __virt_to_phys()
      • 물리 주소 = 가상 주소 + pv_offset
    • __phys_to_virt()
      • 가상 주소 = 물리 주소 – pv_offset

 

fixup_pv_table 순서도

 

pv_table7

 

fixup_pv_table에 사용한 레지스터들

pv_table6

  • r4에서 r7까지의 pv_table 영역에서 엔트리를 r7에 가져온다.
  • r7은 가상주소 값이므로 r3(pv_offset)를 적용하여 물리주소로 변환한 후에 커널 코드에 접근해서 해당 주소의 명령을 ip 레지스터로 받아온다.
  • ip를 조작한 후 다시 그 자리에 기록한다.

 

__fixup_pv_table:

런타임에서 물리 주소를 가상주소로 변환하는 코드를 패치하는데 XIP 커널에서는 사용할 수 없고 최소한도내의 성능 저하로 동작한다.

변환하는 방법은 pv_offset(PHYS_OFFSET – PAGE_OFFSET) 값을 더하고 빼는 것에 기반한다.

  • physical = virtual + (PHYS_OFFSET – PAGE_OFFSET)
    • __virt_to_phys()를 위해  add 명령을 사용한다.
  • virtual = physical – (PHYS_OFFSET – PAGE_OFFSET)
    • __phys_to_virt()를 위해 sub 명령을 사용한다.

pv_offset을 계산할 때 사용하는 PHYS_OFFSET이 컴파일 시 고정되어 만들어지는 초기값이라 실제 런타임시에는 여러 가지 시스템의 각 메모리의 시작 주소가 다른 시스템에서 동작 하게 된다. 이 때 MMU가 켜지기 전에 실제 물리주소를 런타임에서 알아내어 pv_offset를 계산해낸다.

이러한 pv_offset 값을 이용해서 모든 __virt_to_phys() 함수와 __phys_to_virt() 함수에서 더하고 빼는 offset 값을 패치한다.

arch/arm/kernel/head.S

#ifdef CONFIG_ARM_PATCH_PHYS_VIRT

/* __fixup_pv_table - patch the stub instructions with the delta between
 * PHYS_OFFSET and PAGE_OFFSET, which is assumed to be 16MiB aligned and
 * can be expressed by an immediate shifter operand. The stub instruction
 * has a form of '(add|sub) rd, rn, #imm'.
 */
        __HEAD
__fixup_pv_table:
        adr     r0, 1f
        ldmia   r0, {r3-r7}
        mvn     ip, #0
        subs    r3, r0, r3      @ PHYS_OFFSET - PAGE_OFFSET
        add     r4, r4, r3      @ adjust table start address
        add     r5, r5, r3      @ adjust table end address
        add     r6, r6, r3      @ adjust __pv_phys_pfn_offset address
        add     r7, r7, r3      @ adjust __pv_offset address
        mov     r0, r8, lsr #PAGE_SHIFT @ convert to PFN
        str     r0, [r6]        @ save computed PHYS_OFFSET to __pv_phys_pfn_offset
        strcc   ip, [r7, #HIGH_OFFSET]  @ save to __pv_offset high bits
        mov     r6, r3, lsr #24 @ constant for add/sub instructions
        teq     r3, r6, lsl #24 @ must be 16MiB aligned
THUMB(  it      ne              @ cross section branch )
        bne     __error
        str     r3, [r7, #LOW_OFFSET]   @ save to __pv_offset low bits
        b       __fixup_a_pv_table
ENDPROC(__fixup_pv_table)
  • CONFIG_ARM_PATCH_PHYS_VIRT
    • 런타임시 물리 주소를 가상주소로 변환하는 코드를 패치하는 옵션이다.
    • 이 옵션은 XIP_KERNEL에서는 사용할 수 없고 SPARSEMEM 옵션을 사용하는 시스템에서도 사용할 수 없다.
    • 당연히 MMU를 사용하지 않는 시스템에서도 이용할 수 없다.
  • __HEAD
    • .head.text 섹션을 사용한다.
  • ldmia r0, {r3-r7}
    • r3: offset 교정을 위한 현재 위치에 대한 가상 주소
    • r4: pv_table의 시작 물리 주소
    • r5: pv_table의 끝 물리 주소
    • r6: __pv_phys_pfn_offset 데이터를 가리키는 주소
    • r7: __pv_offset 데이터를 가리키는 주소
    • r8: 물리 RAM 시작 주소로 이 함수 호출 전에 계산되어 있는 상태. (phys_offset)
  • mvn ip, #0
    • ip -> 0xffff_ffff
    • mvn 명령은 오퍼랜드 값을 비트단위로 not을 수행한다.
  • subs r3, r0, r3
    • r3: offset (물리주소 – 가상주소)
  • r4 ~ r7까지 offset 주소를 적용해 물리주소로 변경한다.
  • mov     r0, r8, lsr #PAGE_SHIFT
    • 물리주소를 12비트 우측으로 쉬프트하면 pfn(페이지 번호)을 얻을 수 있다.
    • rpi2:
      • r8 = phys_offset(0x0000_0000)
      • r0 = __pv_phys_pfn_offset(0x0)
  • str r0, [r6]
    • r6(__pv_phys_pfn_offset)가 가리키는 주소에 교정된 물리메모리의 pfn값이 들어간다.
    • rpi2: r0 = 0x0
  • strcc ip, [r7, #HIGH_OFFSET]
    • 물리 RAM 주소가 커널 시작 작은 경우 r7+#HIGH_OFFSET이 가리키는 주소에 즉 __pv_offset[63:32]에 ip(-1)값을 저장한다.
    • 64bit 물리 주소를 위해 사용되는 것으로 -1 값은 나중에 carry가 발생하고 그 carry를 adc 명령으로 더할 때 0이되는 것을 이용하기 위함이다.
    • 만일 이 문장이 수행되지 않았다면 __pv_offset[63:32]에는 처음 빌드된대로 0 값이 들어있다.
    • rpi2: 물리 RAM 주소가 가상 커널 주소보다 작아서 0xffff_ffff가 저장됨.
  • mov     r6, r3, lsr #24
    • r6 = 24bit align(16M) 체크를 위해 r3(offset)를 우측으로 24비트 쉬프트한다.
  • teq r3, r6, lsl #24
    • r6를 다시 좌측으로 24비트 쉬프트하여 원래 값과 비교한다.
  • bne __error
    • 16M align이 안되어 있으면 에러를 출력한다.
    • pv_offset 용도로 사용할 이 값은 실제로 변환할 주소의 MSB 8bit만을 사용하여 add, sub 등을 수행 시 오퍼랜드 부분의 immediate 필드의 8비트만을 변경하므로 반드시 24 비트 align 되어 있어야 한다.
  • str r3, [r7, #LOW_OFFSET]
    • r3(offset)을 r7+#LOW_OFFSET이 가리키는 주소 즉 __pv_offset[31:0]에 저장한다.
    • 몇 가지 예)
      • 커널가상주소=0x8000_0000, 물리RAM주소=0x8000_0000
        • pv_offset = 0x0000_0000_0000_0000
        • pv_phys_pfn_0ffset = 0x0008_0000
      • 커널가상주소=0x8000_0000, 물리RAM주소=0x0000_0000
        • pv_offset = 0xffff_ffff_8000_0000
        • pv_phys_pfn_0ffset = 0x0000_0000
      • 커널가상주소=0xC000_0000, 물리RAM주소=0x2000_0000
        • pv_offset = 0xffff_ffff_6000_0000
        • pv_phys_pfn_0ffset = 0x0002_0000
  • b __fixup_a_pv_table
    • __pv_table을 fixup 하는 루틴으로 이동
        .align
1:      .long   .
        .long   __pv_table_begin
        .long   __pv_table_end
2:      .long   __pv_phys_pfn_offset
        .long   __pv_offset
  • 주의할 것은 __pv_phys_pfn_offset나 __pv_offset 항목은 해당 값이 저장되는 장소가 아니고 데이터가 저장되는 곳의 주소가 담겨있다.
  • 실제 offset 관련 데이터가 저장되는 곳은 __pv_phys_pfn_offset: 객체이다.

 

__fixup_a_pv_table:

  • r4(__pv_table_begin) 부터 r5(__pv_table_end)까지 r7에 4바이트 엔트리를 가져와서 r7이 가리키는 가상 주소값에서 ip(명령)을 가져온다.
  • 가져온 ip(명령)가 add, sub, adds 인 경우 immediate[7:0] 필드에 pv_offset를 변경하고, 추가적으로 ip(명령)가 mov 이면서 pv_offset[63:32]가 0xffff_ffff(-1)인 경우 mvn #0으로 변경한다.
  • 변경된 ip(명령)를 다시 원래 가상 주소에 패치를 수행하는 것으로 이 루틴의 수행이 완료된다.
        .text
__fixup_a_pv_table:
        adr     r0, 3f
        ldr     r6, [r0]
        add     r6, r6, r3
        ldr     r0, [r6, #HIGH_OFFSET]  @ pv_offset high word
        ldr     r6, [r6, #LOW_OFFSET]   @ pv_offset low word
        mov     r6, r6, lsr #24
        cmn     r0, #1
#ifdef CONFIG_THUMB2_KERNEL
        (...THUMB2 코드 생략...)
#else
#ifdef CONFIG_CPU_ENDIAN_BE8
        moveq   r0, #0x00004000 @ set bit 22, mov to mvn instruction
#else
        moveq   r0, #0x400000   @ set bit 22, mov to mvn instruction
#endif
        b       2f
  • adr r0, 3f
    • r0 = 3f 레이블의 물리주소
    • 3f 레이블은 _pv_offset 변수를 가리키는 주소
  • ldr r6, [r0]
    • r6 <- r0가 가리키는 주소 즉 __pv_offset가 담겨있는 가상 주소를 알아온다.
  • add r6, r6, r3
    • r6 = __pv_offset를 가리키는 가상 주소를 의 물리주소로 변환
  • ldr r0, [r6, #HIGH_OFFSET]
    • r0 = __pv_offset[63:32]
    • rpi2: 0xffff_ffff
  • ldr r6, [r6, #LOW_OFFSET]
    • r6 = __pv_offset[31:0]
    • rpi2: 0x8000_0000
  • mov r6, r6, lsr #24
    • r6 = add 또는 sub 인스트럭션의 immediate 영역(lsb 8비트)만을 수정하기 위해 r6를 24비트 쉬프트한다.
    • rpi2: 0x0000_0080
  • cmn r0, #1
    • r0(__pv_offset[63:32])와 0xffff_ffff(-1)을 비교
  • moveq r0, #0x400000
    • __pv_offset[63:32]가 -1인 경우 mov 대신 mvn #0명령을 사용하기 위해 r0에 #0x400000(bit 22 on)을 대입한다.
  • b 2f
    • 루프의 마지막 부분으로 이동하여 __pv_table에서 첫 엔트리를 가져온다.

 

1:      ldr     ip, [r7, r3]
#ifdef CONFIG_CPU_ENDIAN_BE8
        @ in BE8, we load data in BE, but instructions still in LE
        bic     ip, ip, #0xff000000
        tst     ip, #0x000f0000 @ check the rotation field
        orrne   ip, ip, r6, lsl #24 @ mask in offset bits 31-24
        biceq   ip, ip, #0x00004000 @ clear bit 22
        orreq   ip, ip, r0      @ mask in offset bits 7-0
#else
        bic     ip, ip, #0x000000ff
        tst     ip, #0xf00      @ check the rotation field
        orrne   ip, ip, r6      @ mask in offset bits 31-24
        biceq   ip, ip, #0x400000       @ clear bit 22
        orreq   ip, ip, r0      @ mask in offset bits 7-0
#endif
        str     ip, [r7, r3]
2:      cmp     r4, r5
        ldrcc   r7, [r4], #4    @ use branch for delay slot
        bcc     1b
        ret     lr
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
#endif
ENDPROC(__fixup_a_pv_table)

        .align
3:      .long __pv_offset
  • ldr     ip, [r7, r3]
    • ip에는 add(s), sub 및 mov 등의 명령이 담겨있다.
    • r7: pv_table에서 가져온 4바이트 엔트리로 가상 주소가 담겨 있다.
    • r3: pv_offset
    • ip = r7이 가리키는 물리주소에서 4바이트 명령을 하나 가져온다.
  • bic     ip, ip, #0x000000ff
    • 가져온 명령을 구성하는 비트 중 immediate[7:0] 필드 부분을 클리어한다.
  • tst     ip, #0xf00
    • 가져온 명령을 구성하는 비트 중 rotation[11:8] 필드를 체크한다
    • rotate 필드 구성 조건
      • 32 비트 물리 주소를 사용
        • __virt_to_phys()에서 add 사용 시 rotate != 0
        • __phys_to_virt()에서 sub 사용 시 rotate != 0
      • 64비트 물리 주소를 사용
        • __virt_to_phys()에서 mov 사용 시 rotate = 0
        • __virt_to_phys()에서 adds 사용 시 rotate != 0
        • __phys_to_virt()에서 sub 사용 시 rotate != 0
  • orrne   ip, ip, r6
    • rotate != 0인 경우로 add, sub. adds 명령이 있다.
    • r6: pv_offset[31:24] >> 24 한 값
    • ip = 명령을 구성하는 비트 중 immediate[7:0] 부분에 r6로 채운다.
  • biceq   ip, ip, #0x400000
    • rotate = 0인 경우로 mov 명령이 있다.
    • bit22를 clear하여 mov 명령으로 변경한다.
      • bit22: 0=mov, 1=mvn
  • orreq    ip, ip, r0
    • ip에 r0(0=mov 유지, 0x400000=mvn으로 변경) 값을 더한다.
    • r0가 mvn이 될 수 있는 경우는 pv_offset[64:32]가 0xffff_ffff 인경우로 물리 RAM 시작 주소가 커널시작 가상 주소보다 작은 경우이다.
      • 예) 커널가상주소=0x8000_0000, 물리RAM주소=0x0000_0000 인 경우  pv_offset = 0xffff_ffff_8000_0000가 된다.
  • str     ip, [r7, r3]
    • 조작한 명령을 다시 원래 주소에 저장한다.
  • 2: cmp     r4, r5
    • 루프(__pv_table_end)의 끝 인지 비교
  • ldrcc   r7, [r4], #4
    • 끝이 아니면 r4(pv_table의 루프 카운터)에서 r7(pv_table 엔트리)을 가져오고 r4를 4만큼 증가한다.
  • bcc     1b
    • 루프로 돌아간다.
  • ret     lr
    • 완료 했으므로 리턴

 

__pv_phys_pfn_offset & __pv_offset 객체

        .data
        .globl  __pv_phys_pfn_offset
        .type   __pv_phys_pfn_offset, %object
__pv_phys_pfn_offset:
        .word   0
        .size   __pv_phys_pfn_offset, . -__pv_phys_pfn_offset

        .globl  __pv_offset
        .type   __pv_offset, %object
__pv_offset:
        .quad   0
        .size   __pv_offset, . -__pv_offset
  • __pv_phys_pfn_offset의 경우 4바이트(1 word)를 사용하고 초기 값은 0이다.
  • __pv_offset의 경우 64비트 물리주소의 offset을 표현하기 위해 8바이트(2 word)를 사용하고 초기 값은 0이다.
  • .quad 0
    • ARM이 아닌 아키텍처에서는 워드가 2바이트 따라서 quad word가 8바이트이다.
    • ARM에서는 워드가 4바이트지만 quad 타입은 위와 동일하게 8바이트이다.

 

주소 변환 함수

virt_to_phys()

static inline phys_addr_t virt_to_phys(const volatile void *x)
{
        return __virt_to_phys((unsigned long)(x));
}
  • rpi2:
    • virt_to_phys(0x8123_4560)
      • add r0, 0x8123_4560, 0x8100_0000
    • pv_fixup 후 (pv_fixup=0xffff_ffff_8000_0000)
      • add r0, 0x8123_4560, 0x8000_0000
    • r0 = 0x0123_4560
  • 64비트 예: 물리램=0x0_8000_0000, 커널가상주소=0xC000_0000
    • virt_to_phys(0xc123_4560)
      • mov r0[64:32], 0x0000_0081
      • adds r0[31:0], 0xc123_4560, 0x8100_0000
      • adc r0[63:32], r0[63:32], #0
    • pv_fixup 후 (pv_fixup=0xffff_ffff_c000_0000)
      • mvn r0[64:32], 0xffff_ffff
      • adds r0[31:0], 0xc123_4560, 0xc0000_0000
      • adc r0[63:32], r0[63:32], #0
    • r0 = 0x0000_0000_8123_4560
  • 64비트 예: 물리램=0x1_8000_0000, 커널가상주소=0xC000_0000
    • virt_to_phys(0xc123_4560)
      • mov r0[64:32], 0x0000_0081
      • adds r0[31:0], 0xc123_4560, 0x8100_0000
      • adc r0[63:32], r0[63:32], #0
    • pv_fixup 후 (pv_fixup=0x0_c000_0000)
      • mvn r0[64:32], 0x0
      • adds r0[31:0], 0xc123_4560, 0xc0000_0000
      • adc r0[63:32], r0[63:32], #0
    • r0 = 0x0000_0001_8123_4560

 

phys_to_virt()

static inline void *phys_to_virt(phys_addr_t x)
{
        return (void *)__phys_to_virt(x);
}
  • rpi2:
    •  phys_to_virt(0x0123_4560)
      • sub r0, 0x0123_4560, 0x8100_0000
    • pv_fixup 후 (pv_fixup=0xffff_ffff_8000_0000)
      • sub r0, 0x0123_4560, 0x8000_0000
    • r0 = 0x8123_4560
  • 64비트 예: 물리램=0x0_8000_0000, 커널가상주소=0xC000_0000
    • phys_to_virt(0x0_8123_4560)
      • sub r0, 0x8123_4560, 0x8100_0000
    • pv_fixup 후 (pv_fixup=0xFFFF_FFFF_C000_0000)
      • sub r0, 0x8123_4560, 0xC000_0000
    • r0 = 0xC123_4560
  • 64비트 예: 물리램=0x1_8000_0000, 커널가상주소=0xC000_0000
    • phys_to_virt(0x_1_8123_4560)
      • sub r0, 0x8123_4560, 0x8100_0000
    • pv_fixup 후 (pv_fixup=0x0_C000_0000)
      • sub r0, 0x8123_4560, 0xC000_0000
    • r0 = 0xC123_4560

 

 CONFIG_ARM_PATCH_PHYS_VIRT 옵션을 사용할 때의 주소 변환 함수

__virt_to_phys()

static inline phys_addr_t __virt_to_phys(unsigned long x)
{
        phys_addr_t t;

        if (sizeof(phys_addr_t) == 4) {
                __pv_stub(x, t, "add", __PV_BITS_31_24);
        } else {
                __pv_stub_mov_hi(t);
                __pv_add_carry_stub(x, t);
        }
        return t;
}
  • 1) 32bit 가상 주소 -> 32비트 물리 주소
    • __pv_stub()의 경우
      • __PV_BITS_31_24를 오퍼랜드로 사용하여 rotate[21:8] 필드 값이 컴파일 시 4로 고정된다.
        • (8 바이트 immediate 부분을 우측으로 8비트 rotation이 필요하다. rotation 1비트당 2번의 rotation 동작되므로 rotation 값을 4로 한다.)
      • add 명령어 사용
  • 2) 32bit 가상 주소 -> 64bit 물리주소(LPAE)
    • __pv_stub_mov_hi()의 경우
      • __PV_BITS_7_0을 사용하여 rotate[12:8] 필드값이 컴파일 시 0으로 고정된다.
      • mov 명령어 사용
    • __pv_add_carry_stub()의 경우
      • __PV_BIT_31_24를 오퍼랜드로 사용하여 rotate[12:8] 필드값이 컴파일 시 4로 고정된다.
      • adds 명령어 사용
      • adc 명령을 사용하여 adds 명령 사용 시 더한 결과가 4G를 넘어 carry가 발생하면 물리주소의 상위 32비트 값에 carry를 더한다.

 

__phys_to_virt()

static inline unsigned long __phys_to_virt(phys_addr_t x)
{
        unsigned long t;

        /*
         * 'unsigned long' cast discard upper word when
         * phys_addr_t is 64 bit, and makes sure that inline
         * assembler expression receives 32 bit argument
         * in place where 'r' 32 bit operand is expected.
         */
        __pv_stub((unsigned long) x, t, "sub", __PV_BITS_31_24);
        return t;
}
  • 64비트 물리주소를 사용하는 시스템(LPAE)에서 64 비트 물리 주소를 32 비트 가상 주소로 변환하는 경우 물리 주소의 상위 32 비트는 무시하고 계산한다.
    • __PV_BITS_31_24를 오퍼랜드로 사용하여 rotate[21:8] 필드 값이 컴파일 시 4로 고정된다.
    • sub 명령어 사용

 

__pv_stub()

#define __pv_stub(from,to,instr,type)                   \
        __asm__("@ __pv_stub\n"                         \
        "1:     " instr "       %0, %1, %2\n"           \
        "       .pushsection .pv_table,\"a\"\n"         \
        "       .long   1b\n"                           \
        "       .popsection\n"                          \
        : "=r" (to)                                     \
        : "r" (from), "I" (type))

 

__pv_stub_mov_hi()

#define __pv_stub_mov_hi(t)                             \
        __asm__ volatile("@ __pv_stub_mov\n"            \
        "1:     mov     %R0, %1\n"                      \
        "       .pushsection .pv_table,\"a\"\n"         \
        "       .long   1b\n"                           \
        "       .popsection\n"                          \
        : "=r" (t)                                      \
        : "I" (__PV_BITS_7_0))
  • %R0
    • 인수 %0의 하위 32비트를 취급하는 레지스터

__pv_add_carry_stub()

#define __pv_add_carry_stub(x, y)                       \
        __asm__ volatile("@ __pv_add_carry_stub\n"      \
        "1:     adds    %Q0, %1, %2\n"                  \
        "       adc     %R0, %R0, #0\n"                 \
        "       .pushsection .pv_table,\"a\"\n"         \
        "       .long   1b\n"                           \
        "       .popsection\n"                          \
        : "+r" (y)                                      \
        : "r" (x), "I" (__PV_BITS_31_24)                \
        : "cc")
  • %Q0
    • 인수 %0의 상위 32비트를 취급하는 레지스터
  • adc 명령은 pv_table에 push 하지 않는다.

 

pv_table

주소 변환 함수 사용 시 __pv_table에 저장되는 내용

32bit 물리 주소 시스템

pv_table1a

  • virt_to_phys() 및 phys_to_virt() 함수는 각각 하나의 ARM 명령어를 만들어내고 그 주소를 __pv_table에 저장한다.
  • 엔트리 주소는 커널이 사용하는 가상 주소이다.

 

64bit 물리 주소 시스템(LPAE)

pv_table2c

  • virt_to_phys() 함수의 경우 3개의 ARM 명령어를 만들어내고 그 중 2 개의 주소를 __pv_table에 저장한다.
    • 첫 번째, pv_stub_mov_hi() 매크로를 호출하고 여기서 1개의 mov 명령이 만들어지며 해당 주소는 __pv_table에 저장된다.
    • 두 번째, pv_add_carry_stub() 매크로를 호출하고 여기서 1개의 adds 명령이 만들어지고 해당 주소는 __pv_table에 저장된다. 다음 또 1개의 adc 명령어를 만들고 이 명령에 대해서는 __pv_table에 저장하지 않는다.
  • phys_to_virt() 함수의 경우 1개의 ARM 명령어를 만들어내고 그 중 1 개의 주소를 __pv_table에 저장한다.
    • 물리 주소 64비트를 가상 주소로 변환 시 phys[63:32]는 버리고 계산한다. 이렇게 버려도 상관없는 이유는 1:1 direct mapping이 허가된 lowmem 영역에 대해서만 변환을 보장하는 특성 때문에 이 영역을 벗어난 영역을 정확히 알아낼 수는 없다.
      • PAGE_OFFSET:0xC000_0000, 물리램 시작 주소: 0x1_8000_0000 에서의 예)
        • 0xC123_0000 = phys_to_virt(0x1_8123_0000);
        • 0xC123_0000 = phys_to_virt(0x2_8123_0000); -> lowmem 벗어나는 영역으로 시도

 

 

저장 섹센

        .init.pv_table : { 
                __pv_table_begin = .;
                *(.pv_table)
                __pv_table_end = .;
        }
        .init.data : {

 

pv_table 엔트리 패치 세부 과정

32bit 물리 주소(물리 RAM 주소: 0x0)

pv_table3

  • 계산된 pv_offset의 하위 32비트를 각 명령의 immediate 필드 부분만 교체한다.

 

64bit 물리 주소(물리 RAM 주소: 0x0_8000_0000)

pv_table4

  • 명령의 rotate 필드가 0인 경우
    • sub, adds 명령이 해당된다.
    • 계산된 pv_offset의 하위 32비트를 각 명령의 immediate 필드 부분만 교체한다.
  • 명령의 rotate 필드가 1인 경우
    • mov 명령이 해당된다.
    • 계산된 pv_offset의 상위 32비트를 mov 명령의 immediate 필드 부분만 교체하는데 만일 음수 값이면 0x0을 넣고 명령을 mvn을 넣는다.

명령어의 비트별 구조

pv_table5

  • Rotate 필드는 1증가 시마다 rotate 2에 해당한다.
    • 우측으로 8비트를 rotate 하고자 할 경우 Rotate 필드에 4를 대입 한다.
  • 컴파일 시 Immediate에 __PV_BITS_31_24(0x8100_0000) 값을 사용하면 Rotate 값이 자동 계산되어 4가 대입된다.
    • __PV_BITS_31_24를 2진수로 표현하면 10000001_00000000_00000000_00000000 과 같다.
      • Immediate는 인접한 8비트만 표현가능하므로 여기선 0b10000001이란 이진 수를 우측으로 8번을 rotate 시키면 0x8100_0000과 같아진다.

 

다양한 주소 변환 방법

  • pv_table을 사용하여 런타임 시 패치하는 방법: CONFIG_ARM_PATCH_PHYS_VIRT=y
    • DTB가 사용되기 시작한 커널 3.x 에서 같이 사용
    • 연속되지 않은 물리메모리(SPARSEMEM) 사용시에도 이 옵션을 사용할 수 없음
    • 커널 내부 명령을 수정하는 기능으로 XIP 커널 이미지에서는 이 기능을 사용할 수 없다.
  • 컴파일 시 고정시킨 offset을 사용하는 Legacy 방법: CONFIG_ARM_PATCH_PHYS_VIRT=n
  • 그 외 제작사가 제공하는 방법: 별도의 함수를 제공해야 한다.

pv_table9

  • PFN: 물리 주소를 물리 페이지 단위의 번호로 바꾼 값
    • 물리 주소 0x0000_0000~0x0000_0FFF → PFN 0
    • 물리 주소 0x0FFF_F000~0x0FFF_FFFF → PFN 0xFFFF
    • 라즈베리파이2:
      • 커널 초기화(__fixup_pv_table) 후 → __pv_phys_pfn_offset=0x0
      • __pv_offset=0xFFFF_FFFF_8000_0000