10. 32비트 보호모드로 전환하자

리얼모드에서 보호모드로 전환하려면 크게 6단계를 거쳐야한다.
상위 2단계는 보호모드전환에 필요한 자료구조를 생성하는 단계, 나머지 4단계는 생성된 자료구조를 프로세서에 설정하는 단계이다.
보호모드에서 반드시 생성해야하는 자료구조는 세그먼트 디스크립터와, GDT이다.
두가지 자료구조는 보호모드로 전환하는 즉시 프로세서에 의해 참조되므로 미리생성해야 한다.

세그먼트 디스크립터 생성

  • 세그먼트 디스크립터는 세그먼트의 정보를 나타내는 자료구조이다.
  • 우리가 만들 os에서는 보호모드의 기본 기능만 사용하므로 4GB전체 메모리 공간을 지정하는 커널코드와 데이터 세그먼트만 사용한다.
  • 세그먼트 디스크립터는 크게 코드 세그먼트 디스크립터와 데이터 세그먼트 디스크립터로 나누어 진다.
  • 코드 세그먼트 디스크립터는
    • 실행 가능한 코드가 포함된 세그먼트에 대한 정보를 나타내며, CS 세그먼트 셀렉터(리얼모드에서는 레지스터, 보호모드에서는 셀렉터)에 사용된다.
  • 데이터 세그먼트 디스크립터는
    • 데이터가 포함된 세그먼트에 대한 정보를 나타내며, CS를 제외한 나머지 셀렉터에 사용할 수 있다.
    • 스택영역 또한 데이터의 한종류 이므로 데이터 세그먼트 디스크립터를 사용한다.

<세그먼트 디스크립터의 구조>
보호 모드의 세그먼트 디스크립터는 8바이트( 2^(8_8) = 2^64 )로 아래 그림과 같이 필드가 나눠져 있음.
_

** # 왜 기준주소 값이 23:16인가? 7:0 이 아니라? **

<세그먼트 디스크립터의 필드와 의미>

세그먼트의 각필드에 값을 설정하기전에 우리가 설정하고자 하는 세그먼트에 대해서 정리

  1. 커널 코드와 데이터용 세그먼트 디스크립터 각 1개
  2. 커널 코드와 데이터용 세그먼트는 0~4GB까지 모든 영역에 접근할 수 있어야 함
  3. 보호모드용 코드와 데이터에 사용할 기본 오퍼랜드 크기는 32비트여야 함
  4. 보호기능은 사용하지 않으며, 프로세서의 명령을 사용하는데 제약이 없어야 하므로 최상위 권한(0)이어야 함

세그먼트 디스크립터 설정

<타입 필드의 값과 의미>

  • S필드 = 1

    • 코드 세그먼트와 데이터 세그먼트는 세그먼트 디스크립터 이므로
  • 코드 세그먼트 타입 = 0x0A

    • 0x0A(Execute/Read)
  • 데이터 세그먼트 타입

    • 0x02(Read/Write)
  • 기준 주소 = 0

    • 기준이 0부터 시작해야 전체영역(4gb)을 쓸 수 있음.
  • 크기 필드 = 2^20 = 1MB

  • G 필드 = 1

    • 우리가 만들 os의 커널 세그먼트 디스크립터는 4GB 전체 영역에 접근 할 수 있어야 한다.
    • G필드의 값을 1로 설정하면 크기 필드에 4kb를 곱한 것이 실제 세그먼트의 크기가 된다.
    • (크기 필드 * G 필드 ) - 기준 주소 = 세그먼트 디스크립터 크기 = 4GB 가 되어야 된다는 말
  • D/B 필드 = 1

    • D/B필드는 오퍼랜드의 크기를 설정 가능
    • 보호모드는 32비트로 동작하므로 기본 오퍼랜드의 크기도 32비트여야 한다.
  • L 필드 = 0

    • IA-32e모드의 64비트 서브모드 또는 32비트 호환모드를 설정할 수 있음
    • 현재 생성하려는 디스크립터는 보호모드용이므로 0로 설정
  • 권한 필드 = 0

    • 보호 기능을 수행함.
    • 프로세서는 디스크립터의 권한 필드에 설정된 값과 세그먼트 셀렉터의 권한을 비교하여 접근 가능한지 판단
    • 만들고자 하는 os의 보호모드는 권한을 따로 구분하지 않으므로 권한필드를 모두 최상위 권한(0)으로 설정
  • P = 1

    • 유효한 세그먼트 디스크립터라는 것을 알려야 하기 때문에 1
  • AVL = 0

    • 임의로 사용가능 한 필드이지만 현재 만드려는 os는 별도의 값을 쓰지 않으므로 0
  • 세그먼트 타입의 비트 8에 있는 접근 여부 비트는 프로세서에서 설정하는 비트이다.

  • 프로세서는 해당 디스크립터가 참조될때마다 비트8을 1로설정한다.

  • 이를 이용하면 비트8을 이용해서 특정 세그먼트 디스크립터가 사용되었는지 여부를 확인할 수 있다.

  • 비트10에 있는 역방향 확장비트는 아래로(상위 addr -> 하위 addr)자라는 스택을 위한 옵션

    • 스택 세그먼트의 크기를 동적으로 확장/축소할 목적으로 사용한다.
    • 역방향 확장기능을 사용하면 세그먼트 크기(limit)값이 기준주소에서 아래방향(0바이트)방향으로 적용
    • 세그먼트의 범위는(세그먼트의 기준 주소 - 세그먼트 크기) ~ 세그먼트 기준주소가 된다.
    • 또한 비트10에 위치한 접근 승인 비트는 권한에 관계없이 해당 코드 세그먼트에 접근할 수 있음을 나타낸다.

자료구조와 메모리공간, 어셈블리어 코드와의 관계

  • 인텔 문서는 하위방향에서 상위방향, 오른쪽에서 왼쪽으로 어드레스가 증가한다.
  • 어셈블리어 코드는 하위 어드레스의 데이터부터 상위 어드레스의 데이터 순서로 기술하므로 메모리 공간에 나타난 순서와 반대이다.

GDT 정보생성

  • GDT(Global Descriptor Table)자체는 연속된 디스크립터의 집합
  • 코드세그먼트 디스크립터와 데이터 세그먼트 디스크립터를 연속된 어셈블리어 코드로 나타내면 그 전체 영역이 GDT가 된다.
  • null 디스크립터를 가장 앞부분에 추가 해야한다.
  • null 디스크립터는 프로세서에 의해 예약된 디스크립터로 모든 필드가 0으로 초기화된 디스크립터이며 일반적으로 참조되지 않음
  • 프로세서에 GDT의 시작 어드레스와 크기 정보를 로딩해야 한다. 따라서 이것을 저장하는 아래와 같은 자료구조가 필요하다.

<GDT 정보를 저장하는 자료구조>

  • GDT정보를 저장하는 자료구조의 기준주소는
    • 32비트 크기
    • 데이터 세그먼트의 기준주소와 관계없이 어드레스 0을 기준으로 하는 선형주소
    • 선형주소이기 때문에 물리메모리주소로 변환해야 한다.
      1. GDT의 선형주소는 현재 코드가 실행되고 있는 세그먼트의 기준주소를 알고 있으므로 현재 세그먼트의 시작을 기준으로 GDT오프셋을 구하고
      2. 세그먼트 기준 주소를 더해주면 구할 수 있다.
    • 현재 코드는 부트로더에 의해 0x10000에 로딩되어 실행되고 있으므로 자료구조를 생성할 때 GDT 오프셋에 0x10000을 더해주면 선형주소가 된다.

