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

7. 디스크에서 OS이미지 로딩

BIOS 서비스와 소프트웨어 인터럽트

  • BIOS는 우리가 일반적으로 많이 쓰는 라이브러리 파일과 달리 특별한 방법으로 외부에 제공한다.
  • 함수의 어드레스를 인터럽트 벡터 테이블(Interrupt Vector Table)에 넣어 두고, 소프트웨어 인터럽트(SWI, Software Interrupt)를 호출하는 방법을 사용한다.
  • 인터럽트 벡터 테이블(Interrupt Vector Table)
    • 메모리 어드레스 0에 있는 테이블로 특정 번호의 인터럽트가 발생했을 때 인터럽트를 처리하는 함수(인터럽트 핸들러, Interrupt Handler) 검색에 사용한다.
    • 테이블의 각항목은 인덱스에 해당하는 인터럽트가 발생했을 때 처리하는 함수 어드레스가 저장되어 있다.
    • 각 항목은 크기가 4바이트 이다.
    • 인터럽트는 최대 256개까지 설정할 수 있으므로 리얼모드의 인터럽트 벡터 크기는 최대 256*4 = 1024바이트가 된다.

  • 디스크 서비스를 사용하려면 위의 표에 나와있듯이 0x13 인터럽트를 발생시켜야 한다.
  • BIOS의 기능을 사용하려면 레지스터를 이용해 함수의 파라미터와 return값을 주고 받는다.
  • BIOS 서비스 마다 요구하는 파라미터의 수가 다르므로 서비스에 맞는 레지스터를 확인하고 사용 할 것

플로피 디스크의 내부구조

  • 헤드는 디스크의 표면을 의미
    • 헤드의 개수는 디스크 수 * 2
    • 플로피 디스크의 경우 디스크가 한장이므로 헤드의 개수는 2이다.
  • 트랙은 디스크를 여러 개의 동심원으로 나눴을 때 그 동심원 하나가 가지는 영역을 의미한다.
    • 플로피 디스크의 경우 80개
  • 섹터는 디스크를 구성하는 가장 작은 단위로 트랙을 여러 조각으로 자른 것을 의미
    • 플로피 디스크의 경우 18개
    • 섹터번호는 1부터 시작하므로 1~18의 값을 갖는다.
  • 플로피디스크의 모든 섹터를 순차적으로 읽는 알골리즘
    1. 섹터 = 1, 헤드 = 0, 트랙 = 0으로 설정
    2. 섹터를 1에서 18까지 증가시키면서 읽음
    3. 섹터 18번까지 다 읽었으면 0번 헤드를 다 읽었으므로 헤드 1증가
      A. 헤드 = 1, 섹터 = 1로 변경
    4. 섹터를 1에서 18까지 증가시키면서 읽음
    5. 섹터 18번까지 다 읽었으면 0번과 1번 헤드를 모두 다 읽었으므로, 트랙 1증가
      A. 트랙 = 1, 헤드 = 0, 섹터 = 1로 변경
    6. 2번에서 5번과정을 79번 트랙까지 반복

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

9. 테스트를 위한 가상 os이미지 생성  (0) 2019.07.16
8. os 이미지 로딩 기능 구현  (0) 2019.07.16
6. Bootloader 만들기  (0) 2019.07.11
5. 부팅과 부트로더  (0) 2019.07.11
4. 운영모드와 메모리 관리기법  (0) 2019.07.11

6. Bootloader 만들기

<우리가 만들 os 디렉터리 구조>

make프로그램

  • 실행파일 또는 라이브러리 파일을 만들어주는 빌드관련 유틸리티
  • make문법의 기본형식은 Target, Dependency, Command 세부분을 구성

  • Target은 생성할 파일
  • Dependency는 Target 생성에 필요한 소스파일이나 오브젝트 파일 등
  • Command는 Dependency에 관련된 파일이 수정되면 실행할 명령

