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 코드가 끝나는 주소가 담겨있다.

 

 

4 thoughts on “kernel/head.S – __create_page_table:

  1. 안녕하세요, 항상 잘 보고 참조하고있습니다.
    다름이 아니라 그림에 대해서 의문점이 생겨서 댓글을 남기게 되었습니다.
    위 그림에서 “kernel/head.S – 페이지 테이블 구성 Diagram” 이 부분의 그림을 보면,
    __turn_mmu_on ~ _turn_mmu_on_end 의 코드를 identical mapping을 하려고 하고 있으며,
    그림에서 표현하신 바에 의하면, __turn_mmu_on(0x8054_db28) 과 _turn_mmu_on_end(0x8054_db08)이 페이지테이블에 기록 될때,
    PA에 해당하는 12비트는 0x805 라고 써질 것이라고 생각이 드는데 0x005로 써져 있어서 어느 것이 맞는지 의문이 듭니다.
    코드를 참조해보자면,
    add r5, r5, r0 @ phys __turn_mmu_on // 이 부분을 통해서 r5는 0x8054_db28이라는 물리주소를 갖게 될것이라고 생각하고,
    mov r5, r5, lsr #SECTION_SHIFT // 이 부분을 통해서 r5는 0x805의 값을 갖게 될 것이고,
    orr r3, r7, r5, lsl #SECTION_SHIFT @ flags + kernel base //실제로 페이지테이블에 써지는 값인 r3에서 PA는 0x805를 갖게 될 것이라고 생각합니다.

    항상 공부하는데 도움이 되고 있고, 감사합니다

  2. 먼저 도움이 된다니 다행입니다.
    __turn_mmu_on_loc: 레이블에 위치한 3개의 .long 값들은 차례로
    1) 컴파일 당시 진행 중인 현재 가상 주소,
    2) __turn_mmu_on: 레이블이 위치한 가상 주소,
    3) __turn_mmu_on_end: 레이블이 위치한 가상 주소가 담깁니다.

    그리고 mov r5, r5, r0에 의해 물리 주소 값으로 변환되게 됩니다.
    물론 r0에는 가상 주소를 물리 주소로 바꿀 수 있도록 delta offset 값을 가지게 됩니다. (rpi2: r0=0x8000_0000)
    결국 __turn_mmu_on: 레이블의 가상주소가 0x8054_db28일 경우 delta offset 값 0x8000_0000을 더해 물리주소인 0x0000_db28로 변경됩니다.
    (실제 rpi2의 arm core 입장에서 DRAM 물리주소는 0x0000_0000 입니다.)

  3. offset에 대해 조금 더 보충을 하자면
    1) adr r0, __turn_mmu_on_loc 명령을 통해 r0에는 __turn_mmu_on_loc: 레이블의 물리 주소를 알아온다.
    2) ldmia r0, {r3, r5, r6} 명령을 통해 물리 주소 r0의 3개의 워드를 각각 r3, r5, r6로 읽어들인다.
    3) sub r0, r0, r3 명령을 통해 물리 주소 r0 – 가상 주소 r3 을 빼면 가상 주소 < -> 물리 주소를 변환할 떄 사용할 수 있는 offset 값을 얻을 수 있다.

  4. 아 저는 물리주소가 0x8054_db28 (가상 주소 + delta_offset)인줄 알았습니다.

    그림을 좀더 자세하게 봤어야 했는데 감사합니다!

답글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.