5 minute read

부트로더(Boot loader)의 정의

부트로더(Bootloader)는 컴퓨터가 부팅될 때 가장 먼저 실행되는 소프트웨어로, 운영체제를 로드하고 실행하는 역할을 한다. CPU가 직접 실행하는 최초의 코드이며, 하드웨어와 OS 사이의 중간 역할을 수행한다. 부트로더는 OS가 실행되기 전에 필요한 하드웨어를 초기화하고, OS 실행에 필요한 환경을 설정하는 역할을 한다. 일반적으로 BIOS/MBR 방식과 UEFI 방식으로 나뉜다.

Alt text

부트로더는 운영체제가 실행되기 전까지 하드웨어 초기화 -> 커널 로드 -> 환경 설정 -> 커널 실행의 과정을 수행하는 필수 소프트웨어이다. 특히 UEFI 기반 부트로더는 더 강력한 기능과 유연한 환경 설정을 제공하여 현대적인 운영체제에서 널리 사용된다.

부트로더의 기능

1. 하드웨어 초기화

BIOS 기반의 부트로더와 달리, UEFI에서는 기본적으로 하드웨어 초기화가 펌웨어 단계에서 대부분 수행된다. 즉, 부트로더에서 직접 하드웨어 초기화를 수행할 필요는 거의 없다. 그러나 다음과 같은 설정은 필요할 수 있다.

  1. GDT(Global Descriptor Table) 설정
    • x86 아키텍처에서 세그먼트 기반 메모리 보호를 제공하는 테이블
    • 보호 모드(Protected Mode) 또는 장기 모드(Long Mode, 64-bit mode)로 전환하려면 GDT 설정이 필요
    • UEFI 부팅 시에는 이미 설정되어 있지만, 커널에 맞게 재설정할 수도 있다.
  2. IDT(Interrupt Descriptor Table) 설정
    • 인터럽트 핸들링을 위한 테이블
    • 커널에서 올바른 인터럽트 처리 및 예외 핸들링을 위해 설정 필요
  3. APIC 및 타이머 초기화
    • UEFI에서는 LAPIC(Local APIC) 및 HPET(High Precision Event Timer)를 기본적으로 활성화 하지 않음.
    • 필요한 경우, 직접 설정 필요
  4. UEFI에서 제공하는 부트 서비스와 런타임 서비스
    • UEFI는 Boot Services와 Runtime Services를 제공
    • 커널이 실행되기 전에는 Boot Services를 사용하여 메모리 맵을 가져오고, 이후 커널이 실행되면 Boot Services는 종료됨

2. 메모리 맵 취득

메모리 맵?

메모리 맵은 시스템의 물리 메모리(RAM)와 특정 영역(예약된 공간, 장치 메모리 등)의 분포를 나타낸 표이다. 운영체제가 정상적으로 메모리를 사용할 수 있도록 부트로더가 이 정보를 커널에 전달해야한다.

Alt text

매모리 맵의 번지, 즉 PhysicalStart 열의 숫자는 바이트 단위 값이다. 표의 첫 번째 행은 가장 선두에 있는 메모리 영역을, 표의 두 번째 행은 0x00001000=4096바이트째부터 존재하는 메모리 영역을 의미한다. Type 열은 그 영역이 어떻게 사용되는지를(또는 사용되지 않은 빈 영역인지)를 나타내며, 각각의 의미는 아래 표와 같다.

Alt text

UEFI 메모리 맵과 페이지 단위 메모리 관리

운영체제가 올바르게 동작하기 위해서는 물리 메모리의 구조와 상태를 정확히 파악해야 한다. 이를 위해 부트로더는 UEFI에서 제공하는 메모리 맵을 이용하여 시스템이 부팅될 때의 메모리 상태를 읽어와야 한다.

UEFI 메모리 맵과 페이지 단위 표현

UEFI에서는 GetMemoryMap() 함수를 호출하면 메모리 맵 정보를 EFI_MEMORY_DESCRIPTOR 배열 형태로 제공한다. 이때, 각 메모리 영역(블록)은 페이지 단위(4KiB)로 표현된다.

  • PhysicalStart : 해당 메모리 영역의 시작 물리 주소
  • NumberOfPages : 해당 메모리 영역이 차지하는 페이지 개수
  • 메모리 크기(바이트 단위) : NumberOfPages x 4KiB
  • 메모리 유형 : EFI_MEMORY_TYPE 값으로 지정됨(예 : EfiConventionalMemory, EfiLoaderCode 등)

