이전 포스트에서 링커 스크립터를 이용하여 오브젝트 파일들을 내가 원하는 주소에 재배치하는 과정을 살펴봤어요. 그러면 이러한 재배치는 그냥 임의의 위치에 해도 되는 것일까요? 사실 가상 주소의 영역들은 특정 목적에 따라 배정되었어요. 이렇게 배정된 영역에 링커 스커립트는 오브젝트 파일들을 재배치할 뿐이지요. 이번 포스트에서는 커널의 가상 주소 영역은 어떻게 결정되는지 확인할 것이에요. 이제 본격적으로 시작해볼게요.
해당 글의 타겟 아키텍처는 aarch64이고, kernel code는 5.1 버전을 다룹니다. 유의하세요!
Virtual memory layout
지난 시간에 vmlinux.lds.S 파일을 잠깐 살펴봤을 텐데요, 호기심이 가득하신 분이라면 제일 head.S의 커널 이미지 헤더가 정말 이미지의 제일 앞에 배치되었는지 확인했을 것 같네요. 그래도 한 번 같이 확인할까요?
Line 3번에서 위치 카운터를 KIMAGE_VADDR + TEXT_OFFSET으로 변경해요. TEXT_OFFSET은 여러분이 생각하시는 부트 헤더에 기록한 test_offset과 동일한 것이에요. KIMAGE_VADDR은 아래에서 다룰게요~
Line 5~8번에서는 _text 심볼에 현재 위치를 할당하고, 바로 .head.text 섹션에 커널 이미지 헤더를 배치해요.
코드를 보시다시피, KIMAGE_VADDR란 녀석이 미리 결정되어있어서, 링커 스크립트는 해당 위치로 오브젝트 파일을 배치할 뿐에이요. 그럼 KIMAGE_VADDR은 어떻게 결정되느냐? 바로 arch/arm64/include/asm/memory.h 파일에서 리눅스의 VM Layout을 매크로로 정의해요! 그 때, KIMAGE_VADDR도 같이 정해지는 것이지요. 그러면 본격적으로 memory.h 파일을 살펴봐요~
단순히 매크로들의 나열이지만 상당히 중요한 의미를 가지고 있어요! 우선 순차적으로 보기보다는 중요한 것 먼저 살펴보도록 해요. 20번 라인에서 가장 중요햔 VA_BITS를 정의하는데요, 이 값은 .config 파일에서 설정되는 값이에요.
이처럼 CONFIG_ 접두사가 붙은 매크로는 .config 파일에서 정의되는 값이에요.
VA_BITS는 가상 주소 영역을 표현하는 bit의 수를 뜻해요, 해당 값이 줄어들면 그만큼 가상 주소의 영역도 줄어드는 것이에요. 64비트 시스템이라도 모든 비트를 사용하지 않고 일부만 사용해요. 보통 VA_BITS를 48bit를 설정해서 사용해요, 해당 비트로는 256TB만큼의 영역을 표현할 수 있어요.
그러면 가상 주소 표현에 사용되지 않은 비트들은 버려지는 것일까요? 이 질문에 답을 하기 위해선 ARM v8 architecture에 대한 지식이 필요해요! 해당 아키텍처는 두 개의 TTBR(Translation Table Base Register)를 가지고 있어요. 각각의 TTBR은 아래 그림처럼 상위 주소와 하위 주소의 변환을 담당해요.
위 그림처럼 64bit 주소 영역에서 상위 일부 영역과 하위 일부 영역만 가상 주소로 활용해요. 이때 상위 영역의 가상 주소의 Translation에 사용되는 레지스터는 TTBR1이라 하고, 하위 영역의 가상 주소의 Translation에 사용되는 레지스터는 TTBR0라 해요. 변환 범위를 벗어난 주소에 접근하게 되면 exception이 발생해요. 그러면 가상 주소 영역의 크기 어떻게 정해지는 거지 생각할 수 있는데, 범위는 TCR_ELx.TXSZ에 의해 결정되요. 아래 그림과 함께 볼까요?
해당 그림에서는,
TTBR0를 통해서 변환되는 주소의 범위는 0x00000000_00000000에서 0x0000FFFF_FFFFFFFF이고,
TTBR1를 통해서 변환되는 주소의 범위는 0xFFFF0000_00000000에서 0xFFFFFFFF_FFFFFFFF이에요.
이때, TCR_ELx.T0SZ와 TCR_ELx.T1SZ가 전부 16으로 정해져 있어요. 즉, 상위 16비트를 제외한 48비트를 변환에 사용하겠다는 의미지요. 해당 값을 줄이면 사용 가능한 가상 주소 영역이 늘어나겠지요. 이런 식으로, 48bit의 VA_BIT를 사용하면 상위 256TB와 하위 256TB를 사용한다는 말이에요.
자 이제 다시 memory.h 파일로 돌아갈게요. 이제 20번 라인과 21번 라인을 같이보면, VA_START는 TTBR1가 변환하는 시작 주소에 해당하는 것이 보이시나요? 그리고 22번 라인은 PAGE_OFFSET을 정의하는데, 상위 영역의 절반에 해당하는 주소이에요. 그림으로 표현하면 아래와 같아요.
PAGE_OFFSET은 굉장히 자주 등장하고, 비슷한 매크로가 많기 때문에 정확히 기억해야 해요.
위 그림에서는 VA_BITS가 48비트라 가정했기 때문에, VA_START에서 0xFFFFFFFF_FFFFFFFF는 256TB 만큼의 크기를 가져요. 또한 PAGE_OFFSET은 위 아래로 128TB만큼 2등분해요. 이제 VA_START와 PAGE_OFFSET를 기준으로 각각의 영역이 어떻게 배치되는지 살펴봐요. 알아보기 쉽게 코드의 순서를 재배치했어요.
VA_START를 기준으로 KASAN 영역, BFP 영역, MODULE 영역 그리고 KIMAGE 영역이 배치되었어요. PAGE_OFFSET 아래로는 VMEMMAP 영역, PCI_IO 영역, FIXMAP 영역이 배치되었어요. 그림으로 보면 아래와 같아요.
주의! 해당 그림은 영역의 정확한 크기를 보여주진 않아요.
이렇게 많은 영역 중, 유심히 봐야할 영역은 KIMAGE, VMEMMAP, PCIO_IO과 FIXMAP이 있는데요. 이 녀석들은 앞으로의 내용에서 등장하게 될 거에요. 눈치가 빠르신 분들은 일부로 PAGE_OFFSET 위의 영역은 사용하지 않는다는 점을 알아채셨을 것 같으데요, 미리 알려드리자면 해당 영역은 물리 메모리를 Linear mapping할 때 사용되는 영역이에요.
KIMAGE_VADDR은 MODULE 영역의 끝에 해당하는데, 여기에다 TEXT_OFFSET이 더해진 주소에 .head.text 섹션이 배치되는 것이지요. 저의 경우, 0xffff000010080000에 .head.text 섹션이 배치되었어요.(제 옵션은 아래와 같아요.)
CONFIG_ARM64_RANDOMIZE_TEXT_OFFSET=n
CONFIG_KASAN=n
CONFIG_ARM64_VA_BITS=48
하지만 QEMU로 디버깅했을 때, .head.text가 로딩된 물리 주소는 0x40080000 였는데요. 실제 로딩된 주소와 링크 과정에서 배치된 가상 주소가 달라요. 그러면 커널 이미지의 있는 C 코드들을 바로 실행할 수가 없어요. C로 작성된 코드를 실행하기 위해선 커널 이미지를 가상 주소와 연결해야만 돼요. 그래서 head.S에서는 커널 이미지의 물리 주소를 가상 주소에 매핑하는 Translation table을 작성하는 일을 진행해요. 또한 그런 Translation table을 만드는 코드들은 위치에 상관없이(Position independent) 동작하는 코드로 구성이 되어야겠지요. 그러면 다음 글에서는 본격적으로 head.S에서 Translation table을 만드는 일을 살펴볼게요.
정리
이번 시간에는 arm64의 memory.h 파일을 보면서 커널의 가상 영역이 어떤 식으로 미리 계산이 되었는지 살펴보았어요. 또한 ARM v8의 VM의 중요한 역할을 하는 TTBR과 TxSZ와 같은 레지스터들을 살펴보았어요. 실제 로딩된 물리 주소와 Linker script를 통해 재배치된 가상 주소와는 값이 서로 다를 수 밖에 없는데요, 그렇기 때문에 head.S에서는 페이지 테이블을 만드는 작업이 필요하다라는 전체적인 흐름을 예측할 수 있었어요.
Comments