Linux Boot

가끔씩 Linux의 부팅과정에 대해서 질문이 들어오면 dmesg를 보여주면서 설명하거나 칠판에 낙서하듯이 설명해 주곤 했는데 이번에 지금까지 받았던 질문과 설명을 토대로해서 개략적으로나마 Linux의 부팅과정을 문서로 정리해 보았다.

  • 본 문서에서는 OS는 CentOS 6를 기준으로 하며 부트로더는 MBR위의 GRUB v1을 기준으로 설명합니다.
  • 오타는 발견 때 마다 수정 중에 있습니다
  • 마지막수정: 12/18/2015

GRUB Boot Loader

GRUB 부트로더는 첫 번째 섹터(Sector 0)인 MBR(Master Boot Record)에 위치하고 있으며 MBR에 대한 문서에서 소개했던 것 처럼 첫 번째의 446바이트 공간안에 존재한다. 여기에 존재하는 부트로더 파일은 전체 GRUB이 아니라 부트로더의 모든 기능을 불러 낼 수 있는 최소한의 기능만을 담은 이미지(stage1)를 담고 있다. 부팅 된 리눅스서버에서 보통 아래 경로에서 해당 파일을 확인 할 수 있다.

$ ls -l /boot/grub/stage1
-rw-r--r--. 1 root root 512 May  2  2013 stage1

512바이트 크기의 이 파일은 정확히 0번 섹터에 들어맞는 크기로 되어있으며 이 파일을 hexdump로 떨구면 아래와 같이 mbr의 시그니처인 0xaa55를 확인 할 수 있다. (하지만, 그 앞의 값은 MBR의 파티션 엔트리 값과 동일하지 않다. 부트로더 설치를 위한 파일이지 설치된 부트로더를 의미하는 파일이 아니기 때문이다)

$ hexdump -s 446 stage1
00001be 1224 090f be00 7dbd c031 13cd 8a46 800c
00001ce 00f9 0f75 dabe e87d ffc9 97eb 6c46 706f
00001de 7970 bb00 7000 01b8 b502 b600 cd00 7213
00001ee b6d7 b501 e94f fee0 0000 0000 0000 0000
00001fe aa55
0000200

Stage1

stage1을 조금 더 자세히 살펴보자. stage1이 설치된 MBR을 덤프하고 이를 hexdump로 살펴보도록 하겠다. 이 부분은 실제 저장된 값을 읽어서 분석하기 때문에 어셈블리에 친숙하지 않을 경우 (물론 나도 그렇게 친숙하지는 않다) 어려울 수 있으나 생각보다 어려운 항목은 없기 때문에 천천히 따라해보면 이해하는데 큰 어려움은 없을 것이다.

$ dd if=/dev/sda of=./mbr bs=512 count=1

/dev/sda는 부트 디스크를 의미하며 512크기로 1개를 복제하도록 하였다

위에서 덤프한 MBR 파일을 hexdump로 좀 더 보기 쉽게 읽어보면 아래와 같다

처음 부터 차근차근 살펴보도록 하자.

mbr-hexdump-1.png

처음 3개의 바이트 값은 eb 48 90인데 여기에서 eb 48JMP(eb) 0x48을 의미하며 현재 명령의 위치로부터 0x48만큼 점프하게 된다. 따라서, 인자 값에서 2바이트를 더한 0x48 + 0x02 = 0x4a 위치로 이동하게 되며 eb 48 뒤에 따르는 90은 아무것도 하지 않는다는 의미다.

0xeb: JMP - short relative jump instruction
0x90: NOP - no operation

이렇게 부트로더는 0x4a로 점프해서 실행하게 되는데, 사이에 있는 데이터는 스토리지 볼륨의 레이아웃에 대한 정보를 알려주는 BPB(BIOS Parameter Block)영역으로 자세한 사항은 여기를 참고하도록 하자.

mbr-hexdump-2.png

그 중에서 위 그림의 녹색영역인 0x3e 부터 12바이트 데이터는 GRUB에 대한 정보를 담고 있다. 앞에서부터 하나씩 해석하면 아래와 같다.

