Skip to content

Latest commit

 

History

History
594 lines (440 loc) · 35.4 KB

Ch4_내 PC를 부팅하자.md

File metadata and controls

594 lines (440 loc) · 35.4 KB

내 PC를 부팅하자

부팅과 부트로더

모든 OS들은 512바이트 크기의 작은 코드에서 시작한다. 512바이트의 작은 코드는 **부트로더(Boot loader)**라고 불리며, OS의 나머지 코드를 메모리에 복사해 실행한다.

부팅과 BIOS

**부팅(Booting)**은 PC가 켜진 후에 OS가 실행되기 전까지 수행되는 일련의 작업과정을 의미한다. 부팅 과정에서 수행하는 작업에는 프로세서 초기화(멀티코어 관련 처리 포함), 메모리와 외부 디바이스 검사 및 초기화, 부트로더를 메모리에 복사하고 OS를 시작하는 과정 등이 포함된다.

MINT64 OS의 부팅과정이다. PC환경에서는 부팅 과정 중 하드웨어와 관련된 작업을 BIOS(Basic Input/Output System)가 담당하며, BIOS에서 수행하는 각종 테스트나 초기화를 POST(Power On Self Test)라고 부른다.

BIOS는 메인보드에 포함된 펨웨어(Firmware)의 일종으로, 이름 그대로 입출력을 담당하는 작은 프로그램이다. 보통 PC 메인보드에 롬(ROM)이나 플래시 메모리로 존재하며, 전원이 켜짐과 동시에 프로세서가 가장 먼저 실행하는 코드이다. BIOS는 부팅 옵션 설정이나 시스템 전반적인 설정 값(Configuration)을 관리하는 역할도 겸하며, 설정값으로 시스템을 초기화하여 OS를 실행할 수 있는 환경을 만든다. BIOS에서 제공하는 기능은 인터럽트를 통해 사용할 수 없으며, MS-DOS 같은 과거의 16비트 OS는 BIOS의 기능에 많이 의존했다. MINT64 OS도 OS 이미지를 메모리에 복사하고 GUI 모드로 변환할 때 BIOS의 기능을 사용한다.

BIOS는 부팅 과정에서 시스템 초기화 외에 수많은 작업을 하지만, 그중에서 우리에게 가장 중요한 것은 부트로더 이미지를 메모리로 복사하는 단계이다. 부트로더는 부트스트랩(Bootstrap) 코드라고도 불리며 우리가 BIOS에서 처음으로 제어를 넘겨받는 부분이다. 부트로더는 플로피 디스크나 하드 디스크 등 저장 매체의 가장 앞부분에 존재한다. PC는 디스크나 플래시 메모리 등 다양한 장치로 부팅할 수 있으므로 BIOS는 POST가 완료된 후 여러 장치를 검사하여 앞부분에 부트로더가 있는지 확인한다. 부트로더가 존재한다면 코드를 0x7C00 어드레스에 복사한 후 프로세서가 0x7C00 어드레스부터 코드를 수행하도록 한다. 부팅가능한 모든 장치를 검사했는데도 부트로더를 찾을 수 없다면 BIOS 'Operating System Not Found'와 같은 메시지를 출력하고 작업을 중단한다.

부트로더가 디스크에서 메모리로 복사되어 실행되었다는것은 BIOS에 의해 PC가 정상적으로 구동되었다는 것을 의미한다. 다시 말하면 우리가 만든 OS를 메모리에 올려서 실행할 준비가 된것이다.

부트 로더의 역할과 구성

부트로더는 플로피 디스크나 하드 디스크 같은 외부 저장 매체에 있으며, 저장 매체에서 가장 첫번째 섹터 MBR(Master Boot Record)에 있는 작은 프로그램이다. 섹터(Sector)는 디스크를 구성하는 데이터의 단위로, 섹터 하나는 512바이트로 구성된다. 부트로더의 가장 큰 역할은 OS 실행에 필요한 환경을 설정하고, OS 이미지를 메모리에 복사하는 일이다. 부트로더는 BIOS가 가장 먼저 실행하는 중요한 프로그램이므로 기능이 다양하다고 생각할지도 모른다. 하지만, 부트로더는 크기가 512바이트로 정해져 있다. 즉 공간 제약이 있어서 처리할 수 있는 기능이 한정된다. 이렇게 작은 공간에 다양한 기능을 우겨 넣는 일은 무리이므로 대부분 부트로더는 OS 이미지를 메모리로 복사하고 제어를 넘겨주는 정형화된 작업을 수행한다. 4장과 5장에서 만들 부트로더 역시 OS 이미지를 디스크에서 메모리로 복사하는 역할만 수행한다.

