반응형

이전에는 예외와 인터럽트 모두 임시 핸들러를 등록하여 처리했었고, 임시 핸들러에서 예외나 인터럽트가 발생하기 전의 코드로 다시 돌아가지 않고 무한 루프를 수행하여 그 자리에서 멈추게 했습니다.

 

무한 루프를 수행한 이유는 돌아가기 싫어서가 아니라 돌아갈 수 없었기 때문입니다.

 


 

→ 임시 핸들러의 문제점

 

임시 핸들러두 가지 문제점으로 인해 인터럽트나 예외처리 후 코드로 복귀했을 때 정상적으로 코드를 수행할 수 없습니다.

 

 

1. 인터럽트에서 복귀할 때 사용하는 명령어를 사용하지 않았다는 점

인터럽트나 예외가 발생하면 프로세서는 수행 중인 코드로 복귀하는 데 필요한 기본 정보를 핸들러의 스택에 삽입합니다.

 

핸들러 수행 후 코드로 복귀하려면 저장된 데이터를 프로세서에 다시 복원해야 하는데, 이러한 작업을 수행하려면 IRET 명령어를 명시적으로 사용해야 합니다.

 

 

2. 프로세서의 상태를 완전히 저장 및 복원하지 않았다는 점

프로세서에는 인터럽트 또는 예외 발생 시 자동으로 저장하는 레지스터 외에도 많은 레지스터가 있는데, 이러한 레지스터들은 프로세서가 별도로 관리하지 않으므로 핸들러에서 이를 처리해야 합니다.

 

레지스터를 제대로 관리하지 않는다면 핸들러를 수행하는 동안 변경된 레지스터 때문에 코드가 정상적으로 실행되지 않습니다.

 

만약, 루프를 1000번 실행하게 작성된 프로그램이 3번만 루프를 돌고 빠져나온다면 인터럽트와 콘텍스트를 의심해볼 만합니다.

 

 

이러한 문제로 인해 임시 핸들러인터럽트 또는 예외처리 후, 수행 중이던 코드로 복귀할 수 없습니다.

 


 

→ 콘텍스트 저장과 복원

 

인터럽트 또는 예외가 발생했을 때, 핸들러를 수행한 후 코드로 복귀하려면 프로세서의 상태를 저장하고 복원해야 합니다.

 

프로세서레지스터를 기반으로 코드를 수행하므로 프로세서의 상태는 코드 수행에 관계된 레지스터의 집합이라 할 수 있고, 이렇게 프로세서의 상태와 관계된 레지스터의 집합을 다른 말로 하면 콘텍스트(Context)라고 합니다.

 

 

인터럽트 또는 예외로 인해 핸들러가 수행되거나 어떠한 이유로 현재 수행 중인 코드를 중단하고 나서 다시 수행해야 한다면 전후의 콘텍스트를 동일하게 유지해야 합니다.

 

콘텍스트 유지하는 방법콘텍스트를 위한 메모리 공간을 할당한 뒤, 정해진 순서대로 레지스터를 저장하고 복원하는 것입니다.

 

FS64 OS콘텍스트 처리를 위해 범용 레지스터부터 세그먼트 셀렉터의 순서로 핸들러의 스택에 콘텍스트를 저장하는 방식을 사용합니다.

 

아래의 그림은 FS64 OS에서 콘텍스트를 저장하는 순서입니다.

 

실수 연산에 관련된 FPU 레지스터를 제외한 거의 모든 레지스터를 저장합니다.

 

FS64 OS의 콘텍스트 저장과 복원 순서

 

아래의 코드는 위의 그림과 같이 콘텍스트를 저장한 뒤 핸들러를 실행하고 나서 콘텍스트를 복원하여 실행 중이던 코드로 복귀하는 예입니다.

 

위의 그림에서 프로세서가 처리하는 부분으로 표시한 영역은 프로세서가 처리하는 부분이므로 이 부분을 제외한 나머지 부분을 저장하고 복원하게 작성합니다.

 

콘텍스트를 저장하고 복원하는 코드 1

 

콘텍스트를 저장하고 복원하는 코드 2

 

위의 그림과 코드를 보면, 인터럽트나 예외가 발생했을 때 프로세서가 하는 역할은 단순히 IST 스택에 콘텍스트의 일부를 저장하고 복원하는 것뿐입니다.

 

즉, 프로세서는 복원하는 콘텍스트가 이전에 자신이 저장한 콘텍스트와 같은지 비교하지 않기 때문에 인터럽트나 예외가 발생했을 때 저장한 콘텍스트가 아닌 다른 콘텍스트로 복원할 수 있음을 뜻합니다.

 

