32비트 ARM에서의 user space vs kernel space

user space 크기 선택

커널을 빌드시 3가지의 VMSPLIT 옵션에 따라 user space가 달라진다.

  • 주로 PC 리눅스에서는 3G 옵션을 사용하고,
  • 임베디드 리눅스에서는 3G 또는 2G 옵션을 사용한다.
  • 1G 옵션은 거의 사용되지 않는다.

user space를 크게할 때의 장단점

  • 장점: 사용자 application에서 사용할 수 있는 가상 메모리 공간이 더 커진다.
  • 단점: NORMAL ZONE의 공간이 작아 HIMEM ZONE의 사용이 많아져 메모리 access의 속도가 저하된다.

vmsplit

 

메모리 공간 할당은 아래 두 시스템에서 추가적으로 이해가 필요하지만 32bit ARM만 이해하기로 한다.

  • 32bit ARM with LPAE
  • 64bit ARM

 

¹) NORMAL ZONE: kernel space에 존재하며, 물리 메모리의 direct 매핑이 가능한 영역.

²) HIMEM ZONE: kernel space에는 선택한 일부 공간만 존재하며, 물리 메모리의 direct 매핑을 할 수 없어 반드시 일부를 선택하여 사용할 수 있다.

compressed/head.S – cache_on

ARMv7에서의 cache on

cache 기능을 사용하기 위해서는 MMU 장치를 동작시켜야 하는데 이 MMU 사용을 위해 1차 페이지 테이블(pgd)을 임시로 구성하여 사용한다.

  • 페이지 테이블은 arch/arm/boot/decompress/head.S에서 압축된 커널의 relocation 또는 decompression 등에서만 사용한다.
  • 가상주소와 물리주소가 서로 동등한 주소로 1:1 매핑을 하는 것이 특징이다.
  • 구성되는 엔트리들은 1M 바이트 크기의 페이지 테이블을 갖는 섹션 엔트리로 만들어지며 다.
  • DRAM 영역 뿐만 아니라 ROM에서 동작하는 경우 2M 페이지 즉 2개의 섹션 엔트리도 추가 배정한다.
  • 아래 순서도는 cache_on을 통해 페이지 테이블을 구성하고 MMU를 켜는 전체 흐름을 나타낸다.

decompress_head.s분석5

cache_on:

/*
 * Turn on the cache.  We need to setup some page tables so that we
 * can have both the I and D caches on.
 *
 * We place the page tables 16k down from the kernel execution address,
 * and we hope that nothing else is using it.  If we're using it, we
 * will go pop!
 *
 * On entry,
 *  r4 = kernel execution address
 *  r7 = architecture number
 *  r8 = atags pointer
 * On exit,
 *  r0, r1, r2, r3, r9, r10, r12 corrupted
 * This routine must preserve:
 *  r4, r7, r8
 */
                .align  5
cache_on:       mov     r3, #8                  @ cache_on function
                b       call_cache_fn
  • align 5
    • 32byte align
  • r3 <- #8번을 넣은 이유는 객체의 멤버가 8byte에 있는 곳을 가리킨다.
  • 그 객체의 8바이트 위치에는 해당 아키텍처의 cache_on 함수의 포인터가 담겨 있다.
  • b call_cache_fn
    • 위 루틴을 통해 __armv7_mmu_cache_on: 주소를 찾아서 점프한다.

call_cache_fn:

/*
 * Here follow the relocatable cache support functions for the
 * various processors.  This is a generic hook for locating an
 * entry and jumping to an instruction at the specified offset
 * from the start of the block.  Please note this is all position
 * independent code.
 *
 *  r1  = corrupted
 *  r2  = corrupted
 *  r3  = block offset
 *  r9  = corrupted
 *  r12 = corrupted
 */
call_cache_fn:  adr     r12, proc_types
#ifdef CONFIG_CPU_CP15
                mrc     p15, 0, r9, c0, c0      @ get processor ID
#else
                ldr     r9, =CONFIG_PROCESSOR_ID
