반응형

앞에서 만든 부트 로더OS 이미지를 메모리로 복사하는 기능을 추가합니다.

 

기능을 추가한 뒤 빌드가 끝나면 플로피 디스크용으로 OS 이미지가 생성됩니다.

 

플로피 디스크를 제어하는 방법은 두 가지로 직접 플로피 디스크 컨트롤러에 접근하는 방법과 BIOS 서비스를 이용하는 방법이 있습니다.

 

BIOS 서비스를 사용하면 OS 이미지를 로딩하는 기능을 구현할 수 있지만

한가지 문제점으로 로딩할 OS 이미지가 없기 때문에 가상 OS 이미지를 만들고 로딩해서 잘 되는지 확인해야 합니다.

 


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

 

BIOS는 키보드와 마우스부터 디스크나 프린터까지 거의 모든 PC 주변기기를 제어하는 기능을 제공합니다.

 

BIOS의 기능을 잘 활용하는 것만으로도 16bit OS를 만들 수 있습니다.

 

BIOS는 일반적으로 많이 쓰는 라이브러리 파일과는 달리 자신의 기능을 특별한 방법으로 외부에 제공하는데, 함수의 주소를 인터럽트 벡터 테이블(Interrupt Vector Table)에 넣어두고, 소프트웨어 인터럽트(SWI, Software Interrupt)를 호출하는 방법을 사용합니다.

 

인터럽트 벡터 테이블메모리 주소 0에 있는 테이블이며, 특정 번호의 인터럽트가 발생했을 때 인터럽트를 처리하는 함수(인터럽트 핸들러, Interrupt Handler) 검색에 사용합니다.

 

테이블의 각 항목은 크기가 4Byte이고, 인덱스에 해당하는 인터럽트가 발생했을 때 처리하는 함수 주소가 저장되어 있습니다.

 

또한 인터럽트최대 256개까지 설정할 수 있기 때문에 리얼 모드의 인터럽트 벡터 크기는 최대 256 x 4 = 1,024Byte입니다.

 

https://velog.io/@wimes/%EB%94%94%EC%8A%A4%ED%81%AC%EC%97%90%EC%84%9C-OS%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A1%9C%EB%94%A9

 

BIOS가 제공하는 디스크 서비스를 사용하려면 소프트웨어 인터럽트 명령을 사용하여 0x13 인터럽트를 발생시켜야 합니다.

 

SWICPU에 가상으로 특정 인터럽트가 발생했다고 알리는 명령어이며, int 0x13과 같은 형태로 사용하고, 직접 만든 함수의 주소를 인터럽트 벡터 테이블에 넣어뒀다면 int 명령으로 언제든지 해당 함수로 이동할 수 있습니다.

 

BIOS 서비스SWI를 이용하여 호출할 수 있지만, AX, BX, CX, DX 레지스터ES 세그먼트 레지스터를 사용하여 작업에 관련된 파라미터를 넘겨주고 결과값도 넘겨받습니다.

 

하지만 BIOS 서비스마다 요구하는 파라미터의 수가 다르기 때문에 서비스를 호출할 때 파라미터로 정의된 레지스터를 확인해야 합니다.

 

참고) AX 레지스터기능 선택처리한 결과값을 받을 때 공통으로 사용되는데 이는 BIOS의 디스크 관련 서비스뿐만 아니라 다른 서비스에서도 적용되는 규칙입니다.

 

https://velog.io/@wimes/%EB%94%94%EC%8A%A4%ED%81%AC%EC%97%90%EC%84%9C-OS%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A1%9C%EB%94%A9

 

플로피 디스크 : 자기 원반(Magnetic Disk)으로 구성된 저장매체입니다.

 

3.5인치 플로피 디스크를 기준으로 일반적인 플로피 디스크는 물리적으로 섹터, 트랙, 헤드로 구성되어 있습니다.

 

→ 헤드 : 디스크의 표면을 의미합니다.

디스크 하나는 두 개의 표면으로 구성되고 각 표면에 데이터 저장이 가능하기에 헤드의 개수는 디스크 수 x 2 입니다.

헤드 번호는 0부터 시작하기 때문에 0 ~ 1의 값을 갖습니다.

 

→ 트랙 : 디스크를 여러 개의 동심원으로 나눴을 때, 해당 동심원 하나가 가지는 영역을 의미합니다.