보호모드로 전환

보호모드로 전환하려면 GDTR 레지스터 설정, CR0 컨트롤레지스터 설정, jmp 명령 수행 3단계를 수행하면 된다.

프로세서에 GDT정보 설정

  • 프로세서에 GDT정보를 설정하려면 lgdt명령어를 사용하면 됨
    • 2바이트 크기와 4바이트 기준주소로 된 GDT 정보 자료구조를 오퍼랜드로 받는다.(아래 코드 처럼)
lgdt [ GDTR ] ; GDTR 자료구조를 프로세서에 설정하여 GDT 테이블을 로드
  • 보호모드로 전환하는 과정과 전환 후 인터럽트 설정을 완료하기 전까지는 인터럽트가 발생하지 않게 하는 것이 좋다.
    • 인터럽트가 발생하면 프로세서는 인터럽트 처리함수(인터럽트 핸들러)를 찾을 수 없음
    • 인터럽트를 발생할 수 없게 설정하려면 cli명령어를 사용하고 인터럽트를 발생할 수 있게 설정하려면 sti명령어를 사용한다.

CR0 컨트롤 레지스터 설정

CR0컨트롤 레지스터에는 Cache, Paging, FPU(Floating Process Unit)등과 관련된 필드가 포함되어 있다.

<CR0의 구조와 보호모드 전환을 위한 설정 값>

  • 우리가 만들 os에서 보호모드는 거쳐가는 임시모드에 불과

  • 세그먼테이션 기능 이외에는 사용하지 않음

  • 페이징, 캐시, 메모리 정렬 검사, 쓰기 금지 기능은 모두 사용하지 않음으로 설정

  • FPU도 안쓰니까 임시값으로 설정

  • FPU관련 필드

    • EM = 0

      • FPU 명령을 소프트웨어로 에뮬레이션하지 않게 한다.
    • ET = 1

      • ?
    • MP = 1, TS = 1, NE = 1

      • 현재 FPU를 사용하면 정상적으로 작동하지 않는다.

      • 따라서 FPU명령이 실행되었을 때 예외가 발생하게 설정한다.
        위의 설명대로 CR0 설정하는 코드

        mov eax, 0x4000003B ; PG=0, CD=1, NW=0, AM=0, WP=0, NE=1, ET=1, TS=1, EM=0, MP=1, PE=1
        mov cr0, eax        ; CR0에 eax를 넣어서 플래그 설정 후 보호모드로 전환
  • CR0를 설정했으니 이제 남은 작업은 jmp명령으로 cs를 갱신하고 32bit코드로 변경하는 것

외부메모리, 캐시 동기화 정책

  • 외부메모리의 내용과 캐시의 내용을 동기화 시키는 정책은 크게 Write-through방식, Write-back 방식으로 구분됨
  • Write-through
    • 메모리에 쓰기가 수행될 때마다 캐시의 내용과 외부메모리의 내용을 모두 갱신
  • Write-back
    • 방금쓴 내용을 캐시에만 갱신하고 외부메모리에 쓰는 작업은 최대한 뒤로 미룬다.
    • 캐시는 작아서 새로운 데이터를 캐시에 넣으려면 덮어씌어야 함
    • Write-back방식은 캐시의 내용을 버릴 때 캐시의 내용을 외부메모리에 갱신함으로서 외부메모리에 접근하는 횟수를 줄이는 것

보호모드로 전환과 세그먼트 셀렉터 초기화

  • 준비는 끝났다. 이제 남은 일은 32비트 코드를 준비한 후, 한 줄의 어셈블리어 코드로 cs의 값을 바꾸는 것
    • [ BITS 32 ] 로 이 명령어 아래의 모든 코드를 32비트 코드로 생성
  • cs를 교체하려면 jmp명령과 세그먼트 레지스터 접두사를 사용해야 한다.
    • jmp dword 0x08: ( PROTECTEDMODE - $$ + 0x10000 )
      • 세그먼트 정보는 디스크립터에 저장
      • 세그먼트를 실행시키는 것은 셀렉터가 하는 일
        • 셀렉터에 어드레스를 설정하면 된다.
        • GDT내의 디스크립터의 어드레스를 사용, 이는 GDT의 시작 어드레스로부터 떨어진 거리(offset)를 의미한다.
      • 명령어 분석
        • 커널 코드 세그먼트가 0x00을 기준으로 하는 반면 실제코드는 0x10000을 기준으로 실행되고 있으므로 오프셋에 0x10000을 더해서 세그먼트 교체 후에도 같은 선형주소를 가리키게 함

보호모드로 전환하는 코드

; 커널 코드 세그먼트를 0x00을 기준으로 하는 것으로 교체하고 EIP의 값을 0x00을 기준으로 재설정
; cs : EIP
jmp dword 0x08: ( PROTECTEDMODE - $$ + 0x10000 )
[BITS 32]       ; 이하의 코드는 32비트 코드로 설정
PROTECTEDMODE:
  mov ax, 0x10  ; 보호모드 커널용 데이터 세그먼트 디스크립터를 ax에 저장
  mov ds, ax
  mov es, ax
  mov fs, ax
  mov gs, ax

  ; 스택을 0x00000000 ~ 0x0000FFFF 영역에 64kb크기로 생성
  mov ss, ax
  mov esp, 0xFFFE
  mov ebp, 0xFFFE

보호모드용 PRINTSTRING 함수

보호모드용 PRINTSTRING함수인 PRINTMESSAGE함수

; 메시지를 출력하는 함수
; 스택에 x좌표, y좌표, 문자열
PRINTMESSAGE:
    push ebp    ; bp를 스택에 삽입
    mov ebp, esp    ; bp에 sp 저장
    ; 함수에서 임시로 사용하는 레지스터들로 함수의 마지막 부분에서 스택에 삽입된 값을 꺼내 원래 값으로 복원
    push esi    
    push edi
    push eax
    push ecx
    push edx

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; x,y의 좌표로 비디오 메모리의 어드레스를 계산함
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; y 좌표를 이용해서 먼저 라인 어드레스를 구함
    mov eax, dword [ ebp +12 ]    ; 파라미터 2(화면 좌표 y)를 eax에 설정
    mov esi, 160            ; 한라인의 바이트 수(2*80컬럼)를 esi에 설정
    mul esi                ; eax와 esi를 곱하여 화면 y addr 계산
    mov edi, eax            ; 계산된 화면 y addr를 edi에 설정

    ; x좌표를 이용해서 2를 곱한 후 최종 addr구함
    mov eax, dword [ ebp +8 ]    ; vkfkalxj 1(화면 좌표 x)를 eax에 설정
    mov esi, 2            ; 한 문자를 나타내는 바이트 수(2)를 esi에 설정
    mul esi                ; eax와 esi를 곱하여 화면 x addr를 계산
    add edi, eax            ; 화면 y addr와 계산된 x addr를 더해서 실제 비디오 메모리 어드레스를 계산

    ; 출력할 문자열의 어드레스
    mov esi, dword [ ebp + 16 ]    ; vkfkalxj 3(출력할 문자열의 어드레스)