#endif
1:              ldr     r1, [r12, #0]           @ get value
                ldr     r2, [r12, #4]           @ get mask
                eor     r1, r1, r9              @ (real ^ match)
                tst     r1, r2                  @       & mask
 ARM(           addeq   pc, r12, r3             ) @ call cache function
 THUMB(         addeq   r12, r3                 )
 THUMB(         moveq   pc, r12                 ) @ call cache function
                add     r12, r12, #PROC_ENTRY_SIZE
                b       1b
  • r3에 offset을 주고 이 레이블로 점프하면 동작하고 있는 CPU 아키텍처를 찾아 해당 캐시 함수를 호출하게된다.
  • adr     r12, proc_types
    • proc_types 객체의 주소를 알아온다.
  • CONFIG_CPU_CP15
    • CPU가 CP15 레지스터를 가지고 있을 경우 설정되는 옵션으로 ARM Cortex A 시리즈들은 CP15 레지스터를 가지고 있다.
  • r3에 #8-cache_on, #12-cache_off, #16-cache_flush가 들어갈 수 있다.
    • rpi2:
      • #8: __armv7_mmu_cache_on:
      • #12: __armv7_mmu_cache_on:
      • #16: __armv7_mmu_cache_flush:
  • r12: proc_types 레이블의 주소를 알아온다. (proc_type별 데이터가 담긴 객체)
  • r9: MIDR 값을 가져온다.
  • r1: architecture code
  • r2: architecture mask

proc_types:

/*
 * Table for cache operations.  This is basically:
 *   - CPU ID match
 *   - CPU ID mask
 *   - 'cache on' method instruction
 *   - 'cache off' method instruction
 *   - 'cache flush' method instruction
 *
 * We match an entry using: ((real_id ^ match) & mask) == 0
 *
 * Writethrough caches generally only need 'on' and 'off'
 * methods.  Writeback caches _must_ have the flush method
 * defined.
 */
                .align  2
                .type   proc_types,#object
proc_types:

(다른 ARM 아키텍처들은 생략하고 아래는 ARMv7 아키텍처)

                .word   0x000f0000              @ new CPU Id
                .word   0x000f0000
                W(b)    __armv7_mmu_cache_on
                W(b)    __armv7_mmu_cache_off
                W(b)    __armv7_mmu_cache_flush

                .word   0                       @ unrecognised type
                .word   0
                mov     pc, lr
 THUMB(         nop                             )
                mov     pc, lr
 THUMB(         nop                             )
                mov     pc, lr
 THUMB(         nop                             )

                .size   proc_types, . - proc_types

                /*
                 * If you get a "non-constant expression in ".if" statement"
                 * error from the assembler on this line, check that you have
                 * not accidentally written a "b" instruction where you should
                 * have written W(b).
                 */
                .if (. - proc_types) % PROC_ENTRY_SIZE != 0
                .error "The size of one or more proc_types entries is wrong."
                .endif

__armv7_mmu_cache_on:

__armv7_mmu_cache_on:
                mov     r12, lr
#ifdef CONFIG_MMU
                mrc     p15, 0, r11, c0, c1, 4  @ read ID_MMFR0
                tst     r11, #0xf               @ VMSA
                movne   r6, #CB_BITS | 0x02     @ !XN
                blne    __setup_mmu
                mov     r0, #0
                mcr     p15, 0, r0, c7, c10, 4  @ drain write buffer
                tst     r11, #0xf               @ vmsa
                mcrne   p15, 0, r0, c8, c7, 0   @ flush I,D TLBs
#endif
  • ARMv7에서 사용하는 캐시 on 루틴이다.
  • mrc     p15, 0, r11, c0, c1, 4
    • ID_MMFR0.vmsa를 읽어온다.
  • MMU가 있는지 확인해서 있는경우 mmu를 동작시키기 위함
  • movne   r6, #CB_BITS | 0x02
    • r6=페이지 속성
    • rpi2 SoC는 Write-Back 캐시 기능이 지원된다.
    • rpi2: 0xE(!XN(0) | C | B | SECTION)
  • blne  __setup_mmu
    • MMU를 사용하기 전에 먼저 임시로 사용할 1차 페이지 테이블을 준비한다.
  • mov r0, #0
    • blne __setup_mmu의 수행 이후에 mcr 명령이 동작하게 하기 위해 r0에 0을 넣은 후 이를 다음 명령에서 사용하게 되면 CPU가 out-of-order execution으로 동작한다 하더라도 레지스터 dependency인해 수행 순서가 바뀌지 않게 하는 효과가 있다. (in-order execution)
  • mcr     p15, 0, r0, c7, c10, 4
  • mcrne   p15, 0, r0, c8, c7, 0
    • TLBIALL (unified TLB Invalidate All)
                mrc     p15, 0, r0, c1, c0, 0   @ read control reg
                bic     r0, r0, #1 << 28        @ clear SCTLR.TRE
                orr     r0, r0, #0x5000         @ I-cache enable, RR cache replacement
                orr     r0, r0, #0x003c         @ write buffer
                bic     r0, r0, #2              @ A (no unaligned access fault)
                orr     r0, r0, #1 << 22        @ U (v6 unaligned access model)
                                                @ (needed for ARM1176)
  • mrc     p15, 0, r0, c1, c0, 0
    • read SCTLR
    • RAO: Read As One
    • SBOP: Should Be One Preserved
    • RAO/SBOP <- 앞쪽은 read 뒤쪽이 write option
  • SCTLR 레지스터를 읽은 후 몇 가지 비트들을 조작해놓는다.
    • 마지막 루틴에서 SCTLR에 저장할 계획
    • I-Cache를 사용하는 것으로 설정, cache replacement  방법은 Round Robin 사용
    • Write buffer 사용
    • A: unaligned access fault를 지원하기 위한 mask
    • U: unaligned access 를 지원하려면 1. (armv7에서는 항상 1)
#ifdef CONFIG_MMU
 ARM_BE8(       orr     r0, r0, #1 << 25 )      @ big-endian page tables
                mrcne   p15, 0, r6, c2, c0, 2   @ read ttb control reg
                orrne   r0, r0, #1              @ MMU enabled
                movne   r1, #0xfffffffd         @ domain 0 = client
                bic     r6, r6, #1 << 31        @ 32-bit translation system
                bic     r6, r6, #3 << 0         @ use only ttbr0
                mcrne   p15, 0, r3, c2, c0, 0   @ load page table pointer
                mcrne   p15, 0, r1, c3, c0, 0   @ load domain access control
                mcrne   p15, 0, r6, c2, c0, 2   @ load ttb control
#endif
                mcr     p15, 0, r0, c7, c5, 4   @ ISB
                mcr     p15, 0, r0, c1, c0, 0   @ load control register
                mrc     p15, 0, r0, c1, c0, 0   @ and read it back
                mov     r0, #0
                mcr     p15, 0, r0, c7, c5, 4   @ ISB
                mov     pc, r12
  • VMSA(MMU)를 지원하지 않는 경우 뒤 조건 명령 3개는 동작하지 않음.
  • mrcne   p15, 0, r6, c2, c0, 2
    • TTBCR(Transalation Table Base Control Register)를 읽는다.
  • MMU 비트를 enable
  • DOMAIN0만 client(0b01)로 설정, 나머지 DOMAIN1~DOMAIN15까지는 manager(0b00)으로 설정
  • TTBCR.N을 0으로 설정하여 16K 페이지 테이블을 사용
  • TTBCR.PXN을 0으로 설정하여 32bit 페이지 변환을 사용
  • r3(start page table) -> TTBR0
  • r1(domain…) -> DACR
  • r6(32bit, N) -> TTBCR
  • r0(시스템 관련 비트들) -> SCTLR
    • 여기서 MMU를 켬. (캐시 기능 on)

Page Table 영역 초기화

매핑 영역 및 규칙

  • 전체 4G 공간을 1:1로 가상주소와 물리주소가 동일하게 매핑
  • 각 엔트리에 대한 메모리 속성 부여
    • DRAM 256M 영역에 read/write, !XN, Cache-able, Buffer-able, Section
    • ROM에서 커널이 동작하는 경우를 위해 현재 실행되는 코드 영역에 대응하는 2개 엔트리에 read/write, !XN, !Cache-able, Buffer-able, Section
      • 전체 ROM 영역이 아니라 현재 코드가 동작하는 ROM 2M 영역만 특별히 !XN을 두는 이유는 당연히 지금 코드가 동작하는 piggy(relocation 코드와 압축해제 코드가 담긴 영역) 영역이 수K~수십K 정도로 아주 작기 때문에 현재 코드가 동작하는 곳만 !XN으로 설정하여 실행 영역으로 변경한다. 추가로 1개의 섹션을 더 매핑을 하는 이유는 코드가 동작하면서 섹션 경계를 넘어갈 수 있으므로 그 위로 하나 더 섹션을 사용하는 것으로 만들면 충분하다. (2 개의 섹션)
      • Cache-able을 사용하지 않는 이유는 이 2개의 엔트리에 대응하는 현재 실행 영역이 ROM인지 RAM인지 특별히 구분하지 않게 하기 위해 ROM 속성을 위주로 설정한다. 참고로 ROM에서는 캐시를 사용하지 못하고 버퍼만 사용가능하다.
    • 그외 영역에는 read/write, XN, !Cache-able, !Buffer-able, Section을 설정하여 실행되지 않는 섹션 매핑 속성으로 둔다.

decompress_head.s분석6b

__setup_mmu:

16KB의 페이지 테이블을 초기화하는데 이 영역은 relocation 및 decompressed 루틴에서 잠시 이용하므로 물리메모리 변환을 1단계로만 만들며 페이지를 1M 단위로 4G 전체 메모리에 대응하는 섹션 엔트리(총 4096개)로 구성한다.

__setup_mmu:    sub     r3, r4, #16384          @ Page directory size
                bic     r3, r3, #0xff           @ Align the pointer
                bic     r3, r3, #0x3f00
/*
 * Initialise the page tables, turning on the cacheable and bufferable
 * bits for the RAM area only.
 */
                mov     r0, r3
                mov     r9, r0, lsr #18
                mov     r9, r9, lsl #18         @ start of RAM
                add     r10, r9, #0x10000000    @ a reasonable RAM size
                mov     r1, #0x12               @ XN|U + section mapping
                orr     r1, r1, #3 << 10        @ AP=11
                add     r2, r3, #16384
  • r3=decompressed kernel-16K 부터 page direcotry의 시작
    • r3의 align을 16K단위로 맞춘다. (14 bits)
    • 인스트럭션을 구성하는 immediate의 길이가 제한되어 있어 2번에 나누어 처리
    • bic r3, r3, #3fff 를 하려는 목적
  • mov r0, r3
    • r0=r3(페이지 디렉토리 시작) 부터 시작하는 카운터로 초기화할 엔트리를 가리키며 페이지 디렉토리의 끝을 만날때까지 4바이트 단위로 증가시킨다.
  • r9=DRAM 시작 주소(align 256KB:18비트 쉬프트)
  • r10=resonable DRAM(256M) 영역의 끝(r9 + 256M)
  • r1=엔트리 값으로 처음에 XN | Section | AP=full access로 시작한다.
    • 0xc12
  • r2=페이지 디렉토리의 끝(시작 + 16KB)
    • U: Uncache로 추정(0)
    • AP(11): full access – read/write
1:              cmp     r1, r9                  @ if virt > start of RAM
                cmphs   r10, r1                 @   && end of RAM > virt 
                bic     r1, r1, #0x1c           @ clear XN|U + C + B
                orrlo   r1, r1, #0x10           @ Set XN|U for non-RAM
                orrhs   r1, r1, r6              @ set RAM section settings
                str     r1, [r0], #4            @ 1:1 mapping
                add     r1, r1, #1048576
                teq     r0, r2
                bne     1b
  • 엔트리가 RAM 영역에 대응하면 r1 += full access | C | B | !XN | Section으로 그 외 영역인 경우 r1 += full access | XN | Section으로 기록
  • r1을 1M씩 증가시킨 후 page directory의 끝을 만날때까지 반복한다.
    • 256M 영역내: 0xc0e, 영역외: 0xc12
/*
 * If ever we are running from Flash, then we surely want the cache
 * to be enabled also for our execution instance...  We map 2MB of it
 * so there is no map overlap problem for up to 1 MB compressed kernel.
 * If the execution is in RAM then we would only be duplicating the above.
 */
                orr     r1, r6, #0x04           @ ensure B is set for this
                orr     r1, r1, #3 << 10
                mov     r2, pc
                mov     r2, r2, lsr #20
                orr     r1, r1, r2, lsl #20
                add     r0, r3, r2, lsl #2
                str     r1, [r0], #4
                add     r1, r1, #1048576
                str     r1, [r0]
                mov     pc, lr
ENDPROC(__setup_mmu)
  • r1 값은 0xc0e (full access | C | B | section)
  • ROM에서 시작한 경우 플래쉬에 대응하는 페이지 테이블을 !XN | B를 하여 2칸(2MB) 기록한다. DRAM에서 시작한 경우에는 그냥 2번 겹쳐 기록 하게 되는데 겹쳐 기록해도 무방하므로 상관없다.
  • 현재 실행되고 있는 위치의 하위 20비트를 제거하면 페이지테이블의 index가 된다. 이를 r1(short descriptor page table entry)에 추가
  • r0(현위치에 대응하는 PTE) = r3(page directory 시작) + index(r2 * 4)
  • 1M를 증가시켜 한 번 더한다. (총 2M까지면 충분하다고 판단)

 

참고

compressed/head.S – cache_clean_flush:

캐시를 모두(d-cache, i-cache) flush(clean & invalidate) 한다. 아키텍처마다 캐시를 flush하는 구현이 각각 다르기 때문에 현재 동작하는 CPU 아키텍처코드를 알아내어 해당 아키텍처에서 구현해놓은 cache flush 루틴을 수행하게 한다. 아래 순서도는 ARMv7에 대응하는 cache flush 루틴의 순서도이다.

decompress_head.s분석7

 

Flush & Clean

Data 캐시에서 flush와 Clean이라는 용어는 여러 아키텍처에 따라 각각 의미가 다르게 해석되어 사용되므로 유의해야 한다.

  • flush는 arm에 해당하는 clean, invalidate, clean & invalidate의 3가지 의미로 혼용 사용된다.
  • clean은 arm에 해당하는 clean, clean & invalidate의 2가지 의미로 혼용 사용된다.

따라서 arm 매뉴얼에서는 flush라는 용어를 가급적 사용하지 않고 clean, invalidate, clean & invalidate의 3가지로 정확히 나누어 사용한다.

  • clean
    • dirty(기록) 설정된 캐시 라인 -> 메모리로 기록하고 캐시에서 그대로 유지
      • dirty를 0으로 변경, valid는 1로 유지
  • invalidate
    • dirty 유무와 상관 없이 캐시 라인을 버린다. (캐시 라인은 메모리에 기록하지 않는다)
      • valid를 0으로 변경
  • clean & invalidate
    • dirty(기록) 설정된 캐시 라인이 있는 경우 메모리로 기록하고 캐시 라인을 버린다.
      • dirty 및 valid를 0으로 변경
    • dirty(기록) 설정되지 않은 캐시 라인은 곧바로 캐시 라인을 버린다.
      • valid를 0으로 변경

Instruction 캐시, Branch Pridict 캐시, TLB 캐시 등은 Dirty 비트가 없으므로 flush, clean 및 invalidate 모두 같은 의미로 사용된다.

  • arm 매뉴얼에서는 invalidate를 표준으로 사용하고 flush 라는 용어도 사용된다.

 

cache_clean_flush:

/*
 * Clean and flush the cache to maintain consistency.
 *
 * On exit,
 *  r1, r2, r3, r9, r10, r11, r12 corrupted
 * This routine must preserve:
 *  r4, r6, r7, r8
 */
                .align  5
cache_clean_flush:
                mov     r3, #16
                b       call_cache_fn
  • 캐시 clean & flush를 수행한다.
  • rpi2: ARMv7 아키텍처를 사용하므로 __armv7_mmu_cache_flush로 진행

 

__armv7_mmu_cache_flush:

__armv7_mmu_cache_flush:
                tst     r4, #1
                bne     iflush
                mrc     p15, 0, r10, c0, c1, 5  @ read ID_MMFR1
                tst     r10, #0xf << 16         @ hierarchical cache (ARMv7)
                mov     r10, #0
                beq     hierarchical
                mcr     p15, 0, r10, c7, c14, 0 @ clean+invalidate D
                b       iflush
  • tst r4, #1
    • r4에는 decompressed 커널 시작 주소가 담겨있는데 임시로 하위 1비트를 캐시 on 유보 비트로 이용하고 있다.
    • 캐시 유보 비트가 설정되어 있으면 d-cache를 사용하지 않았으모로 d-cache에 대해 flush 하지 않고 i-cache만 flush하도록 iflush: 루틴으로 이동한다.
  • mrc p15, 0, r10, c0, c1, 5
    • ID.MMFR1.L1_Harvard_cache 설정 상태를 읽어온다.
      • ARMv7은 hierarchical 캐시를 사용하므로 값이 0으로 설정되어 있다.
  • mov r10, #0
    • 아래 mcr 명령을 수행할 일이 있는 경우 register dependency 기법을 이용하여 CPU의 in-order execution을 보장하게 한다.
  • beq hierarchical
    • ARMv7 의 모든 캐시들은 hierarchical 캐시를 사용하는 것으로 알려져 있다. 따라서 모두 이루틴으로 진입한다.
    • 2015년 1월 22일 이 부분의 패치가 제출되어 있는 상태로 아직 메인 스트림에 반영되지 않고 있다.
  • mcr p15, 0, r10, c7, c14, 0
    • clean & invalidate d-cache all을 수행하는 명령이지만 ARMv7은 모두 hierarchical 캐시를 사용하므로 이 루틴으로 들어올 일이 없다.

 

hierarchical:

  • 캐시 레벨 관리
    • ARMv7 이전의 아키텍처는 직접적으로 관리하는  캐시가 L1 밖에 없었다.
    • ARMv7 아키텍처가 만들어지면서 다중 캐시를 지원하게 되었다.
      • 캐시는 Level 1부터 Level 7까지의 정보로 구성될 수 있는데 아키텍처나 SoC를 제조하는 회사마다 캐시의 구성방법과 LoC(Level of Cache Coherency)가 모두 다르다.
  • 캐시 flush 방법
    • ARMv7 이전의 아키텍처
      • L1 i-cache는 한 번의 명령으로 삭제 가능
      • L1 d-cache는 한 번의 명령으로 삭제 가능
    • ARMv7 아키텍처
      • 각 레벨별로 i-cache를 한 번의 명령으로 삭제 가능
      • 각 레벨별로 d-cache를 한 번의 명령으로 삭제 불가능
        • 모든 캐시 레벨에 대해 set/way 방식으로 하나씩 지워나간다.
        • 각 cache line 하나에 대해 한 번의 명령으로 삭제하므로 모두를 지우기 까지 상당한 overhead가 소모되는 것이 단점이다. (최대 수 ms 시간 소모)
hierarchical:
                mcr     p15, 0, r10, c7, c10, 5 @ DMB
                stmfd   sp!, {r0-r7, r9-r11}
                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
  • mcr p15, 0, r10, c7, c10, 5
    • 캐시를 set/way 방식으로 하나씩 지워나가기 전에 DMB (Data Memory Barrier operation)를 사용하여 이미 사용중인 memory 작업이 완료할 때까지 기다린다.
  • stmfd sp!, {r0-r7, r9-r11}
    • 이 루틴이 동작하는 동안 레지스터들을 보호하기 대부분의 레지스터들을 백업해둔다.
  •  mrc   p15, 1, r0, c0, c0, 1
    • CLIDR(Cache Level ID Register)를 사용하여 캐시 레벨 정보를 읽어온다.
  • ands r3, r0, #0x7000000
    • LoC가 담긴 비트들만 and 연산으로 확보한다.
    • pi2: LoC=2 (Level 2 Coherency를 가지고 있다.)
  • mov r3, r3, lsr #23
    • 잃어온 LoC 값을 24비트 우측으로 쉬프트하지 않고 23비트만큼만 우측으로 쉬프트한다.
    • r3 = LoC x 2와 동일하다.
      • rpi2:  r3=4
  • beq finished
    • LoC가 0이면 finish로 빠져나간다.
                mov     r10, #0                 @ start clean at cache level 0
loop1:
                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
  • mov r10, #0
    • 가장 바깥쪽 루프(loop1) 카운터로 사용되는 r10(캐시 레벨)을 0으로 설정한다.
      • 이 값은 2씩 증가하여 r3(LoC x 2)이 될 때까지 증가한다.
      • rpi2: 0(L1 캐시 삭제), 2(L2 캐시 삭제),  4(루프 종료)
  • add     r2, r10, r10, lsr #1
    • r2: 해당 캐시레벨의 타입을 가져오기 위해 3bit씩 위치를 곱한다.
    • 0, 3, 6, 9, …씩 증가
  • mov     r1, r0, lsr r2
    • r1 = r0(CLIDR) 값을 r2만큼 우측으로 쉬프트하면 해당 캐시 레벨 타입 정보가 있는 곳까지 쉬프트한다.
  • and r1, r1, #7
    • 캐시 타입 정보 비트는 3비트로 구성되어 있다.
  • cmp r1, #2
    • r1: 캐시 타입 정보
      • 0=0=no cache
      • 1=instrunction cache only
      • 2=data cache only
      • 3=seperate inst & data cache
      • 4=unified cache (inst + data)
      • 5~7=reserved
  • blt skip
    • r1(현재 캐시 레벨이 지원하는 캐시 타입)이 2보다 작으면 d-cache가 없으므로 skip으로 이동
    • d-cache에서만 루프에서 flush 작업을 수행할 예정이다.

 

                mcr     p15, 2, r10, c0, c0, 0  @ select current cache level in cssr 
                mcr     p15, 0, r10, c7, c5, 4  @ isb to sych the new cssr&csidr
                mrc     p15, 1, r1, c0, c0, 0   @ read the new csidr
                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
  • mcr p15, 2, r10, c0, c0, 0
    • r1: CCSIDR을 읽어온다.
    • 선택된 캐시의 정보가 담김.
      • WT(Write Through) 지원 여부 bit
      • WB (Write Back) 지원 여부 bit
      • RA(Read Allocation) 지원 여부 bit
      • WA(Write Allocation) 지원 여부 bit
      • LineSize: cache line 바이트 수
        • 1=8 words, 2=16 bytes, 3=32 words, …
        • rpi2: 2 (L1 & L2 data cache line size = 16 words)
      • Associativity
        • way 수 – 1
        • rpi2: L1 i-cache=1(2 way), L1 d-cache=3(4 way), L2 cache=7(8 way)
      • Numsets
        • Set(index) 수 – 1
        • rpi2: L1 i-cache=0x1ff, L1 d-cache=0x7f, L2 d-cache=0x3ff
  • and r2, r1, #7
    • cache line 정보 비트를 읽어온다.
  • add r2, r2, #4
    • r2 += 4를 취한다.
    • 5=8 words, 6=16 words, 7=32 words, …
    • rp2: 6
  • ldr r4, =0x3ff
    • associativity(way – 1) 추출을 위한 비트마스크를 0x3ff로 한다.
  • ands r4, r4, r1, lsr #3
    • r4 = loop2용 max associativity(way – 1) 값
    • r1(CLIDR)값을 우측으로 3 쉬프트하여 associativity(way – 1) 값 만을 읽어온다.
    • rpi2: L1 d-cache=0x7f, L2 d-cache=0x3ff
  • clz r5, r4
    • r5: clz: MSB(최상위 비트)부터 시작하여 비트가 0인 갯수를 알아낸다.
      • 예) 0b 00000000 00000000 00000000 00000011 -> 30
  • ldr r7, =0x7fff
    • NumSets(index-1) 추출을 위한 비트마스크를 0x7fff로 한다.
  • ands r7, r7, r1, lsr #13
    • r7: loop2용 NumSets(index-1) 카운터
      • for (r7 = Max NumSets(r1에서 추출) ; r7 >= 0; r7–)
    • rpi2:
      • L1 d-cache: 0x7f, 0x7e, ……, 0까지 루프 수행(-1이면 루프2 탈출)
      • L2 d-cache: 0x3ff, 0x3fe, ……, 0까지 루프 수행(-1이면 루프2 탈출)
loop2:
                mov     r9, r4                  @ create working copy of max way size 
loop3:
 ARM(           orr     r11, r10, r9, lsl r5    ) @ factor way and cache number into r11
 ARM(           orr     r11, r11, r7, lsl r2    ) @ factor index number into r11
 THUMB(         lsl     r6, r9, r5              )
 THUMB(         orr     r11, r10, r6            ) @ factor way and cache number into r11
 THUMB(         lsl     r6, r7, 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 way
                bge     loop3
                subs    r7, r7, #1              @ decrement the index
                bge     loop2
  • mov r9, r4
    • r9(loop3용 way 카운터): r4(현재 캐시 레벨의 way 값-1)를 사용하여 0까지 루프를 돈다.
      • for (r9 = r4; r9 >= 0; r9–)
    • rpi2:
      • L1 d-cache: 3, 2, 1, 0까지 루프 수행(-1이면 루프3 탈출)
      • L2 d-cache: 7, 6, …, 0까지 루프 수행(-1이면 루프3 탈출)
  • orr r11, r10, r9, lsl r5
    • r11: DCCISW 캐시 삭제 명령을 수행하기 위해 필요한 r9(Way) 및 r10(캐시 레벨값)을 저장.
  • orr r11, r11, r7, lsl r2
    • r11: DCCISW 캐시 삭제 명령을 수행하기 위해 필요한 r7(Set)값을 저장.
  • mcr p15, 0, r11, c7, c14, 2
    • DCCISW(Data Cache Clean & Invalidate by Set/Way)를 사용하여 캐시 한 라인을 삭제
  • subs r9, r9, #1
    • way 카운터를 1 감소
  • bge loop3
    • r9 값이 0보다 같거나 크면 loop3로 다시 반복
  • subs r7, r7, #1
    • NumSets 카운터를 1 감수
  • bge loop2
    • r7 값이 0보다 같거나 크면 loop2로 다시 반복
skip:
                add     r10, r10, #2            @ increment cache number
                cmp     r3, r10
                bgt     loop1
finished:
                ldmfd   sp!, {r0-r7, r9-r11}
                mov     r10, #0                 @ swith back to cache level 0
                mcr     p15, 2, r10, c0, c0, 0  @ select current cache level in cssr
  • add r10, r10, #2
    • r10(캐시 레벨 카운터)을 2 증가 시킨다.
  • cmp r3, r10
    • r3(Loc x 2)와 r10(캐시 레벨 카운터)를 비교
  • bgt loop1
    • r3(Loc x 2)가 r10(캐시레벨 카운터)보다 큰 경우 loop1으로 다시 반복
    • rpi2: LoC=L2 이므로 L1에서 Loc(L2) 캐시 레벨까지 수행한다. (두 번의 루프가 수행)
  • ldmfd sp!, {r0-r7, r9-r11}
    • 백업해두었던 레지스터들을 복원한다.
  • mov r10, #0
    • 동작할 캐시 레벨을 다시 L1(0) 처음으로 돌리려한다.
  • mcr p15, 2, r10, c0, c0, 0
    • CSSELR 레지스터로 동작할 캐시 레벨을 지정한다.

iflush:

i-cache는 한 번에 flush 한다.

iflush:
                mcr     p15, 0, r10, c7, c10, 4 @ DSB
                mcr     p15, 0, r10, c7, c5, 0  @ invalidate I+BTB
                mcr     p15, 0, r10, c7, c10, 4 @ DSB
                mcr     p15, 0, r10, c7, c5, 4  @ ISB
                mov     pc, lr
  • mcr p15, 0, r10, c7, c10, 4
    • DSB (Data Synchronization Barrier operation)를 사용하여 이미 동작중인 모든 캐시 조작 명령이 완료될 때 까지 기다린다.
  • mcr p15, 0, r10, c7, c5, 0
    • ICIALLU(Instruction Cache Invalidate ALL for LoU)를 사용하여 i-cache를 모두 비운다.
  • mcr p15, 0, r10, c7, c10, 4
    • DSB
  • mcr p15, 0, r10, c7, c5, 4
    • ISB(Instruction Synchronization Barrier operation)를 사용하여 명령 파이프 라인을 비운다.
  • mov pc, lr
    • 호출한 곳으로 리턴한다.

 

참고

compressed/head.S – start: – 1st restart ~ relocation 수행 과정

DTB 및 재배치 코드 보호

다음 코드는 relocation을 하기 위해 필요한 주소들을 계산한다.

code relocation은 r6(source 상단)부터 r5(source 하단)로 32바이트씩 읽어서 r9(dest 상단)에 복사하면서 내려오는데 각 주소는 다음과 같이 복잡하다. 간단히 알아보고 실제 계산은 각 코드에서 비교를 해야 정확히 알 수 있다.

  • N=복사할 relocation 코드 사이즈
    • (_edata + DTB size – r5)한 사이즈를 상향으로 32바이트 align한 주소
  • r5: source 하단 주소
    • compressed 커널의 restart 주소를 32바이트 align한 주소
  • r6: source 상단 주소
    • r5 + N(복사할 relocation 코드 사이즈)한 주소를 상향으로 32 byte  align 한 주소
  • r9: dest 상단 주소
    • r10(decompressed 커널의 시작) + A + B + C + N
      • A=decompressed 커널 사이즈
      • B=bss가 DTB 영역을 침해하지 않도록 추가한 사이즈
        • bss가 _edata – wont_overwrite 영역을 초과하는 사이즈
      • C=재배치 수행 중 재배치 코드가 파괴되지 않을 만큼의 사이즈
        • reloc_code_end – restart 만큼의 사이즈를 상향으로 256 byte align 한 사이즈
/*
 * Relocate ourselves past the end of the decompressed kernel.
 *   r6  = _edata
 *   r10 = end of the decompressed kernel
 * Because we always copy ahead, we need to do it from the end and go
 * backward in case the source and destination overlap.
 */
                /*
                 * Bump to the next 256-byte boundary with the size of
                 * the relocation code added. This avoids overwriting
                 * ourself when the offset is small.
                 */
                add     r10, r10, #((reloc_code_end - restart + 256) & ~255)
                bic     r10, r10, #255 

                /* Get start of code we want to copy and align it down. */
                adr     r5, restart
                bic     r5, r5, #31

/* Relocate the hyp vector base if necessary */
#ifdef CONFIG_ARM_VIRT_EXT
                mrs     r0, spsr 
                and     r0, r0, #MODE_MASK
                cmp     r0, #HYP_MODE
                bne     1f   

                bl      __hyp_get_vectors
                sub     r0, r0, r5
                add     r0, r0, r10
                bl      __hyp_set_vectors
1:
#endif

                sub     r9, r6, r5              @ size to copy 
                add     r9, r9, #31             @ rounded up to a multiple
                bic     r9, r9, #31             @ ... of 32 bytes
                add     r6, r9, r5
                add     r9, r9, r10
  • decompressed 커널의 끝 주소(위 그림에서의 space B 포함)에 일정 부분의 공간을 추가한다.
    •  기존에는 decompressed 커널의 끝에 256바이트의 space 공간을 두고 relocation되는 compressed 커널이 놓이게 되었었는데 compressed 커널의 시작 주소가 relocation되는 곳의 시작주소의 바로 밑에 바짝 붙어 있는 경우 32바이트씩 copy하면서 overwrite될 가능성과 cache flush code에서 문제가 발생할 확률이 있어서 아예 이슈가 될만한 사이즈만큼 더 띄우게 되었다.
    • add r10, r10, #((reloc_code_end – restart + 256) & ~255)
      • r10: 한 번 교정된 decompressed kernel의 마지막 주소에다가 relocation 전용 코드 길이만큼 추가한다.
        • reloc_code_end 주소 – restart 영역에 256 바이트를 더한 후 256 byte align한 사이즈이다.
        • rpi2:
          • r10 += 0x900((0x928 – 0xc0 + 0x100) & 0xffffff00)
          • 0x900 만큼의 space를 두었다.
    • bic r10, r10, #255
      • 256바이트 align한 주소가 최종적으로 relocated compressed 커널 영역의 시작 주소가 된다.
  • adr r5, restart
    • r5: compressed 커널의 restart 주소를 읽어온다.
  • bic r5, r5, #31
    • restart 주소의 32바이트 align 주소를 compressed 커널에서 복사할 영역의 시작 주소로 한다.
    • 32바이트씩 복사할 수 있도록 32byte align한다.
  • 하이퍼바이저용 벡터 베이스를 위치가 바뀔 offset 만큼 재 설정한다.
    • bl __hyp_get_vectors
      • 하이퍼바이저용 벡터 베이스 주소를 가져온다.
    • sub r0, r0, r5
      • compressed 커널이 relocation될 예정이므로 하이퍼바이저용 벡터 베이스에서 먼저 r5(compressed 커널에서 relocation할 코드 시작 주소)만큼 뺀다.
    • add r0, r0, r10
      • 다시 r10(compressed kernel이 relocation 되는 곳의 시작 주소) 만큼 더해 offset 조정을 완료한다.
    • bl __hyp_set_vectors
      • offset 조정된 주소를 다시 하이퍼바이저 벡터 베이스값으로 지정한다.
  • r9(relocation될 곳의 relocation 영역의 끝 주소)과 r6(compress 커널에서 relocation 코드 영역의 끝 주소)를 계산한다.
    • sub r9, r6, r5
    • add r9, r9, 31
    • bic r9, r9, 31
      • r9 = round_up(r6(_edata + DTB size) + 31 후 32바이트 align
      • 여기까지 계산된 r9 값이 복사할 사이즈이다.
    • add r6, r9, r5
      • r6 = r9(복사할 사이즈) + r5(compressed 커널에서 relocation 영역의 시작 주소)
      • 마지막으로 r6에 compress 커널에서 relocation 영역의 끝 주소가 지정된다.
    • add r9, r9, r10
      • r9 +=  r10(2 개의 추가 영역을 더한 decompressed 커널의 끝 주소)
      • 마지막으로 r9에 decompressed 커널의 reloocation 될 영역 끝 주소가 지정된다.
  • r9: relocation될 곳의 relocation 코드 주소의 끝을 계산한다.
    • 32바이트씩 복사하기 위해 r9 값을 32byte round up 하여 align 한다.
  • add r6, r9, r5
    • r6 = r9(compressed 커널에서 relocation할 코드의 끝 주소) + r5(compressed 커널에서 relocation할 코드 시작 주소)

재배치 코드 보호

 

decompress_head.s분석11

relocation 진행

decompress_head.s분석4f

1:              ldmdb   r6!, {r0 - r3, r10 - r12, lr}
                cmp     r6, r5
                stmdb   r9!, {r0 - r3, r10 - r12, lr}
                bhi     1b
  • r6주소로부터 32바이트씩 읽어 r9 주소에 복사한다.
  • r6주소와 r9주소는 4바이트씩 감소한다.
  • r6 주소가 r5보다 큰동안 루프를 돌며 반복한다.

relocation 완료 후 restart로 다시 이동

                /* Preserve offset to relocated code. */
                sub     r6, r9, r6

#ifndef CONFIG_ZBOOT_ROM
                /* cache_clean_flush may use the stack, so relocate it */
                add     sp, sp, r6
#endif

                bl      cache_clean_flush

                adr     r0, BSYM(restart)
                add     r0, r0, r6
                mov     pc, r0
  • sub r6, r9, r6
    • r6에는 offset이 담긴다.
    • 이 offset을 이용하여 잠시 뒤 재배치된 restart 루틴으로 점프해야 할 때 사용된다
  • add sp, sp, r6
    • ROM에서 동작한 경우가 아니면 스택 위치를 조정한다.
    • 캐시 클린 플러쉬 루틴을 이용하려면 스택이 필요해서 재배치 코드의 위쪽으로 스택을 설정한다.
  • bl cache_clean_flush
    • 캐시를 비운다.
  • 이 루틴에서 restart: 레이블로 점프한다.
    • relocation이 완료되었으므로 다시 restart 부터 영역 비교를 한다.

 

참고