플로피 디스크는 디스크를 모두 80개의 동심원으로 구분하므로 트랙의 수는 모두 80개입니다.

0 ~ 79의 값을 갖습니다.

 

→ 섹터 : 디스크를 구성하는 가장 작은 단위로 트랙을 다시 여러 조각으로 자른 것입니다.

한 트랙18개의 같은 크기로 구분하므로 트랙에 포함된 섹터의 수는 모두 18개입니다.

섹터 하나는 512byte로 구성되며, 이는 디스크에 관련된 작업을 수행할 때 사용하는 기본 단위가 됩니다.

1 ~ 18의 값을 갖습니다.

 

플로피 디스크의 모든 섹터를 순차적으로 읽는 알고리즘

1. 섹터 = 1, 헤드 = 0, 트랙 = 0으로 설정.

2. 섹터를 1에서 18까지 증가시키면서 읽음

3. 섹터 18번까지 다 읽었다면 0번 헤드를 다 읽었으므로 헤드 1 증가

헤드 = 1, 섹터 = 1로 변경

4. 섹터를 1에서 18까지 증가시키면서 읽음

5. 섹터 18번까지 다 읽었으면 0번과 1번 헤드를 모두 다 읽었으므로, 트랙 1 증가

트랙 = 1, 헤드 = 0, 섹터 = 1로 변경

6. 2번에서 5번 과정을 79번 트랙까지 반복

 

https://velog.io/@wimes/%EB%94%94%EC%8A%A4%ED%81%AC%EC%97%90%EC%84%9C-OS%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A1%9C%EB%94%A9

 


→ 디스크 읽기 기능 구현

 

FS64 OS의 이미지는 크게 부트로더, 보호 모드 커널, IA-32e 모드 커널로 구성되며, 각 부분은 섹터 단위로 정렬해서 하나의 부팅 이미지 파일로 합치기에 디스크의 두 번째 섹터부터 읽어서 특정 메모리 주소에 순서대로 복사하면 이미지 로딩이 끝납니다.

 

FS64 OSOS 이미지를 0x10000(64Kbyte)에 로딩해서 실행하지만 반드시 0x10000 위치에 로딩해야 하는 것은 아닙니다.

 

부트 로더 이후(0x7C00)에 연속해서 복사해도 OS를 실행하는 데에는 전혀 문제가 없습니다.

 

참고) FS64 OS0x10000 하위 영역을 다른 용도로 사용하기 위해 남겨둡니다.

 

플로피 디스크첫 번째 섹터(512byte)는 부트 로더로 BIOS가 메모리에 로딩하기 때문에 두 번째 섹터부터 OS 이미지 크기만큼을 읽어서 메모리에 복사하면 됩니다.

 

플로피 디스크의 섹터섹터 → 헤드 → 트랙의 순서로 배열되어 있기에 이 순서만 잘 지키면 로딩하는 데에 큰 문제가 없습니다.

 

다음은 섹터 배열 순서를 고려하여 작성한 C언어와 어셈블리어 소스 코드입니다.

 

섹터 번호를 순서대로 읽다가 마지막 섹터에서 헤드와 트랙 번호를 증가시키는 것이 핵심 포인트입니다.

 

참고) BIOS의 섹터 읽기 기능은 최대 128 섹터까지 읽을 수 있기 때문에 한 트랙에 있는 섹터의 배수 단위(18 섹터)로 읽게 하면 코드를 간단히 할 수 있지만, 여러 섹터를 읽는 기능이 정상적으로 동작하지 않는 BIOS와 가상 머신이 있어서 안전하게 한 섹터씩 읽는 방법으로 구현했습니다.

 

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

 

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

 

위의 어셈블리어 코드디스크 리셋 기능부트 로더에 추가하면 로딩할 준비가 끝납니다.

 

하지만 화면에 출력하는 코드가 없어서 확인이 불가능하므로 환영 메시지를 함수 형태로 구현하여 함수 호출이 가능한 구조로 코드를 변경하겠습니다.

 


→ 스택 초기화와 함수 구현

 

x86 프로세서에서 함수를 사용하려면 스택(Stack)이 꼭 필요합니다.

 

