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

 

 

16 thoughts to “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)인줄 알았습니다.

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

  5. 궁금한 점이 있는데요

    __turn_mmu_on함수만 유독 이렇게 직접 맵핑하는 것은 mmu를 키기 전이기 때문에 램이 적으면 호출 불가능하기 때문에, 무조건적으로 맵핑하는게 맞는건가요?

    다른 함수들은 함수 이름으로 바로 점프하는 형태인데 굳이 이 것만 그렇게 하는 것도 이상하고,

    그보다 꼭 adr인스트럭션을 사용해야하는 이유가 있는건가요?

    답변해주시면 감사해용 ㅠㅠ

    1. 좋은 의견 주셨습니다.

      이 부분은 혼동하기 쉬운 부분인데 쉽게 설명하지 못했었습니다.

      turn_mmu_on ~ turn_mmu_on_end 레이블 범위의 코드는 MMU가 켜지는 아주 짧은 타임에 CPU의 address 시그널이 PA -> VA로 변경됩니다.
      조심해야 할 부분은 MMU를 on하여 PA -> VA로 변경한다고 해도 pc 레지스터는 0x80**_****으로 시작하는 가상 커널 주소로 이동하지 않습니다.
      (갑자기 매핑되지 않은 허공이 나타나므로 시스템이 다운됩니다. RAM의 크기와는 상관 없습니다.)

      즉 pc 레지스터는 기존에 순차적으로 이동하고 있던 PA(0x0054_db08 ~ 0x0054_db28) 주소 값을 그대로 사용하고 MMU를 on 하여 VA 주소로 변경되는 순간에도 연속하여 그 코드가 동작해야 합니다.

      결국 커널 전체 코드는 일반적인 1:1 매핑이 필요지만, 위의 MMU를 켜는 코드는 추가로 또 한 번의 특별한 PA=VA 1:1 identical 매핑이 아주 잠깐 필요해서 만들어야 합니다.

      ldr 명령이 컴파일 시 결정된 가상 주소(0x80**_****) 값을 사용하는 것에 비해 adr 명령은 런타임 시 adr 명령이 수행될 때 pc 레지스터를 기준으로 레이블 주소의 차이를 상대주소로 읽어오므로 코드가 relocation되는 경우를 위해 adr 명령을 사용해야 정확히 현재 pc 레지스터 주소를 기준으로 읽어오고자 하는 레이블의 변경된 주소를 읽어오기 위함입니다.

      1. 답변 감사합니다. 설명 들으니 알 것 같으면서도 헷갈리네요.
        제가 처음부터 소스를 다 보지 않아서 이해가 안 가는 건지

        우선, 부트로더가 물리메모리에 커널이미지를 정확히 초기에 어디에 어떤식으로 올리는지를 몰라서요
        (물리 주소 동작이라면 mmu on 전에 bl로 부르는 함수들을 작성한 코드들은 물리메모리의 낮은 주소로 컴파일 시 되었다고 치고)

        그 이후의 소스 코드들은 mmu on시 가상주소로 맵핑 될 테니 상관 없지만 turn_mmu_on은 pc가 변하기 전까지 원래 주소를 읽어야하므로

        저 위의 그림처럼 중복으로 맵핑이 되어있다는 건가요 ? (잠시동안)

        1. head.S 어셈블리 코드의 작성 기준은 relocation 가능한 코드로 작성되어 특정 address로 jump하지 않고 현재 수행 주소를 기준으로 상대 jump 만하게 되어 있습니다.
          그리고 rpi2에서 head.S 는 컴파일 시 전체 커널(head.S와 다르게 relocation이 아님)은 가상 커널 주소인 0x80**_****를 가정하여 컴파일됩니다.
          다만 처음 head.S 부분이 실행될 때에는 시스템의 물리 주소에서 (rpi2: 0x00**_0000)에서 먼저 실행되며,
          head.S 코드가 압축을 풀때 파괴될 가능성이 있는 경우 스스로 head.S 코드를 relocation(복사하여 이동)한 후 이동하여 실행됩니다.

          어찌하든 현재의 물리 주소(rpi2: 0x00**_****)에서 mmu_on 코드가 수행되는데 MMU가 켜져도 가상 주소(0x00**_****에 이 코드가 매핑되어 있어야 합니다.
          결국 MMU off -> on 순간에도 cpu가 계속하여 0x00**_**** 루틴을 수행할 수 있도록 하기 위함입니다. (잠시동안 사용하기 위해 2중 매핑합니다)

          헷갈리시면 안되는 것은 mmu on 할 때 0x00**_**** 주소를 실행하다가 갑자기 자동으로 매핑된 주소 0x80**_**** 위치로 이동하지 않는다는 점입니다

          1. 그렇군요 저도 relocation 가능하게 짜지겠지 하면서도 실제로 덤프하거나 한 것은 아니라

            어셈블러가 소스를 어떻게 변경했을지 보고 싶은데 아쉽네요(물론, 아직 relocation이해가 부족)

            갑자기 이동하는것 보단 일단 2중 맵핑이라고 하면 분명 fault가 발생할 터인데;;
            인터럽트가 금지된 상태여서 그런가요… 상관이 없는지 ;

            답변 매번 감사드려요 바쁘실텐데

  6. 참고로 물리 메모리 페이지를 다중 매핑하여 사용해도 무방합니다.

    유저 태스크에서 사용하는 메모리 페이지들 중 다른 태스크와 공유되어 사용하는 페이지들이 있습니다.
    공유 메모리나 파일들을 여러 태스크에서 열어 사용할 경우인데 이 때 각 태스크에 다중 매핑됩니다.

    화이팅 하시기 바랍니다.

  7. 안녕하십니까 arm 리눅스 커널 2판 보면서 여기저기 뒤져보고있는 사람입니다.

    그럼
    idmap 페이지 테이블에도 1:1 매핑이되어야 하니까 create_block_map매크로를 써서 매핑할때

    가상주소 | 물리주소
    _enable_mmu 1번째코드 의 물리주소 _enable_mmu 1번째코드의 물리주소
    …..

    이런식으로 __enable_mmu()프로시져 끝까지 구성되는게 맞나요??

    1. 안녕하세요?

      이 페이지는 32비트 arm 시스템의 head.S 설명인데, 보신 책은 arm64입니다.
      create_block_map 매크로 <--- 어떤 커널 버전에서 보신 매크로인지 알려주실 수 있나요? 가상주소 | 물리주소 <--- 가상 주소와 물리 주소를 or로 처리하는 경우가 있나요? _enable_mmu 1번째코드 의 물리주소 _enable_mmu 1번째코드의 물리주소 <--- 정확한 설명이 없어서 다시 한 번 자세히 알려주시기 바랍니다. 감사합니다.

  8. 안녕하세요. 블로그 잘보고 있습니다. 덕분에 공부하는데 많은 시간이 줄어드는것 같습니다. 한가지 궁금한점이 있습니다.

    우선 idmap을 하는 이유는 head.S에서 mmu_on 하고 start_kernel로 이동하는 잠깐의 순간에도 pc는 물리주소를 가르키고있기 때문이라고 이해했습니다.

    그런데 여기서 질문이 있습니다.
    rpi Qemu 돌려보면 0x00**_**** 에서 mmu on하고 start_kernel 함수로 진입하면서 pc가 0x80**_****로 변하는것을 확인했습니다. 그러면 제 생각으로는 실제로 idmap을 사용하는것은 단지 몇개의 명령일것같은데 커널이미지 전체를 맵핑하는 이유가 무엇인지 궁금합니다.
    혹시 mmu_on하고 start_kernel로 점프하는것 이외에 물리주소와 동일한 가상주소 0x00**_****를 참조하는 경우가 있는건가요?

    감사합니다.

    1. 안녕하세요?

      승철님께서 생각하신대로 boot core가 mmu를 enable하고 start_kernel() 함수로 이동하기 직전 약간의 코드가 .idmap.text 섹션에 빌드됩니다.

      이외에도 다음과 같은 사용을 이유로 .idmap.text 섹션에 빌드됩니다.
      – seconary(두 번째 이상) cpu들이 bootup 시 사용하는 코드
      – cpu의 reset 및 sleep context에서 사용하는 코드

      .idmap.text 섹션에 위치한 코드 및 데이터는 mmu enable/disable 상태에서 VA=PA(가상 주소와 물리 주소가 같은)를 유지하도록 1:1 identity 매핑하여 사용합니다. 따라서 커널 전체가 아니라 약 4K 미만의 코드가 해당합니다.

      .idmap.text 섹션에 빌드되는 코드와 데이터는 다음과 같습니다. (커널 v5.4 기준)

      arch/arm64/head.S
      – kimage_vaddr
      – el2_setup
      – set_hcr
      – install_el2_stub
      – set_cpu_boot_mode_flag
      – secondary_holding_pen
      – pen
      – secondary_entry
      – secondary_startup
      – __secondary_switched
      – __secondary_too_slow
      – __enable_mmu
      – __cpu_secondary_check52bitva
      – __no_granule_support
      – __primary_switch

      arch/arm64/cpu-reset.S
      – __cpu_soft_restart

      arch/arm64/sleep.S
      – cpu_resume

      감사합니다.

      1. 아 ㅎㅎ 어느순간 커널이미지를 전부 1:1매핑한다고 착각을 했네요 ㅎㅎ
        감사합니다. 즐거운 연말 보내세요.

문영일에게 댓글 남기기 댓글 취소

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