.MESSAGELOOP:            ; 메세지를 출력하는 루프
    mov cl, byte [ esi ]    ; esi가 가리키는 문자열 위치에서 한문자를 cl에 복사
                ; cl은 ecx의 하위 1바이트를 의미
                ; 문자열은 1바이트면 충분하므로 ecx의 하위 1바이트만 사용

    cmp cl, 0        ; 복사된 문자와 0을 비교
    je .MESSAGEEND        ; 복사된 문자의 값이 0이면 문자열이 종료되었음을 의미하므로 .MESSAGEEND로 이동하여 문자 출력 종료

    mov byte [ edi +0xB8000 ], cl    ; 0이 아니라면 비디오 메모리 addr
                    ; 0xB8000 + edi에 문자를 출력

    add esi, 1        ; 다음 문자열로 이동
    add edi, 2        ; 비디오 메모리의 다음 문자 위치로 이동
                ; 비디오 메모리는 (문자, 속성) 형식이므로 문자만 출력하려면 2를 더해야 함

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

.MESSAGEEND:
    ; 함수 사용이 끝나면 스택 복원
    pop edx
    pop ecx
    pop eax
    pop edi
    pop esi
    pop ebp        ; bp 복원
    ret
  • 리얼모드의 PRINTMESSAGE 함수와의 차이점은
    • 오프셋이 4의 배수로 바뀌었다.
    • 대부분 32비트 범용 레지스터로 수정되었다.
    • 리얼모드에서 비디오 메모리 어드레스 지정을 위해 사용하던 es가 사라졌다는 것
      • 리얼모드에서는 64kb 범위의 어드레스만 접근가능했으므로 화면 표시를 위해 세그먼트가 필요했음
      • 그러나 보호모드는 4gb 전 영역에 접근이 가능함
      • 따라서 es를 제거하고 직접 비디오 메모리에 접근해서 데이터를 쓰도록 수정

보호모드용 커널 이미지 빌드와 가상 os이미지 교체

커널 엔트리 포인트 생성

./01.Kernel32/Source/EntryPoint.s
EntryPoint.s 파일은 보호모드 커널의 가장 앞부분에 위치하는 코드
보호모드 전환과 초기화를 수행하여 이후에 위치하는 코드를 위한 환경을 제공한다.

  • EntryPoint는 우ㅚ부에서 해당 모듈을 실행할 때 실행을 시작하는 지점을 의미
[ORG 0x00]    ; 코드의 시작 어드레스를 0x00으로 설정
[BITS 16]    ; 이하 코드는 16비트 코드로 설정

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

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 코드 영역
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
START:
    mov ax, 0x1000    ; 보호모드 엔트리 포인트의 시작 어드레스(0x10000)를 세그먼트 레지스터 값으로 변환
    mov ds, ax    ; ds에 설정
    mov es, ax    ; es에 설정

    cli        ; 인터럽트 발생 못하게
    lgdt [ GDTR ]    ; gdtr 자료구조를 프로세서에 설정하여 gdt 테이블을 로드
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 보호 모드로 진입
    ; Disable Pagin, Disable Cache, Internal FPU, Disable Align Check,
    ; Enable ProtectedMode
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    mov eax, 0x4000003B    ; PG=0, NW=0, AM=0, WP=0, NE=1, ET=1, TS=1, EM=0, MP=1, PE=1
    mov cr0, eax        ; cr0에 위에서 저장한 플래그를 설정하여 보호모드로 전환

    ; 커널 코드 세그먼트를 0x00을 기준으로 하는 것으로 교체하고 EIP의 값을 0x00을 기준으로 재설정
    ; cs 세그먼트 셀렉터 : EIP
    jmp dword 0x08: ( PROTECTEDMODE - $$ + 0X10000 )

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; 보호모드로 진입
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[BITS 32]    ; 이하 코드는 32비트 코드로 설정
PROTECTEDMODE:
    mov ax, 0x10    ; 보호모드 커널용 데이터 세그먼트 디스크립터를 ax에 저장
    mov ds, ax    
    mov es, ax
    mov fs, ax
    mov gs, ax

    ; 스택을 0x00000000 ~ 0x0000FFFF영역에 64kb크기로 생성
    mov ss, ax
    mov esp, 0xFFFE    ; esp addr = 0xfffe
    mov ebp, 0xFFFE    ; ebp addr = 0xfffe

    ; 화면에 보호모드로 전환되었다는 메시지를 찍는다.
    push ( SWITCHSUCCESSMESSAGE - $$ + 0X10000 )    ; 출력할 메시지의 어드레스를 스택에 삽입
    push 2                        ; 화면 y 좌표(2)를 스택에 삽입
    push 0                        ; 화면 x 좌표(0)를 스택에 삽입
    call PRINTMESSAGE                ; PRINTMESSAGE 함수 호출
    add esp, 12                    ; 삽입한 파라미터 제거

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

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 함수 코드 영역
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 메시지를 출력하는 함수
; 스택에 x좌표, y좌표, 문자열
PRINTMESSAGE:
    push ebp    ; bp를 스택에 삽입
    mov ebp, esp    ; bp에 sp 저장
    ; 함수에서 임시로 사용하는 레지스터들로 함수의 마지막 부분에서 스택에 삽입된 값을 꺼내 원래 값으로 복원
    push esi    
    push edi
    push eax
    push ecx
    push edx

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; x,y의 좌표로 비디오 메모리의 어드레스를 계산함
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; y 좌표를 이용해서 먼저 라인 어드레스를 구함
    mov eax, dword [ ebp +12 ]    ; 파라미터 2(화면 좌표 y)를 eax에 설정
    mov esi, 160            ; 한라인의 바이트 수(2*80컬럼)를 esi에 설정
    mul esi                ; eax와 esi를 곱하여 화면 y addr 계산
    mov edi, eax            ; 계산된 화면 y addr를 edi에 설정

    ; x좌표를 이용해서 2를 곱한 후 최종 addr구함
    mov eax, dword [ ebp +8 ]    ; vkfkalxj 1(화면 좌표 x)를 eax에 설정
    mov esi, 2            ; 한 문자를 나타내는 바이트 수(2)를 esi에 설정
    mul esi                ; eax와 esi를 곱하여 화면 x addr를 계산
    add edi, eax            ; 화면 y addr와 계산된 x addr를 더해서 실제 비디오 메모리 어드레스를 계산

    ; 출력할 문자열의 어드레스
    mov esi, dword [ ebp + 16 ]    ; vkfkalxj 3(출력할 문자열의 어드레스)

.MESSAGELOOP:            ; 메세지를 출력하는 루프
    mov cl, byte [ esi ]    ; esi가 가리키는 문자열 위치에서 한문자를 cl에 복사
                ; cl은 ecx의 하위 1바이트를 의미
                ; 문자열은 1바이트면 충분하므로 ecx의 하위 1바이트만 사용

    cmp cl, 0        ; 복사된 문자와 0을 비교
    je .MESSAGEEND        ; 복사된 문자의 값이 0이면 문자열이 종료되었음을 의미하므로 .MESSAGEEND로 이동하여 문자 출력 종료

    mov byte [ edi +0xB8000 ], cl    ; 0이 아니라면 비디오 메모리 addr
                    ; 0xB8000 + edi에 문자를 출력

    add esi, 1        ; 다음 문자열로 이동
    add edi, 2        ; 비디오 메모리의 다음 문자 위치로 이동
                ; 비디오 메모리는 (문자, 속성) 형식이므로 문자만 출력하려면 2를 더해야 함

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

