본문 바로가기

MINT64 OS 개발

2. 간단한 부트로더 제작

본 내용은 본인 공부에 대한 정리용으로, 자세한 내용이 포함되지 않음을 참고 바랍니다.


Ⅰ. 부팅과 부트로더

1. 부팅과 BIOS

  • 부팅(Booting): PC가 켜진 후, OS가 실행되기 전까지 수행되는 일련의 작업 과정이다.
  • BIOS(Basic Input Ouput System)
    • 부팅 과정 중, 하드웨어와 관련된 작업이다.
    • 보통 PC 메인보드에 ROM이나 플래시 메모리로 존재한다.
    • 전원이 켜짐과 동시에 프로세서가 가장 먼저 실행되는 코드이다.
    • 부팅 옵션 설정이나 시스템 전반적인 설정 값을 관리하는 역할도 겸하고 있으며, 설정 값으로 시스템을 초기화하여 OS를 실행할 수 있는 환경을 만들어주는 역할이다.
    • BIOS가 제공하는 기능은 인터럽트를 통해, 사용할 수 있다.
  • POST(Power On Self Test): BIOS에서 수행하는 각종 테스트나 초기화하는 작업이다.
  • 부트로더
    • 부트스트랩(Bootstrap) 코드라고도 불린다.
    • BIOS에서 처음으로 제어를 넘겨받는 부분이다.
    • 플로피 디스크나 하드디스크 등 저장매체의 가장 앞부분에 존재한다.
    • PC는 디스크나 플래스 메모리 등 다양한 장치로 부팅할 수 있으며, BIOS는 POST가 완료된 후 여러 장치를 검사하여 앞부분에 부트로더가 존재하는지 확인한다.
    • 부트로더가 존재하면, 코드를 0x7C00 주소에 복사한 후, 프로세서가 0x7C00주소부터 코드를 수행하도록 한다.
    • BIOS가 부트로더를 찾지 못했다면, “Operating System Not Found”와 같은 에러 메시지를 출력하고 작업이 중단된다.
    • 부트로더가 디스크에서 메모리로 복사되어 실행되었다는 것은 BIOS에 의해 PC가 정상 구동되었음을 의미한다.

2. 부트 로더의 역할과 구성

  • 부트 로더는 플로피 디스크나 하드 디스크 같은 외부 저장 매체에 존재한다.
    • 저장 매체에서 가장 첫번째 섹터 MBR(Master Boot Record)에 있는 작은 프로그램으로, 섹터(Sector)는 디스크를 구성하는 데이터의 단위로, 섹터 하나는 512byte로 구성되어 있다.
  • 부트 로더의 가장 큰 역할
    • OS실행에 필요한 환경을 설정한다.
    • OS 이미지를 메모리에 복사하는 일을 수행한다.
  • 부트로더는 BIOS가 가장 먼저 실행하는 프로그램으로, 크기는 512byte로 정해져 있다.
    • 부트로더의 공간제약으로 인해, 대부분 OS이미지를 메모리로 복사하고 제어를 넘겨주는 정형화된 작업이 수행된다.
  • 첫번째 섹터에 부트로더가 있는지 확인하는 방법
    • 디스크를 부팅할 용도로 사용하지 않을 경우, 첫번째 섹터에는 부트로더 대신 일반데이터가 저장된다. (부트로더로 인식되어 일반데이터가 실행될 경우, 컴퓨터가 망가지는 불상사가 발생한다.)
    • BIOS의 첫번째 섹터가 부트로더일 경우, 512byte 중 가장 마지막 2byte값은 0x55, 0xAA 로 작성되어 있다.
    • 따라서, BIOS는 마지막 2byte를 통해 부트로더인지 검증하고 부트로더일 경우 실행한다.

 

Ⅱ. 부트 로더 제작을 위한 준비

1. MINT64 OS의 디렉토리 구조 생성

  • MINT64 OS는 리얼모드, 보호모드, IA-32e모드용 코드를 나눠서 관리한다.

