본문 바로가기
공부/OS

[운영체제] 운영체제 만들기

by algosketch 2021. 4. 19.

32비트 인텔 CPU를 가정한다.
bios 는 다루지 않는다.
키보드, 디스크, 화면을 다룬다.

general purpose registers
EAX, EBX, ECX, EDX

stack registers
ESP, EBP
EBP 는 base pointer 인데, 현대의 컴파일러들은 EBP 를 거의 사용하지 않는다.

other registers
EIP, EFLAGS(bit vector of flags, stroes things like carry, ...)

메모리의 값을 바로 메모리에 넣을 수는 없고 반드시 레지스터를 거쳐야 한다.

운영체제에 따라 RAX, EAX, AX 로 나눌 수 있고 이전 버전과 호환되어야 한다. AX 는 AH + AL 로 나눌 수 있다.

jmp : 무조건 점프
jz : zero 면 점프
jnz : zero 가 아니면 점프
jne : 다르면 점프 (jump not equal)
jle : jump less than or equal? 작거나 같으면 점프인 듯
zxx 명령어는 대충 이런 식...

메모리는 Interrupt Vector Table, BIOS Code, Memory mapped devices 가 먼저 들어가고 남은 메모리에 운영체제 코드가 적재되어야 한다.

인텔 아키텍처에서 각 주소는 한 바이트를 가리킨다.
byte : 1byte
word : 2byte
dword (double word) : 4byte

byte ordering

big endian 과 little endian 방법이 있다.
리틀 엔디안은 인텔에서 사용하고 나머지는 빅 엔디안을 사용하는데 PC 는 리틀 엔디안이 표준, 서버나 네트워크는 빅 엔디안을 사용하고 있다. 그래서 네트워크로 패킷을 보낼 때에는 바꿔서 보내야 한다.
리틀 엔디안은 주소 체계가 거꾸로 되어 있음.
ab 12 00 00 -> 000012ab

디바이스와 운영체제가 통신하는 3 가지 방법

  1. I/O Port
  2. Memory mapped
  3. interrupt

I/O Port Example

I/O Port 는 0x3F8 이라 가정

 <Output>
 mov eax, 1
 mov dx, 0x3F8
 out dx, al    <-- start
 inc eax
 cmp eax, 32
 jne start

 <Input>
 mov ebx, 0
 mov dx, 0x3F8
 in dx, ax    <-- start
 mov [esp+ebx], ax
 inc ebx
 cmp ebx, 31
 jle start

out dx, al : dx 포트에 al 값을 출력
in dx, ax : dx 포트에서 입력 받은 값을 ax 에 저장

Mamory Mapped Devices

CPU 와 I/O Device 가 램의 일정 영역을 공유한다.
frame : 한 화면
buffer : 화면에 직접 뿌리는 것이 아니라 버퍼에 입력하면 CPU 가 읽어서 그래픽 카드가 출력한다.

Keyboard Controller
2 가지 byte 를 갖는다.
keycode : 입력된 키 값
status : 상태 값 (key 누르면 1, CPU 에서 읽으면 0)

우리는 프레임 버퍼에 쓰기만 하고, 화면에 출력하는 건 비디오 카드가 한다.

<output>
fbuf = 0xF000
str : 'Hello World'

mov eax, str    <-- begin
mov ebx, 11
mov ecx, 0xF000
mov edx, byte [eax]    <-- loop
inc eax
mov [ecx], byte edx
inc ecx
sub ebx, 1
jnz_loop
jmp done    <-- done

프레임 버퍼에 직접 접근하는 이 프로그램은 OS 가 아니다.
문자열, 프레임 버퍼를 가져와서 문자열 길이만큼 반복하여 출력하는 코드이다. 읽은 값은 edx 에 임시로 저장한 후 edx 에서 버퍼 프레임에 넣어준다.

입력 받을 때마다 화면에 출력하는 코드
교수님이 프레임 버퍼에 출력한다고 하는 편이 더 전문적인 느낌이 날 것 같다고 말씀하셔따

<input>
framebuf = 0xF000
status = 0xF800
keycode = 0xF801

mov eax, 0xF000                <-- begin
mov ebx, byte [0xF800]    <-- loop
cmp ebx, 0
jz loop

