Operating System - Booting

조성열·2025년 7월 28일

Operating System

목록 보기
1/3
post-thumbnail

Bootstrapping

다양한 분야에서 조금씩 다른 뜻으로 사용되는 용어지만, OS에서 Bootstrapping이란 CPU에 전원이 들어오고 나서, OS 커널이 로드되기까지 외부 입력 없이 스스로 진행하는 일련의 과정을 의미합니다.
컴퓨터 구성 요소인 CPU는 첫 명령이 필요하고, Memory는 초기 코드/데이터가 필요하며, I/O는 통신 방법 인지가 필요하기 때문에 Bootstrapping이 필요합니다.


Firmware: Power on sequence

Firmware는 PROM, EEPROM과 같은 비휘발성(non-volatile)메모리에 설치 되어 있습니다.
이는 Low Level Hardware를 초기화 시키는데, Memory controller의 초기화 타이밍과 Boot device들에 전원 공급 후 OS 로더에 제어권을 넘겨줍니다.
전원 공급 이후 firmware → Bootloader → OS kernel 순으로 동작합니다.
xv6는 BIOS → bootblock → kernel 순으로 동작하고, Linux는 BIOS/UEFI → LILO/GRUB/syslinux → vmlinuz 순으로 동작합니다.


BIOS

컴퓨터의 기본적인 입출력을 담당하는 펌웨어입니다. 컴퓨터를 켜면 가장 먼저 실행되어 하드웨어를 초기화하고 운영체제를 부팅하는 역할을 하고, 16bit real mode interface입니다. real mode란 실제 물리적 메모리에 프로그램을 직접 올리는 방식을 의미합니다.


x86 Registers (32bit)

범용 레지스터 (EAX, EBX, ECX, EDX)와 스택 레지스터(ESP, EBP), EIP, EFLAGS와 같은 레지스터들로 구성되어 있습니다.
ESP는 스택에 최상단을 가리키고, 이때 스택은 아래로 커지기 때문에 최하단 주소를 가리키고 있습니다.
EBP는 현재 스택 프레임에 최하단을 가리키는 레지스터로 꼭 필요하진 않아 범용 레지스터로 사용 가능합니다.
EIP는 현재 실행중인 명령어를 가리키고, EFLAGS는 flag들의 bit vector입니다. carry(덧셈 후), equals zero(비교 후) 등을 저장합니다.


레지스터 구조

하나의 레지스터를 전체 혹은 일부만 골라서 사용할 수 있습니다.
16-bit 환경에서는 AX를, 32-bit 환경에서는 EAX를, 64-bit 환경에서는 RAX를 쓰도록 설계되어 있어, 하위 호환성을 유지하면서도 더 큰 수를 처리할 수 있습니다.
예를 들어 RAX는 64-bit 전체 레지스터 이름이고, 그 하위 32-bit 부분이 EAX, 그 하위 16-bit 부분이 AX입니다. AX(16-bit)를 다시 위쪽 8-bit와 아래쪽 8-bit로 나누면, 상위 바이트가 AH, 하위 바이트가 AL이 됩니다.
이 구조 덕분에 프로그램은 필요에 따라 8, 16, 32, 64-bit 크기의 데이터를 같은 물리 레지스터 공간에서 효율적으로 읽고 쓸 수 있게 됩니다.


x86 주요 명령어

명령어는 크게 Basics, Function, Stack, Control Flow 네 가지 항목으로 나눌 수 있습니다.
참고로 인텔 어셈블리 구문에서 []는 참조 해제를 의미합니다.

  • Basics
    mov : data를 다른 곳으로 옮깁니다.
    add / sub : 레지스터 내 값을 더하고 뺍니다.
    inc / dec : 레지스터 내 값을 증가 또는 감소시킵니다.

  • Function
    call : EIP를 스택에 push하고 해당 함수로 jump합니다.
    ret : 스택의 최상단 값을 pop하여 EIP 레지스터에 저장합니다.

  • Stack
    push / pop : 값을 스택에 넣가나 제거합니다.
    int : Interrupt handler를 실행합니다.

  • Control Flow
    imp : EIP 내에 있는 값을 load합니다.
    cmp : 두 개에 레지스터 값을 비교하고, 해당 값을 flag 레지스터에 값을 저장합니다.
    j-- : 특정 조건이 충족되면 주어진 값을 EIP에 로드합니다.