2. makefile 파일 생성

  • make 프로그램
    • 소스파일을 이용해서 자동으로 실행파일 또는 라이브러리 파일을 만들어주는 빌드 관련 유틸리티이다.
    • make 프로그램이 빌드를 자종으로 수행하려면, 각 소스 파일의 의존 관계나 빌드 순서, 빌드 옵션 등에 대한 정보가 필요하다. ( 이러한 저장된 파일을 “makefile”이라고 한다. )
  • make 문법
    • 참고: https://www.gnu.org/software/make/manual/ 
      ( MINT64 OS 빌드에 필요한 기능 위주로 작성되어 있다. )
    • make 문법의 기본 형식은 Target, Dependency, Command 세부분으로 구성되어 있다.
      • Target : 일반적으로 생성할 파일을 나타내며, 특정 레이블(Label)을 지정하여 해당 레이블과 관련된 부분만 빌드하는 것이 가능하다.
      • Dependency : Target 생성에 필요한 소스 파일이나 오브젝트 파일 등을 나타낸다.
      • Command : Dependency에 관련된 파일이 수정되면 실행할 명령을 의미한다.
                           ( 명령창이나 터미널에서 실행할 명령 또는 프로그램을 기술한다. )
      • <tab> : 반드시 TAB 문자로 띄어야 한다.
Target: Dependency ...
<tab> Command
<tab> Command
<tab> ...
  • 예제
    • a.c 파일과 b.c 파일을 gcc로 빌드하여, output.exe를 생성하는 makefile 예제이다.
# a.c와 b.c를 통해, output.exe 파일 생성  --> 주석

all: output.exe

a.o: a.c
	gcc -c a.c

b.o: b.c
	gcc -c b.c

output.exe: a.o b.o
	gcc -o output.exe a.o b.o

3. MINT64용 makefile생성

(1) 최상위 디렉터리의 makefile 생성

  • 최상위 makefile목적은 OS이미지 생성을 위해 각 디렉터리의 makefile을 실행하는 것이다.
  • 현재, 부트 로더만 존재하므로 해당 디렉토리로 이동해서 빌드하고 빌드한 결과물을 복사하여 OS이미지를 생성하는 코드이다.
    • @echo는 다음에 오는 문자열을 화면에 출력해주는 명령어이다.
    • clean Target은 빌드 과정에서 생성된 파일을 삭제할 목적으로 정의하였다.
    all: BootLoader Disk.img
    
    BootLoader:
    	@echo
    	@echo =================== Build Boot Loader ===================
    	@echo
    
    	make -C 0.BootLoader
    
    	@echo
      @echo ==================== Build Complete ====================
      @echo
    
    Disk.img: 0.BootLoader/BootLoader.bin
    	@echo
      @echo =================== Disk Image Build Start ===================
      @echo
    
    	cp 0.BootLoader/BootLoader.bin Disk.img
    
    	@echo
      @echo =================== All Build Complete ===================
      @echo
    
    clean:
    	make -C 00.BootLoader clean
    	rm -f Disk.img
    
  • 실행결과
    • make입력 시, 아래와 같은 에러가 발생하면 정상적으로 동작하는 것이다.
    • 이는 아직 BootLoader.asm과 BootLoader.bin파일이 존재하지 않으므로 발생하는 빌드 에러이다.

 

(2) 부트 로더 디렉터리의 makefile 생성

  • 해당 makefile은 BootLoader.asm파일을 nasm 어셈블리어 컴파일러로 빌드하여 BootLoader.bin파일을 생성하기 위한 목적이다.
  • clean target을 정의하여, 디렉토리에 존재하는 BootLoader.bin 파일을 삭제한다.
all: BootLoader.bin

BootLoader.bin: BootLoader.asm
        nasm -o BootLoader.bin BootLoader.asm