메모리 영역 간의 간격

메모리 맵을 보면 각 메모리 블록이 연속적으로 배치된 것처럼 보일 수 있지만, 실제 메모리 주소는 불연속적일 수 있다. 즉, PhysicalStart + (NumberOfPages x 4KiB)가 다음 블록의 PhysicalStart와 일치하지 않을 수 있다.

불연속적인 이유

  • 하드웨어 예약 영역
    • 특정 메모리 공간이 하드웨어(ex. GPU, MMIO장치)에서 사용 중일 수 있다.
    • 이 영역은 OS가 임의로 덮어쓰면 안 된다.
  • UEFI 펌웨어 사용 메모리
    • EfiBootServiceData, EfiRuntimeServiceData 등의 영역은 UEFI 자체에서 사용 중인 공간일 수 있다.
  • 메모리 보호 및 할당 정책
    • 일부 메모리 영역은 향후 시스템 안전성을 위해 예약도리 수도 있다.

메모리 맵을 활용한 안전한 메모리 관리

운영체제는 커널과 사용자 프로그램을 메모리에 올릴 때, 사용 가능한(빈) 메모리 영역을 찾아야 한다. 만약 중요한 하드웨어 설정 데이터나 UEFI에서 사용 중인 영역을 덮어쓰면, 시스템이 정상적으로 동작하지 않을 수 있다.

빈 메모리 영역 찾기 : 부트로더가 UEFI 메모리 맵을 조사할 때, 사용 가능한 메모리 유형 : EfiConventionalMemory을 찾아야 한다. EfiConventionalMemory는 OS가 사용할 수 있는 메모리로, 이 메모리 영역만을 대상으로 커널과 프로그램을 배치해야 한다.

UEFI 메모리 유형(EFI_MEMORY_TYPE)

Alt text

부트로더가 커널을 로드할 때 주의할 점

  1. 커널을 로드할 메모리 영역 확인
    • EfiConventionalMemory 유형의 블록을 찾아 가장 적절한 위치 선택
    • 메모리 맵을 스캔하여 적절한 크기의 빈 영역을 찾아야 함
  2. UEFI 런타임 서비스 영역을 덮어쓰면 안됨
    • EfiRuntimeServiceData, EfiRuntimeServiceCode 를 덮어쓰면, OS 실행 중 특정 UEFI 기능이 제대로 동작하지 않을 수 있다.
  3. BIOS/레거시 환경에서는 E820 인터럽트 활용
    • BIOS 기반 시스템에서는 INT 0x15, E820 호출로 메모리 맵을 얻음
    • 페이지 단위가 아닌 바이트 단위로 크기가 제공되므로, 별도로 계산이 필요

메모리 맵을 읽는 프로그램 작성 시 고려 사항

부트로더가 GetMemoryMap()을 호출하여 정보를 읽어올 때, 다음 사항을 주의해야 한다.

  1. 정렬과 메모리 간격 고려
    • 메모리 맵의 엔트리들이 연속적이지 않을 수 있음을 염두에 두어야 함
    • 엔트리의 PhysicalStart 값이 불연속직일 가능성이 높음
  2. UEFI 메모리 맵 버퍼 크기 충분히 확보
    • GetMemoryMap() 호출 시, 버퍼 크기가 부족하면 EFI_BUFFER_TOO_SMALL 오류가 발생할 수 있음.
    • 충분한 버퍼 크기를 확보한 후, 재시도해야 함.
  3. UEFI 서비스 호출 이후 메모리 상태 변화 감지
    • ExitBootServices() 호출 후에는 UEFI 부트 서비스 메모리를 사용할 수 없음.
    • 이 호출 전후로 메모리 맵을 다시 확인할 필요가 있음