<간단한 makefile 예제>

  • makefile은 역순으로 따라가면 된다.
  • 최종으로 생성해야 하는 결과물이 output.exe
  • output.exe: 라인을 보면 dependency가 a.o b.o가 필요한 것을 알 수 있다.
  • a.o와 b.o는 a.c 와 b.c를 컴파일 해서 생성되는 것을 알 수 있다.
  • 가장 윗부분에 all: target이 보이는데 기본적으로 사용하는 target이다.
  • 여러 target을 빌드할 때 all target의 오른쪽에 순서대로 나열하면 한번에 처리할 수 있다.
  • 빌드 과정에서 library 디렉터리를 빌드해야 한다면 -C옵션을 사용하면 된다. (아래 그림 참조)

Bootloader 작성

./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을 실행하는 것
  • clean target은 빌드 과정에서 생성된 파일을 삭제할 목적으로 정의했음.

./00.BootLoader/makefile

all: BootLoader.bin

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

clean:
  rm -f BootLoader.bin

BootLoader 제작

기본적인 BootLoader 제작

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

./00.BootLoader/BootLoader.asm

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

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

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

time 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실행

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

실행결과

화면버퍼와 화면제어

  • 기본으로 설정되는 화면 모드는 텍스트모드로 크기는 가로 80character, 세로 25character이다.
  • 비디오 메모리 어드레스는 0xB8000에서 시작한다.
  • 총 메모리 크기는 80252=4000바이트

  • 속성값은 하위 4비트의 전경색, 상위 4비트의 배경색으로 구분된다.
  • 각각 전경색, 배경색에서 최상위의 특수기능 비트가 있따.

<0xB8000와 0xB8001 어드레스에 값을 설정하는 코드>

  • 물리주소 0xB8000이 기준 어드레스로 사용
  • 화면 맨위는 0xB8000이므로 0xB8000과 0xB8001에 'M'과 0x4A를 쓰면 빨간색 배경에 밝은 녹색으로 'M'을 출력할 수 있다.
  • DS 세그먼트의 값이 0xB800이므로 0x00, 0x01을 지정하면 세그먼트:오프셋이 0xB800:0xB0000과 0xB800:0x0001이 되며 이는 물리주소 0xB8000, 0xB8001을 뜻한다.

세그먼트 레지스터 초기화

  • 세그먼트 레지스터를 초기화하는 코드가 필요하다.
  • 세그먼트 레지스터에는 BIOS가 사용하던 값이 들어 있기 때문

  • 그렇다면 어떤 값으로?
  • BIOS가 부트로더를 디스크에서 읽어 메모리에 복사하는 위치가 0x7C00이기 때문에 0x7C0로 초기화 한다.

  • 부트로더의 코드(Code Segment)와 데이터(Data Segment)는 0x7C00부터 512바이트 범위에 존재 하므로 CS와 DS 세그먼트 레지스터를 모두 0x7C0으로 설정했다.
  • CS 세그먼트 레지스터는 mov명령으로 처리할 수 없으며, 수정하려면 jmp명령세그먼트 레지스터 접두사를 이용해야 한다.

세그먼트 레지스터 초기화

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

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

START:
  mov ax, 0x07C0  ; 부트로더의 시작 어드레스(0x7C00)를 세그먼트 레지스터 값으로 변환
  mov ds, ax      ; DS 세그먼트 레지스터에 설정
  mov ax, 0xB800  ; 비디오메모리의 시작어드레스(0xB800)를 세그먼트 레지스터 값으로 변환
  mov ex, ax      ; ES 세그먼트 레지스터에 설정
  • 비디오 모드에 관련된 세그먼트 레지스터가 DS -> ES 로 되었으니 이후 출력에 관계된 코드는 모두 ES세그먼트 레지스터를 기준으로 하게 수정 해야한다.
  • 세그먼트 레지스터 접두사를 쓰는 방법은 [세그먼트 레지스터:오프셋] 형식으로 쓰면 된다.

./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

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)

.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

    mov si, 0            ; initialize si register
    mov di, 0            ; initialize di register

.MESSAGELOOP:
    mov cl, byte [ si + MESSAGE1 ] ; copy charactor which is on the address MESSAGE1's addr + SI register's value
    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     ; if value is not 0 -> print the charactor on 0xB800 + di
    add si, 1            ; go to next index
    add di, 2            ; go to next video address

    jmp .MESSAGELOOP        ; loop code

.MESSAGEEND:
    jmp $              ; infinite loop

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

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

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