부트 로더의 크기 문제는 부트로더의 기능을 최소화해서 해결했다. 그렇다면 BIOS에 첫번째 섹터가 부트로더란 것을 어떻게 알려줄까? 그리고 BIOS는 디스크에서 읽은 첫 번째 섹터가 정상적인 부트로더인지 어떻게 판단할까?

디스크를 부팅할 용도로 사용하지 않는다면, 첫번째 섹터는 부트로더가 아닌 일반 데이터가 저장된다. 만약 BIOS가 실수로 데이터를 메모리에 올려 실행한다면 모니터에 번쩍하는 섬광과 함께 PC가 리부팅된다. 이러한 사태를 방지하려면 BIOS는 첫번째 섹터에 있는 데이터가 부트로더인지 확인해야한다.

이를 위해 BIOS는 읽어들인 512바이트 중에 가장 마지막 2바이트의 값이 0x55, 0xAA인지 검사해서 부트로더인지 확인해야한다. 읽은 데이터가 0x55, 0xAA로 끝나지 않는다면 데이터로 인식하고 부팅 과정을 더 진행하지 않는다. 첫번째 섹터에 부트로더가 아닌 데이터를 저장할 생각이라면 마지막 2바이트는 0x55, 0xAA가 아닌 다른 값으로 써야한다.

부트로더 제작을 위한 준비

이제부터 만들 OS의 이름을 MINT64 OS라고 지칭하겠다. 본격적인 프로그래밍에 앞서, MINT64 OS 프로젝트를 생성한다. 책에서는 이클립스로 진행하지만, 편의를 위해 Jetbrains의 Clion으로 진행하겠다.

프로젝트 생성

Clion에서 MINT64라는 프로젝트를 생성한다. 특별히 설명할 내용은 없으므로 건너 뛰도록 하겠다. MINT64보고 따라오면 된다.

MINT64 OS의 디렉터리 구조 생성

MINT64 OS는 리얼모드, 보호모드, IA-32e모드용 코드를 나눠서 관리한다. 이러한 파일을 디렉터리 하나로 관리하는 것은 매우 비효율적이므로 디렉터리 구조를 다래 그림과 같이 항목별로 나누었다.

부트로더 디렉터리와 대부분 유틸리티 디렉터리는 다른 디렉터리와 달리 소스파일 디렉터리와 임시 파일 디렉터리를 구분하지 않는다. 부트로더는 어셈블리어 파일 하나로 이루어진 작은 프로그램이므로 굳이 디렉터리를 구분하지 않아도 관리가 가능하다. 하지만, 보호모드 커널, IA-32e 커널, 각 응용프로그램은 여러 파일로 복잡하게 구성되므로 혼잡함을 줄이려고 임시 파일 디렉터리를 별로도 생성했다.

아래 이미지는 Clion project explorer 화면이다. 이렇게 디렉터리를 설정해주면 된다.

이제 빌드할때 필요한 make 파일을 작성해보도록 하겠다.

makefile 작성

make문법의 기본 형식은 다음과 같이 Target, Dependency, Command 세 부분으로 구성되어 있다.

Target: Dependency ...
  Command
  Command
  ...

Target은 일반적으로 생성할 파일을 나타내며, 특정 라벨(Label)을 지정하여 해당 라벨과 관련된 부분만 빌드하는것도 가능하다.

Dependency는 Target 생성에 필요한 소스파일이나 오브젝트파일등을 나타낸다.

Command는 Dependency에 관련된 파일이 수정되면 실행할 명령을 의미한다. Command에는 명령창이나 terminal에서 실행할 명령 또는 프로그램을 기술한다. Command앞에 빈 공간은 TAB이다. 반드시 TAB으로 작성해야한다. 그렇지 않으면 make가 정상적으로 실행되지 않을수 있다.

예제를 통해 살펴보도록하자. 아래 예제는 a.c와 b.c 파일을 gcc로 빌드하여 output를 생성하는 makefile 예제이다. 다음과 같이 makefile을 생성한 뒤에 명령창이나 터미널에서 make를 입력하면 간단히 빌드할 수 있다.

# a.c, b.c를 통해서 output 파일을 생성하는 예제

