1.4 Linux startup sequence
1.4: Linux startup sequence
Searching for the entry point
리눅스 프로젝트 구조를 살펴보고 어떻게 빌드가 되는 지 확인한 후 다음 단계는 프로그램 시작 포인트(entry point)를 찾는 것입니다. 이 단계는 많은 프로그램에서는 사소할 수 있지만 리눅스 커널에는 사소하지 않습니다.
우리가 할 첫 번째 일은 arm64 linker script를 살펴보는 것입니다. 링커 스크립트가 메인 Makefile에서 어떻게 사용되는지 이미 살펴봤습니다. 이 코드에서 쉽게 특정 아키텍처를 위한 링커 스크립트가 어디있는 지 쉽게 찾을 수 있다는 것을 추측 할 수 있습니다.
살펴볼 파일은 실제 링커 스크립트가 아니라는 것을 아셔야 합니다. - 이것은 하나의 템플릿이고 실제 링커 스크립트는 실제 값으로 몇 매크로들을 대체하여 생성됩니다. 그러나 이 파일은 대부분 매크로로 이뤄지기 때문에 정확하게는 다른 아키텍처 사이에서 포팅을 하고 읽기가 쉬워집니다.
링커 스크립트에서 찾을 수 있는 첫 번째 섹션은 .head.text.라 불립니다. 이 섹션에서 엔트리 포인트가 정의되어야 하기 때문에 매우 중요합니다. 만약 이것에 대해 조금 생각해 본다면 말이 될것입니다: 커널이 로드된 후 바이너리 이미지의 내용은 어떤 메모리 영역에 복사가 되고 그 영역에 처음부터 실행될 것 입니다. 이것은 누가 .head.text 섹션을 사용하는지 찾기만 하면 엔트리 포인트를 찾을 수 있다는 것을 의미합니다. 그리고 arm64 아키텍처는 __HEAD 매크로를 사용하는 하나의 파일을 가집니다. 이 파일은 .section ".head.text", "ax"로 확장됩니다 - 이 파일은 head.S입니다.
head.S파일에서 찾을 수 잇는 첫 번째 실행 가능한 코드는 이 것입니다. 여기서 stext 함수로 점프하기 위해서 branch 명령어의 b arm 어셈블러를 사용합니다. 그리고 커널이 부팅된 후 처음으로 실행되는 함수 이기도 합니다.
다음 단계는 stext 함수안에서 어떤 작업이 수행되는지 확인하는 것입니다.-하지만 아직 준비가 되지는 않았습니다. 우선 비슷한 기능을 RPi OS에서 구현을 해야합니다 그리고 다음 몇 레슨에서 이 부분을 다루도록 하겠습니다. 지금 당장 하려는 것은 몇가지 커널 부팅과 연관된 중요한 개념을 살펴 보는 것입니다.
Linux boot loader and boot protocol
리눅스 커널이 부팅될 때 기계 하드웨어가 어떤 “known state”에 준비가 되있다고 가정합니다. 이 상태를 정의하는 규칙의 집합은 “boot protocol”이라 불리고 arm64아키텍처를 위해서 여기 문서화 되있습니다. 예를 들어 다른 것들 중에서 실행이 CPU, MMU가 꺼지고 모든 인터럽트가 비활성화가 된 후에야 시작할 수 있다고 정의합니다.
Ok 하지만 그럼 누가 기계를 이러한 known state로 만드는 역할을 하는걸까?
보통 커널이 시작되기 전에 실행되고 모든 초기화를 진행하는 특별한 프로그램이 존재합니다. 이러한 프로그램을 bootloader라고 부릅니다. 부트로더 코드는 매우 기계의 특정적이고 라즈베리 파이도 그러합니다. 라즈베리 파이는 보드에 설치된 부트로더가 있습니다. 부트로더의 동작을 바꾸기 위해서는 오로지 config.txt 파일만을 사용할 수 있습니다.
### UEFI boot
그러나 커널 이미지 자체에 빌드될 수 잇는 부트로더가 있습니다. 이 부트로더는 Unified Extensible Firmware interface(UEFI)를 지원하는 플랫폼에서만 사용될 수 있습니다. UEFI를 지원하는 디바이스는 소프트웨어를 실행하는 표준화된 서비스 집합을 제공하고 이러한 서비스들은 기계 자체나 수용력에 대한 모든 필요한 정보를 채울 때 사용됩니다. UEFI는 또한 컴퓨터 펌웨어가 Portable Executable(PE) 포맷의 실행파일을 실행할 수 있어야 합니다. 리눅스 커널 UEFI 부트로더는 이 기능을 사용합니다: 리눅스 커널 이미지의 시작에 PE 헤더를 삽입하여 컴퓨터 펌웨어가 이 이미지를 평범한 PE 파일로 생각합니다. 이러한 것은 efi-header.S 파일에서 이뤄집니다. 이 파일은 head.S안에서 사용되는 __EFI_PE_HEADER 매크로를 정의합니다.
__EFI_PE_HEADER에서 정의된 중요한 재산(property) 하나는 UEFI 엔트리 포인트의 위치를 알려주는 것이고 이 엔트리 포인트 자체는 efi-entry.S에서 찾을 수 있습니다. 이 위치에서 시작해서 소스코드를 따라가 정확히 UEFI 부트로더가 어떤 일을 하는지 살펴보실 수 있습니다.(소스코드 자체는 직관적이거나 아닐 수 있습니다). 그러나 이 섹션의 목적이 UEFI 부트로더를 자세히 살펴보는 것이 아니기 때문에 여기서 멈추고 대신에 UEFI가 무엇인지 일반적인 개념을 알려주고 리눅스 커널이 어떻게 사용하는지 알려주겠습니다.
Device Tree
리눅스 커널의 스타트업 코드를 살펴보기 시작할 때 Device Trees가 엄청 언급되는 것을 찾을 수 있었습니다. 이것은 필수적인 개념으로 보이고 논의가 필요하다고 생각합니다.
Raspberry PI OS 커널을 다룰때 특정 메모리 매핑된 레지스터가 정확히 어떤 오프셋에 위치하는지 알아보기 위해 BCM2837 ARM Peripherals manual를 사용했습니다. 이러한 정보는 명확하게 각 보드마다 다르고 단 한개만을 지원한다는 것은 행운인 것입니다. 그러나 수많은 다른 보드를 지원해야 한다면 어떨까요? 만약 각 보드에 대한 정보를 커널 코드에 하드코드 하려면 완전히 엉망일 것입니다. 그리고 심지어 그렇게 하려고 해도 지금 어떤 보드를 사용하려는지 어떻게 알아낼까요? 예를 들어 BCM2837은 실행중인 커널에 그러한 정보를 의사교환하는 어떤 방법도 제공하지 않습니다.
디바이스 트리는 위에서 언급된 이러한 문제에 해결책을 제공합니다. 디바이스 트리는 컴퓨터 하드웨어를 설명하기 위해 사용되는 특별한 포맷입니다. 디바이스 트리 명세는 여기서 찾으실 수 있습니다. 커널이 실행되기 전에 부트로더는 적절한 디바이스 트리 파일을 선택하고 커널에 매개변수로 전달하게 됩니다. 만약 라즈베리 파이 SD 카드안에 부트 파티션에 파일들을 살펴보시면 수많은 .dtb 파일을 찾으실 수 있을 것입니다. .dtb는 컴파일된 디바이스 트리 파일입니다. 몇 라즈베리 파이 하드웨어를 활성화하거나 비활성화 하기 위해 config.txt안에 선택할 수 있습니다. 이러한 과정은 라즈베리 파이 공식 문서에서 자세히 서술되 있습니다.
자 그럼 이제 실제 디바이스 트리가 어떻게 생겼는지 살펴 볼 시간입니다. 빠른 실습으로 라즈베리 파이 3 모델 B를 위한 디바이스 트리를 찾아봅시다. 이 문서로 라즈베리 파이 모델 3 B 이 BCM2837이라는 칩을 사용하는 것을 보실 수 있습니다. 만약 이 이름으로 찾아보면 /arch/arm64/boot/dts/broadcom/bcm2837-rpi-3-b.dts 파일을 찾을 수 있습니다. arm 아키텍처에서 같은 파일을 포함한다. ARM.v8프로세서가 32비트 모드도 지원하기 때문에 말이 됩니다.
다음으로 arm 아키텍처에 있는 bcm2837-rpi-3-b.dts을 찾을 수 있습니다. 디바이스 트리가 다른 것에 포함될 수 있다는 것을 이미 확인했습니다. bcm2837-rpi-3-n.dts 가 이러한 케이스 입니다. - 오로지 BCM2837 에 특정된 정의만을 포함하고 나머지는 재사용합니다. 예를 들어 bcm2837-rpi-3-b.dts는 기기가 1GB의 메모리를 갖는다고 특정합니다.
이전에 언급했듯이 BCM2837과 BCM2835는 동일한 외부 하드웨어를 가지고 만약 인클루드 체인을 따라가보면 bcm283x.dtsi가 대부분의 하드웨어를 사실상 정의한다는 것을 찾으실 수 있습니다.
한 디바이스 트리 정의는 서로 포함되는 블락(blocks nested one in another)으로 구성됩니다. 최상위 수준에서 cpus 나 memory 같은 블락들을 찾아볼 수 있습니다. 이러한 블락의 의미는 상당히 자명해야 합니다. bcm283x.dtsi에서 찾을 수 있는 다른 최상위 수준의 흥미로운 요소는 System on a chip을 의미하는 SoC입니다. 모든 외부 디바이스가 메모리 매핑된 레지스터로 인해 어떤 메모리 영역에 직접적으로 매핑된다는 것을 알려줍니다. soc 요소는 모든 외부 디바이스를 위한 부모 요소로 역할을 합니다. gpio 요소가 이 자식 중 하나입니다. 이 요소는 GPIO 메모리 매핑된 레지스터가 [0x7e200000 : 0x7e2000b4] 영역에 위치한다는 것을 알려주는 reg = <0x7e200000 0xb4>을 정의합니다. gpio 요소의 자식 요소 중 하나는 아래와 같은 정의를 가집니다.
uart1_gpio14: uart1_gpio14 {
brcm,pins = <14 15>;
brcm,function = <BCM2835_FSEL_ALT5>;
};
이 정의는 만약 alternative 함수 5가 핀 14번, 15번에서 선택되었다면 이 핀들이 uart1 디바이스에 연결 될 것이라는 것을 말해줍니다. uart1 기기는 이미 사용한 Mini UART라는 것으 쉽게 추측할 수 있습니다.
디바이스 트리에 관해 알아야 할 중요한 점 한가지는 포맷이 확장가능하다는 것입니다. 각 디바이스는 고유 특성과 내부 블락을 정의할 수 있습니다. 이러한 특성은 디바이스 드라이버로 전달되고 해석하는 것은 디바이스 드라이버의 역할입니다. 하지만 커널이 어떻게 디바이스 트리 안에 블락과 드라이버 간에 상응함을 알아낼 수 있을까? 이러한 일을 하기 위해서 compatible 특성을 사용합니다. 예를 들어 uart1 기기의 compatible 특성은 아래와 같이 특정됩니다.
compatible = "brcm,bcm2835-aux-uart";
그리고 만약 리눅스 소스 코드에서 bcm2835-aux-uart를 찾아보면 알맞은 드라이버를 찾을 수 있을 것입니다. 이 드라이버는 8250_bcm2835aux.c에 정의 되 있습니다.
Conclusion
arm64 부트 코드를 읽기 위한 준비로 이번 챕터를 생각하시면 될 것 같습니다. - 이러한 개념의 이해 없이는 배우기 힘들 수 있습니다. 다음 레슨에서 stext 함수로 돌아가 상세하게 어떻게 작동하는지 살펴볼 것입니다.
댓글남기기