스택은 아래의 사진과 같이 데이터를 삽입하는 포인트와 제거하는 포인트가 같기 때문에, 마지막에 들어간 데이터가 가장 먼저 나오는 LIFO(Last-In, First-Out) 자료구조를 사용합니다.

 

http://www.incodom.kr/%EC%8A%A4%ED%83%9D

x86 프로세서에서는 스택의 용도를 함수를 호출한 코드의 다음 주소, 즉 되돌아갈 주소(복귀 주소, Return Address)를 저장하는 데 사용합니다.

 

함수를 호출하면 프로세서가 자동으로 되돌아올 주소를 스택에 저장하고, 호출된 함수에서 되돌아감(ret)을 요청하면 자동으로 스택에서 주소를 꺼내 호출한 다음 주소로 이동합니다.

 

또한, 스택복귀 주소를 저장하는 것뿐만 아니라 함수의 파라미터를 저장하기 때문에 호출하는 쪽(Caller)과 호출되는 쪽(Callee)은 정해진 규칙에 따라 파라미터를 스택에 저장함으로써 협업할 수 있습니다.

 

 

x86 프로세서스택 관련 레지스터스택 세그먼트 레지스터(SS), 스택 포인터 레지스터(SP), 베이스 포인터 레지스터(BP)가 있는데, 이 3가지 레지스터를 이용하여 스택을 만들 수 있습니다.

 

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

 

스택 포인터 레지스터(SP) : 데이터를 삽입하고 제거하는 상위(Top)를 지정

 

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

 

16bit 모드세그먼테이션 방식으로 주소를 변환하기 때문에 스택 세그먼트 레지스터(SS)를 이용하여 최대 64KB(0x10000)를 스택 영역으로 지정할 수 있습니다.

 

하지만 스택 세그먼트 레지스터(SS)스택 세그먼트의 범위를 지정할 수는 있어도, 실제 스택의 크기는 지정할 수 없기 때문에 스택 포인터 레지스터(SP)와 베이스 포인터 레지스터(BP)의 초기값을 이용하여 지정합니다.

 

https://velog.io/@wimes/OS-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A1%9C%EB%94%A9-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84

 

먼저 스택으로 사용할 영역을 결정해야 하는데 0x010000(64KB) 주소부터는 OS 이미지가 로딩되므로 0x010000 이하,0x0000:0000 ~ 0x0000:FFFF 영역을 사용할 것이므로 스택 세그먼트 레지스터(SS)의 값을 0x0000으로 설정합니다.

 

그리고 스택의 크기는 넉넉한 것이 좋기 때문스택 포인터 레지스터(SP)와 베이스 포인터 레지스터(BP)를 0xFFFF로 설정하여, 스택 영역의 크기를 세그먼트의 최대 크기로 지정합니다.

 

아래의 어셈블리어 코드부트 로더 앞부분에 추가될 스택 초기화 코드입니다.

 

스택 초기화 어셈블리어 코드

 

이제 이전에 만들었던 메시지 출력 함수를 수정하는데, 함수에서 사용하는 레지스터를 저장하고, 복구하는 코드와 넘겨받은 파라미터를 스택에서 꺼내는 코드 정도만 추가합니다.

 

x86 프로세서에서 스택에 데이터를 넣는 push 명령SP 레지스터가 가리키는 주소에 데이터를 저장하고 SP 레지스터를 감소시키며

 

스택에서 데이터를 꺼내는 pop 명령은 반대로 SP 레지스터를 증가시킵니다.

 

참고) pushpop 명령SP 레지스터와 관계가 있기 때문에 pushpop 명령 대신 데이터를 스택에 직접 복사하고 나서 SP 레지스터의 값을 변경하는 방법으로 같은 결과를 얻을 수 있습니다.

 

 

화면에서 원하는 위치에 문자열을 출력하려면 X 좌표, Y 좌표, 출력할 문자열 주소가 필요합니다.

 

C언어에서는 파라미터의 역순(오른쪽에서 왼쪽)으로 삽입하여 스택에서 꺼낸 순서가 파라미터 순서와 같게 하기 때문에 C언어와의 연계를 고려한다면 중복 작업을 피할 수 있게 C언어의 호출 규약(cdecl 방식)을 따릅니다.

 

아래의 코드는 C언어와 어셈블리어의 함수 호출 코드를 비교하는 코드입니다.

 

