• Wonhyuk Yang

[Training] Arm v8 Linux kernel head.S 찍어먹기 (1)

최종 수정일: 2021년 6월 19일


Linux kernel을 처음 분석하는 분들은 대부분 head.S에서 시작할 것 같은데요, 아무 것도 모르는 상태에서 시작하면 어셈블리어의 압박도 있지만 생소한 설정 뿐만 아니라 아직 모르는 것들이 중간 중간 나와 분석을 하는데에 어려움이 있어요.

해당 글을 통해, start_kernel로 빠르게 진입할 수 있도록 head.S의 핵심적인 부분만 "찍먹"해봐요!

해당 글의 타겟 아키텍처는 aarch64이고, kernel code는 5.1 버전을 다룹니다. 유의하세요!

Prequisite

시작하기에 앞서, 먼저 읽어봐야 할 문서가 있습니다(커널 분석은 자료 읽는 과정이 절반 이상인 것 같네요...). booting.txt에는 부트로더가 반드시 해줘야 할 일의 목록이 있어요. grub 또는 uboot 등등의 다른 종류의 bootloader가 사용되더라도, 약속된 일을 해준다면 커널은 실행될 수 있어요.


이제 부트로더가 반드시 해줘야 할 일에 대해서 살펴봐요.

  1. Setup and initialise the RAM

  2. Setup the device tree

  3. Decompress the kernel image(Optional)

  4. Call the kernel image

위 목록 중에서 유심히 봐야할 항목들은 2번4번이에요. 2번에서 설명된 것처럼 부트로더는 메모리의 적절한 위치에 디바이스 트리를 올려요. 디바이스 트리는 하드웨어를 기술한 자료 구조인데, 쉽게 말해 실행하는 하드웨의 DRAM의 주소는 어디있고 크기는 얼마이고, CPU는 어떤 것인지 등등 하드웨어에 대한 자세한 설명이 담겨 있어요. 디바이스 트리에 대한 자세한 설명은 다른 글에서 하도록 할게요. 그런 다음 4번에서 kernel image를 호출하여 제어를 커널에 넘겨주면서 head.S가 실행되어요!


부트로더는 커널 이미지를 올릴 메모리의 적절한 위치를 찾는데, 아무 주소에 다 올릴 수 있는 것은 아니고 2MB aligned 된 주소여야만 해요. 또한 커널 이미지의 헤더는 아래와 같이 정의된 포맷을 가져야 해요.

위의 헤더 중 text_offset는, 부트로더가 선택한 주소(2MB aligned)에서 얼마 만큼 떨어져서 이미지를 로드할지를 알려주는 값이에요.


부트로더는 커널 이미지를 로드하고, 그 외 필요한 일을 다하고 커널 이미지 헤더의 code0으로 jump하게 되요. 이때 대략적인 CPU의 상태는 아래와 같아요.

  1. X0 레지스터에 디바이스 트리가 위치한 물리 주소를 저장한다.

  2. MMU는 off된 상태이다.

이 외에도 캐쉬와 관련된 내용이 있지만 아직은 다룰 때가 아닌 것 같네요. 다시 앞에서 얘기한 내용을 정리해서 그림으로 표현하면 아래와 같아요.

부트로더는 메모리에 커널 이미지와, 디바이스 트리가 올리고, 커널 이미지 헤더로 제어를 넘겨주고 head.S가 실행되는 거에요.


QEMU를 통해 부트 로더 맛보기

위에서 설명한 내용으로 부트로더가 어떻게 동작하는지 직관적으로 이해가시나요? 커널을 들여다 볼 결심을 한 여러분이라면 추상적인 설명으로는 부족할 것 같아요. 그래서 qemu의 소스를 살짝 들여다 보면서 정말 약속에 맞게 일을 하는지 살펴봐요.

해당 함수는 QEMU를 실행시킬 때 Image 파일을 인자로 주면 호출되는 함수에요. QEMU를 공부하는게 목적이 아니므로 큰 흐름만 따라갈게요.

  • Line 10: load_image_gzipped_buffer 함수를 통해 이미지를 압축 해제하고 buffer에 담아요

  • Line 18~19: 해당 라인에서 앞서 본 헤더의 매직 넘버("arm\x64")를 확인해요.

  • Line 26: 해당 라인에서는 hdrvals[2]에 헤더의 text_offset과 image_size를 복사해와요.

  • Line 45: 이제 찾은 2MB aligned된 주소 mem_base와 text_offset을 합쳐 entry point를 계산해요.

  • Line 46: entry의 주소에 buffer에 받은 이미지 파일을 복사해요

위 과정을 거친 후, entry point로 점프하면서 본격적으로 커널이 실행해요!


이제 정말 앞에서 말한대로 동작하는지 확인해볼까요? QEMU으로 virt board를 가상화할 건데 이 보드의 메모리 맵 구성은 아래와 같아요

해당 보드의 DRAM은 물리주소 0x40000000 번지에서 시작해요.


이제 위 구성을 기억하고 QEMU를 GDB를 통해 디버깅해서 커널 이미지가 어디에 로딩됬는지 확인해본 결과는 아래와 같아요. QEMU와 GDB를 이용한 디버깅은 다른 글 자세히 다룰게요.


디버거를 연결하면 0x400000000에서 시작하게 되는데 이 주소에 있는 코드는 QEMU가 복사한 부팅 코드들이에요. 해당 코드를 진행하면 10번 라인에서 0x40080000(head.S)로 점프해요. 해당 주소의 메모리 값을 덤프하면 이것들이 헤더를 의미한다는 것을 확인할 수 있어요, text_offset은 0x0080000으로 설정되어 0x4000000에서 0x0080000 만큼 떨어진 곳에 이미지가 로드되었어요.


정리

우리는 head.S를 바로 진입하기 전에, 부트로더에서 커널로 제어권이 넘어가는 과정을 살펴봤어요. 커널 코드로 진입하기 전에 부트로더가 꼭 해줘야 하는 일들이 있었는데 이것들은 모르고 넘어가면 커널 코드를 분석하면 잘 이해가지 않는 부분이 있을 거에요. 그렇기 때문에 시간을 소요해서 몇 가지 포인트들에 대해 알아보았어요. 다음 시간에는 커널의 Linker script에 대해 가볍게 알아보는 시간을 가져볼게요.


Series

조회수 480회댓글 1개

관련 게시물

전체 보기