.MESSAGEEND:
    ; 함수 사용이 끝나면 스택 복원
    pop edx
    pop ecx
    pop eax
    pop edi
    pop esi
    pop ebp        ; bp 복원
    ret

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 데이터 영역
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 아래의 데이터들을 8바이트에 맞춰 정렬하기 위해 추가
align 8, db 0

; GDTR의 끝을 8byte로 정렬하기 위해 추가
dw 0x0000
; GDTR 자료구조 정의
GDTR:
    dw GDTEND - GDT - 1        ; 아래에 위치하는 gdt테이블의 전체 크기
    dd ( GDT - $$ + 0x10000 )    ; 아래에 위치하는 gdt테이블의 시작 addr

;GDT 테이블 정의
GDT:
    ; null디스크립터, 반드시 0으로 초기화해야 함
    NULLDescriptor:
        dw 0x0000
        dw 0x0000
        db 0x00
        db 0x00
        db 0x00
        db 0x00

    ; 보호 모드 커널용 코드 세그먼트 디스크립터
    CODEDESCRIPTOR:
        dw 0xFFFF    ; Limit [15:0]
        dw 0x0000    ; Base [15:0]
        db 0x00        ; Base [23:16]
        db 0x9A        ; P=1, DPL=0, code segment, Execute/Read
        db 0xCF        ; G=1, D=1, L=0, Limit[19:16]
        db 0x00        ; Base [31:24]

    ; 보호모드 커널용 데이터 세그먼트 디스크립터
    DATADESCRIPTOR:
        dw 0xFFFF    ; limit [15:0]
        dw 0x0000    ; base [15:0]
        db 0x00        ; base [23:16]
        db 0x92        ; P=1, DPL=0, data segment, read/write
        db 0xCF        ; G=1, D=1, L=0, limit[19:16]
        db 0x00        ; base [31:24]
GDTEND:

; 보호모드로 전환되었다는 메시지
SWITCHSUCCESSMESSAGE: db 'Switch To Protected Mode Success!', 0

times 512 - ( $ - $$ ) db 0x00    ; 512바이트를 맞추기 위해 남은 부분을 0으로 채움

makefile 수정과 가상 os 이미지 파일 교체

./01.Kernel32/makefile

all: Kernel32.bin

Kernel32.bin: Source/EntryPoint.s
  nasm -o Kernel32.bin $<

clean:
  rm -f Kernel32.bin
  • $< 매크로: Dependency(:의 오른쪽)의 첫번 째 파일을 의미하는 매크로

./makefile


...이상 동일...

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

  car $^ > Disk.img

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

...이하 동일...
  • $^ 매크로: Dependency(:의 오른쪽)에 나열된 전체파일을 의미
    • 위에서는 00.BootLoader/BootLoader.bin 01.Kernel32/Kernel32.bin 으로 치환된다.
    • 위의 두개의 파일을 합쳐 Disk.img를 생성한다.

os이미지 통합과 qemu 실행

빌드한 보호모드 커널의 이미지의 크기는 512바이트(1섹터)밖에 되지 않으므로 부트로더가 한섹터를 로딩한 후 나머지 1023섹터를 읽으려다 정지한다.

00.BootLoader/BootLoader.asm 의 TOTALSECTORCOUNT를 아래와 같이 바꾸자

TOTALSECTORCOUNT: dw 1

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

'Project > OS' 카테고리의 다른 글

9. 테스트를 위한 가상 os이미지 생성  (0) 2019.07.16
8. os 이미지 로딩 기능 구현  (0) 2019.07.16
7. 디스크에서 os이미지 로딩  (0) 2019.07.16
6. Bootloader 만들기  (0) 2019.07.11
5. 부팅과 부트로더  (0) 2019.07.11

9. 테스트를 위한 가상 os이미지 생성

가상 os이미지는 이전에 했던 os이미지 로딩 기능 구현이 제대로 돌아가는지 확인을 위해 만드는 임시적인 것이다.
자신의 섹터번호를 화면 위치에 대응시켜서 0~9까지 번호를 출력하게 만들것.

1024섹터나 되는 코드를 작성하려면 nasm의 전처리기(Preprocessor)를 사용할 것이다.

./01.Kernel32/VirtualOS.asm

[ORG 0x00]
[BITS 16]

SECTION .text

jmp 0x1000:START

SECTORCOUNT:  dw 0x0000 ; 현재 실행중인 섹터 번호를 저장

START:
  ; 비디오 초기화
  mov ax, cs
  mov ds, ax
  mov ax, 0xB800
  mov es, ax

  mov ax, 2                               ; 한문자를 나타내는 바이트 수(2)를 ax에 설정
  mul word [ SECTORCOUNT ]                ; ax와 섹터 수를 곱함
  mov si, ax                              ; 곱한 결과를 si에 설정
  mov byte [ es : si + (160 * 2 ) ], '0'  ; 계산된 결과를 비디오 메모리에 오프셋으로 삼아 세번째 라인부터 화면에 0을 출력

  add word [ SECTORCOUNT ], 1             ; 섹터 수를 1증가

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

  times 512 - ( $ - $$ )  db 0x00
  • SECTORCOUNT라는 메모리 어드레스는 정상저긍로 처리된 섹터의 수를 기록한다.
  • 위의 코드는 1섹터만 작성 한 것으로 1024섹터를 만들어야 한다.
    • 1024섹터중 마지막섹터를 제외한 1023섹터의 코드는 화면에 자신을 출력하는 코드 및 다음 섹터의 어드레스로 이동하는 코드를 반복하면 된다.
    • 마지막 섹터는 더이상 섹터가 없으므로 무한루프를 수행하면 된다.

./01.Kernel32/VirtualOS.asm

[ORG 0x00]
[BITS 16]

SECTION .text

jmp 0x1000:START

SECTORCOUNT:      dw 0x0000 ; 현재 실행중인 섹터 번호를 저장
TOTALSECTORCOUNT  equ 1024  ; 가상 os의 총 섹터 수 최대 1152섹터(0x90000byte)까지 가능

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 코드 영역
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
START:
  ; 비디오 초기화
  mov ax, cs
  mov ds, ax
  mov ax, 0xB800
  mov es, ax

  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ; 각 섹터별로 코드를 생성
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  %assign i 0               ; value i = 0;
  %rep TOTALSECTORCOUNT     ; TOTALSECTORCOUNT 만큼 반복
    %assign i i + 1         ; i++;

    ; 현재 실행중인 코드가 포함된 섹터의 위치를 화면 좌표로 변환
    mov ax, 2                               ; 한문자를 나타내는 바이트 수(2)를 ax에 설정
    mul word [ SECTORCOUNT ]                ; ax와 섹터 수를 곱함
    mov si, ax                              ; 곱한 결과를 si에 설정
    mov byte [ es : si + (160 * 2 ) ], '0' + ( i % 10 )  ; 계산된 결과를 비디오 메모리에 오프셋으로 삼아 세번째 라인부터 화면에 0을 출력
    add word [ SECTORCOUNT ], 1             ; SECTORCOUNT++;

    ; if 마지막 섹터 일경우 무한루프 수행
    ; else 다음 섹터로 이동해서 코드 수행
    %if i == TOTALSECTORCOUNT               ; 마지막 섹터이면
      jmp $                                 ; 현재위치에서 무한루프
    %else                                   ; 마지막 센터 아니면
      jmp ( 0x1000 + i * 0x20 ): 0x0000     ; 다음 섹터 오프셋으로 이동
    %endif

    times 512 - ( $ - $$ )  db 0x00
  %endrep
  • line 9: TOTALSECTORCOUNT equ 1024 ; 가상 os의 총 섹터 수 최대 1152섹터(0x90000byte)까지 가능
    • sector의 최댓값이 1152인 이유는
    • os이미지가 로딩되는 어드레스가 0x10000이고 상위 0xA0000이후 영역은 비디오 메모리로 사용되기 때문이다.
    • 따라서 os를 로딩할 수 있는 전체 크기는 0x90000byte(576KB)가 되며, 섹터가 512 바이트 크기이므로 최대 1152섹터가 된다.