메모리 구조

예를 들어 64KB 메모리가 있다라고 합시다. 64KB는 2의 16제곱 byte이고 0 ~ 65535까지 주소로 할당할 수 있습니다. 특정 구간은 기기, system이 사용해야 하는 공간이기 때문에 모든 공간을 자유롭게 사용할 수는 없습니다.
그리고 메모리는 개별 바이트, 여러 바이트로 구성된 워드, 더블 워드, 쿼드 워드 등으로 접근할 수 있습니다.


Byte Ordering

여러 바이트로 이루어진 데이터를 메모리에 저장할 때 각 바이트가 어떤 순서로 저장되는지를 정하는 규칙입니다.

  • Big Endian

    • 가장 중요한 바이트(최상위 바이트)가 가장 낮은 주소에 저장됩니다.
    • 반대로 가장 덜 중요한 바이트(최하위 바이트)가 가장 높은 주소에 저장됩니다.
  • Little Endian

    • 가장 덜 중요한 바이트(최하위 바이트)가 가장 낮은 주소에 저장됩니다.
    • 가장 중요한 바이트(최상위 바이트)가 가장 높은 주소에 저장됩니다.

예를들어 변수 x가 4바이트로 0x01234567의 값을 가진다고 가정하고, 변수의 시작 주소는 0x100이라고 합시다.
Big Endian 방식은 01234567 순서대로 낮은 주소에서 높은 주소로 저장이 되고, Little Endian 방식은 67452301 순서대로 낮은 주소에서 높은 주소로 저장됩니다.
바이트 순서가 반전된(리틀 엔디언) 리스팅을 어떻게 읽을까요?

Address     Instruction Code        Assembly Rendition
8048365:    5b                      pop    %ebx
8048366:    81 c3 ab 12 00 00       add    $0x12ab, %ebx
804836c:    83 bb 28 00 00 00       cmpl   $0x0, 0x28(%ebx)

두 번째 줄을 예로 들면, 81 c3 ab 12 00 00 여기서 add $0x12ab, %ebx로 해석됩니다.
해석 방법은 먼저 4바이트(32비트)로 패딩 0x000012ab하고, 바이트 단위로 분해 00 00 12 ab하며, 역순(리틀 엔디언)으로 정렬: ab 12 00 00 합니다.
실제 메모리에 저장될 때 리틀 엔디언 방식으로 ab 12 00 00이 됩니다.


Segmentation

CPU 리셋 직후 시스템이 가져야 할 전체 주소는 0xfffffff0 (32비트)입니다. 그러나 EIP 레지스터에는 0x000fff0만 들어있습니다.
이것은 세그멘테이션에 의해 결합되어 해석됩니다. (0xfffffff0 = 0xffff0000 (CS 세그먼트 베이스)  + 0x0000fff0 (EIP 오프셋))
“어느 세그먼트(=어느 64KB 블록)”를 참조할지 식별하는 역할을 하는 네 개 세그먼트 레지스터는 각각 16비트 크기로 구성되어 있습니다.

  • CS: 코드(명령어) 패치용
  • DS: 일반 데이터 접근용
  • SS: 스택 포인터(SP·BP) 연산용
  • ES: 문자열 처리 명령의 목적지 등 추가 데이터 영역

x86에서 모든 일반 레지스터가 16 비트로 구성되어 있지만, 초기 IBM PC(8086)는 1 MiB(2²⁰ 바이트)까지 접근해야 했습니다.
이때 레지스터 크기(16 비트)와 실제 물리 메모리 크기(20 비트)가 맞지 않는 문제가 발생했습니다.
그래서 상위 4bit을 세그먼트 레지스터가 담당하고, 하위 16bit을 일반 레지스터가 담당하여 실제 물리 메모리 크기와 맞지 않는 문제를 해결합니다.
주소를 변환할 때는 물리 주소(pa) = (세그먼트 값(seg) × 16) + 오프셋(offset)식을 사용합니다.
여기서 세그먼트 값에 16을 곱하는 것은 16진수로 뒤에 0을 하나 붙이는 것입니다.