Clean:
        rm -f BootLoader.bin
  • 실행결과
    • make입력 시, 아래와 같은 에러가 발생하면 정상적으로 동작하는 것이다.
    • 이는 아직 BootLoader.asm과 BootLoader.bin파일이 존재하지 않으므로 발생하는 빌드 에러이다.

 

Ⅲ. 부트 로더 제작과 테스트

1. 세상에서 가장 간단한 부트 로더

  • 부트 로더를 메모리에 정상적으로 복사하려면
    • 부트 섹터 512byte에서 마지막 2byte에 0x55, 0xAA로 저장하면 된다.
  • 0.BootLoader에서 BootLoader.asm파일 생성
    • BootLoader.asm
      • times 510 - ( $ - $$)
        :현재 어드레스부터 어드레스가 510이 되는 시점까지 작업을 반복 수행
      • times : times 명령 다음에 오는 횟수만큼 작업을 반복하라는 의미
      • $$ : 현재 어드레스가 포함된 섹션의 시작 어드레스
      • $ - $$ : 섹션의 시작을 기준으로 하는 오프셋
      • db 0x00 : 현재 어드레스에 1byte 크기의 0x00을 삽입하라는 의미
      • db : 현재 어드레스에 값을 할당하고 저장
        (db: 1byte / dw: 2byte / dd: 4byte / dq: 8byte)
[ORG 0x00]  ; 코드의 시작 어드레스를 0x00으로 설정
[BITS 16]   ; 이하의 코드는 16비트 코드로 설정

SECTION .text  ; text 섹션(세그먼트 정의)

jmp $ 
; 현재 위치에서 무한 루프 수행
; c언어의 'while(1);'과 같은 의미

times 510 - ($ - $$)  db  0x00
; $: 현재 라인의 어드레스
; $$: 현재 섹션(.text)의 시작 어드레스
; $ - $$: 현재 섹션을 기준으로 하는 오프셋
; 510 - ( $ - $$ ): 현재부터 어드레스 510까지
; db 0x00: 1byte를 선언하고 값은 0x00

db 0x55 ; 1byte를 선언하고 값은 0x55
db 0xAA ; 1byte를 선언하고 값은 0xAA
        ; 어드레스 511, 512에 0x55, 0xAA를 써서 섹터로 표기함
  • 정상적으로 빌드될 경우, MINT64 OS의 최상위 디렉터리에 Disk.img파일이 생성된다.
    • 이전 최상위 디렉토리에서 만든 makefile실행시 생성된다.

 

2. QEMU 실행

(1) 실행

  • 명령어: qemu-system-x86_64 -L . -m 64 -fda ./Disk.img -M pc -curses
    • 64MB의 메모리를 할당하고 MINT64 OS의 이미지 파일을 플로피 디스크로 설정
    • -m 64 : 64MB의 물리 메모리 할당
    • -fda /mint64/Disk.img : 플로피 디스크 이미지로 MINT64 OS이미지(Disk.img) 설정
      • 플로피 디스크 이미지 경로 설정 시, ‘/’ 문자를 사용해야 한다.
        ( \입력 시, QEMU가 정상적으로 실행되지 않을 수 있으니 주의)
    • -M pc : 가상머신을 일반 PC 환경으로 설정

(2) 결과

  • 정상 실행 시

  • 부트 로더가 정상적으로 실행되었는지 확인
    • 부트 로더의 마지막에 있는 0x55와 0xAA값을 0x00, 0x00으로 바꿔본 후 실행해본다.

 

3. 화면 버퍼와 화면 제어

  • 화면에 문자를 출력하려면, 현재 동작 중인 화면 모드와 관련된 비디오 메모리의 어드레스를 알아야 한다.
    • 비디오 메모리는 모드별로 정해진 형식에 따라, 데이터를 채우면 화면에 원하는 문자나 그림을 출력하는 구조로 되어 있다.
  • PC부팅 후, 기본으로 설정되는 화면모드는 텍스트 모드로 화면 크기는 가로 80문자, 세로 25문자이며 비디오 메모리 어드레스는 0xB8000에서 시작한다.
  • 리얼모드 어드레스 계산 방식이 세그먼트 레지스터에 정의된 기준 주소에 범용 레지스터를 조합하여 계산한다.
    • 이를 이용하여, 세그먼트 값을 0xB800으로 설정하면 범용 레지스터를 0x0000으로 설정하여 편리하게 처리할 수 있다.
  • 빨간색 배경에 밝은 녹색으로 ‘M’ 출력