./01.Kernel32/makefile

all: VirtualOS.bin

VirtualOS.bin: VirtualOS.asm
  nasm -o VirtualOS.bin VirtualOS.asm

clean:
  rm -f VirtualOS.bin

os이미지 통합과 qemu 실행

  1. 01.Kernel32에서 make 수행 하여 가상 os이미지를 빌드
  2. 부트 이미지를 복사해서 부팅이미지를 만드는 기존 방식을 수정하여 부트로더와 가상 os이미지를 하나의 파일로 합치도록 수정

./makefile

all: BootLoader Kernel32 Disk.img

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

  make -C 00.BootLoader

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

Kernel32:
  @echo
  @echo =================== Build 32bit Kernel ===================
  @echo

  make -C 01.Kernel32

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

Disk.img: BootLoader Kernel32
  @echo
  @echo =================== Disk Image Build Start ===================
  @echo 

  cat 00.BootLoader/BootLoader.bin 01.Kernel32/VirtualOS.bin > Disk.img

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

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

<실행 결과>

issue: 모든 섹터를 다돌아서 출력을 하지 못한다. 64bit 멀티코어 OS 제작하기 [4] - 3 : 부트 로더 테스트용 가상 이미지 제작에서 코드, makefile을 모두 복사, 붙여넣기 해도 마찬가지 이다.

'Project > OS' 카테고리의 다른 글

10. 32비트 보호모드로 전환하자  (0) 2019.07.18
8. os 이미지 로딩 기능 구현  (0) 2019.07.16
7. 디스크에서 os이미지 로딩  (0) 2019.07.16
6. Bootloader 만들기  (0) 2019.07.11
5. 부팅과 부트로더  (0) 2019.07.11

8. OS 이미지 로딩 기능 구현

디스크 읽기 기능 구현

  • 우리가 만들 os의 이미지는 크게 부트로더, 보호모드 커널, IA-32e모드 커널로 구성되어있음.
  • 각 부분은 섹터 단위로 정렬해서 하나의 부팅 이미지 파일로 합친다.
  • 디스크의 첫번째 섹터는 부트로더로 BIOS가 메모리에 로딩한다.
  • 두번째 섹터부터 os이미지 크기만큼을 읽어서 메모리에 복사하면 된다.
  • 디스크를 읽을 때는 섹터->헤드->트랙 순으로 읽어서 올린다.
  • 섹터번호를 순서대로 증가시키며 읽다가 마지막 섹터에서 헤드와 트랙번호를 증가시키는 것이 핵심 포인트

1024섹터 크기의 이미지를 메모리로 복사하는 소스코드(C언어)

int main(int argc, char* argv[]){
  int iTotalSectorCount = 1024;
  int iSectorNumber = 2;
  int iHeadNumber = 0;
  int iTrackNumber = 0;
  //실제 이미지를 복사할 어드레스(물리주소)
  char* pcTargetAddress = (char*)0x10000;

  while(1){
    //전체 섹터 수를 하나씩 감소시키면서 0이 될때까지 섹터를 복사
    if(iTotalSectorcount == 0){
      break;
    }
    iTotalSectorCount = iTotalSectorCount - 1;

    //1섹터를 읽어들여서 메모리 어드레스에 복사
    //BIOSReadOneSector: BIOS 섹터읽기 기능을 호출하는 임의의 함수
    if(BIOSReadOneSector(iSectorNumber, iHeadNumber, iTrackNumber, pcTargetAddress) == ERROR){
      HandleDiskError();
    }

    //1섹터는 512(0x200) 바이트이므로, 복사한 섹터 수만큼 어드레스 증가
    pcTargetAddress = pcTargetAddress + 0x200;

    //섹터->헤드->트랙 순으로 번호 증가
    iSectorNumber = iSectorNumber + 1;
    if(iSectorNumber < 19){
      continue;
    }

    iHeadNumber = iHeadNumber ^ 0x01; //헤드의 번호는 0과 1이 반복되므로 XOR 연산을 이용
    iSectorNumber = 1;

    if(iHeadNumber != 0){
      continue;
    }

    iTrackNumber = iTrackNumber + 1;
  }

  return 0;
}

void HandleDiskError(){
  printf("DISK Error!");
  while(1);
}

1024섹터 크기의 이미지를 메모리로 복사하는 소스코드(어셈블리어)

TOTALSECTORCOUNT: dw  1024  ; 부트로더를 제외한 os이미지의 크기
                            ; 최대 1151섹터(0x90000byte)까지 가능
SECTORNUMBER:     db  0x02  ; os이미지가 시작하는 섹터번호를 저장하는 영역
HEADNUMBER:       db  0x00  ; os이미지가 시작하는 헤드번호를 저장하는 영역
TRACKNUMBER:      db  0x00  ; os이미지가 시작하는 트랙번호를 저장하는 영역

  ; 디스크의 내용을 메모리로 복사할 어드레스(ES:BX)를 0x10000으로 설정
  mov si, 0x1000              ; os이미지를 복사할 어드레스(0x10000)를
                              ; 세그먼트 레지스터 값으로 변환
  mov es, si                  ; ES세그먼트 레지스터에 값 설정
  mov bx, 0x0000              ; BX레지스터에 0x0000을 설정하여 복사할
                              ; 어드레스를 0x1000:0000(0x10000)으로 최종 설정

  mov di, word [ TOTALSECTORCOUNT ] ; 복사할 os이미지의 섹터 수를 DI레지스터에 설정