all: output

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

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

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

가장 윗부분에 all이라고 표기한 특별한 Target이 있다. all은 make를 실행하면서 옵션으로 Target을 직접 명시하지 않았을때 기본적으로 사용하는 Target이다. 여러 Target을 빌드할때 all Target의 오른쪽에 순서대로 나열하면 한번에 처리할 수 있다.

make는 빌드를 수행하는 도중에 다른 make를 실행할 수 있다. 이는 빌드 단계를 세부적으로 나누고, 계층적으로 수행할 수 있음을 의미한다. 최상위 디렉터리의 하위에 Library 디렉터리가 있고, 빌드 과정에서 Library 디렉터리를 빌드해야한다면 -C 옵션을 사용해서 다음과 같이 간단히 처리할 수 있다.

# output을 빌드한다
all: output

# Library 디렉터리로 이동한 후 make 수행
libtest.a:
    make -C Library

output.o: output.c
    gcc -c output.c

output: libtest.a output.o
    gcc -o output output.c -ltest -L./

-ltest -L./라는 부분이 있다. -l옵션 다음에 오는건 libtest.a 라이브러리 파일이다. 앞에 lib는 빼고 온다. 예를들어 libm.a를 링크하고 싶을때는 -lm으로 작성한다. 맨뒤에 -L./는 라이브러리 파일의 경로를 나타낸다. 여기서는 현재 디렉토리를 가리킨다.

MINT64용 makefile 생성

최상단 디렉토리에 makefile을 만들고 아래처럼 작성한다.

all: BootLoader Disk.img

BootLoader:
    @echo
    @echo =============== Build Boot Loader ===============
    @echo

    make -C 00.BootLoader

    @echo
    @echo =============== Build Complete ===============
    @echo

Disk.img: 00.BootLoader/BootLoader.bin
    @echo
    @echo =============== Disk Image Build Start ===============
    @echo

    cp 00.BootLoader/BootLoader.bin Disk.img

    @echo
    @echo =============== All Build Complete ===============
    @echo

clean:
    make -C 00.BootLoader clean
    rm -f Disk.img

최상위 makefile의 목적은 OS 이미지 생성을 위해 각 하위 디렉터리의 makefile을 실행하는 것이다. 지금은 부트 로더만 있으므로 해당 디렉터리로 이동해서 빌드하고, 빌드한 결과물을 복사하여 OS이미지를 생서하는것이 전부이다.

최상위 디렉터리에 makefile을 생성했으니 다음은 부트로더 디렉터리에 makefile을 생성할 차례이다. MINT64 OS의 최사우이 디렉터리에 makefile을 생성하는 것과 같은 방법으로 00.BootLoader디렉터리에 makefile을 생성한다.

all: BootLoader.bin

BootLoader.bin: BootLoader.asm
    nasm -o BootLoader.bin BootLoader.asm
    
clean:
    rm -f BootLoader.bin 

부트로더 makefile의 목적은 BootLoader.asm 파일을 nasm 어셈블리어 컴파일러로 빌드하여 BootLoader.bin 파일을 생성하는 것이다. clean Target이 정의되어 잇으며 자신의 디렉터리에 있는 BootLoader.bin 파일을 삭제한다.

최상위 디렉터리의 makefile로 빌드하면 BootLoader.asm파일이 없으므로 아래와 같은 에러가 발생한다.


=============== Build Boot Loader ===============

make -C 00.BootLoader
make[1]: *** No rule to make target `BootLoader.asm', needed by `BootLoader.bin'.  Stop.
make: *** [BootLoader] Error 2

참고로 이런 에러가 발생할 수 있다.

makefile:4: *** missing separator.  Stop.

Command 앞에 tab이 오지 않아서 그런것이다. Clion에서 tab으로 입력해도 스페이스 여러개로 입력된것처럼 입력되기도 한다. vim같은걸로 수정하는걸 추천한다.

부트로더 제작과 테스트

어셈블리어 기초

나같이 어셈블리어를 잘 모르는 사람을 위해 자주 사용되는 어셈블리어를 정리해보았다.