Segmentation descripter tables

위 사진은 Protected Mode x86 CPU에서의 세그멘테이션과 task 전환, 그리고 GDT/LDT 구조를 보여줍니다. 세그먼트 선택과 디스크립터 테이블, 세그먼트 디스크립터와 태스크 상태 세그먼트(TSS), 태스크 전환과 인터럽트·예외 처리 흐름 크게 세 부분으로 나눠 볼 수 있습니다.

1. 세그먼트 선택과 디스크럽터 테이블

세그먼트 셀렉터(Segment Selector)는 코드나 데이터 접근 시 세그먼트 레지스터(CS, DS, SS, ES 등)에 로드되는 16비트 값을 담고 있고, 이 값이 “GDT(Global Descriptor Table)” 또는 “LDT(Local Descriptor Table)”의 인덱스를 가리킵니다.
GDT는 시스템 전체에서 공유되는 전역 디스크립터 테이블, LDT는 프로세스(또는 태스크)별로 갖는 로컬 디스크립터 테이블을 의미합니다.
각각의 테이블 베이스 주소와 크기는 CPU 레지스터인 GDTR, LDTR에 보관됩니다.

2. 세그먼트 디스크립터와 태스크 상태 세그먼트(TSS)

세그먼트 디스크립터(Segment Descriptor)는 베이스(base), 리미트(limit), 접근 권한(access rights) 등을 담고 있어 코드, 데이터, 스택 세그먼트를 정의합니다.
태스크 상태(Task-State Segment) 디스크립터는 태스크 스위칭에 필요한 레지스터 집합(CS, DS, SS, EIP, ESP 등)을 저장하고, GDT에 TSS 디스크립터를 두면 CALL이나 JMP 명령으로 별도의 태스크 전환이 가능합니다.

3. 태스크 전환과 인터럽트·예외 처리 흐름

태스크 전환시 현재 태스크의 TSS 디스크립터 → TSS 메모리 영역 → 새 태스크의 코드, 데이터, 스택 세그먼트를 로드합니다.
인터럽트 시 GDT의 TSS 디스크립터를 통해 현재 TSS를 읽어들여 자동으로 커널 스택으로 스택 포인터(SS:ESP) 전환 후 핸들러 진입하게 됩니다.
예외·보호 프로시저 호출 시 LDT에 정의된 TSS 디스크립터를 이용해 유저(=낮은 권한)에서 커널(=높은 권한)으로 제어 이동하게 됩니다.


BIOS를 활용한 Booting Process

시스템 전원이 켜지면 BIOS가 POST(Power-On Self-Test) 등을 거쳐 하드웨어 초기화를 마칩니다. 그 다음 마지막 단계로 “부트 로더(boot loader)”를 메모리로 읽어 들이는 역할을 수행합니다.
BIOS는 부트로더를 HDD, USB, CD/DVD 등 다양한 장치에서 읽어올 수 있습니다. 특별히 지정된 장치 순서(boot order)에 따라 첫 번째로 찾은 장치의 첫 섹터(512 바이트)를 가져오는 것이 기본 동작입니다.
읽어들인 512byte(MBR: Master Boot Record)는 물리 메모리 주소 0x0000:0x7C00 (실제 linear address 0x7C00) 위치에 복사됩니다. 이 위치는 오랫동안 표준으로 정해져 온 부트로더 로드 주소입니다.

정리 해보면 전원이 켜지면 CPU 내부의 리셋 벡터(reset vector)로 제어가 넘어가며, BIOS firmware가 메모리, CPU, I/O 장치 등 주요 하드웨어를 순차적으로 검사(POST) 및 초기화하고 이 과정이 끝나면 부트 로더 로딩이 시작됩니다. BIOS 설정상의 부팅 순서(boot order)에 따라 HDD, USB등 장치를 차례로 검사하며, 각 장치의 첫 섹터(512바이트 MBR)에 “0x55 0xAA” 시그니처가 있는지 확인 후 유효한 시그니처를 찾으면 MBR을 물리 메모리 0x0000 : 0x7C00 위치에 복사하여 부트 로더 코드를 실행하며, 이 후 부트 로더가 운영체제 커널을 추가로 읽어오는 과정을 진행합니다.