READATA:                      ; 디스크를 읽는 코드의 시작
  ; 모든 섹터를 다 읽었는지 확인
  cmp di, 0       ; 복사할 os 이미지의 섹터 수를 0과 비교
  je READEND      ; 복사할 섹터 수가 0이라면 다 복사 했으므로 READEND로 이동
  sub di, 0x1     ; 복사할 섹터 수를 1감소


  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ; BIOS Read Function 호출
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  mov ah, 0x02                  ; BIOS 서비스 번호 2(Read Sector)
  mov al, 0x01                  ; 읽을 섹터 수는 1
  mov ch, byte [ TRACKNUMBER ]  ; 읽을 트랙 번호 설정
  mov cl, byte [ SECTORNUMBER ] ; 읽을 섹터 번호 설정
  mov dh, byte [ HEADNUMBER ]   ; 읽을 헤드 번호 설정
  mov dl, 0x00                  ; 읽을 드라이브 번호(0=Floppy)설정
  int 0x13                      ; 인터럽트 서비스 수행
  jc HANDLEDISKERROR            ; 에러가 발생했다면 HANDLEDISKERROR로 이동

  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ; 복사할 어드레스와 트랙, 헤드, 섹터 어드레스 계산
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  add si, 0x0020                ; 512(0x200)바이트만큼 읽었으므로, 이를 세그먼트 레지스터 값으로 변환
  mov es, si                    ; ES 세그먼트 레지스터에 더해서 어드레스를 한 섹터만큼 증가

  ; 한 섹터를 읽었으므로 섹터 번호를 증가시키고 마지막 섹터(18)까지 읽었는지 판단
  ; 마지막 섹터가 아니면 섹터 읽기로 이동해서 다시 섹터 읽기 수행
  mov al, byte [ SECTORNUMBER ] ; 섹터번호를 AL 레지스터에 설정
  add al, 0x01                  ; 섹터번호를 1 증가
  mov byte [ SECTORNUMBER ], al ; 증가시킨 섹터번호를 SECTORNUMBER에 다시 설정
  cmp al, 19                    ; 증가시킨 섹터번호를 19와 비교
  jl READDATA                   ; 섹터번호가 19미만이라면 READDATA로 이동

  ; 마지막 섹터까지 읽었으면(섹터번호가 19이면) 헤드를 토글(0->1, 1->0)하고,
  ; 섹터번호를 1로 설정
  xor byte [ HEADNUMBER ], 0x01   ; 헤드번호를 0x01과 XOR하여 토글(0->1, 1->0)
  mov byte [ SECTORNUMBER ], 0x01 ; 섹터 번호를 다시 1로 설정

  ; 만약 헤드가 1->0으로 바뀌었으면 양쪽 헤드를 모두 읽은 것이므로 아래로 이동하여
  ; 트랙 번호를 1증가
  cmp byte [ HEADNUMBER ], 0x00   ; 헤드번호를 0x00과 비교
  jne READDATA                    ; 헤드번호가 0이 아니면 READDATA로 이동

  ; 트랙을 1증가시킨 후, 다시 섹터 읽기로 이동
  add byte [ TRACTNUMBER ], 0x01  ; 트랙번호를 1증가
  jmp READDATA                    ; READDATA로 이동

READEND:

HANDLEDISKERROR:                  ; 에러를 처리하는 코드
;;;색략;;;
  jmp $

스택초기화와 함수구현

  • 함수를 사용하려면 stack이 꼭 필요하다.

  • 스택은 Last In First Out 형태의 자료구조이다.

  • x86프로세서에서는 함수를 호출한 코드의 다음 어드레스, 되돌아갈 어드레스를 저장하는 용도로 스택을 사용한다.

  • 함수를 호출하면 프로세서가 자동으로 되돌아올 어드레스를 스택에 저장, 호출된 함수에서 ret 명령어를 요청하면 자동으로 스택에서 어드레스를 꺼내 호출한 다음 어드레스로 이동하는 것.

  • 함수 호출을 위해 가장 먼저 해야할 일은 스택 생성이다.

  • SS(스택 세그먼트 레지스터)는 스택영역으로 사용할 세그먼트의 기준주소를 지정

  • SP(스택 포인터 레지스터)는 데이터를 삽입하고 제거하는 top을 지정

  • BP(베이스 포인터 레지스터)는 스택의 기준주소를 임시로 지정할 때 사용

  • 16비트모드는 최대 64kb(0x10000)를 스택영역으로 지정할 수 있다.

    • SS에 0x0000을 설정한다면 사용가능한 영역은 0x00000 ~ 0x0FFFF까지가 된다.
  • SS로 스택세그먼트의 범위는 지정할 수 있지만, 실제 스택의 크기는 지정할 수 없다.

  • 스택의 실제 크기는 sp와 bp의 초기값으로 지정한다.

  • sp가 움직이면서 항상 top의 값을 가지고 있다.

<스택 초기화 코드>

  • 0x10000(64kb)어드레스 부터는 os이미지가 로딩 되므로 0x10000이하 부터 스택을 사용한다.*
  • 스택이 쌓이는 방향은 0xffff 부터 시작해서 0x0000까지 이다.

다시 화면 출력함수로 돌아와서,
화면에서 원하는 위치에 문자열을 출력하려면 x좌표, y좌표, 출력할 문자열 어드레스가 필요하다.
이 3가지를 함수 파라미터로 정의하고 스택에 삽입하는 순서를 정할 것이다.
c언어와 연계를 고려한다면 중복작업을 피할 수 있게 c언어의 호출규약(cdecl 방식)을 따르는 것이 좋다.
c언어는 파라미터의 역순(오른쪽에서 왼쪽)으로 삽입하여 스택에서 꺼낸 순서가 파라미터순서와 같게 한다.

c언어의 함수 호출 코드

PrintMessage( iX, iY, pcString );

어셈블리어의 함수 호출 코드

push word [ pcString ]  ; 문자열의 어드레스를 스택에 삽입
push word [ iY ]        ; 화면의 y좌표를 스택에 삽입
push word [ iX ]        ; 화면의 x좌표를 스택에 삽입
call PRINTMESSAGE       ; PRINTMESSAGE 함수 호출
add sp, 6               ; 스택에 삽입된 함수 파라미터 3개 (2바이트 * 3)제거
  • sp에 6을 더하는 이유는 함수파라미터로 스택에 삽입된 값을 제거하기 위함
  • 16비트 모드에서는 스택에 2바이트(word)크기로 삽입/제거되고 삽입은 sp를 아래로 이동시킨다.

호출되는 쪽 코드를 살펴보자.
스택의 특정위치를 기준으로 오프셋을 이용해 접근하면 파라미터를 찾게 된다.
그러나 스택의 top을 가리키는 sp는 push, pop에 따라 계속 변하기 때문에 문제가 된다.
고정된 값을 가리키는 레지스터를 사용하는 것이 편리하다.
이러한 역할을 하는 것이 bp이며 호출된 함수는 bp + 오프셋으로 파라미터에 접근하게 된다.

그리고 호출되기 전후의 레지스터 상태가 같아야 한다.
이를위해 호출되는 함수에서는 레지스터의 값을 미리 스택에 저장해두고 수행이 끝나면 이를 복원한다.

어셈블리어 함수의 일반적인 형식

push bp                 ; bp를 스택에 삽입
mov bp, sp              ; bp에 sp의 값을 설정
                        ; bp를 이용해서 파라미터에 접근할 목적
                        ; 호출된 직후의 sp값을 저장하여 bp와 고정된 오프셋으로 파라미터에 접근하게 함(아래 그림 참고)

; 레지스터 백업
push es
push si
push di
push ax
push cx
push dx

;;; 생략 ;;;

mov ax, word [ bp + 4 ] ; word: 메모리에 접근할 때  2byte단위로 접근
mov bx, word [ bp + 6 ] ; 해석하면 메모리의 bp + 6 어드레스에 있는 값을 2byte로 끊어서 가져와라
mov cx, word [ bp + 8 ]

;;; 생략 ;;;

; 레지스터 복원
pop dx
pop cx
pop ax
pop di
pop si
pop es
pop bp
ret                       ; 복원 후 리턴

  • 함수를 호출하기 전에는 스택에 함수 파라미터만 들어 있음
  • call명령 후
    • 자동으로 복귀 어드레스를 스택에 저장하고
    • sp 값에 2를 빼서 복귀 어드레스를 가리키게 한다.
    • 호출된 함수는 파라미터 사용을 위해 bp의 값을 스택에 저장하고
    • sp의 값으로 바꾼다.
  • 저장 후 bp의 값은 자신이 저장된 스택의 위치를 가리키고 있음
    • 이때 함수 파라미터는 bp를 기준으로 일정한 값만큼 증가하는 어드레스에 위치하고 있다.

보호모드에서 사용되는 세가지 함수 호출 규약