그룹 명령어 설명
사칙연산 add A, B A에 B의 값을 더한 후 A에 저장
사칙연산 sub A, B A에서 B의 값을 뺀 후 A에 저장
사칙연산 mul A AX의 레지스터 값과 A의 값을 곱한 후 AX 또는 DX:AX에 저장
사칙연산 inc A A의 값을 1 증가
사칙연산 dec A A의 값을 1 감소
사칙연산 div A AX나 DX:AX의 값을 A로 나누어 몫과 나머지를 각기 AL와 AH 또는 AX와 D에 저장
논리연산 and A, B A에 B값을 AND하여 A에 저장
논리연산 or A, B A에 B값을 OR하여 A에 저장
논리연산 xor A, B A에 B값을 XOR하여 A에 저장
논리연산 not A, B A의 값을 반전(0->1, 1->0)하여 A에 저장
대입 및 분기 mov A, B B에서 A로 값을 이동
대입 및 분기 cmp A, B 두 값을 비교하여 결과를 FLAGS 레지스터에 적용
대입 및 분기 jmp A 무조건 해당 어드레스로 이동하여 A 위치의 코드를 실행
대입 및 분기 je, ja, jz, jne, jna, jnb, jnz A 조건 분기 명령으로 FLAGS 레지스터의 값에 따라 jmp수행
일반적으로 값을 비교하는 cmp명령어와 함께 사용
Equal(e), Above(a), Bellow(b), Zero(z), Not(n)등의 다양한 조건 포함
함수호출 call A 스택에 call 명령 다음의 어드레스를 삽입하고 A위치의 코드를 실행
함수를 호출하는 용도로 사용
함수호출 ret A 스택을 A 만큼 줄인 후에 되돌아갈 어드레스를 꺼내 해당 어드레스의 코드 실행
일반적으로 call명령과 짝을 이루어 사용
스택 push A A의 값을 스택에 저장
스택 pop A 스택에서 값을 꺼내 A에 저장
인터럽트 int A A번째 소프트웨어 인터럽트 발생
인터럽트 cli 인터럽트를 발생 불가능하도록 설정
인터럽트 sti 인터럽트를 발생 가능하도록 설정
I/O 포트 제어 in A, B I/O 포트 B에서 값을 입력받아 A에 저장
주변 장치에서 값을 읽는 용도로 사용
I/O 포트 제어 out B, A A의 값을 I/O 포트 B에 출력
주변 장치에 값을 쓰는 용도로 사용

실제 명령어는 훨씬 많치면 OS개발할때 이정도 명령어만 사용하기때문에 추가적인 명령어 설명은 생략하도록 하겠다.

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

부트로더를 메모리에 정상적으로 복사하려면 한 가지만 지키면 된다. 부트섹터 512바이트에서 마지막 2바이트를 0x55, 0xAA로 저장하면 된다.

00.BootLoader 디렉터리에 BootLoader.asm 파일을 생성하고 아래와 같이 입력한 후 빌드를 수행한다. 빌드가 정상적으로 끝나면 MINT64 OS의 최상단 디렉터리에 Disk.img 파일이 생성된다

[ORG 0x00]          ; 코드의 시작 어드레스를 0x00으로 설정
[BITS 16]           ; 아래 코드를 16비트 코드로 설정

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

jmp $               ; 현재 위치에서 무한 루프 실행

times 510 - ( $ - $$ )  db  0x00    ; $: 현재 라인의 어드레스
                                    ; $$: 현재 섹션(.text)의 시작 어드레스
                                    ; $ - $$: 현재 섹션을 기준으로 하는 오프셋
                                    ; 510 - ( $ - $$ ): 현재부터 어드레스 510까지
                                    ; db 0x00: 1바이트를 선언하고 값은 0x00
                                    ; time: 반복 수행
                                    ; 현재 위치에서 어드레스 510까지 0x00으로 채움

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

소스코드가 꽤나 복잡해 보이지만, 실제로 의미있는 어셈블리어 명령은 jmp $ 명령뿐이다. 나머지는 어드레스를 정렬하고 0x55와 0xAA를 정확한 위치에 삽입하려고 추가된 부분이다. 앞으로는 jmp $부분에 코드를 추가하면서 부트로더를 완성할 것이다. 이제 잘 동작하는지 확인해보자

> make

=============== Build Boot Loader ===============

make -C 00.BootLoader
nasm -o BootLoader.bin BootLoader.asm

=============== Build Complete ===============


=============== Disk Image Build Start ===============

cp 00.BootLoader/BootLoader.bin Disk.img

=============== All Build Complete ===============

make 명령으로 빌드를 하면 Disk.img 파일이 생성될것이다.

QEMU 실행