8. os 이미지 로딩 기능 구현  (0) 2019.07.16
7. 디스크에서 os이미지 로딩  (0) 2019.07.16
5. 부팅과 부트로더  (0) 2019.07.11
4. 운영모드와 메모리 관리기법  (0) 2019.07.11
3. 운영모드와 레지스터  (0) 2019.07.11

5. 부팅과 부트로더

BIOS

  • 부팅은 PC가 켜진 후에 OS가 실행되기 전까지 수행되는 일련의 작업과정을 의미한다.
  • 부팅과정 중 하드웨어와 관련된 작업을 BIOS(Basic Input/Output System)가 담당한다.
  • BIOS에서 수행하는 각종 테스트나 초기화를 POST(Power On Self Test)라고 부른다.
  • 메인보드에 포함된 펌웨어의 일종
  • 입출력을 담당하는 프로그램
  • 부팅옵션 설정이나 시스템 전반적인 설정값(configuration)을 관리하는 역할도 한다.
  • 설정값으로 시스템을 초기화하여 os를 실행할 수 있는 환경을 만든다.

<우리가 만들 os의 부팅과정>

  • 부트로더 이미지를 메모리로 복사하는 단계가 가장 중요 (BIOS영역에서 os영역으로 넘어가는 부분)
  • POST가 완료된 후 부트로더가 존재한다면 코드를 0x7c00 어드레스에 복사한 후 프로세서가 0x7c00 어드레스부터 수행하도록 한다.
  • 하필 0x7c00인 이유는 IBM 사가 메모리의 0x00007c00 ~ 0x00007dff 번지(크기 512바이트) 를 부트 섹터가 읽혀지는 어드레스로 지정했기 때문
    출처: https://heeyamsec.tistory.com/19 [HeeYamSec]

부트로더

  • 가장 첫번째 섹터 MBR(Master Boot Record)에 있는 작은 프로그램 (섹터하나는 512바이트로 구성)
  • 부트로더 checklist
    • BIOS에 첫번째 섹터가 부트로더라는 것을 어떻게 알려줄 것인가?
    • 첫번째 섹터가 과연 정상적인 부트로더인지 어떻게 확인?
  • BIOS는 읽어드인 512바이트 중에 가장 마지막 2바이트의 값이 0x55인지 검사해서 부트로더인지 확인한다.

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

7. 디스크에서 os이미지 로딩  (0) 2019.07.16
6. Bootloader 만들기  (0) 2019.07.11
4. 운영모드와 메모리 관리기법  (0) 2019.07.11
3. 운영모드와 레지스터  (0) 2019.07.11
2. 운영모드  (0) 2019.07.11

4. 운영모드와 메모리 관리기법

x86과 x86-64프로세서에서 지원하는 메모리관리 기법은 크게 두가지이다.(세그먼테이션과 페이징)

  • 세그먼테이션
    • 우리가 전체역역을 원하는 크기로 분할하여 관리하는 것
  • 페이징
    • 지정 단위로 잘린 영역을 모아 원하는 크기를 만들어 관리하는 방식

메모리 관리기법을 사용하려면 관련 레지스터에 특정한 자료구조를 설정해야 한다.
세그먼테이션은
세그먼테이션 레지스터에 시작주소 혹은 Descriptor라고 불리는 자료구조의 위치를 설정해야한다.
페이징은
컨트롤 레지스터에 CR3레지스터에 Page directory라고 불리는 자료구조의 물리주소를 설정해야 사용할 수 있다.

리얼모드의 메모리 관리방식

  • 리얼모드는 최대 1MB까지 주소공간을 사용하며 세그먼테이션만 지원한다.
  • 리얼모드에서 세그먼트 크기는 64k로 고정
  • 세그먼트의 시작 어드레스는 세그먼트 레지스터에 직접 설정해야 한다.
  • 세그먼테이션에서 세그먼트의 시작 어드레스는 코드나 메모리에 접근할 때 기준 어드레스(Base address)로 사용됨

<리얼모드의 세그먼트 레지스터와 세그먼트, 몰리주소의 관계>


