System Programming/Linux Device Driver

Linux Inside #1 Booting: Bootloader에서 Kernel까지

uzguns 2024. 10. 1. 12:20

이 게시물은 x86_64 아키텍쳐를 기준으로 하고 있다. 로우레벨이 내부적으로 어떻게 동작하는지 프로그램이 컴퓨터에서 어떻게 실행되는지 어떻게 그것들이 메모리에 적재되는지 커널이 프로세스와 메모리 관리를 어떻게 하는지, 네트워크 스택이 로우레벨에서 어떻게 동작하는지 등...

 

컴퓨터의 시작: 전원 버튼에서 리얼모드까지

컴퓨터를 켤 때 먼저 메인보드가 전력을 공급받아야한다. 그 후 메인보드는 CPU를 작동시킨다. 이 때 CPU는 리셋 상태에서 시작하며 기본 레지스터 값을 초기화하고 real mode 라는 매우 기초적인 운영모드에서 작동을 시작한다.

 

Real Mode란

리얼 모드는 8086이라는 매우 오래된 CPU 부터 모든 현대적인 x86 프로세서까지 지원되는 모드이다.

 

리얼모드에서는 CPU가 최대 1MB 의 메모리만 접근할 수 있다. 8086 CPU의 레지스터는 16 비트였기 때문에 최대 64KB 크기의 데이터를 처리할 수 있다. 그래서 이 한계를 극복하기 위해 세그먼트 방식을 사용하여 1MB 메모리 공간을 활용했다.

 

세그먼트 방식은 segment selector 와 offset 이라는 두 부분으로 메모리 주소를 구성한다.

물리 주소는 세그먼트 셀렉터에 16을 곱하고 offset을 더하여 계산된다.

물리주소 = 세그먼트 셀렉터 * 16 + 오프셋

예를 들어 CS:IP 값이 0x2000:0x0010 이라면 이 값을 물리 주소로 변환하면 0x20010 이 된다.

>>> hex((0x2000 << 4) + 0x0010)
'0x20010'

 

 

Reset 이후의 CPU 상태

 리셋 이후 CPU는 리얼 모드에서 시작하며 이 때 중요한 레지스터 값들이 다음과 같이 설정된다.

  • IP: 0xfff0
  • CS selector(visible segement selector) : 0xf000
  • CS base( hidden base address) : 0xffff0000

CPU는 CS:IP 조합을 사용하여 실행할 첫 번째 명령어의 위치를 찾는다.

 

이 위치는 4GB 메모리 공간의 끝에서 16 바이트 아래인 0xfffffff0 이다. EIP 레지스터의 값에 기준 주소를 더함으로써 형성된다.

>>> 0xffff0000 + 0xfff0
'0xfffffff0'

이 위치는 reset vector 라고 불리며 CPU가 리셋되었을 때 실행해야할 첫번째 명령어가 저장된 위치이다.

 

BIOS로의 진입

CPU는 리셋 벡터에 저장된 명령어를 실행하여 BIOS에 진입한다.

BIOS는 하드웨어를 초기화하고 부팅가능한 장치를 찾는다. 하드 드라이버의 경우 BIOS 는 부트 섹터(첫 512 바이트)를 읽고 이 곳에서 부팅에 필요한 명령어를 찾는다.

 MBR 파티션 레이아웃 으로 파티션된 하드 드라이브에서 각 섹터가 512 바이트일때, 부트 섹터는 첫 섹터의 첫 446 바이트에 저장됩니다. 첫 섹터의 마지막 두 바이트는 0x55와 0xaa 입니다. 이는 BIOS에게 부팅 가능한 장치라는 것을 알려주기 위해 디자인 되었습니다.

어셈블리 코드를 작성하여 부트 섹터를 만들고 나면 이 코드를 QEMU 와 같은 가상 머신에서 실행해볼 수 있다. 

 

컴퓨터와 노트북은 바로 작동하기 시작한다. 메인보드는 파워 서플라이에 신호를 보낸다. 신호를 받고 나면 파워 서플라이는 컴퓨터에 적잘한 양의 전력을 제공하기 시작한다. 메인보드가 Power Good Signal을 받고 나면 메인보드는 CPU 시작을 시도한다. CPU는 모든 레지스터에 남아있는 데이터를 초기화하고 각각에 미리 정의된 값들을 설정한다. 

 

아래 어셈블리 코드를 바이너리로 컴파일 후 QEMU에서 부팅해보자.

[BITS 16]

boot:
    mov al, '!'
    mov ah, 0x0e
    mov bh, 0x00
    mov bl, 0x07

    int 0x10
    jmp $

times 510-($-$$) db 0

db 0x55
db 0xaa
  • [BITS 16]:
    • NASM 어셈블러에게 16비트 코드를 작성하고 있음을 알려줌.
    • 이는 부트로더가 16비트 모드에서 실행되기 때문입니다.
  • boot 레이블: 
    • 부트 코드의 시작 지점
      mov al, '!': ASCII 값이 !인 값을 AL 레지스터에 저장
      mov ah, 0x0e: AH 레지스터에 BIOS 인터럽트 0x10의 0x0E 기능(문자 출력 기능)을 설정
      mov bh, 0x00: BH 레지스터에 화면 페이지 번호를 설정합니다. 여기서는 0페이지를 의미
      mov bl, 0x07: BL 레지스터에 텍스트 색상을 설정. 0x07은 흰색 텍스트에 검은색 배경을 의미.
      int 0x10: BIOS 인터럽트 0x10을 호출하여 '!' 문자를 출력.
      jmp $: 무한 루프. 현재 위치로 계속 점프하라는 의미.
  • times 510-($-$$) db 0: 
    • 512바이트 부트 섹터를 맞추기 위해 남은 바이트를 0으로 채움. 
    • 부트 섹터는 반드시 512바이트여야 함.
  • db 0x55와 db 0xaa:
    • 부트 섹터의 마지막 2바이트는 0x55와 0xAA로 설정됨
    • 이는 부팅 가능한 디스크임을 BIOS에 알리기 위한 매직 넘버

 

NASM 어셈블러를 사용하여 바이너리 파일로 컴파일한다.

nasm -f bin boot.nasm -o boot.bin

 

QEMU를 이용하여 이 바이너리 파일을 부팅한다.

qemu-system-x86_64 -drive format=raw,file=boot.bin

 

QEMU 창에서 !가 출력되는 것을 확인할 수 있다.

 

References