빌드한 부트로더는 2장에서 설치한 QEMU를 사용해서 테스트할 수 있으며, QEMU는 가상머신 실행 파일, 옵션과 함께 실행하는 배치파일(.bat)로 구성되어 있다. 실행할 때마다 옵션을 입력하기는 불편하므로 배치파일로 실행을 자동화하면 편하다.

최상단 디렉토리에 qemu-system-x86_64MINT.bat파일을 만들도록 하겠다.

qemu-system-x86_64 -L . -m 64 -fda Disk.img -localtime -M pc

64MB의 메모리를 할당하고 MINT64 OS 이미지 파일을 플로피 디스크로 설정하는 명령어이다. bat파일을 만들었으니 실행해보자 참고로 ./qemu-system-x86_64MINT.bat 명령으로 실행을 못할 수 있다. 실행권한이 없기때문이다. chmod 775 qemu-system-x86_64MINT.bat으로 실행권한을 부여하면된다.

플로피 디스크로 부팅한다는 메시지가 표시되고 멈춘다. 위에서 만든 부트로더는 무한루프를 돌도록 jmp $ 명령을 넣어서 Booting from Floppy에서 더이상 진행이 안된다. 부트로더 마지막에 511, 512번째를 0x55, 0xAA로 지정했기때문에 부트로더로 인식하고 부팅했다. 만약에 이걸 0x00, 0x00으로 다르게 변경하면 어떻게 될까? 아래 사진처럼 부팅 실패 메시지를 표시하고 멈추게 된다.

화면 버퍼와 화면 제어

MINT64 OS에 환영 메시지와 함께 부팅과정을 화면에 표시하는 기능을 넣어보겠다.

화면에 문자를 출력하려면 현재 동작 중인 화면 모드와 관련된 비디오 메모리의 어드레스를 알아야한다. 비디오 메모리는 화면 출력과 관계된 메모리로 모드별로 정해진 형식에 따라 데이터를 채우면 화면에 원하는 문자나 그림을 출력하는 구조로 되어있다.

PC부팅 후 기본으로 설정되는 화면 모드는 텍스트 모드로 화면 크기는 가로 80문자, 세로 25문자이며 비디오 메모리 어드레스는 0xB8000에서 시작한다. 또한 화면에 표시하는 한 문자는 문자값 1바이트와 속성값 1바이트로 구성되며 총 메모리 크기는 화면 크기를 곱한 크기인 80 * 25 * 2 = 4,000바이트이다. 속성값은 하위 4비트의 전경색과 상위 4비트의 배경색으로 구분된다. 각 전경색과 배경색은 다시 최상위 특수 기능 비트와 하위 3비트의 색상으로 구분된다. 전경색은 강조 효과만 지원하지만, 배경색은 강조와 깜박임 두 기능을 제공한다. 전경색과 배경색의 최상위 비트를 모두 강조 기능으로 사용하면 16가지 색상을 표현할 수 있으므로 이를 이용하면 그럴듯한 텍스트 화면을 표시할 수 있다. 아래 사진은 이러한 텍스트 모드의 화면구조이며, 밑에 표는 전경색과 배경색의 값과 의미를 정리한 것이다.

이제 화면에 문자를 표시하려면 0xB8000 어드레스에 문자와 속성을 순서대로 지정하면 된다는 것을 알았다. 이를 이용해서 화면 맨 위에 M을 빨간색 배경에 밝은 녹색으로 출력해 보겠다. 3장에서 설명했듯이 리얼 모드의 어드레스 계산 방식은 세그먼트 레지스터에 정의된 기준 주소 값을 더해 계산한다. 따라서 9xB8000 어드레스에 접근하려면 세그먼트 레지스터에 세그먼트의 기준 주소부터 설정해야한다. 세그먼트 레지스터와 범용 레지스터를 조합해서 0xB8000 어드레스에만 접근할 수 있다면 어떤 값을 설정해도 문제는 없다. 그러나 세그먼트 레지스터의 값을 0xB8000으로 설정하면 세그먼트 레지스터:오프셋이 0xB800:0x0000이 되어 범용 레지스터의 0을 비디오 메모리의 첫 번째 어드레스로 지정할 수 있어서 편리하다. 세그먼트 레지스터에 0xB800을 설정하는 코드는 아래오 같이 mov명령을 이용해서 간단히 처리할 수 있다.

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