만약, 콘텍스트 영역을 여러 개 만들고, 인터럽트가 발생했을 때 이들을 순차적으로 교환한다면 소프트웨어 멀티태스킹을 구현할 수 있습니다.

 

멀티 태스킹이란 여러 개의 태스크(작업)를 교대로 실행하여 마치 동시에 여러 개의 프로그램이 실행되는 것과 같은 효과를 내는 것입니다.

 

멀티태스킹에 대한 자세한 내용은 후에 알아보도록 합니다.

 


 

→ 인터럽트와 예외 핸들러 업그레이드

 

 

이전에는 테스트를 위해 간단히 작성한 임시 핸들러를 모든 IDT 테이블에 등록했었지만 이번에는 인터럽트와 예외에 따라 핸들러를 개별적으로 등록하여, 인터럽트와 예외를 구분하여 처리하도록 합니다.

 

인터럽트와 예외처리 방법은 스택에 삽입된 에러 코드만 제외하면 완전히 같으므로 먼저 인터럽트를 중심으로 봅니다.

 

 

핸들러 작성하는데 필요한 내용은 이전에 이미 알아보았습니다.

 

핸들러크게 콘텍스트를 저장하고 복원하는 역할을 담당하는 어셈블리어 코드(ISR, Interrupt Service Routine)실제 처리를 담당하는 C 코드 핸들러 함수로 구성됩니다.

 

핸들러 전체를 어셈블리어로 작성할 계획이라면 C 코드로 작성된 핸들러 함수는 필요하지 않지만, 어셈블리어로 작성하기에는 핸들러의 기능이 다소 복잡하기 때문에 어셈블리어 코드는 콘텍스트 처리만 담당하고 나머지 작업은 C 코드에서 처리합니다.

 

게다가 반복 입력도 입력이지만 이런 코드는 유지보수에도 문제가 있습니다.

 

하지만 다행히도 NASM은 반복되는 작업을 위해 매크로 기능을 제공하고 있기 때문에 매크로를 사용하여 ISR 함수를 작성합니다.

 

NASM에서 매크로를 정의하려면 %macro와 %endmacro 사이에 매크로 이름과 파라미터 수, 코드를 삽입합니다.

 

매크로를 사용하는 방법C 언어와 같으며 삽입할 위치에 정의한 이름을 입력하면 됩니다.

 

아래의 코드는 KSAVECONTEXT와 KLOADCONTEXT 매크로를 정의하여 ISR 함수를 작성한 것입니다.

 

매크로를 활용하여 작성한 ISR 함수 1

 

매크로를 활용하여 작성한 ISR 함수 2

 

위 코드를 보면 C 코드로 작성된 핸들러 함수를 호출하기 전에 RDI 레지스터에 인터럽트 벡터를 삽입합니다.

 

이것은 핸들러 함수에 벡터 번호를 넘겨줌으로써, 인터럽트가 발생한 PIC 컨트롤러에 EOI를 전송하려고 추가한 것입니다.

 

 

아래는 위 코드에서 사용한 핸들러의 코드입니다.

 

파라미터로 넘겨받은 벡터 번호를 이용하여 PIC 컨트롤러에 EOI를 전송하고, 화면의 왼쪽과 오른쪽 위에 벡터 번호와 발생한 횟수를 출력합니다.

 

인터럽트 핸들러 코드

 


 

인터럽트와 예외 핸들러를 추가했으므로 IDT 테이블에 등록하는 핸들러 함수 역시 변경해야 합니다.

 

핸들러를 변경하는 방법은 이전 코드의 kDummyHandler 부분을 추가한 핸들러로 대체하면 됩니다.

 

아래는 IDT 테이블의 핸들러를 대체하는 코드입니다.

 

핸들러의 주소를 제외한 나머지는 기존 코드와 같습니다.

 

IDT 테이블의 핸들러를 새로운 핸들러로 대체하는 코드

 


 

→ 인터럽트 활성화와 비활성화

 

PIC 컨트롤러가 프로세서에 아무리 인터럽트 신호를 전달한다 해도, 프로세서가 인터럽트를 무시하게 설정되어 있다면 핸들러가 수행되지 않습니다.

 

프로세서의 RFLAGS 레지스터에는 인터럽트 발생 가능 여부를 설정하는 IF 비트가 있으며, 해당 비트가 1로 설정되었을 때만 인터럽트가 발생합니다.

 

그러므로 OS가 인터럽트를 처리하려면 IF 비트를 직접 관리해야 합니다.

 