호출규약(calling convention)은 함수를 호출할 때 파라미터와 복귀 어드레스 등을 지정하는 규칙

  • stdcall 방식은 파라미터를 스택에 저장하면, 호출된 쪽에서 스택을 정리한다.
  • cdecl 방식은 파라미터를 스택에 저장하면, 함수를 호출한 쪽에서 스택을 정리한다.
  • fastcall 방식은 일부 파라미터를 레지스터에 저장하는 것을 제외하면 stdcall방식과 같다.

이전에는 16비트모드 기준으로 레지스터와 스택을 설명했음.
보호모드는 32비트 이므로 레지스터와 스택의 크기가 리얼모드의 2배이다.
따라서 스택의 기본크기는 4byte(dword)가 된다.
당연히 오프셋도 리얼모드의 2배이다.

함수 호출 예(c언어)

int Add(int iA, int iB, int iC)
{
  return iA + iB + iC;
}

void main()
{
  int iReturn;

  iReturn = Add(1, 2, 3);
}

함수 호출 예(어셈블리어 - stdcall 방식)

  • stdcall은 파라미터를 스택에 넣을 때 오른쪽에서 왼쪽 순서로 집어 넣는다.
  • stdcall은 함수의 반환값은 eax레지스터(32비트 ax 레지스터)를 사용하며 스택에서 파라미터를 제거하는 작업을 호출된 함수가 처리하게 한다.

함수 호출 예(어셈블리어 - cdecl 방식)

  • cdecl은 파라미터를 스택에 넣을 때 오른쪽에서 왼쪽 순서로 집어 넣는다.
  • 함수의 반환값은 eax 레지스터를 사용
  • stdcall과 단한가지 차이점은 스택에서 파라미터를 제거하는 작업을 호출한 함수가 처리하게 한다.

함수 호출 예(어셈블리어 - fastcall 방식)

  • 처음 2개의 파라미터를 ecx와 edx에 삽입한다는 점을 제외하고는 stdcall과 같다.

최종 부트로더 소스코드

os이미지가 정상적으로 로딩되었다면 0x10000위치로 이동해서 보호모드 커널 코드를 실행하도록 했다.

./00.BootLoader/BootLoader.asm

[ORG 0x00]   ; Code start address : 0x00
[BITS 16]    ; 16-bit environment

SECTION .text  ; text section(Segment)

jmp 0x07C0:START    ; copy 0x0C70 to cs, and goto START

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; OS에 관련된 환경설정값
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
TOTALSECTORCOUNT:    dw 1024    ; 부트로더를 제외한 os이미지의 크기
                ; 최대 1152 섹터(0x90000byte)까지 가능

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 코드 영역
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
START:
    mov ax, 0x07C0 ; convert start address to 0x0C70
    mov ds, ax   ; set ds register
    mov ax, 0xB800 ; base video address
    mov es, ax   ; set es register(videos address)

    ;  스택을 0x0000:0000 ~ 0x0000:FFFF 영역에 64kb크기로 생성
    mov ax, 0x0000    ; 스택 세그먼트의 시작 어드레스(0x0000)를 세그먼트 레지스터 값으로 변환
    mov ss, ax    ; SS세그먼트 레지스터에 설정
    mov sp, 0xFFFE    ; SP레지스터의 어드레스를 0xFFFE로 설정
    mov bp, 0xFFFE    ; BP레지스터의 어드레스를 0xFFFE로 설정

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 화면을 모두 지우고 속성값을 녹색으로 설정
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    mov si,    0    ; SI 레지스터(문자열 원본 인덱스 레지스터)를 초기화

.SCREENCLEARLOOP:
    mov byte [ es: si ], 0     ; delete character at si index
    mov byte [ es: si + 1], 0x0A  ; copy 0x)A(black / gree)
    add si, 2            ; go to next location
    cmp si, 80 * 25 *2       ; compare si and screen size
    jl .SCREENCLEARLOOP       ; end loop if si == screen size

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 화면 상단에 시작 메시지 출력
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    push MESSAGE1        ; 출력할 메시지의 어드레스를 스택에 삽입
    push 0            ; 화면 Y좌표(0)를 스택에 삽입
    push 0            ; 화면 x좌표(0)를 스택에 삽입
    call PRINTMESSAGE    ; PRINTMESSAGE 함수 호출
    add sp, 6        ; 삽입한 파라미터 제거

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; OS이미지를 로딩한다는 메시지 출력
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    push IMAGELOADINGMESSAGE    ; 출력할 메시지의 어드레스를 스택에 삽입
    push 1                ; 화면 y좌표(1)를 스택에 삽입
    push 0                ; 화면 x좌표(0)를 스택에 삽입
    call PRINTMESSAGE        ; PRINTMESSAGE 함수 호출
    add sp, 6            ; 삽입한 파라미터 제거

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 디스크에서 os이미지를를 로딩
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 디스크를 읽기 전에 먼저 리셋
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
RESETDISK:                    ; 디스크를 리섹하는 코드의 시작
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; BIOS Reset Function호출
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 서비스 번호 0, 드라이브 번호(0=Floppy)
    mov ax, 0
    mov dl, 0
    int 0x13
    ; 에러가 발생하면 에러 처리로 이동
    jc HANDLEDISKERROR

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 디스크에서 섹터를 읽음
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 디스크의 내용을 메모리로 복사할 어드레스(ES:BX)를 0x10000으로 설정
    mov si, 0x1000                ; os이미지를 복사할 어드레스(0x10000)를 세그먼트 레지스터 값으로 변환
    mov es, si                ; es 세그먼트 레지스터에 값 설정
    mov bx, 0x0000                ; bx레지스터에 0x0000을 설정하여 복사할 어드레스를 0x1000:0000(0x10000)으로 최종 설정

    mov di, word[ TOTALSECTORCOUNT ]    ; 복사할 OS이미지의 섹터 수를 DI레지스터에 설정

READDATA:                    ;디스크를 읽는 코드의 시작
    ; 모든 섹터를 다 읽었는지 확인
    cmp di, 0            ; 복사할 os이미지의 섹터수를 0과 비교
    je READEND            ; 복사할 섹터 수가 0이라면 다 복사 했으므로 READEND로 이동
    sub di, 0x1            ; 복사할 섹터 수를 1감소

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; BIOS read function 호출
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    mov ah, 0x02            ; BIOS 서비스 번호2(Read Sector)
    mov al, 0x1            ; 읽을 섹터 수는 1
    mov ch, byte [ TRACKNUMBER ]    ; 읽을 트랙 번호 설정
    mov cl, byte [ SECTORNUMBER ]    ; 읽을 섹터 번호 설정
    mov dh, byte [ HEADNUMBER ]    ; 읽을 헤드 번호 설정
    mov dl, 0x00            ; 읽을 드라이브 번호(0=플로피) 설정
    int 0x13            ; 인터럽트 서비스 수행
    jc HANDLEDISKERROR        ; 에러가 발생했다면 HANDLEDISKERROR로 이동

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 복사할 어드레스와 트랙, 헤드, 섹터 어드레스 계산
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    add si, 0x0020            ; 512(0x200)바이트만큼 읽었으므로 이를 세그먼트레지스터 값으로 변환
    mov es, si            ; es세그먼트 레지스터에 더해서 어드레스를 한 섹터 만큼 증가

    ; 한섹터를 읽었으므로 섹터 번호를 증가시키고 마지막 섹터(18)까지 읽었는지 판단
    ; 마지막 섹터가 아니면 섹터 읽기로 이동해서 다시 섹터 읽기 수행
    mov al, byte[ SECTORNUMBER ]    ; 섹터번호를 al레지스터에 설정
    add al, 0x01            ; 섹터 번호를 1증가
    mov byte[ SECTORNUMBER ], al    ; 증가시킨 섹터번호를 SECTORNUMBER에 다시 설정
    cmp al, 19            ; 증가시킨 섹터 번호를 19와 비교
    jl READDATA            ; 섹터번호가 19미만이라면 READDATA로 이동

    ; 마지막 섹터까지 읽었으면(섹터 번호가 19이면) 헤드를 토글(0->1, 1->0)하고, 섹터 번호를 1로 설정
    xor byte[ HEADNUMBER ], 0x01    ; 헤드 번호를 0x01과 xor하여 토글(0->1, 1->0)
    mov byte[ SECTORNUMBER ], 0x01    ; 섹터 번호를 다시 1로 설정

    ; 만약 헤드가 1->0으로 바뀌었으면 양쪽 헤드를 모두 읽은 것이므로 아래로 이동하여
    ; 트랙번호를 1증가
    cmp byte [ HEADNUMBER ], 0x00    ; 헤드 번호를 0x00과 비교
    jne READDATA            ; 헤드 번호가 0이 아니면 READDATA로 이동

    ; 트랙을 1증가시킨 후 다시 섹터 읽기로 이동
    add byte [ TRACKNUMBER ], 0x01    ; 트랙번호를 1증가
    jmp READDATA            ; READDATA로 이동