그림의 세그먼트레지스터 ES와 DS가 0x0200으로 되어있음. 16을 곱하면 0x2000이 된다.
그림의 밑에서 두번째 블럭(64K데이터)의 범위를 보면 0x2000 ~ 0x2fff인데 0x2fff-0x2000=0x0fff인데 이는 64k의 크기가 안된다.
범위가 0x2000 ~ 0x2fff이면 그림의 첫번째 블럭(64k 코드)부분의 영역과 겹치게 됨
그림이 이상한건지 내가 모르는게 있는 건지

  • 리얼모드에서는 세그먼테이션을 거쳐 나온 어드레스가 바로 물리주소가 됨
  • 리얼모드의 세그먼테이션은 세그먼트 레지스터의 값에 범용 레지스터의 값을 더하는 방식으로 동작함

<세그먼트 레지스터에 0x1000, 범용 레지스터에 0x1234가 설정되었을 때 물리주소를 계산하는 방법>

  • 세그먼트의 크기가 64KB인 이유는 바로 범용 레지스터의 크기 때문
  • 16비트 프로세서에는 32비트 레지스터가 없으며 범용레지스터는 모두 16비트 크기 따라서 16비트로 접근할 수 있는 범위가 0~0xffff이므로 세그먼트 크기도 64kb가 된 것

  • 보호모드 역시 같은 방식으로 세그먼트의 기준주소에 범용레지스터를 더해 계산함.
    다만, 페이징이 추가되어서 계산 결과는 논리주소로 바뀌었고, 논리주소는 페이징을 거쳐 물리주소로 바뀌게 됨.

보호모드의 메모리 관리 방식

  • 보호모드는 세그먼테이션과 페이징을 모두 지원함
  • 보호모드의 세그먼테이션은 디스크립터 자료구조의 위치(offset)를 설정하는 방식
  • 세그먼트 레지스터의 명칭도 세그먼트 셀렉터로 변경됨
  • 디스크립터는 메모리영역의 정보를 저장하는 자료구조로 여러 종류가 있음
  • 그중 세그먼트에 대한 정보를 나타내는 디스크립터를 세그먼트 디스크립터라고 부름

  • 세그먼트 디스크립터에는 세그먼트의 시작 어드레스와 크기, 권한, 타입등의 정보가 있음

<세그먼트 디스크립터의 구조>

  • 세그먼트 디스크립터에 포함된 특권레벨(DPL, Descriptor privilege level)은 해당 세그먼트에 접근하기 위한 최소한의 권한을 나타냄 숫자가 작을 수록 높은 권한이다.
  • 보호모드에서는 세그먼트 디스크립터의 위치를 가르킴
  • 세그먼트 디스크립터는 GDT(Global descriptor table)라고 불리는 곳에 모여있다.
  • GDT는 연속된 디스크립터의 집합
  • GDT 위치와 관련된 reg. 는 GDTR이다.
    • 16bit GDT size 필드와 32bit base addr필드로 구성된 자료구조의 물리주소를 넘겨 받는다.
    • processor는 이값을 가지고 있다가 segment selector를 통해 addr에 접근할 때마다 GDT의 위치를 찾는데 참조한다.

  • 선형주소와 물리주소가 1:1로 대응하도록 설정
  • 메모리구조는 최대한 간단하게 유지하는 편이 좋다

아래그림은 세그먼테이션과 페이징을 통해 논리주소->물리주소로 변환하는 과정

  • 3단계 페이징은 선형주소(논리주소)를 directory, table, offset 3부분으로 나누며 물리메모리를 4kb 페이지로 나누어 관리하는 방식


-> 나온 선형주소에서 table(물리적인 공간, 메인메모리에 위치)에 있는 entry(32bit 주소)로 접근해서 entry로 다음 table의 기준주소를 정함

  • 프로세서가 페이징처리 과정에서 table을 사용하려면 우리가 직접위치를 알려줘야함
  • CR3 control reg.는 페이지 디렉터리의 시작주소를 가르킨다.
  • 페이지 디렉터리 엔트리의 위치계산에 사용됨

  • 선형주소는 디렉터리 오프셋 10bit, table offset 10bit, page offset 12bit로 구분
  • 2^12 = 2^2 * 2^10 = 4k

  • page directory entry = 다음에 위치하는 page table의 start address
  • page table entry = 다음에 위치하는 page의 start address
    • 선형주소의 offset = 물리주소
  • 아래 그림 참고

IA-32e 메모리관리