mov ebx, byte [0xF801]
mov [eax], byte ebx
inc eax
mov [0xF800], 0
jmp loop

프레임 버퍼를 가져온다.
status 값을 확인하고 1이면 키 값을 가져와 프레임 버퍼에 출력하고 0으로 만들어 준 뒤 돌아간다. 0이면 그냥 돌아간다.

함수가 매개변수를 저장하는 방법

add(1, 2) 라면 2, 1 순서로 스택에 넣는다. 스택에서 뺄 때는 역순이 되기 때문이다. 매개변수는 각각 eax, ebx, ... 에 넣던데 매개변수가 5개 이상이면 어떻게 처리하는 거지...?
반환값은 eax 에 저장한다.

입력한 값을 출력하는 OS 를 만들어 보자!!

이것은 API 형태로 제공할 것이다. getkey() 와 putchar() 를 만들어 보자!

mov edx, byte [status]    <-- getkey
cmp edx, 0
jz getkey
mov [status], 0
mov eax, byte [keycode]
ret

키 입력이 들어왔으면 키 값을 반환하고, 안 들어왔다면 들어올 때까지 기다리는 코드이다.

<putchar>
putchar: mov eax, word [esp+4]
mov ebx, dword [bufptr]
mov [ebx], word eax
add ebx, 1
mov [bufptr], ebx
ret

bufptr 은 프레임 버퍼 포인터를 저장하고 있는 포인터 변수이다. bufptr 자체는 프레임 버퍼 포인터의 주소값이고 [bufptr]은 프레임 버퍼의 주소이고 [ebx]가 키 값인 것 같다.
putchar 의 매개변수(2바이트)를 하나 받아서 그 값을 버퍼프레임에 출력한다.
2바이트를 읽었으니 add ebx, 1 이 아니라 add ebx, 2 가 되어야 할 것 같은데 이 부분은 잘 모르겠다... C언어의 포인터 연산이랑 같은 개념인 걸까...?

그렇게 새로운 입력 코드는 다음과 같다.

call getkey    <-- loop
push eax
call putchar
pop eax
jmp loop

push eax 는 매개변수이다.

왜 이런 까다로운 짓을 할까?

개발에 익숙하다면 오히려 함수화 하는 게 좋다는 것을 바로 알아챌 것이다. 개발에서 함수화 시키는 것과 같은 이점을 갖는다.

  • Reusability
    재사용성이 좋다. 다른 곳에서도 getkey, putchar 함수를 사용할 수 있다.
  • Abstraction
    사용자는 프레임 버퍼, 출력에 대해 신경쓰지 않아도 된다.
  • Portability
    OS 내부 코드가 바뀌어도 함수를 사용하는 방법은 그대로이기 때문에 코드를 사용자는 코드를 바꾸지 않아도 된다.

쉘을 만들고 싶어여!

왜 그런 걸 만들려고 하는 거야?
cmd 같은 것을 쉘이라 하는데, 한 라인씩 읽는다. 읽은 값이 명령어라면 운영체제에서 실행하면 되지만 파일이라면 프로그램을 찾아서 메모리에 로드 해 줘야 한다.

프로그램은 어디에 로드해야할까?
하드웨어, 운영체제, IVT, BIOS 가 사용하고 남은 메모리에 로드해야 한다.

Disk Controller
디스크는 블록 디바이스이다. 블록 단위로 읽어들인다. (512byte, 4kb)
블록 주소에 B, cmd/status 에 'W'를 쓰면 블록 B 에 작성하라는 뜻이고 'R'이면 블록 B 에서 읽으라는 뜻이다.

File System Structures

struct dirent {
    bool valid;
    char name[16];
    int start;
    int len;
}

start = 3 이고 len = 3 이라면 디스크에서 3, 4, 5 블럭을 읽는다.

우리가 만든 운영체제의 문제점은 무엇일까?

  • 예외 처리가 안 되어있다.
  • 주소값에 직접 접근하고 있다. (getkey, putchar)
    운영체제 코드가 바뀌면 어플리케이션도 동작하지 않을 것이다. 이 문제를 해결하기 위해서는 인터럽트를 사용하고 가상 메모리를 사용해야 한다.
  • 인천대학교 컴퓨터공학부 3학년 전공필수 과목인 박문주 교수님의 운영체제 강의를 듣고 작성한 글입니다.