0x003e - 03: Major 버전 번호
0x003f - 02: Minor 버전 번호
0x0040 - ff: Boot Drive
0x0041 - 00: Force LBA Mode byte
0x0042 - 00 20: 다음 스테이지 GRUB 메모리 주소 (0x2000)
0x0044 - 01 00 00 00: stage1이 알고 있는 stage2의 섹터 위치 값이다. 보통 GRUB MBR 뒤에 오게 된다면 01 00 00 00이지만 그렇지 않을 경우 실제 섹터 값이 들어있다.
0x0048 - 00 02: 다음 스테이지의 위치 (메모리 주소가 아니다 - 0x0200 = 512)

mbr-hexdump-3.png

0x4a부터 시작되는 프로그램 코드는 다음 스테이지로 넘어가기위한 작업을 진행하고 문제가 발생 할 경우에는 아래의 0x0179에 위치한 문자열로 오류 메시지를 출력하게 된다. 프로그램 코드와 파티션 테이블의 위치 (0x01be)사이의 데이터는 보통 사용하지 않는데 0x01b8에서 0x01bb까지의 1WORD는 Windows에서 NT Drive 시리얼넘버로 사용한다. 그리고 0x01be부터 시작되는 파티션 엔트리는 MBR문서에서 소개한 것 처럼 파티션 테이블의 정보를 담고 있다.

stage1.5

앞서 살펴본 것 처럼 stage1 이미지는 512byte의 MBR에 들어갈 수 있는 작은 크기로 다음 단계의 부트로더 이미지를 읽기 위한 작업만을 처리하게 된다. 다음 단계에 해당하는 stage2 이미지는 GRUB의 코어 이미지로 스스로 부팅 할 수 있는 기능을 제외한 모든 기능을 담고 있다. 하지만, 요즘에는 다양한 파일시스템과 볼륨 구조위에 stage2 이미지가 존재하기 때문에 stage1에서 모든 파일시스템을 다룰 수 없어서 중간 단계에 해당하는 stage1.5 이미지를 먼저 불러들인다.

/boot/grub 아래에는 아래와 같이 stage1.5에 해당하는 파일들이 존재한다.