IA-32e모드의 segmentation은 보호모드의 segmentation과 차이가 거의없음

  1. segemnt descriptor에 설정된 기준주소와 크기에 관계없이 모든 segment가 기준주소는 0, 크기는 64bit전체로 설정.
    따라서 기준주소가 segment를 구분하지 않는다.
  2. 두가지 서브모드를 지원하므로 code segment descriptor에 L필드가 추가로 있음.
    0이면 호환모드, 1이면 64bit모드.

IA-32e모드의 페이징은 64bit 주소공간이므로 PAE기능이 기본으로 활성화
변환단계도 4kb 페이지는 5단계, 2mb페이지는 4단계
그로인해 새롭게 추가된 table: page map level 4 (PML4) table, page directory pointer table(PDPT)
PAE기능이 뭔지 모르겠다.

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

6. Bootloader 만들기  (0) 2019.07.11
5. 부팅과 부트로더  (0) 2019.07.11
3. 운영모드와 레지스터  (0) 2019.07.11
2. 운영모드  (0) 2019.07.11
1. ubuntu18.04.2 LTS개발환경 세팅  (0) 2019.07.11

3. 운영모드와 레지스터

os를 개발하는 관점에서 운영모드는 크게 16비트모드, 32비트모드, 64비트 모드 3가지로 나뉠 수 있음.

범용레지스터

  • 계산, 메모리 어드레스 지정, 임시저장 공간등의 목적으로 사용함
  • 범용레지스터의 수는 프로세서가 지원하는 운영모드에 따라 다름
  • 범용 레지스터의 수가 늘어나면 수행속도가 개선됨
  • 관련된 값을 레지스터에 모두 올려서 계산함으로서 메모리에 접근을 최대한 줄이는 것
  • 이와 비슷한 효과는 함수 호출에도 그대로 적용됨
  • 다수의 범용레지스터에 함수 파라미터를 넣어 넘겨줌으로서 스택영역의 메모리에 접근하는 시간과 스택을 정리하는 시간을 줄일 수 있음
    <x86-64프로세서의 범용레지스터와 용도>

  • 64비트 범용레지스터는 하위 32비트, 16비트, 8비트의 크기로 구분하여 접근할 수 있고, 레지스터에 접두사를 붙여 접근하는 크기를 표시함

<운용모드에 따른 범용레지스터의 크기와 이름>

  • 리얼모드에서 16비트 크기 이하의 레지스터만 접근 가능한 것으로 표시되어 있지만, 오퍼렌드 크기 접두사(Operand-Size Prefix, 0x66)를 사용하거나 어드레스 크기 접두사(Address-Size Prefix, 0x67)를 사용하면 32비트 레지스터도 접근할 수 있음
  • 운영모드에 따라 접두사를 결합하는 방법에는 일정한 규칙이 있으며 사용한 접두사에 따라 명령어가 처리하는 오퍼랜드나 어드레스의 크기가 달라짐

<운영모드와 접두사에 따른 오퍼랜드 및 어드레스의 크기>


-> 리얼모드에서
REX접두사:X,
operand-size prefix: N,
address-size prefix: N
일 때
유효한 operand 크기가 16bit,
유효한 address 크기가 16bit
이다.

여기서 한가지 의문점은 과연 접두사가 의미하는 것이 무엇이며
어떻게 사용하는지 이다.
원래 사이즈가 64bit 레지스터인데 접두사를 붙임으로서 확장, 축소 해서 사용하는 것인지
따로 각 접두사별 레지스터가 존재하는 것인지

  • 그림을 보면 IA-32e모드의 기본 오퍼랜드 크기가 32비트로 표시되어 있음
  • IA-32e 64bit모드는 64비트모드 이므로 오퍼랜드의 크기와 어드레스트의 크기가 모두 64비트인 것이 당연함
  • IA-32e모드의 기본 오퍼랜드의 크기는 32비트, 기본 어드레스의 크기는 64비트로 설계되어 있음
  • 기본 오퍼랜드의 크기가 32비트로 설계되어 있어서 64비트 어드레스를 표현할 수 없다.
  • 그래서 RIP상대 어드레스라는 새로운 어드레스 계산방식이 도입되었음
  • RIP상대 어드레스 레지스터는 현재 수행중인 명령의 어드레스를 가리키는 레지스터
  • RIP레지스터의 값과 32비트 오퍼랜드를 통해 64비트 주소공간을 나타낼 수 있다.
  • 하지만, 기본 오퍼랜드의 크기가 32비트이므로 RIP레지스터의 값에 상위 2G와 하위 2G범위 까지만 표현할 수 있는 단점이 있음
  • 64비트 오퍼랜드와 관계있는 REX접두사를 사용해 범위를 벗어나느 어드레스에 접근해야 함
  • REX접두사를 사용하면 오퍼랜드의 크기가 64비트가 되고 따라서 64비트 어드레스를 모두 표현할 수 있음

