64비트 멀티코어 OS 만들기 - 부트 로더 화면 제어

170916


부트 로더 제작과 테스트

지난 시간에 이어서 이제 화면 버퍼와 화면 제어를 하는 법을 구체적으로 알아보겠다.

우선, 화면에 문자를 출력하려면 현재 동작 중인 화면 모드와 관련된 비디오 메모리의 주소를 알아야 한다. PC 부팅 후 기본으로 설정되는 화면 모드는 텍스트 모드로 화면 크기는 가로 80문자, 세로 25문자이며 비디오 메모리 주소는 0xB8000에서 시작한다.

화면에 표시하는 한 문자는 문자값 1바이트와 속성값 1바이트로 구성되며, 총 메모리 크기는 화면 크기를 곱한 크기인 80252=4,000바이트이다.

속성값은 하위 4비트 전경색, 상위 4비트 배경색으로 구분된다. 전경색은 강조 효과만 지원하고, 배경색은 강조와 깜빡임 두 기능을 제공한다.


텍스트 모드 속성값의 구성

| 값 | 배경색, 전경색 | :————- | :————- | | 0x00 | 검은색 | | 0x01 | 파란색 | 0x02 | 녹색 | 0x03 | 청색 | 0x04 | 빨간색 | 0x05 | 자홍색 | 0x06 | 노란색 | 0x07 | 흰색

위 구성은 비트 위치가 0~2인 경우 해당되며, 비트 위치가 3이면 0x00: 효과 없음, 0x01: 하위 3비트 색상에 깜빡임 효과나 강조 효과 추가(배경색)하거나, 하위 3비트 색상에 강조 효과를 추가(전경색)한다.

리얼 모드의 주소 계산 방식은 전에 배웠듯이 세그먼트 레지스터에 정의된 기준 주소에 값을 더해 계산한다. 여기서는, 세그먼트 레지스터의 값을 0xB800으로 설정하면 세그먼트 레지스터:오프셋이 0xB800:0x0000이 되어 범용 레지스터의 0을 비디오 메모리의 첫 번째 주소로 지정할 수 있다. 아래 명령을 참고하자.


세그먼트 레지스터에 비디오 메모리 주소 설정

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

즉, 0xB8000과 0xB8001에 각각 ‘M’과 0x4A를 쓰면 빨간색 배경에 밝은 녹색으로 ‘M’을 출력할 수 있다.

어셈블리어에서 주소에 해당하는 메모리 값을 참조할 때 [] 기호를 사용하며, 기호 앞에 byte(1바이트), word(2바이트), dword(4바이트), qword(8바이트)를 사용하여 메모리 크기를 지정한다. 아래 코드를 보자.

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

위 코드를 실제 부트 로더에 적용하자.


화면 최상단에 문자를 출력하는 부트 로더 소스 코드(BootLoader.asm)

[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 - ( $ - $$ ): 현재부터 주소
                                ; db 0x00: 1바이트를 선언하고 값은 0x00
                                ; time: 반복 수행
                                ; 현재 위치에서 주소 510까지 0x00으로 채움
db 0x55							            ; 1바이트를 선언하고 값은 0x55
db 0xAA							            ; 1바이트를 선언하고 값은 0xAA
                                ; 주소 511, 512에 0x55, 0xAA를 써서 부트 섹터로 표기함

빌드 후, QEMU를 실행하면 아래와 같이 빨간색 배경에 밝은 녹색으로 출력된 M을 확인할 수 있다. 그렇다면 이번에는 세그먼트 레지스터를 초기화하여 더 많은 작업을 실행해보자. BIOS가 부트 로더를 실행했을 때, 세그먼트 레지스터에 BIOS가 사용한 값이 들어 있으므로 세그먼트 레지스터를 초기화하지 않으면 엉뚱한 주소에 접근할 수 있다.

MINT64 OS에서는 0x7C0으로 초기화 했는데, 그 이유는 BIOS가 부트 로더를 디스크에서 읽어 메모리에 복사하는 위치가 0x07C0이기 때문이다. 부트 로더의 코드(Code Segment)와 데이터(Data Segment)는 0x0700부터 512바이트 범위에 존재하므로 CS와 DS 세그먼트 레지스터 모두 0x7C0으로 설정할 수 있다. 여기서는 jmp 명령과 mov 명령을 이용해서 세그먼트 초기화를 진행했다.


세그먼트 레지스터 초기화

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:0x01처럼 [세그먼트 레지스터:오프셋] 형식으로 쓰면 된다.


세그먼트 레지스터 접두사를 사용해서 화면 맨 위에 M을 표시하는 소스 코드

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

여기에 이제 몇 라인을 더 추가해서 메시지를 출력하자. 우선, QEMU 부팅 화면을 깨끗하게 정리하려면 0xB8000 주소부터 80x25x2바이트를 모두 0으로 채우면 되지만, 다른 값으로 채워보자. 예를 들면, 아래 C언어 코드를 확인하자.


화면을 지우는 소스 코드(C언어)

int i = 0;
char* pcVideoMemory = ( char* ) 0xB8000;

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

  i += 2;

  if( i >= 80 * 25 * 2 )
  {
    break;
  }
}
// 위 코드는 0xB8000 주소부터 녹색 모노크롬을 80x25x2 바이트 만큼 채운다는 것이다.

C언어 소스 코드를 objdump.exe를 이용해서 어셈블리어 코드를 추출할 수 있다.

gcc -c a.c -o a.o -O2

-c 옵션은 컴파일만 수행하여 오브젝트 파일 생성, -O2 옵션은 최적화 2단계로 설정해서 필요 없는 코드를 생성하지 않도록 한다.

위 명령어를 실행한 결과는 아래와 같다.

a.o 파일이 생성되면 이 파일을 objdump.exe의 파라미터로 -d 옵션과 함께 넘겨주면 어셈블리어 코드가 화면에 출력된다.

objdump -d a.o

위 명령어를 실행한 결과는 아래와 같다.


화면을 지우는 소스 코드(어셈블리어)

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라인임
    jl .SCREENCLEARLOOP             ; SI 레지스터가 80 * 25 * 2보다 작다면 아직 지우지 못한 영역이 있으므로 .SCREENCLEARLOOP 레이블로 이동

이제 메시지를 출력하는 소스 코드를 작성해보자. 효율적으로 처리하려면 루프와 메시지 문자열을 사용하는 것이 좋다. 마찬가지로, C언어로 구현한 후, 어셈블리어로 바꿔보자.


메시지를 출력하는 소스 코드(C언어)

int i = 0;
int j = 0;
char* pcVideoMemory = ( char* ) 0xB8000;
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로 이동하여 문자 출력 종료

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에서 처리할 수 있게 함

화면을 정리하고 메시지를 출력하는 코드까지 살펴봤다. 설명했던 모든 내용을 모두 조합한 부트 로더 코드는 아래와 같다.


최종 부트 로더 소스 코드(BootLoader.asm)

[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라인임
    jl .SCREENCLEARLOOP             ; SI 레지스터가 80 * 25 * 2보다 작다면 아직 지우지 못한 영역이 있으므로 .SCREENCLEARLOOP 레이블로 이동

	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로 이동하여 문자 출력 종료

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

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

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

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

MESSAGE1:	db 'MINT64 OS Boot Loader Start~!!', 0	; 출력할 메시지 정의
													                        ; 마지막은 0으로 설정하여 .MESSAGELOOP에서 문자열이 종료되었음을 알 수 있도록 함

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를 써서 부트 섹터로 표기함

이제 QEMU(가상머신)으로 부트 로더를 실행하면 메시지가 출력되는 부팅 화면을 볼 수 있다.