BIOS는 간단한 하드웨어 초기화만 담당하고, 실제 복잡한 디스크 파일 시스템 파싱이나 커널 로딩은 별도의 부트로더(예: GRUB, LILO 등)가 수행할 수 있도록 설계되어 있습니다.


Example

1. Boot loader start

  • bootasm.S
# Start the first CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.

.code16                       # Assemble for 16-bit mode
.globl start
start:
  cli                         # BIOS enabled interrupts; disable
...

16비트 모드(real mode) 전용 명령어로 어셈블하고, start 라벨을 전역 심볼로 등록해서 링커가 진입점(entry point)으로 인식하도록 합니다.
cli는 “Clear Interrupt flag” 명령어로, 부트로더가 실행되는 동안 외부 인터럽트로부터 안전하게 환경을 초기화하기 위해 BIOS가 남겨둔 인터럽트를 모두 끕니다.

  • Makefile
...
bootblock: bootasm.S bootmain.c
	$(CC) $(CFLAGS) -fno-pic -O -nostdinc -I. -c bootmain.c
	$(CC) $(CFLAGS) -fno-pic -nostdinc -I. -c bootasm.S
	$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 -o bootblock.o bootasm.o bootmain.o
...

bootblock이라는 이름의 타겟을 만들기 위해, 부트 어셈블러 소스(bootasm.S)와 C 코드(bootmain.c)를 이용합니다.

$(CC) $(CFLAGS) -fno-pic -O -nostdinc -I. -c bootmain.c
$(CC) $(CFLAGS) -fno-pic -nostdinc -I. -c bootasm.S

-fno-pic : 위치 독립 코드(PIC)가 아니라 고정 주소용 코드로 만듭니다.
-nostdinc : 표준 include 디렉터리를 검색하지 않고, 현재 디렉터리(-I.)만 헤더를 찾습니다.
-c : 각각을 오브젝트 파일(.o)로만 컴파일합니다.

$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 -o bootblock.o bootasm.o bootmain.o

시작점은 start, 코드 위치는 물리 메모리 0x7C00 메모리 레이아웃은 페이지 정렬 없이(-N)로 설정하여 BIOS가 0x7C00에 올려 둔 512B 부트섹터 내부에서 정확히 start: 라벨부터 실행이 시작될 수 있습니다.

2. Switch to Protected mode

  • bootasm.S
# Switch from real to protected mode.  Use a bootstrap GDT that makes
# virtual addresses map directly to physical addresses so that the
# effective memory map doesn't change during the transition.

lgdt   gdtdesc
movl   %cr0, %eax
orl    $CR0_PE, %eax
movl   %eax, %cr0

위 코드는 GDT를 GDTR에 로드하고, 제어 레지스터 CR0 값을 EAX로 복사한 후 EAX에 PE(Protected Enable) 비트(0x1)를 설정하여 수정된 값을 CR0에 써서 PE 비트 활성화 시킵니다.
gdtdesc는 “GDT Base 주소”와 “Limit(크기)”를 함께 담은 구조체입니다. lgdt 명령으로 GDT 레지스터(GDTR)에 부트용 GDT를 가리키게 세팅합니다.
orl은 OR 연산으로 활성화하는 명령으로 32비트 제어 레지스터인 CR0의 최하위 비트(PE, Protection Enable)를 1로 켜면, CPU가 곧바로 프로텍티드 모드로 넘어갑니다.

3. bootmain() read the kernel

  • bootasm.S
# Set up the stack pointer and call into C.
movl    $start, %esp
call    bootmain

부트섹터가 0x7C00에 로드된 상태이므로, 스택도 그 아래 적당한 위치(여기서는 start 바로 아래)로 잡아주고, C로 작성된 bootmain() 함수를 호출합니다.

  • bootmain.c