여기서 REX접두사가 뭘까?

<RIP상대 어드레스와 메모리 영역>


->RIP는 현재 수행중인 명령의 어드레스를 가리키는 reg.
RIP가 옮겨다니며 -2G ~ +2G의 범위 만큼 RIP상대
어드레스에 접근이 가능한 것

세그먼트 레지스터

  • 16비트 레지스터로 어드레스 영역을 다양한 크기로 구분함

  • 리얼모드에서는 단순히 고정된 크기의 어드레스 영역을 지정함

  • 보호모드와 IA-32e모드에서는 접근권한, 세그먼트의 시작 어드레스와 크기등을 지정하는데 사용되기도 함
    <x86-64프로세서의 세그먼트 레지스터>

  • 세그먼트 레지스터의 역할은 주소 공간을 목적에 따라 구분한는 것이며, 주소공간을 구분하는 방법은 메모리 관리기법과 깊은 관계가 있음.

  • 메모리관리 기법에는 크게 세그먼테이션과 페이징 두가지가 있음. 이중 세그먼트레지스터를 통해 주소공간을 구분하는 방식이 세그먼테이션이다.*

컨트롤레지스터

  • 운영모드를 변경하고, 현재 운영중인 모드의 특정기능을 제어하는 레지스터
  • x86프로세서에는 CR0 ~ CR4의 5개의 컨트롤 레지스터가 존재
  • x86-64프로세서에는 CR8이 추가되어 총 6개의 컨트롤 레지스터가 있다.

<x86-64프로세서의 컨트롤 레지스터>

  • 컨트롤 레지스터는 리얼모드와 보호모드일 때 32비트 크기이다.
  • IA-32e모드에서는 64비트로 확장되지만 일부 제약 사항이 있다.
    • CR0와 CR4, CR8레지스터에서는 64비트 영역을 모두 사용할 수 있다.
    • CR2레지스터의 경우는 64비트 영역을 모두 사용할 수 있다.
    • CR3레지스터는 비트40부터 51까지 모두 0으로 설정해야 한다.

<컨트롤 레지스터의 구조>

  • 각필드는 저마다 특정기능을 활성화/비활성화 하며, 현재 운영모드에 따라 필수 필드와 옵션필드가 달라진다.
  • 컨트롤 레지스터의 특정기능은 해당비트를 1로 설정해도 충분하지만, 특정기능은 1로 설정하기 전에 프로세서가 사용할 자료구조를 미리 준비해야한다.

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

6. Bootloader 만들기  (0) 2019.07.11
5. 부팅과 부트로더  (0) 2019.07.11
4. 운영모드와 메모리 관리기법  (0) 2019.07.11
2. 운영모드  (0) 2019.07.11
1. ubuntu18.04.2 LTS개발환경 세팅  (0) 2019.07.11

2. 운영모드

주변에서 많이 사용하는 intel 64bit 호환프로세서(x86-64 프로세서)에는 크게 5가지 운영모드가 있음.

리얼모드

  • 전원이 켜지거나 리셋되면 리얼모드로 진입한다.
  • 프로세서의 초기상태로서 16bit 모드로 동작(리얼모드는 과거의 16비트 프로세서와 동일하게 동작)
  • BIOS의 여러기능을 사용할 수 있음
  • 최대 1MB(2^20)의 주소공간을 지원
  • 디바이스 드라이버를 제작하지 않아도 됨
  • 리얼모드에서 하는 작업은 os이미지를 디스크에서 메모리로 복사하여 보호모드로 변경하는 것
  • 대부분 작업을 어셈블리어로 처리해야한다.