[ORG 0x00]  ; 코드의 시작 어드레스를 0x00으로 설정
[BITS 16]   ; 이하의 코드는 16비트 코드로 설정

SECTION .text  ; text 섹션(세그먼트 정의)

mov ax, 0xB800 ;AX 레지스터에 0xB800복사
mov ds, ax ; DS 세그먼트 레지스터에 AX 레지스터 값 (0xB800)을 복사

mov byte [0x00], 'M' ;DS 세그먼트: 오프셋 0xB800:0x0000에 'M' 복사
mov byte [0x01], 0x4A
;DS 세그먼트: 오프셋 0xB800:0x0001에 0x4A(빨간배경에 밝은 녹색 속성)를
 복사

jmp $
times 510 - ($ - $$)  db  0x00
; $: 현재 라인의 어드레스
; $$: 현재 섹션(.text)의 시작 어드레스
; $ - $$: 현재 섹션을 기준으로 하는 오프셋
; 510 - ( $ - $$ ): 현재부터 어드레스 510까지
; db 0x00: 1byte를 선언하고 값은 0x00

db 0x55 ; 1byte를 선언하고 값은 0x55
db 0xAA ; 1byte를 선언하고 값은 0xAA
        ; 어드레스 51all: BootLoader.bin

BootLoader.bin: BootLoader.asm
        nasm -o BootLoader.bin BootLoader.asm

Clean:
        rm -f BootLoader.bin1, 512에 0x55, 0xAA를 써서 섹터로 표기함

 

4. 화면 정리

  • C 코드로 작성
    • 문자는 0으로 설정해주고, 속성은 녹색배경에 자홍색으로 설정하였다.
#include <stdio.h>

int main(){
	int i = 0;

	char* pcVideoMemory = (char*) 0xB8000;

	while(1){
		pcVideoMemory[i] = 0;
		pcVideoMemory[i+1] = 0x2D;

		i += 2;

		if(i >= 80 * 25 * 2){
			break;
		}
	}
}
  • 작성한 C코드를 Object파일로 변경
gcc -c print_str.c -o print_str -O2
  • objdump를 이용하여 어셈블리어 코드 추출
    • 기본은 AT&T형식으로 추출된다.
    $ objdump -M intel -d clear.o
    
    clear.o:     file format elf64-x86-64
    
    Disassembly of section .text:
    
    0000000000000000 <main>:
       0:	55                   	push   rbp
       1:	48 89 e5             	mov    rbp,rsp
       4:	c7 45 f4 00 00 00 00 	mov    DWORD PTR [rbp-0xc],0x0
       b:	48 c7 45 f8 00 80 0b 	mov    QWORD PTR [rbp-0x8],0xb8000
      12:	00 
      13:	8b 45 f4             	mov    eax,DWORD PTR [rbp-0xc]
      16:	48 63 d0             	movsxd rdx,eax
      19:	48 8b 45 f8          	mov    rax,QWORD PTR [rbp-0x8]
      1d:	48 01 d0             	add    rax,rdx
      20:	c6 00 00             	mov    BYTE PTR [rax],0x0
      23:	8b 45 f4             	mov    eax,DWORD PTR [rbp-0xc]
      26:	48 98                	cdqe   
      28:	48 8d 50 01          	lea    rdx,[rax+0x1]
      2c:	48 8b 45 f8          	mov    rax,QWORD PTR [rbp-0x8]
      30:	48 01 d0             	add    rax,rdx
      33:	c6 00 2d             	mov    BYTE PTR [rax],0x2d
      36:	83 45 f4 02          	add    DWORD PTR [rbp-0xc],0x2
      3a:	81 7d f4 9f 0f 00 00 	cmp    DWORD PTR [rbp-0xc],0xf9f
      41:	7f 02                	jg     45 <main+0x45>
      43:	eb ce                	jmp    13 <main+0x13>
      45:	90                   	nop
      46:	b8 00 00 00 00       	mov    eax,0x0
      4b:	5d                   	pop    rbp
      4c:	c3                   	ret
    