파라미터오른쪽에서 왼쪽 방향으로 스택에 삽입하고 함수 호출이 끝난 후에 스택을 정리하는 것을 볼 수 있습니다.

 

참고) 어셈블리어 코드에서 word메모리에 접근할 때 2byte 단위로 접근하라는 것을 의미합니다.

 

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

위의 코드에서 함수를 호출한 뒤, 스택 포인터 레지스터(SP)함수 파라미터로 스택에 삽입된 값을 제거하기 위해 6을 더하였는데 그 이유는

 

16bit 모드에서는 스택에 2바이트(WORD) 크기로 삽입/제거되고 삽입은 스택 포인터 레지스터(SP)를 아래로 이동시키는데

파라미터 3개가 삽입되면 삽입되기 전의 위치에서 -6(2byte * 3)만큼 이동하기 때문에 함수 수행이 끝난 후 스택을 다시 원래대로 복원하기 위해 감소한 만큼 더해줘야 하기 때문입니다.

 


이제 호출되는 함수 쪽 코드를 살펴보면, 호출되는 함수는 파라미터가 순서대로 삽입되어 있다는 것을 이미 알고 있습니다.

 

그렇기 때문에 스택의 특정 위치를 기준으로 오프셋을 이용하여 접근하면 파라미터를 찾게 되는데, 스택의 상위(Top)를 의미하는 스택 포인터 레지스터(SP)스택 관련 명령(push, pop)에 따라 계속 변한다는 문제가 있습니다.

 

스택에 삽입된 파라미터에 접근할 때 시시각각 변하는 스택 포인터 레지스터(SP) 대신 스택에 고정된 값을 가리키는 베이스 포인터 레지스터(BP)를 사용하는 것이 편리하며, 호출된 함수는 베이트 포인터 레지스터(BP) + Offset으로 파라미터에 접근하게 됩니다.

 

호출되는 함수가 작업을 마치고 호출한 코드로 복귀했을 때 코드가 정상적으로 수행되려면 호출되기 전후의 레지스터 상태가 같아야 하는데, 이를 위해서 호출되는 함수에서는 자신이 사용하는 레지스터의 값을 미리 스택에 저장해 두고, 수행이 끝나면 이를 복원하여 호출한 이후의 코드 수행에 영향을 미치지 않아야 하는 특징 때문에 대부분 어셈블리어 함수는 다음과 같은 형태로 정형화되어 있습니다.

 

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

 

위의 코드에서 파라미터에 접근하는 부분베이스 포인터 레지스터(BP)와 파라미터, 복귀 주소, 스택의 관계를 알아야 합니다.

 

아래의 사진은 함수 호출과 그에 따른 스택의 변화를 나타낸 것입니다.

 

 함수를 호출하기 전에는 왼쪽처럼 스택에 함수 파라미터만 들어 있습니다.

 

● 함수 호출을 위해 call 명령을 수행하면 프로세서의 가운데처럼 자동으로 복귀 주소를 스택에 저장하고 스택 포인터 레지스터(SP)의 값에 2를 빼서 복귀 주소를 가리키게 합니다.

 

 그 후호출된 함수는 파라미터 사용을 위해 베이스 포인터 레지스터(BP)의 값을 스택에 저장하고 스택 포인터 레지스터(SP)의 값으로 바꿉니다.

 

 저장한 후 베이스 포인터 레지스터(BP)의 값은 오른쪽처럼 자신이 저장된 스택의 위치를 가리키고 있습니다.

 

 이때 함수 파라미터는 베이스 포인터 레지스터(BP)를 기준으로 일정한 값만큼 증가하는 주소에 위치해 있습니다.

 

 이렇게 베이스 포인터 레지스터(BP)를 기준으로 하여 4byte를 더해준 주소에서 2byte씩 증가시키면서 스택에 접근하는 것입니다.

 

https://velog.io/@wimes/OS-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A1%9C%EB%94%A9-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84

 

기존의 메시지 출력 코드PRINTMESSAGE 함수로 수정하되, 핵심 코드는 같고 파라미터로 출력할 화면 주소를 계산하는 부분만 추가합니다.

 

함수 형태로 수정된 PRINTJMESSAGE 함수의 코드

반응형

+ Recent posts