DS 세그먼트 레지스터에 0xB800의 값을 설정했으니, 이후 데이터에 접근하는 명령어는 물리 주소 0xB8000이 기준 어드레스로 사용된다. 화면 맨 위의 어드레스는 0xB8000과 같으므로 0xB8000과 0xB8001에 각각 'M'과 0x4A를 쓰면 빨간색 배경에 밝은 녹색으로 'M'을 출력할 수 있다. (위에 그림들 참고)

어셈블리어에서 어드레스에 해당하는 메모리 값을 참조할때 [] 기호를 사용하며, 기호 앞에 byte, word(2바이트), dword(4바이트), qword(8바이트)를 사용하여 메모리 크기를 지정한다. 다음은 [] 기호와 mov 명령을 이용해서 0xB8000와 0xB8001 어드레스에 값을 설정하는 코드이다. DS 세그먼트의 값이 0xB800이므로 0x00, 0x01을 지정하면 세그먼트:오프셋이 0xB800:0x0000과 0xB800:0x0001이 되며, 이를 물리 어드레스로 변환하면 0xB8000, 0xB8001이 된다.

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

전체 소스 코드

[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: 1바이트를 선언하고 값은 0x00
                                    ; time: 반복 수행
                                    ; 현재 위치에서 어드레스 510까지 0x00으로 채움

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

빌드한 후 실행해보면 아래처럼 맨 첫칸에 빨간 바탕해 녹색으로된 'M' 문자를 볼 수 있다.

세그먼트 레지스터 초기화

앞으로 기능을 추가해나가기 위해서 세그먼트 레지스터를 초기화하는 코드가 필요하다. 왜냐하면, BIOS가 부트로더를 실행했을 때 세그먼트 레지스터에는 BIOS가 사용하던 값이 들어잇기 때문이다. 세그먼트 레지스터를 최화하지 않으면 엉뚱한 어드레스에 접근할 수 있으므로 미리 초기화하고 사용해야 좋다.

MINT64 OS에서는 0x07C0으로 초기화 했다. 그 이유는 BIOS가 부트 로더를 디스크에서 읽어 메모리에 복사하는 위치가 0x7C00이기 때문이다. 또한 부트 로더의 코드(Code Segment)와 데이터(Data Segment)는 0x7C00부터 512바이트 범위에 존재하므로 CS와 DS세그먼트 레지스터를 모두 0x07C0을 설정하여 부트 로더의 시작을 기준으로 하도록 했으며, ES세그먼트 레지스터는 화면 출력에 관련된 세그먼트로 사용하려고 0xB800을 설정했다.

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

jmp 0x07C0:START    ; CS 세그먼트 레지스터에 0x07C0을 복사하면서 START 레이블로 이동

START:
    mov ax, 0x07C0  ; 부트로더의 시작 어드레스(0x07C0)를 세그먼트 레지스터 값으로 변환
    mov ds, ax      ; DS 세그먼트 레지스터에 설정
    mov ax, 0xB800  ; 비디오 메모리의 시작 어드레스(0xB800)를 세그먼트 레지스터 값으로 변환
    mov es, ax      ; ES 레지스터에 설정

비디오 모드에 관련된 세그먼트 레지스터가 DS 세그먼트 레지스터에서 ES 세그먼트 레지스터로 변경되었으니 이후 출력에 관계된 코드는 모두 ES 세그먼트 레지스터를 기준으로 하게 수정해야한다.비디오 메모리에 접근하려면 ES 세그먼트 레지스터를 사용해야하는데 어덯게 하면 세그먼트 레지스터를 지정할 수 있을까? 세그먼트 레지스터 접두사를 사용하면 된다. 세그먼트 레지스터 접두사는 해당 명령을 수행하는 동안 일시적으로 세그먼트를 교체한다. 세그먼트 레지스터 접두사를 스는 방법은 아주 간단하다. 어드레스를 지정하는 오퍼랜드에 ES:0x01처럼 [세그먼트 레지스터: 오프셋]형식으로 쓰면 된다. 다음은 화면 맨 위에 M을 출력했던 소스 코드를 세그먼트 레지스터 접두사를 사용하게 수정한 것이다.

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

이렇게 변경한 후 실행하면 위에서처럼 M이 출력된다.

화면 정리 및 부팅 메시지 출력

QEMU를 실행하면 BIOS가 출력한 메세지 때문에 화면이 지저분하다. 이 상태로 부팅 메시지를 출력하면 눈에 잘 띄지 않으므로, 일단 화면부터 깨끗이 지워보자 화면을 깨끗하게 정리하는 가장 간단한 방법은 0xB8000어드레스부터 80252바이트를 모두 0으로 채우기이다. 하지만 속성까지 모두 0으로 채우면 화면에 출력할 문자는 속성값을 같이 지정해야하는 불편함이 있어 문자부분만 0으로 채우고 속성값은 0이 아닌 다른값으로 채울것이다. 검은색 바탕에 밝은 녹색으로 표시하도록 속성값은 0x0A로 진행하겠다.

C언어로 작성하면 아래와 같다.

int i = 0;
char *pcVideoMemory = (char *) 0xB800;
while (1) {
    pcVideoMemory[i] = 0;
    pcVideoMemory[i + 1] = 0x0A;
    i += 2;
    if (i >= 80 * 25 * 2)
        break;
}

위 C언어 소스를 세그먼트 레지스터 접두사를 사용하는 어셈블리어로 변경해보겠다. C언어 소스코드와 어셈블리어 소스 코드를 라인 단위로 구분해서 비교하면 어셈블리어와 C언어가 크게 다르지 않음을 알게 된다. 변경한 어셈블리어 코드는 아래와 같다

    mov     si, 0                   ; SI 레지스터문자열 원본 인덱스 레지스터)를 초기화