5. 부팅 메시지 출력

  • C 코드로 작성
    • 문자는 0으로 설정해주고, 속성은 녹색배경에 자홍색으로 설정함
#include <stdio.h>

int main(){
	int i = 0;
	int j = 0;

	char* pcVideoMemory = (char*) 0xB8000;
	char* pcMsg = "MINT64 OS Boot Loader Start~!!";
	char cTemp;

	while(1){
		cTemp = pcMsg[i];

		if(cTemp ==0){
			break;
		}

		pcVideoMemory[j] = cTemp;

		i += 1;
		j += 2;
	}
}
  • 작성한 C코드를 Object파일로 변경
gcc -c print_str.c -o print_str
  • objdump를 이용하여 어셈블리어 코드 추출
$ objdump -M intel -d print_str

print_str:     file format elf64-x86-64


Disassembly of section .text.startup:

0000000000000000 <main>:
   0:	48 8d 0d 00 00 00 00 	lea    rcx,[rip+0x0]        # 7 <main+0x7>
   7:	ba 00 80 0b 00       	mov    edx,0xb8000
   c:	b8 4d 00 00 00       	mov    eax,0x4d
  11:	0f 1f 80 00 00 00 00 	nop    DWORD PTR [rax+0x0]
  18:	88 02                	mov    BYTE PTR [rdx],al
  1a:	0f b6 01             	movzx  eax,BYTE PTR [rcx]
  1d:	48 83 c2 02          	add    rdx,0x2
  21:	48 83 c1 01          	add    rcx,0x1
  25:	84 c0                	test   al,al
  27:	75 ef                	jne    18 <main+0x18>
  29:	31 c0                	xor    eax,eax
  2b:	c3                   	ret

6. 최종 코드

  • 자홍색은 별로 안 예뻐서 검은색으로 변경한 코드 (0x20 적용함)
[ORG 0x00]  ; 코드의 시작 어드레스를 0x00으로 설정
[BITS 16]   ; 이하의 코드는 16비트 코드로 설정

SECTION .text  ; text 섹션(세그먼트 정의)

jmp 0x07C0:START

START:
	mov ax, 0x07C0
	mov ds, ax ;segment reset	

	mov ax, 0xB800 
	mov es, ax ; video
	
	mov si,	0

.SCREENCLEARLOOP:
	mov byte [es:si], 0
	mov byte [es:si+1], 0x20

	add si, 2
	cmp si, 80*25*2

	jl .SCREENCLEARLOOP

	mov si, 0
	mov di, 0
.MESSAGELOOP:
	mov cl, byte [si + MSG1]
	cmp cl, 0
	je .MESSAGEEND

	mov byte [es:di], cl
	add si, 1
	add di, 2

	jmp .MESSAGELOOP
.MESSAGEEND:
	jmp $
MSG1: db 'MINT64 OS Boot Loader Start~!!', 0;

times 510 - ($ - $$)  db  0x00

db 0x55
db 0xAA ; 1byte를 선언하고 값은 0xAA
        ; 어드레스 511, 512에 0x55, 0xAA를 써서 섹터로 표기함

'MINT64 OS 개발' 카테고리의 다른 글

6. 32비트 보호 모드로 전환  (0) 2023.05.06
5. OS이미지 메모리에 복사  (0) 2023.05.06
4. 32비트 보호 모드로 전환  (0) 2023.04.13
3. OS이미지 메모리에 복사  (0) 2023.04.10
1. 개요 & 환경 구축  (0) 2023.04.10