x86 프로세서는 인터럽트를 활성화하고 비활성화하는 명령어를 제공합니다.

 

만약 인터럽트를 활성화하고 싶다면 STI 명령어를, 비활성화하고 싶다면 CLI 명령어를 사용하면 되지만, 프로세서는 인터럽트를 활성화 또는 비활성화하는 명령어만 지원할 뿐 지금 프로세서의 상태가 어떤지에 대한 명령어는 별도로 지원하지 않습니다.

 

그 대신 RFLAGS 레지스터를 스택에 저장하는 PUSHFQ 명령어를 제공하므로 이를 이용하여 간접적으로 확인할 수 있습니다.

아래의 코드는 인터럽트를 화성화 또는 비활성화하는 어셈블리어 함수와 RFLAGS 레지스터의 값을 읽는 어셈블리어 함수, 함수 선언입니다.

 

참고)

PUSHFQ 명령어 - www.felixcloutier.com/x86/pushf:pushfd:pushfq 

 

인터럽트 발생 가능 여부를 제어하고 RFLAGS 레지스터의 값을 읽는 어셈블리어 함수

 


 

→ PIC 컨트롤러 파일 추가

 

PIC 컨트롤러 파일은 ISR 파일과 더불어 이번 내용에서 핵심 파일입니다.

 

PIC 컨트롤러 파일은 PIC 컨트롤러 제어에 관련된 함수와 매크로가 정의되어 있습니다.

 

PIC 컨트롤러 파일은 02.Kernel64/source 디렉터리에 있으며 파일의 내용은 아래와 같습니다.

 

 

PIC 컨트롤러 소스 파일(02.Kernel64/Source/PIC.c) 1

 

PIC 컨트롤러 소스 파일(02.Kernel64/Source/PIC.c) 2

 


 

PIC 컨트롤러 헤더 팡리(02.Kernel64/Source/PIC.h)

 


 

→ ISR 파일 추가

 

ISR 파일은 IDT 테이블에 등록할 어셈블리어 핸들러 코드가 정의된 파일입니다.

 

ISR 파일은 02.Kernel64/Source 디렉터리에 있으며 파일 내용은 아래와 같습니다.

 

ISR 소스 파일(02.Kernel64/Source/ISR.asm) 1

 

ISR 소스 파일(02.Kernel64/Source/ISR.asm) 2

 

ISR 소스 파일(02.Kernel64/Source/ISR.asm) 3

 

ISR 소스 파일(02.Kernel64/Source/ISR.asm) 4

 

ISR 소스 파일(02.Kernel64/Source/ISR.asm) 5

 

ISR 소스 파일(02.Kernel64/Source/ISR.asm) 6

 

ISR 소스 파일(02.Kernel64/Source/ISR.asm) 7

 

ISR 소스 파일(02.Kernel64/Source/ISR.asm) 8

 

ISR 소스 파일(02.Kernel64/Source/ISR.asm) 9

 

ISR 소스 파일(02.Kernel64/Source/ISR.asm) 10

 

ISR 소스 파일(02.Kernel64/Source/ISR.asm) 11

 

ISR 소스 파일(02.Kernel64/Source/ISR.asm) 12

 

ISR 소스 파일(02.Kernel64/Source/ISR.asm) 13

 

ISR 소스 파일(02.Kernel64/Source/ISR.asm) 14

 


 

ISR 헤더 파일(02.Kernel64/Source/ISR.h)

 

ISR 헤더 파일(02.Kernel64/Source/ISR.h) 2

 


 

→ 인터럽트 핸들러 파일 추가

 

인터럽트 핸들러 파일은 C 언어로 작성된 핸들러의 코드가 담긴 파일입니다.

 

C 언어로 작성된 핸들러는 어셈블리어로 작성된 핸들러가 호출하는 함수로 콘텍스트에 관련된 부분을 제외한 나머지 처리를 담당합니다.

 

인터럽트 핸들러 파일은 02.Kernel64/Source 디렉터리에 있으면 파일 내용은 아래와 같습니다.

 

인터럽트 핸들러 소스 파일(02.Kernel64/Source/InterruptHandler.c) 1

 

인터럽트 핸들러 소스 파일(02.Kernel64/Source/InterruptHandler.c) 2

 


 

인터럽트 핸들러 헤더 파일(02.Kernel64/Source/InterruptHandler.h)

 


 

→ 어셈블리어 유틸리티 파일 수정

 

이번 내용에서 kEnableInterrupt(), kDisableInterrupt(), kReadRFLAGS()의 세 가지 어셈블리어 함수가 추가되었으므로, 어셈블리어 유틸리티 파일에 반영해야 합니다.

 