.SCREENCLEARLOOP:                   ; 화면을 지우는 루프
    mov byte [ es: si ], 0          ; 비디오 메모리의 문자가 위치하는 어드레스에 0을 복사하여 문자를 삭제
    mov byte [ es: si + 1 ], 0x0A   ; 비디오 메모리의 속성이 위치하는 어드레스에 0x0A를 복사
    add si, 2                       ; 문자와 속성을 설정했으므로 다음 위치로 이동

    cmp si, 80 * 25 * 2             ; 화면 전체 크기는 80문자 * 25라인이다
                                    ; 출력한 문자의 수를 의미하는 SI 레지스터와 비교
    jl  .SCREENCLEARLOOP            ; SI 레지스터가 80 * 25 * 2보다 작다면 아직 지우지 못한 영역이 있으므로 .SCREENCLEARLOOP 레이블로 이동
                                    ; jl은 Jump if less의 줄임말이다.

화면을 정리했으니 메시지를 출력해 보도록하자. C언어로 먼저구현한후 어셈블리어로 변경하겠다.

int i = 0;
int j = 0;
char *pcVideoMemory = (char *) 0xB800;
char *pcMessage = "MINT64 OS Boot Loader Start~!!";
char cTemp;

while (1) {
    cTemp = pcMessage[i];
    
    if (cTemp == 0)
      break;
    
    pcVideoMemory[j] = cTemp;
    i += 1;
    j += 2;
}
mov si, 0                       ; SI 레지스터(문자열 원본 인덱스 레지스터)를 초기화 
    mov di, 0                       ; DI 레지스터(문자열 대상 인덱스 레지스터)를 초기화
    
.MESSAGELOOP:                       ; 메세지 출력하는 루프
    mov cl, byte [ si + MESSAGE1 ]  ; MESSAGE1의 어드레스에서 SI 레지스터 값만큼 더한 위치의 문자를 CL 레지스터에 복사
                                    ; CL 레지스터는 CX 레지스터의 하위 1바이트를 의미
                                    ; 문자열은 1바이트면 충분하므로 CX 레지스터의 하위 1바이트만 사용
    cmp cl, 0                   ; 복사도니 문자와 0을 비교
    je  .MESSAGEEND             ; 복사한 문자의 값이 0이면 문자열이 종료되었음을 의미하므로 .MESSAGEEND로 이동하여 문자 출력 종료
                                ; je는 Jump if equal의 줄임말이다
    
    mov byte [ es: di ], cl     ; 0이 아니면 비디오 메모리 어드레스 0xB800:di에 문자를 출력
    
    add si, 1                   ; SI 레지스터에 1을 더하여 다음 문자열로 이동
    add di, 2                   ; DI 레지스터에 2를 더하여 비디오 메모리의 다음 문자 위치로 이동
                                ; 비디오 메모리는 문자, 속성의 쌍으로 구성되므로 문자만 출력하려면 2를 더해야한다.
                                
    jmp .MESSAGELOOP            ; 메시지 출력 루프로 이동하여 다음 문자 출력
.MESSAGEEND:

MESSAGE1:   db 'MINT64 OS Boot Loader Start~!!', 0  ; 출력할 메시지 정의
                                                    ; 마지막은 0으로 설정하여 .MESSAGELOOP에서 처리할 수 있게 함

전체 소스는 첨부

[ORG 0x00]          ; 코드의 시작 어드레스를 0x00으로 설정
[BITS 16]           ; 아래 코드를 16비트 코드로 설정

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

jmp 0x07C0:START    ; CS 세그먼트 레지스터에 0x07C0을 복사하면서 START 레이블로 이동

START:
    mov ax, 0x07C0  ; 부트로더의 시작 어드레스(0x07C0)를 세그먼트 레지스터 값으로 변환
    mov ds, ax      ; DS 세그먼트 레지스터에 설정
    mov ax, 0xB800  ; 비디오 메모리의 시작 어드레스(0xB800)를 세그먼트 레지스터 값으로 변환
    mov es, ax      ; ES 레지스터에 설정

    mov     si, 0                   ; SI 레지스터문자열 원본 인덱스 레지스터)를 초기화

.SCREENCLEARLOOP:                   ; 화면을 지우는 루프
    mov byte [ es: si ], 0          ; 비디오 메모리의 문자가 위치하는 어드레스에 0을 복사하여 문자를 삭제
    mov byte [ es: si + 1 ], 0x0A   ; 비디오 메모리의 속성이 위치하는 어드레스에 0x0A를 복사
    add si, 2                       ; 문자와 속성을 설정했으므로 다음 위치로 이동

    cmp si, 80 * 25 * 2             ; 화면 전체 크기는 80문자 * 25라인이다
                                    ; 출력한 문자의 수를 의미하는 SI 레지스터와 비교
    jl  .SCREENCLEARLOOP            ; SI 레지스터가 80 * 25 * 2보다 작다면 아직 지우지 못한 영역이 있으므로 .SCREENCLEARLOOP 레이블로 이동
                                    ; jl은 Jump if less의 줄임말이다.

    mov si, 0                       ; SI 레지스터(문자열 원본 인덱스 레지스터)를 초기화
    mov di, 0                       ; DI 레지스터(문자열 대상 인덱스 레지스터)를 초기화

.MESSAGELOOP:                       ; 메세지 출력하는 루프
    mov cl, byte [ si + MESSAGE1 ]  ; MESSAGE1의 어드레스에서 SI 레지스터 값만큼 더한 위치의 문자를 CL 레지스터에 복사
                                    ; CL 레지스터는 CX 레지스터의 하위 1바이트를 의미
                                    ; 문자열은 1바이트면 충분하므로 CX 레지스터의 하위 1바이트만 사용
    cmp cl, 0                   ; 복사도니 문자와 0을 비교
    je  .MESSAGEEND             ; 복사한 문자의 값이 0이면 문자열이 종료되었음을 의미하므로 .MESSAGEEND로 이동하여 문자 출력 종료
                                ; je는 Jump if equal의 줄임말이다

    mov byte [ es: di ], cl     ; 0이 아니면 비디오 메모리 어드레스 0xB800:di에 문자를 출력

    add si, 1                   ; SI 레지스터에 1을 더하여 다음 문자열로 이동
    add di, 2                   ; DI 레지스터에 2를 더하여 비디오 메모리의 다음 문자 위치로 이동
                                ; 비디오 메모리는 문자, 속성의 쌍으로 구성되므로 문자만 출력하려면 2를 더해야한다.

    jmp .MESSAGELOOP            ; 메시지 출력 루프로 이동하여 다음 문자 출력
.MESSAGEEND:

MESSAGE1:   db 'MINT64 OS Boot Loader Start~!!', 0  ; 출력할 메시지 정의
                                                    ; 마지막은 0으로 설정하여 .MESSAGELOOP에서 처리할 수 있게 함


jmp $               ; 현재 위치에서 무한 루프 실행

times 510 - ( $ - $$ )  db  0x00    ; $: 현재 라인의 어드레스
                                    ; $$: 현재 섹션(.text)의 시작 어드레스
                                    ; $ - $$: 현재 섹션을 기준으로 하는 오프셋
                                    ; 510 - ( $ - $$ ): 현재부터 어드레스 510까지
                                    ; db 0x00: 1바이트를 선언하고 값은 0x00
                                    ; time: 반복 수행
                                    ; 현재 위치에서 어드레스 510까지 0x00으로 채움

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

실행하면 아래처럼 나온다