READEND:

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; os이미지가 완료되었다는 메시지를 출력
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    push LOADINGCOMPLETEMESSAGE    ; 출력할 메시지의 어드레스를 스택에 삽입
    push 1                ; 화면 y좌표(1)를 스택에 삽입
    push 20                ; 화면 x좌표(20)를 스택에 삽입
    call PRINTMESSAGE        ; PRINTMESSAGE 함수 호출
    add sp, 6            ; 삽입한 파라미터 제거

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 로딩한 가상 os이미지 실행
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    jmp 0x1000:0x0000

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 함수코드 입력
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 디스크 에러를 처리하는 함수
HANDLEDISKERROR:
    push DISKERRORMESSAGE    ; 에러 문자열의 어드레스를 스택에 삽입
    push 1            ; 화면 y좌표(1)를 스택에 삽입
    push 20            ; 화면 x좌표(20)를 스택에 삽입
    call PRINTMESSAGE    ; PRINTMESSAGE 함수 호출

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

; 메시지를 출력하는 함수
; PARAM: x좌표, y좌표, 문자열
PRINTMESSAGE:
    push bp        ; 베이스포인터레지스터(BP)를 스택에 삽입
    mov bp, sp    ; bp에 sp의 값을 설정
            ; bp를 이용해서 파라미터에 접근할 목적

    push es        ; es세그먼트 레지스터부터 dx레지스터까지 스택에 삽입
    push si        ; 함수에서 임시로 사용하는 레지스터로 함수의 마지막 부분에서 스택에 삽입된 값을 꺼내 원래 값으로 복원
    push di
    push ax
    push cx
    push dx

    ; es에 비디오 모드 어드레스 지정
    mov ax, 0xB800    ; 비디오 메모리 시작 어드레스(0x0B8000)를 세그먼트 레지스터 값으로 변환
    mov es, ax    ; es세그먼트 레지스터에 설정

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; x, y좌표로 비디오 메모리의 어드레스를 계산함
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; y좌표를 이용해서 먼저 라인 어드레스를 구함
    mov ax, word [ bp + 6 ]    ; 파라미터 2(화면좌표 Y)를 ax레지스터에 설정
    mov si, 160        ; 한 라인의 바이트 수(2*80컬럼)를 si에 설정
    mul si            ; ax와 si를 곱하여 화면 y어드레스 계산
    mov di, ax        ; 계산된 화면 y어드레스를 di에 설정

    ; x좌표를 이용해서 2를 곱한 후 최종 어드레스를 구함
    mov ax, word [ bp +4 ]    ; 파라미터 1(화면 좌표 x)를 ax레지스터에 설정
    mov si, 2        ; 한문자를 나타내는 바이트 수(2)를 si에 설정
    mul si            ; ax와 si를 곱하여 화면 x어드레스를 계산
    add di, ax        ; 화면 y어드레스와 계산된 x어드레스를 더해서 실제 비디오 메모리 어드레스를 계산

    ; 출력할 문자열의 어드레스
    mov si, word [ bp + 8 ]    ; 파라미터 3(출력할 문자열의 어드레스)

.MESSAGELOOP:
    mov cl, byte [ si ]        ; copy charactor which is on the address SI register's value
                    ; cl은 cx의 하위 1바이트를 의미
                    ; 문자는  1바이트면 충분하므로 cx레지스터의 하위 1바이트만 사용

    cmp cl, 0            ; compare the charactor and 0
    je .MESSAGEEND            ; if value is 0 -> string index is out of bound -> finish the routine

    mov byte [ es : di ], cl    ; 0이 아니라면 비디오 메모리 어드레스 0xB800:di에 문자를 출력
    add si, 1            ; go to next index
    add di, 2            ; di 레지스터에 2를 더하여 비디오 메모리의 다음 문자 위치로 이동
                    ; 비디오 메모리는 (문자, 속성)의 쌍으로 구성되므로 문자만 출력하려면 2를 더해야함

    jmp .MESSAGELOOP        ; loop code

.MESSAGEEND:
    pop dx        ; 함수에서 사용이 끝난 dx부터 es까지를 스택에 삽입된 값을 이용해서 복원
    pop cx        ; pop(제거)
    pop ax
    pop di
    pop si
    pop es
    pop bp        ; bp 복원
    ret        ; 함수를 호출한 다음 코드의 위치로 복귀

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 데이터 영역
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
MESSAGE1:    db 'OS Boot Loader Start!!', 0 ; define the string tha I want to print

DISKERRORMESSAGE:    db 'DISK Error!', 0
IMAGELOADINGMESSAGE:    db 'OS image loading...', 0
LOADINGCOMPLETEMESSAGE:    db 'Complete!', 0

; 디스크 읽기에 관련된 변수들
SECTORNUMBER:        db 0x02    ; os이미지가 시작하는 섹터번호를 저장하는 영역
HEADNUMBER:        db 0x00    ; " 헤드번호를 "
TRACKNUMBER:        db 0x00    ; " 트랙번호를 "

times 510 - ($ - $$)  db   0x00    ; $ : current line's address
                ; $$ : current section's base address
                ; $ - $$ : offset!
                ; 510 - ($ - $$) : offset to addr 510
                ; db - 0x00 : declare 1byte and init to 0x00
                ; time : loop
                ; fill 0x00 from current address to 510

db 0x55 ; declare 1byte and init to 0x55
db 0xAA ; declare 1byte and init to 0xAA
    ; Address 511 : 0x55
    ; 512 : 0xAA -> declare that this sector is boot sector

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

'Project > OS' 카테고리의 다른 글

10. 32비트 보호모드로 전환하자  (0) 2019.07.18
9. 테스트를 위한 가상 os이미지 생성  (0) 2019.07.16
7. 디스크에서 os이미지 로딩  (0) 2019.07.16
6. Bootloader 만들기  (0) 2019.07.11
5. 부팅과 부트로더  (0) 2019.07.11

+ Recent posts