보호모드

  • 32bit 모드로 동작하며 세그먼트, 페이징, 보호, 멀티태스킹등의 기능을 제공하는 모드
  • 4GB(2^32)의 주소공간을 지원
  • IA-32e 모드로 전환하려면 반드시 거쳐야 하는 모드
  • 32bit윈도우나 리눅스 os가 동작하는 기본모드
  • os의 필수기능으로 자리잡은 보호, 멀티태스킹, 세그멘테이션, 페이징등의 기능을 하드웨어적으로 지원한다.

IA-32e 모드

  • 32bit 호환모드와 64bit 모드 두가지 서브모드로 구성(이중 32bit 호환모드는 보호모드와 같은 기능을 수행함)
  • 16EB(2^64)의 주소공간을 지원하는 모드
  • 대부분의 자료구조는 보호모드와 같고, 크기만 2배로 확장되고 일부 필드의 의미가 변하는 정도 이므로 보호모드와 큰차이는 없음
  • 프로세서가 32bit 호환모드에 있는 것 처럼 동작하므로 32bit코드를 그대로 실행할 수 있음.
  • 이는 간단히 서브모드만 변경함으로서 보호모드 코드를 실행할 수 있다는 뜻

시스템 관리 모드

  • 전원 관리나 하드웨어 제어같은 특수기능을 제공하는 모드

가상 8086 모드

  • 보호모드 내부에서 가상의 환경을 설정하여 리얼모드처럼 동작하는 모드

이중 64bit os가 필수적으로 갖추어야 할 운영모드는
[리얼모드], [보호모드], [IA-32e 64bit 모드] 3가지 이다.

<운영모드 사이의 관계>

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

6. Bootloader 만들기  (0) 2019.07.11
5. 부팅과 부트로더  (0) 2019.07.11
4. 운영모드와 메모리 관리기법  (0) 2019.07.11
3. 운영모드와 레지스터  (0) 2019.07.11
1. ubuntu18.04.2 LTS개발환경 세팅  (0) 2019.07.11

Project 카테고리에 있는 OS에 들어있는 내용은 "IT EXPERT, 64비트 멀티코어 OS 원리와 구조 - 저자: 한승훈" 책을 보면서 만든 것입니다.

1. ubuntu 18.04.2 LTS 개발환경 세팅

필수적으로 설치해야하는 패키지

  • GNU binutils: 여러 종류의 오브젝트 파일들을 핸들링하기 위한 바이너리들. 주로 make, gcc, gdb등과 함께 사용한다.
  • GNU bison: Parser의 일종으로 LARLR로 이루어진 문법을 해석하여 C코드로 작성하여 준다.
  • gcc-multilib: c의 크로스 컴파일을 위해 설치한다.
  • g++-multilib: C++의 크로스 컴파일을 위해 설치한다.
  • libc6-dev: libiconv(인코딩 변환 라이브러리)를 사용하기 위해 설치한다.
  • libtool: Portable library를 만들기 위한 도구이다.
  • make: 프로그램 그룹 유지를 위한 유틸리티로, 새롭게 컴파일 되어야 하는 부분을 판단하여 gcc를 이용해 재컴파일 한다.
  • patchutils:패치 파일을 핸들링하기 위한 유틸리티
  • libgmp-dev: GNU MP라이브러리를 사용하기 위해 설치
  • libmpfr-dev: MPFR라이브러리를 사용하기 위해 설치한다
  • libmpc-dev: MPC라이브러리를 사용하기 위해 설치

쉘명령어 입력

$ sudo apt-get update
$ sudo apt-get upgrade
$ sudo apt-get install gcc-multilib g++-multilib binutils bison flex libc6-dev libtool make patchutils libgmp-dev libmpfr-dev libmpc-dev

NASM 설치

어셈블리 빌드를 위해서 설치

$ sudo apt-get install nasm

QEMU 설치

emulator를 이용하기 위해서 설치

$ sudo apt-get install qemu-kvm

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

6. Bootloader 만들기  (0) 2019.07.11
5. 부팅과 부트로더  (0) 2019.07.11
4. 운영모드와 메모리 관리기법  (0) 2019.07.11
3. 운영모드와 레지스터  (0) 2019.07.11
2. 운영모드  (0) 2019.07.11

+ Recent posts