$ ls -l /boot/grub/*1_5
-rw-r--r--. 1 root root 13380 May  2  2013 /boot/grub/e2fs_stage1_5
-rw-r--r--. 1 root root 12620 May  2  2013 /boot/grub/fat_stage1_5
-rw-r--r--. 1 root root 11748 May  2  2013 /boot/grub/ffs_stage1_5
-rw-r--r--. 1 root root 11756 May  2  2013 /boot/grub/iso9660_stage1_5
-rw-r--r--. 1 root root 13268 May  2  2013 /boot/grub/jfs_stage1_5
-rw-r--r--. 1 root root 11956 May  2  2013 /boot/grub/minix_stage1_5
-rw-r--r--. 1 root root 14412 May  2  2013 /boot/grub/reiserfs_stage1_5
-rw-r--r--. 1 root root 12024 May  2  2013 /boot/grub/ufs2_stage1_5
-rw-r--r--. 1 root root 11364 May  2  2013 /boot/grub/vstafs_stage1_5
-rw-r--r--. 1 root root 13964 May  2  2013 /boot/grub/xfs_stage1_5

stage1.5 파일들의 이름을 유심히 보면 파일시스템 이름으로 시작하는데 이 파일들을 통해서 각각의 파일시스템을 이해하고 stage2 이미지를 불러들일 수 있는 것이다. 만약 ext4 파일시스템 위에 /boot 파티션과 stage2 이미지가 있다면 GRUB 부트로더가 설치 될 때에 stage1.5로 e2fs_stage1_5 파일이 설치된다.

앞서 stage1을 살펴볼 때 GRUB 정보에서 다음 스테이지의 섹터 위치가 MBR 바로 뒤(01 00 00 00)로 표기되었기 때문에 Sector 1번 부터 stage1.5 이미지가 설치되어 있을 것이다. 테스트 시스템의 경우 /boot가 ext4 파일시스템에 있기 때문에 e2fs_stage1_5 파일의 내용이 들어 있는지 확인해 보도록 하자.

먼저 e2fs_stage1_5 파일을 hexdump로 확인해보고 시작 데이터와 마지막 부분을 확인 해 두자.

52 56으로 시작 된 내용은 중간에 stage1.5 에러 텍스트와 stage2 이미지경로를 담고 있으며 0x3443까지 데이터가 있다. (13380바이트 파일이므로 0x3444 크기이다) 이제 실제 디스크에서 MBR 바로 뒤 부터 27개의 섹터를 저장해 보자

  • e2fs_stage1_5 파일은 13380 byte
  • 1섹터는 512바이트
  • 13380512 = 26.1328125 이므로 최소 27개 섹터가 필요하다

디스크로 부터 저장한 27섹터를 열어보면 e2fs_stage1_5의 내용이 그대로 들어있는 것을 확인 할 수 있다. 그리고, 해당 파일은 0x3443에서 끝났기 때문에 위의 내용처럼 0x3444부터 00으로 섹터 끝까지 채워져 있는 것을 볼 수 있다.

과거에는 MBR아래에서 관리되던 파티션 들은 대부분이 Sector 63부터 시작하였다. MS-DOS 시절부터 첫 번째 트랙은 시스템정보를 저장하기 위한 용도로 사용되었으며 플로피의 경우 ‘Track 0’가 망가지면 전체 디스크를 사용할 수 없었기 때문에 복구를 위해 놔두기도 하였다. DOS가 사용되던 시절에는 CHS 기준으로 1 트랙은 63개의 섹터를 가지고 있었다. 그렇기 때문에 보통 디스크 볼륨의 부트레코드는 두 번째 트랙에 해당하는 64번 섹터(LBA 63)부터 시작하였고 MBR을 제외한 Sector 1~62는 비어있는 섹터이면서 기본적으로 사용하지 않는 섹터였다. 그리고, GRUB은 여기에 stage1.5를 저장해서 동작 한다.

여담으로, 최근에는 AF 디스크들을 위해서 파티션의 섹터 정렬이 필요하게 되었는데 정렬을 위해서는 파티션 시작 섹터 값이 8의 배수이면 문제가 없지만 Windows가 LBA 2048 섹터부터 시작하기 때문에 파티셔닝 툴 들이 LBA 2048 섹터부터 시작하는 것을 기준으로 설정하는 것을 권장하고 있다.

stage 2

앞서 살펴 본 stage1, stage1.5의 과정을 도식화 하면 아래와 같다.

grub-mbr.png - 출처: GNU GRUB

위의 그림처럼 MBR에 위치한 stage1은 파일시스템 접근을 위해 stage1.5 파일을 불러들이고 stage1.5를 통해서 root 디스크의 지정된 위치로 부터 stage2 이미지를 불러들여서 GRUB 부트로더가 정상적인 모든 기능을 수행 할 수 있도록 준비를 마친다. stage2는 아래 처럼 용량이 크기 때문에 (어디까지나 MBR과 Track 0에 해당하는 63섹터 이내 기준보다) 파일시스템에 보통 위치하게 된다.

$ ls -l /boot/grub/stage2
-rw-r--r--. 1 root root 125976 May  2  2013 /boot/grub/stage2
  • 125976 byte = 125976 / 512 = 246.046875 = 247섹터 필요

사실, 커널의 위치가 고정되어서 관리된다면 굳이 다음 단계의 부트로더를 불러들일 필요 없이 MBR(stage1)에서 바로 해당 커널이 위치한 섹터로 접근해서 부팅을 시도할 수도 있다. 이 경우에는 커널파일이 존재하는 파일시스템의 종류나 구조에 대해서 전혀 알 필요가 없게 된다.

하지만, 이런식으로 부팅을 관리하게 되면 새로 설치되는 커널에 대해서는 바뀐 위치를 매번 갱신해야하거나 기존 섹터가 새로운 섹터를 가리키도록 지정 해야만 한다. 따라서 이러한 방법들은 오히려 관리의 복잡성을 높이고 다양한 기능을 추가하기 어렵기 때문에 GRUB과 같은 형태의 부팅 방식을 취하고 있다.

그렇기 때문에 한번 설치한 GRUB 부트로더 환경에서는 /boot/grub/grub.conf 파일만 수정해서 설정을 쉽게 변경 할 수 있는 것이다. (보통 /etc/grub.conf로 심볼릭 링크가 걸려있다)

Linux Kernel

BIOS에서는 디스크 부트섹터를 읽어 들여서 GRUB 부트로더가 구동이 되며 GRUB 부트로더는 설정 된 값에 따라서 커널 이미지를 읽어 실행시키는 작업을 하게 된다. GRUB 부트로더가 커널 이미지를 읽어서 부팅하는 과정에 대해서 살펴보자.

bzImage

현재의 리눅스커널은 bzImage 형태로 되어있는데 이름에서도 유추할 수 있듯이 gzip으로 압축된 바이너리 이미지이다. (zImage와 bzImage 모두 압축된 형태를 의미하며 bzImage가 더 큰 이미지로 리얼모드 메모리 영역 밖에 커널 이미지를 둬야하는 차이가 있다) bzImage는 크게 부트섹션(bootsect), 셋업코드(bootsetup), 커널이미지(vmlinux + 전개루틴)로 구성되어 있다. 부트섹션은 512 byte 공간으로 커널 2.4까지는 플로피 디스크를 위한 부트로더가 들어있었으나 2.6부터는 에러메시지와 부트로더가 참조 할 파라미터 정도만 들어있다. (그렇기 때문에 2.4커널의 경우 플로피에 복사하면 바로 부팅이 가능했다). - 참고문헌

- 이미지출처: 위키피디아

Memory - Boot loader

BIOS는 부트로더를 물리 메모리 0x7C00에 로드 시킨다. - 왜 0x7C00에 로딩하는지에 대해서는 여기 문서에 잘 설명되어 있다. 쉽게 말해 BIOS 개발진이 최소 필요한 메모리 영역을 비워두고 부트로더를 로딩 할 메모리 주소를 선택한 것이 0x7C00이다.

로딩 된 부트로더는 커널을 메모리에 올리고 부팅하기 위한 사전 작업을 수행하는데 먼저 부트섹션과 부트섹션에 기재 된 셋업코드 크기를 참고하여 (512 + 512*n byte)를 메모리에 세팅하게 된다. 세팅하는 메모리 위치는 리얼 모드에서 접근 가능한 주소여야 하기 때문에 0x090000 (576KB)에 부트섹션을 세팅하고 0x90200에 셋업코드를 세팅하게 된다. 그리고 나머지 커널 이미지를 0x010000 (64KB)에 세팅하게 된다.

하지만, 위와 같은 메모리 전개는 과거 커널 이미지(Image or zImage)에서 주로 사용하던 메모리 전개이며 최근 bzImage 커널 이미지의 경우는 [리얼 모드]에서 접근 가능한 위치 중 부트로더가 제공하는 제일 낮은 위치에 부트섹터와 셋업코드를 세팅하고 커널이미지는 0x100000(1MB 이후)에 세팅하여 아래와 같은 모습을 가지고 있다. 이는 큰 커널이미지를 사용 할 때도 작은 커널과 같은 부팅 정책을 사용하지만 ISA Hole 위치까지 커널이미지가 로딩되는 것을 방지하기 위해서 1MB 이후에 로딩하는 것이다.

  • ISA Hole 이란?
    • IBM 호환 PC에서는 물리주소 0x000a0000 ~ 0x000fffff까지는 BIOS의 함수와 ISA 그래픽카드의 내부 메모리를 매핑하는 용도로 예약이 되어 있으며 운영체제가 해당 페이지 프레임을 사용할 수 없다. 이는 640KB~1MB까지의 공간으로 ISA Hole이라고 칭한다.

부트로더는 커널 이미지 뿐만아니라 설정파일에 initrd/initramfs가 설정되어 있다면 해당 이미지 파일 또한 메모리에 올려두며 커널에 전달 할 커맨드 라인(커널 파라미터)도 저장한다. 과거(Image/zImage)에는 커맨드 라인이 0x98000에서 0x9A000까지 저장되었기 때문에 255글자 제한이 있었지만 지금은 cmd_line_ptr 필드값을 통해서 원하는 위치에 저장이 가능하다. (셋업코드가 사용하는 힙이 끝나는 영역부터 0x0A0000 사이에 아무곳이나 가능)

kernel :: setup()

커널의 셋업코드(부트섹터 바로 다음)는 하드웨어 장치를 리눅스 커널에 맞게 초기화 작업을 수행한다. 그렇기 때문에 BIOS에서 보여지는 하드웨어 정보와 별개로 하드웨어를 인식하고 제어할 수 있는 것이다.

  • ACPI 적용 시스템에서 시스템의 물리 메모리 배치를 나타내는 메모리 테이블을 구축하기 위해서 BIOS루틴에서 BIOS-e820을 찾는다
  • 키보드 반복 간격과 속도를 설정한다
  • VGA를 초기화 한다
  • 디스크 컨트롤러를 다시 초기화 하고, 하드 디스크의 파라미터를 알아낸다
  • IBM MCA(Micro Channel Bus)를 검사한다
  • PS/2 장치 검사
  • APM(Advanced Power Management)의 BIOS지원 여부를 검사
  • EDD(Enhanced Disk Drive)서비스의 BIOS지원 여부를 검사
  • 만약 커널 이미지가 0x00010000(64KB)에 위치해 있다면 0x00001000(4KB)으로 옮긴다
    • 이는 커널 이미지가 압축을 풀기위해 이미지 뒤에 여유공간이 필요하기 때문에 더 앞쪽 주소로 옮긴다
    • 만약 1MB(0x00100000) 뒤에 있다면 이 과정은 생략된다
  • 8042 키보드 컨트롤러의 A20 핀을 설정한다 - 참고문서
    • 80286과 8088 마이크로프로세서의 물리 주소 호환을 위한 것으로 설정되지 않으면 모든 21번째 핀이 0으로 취급되어버린다
  • 부트 작업을 위한 IDT(Interrupt Descriptor Table)과 GDT(Global Descriptor Table)을 세팅한다
  • FPU(Floating Point Unit)이 존재 한다면 이를 초기화 한다
  • PIC(Programmable Interrupt Controllers)를 다시 프로그래밍하여 모든 인터럽트를 마스크 한다
  • CPU의 cr0 상태 레지스터 PE 비트를 설정하여 CPU를 real mode에서 protected mode로 전환한다
  • startup_32()로 넘어간다.

하드웨어 처리와 관련 된 부분이기 때문에 상세한 내용은 실제 어셈블리 코드를 설명한 문서를 참조하도록 하고 이 중에서 눈여겨 볼 만한 것만 자세히 짚어보면 BIOS-e820에 대한 부분이 있다. BIOS-e820은 시스템 주소 맵을 얻기 위한 것으로 INT 15h, AX=E820h를 호출하면 real mode에서 현재 시스템에서 사용가능한 메모리 맵 정보를 얻을 수 있다. 이 정보는 시스템이 부팅되면 “/sys/firmware/memmap” 에 반영된다. 상세 정보는 여기여기를 참고하자.

startup_32()로 넘어가기 전에 cr0 상테 레지스터에 대한 내용이 나오는데 cr은 Control Register를 의미하며 cr0는 프로세서에 대한 다양한 동작을 지정하는 플래그를 가지고 있다. 그 중에서 0번째 비트가 1로 세팅이되면 시스템은 리얼모드에서 보호모드로 넘어가게 된다.

리얼모드보호모드에 대한 설명을 전부 하기에는 그 내용이 많다. 따라서, 간단히 설명하면 리얼모드는 시스템 물리 메모리 주소를 바탕으로 단일 프로세스가 독점으로 접근하는 상태이며 보호모드는 가상 메모리 주소를 제공하고 이를 통해서 Linux와 같은 시스템 소프트웨어가 가상 메모리, 페이징, 멀티태스킹을 가능하도록 지원하는 상태라고 이해하면 된다. 80286 CPU가 나오기 전에는 리얼모드/보호모드로 구분하는 것은 없었다. 80286이 하드웨어 수준의 메모리 보호를 적용하도록 디자인 되면서 기존 80868088 CPU와의 호환성을 맞추기 위해서 리얼모드라는 개념을 도입한 것이다.

cr0.png - 출처: 위키피디아

kernel :: startup_32()

arch/i386/boot/compressed/head.S 파일에 있는 startup_32()는 아래와 같은 작업을 수행한다.

  • Segmentation 레지스터와 임시 스택을 초기화한다
  • eflags 레지스터 안의 모든 비트를 지운다
  • _edata와 _end 심볼 사이에 있는 커널의 초기화되지 않은 영역을 0으로 채운다
  • decompress_kernel() 함수를 호출하여 커널 이미지의 압축을 푼다
    • 0x00001000(4KB)에 이미지가 있을 경우에는 0x00100000(1MB)로 압축해제 된 커널을 옮긴다
    • 0x00100000(1MB)에 이미지가 있을 경우에는 이미지 뒤의 임시버퍼에 압축을 풀고 0x00100000로 옮긴다
  • 0x00100000(1MB)위치로 점프(JMP)한다

여기에서 초기화하는 Segmentation 레지스터는 아래 그림처럼 메모리 관리 방법인 세그먼트에 대한 정보를 담고 있는 레지스터를 의미한다.

compressed/head.S의 startup_32()는 주로 압축해제의 역할만을 수행한다. 여기에서 eflags는 현재 프로세서의 상태를 저장하는 레지스터로 아래와 같은 정보들을 가지고 있다.

eflags.png - 출처: Introuction to x86 assembly language

그리고, _edata와 _end는 아래 그림처럼 초기 3MB의 공간에서 커널 뒤에오는 초기화 되지 않은 커널 데이터 영역을 의미한다.

768page.png - 출처: Understanding the Linux Kernel

압축해제 작업을 마치고 커널이미지의 시작지점(0x00100000)으로 이동하고 이는 다시금 arch/i386/kernel/head.S 파일에 있는 startup_32()를 실행하게 된다. 이름은 같지만 두 번째 startup_32() 함수는 첫 번째 리눅스 프로세스(PID 0)를 실행하는 환경을 구성하게 된다.

  • BSS 영역을 초기화 한다
  • 페이징 모드로 변환한다
    • cr3 레지스터에 페이지 전역 디렉토리 주소를 설정하고 cr0의 PG 비트를 1로 설정
  • 커맨드라인(커널 파라미터)을 첫 번째 페이지 프레임에 저장한다 (커널의 데이터 영역)
  • IDT를 NULL(가짜)로 채우고 GDT 설정
    • gdtr과 idtr 레지스터를 각각 GDT와 IDT 테이블 주소로 설정한다
  • eflags 레지스터 안의 모든 비트를 지운다
  • 프로세스 0을 위한 커널 모드 스택을 설정한다.
  • CPU의 타입을 판별한다
  • init_task_union()을 통해서 idle 프로세스 구조(task_struct)를 갖추게 되고 이는 곧 PID 0이 된다
    • 이 과정에서 커널스택을 사용하도록 설정이 이루어진다
  • start_kernel() 함수로 점프(JMP)한다

이 다음부터는 C언어로 작성된 실제 커널의 시작지점으로 진입하게 되는데 startup_32()를 통해서 task_struct 구조와 페이지에 기반한 환경을 최소한으로 갖추었기 때문에 커널 바이너리의 초기화 작업을 준비된 환경에서 실행 될 수 있도록 해줍니다. 참고로, startup_32()의 페이지환경은 8MB 짜리의 일시적인 형태의 페이지 테이블입니다. 상세한 코드 설명은 이 문서를 참고하세요.

kernel :: start_kernel()

본격적으로 커널을 실행하는 부분으로 init/main.c 안에 들어있는 C언어 함수이다. 코드에 대한 설명은 이 문서를 참조하면 되며 내용 중에서 흐름의 이해를 돕기 위한 함수에 대해서 간단히 코멘트를 달아두었다. 또한, 최신 커널에만 있는 함수를 언급하기 위해서 kernel 4.3에 있는 init/main.c 코드를 인용하였다.

- 버디시스템은 메모리 할당을 위한 알고리즘으로 전체 설명은 이 문서의 범위를 벗어나므로 이 영상을 보도록 하자.

마지막의 rest_init()이 수행되면 앞서 초기화 하였던 관리환경을 토대로 프로세스가 생성되게 된다. - kernel_thread()를 통해서 PID 1인 커널 스레드를 생성한다 - PID 1 커널 스레드는 필요한 커널 스레드 들을 생성하고나서 init()으로 넘어간다 - 최신 커널은 kernel_init() 함수이다 - init()에서 멀티프로세서의 초기화, 드라이버 초기화, initrd/initramfs 처리 및 rootfs 마운트 - /sbin/init, /etc/init, /bin/init, /bin/sh 등을 찾아서 init 단계로 넘어간다 - init을 찾지 못하면 커널패닉 발생

부트 메시지

앞서 살펴보았던 부트로더, 커널의 부팅 및 초기화 과정의 내용을 바탕으로 하여 Linux 커널이 부팅할 때 기록하는 메시지에 대해서 이해해 보도록 하자. 이 메시지는 부팅이 완료된 이후에 dmesg와 같은 명령을 통해서 확인하거나 /var/log 아래에 기록된 로그파일로 확인 할 수 있다.

[/var/log/dmesg]
Initializing cgroup subsys cpuset
Initializing cgroup subsys cpu
Linux version 2.6.32-573.8.1.el6.centos.plus.x86_64 ([email protected]) (gcc version 4.4.7 20120313 (Red Hat 4.4.7-16) (GCC) ) #1 SMP Tue Nov 10 18:20:27 UTC 2015
Command line: ro root=UUID=f7b6075b-d9f0-4ce7-bbf8-eec678d60269 intel_idle.max_cstate=0 crashkernel=auto biosdevname=0 console=tty0 console=ttyS0,115200
KERNEL supported cpus:
  Intel GenuineIntel
  AMD AuthenticAMD
  Centaur CentaurHauls

처음 Linux control group에 대한 초기화 메시지와 함께 현재 사용되는 커널에 대한 정보를 출력하고 있다. 그리고, 해당 커널을 부팅하기 위해서 전달 된 커널 파라미터(Command line)의 정보를 확인 할 수 있으며 해당 커널이 지원하는 CPU 아키텍처에 대한 정보가 나타난다.

[/var/log/dmesg]
BIOS-provided physical RAM map:
 BIOS-e820: 0000000000000000 - 000000000009c000 (usable)
 BIOS-e820: 0000000000100000 - 00000000bf699000 (usable)
 BIOS-e820: 00000000bf699000 - 00000000bf6af000 (reserved)
 BIOS-e820: 00000000bf6af000 - 00000000bf6ce000 (ACPI data)
 BIOS-e820: 00000000bf6ce000 - 00000000c0000000 (reserved)
 BIOS-e820: 00000000e0000000 - 00000000f0000000 (reserved)
 BIOS-e820: 00000000fe000000 - 0000000100000000 (reserved)
 BIOS-e820: 0000000100000000 - 0000000440000000 (usable)

이 메시지는 앞서 보았던 BIOS-e820에 따른 현재 메모리 중에서 사용가능한 영역에 대해서 BIOS로부터 받은 내용을 보여준다. 마지막 메모리 주소는 0x440000000(=17GB)로 확인된다.

DMI 2.6 present.
SMBIOS version 2.6 @ 0xFCAB0
DMI: Dell Inc. PowerEdge R210/05KX61, BIOS 1.10.0 09/10/2013
e820 update range: 0000000000000000 - 0000000000001000 (usable) ==> (reserved)
e820 remove range: 00000000000a0000 - 0000000000100000 (usable)
No AGP bridge found
last_pfn = 0x440000 max_arch_pfn = 0x400000000
x86 PAT enabled: cpu 0, old 0x7040600070406, new 0x7010600070106
e820 update range: 00000000c0000000 - 0000000100000000 (usable) ==> (reserved)
last_pfn = 0xbf699 max_arch_pfn = 0x400000000
initial memory mapped : 0 - 20000000
init_memory_mapping: 0000000000000000-00000000bf699000
 0000000000 - 00bf600000 page 2M
 00bf600000 - 00bf699000 page 4k
kernel direct mapping tables up to bf699000 @ 8000-d000
Use unified mapping for non-reserved e820 regions.
init_memory_mapping: 0000000100000000-0000000440000000
 0100000000 - 0440000000 page 2M
kernel direct mapping tables up to 440000000 @ b000-19000
.... 중략 ....
Reserving 130MB of memory at 48MB for crashkernel (System RAM: 17408MB)
 [ffffea0000000000-ffffea000ebfffff] PMD -> [ffff880028600000-ffff8800363fffff] on node 0
 [ffffea000ec00000-ffffea000edfffff] PMD -> [ffff880038000000-ffff8800381fffff] on node 0
Zone PFN ranges:
  DMA      0x00000001 -> 0x00001000
  DMA32    0x00001000 -> 0x00100000
  Normal   0x00100000 -> 0x00440000
Movable zone start PFN for each node

하드웨어 정보를 인지하고 그에 따라서 BIOS-e820 주소 정보를 계속해서 업데이트하게 된다. 앞서 살펴보았던 start_kernel()에서 진행되는 내용에 따라서 초기화 작업에 따라 지속적으로 갱신된다.

Kernel command line: ro root=UUID=f7b6075b-d9f0-4ce7-bbf8-eec678d60269 intel_idle.max_cstate=0 crashkernel=130M@0M biosdevname=0 console=tty0 console=ttyS0,115200
  • 커널에 전달되는 파라미터를 확인한다

    Memory: 16290300k/17825792k available (5425k kernel code, 1058608k absent, 476884k reserved, 6986k data, 1296k init)
    
  • 앞서 진행했던 초기화 작업에 의한 최종 메모리 사용량을 확인한다

    HPET: 8 timers in total, 5 timers will be used for per-cpu timer
    hpet0: at MMIO 0xfed00000, IRQs 2, 8, 24, 25, 26, 27, 28, 0
    hpet0: 8 comparators, 64-bit 14.318180 MHz counter
    hpet: hpet2 irq 24 for MSI
    hpet: hpet3 irq 25 for MSI
    hpet: hpet4 irq 26 for MSI
    hpet: hpet5 irq 27 for MSI
    hpet: hpet6 irq 28 for MSI
    Switching to clocksource hpet
    
  • 부팅 시점에서 클럭소스를 HPET로 설정

    Trying to unpack rootfs image as initramfs...
    Freeing initrd memory: 27291k freed
    
  • initrd/initramfs를 임시로 root 파일시스템으로 설정하여 자세한 하드웨어 초기화 및 준비를 진행한다

    Refined TSC clocksource calibration: 2925.981 MHz.
    Switching to clocksource tsc
    dracut: Starting plymouth daemon
    usb 1-1: new high speed USB device number 2 using ehci_hcd
    mpt2sas version 20.101.00.00 loaded
    mpt2sas 0000:01:00.0: PCI INT A -> GSI 16 (level, low) -> IRQ 16
    mpt2sas 0000:01:00.0: setting latency timer to 64
    
  • 클럭소스가 TSC로 변경 되고 시스템에 있는 여러 PCI 장치들을 설정하기 시작한다

전체 메시지 중에서 눈여겨 볼만한 부분만 추려서 코멘트를 달았다.

중간에 initramfs를 root 파일시스템으로 설정하고나서 여러 PCI 장치에 대한 초기화 작업을 수행하는 것을 확인 할 수 있을 것이다. 그 전에도 하드웨어 관련된 초기화 작업이 진행되었는데 initramfs가 초기화 되고나면 드라이버(커널 모듈)형태의 장치에 대한 초기화/설정이 본격적으로 이루어 진다.

sd 0:0:0:0: [sda] 585937500 512-byte logical blocks: (300 GB/279 GiB)
sd 0:0:0:0: [sda] Write Protect is off
sd 0:0:0:0: [sda] Mode Sense: bb 00 10 08
sd 0:0:0:0: [sda] Write cache: enabled, read cache: enabled, supports DPO and FUA
 sda: sda1 sda2 sda3
sd 0:0:0:0: [sda] Attached SCSI disk
.... 중략 ....
EXT4-fs (sda3): mounted filesystem with ordered data mode. Opts:
dracut: Remounting /dev/disk/by-uuid/f7b6075b-d9f0-4ce7-bbf8-eec678d60269 with -o noatime,nodiratime,data=writeback,errors=remount-ro,ro
EXT4-fs (sda3): mounted filesystem with writeback data mode. Opts:
dracut: Mounted root filesystem /dev/sda3
SELinux:  Disabled at runtime.
SELinux:  Unregistering netfilter hooks
type=1404 audit(1450120538.478:2): selinux=0 auid=4294967295 ses=4294967295
dracut:
dracut: Switching root
udev: starting version 147
.... 하략 ....

mpt2sas 호스트 인터페이스 드라이버를 통해서 인식 된 sda 디스크 장치에서 커널파라미터로 전달 받은 UUID로 root 파티션을 찾아내고 이를 정상적으로 마운트하는 작업을 거치게 된다. 메시지를 보면 해당 파티션을 기본적으로 마운트 작업을 한 뒤에 다시 /etc/fstab에 설정된 값으로 마운트하는 것을 볼 수 있다. 그리고 이제 init으로 넘어가면서 사용자 영역에서의 부팅 작업이 본격적으로 시작된다. 이 과정에서 네트워크 장치의 활성화 및 시스템 서비스/데몬 들을 실행하게 된다.

이러한 사용자 영역에서의 부팅 과정은 SysV init이나 Systemd를 참고하도록 하자. (이 사이트에도 관련 글이 있다)

END

어쩌면 사용자 영역에서 부팅을 진행하는 부분이 더 이해하기 쉽고 시스템을 사용하는데 있어서 좀 더 유용할 수도 있지만 사용자 영역의 부팅을 위한 사전 준비과정을 개략적으로 알고 있는 것도 시스템을 이해하는데 도움이 될 것 이다. 추가로, 여기에서 설명한 여러 레지스터나 메모리 구조에 대해서는 Intel IA-32 메뉴얼을 참고하면 많은 도움이 된다. (다만, 어렵다..)