아래의 코드는 어셈블리어 유틸리티 파일의 내용입니다.

 

어셈블리어 유틸리티 소스 파일(02.Kernel64/Source/AssemblyUtility.asm) 1

 

어셈블리어 유틸리티 소스 파일(02.Kernel64/Source/AssemblyUtility.asm) 2

 


 

어셈블리어 유틸리티 헤더 파일(02.Kernel64/Source/AssemblyUtility.h) 2


 

→ 디스크립터 파일 수정

 

새로운 핸들러가 추가되었으므로 IDT 테이블 역시 수정하여 핸들러를 등록해야 합니다.

 

아래의 코드는 02.Kernel64/Source 디렉터리의 Descriptor.c 파일의 내용입니다.

 

바뀐 부분은 kInitializeIDTTables() 함수이므로 이 함수를 제외한 나머지 부분은 기존 코드와 같습니다.

 

디스크립터 소스 파일(02.Kernel64/Source/Descriptor.c 1

 

디스크립터 소스 파일(02.Kernel64/Source/Descriptor.c 2

 

디스크립터 소스 파일(02.Kernel64/Source/Descriptor.c 3

 

디스크립터 소스 파일(02.Kernel64/Source/Descriptor.c 4

 

디스크립터 소스 파일(02.Kernel64/Source/Descriptor.c 5

 

디스크립터 소스 파일(02.Kernel64/Source/Descriptor.c 6

 


 

→ C 언어 커널 엔트리 포인트 파일 수정

 

이제 마지막 단계인 C 언어 커널 엔트리 포인트 파일만 수정하면 됩니다.

 

이번에도 이전의 내용에서와 마찬가지로 추가된 함수를 호출하여 PIC 컨트롤러를 초기화하고 인터럽트를 활성화하는 순서로 진행합니다.

 

main() 함수 뒷부분에 PIC를 초기화하는 kInitializePIC() 함수를 호출한 뒤, PIC 컨트롤러에 연결된 모든 디바이스에 대해서 인터럽트가 발생할 수 있게 kMaskPICInterrupt() 함수를 호출하고, 마지막으로 kEnableInterrupt() 함수를 호출하여 프로세서가 인터럽트를 처리하게 합니다.

 

아래의 코드는 위의 방법으로 수정한 main() 함수입니다.

 

IA-32e 모드 커널의 C 언어 엔트리 포인트 소스 코드(02.Kernel64/source/Main.c) 1

 

IA-32e 모드 커널의 C 언어 엔트리 포인트 소스 코드(02.Kernel64/source/Main.c) 2

 

IA-32e 모드 커널의 C 언어 엔트리 포인트 소스 코드(02.Kernel64/source/Main.c) 3

 


 

→ 빌드와 실행

 

이렇게 인터럽트 처리와 예외처리를 위해 필요한 모든 여건이 갖춰졌으며, 이제 정상적으로 동작하는지 확인하면 됩니다.

 

소스 코드를 빌드한 후, 생성된 Disk.img 파일을 QEMU나 PC에서 실행하면 아래의 사진과 같은 화면이 표시됩니다.

 

오른쪽 위의 INT 32가 표시되고, 발생 횟수가 계속해서 바뀌면 일정한 간격으로 인터럽트가 발생하고 있음을 추측할 수 있습니다.

 

인터럽트 벡터 32번에 대한 내용은 이후에 더 자세히 다룹니다.

 

INT 32번이 발생한 화면

 

이제 키보드를 눌렀을 때 정상적으로 핸들러가 등록되었다면 키보드를 누를 때마다 아래의 사진처럼 왼쪽 위에 INT 33과 발생 횟수가 표시됩니다.

 

키보드를 반복해서 입력할 때마다 인터럽트의 횟수가 바뀌면 키보드에 전송되는 스캔 코드에 맞춰 인터럽트가 발생한다는 것입니다.

 

INT 33번이 발생한 화면

 

이렇게 인터럽트 처리와 예외 처리에 대한 내용이 모두 끝났고, 이전 내용과 이번 내용에서 구현한 인터럽트와 예외처리 부분은 고속도로와 같아서 한 번 구축해두면 여러 디바이스가 공통으로 사용할 수 있습니다.

 

이번 내용을 기점으로 어떤 예외 혹은 인터럽트가 발생하더라도 처리할 수 있는 능력이 생겼으니 위의 QEMU 화면은 부실해 보여도 큰 의미가 있습니다.

반응형

+ Recent posts