void
bootmain(void)
{
  struct elfhdr *elf;
  struct proghdr *ph, *eph;
  void (*entry)(void);
  uchar* pa;

  elf = (struct elfhdr*)0x10000;  // scratch space

  // Read 1st page off disk
  readseg((uchar*)elf, 4096, 0);

  // Is this an ELF executable?
  if(elf->magic != ELF_MAGIC)
    return;  // let bootasm.S handle error
 ...
 
 }

물리 메모리 0x10000(=64 KB) 위치를 임시 버퍼로 잡고, 디스크에 있는 커널 ELF 파일의 첫 4096바이트(=페이지 크기)를 읽어서 elf 버퍼에 복사합니다.
ELF 파일 포맷의 식별자(“0x7F ‘E’ ‘L’ ‘F’”)가 맞는지 검사를 진행하는데, 일치하지 않으면 return해 어셈블리로 복귀시킵니다.


EFI - Extensible Firmware Interface

EFI는 기존 BIOS의 제약을 없애고, 현대적인 언어·모듈 구조·엄격한 사양 검증을 통해 펌웨어 개발과 확장성을 대폭 개선한 차세대 펌웨어 인터페이스입니다.
기존 BIOS가 가지고 있는 16bit real mode 한계와 유지보수·확장성 문제를 극복하기 위해 BIOS 대체 기술로 설계 되었습니다. Native processor mode로 동작하기 때문에 별도 real/protected mode 전환이 필요 없고, C/C++ 같은 고급 언어로 firmware code를 작성할 수 있습니다.


EFI to UEFI

64비트 프로세서(x64)가 보편화되면서, 업계 전반이 BIOS를 대체할 EFI 기반 펌웨어로 전환하게 되었습니다.
Firmware 단계에서 제공하는 런타임 서비스(Call Services)도, 동작 중인 운영체제와 동일한 비트 모드여야만 호출할 수 있기 때문에 x64(64비트) 지원을 위해 몇 가지 세부 규격이 추가·수정 되었습니다.


UEFI vs BIOS

UEFI와 기존 BIOS를 비교하며, UEFI가 BIOS의 한계를 어떻게 극복했는지 1) 설정·부팅 정보 저장 방식, 2) 드라이브 용량 지원, 3) 부팅 속도 및 드라이버 구조, 4) 보안 기능, 5) 실행 모드 및 UI 다섯가지 측면에서 보도록 하겠습니다.

  1. 설정·부팅 정보 저장 방식
    BIOS는 부팅 로직과 드라이버 코드를 메인보드 펌웨어(ROM)에 하드코딩 해서 업데이트하려면 펌웨어 전체를 교체하거나 플래시 해야하는 문제가 있습니다. UEFI는 모든 초기화·스타트업 코드를 .efi 실행 파일 형태로 디스크(ESP: EFI System Partition)에 저장하여 ESP 파티션에 부트로더와 함께 보관되므로 개별 파일 단위로 쉽게 교체·업데이트 가능한 방식으로 설계 되었습니다.

  2. 드라이브 용량 지원
    BIOS는 MBR 파티션 구조 한계로 최대 약 2.2 TB밖에 지원을 못하지만, UEFI는 GPT 파티션 구조를 사용하여 최대 9 ZB까지 지원 가능합니다.

  3. 부팅 속도 및 드라이버 구조
    부팅 속도는 당연히 발전된 형태인 UEFI가 빠르고, BIOS는 ROM에 내장된 드라이버 사용하기 때문에 업데이트가 번거롭습니다. 하지만 UEFI는 모듈화된 드라이버(프로토콜) 형태로 ESP에 둘 수 있어 교체·확장이 쉽습니다.

  4. 보안 기능
    UEFI Secure Boot는 인증된 부트로더·커널만 실행되도록 검증하고, 루트킷(Rootkit) 같은 부팅 단위 악성코드를 차단합니다.

  5. 실행 모드 및 UI
    BIOS는 CLI만 제공하지만, UEFI는 32-bit 또는 64-bit 네이티브 모드 지원하여 GUI를 제공합니다.

2개의 댓글

comment-user-thumbnail
2025년 8월 1일

글이 좋네요

1개의 답글