이 글에서는 리눅스 커널이 빌드 된 후, 커널 이미지의 헤더에 대해서 알아보도록 한다.

임베디드 환경(통상 ARM Architecture)에서,리눅스 커널과 같은 운영체제는 메모리에 로드가 된 후 실행된다.
그렇다면 여기서!, 드는 질문 2가지?!
  • 메모리에 적재시켜주는데... 메모리 몇 번지에 로드시켜야 하는가?
  • 커널 이미지는 누가 메모리에 로드시켜 주는가?
첫 번째 질문에 대한 대답을 먼저 하면..

 리눅스 커널은 PIC(Position Independent Code)라고 한다. 말 그대로 어느 메모리에 올려도 자기가 알아서 원하는 메모리 주소로 Copy해서 동작할 줄 아는 소프트웨어이다.(물론, 프로그램이 정상적으로 동작 할 수 있는 메모리여야 한다)

 결론적으로, 그래서 딱히 어느 메모리에 로드웨어야 한다 이런것은 없다.얕지만 알고 있는 경험에 의하면, 이미지를 로드하는 메모리 주소가 Align(이 부분에 대해서는 나중에 글을 올릴 예정이다)만 되어있다면 어느 주소에도 동작하는 것을 확인했었다. arm32 에서는 zImage가 원하는 주소에 kernel image를 압축해제 했었다.

두 번째 질문에 대한 대답을 하면..

 커널을 메모리에 올려주는 놈은 부트로더(Bootloader)라고 불리는 소프트웨어가 해준다. 보드를 몇 번 실행해봤다면 흔히 들어봤을 법한 U-boot, LK(Little Kernel), UEFI 이런 놈들이 부트로더에 해당된다.

부트로더도 크게는 First-bootloader, Second-boorloader 로 나눠진다. 간랸히 설명해보면 First-Bootloader는 ATF(ARM Trusted Firmware)에 해당하는 보드를 만드는 vendor사에서 만드는 펌웨어로 보드를 부팅시키는 놈이다.(사실 하나의 소프트웨어가 아니라 여러개로 나뉘어져있다.)
Second-Booloader가 위에서 언급한 부트로더들이다.

이로써 두 개의 질문과 답을 마치고 이어서 이 글의 주제로 넘어와 보면,(지금부터 말하는 부트로더는 Second-Bootloader 이다.)
부트로더는 크게 2개의 역할로 볼 수 있다.(물론 세부적으로는 많은 일들을 하지만 )
첫 번째는 보드의 디바이스, 메모리 초기화. 두 번째는 운영체제 메모리에 적재이다. 그럼 이 때 부트로더는 내가 로드하고 있는 이 운영체제가 어떠한 운영체제인지, 어떠한 프로토콜(운영체제마다 다르겠지만 리눅스에서는 레지스터에 값을 전달해주는 정도를 뜻한다)을 따라주어야 하는지를 먼저 알아야 한다. 그 때 하는 것이 리눅스커널 이미지를 읽어보는 것이다. (전부다는 아니고 처음 시작 부분을 읽어본다)

그럼 리눅스 커널은 부트로더에게 "내가 커널이미지야!" 라고 말해주는 역할로 헤더를 만들어놓았다. Header에 대한 자세한 설명은 참조[1]에 있지만 간략히 설명해보자면 아래와 같이 생겼다.

u32 code0;                            /* Executable code */

u32 code1;                            /* Executable code */

u64 text_offset;                     /* Image load offset, little endian */

u64 image_size;                     /* Effective Image size, little endian */

u64 flags;                               /* kernel flags, little endian */

u64 res2  = 0;                        /* reserved */

u64 res3  = 0;                        /* reserved */

u64 res4  = 0;                        /* reserved */

u32 magic = 0x644d5241;   /* Magic number, little endian, "ARM\x64" */

u32 res5;                               /* reserved (used for PE COFF offset) */


그리고 실제로 커널의 코드를 통해서 이 헤더가 구현되어있는 모습을 보자면, (단 EFI가 아니라 Uboot를 위한 헤더라고 가정한다)


b   stext                                      // branch to kernel start, magic

.long   0                                     // reserved

le64sym _kernel_offset_le       // Image load offset from start of RAM, little-endian

le64sym _kernel_size_le          // Effective size of kernel image, little-endian

le64sym _kernel_flags_le        // Informative flags, little-endian

.quad   0                                            // reserved

.quad   0                                            // reserved

.quad   0                                            // reserved

.ascii  ARM64_IMAGE_MAGIC       // Magic number

.long   0                                             // reserved

                                         <linux/arch/arm64/kernel/head.S>


이 헤더는 리눅스 커널 이미지의 처음 시작 64byte 값이다. 부트로더 코드를 읽어보지는 않았지만, 일단 ARM64_IMAGE_MAGIC("ARM\x64") String값을 읽고 일단 리눅스 커널이라는 것을 알아낸 후 사이즈에 대한 정보를 통해서 메모리에 적재하는 것으로 추정된다. 그리고 메모리에 올린 후 제어권을 리눅스 커널로 넘겨주어야 하는데 이 때 아래와 같은 값을 각각 레지스터에 써서 넘겨준다. (이 때, 제어권을 넘겨준다는 말은 PC값을 이미지의 처음 주소로 assign한다는 의미가 주된 의미지만 필요시 CPU mode를 바꿔줌으로써 넘겨준다.)


- Primary CPU general-purpose register settings
  x0 = physical address of device tree blob (dtb) in system RAM.
  x1 = 0 (reserved for future use)
  x2 = 0 (reserved for future use)
  x3 = 0 (reserved for future use)

(DTB가 무엇인지 대한 설명은 다른 글에서 하도록 한다...하 씨발 쓸거 개 많네)

이 글에서는 리눅스 커널 이미지에는 헤더라는게 왜 있어야 하는지에 대한 배경설명과, 헤더의 모양은 어떠한지 그리고 커널의 실제 헤더의 값은 어떻게 되어있는지에 대해서 보았다.







참조:
[1] linux/Documentation/arm64/booting.txt


+ Recent posts