요약

  • UEFI 메모리 맵은 페이지 단위(4KiB)로 메모리 블록 크기를 표현한다.
  • 메모리 블록이 연속적으로 배치되지 않을 수 있다.
  • OS는 EfiConventionalMemory 유형의 메모리만 사용해야 한다.
  • EfiRuntimeServicesData, EfiBootServicesData 같은 영역을 덮어쓰면 시스템 오작동 가능
  • 부트로더가 메모리 맵을 읽을 때 충분한 버퍼 크기 확보, 불연속적인 메모리 주소 고려, ExitBootServices() 이후의 변화 감지 등을 신경 써야 한다.

3. 디스크에서 OS 커널 로드

부트로더는 OS 커널을 디스크에서 읽어 메모리에 적재한 후 실행해야 한다. BIOS 기반 부트로더는 MBR(512바이트 제한)을 사용하여 부팅했지만, UEFI 부트로더는 EFI 시스템 파티션(EFI System Partition, ESP)에서 부팅 파일을 로드한다.

UEFI는 FAT 파일 시스템을 기본적으로 지원하며, EFI_SIMPLE_FILE_SYSTEM_PROTOCOL을 사용하여 ESP 파티션에서 kernel.elf 파일을 읽고 실행 가능하다. 파일을 읽어 ELF(Executable and Linkable Format) 포맷을 파싱한 후, 적절한 메모리 영역에 적재하여 커널을 로드한다.

4. 커널 실행을 위한 환경 설정

커널이 실행되기 전에 레지스터, 스택, 페이지 테이블을 올바르게 설정해야한다.

  1. 스택(Stack) 설정
    • 부트로더 실행 중에는 UEFI 펌웨어가 제공하는 임시 스택을 사용한다.
    • 커널 실행 전, 별도의 스택 메모리를 설정해야 한다.
  2. ELF 헤더 분석 및 엔트리 포인트 찾기
    • 커널이 ELF 실행 파일이라면, ELF 헤더를 읽어 엔트리 포인트(Entry Point, 실행 시작 주소)를 찾아야 한다.
  3. CPU 모드 설정
    • x86_64 환경에서는 64비트 장기 모드(Long Mode) 활성화가 필요하다.

5. CPU 보호 모드(Protected Mode) 또는 장기 모드(Long Mode) 전환 (64비트 OS의 경우)

  • x86 CPU는 리얼 모드(Real Mode)로 시작
  • 32비트 모드에서는 보호 모드(Protected Mode)를 사용
  • 64비트 OS에서는 장기 모드(Long Mode)로 전환해야 한다.

모드 전환 과정

  1. 리얼 모드 -> 보호 모드(32비트)
    • CR0 레지스터의 PE(Protection Enable) 비트를 설정
    • GDT(Global Descriptor Table) 설정 후 적용
  2. 보호 모드 -> 장기 모드(64비트)
    • 페이징 활성화 필요(CR3 레지스터 설정)
    • IA-32e 모드를 활성화(CR4, EFER MSR 레지스터 설정)

6. 페이징 및 가상 메모리 설정(필요 시)

x86_64에서는 페이징(Paging)이 필수이다. CPU는 메모리를 물리 주소가 아니라 가상 주소(Virtual Address)로 참조한다.

4레벨 페이징 구조

  • x86_64에서는 4단계 페이징을 사용한다.
    1. PML4(4th Level) : 최상위 페이지 테이블
    2. PDPT(3rd Level) : 페이지 디렉토리 포인터 테이블
    3. PDT(2nd Level) : 페이지 디렉토리 테이블
    4. PT(1st Level) : 실제 페이지 테이블
  • 페이지 테이블 설정 과정
    1. 페이지 테이블(PML4, PDPT, PDT, PT) 생성
    2. CR3 레지스터에 PML4의 주소 설정
    3. CR0 및 CR4 레지스터 설정하여 페이징 활성화

결론

UEFI 기반 부트로더는 펌웨어의 기능을 활용하면서 OS를 실행하는 환경을 구성하는 것이 핵심이다. 보호 모드 및 장기 모드로 전환하고, 페이징을 설정하여 메모리를 가상화하는 것이 중요하다. UEFI 환경에서는 파일 시스템 지원과 메모리 맵 제공이 있어서, BIOS보다 개발이 용이하지만 초기 환경 설정이 더 복잡하다.

Tags: ,

Categories:

Updated: