반응형

→ 인터럽트와 예외의 차이점

 

인터럽트와 예외의 공통점코드 수행 도중에 발생하거나 프로세서에 의해 처리가 필요한 일종의 이벤트(Event)라는 점입니다.

 

인터럽트나 예외가 발생하면 프로세서는 이를 처리하려고 준비한 특수 코드를 실행하며, 실행이 끝난 뒤에는 수행 중이던 코드로 복귀합니다.

 

인터럽트와 예외의 차이점이벤트를 발생시키는 주체에 있습니다.

 

인터럽트 : 키보드나 마우스, 하드 디스크 같은 외부 디바이스에 의해 발생하여 프로세서에 전달되는 이벤트입니다.

 

예외 : 프로세서가 코드를 수행하는 도중에 페이지 폴트나 잘못된 명령 같은 오류를 발견했을 때 발생하는데, 즉 주체가 인터럽트와 달리 프로세서 자신입니다.

 

아래의 그림을 보면 이러한 공통점과 차이점을 알 수 있습니다.

 

인터럽트와 예외 발생 시점의 처리 과정

 

이벤트가 인터럽트에 의해 발생하든 예외에 의해 발생하든 이벤트별로 특수한 처리 함수가 필요하고, 이런 처리 함수인터럽트 또는 예외 핸들러라고 부릅니다.

 

핸들러인터럽트 또는 예외처리 후 발생한 시점으로 정상적으로 복귀하도록 프로세서의 상태를 저장/복구하는 역할도 같이 수행합니다.

 

IA-32e 프로세서는 인터럽트와 예외에 대해서 개별 핸들러를 설정할 수 있게 특별한 테이블을 제공합니다.

 


 

→ IDT와 IDT 게이트 디스크립터

 

프로세서외부 인터럽트나 소프트웨어 인터럽트(SWI)가 발생했을 때 벡터 테이블의 인덱스에 해당하는 주소로 이동하여 처리 함수를 수행합니다.

 

벡터 테이블각 운영 모드마다 존재하며 리얼 모드의 벡터 테이블은 세그먼트:주소의 형태로 주소 0x0000:0x0000에 위치하고, 보호 모드와 IA-32e 모드에서는 이와 달리 IDT라고 불리는 특수한 형태의 벡터 테이블을 사용합니다.

 

IDTGDT와 마찬가지로 IDT 게이트 디스크립터로 구성된 테이블이며, IDTR 레지스터를 통해 프로세서에 IDT 테이블의 정보를 설정할 수 있습니다.

 

IDT 테이블은 최대 256개의 엔트리를 포함할 수 있고, IA-32e 모드의 IDT 게이트 디스크립터는 16byte를 차지합니다.

 

아래의 그림은 IDT 테이블과 IDTR 레지스터, 인터럽트와 예외 핸들러의 관계를 나타낸 그림입니다.

 

IDT 테이블, IDTR 레지스터, 핸들러의 관계

 

IDT 게이트 디스크립터인터럽트, 예외처리 함수의 주소, 코드 세그먼트 셀렉터, 게이트 디스크립터 타입이나 권한 같은 여러 정보를 포함하고 있습니다.

 

아래는 IDT 게이트 디스크립터의 구조와 각 필드에 대한 세부 설명입니다.

 

IA-32e 모드의 IDT 게이트 디스크립터 구조

 

IDT 게이트 디스크립터의 필드와 의미

 

IDT 게이트 디스크립터는 다른 디스크립터와 달리 세그먼트 셀렉터를 포함하는데, 세그먼트 셀렉터는 인터럽트 또는 예외 핸들러가 수행될 때 코드 세그먼트를 교체하여, 핸들러 함수를 수행하는데 충분한 권한(특권 레벨)으로 상승시키는 역할을 합니다.

 

핸들러 실행을 위해 권한을 상승시켜야 하는 이유유저 레벨의 애플리케이션 코드(Ring 3)를 실행하는 도중 인터럽트 또는 예외가 발생한 경우를 예를 들자면, 일반적으로 유저 레벨은 특권 레벨이 가장 낮고 커널 레벨은 가장 높습니다.

 

프로세서는 높은 레벨(커널 레벨, Ring 0)에서 낮은 레벨(유저 레벨, Ring 3)의 영역에 접근하는 것은 허락하지만, 그 반대로 낮은 레벨에서 높은 레벨에 접근하는 것은 허락하지 않기 때문에 유저 레벨의 애플리케이션 코드(Ring 3)를 실행하는 도중 인터럽트 또는 예외가 발생할 경우, 커널 영역에 있는 핸들러(Ring 0)를 실행하면 권한 문제로 오류가 발생합니다.

 

이러한 문제를 해결하기 위해 게이트 디스크립터의 세그먼트 셀렉터에 커널 레벨의 코드 디스크립터를 설정하고, 프로세서가 인터럽트 또는 예외 발생 시 자동으로 코드 세그먼트 셀렉터를 교체하는 방법을 사용합니다.

 

 

위의 그림과 표를 보면 IDT 게이트 디스크립터에도 여러 가지 타입이 있습니다.

 

보호 모드태스크 게이트(Task Gate), 인터럽트 게이트(Interrupt Gate), 트랩 게이트(Trap Gate) 등 세 가지 게이트 디스크립터 타입을 지원하며, IA-32e 모드는 태스크 게이트를 제외한 두 가지 타입만 지원합니다.

 

태스크 게이트 : 인터럽트나 예외가 발생했을 때 특정 태스크로 전환하여 처리하는 방식입니다.

인터럽트 게이트, 트랩 게이트 : 특정 핸들러로 처리하는 방식입니다.

 

IA-32e 모드에서는 태스크 게이트를 지원하지 않으므로 인터럽트 게이트와 트랩 게이트만 알아봅니다.

 

 

인터럽트 게이트와 트랩 게이트의 차이핸들러를 수행하는 동안 다른 인터럽트가 발생할 수 있는가입니다.

 

인터럽트나 예외를 처리하는 동안 인터럽트가 더 이상 발생하지 않게 하고 싶다면 인터럽트 게이트를 사용하고, 반대로 핸들러를 수행하는 도중에 인터럽트가 발생하는 것을 허락한다면 트랩 게이트를 사용합니다.

 

FS64 OS는 핸들러를 수행하는 도중 인터럽트가 발생하는 것을 허락하지 않으므로 인터럽트 게이트만 사용합니다.

 

 

ISTIA-32e 모드에서 도입된 새로운 개념으로, 보호 모드에서는 지원하지 않는 기능입니다.

 

IST인터럽트 스택 테이블(Interrupt Stack Table)의 약자로 인터럽트나 예외가 발생했을 때 별도의 스택 공간을 할당하는 기능입니다.

 

별도의 스택 공간을 할당하는 이유핸들러를 수행하던 도중 스택 공간이 부족하여 코드나 데이터 영역을 덮어쓰는 불상사를 막기 위해서입니다.

 

 

인터럽트예외코드를 수행하는 도중에 발생하는 이벤트이므로 핸들러가 수행될 때 스택의 상태를 예측할 수 없는데, 수행 중이던 코드가 스택을 거의 다 할당할 정도로 지역 변수를 사용하고 있을 때 핸들러가 수행되었다면, 스택 범위를 초과하여 다른 영역을 침범할 수 있습니다.

 

다른 영역을 침범하면 코드로 복귀했을 때 코드 또는 데이터가 변경되어 정상적으로 수행되지 않으므로 이러한 문제를 해결하고자 새롭게 도입된 방식이 IST입니다.

 

 

IST 방식의 특징인터럽트나 예외가 발생했을 때 핸들러 수행 전에 IST 필드에 지정된 스택 주소로 자동으로 전환합니다.

 

태스크가 사용하던 스택을 그대로 사용하는 것이 아니라 완전히 새로운 스택에서 처음부터 시작함으로써 핸들러 수행에 필요한 충분한 크기의 스택을 할당하는 것입니다.

 

FS64 OS 역시 IST 방식을 사용하여 스택 부족으로 말미암은 문제를 해결합니다.

 


 

→ 인터럽트와 예외의 종류

 

x86 프로세서IDT 테이블의 상위 32개 디스크립터를 예약해서 예외 처리에 사용하는데, 그 중에서 실제로 사용하는 것은 20개이며, 나머지 12개는 앞으로의 사용을 위해 미리 예약한 영역입니다.

 

예외 처리에 할당한 0 ~ 31번을 제외한 나머지 224개는 OS가 임의로 사용할 수 있는 부분인데, 일반적으로 인터럽트 처리와 애플리케이션 라이브러리 지원을 위한 시스템 콜 용도로 사용됩니다.

 

 

프로세서가 사용하는 20개의 예외는 다시 Faults, Traps, Aborts 세 가지로 분류할 수 있습니다.

 

Faults : 문제가 발생했으나 해당 코드의 문제를 수정하면 정상적으로 실행 가능한 예외를 의미합니다.

Fault가 발생하면 복귀할 주소는 Fault가 발생한 코드를 가리키며, 핸들러의 실행이 완료된 후 해당 코드부터 다시 수행합니다.

 

Traps : Trap을 유발하는 명령어를 실행했을 때 발생하며, 디버깅 명령어와 깊은 관계가 있습니다.

Trap이 발생하면 복귀할 주소는 Trap이 발생한 코드의 다음 코드를 가리키며, 핸들러의 실행이 완료된 후 해당 코드의 다음부터 수행합니다.

 

Aborts : 심각한 문제가 발생하여 문제가 발생한 정확한 위치를 찾을 수 없으며, 더 이상 코드를 수행할 수 없는 경우에 발생합니다.

Abort가 발생하면 시스템은 정상적으로 동작할 수 없는 상태이므로 OS가 오동작 하거나 시스템이 리부팅될 수 있습니다.

 

 

아래의 표는 인텔에서 정의한 32개의 예외와 인터럽트 목록입니다.

 

예외가 발생하는 상황을 모두 설명하기에는 양이 많으므로 예외를 처리하는데 필요한 최소한의 정보만 있습니다.

 

자세한 내용은 인텔 매뉴얼 Volume 3A:System Programming Guide를 참고하면 됩니다.

 

목록을 보면 몇 가지 특이한 점이 있는데, 예외가 반환하는 에러 코드와 벡터 2에 자리 잡고 있는 NMI Interrupt 입니다.

 

에러 코드는 예외가 발생했을 때의 상태를 나타내는 코드로 최상위(Top)에 위치합니다.

 

에러 코드가 포함된 예외의 경우 핸들러 함수는 스택에서 에러 코드를 제거하는 작업이 필요한데 이 부분은 나중에 알아봅니다.

 

보호 모드, IA-32e 모드의 예외와 인터럽트 목록

 

벡터 2에 있는 NMI Interrupt프로세서에 연결된 NMI 핀(Nonmaskable Interrupt Pin)으로 전달되는 인터럽트를 의미하며, 프로세서에 직접 전달되는 인터럽트인 만큼 프로세서 외부에서 심각한 장애가 발생했을 때 발생합니다.

 

다른 예외와 달리 인터럽트 타입이지만 OS 입장에서는 큰 차이가 없으므로 다른 예외와 같은 방식으로 처리하면 됩니다.

 


 

→ PC 인터럽트의 종류와 발생 원인

 

과거의 PCPIC(Programmable Interrupt Controller) 컨트롤러를 사용해서 외부 인터럽트를 관리했습니다.

 

PIC 컨트롤러 한 개에는 8개의 인터럽트 핀이 있으며, PC는 PIC 컨트롤러 2개를 마스터-슬레이브(Master-Slave) 방식으로 연결하여 최대 15개의 외부 인터럽트를 처리했습니다.

 

마스터-슬레이브 방식으로 연결된 PIC 컨트롤러는 총 16개의 핀으로 구성되며, 실제로 디바이스를 연결할 수 있는 핀의 수는 15개입니다.

 

아래의 표는 각 핀에 연결된 PC 디바이스를 정리한 것입니다.

 

키보드와 마우스부터 하드디스크까지 다양한 PC 디바이스에 대한 인터럽트를 관리하고 있음을 알 수 있습니다.

 

참고)

IRQ 번호는 마스터에서 슬레이브 순으로 핀 번호를 할당한 것으로 디바이스에 관련된 인터럽트 핀을 언급할 때 물리적인 핀 번호 대신 IRQ 번호를 주로 사용하며, FS64 OS 역시 IRQ 번호를 사용합니다.

 

 

비록 IRQ 번호는 0부터 15번까지 고정되어 있지만 인터럽트 벡터까지 0번부터 15번으로 고정된 것은 아닙니다.

 

PIC 컨트롤러의 경우, 프로세서로 전달하는 인터럽트 벡터를 임의로 설정할 수 있기 때문입니다.

 

FS64 OS는 32개의 예외에 이어서 IRQ용 인터럽트 벡터를 할당하여, 벡터 32번부터 47번까지를 사용하고 있습니다.

(PIC 컨트롤러의 IRQ 별로 벡터를 할당하는 방법은 다음에 알아봅니다)

 

PIC 컨트롤러의 핀과 연결된 디바이스

 


 

→ 인터럽트와 예외, 스택과 태스크 상태 세그먼트

 

인텔 프로세서인터럽트나 예외가 발생했을 때 이를 처리한 후 다시 코드로 복귀할 수 있는 메커니즘을 제공하며, 핵심적인 역할을 하는 것이 바로 스택과 스택 스위칭입니다.

 

스택 스위칭인터럽트나 예외가 발생했을 때 태스크를 수행 중이던 스택에서 인터럽트 또는 예외처리용 스택으로 전환하는 것을 의미하고, 이때 프로세서는 다시 복귀할 위치의 정보를 함께 저장합니다.

 


 

→ 스택 스위칭과 IST

 

x86 프로세서인터럽트나 예외가 발생했을 때, IDT 게이트 디스크립터에 설정된 코드 디스크립터의 권한이 현재 수행 중인 코드의 권한보다 높으면 새로운 스택으로 전환하는데, 이러한 과정을 스택 스위칭(Stack Switching)이라고 하며, 스택 스위칭을 하는 이유는 크게 두 가지입니다.

 

1. 높은 권한의 코드, 즉 핸들러가 스택 공간의 부족으로 오류가 발생하는 일을 미리 방지합니다.

유저 애플리케이션의 함수와 달리 높은 권한을 가지는 커널 영역의 함수들은 한 프로그램에 국한되는 것이 아니라 시스템 전체에 영향을 줍니다.

커널 함수를 수행하다 스택 부족으로 오류가 발생한다면 그 영향은 시스템 전체로 퍼지게 됩니다.

 

2. 권한이 높은 함수가 낮은 권한의 스택을 공유함으로써 발생할 수 있는 간섭을 최소화합니다.

커널 레벨의 함수는 애플리케이션 함수보다 견고하게 작성되지만, 버그에서 완전히 벗어나지는 못하기 때문에 스택을 공유한다면 커널 함수의 버그로 스택의 내용이 변조될 수 있으며, 실행 중이던 코드로 복귀했을 때 정상적으로 수행되지 않을 수 있습니다.

 

 

보호 모드도 스택 스위칭 기능을 지원하지만, 한 가지 맹점으로 권한이 변경되어야만 스택 스위칭이 발생하는데, 이는 이미 최상위 권한을 가지는 커널 코드를 실행하다가 인터럽트가 발생하면, 권한의 변동이 없으므로 스택이 스위칭되지 않음을 의미합니다.

 

IA-32e 모드에서는 이러한 문제를 해결하기 위해 인터럽트 스택 테이블(IST, Interrupt Stack Table)이라는 새로운 기법을 도입했습니다.

 

 

IST인터럽트 처리를 위한 스택을 정의한 테이블로 최대 7개의 스택을 지정할 수 있습니다.

 

IST를 사용하면 기존의 스택 스위칭 기법과는 달리 무조건 스택 스위칭이 발생하게 되는데, 이를 통해 커널 코드 수행 중에 인터럽트나 예외가 발생했을 때 스택 부족으로 인한 문제를 해결할 수 있습니다.

 

FS64 OSIST를 사용하며 커널 스택 영역 이후 7MB ~ 8MB 영역을 IST 공간으로 할당합니다.

 

인터럽트나 예외 벡터에서 IST 스택을 사용하게 설정하는 방법IDT 테이블의 해당 게이트 디스크립터를 찾아 IST 필드를 1 ~ 7 사이의 값으로 설정하면 됩니다.

IST 필드를 0으로 설정하면 기존의 스택 스위칭 방식을 사용합니다.

 

 

IST를 사용하면 무조건 스택 스위칭이 발생하게 되는데, 인터럽트나 예외가 발생했을 때는 스택 스위칭이 수행된 직후에 IST 스택에 삽입된 데이터에서 찾는 방식으로 스택 스위칭을 수행하고 다시 수행 중이던 코드로 복귀합니다.

 

아래의 그림은 인터럽트가 발생한 전후의 상태와 각 스택의 내용을 나타낸 것입니다.

 

인터럽트 발생 전후의 각 스택의 내용

 

위의 그림을 보면 인터럽트 또는 예외가 발생했을 때 핸들러의 스택에서 수행 중이던 코드의 정보(CS와 RIP 레지스터)와 스택의 정보(SS와 RSP 레지스터)가 삽입됨을 알 수 있는데, 이런 정보와 함께 프로세서의 플래그 레지스터(RFLAGS)를 저장하기 때문에 핸들러가 수행을 완료하고 나서 코드로 복귀했을 때 코드가 정상적으로 수행되는 것입니다.

 

즉 x86 프로세서의 인터럽트와 예외처리의 비밀은 핸들러의 스택에 수행 중이던 태스크의 스택과 코드와 상태 정보를 저장하고 복원하는 것입니다.

 


 

→ 프로세서와 태스크 상태 세그먼트, 태스크 디스크립터

 

태스크 상태 세그먼트(TSS, Task Status Segment)태스크의 상태를 저장하는 영역으로 프로세서의 상태, 즉 레지스터의 값을 저장하는 역할을 합니다.

 

보호 모드의 TSSFPU에 관련된 레지스터를 제외한 프로세서의 모든 레지스터를 저장할 수 있으며, 하드웨어 멀티태스킹 구현에서 핵심 역할을 맡습니다.

 

그리고 권한별 스택 정보를 저장하는 역할과 유저 애플리케이션이 I/O 포트에 접근하는 것을 제한하는 I/O 맵 어드레스의 주소를 지정하는 역할도 같이 수행합니다.

 

아래의 그림은 보호 모드의 TSS를 나타낸 것입니다.

 

SS와 ESP 뒤에 붙은 숫자는 특권 레벨(Ring 0~3)을 의미합니다.

 

하지만, IA-32e 모드로 옮겨가면서 TSS의 위상은 바닥으로 추락했는데, 레지스터의 크기가 커지면서 기존의 104byte로는 모든 레지스터를 담을 수 없게 된 것입니다.

 

하드웨어 멀티태스킹에서 저장소의 역할을 하던 TSS가 제 역할을 못하게 됐기 때문에, IA-32e 모드에는 더 이상 하드웨어 멀티태스킹 기능을 지원하지 않지만, TSS가 완전히 쓸모없어진 것은 아닙니다.

 

IA-32e 모드총 7개의 IST의 정보를 저장하는 역할을 TSS에 부여했기 때문에 그 외에 특권 레벨에 따른 스택 정보를 저장하는 역할과 I/O 맵 기준 주소를 저장하는 역할도 여전히 건재합니다.

 

보호 모드의 TSS 세그먼트 구조

 

아래는 IA-32e 모드의 TSS 세그먼트 구조를 나타낸 것입니다.

 

IST 1부터 IST 7까지의 영역은 IDT 게이트 디스크립터의 IST 필드와 관계가 있습니다.

 

IDT 게이트 디스크립터의 IST 영역에 정보를 저장하고, TSS 세그먼트의 해당 필드에 스택 정보를 저장함으로써 IST 방식을 사용할 수 있습니다.

 

기존 스택 스위칭 방식에서 사용되는 RSP0 ~ RSP2 필드는 IST 기능을 사용하면 참조하지 않습니다.

 

IA-32e 모드의 TSS 세그먼트 구조

 

TSS 세그먼트에 있는 I/O 맵 기준 주소 필드I/O 제한을 설정하는 비트맵(I/O Permission Bit Map)의 시작 주소를 나타냅니다.

 

I/O 제한 비트맵은 현재 수행 중인 코드의 특권 레벨(CPL)과 RFLAGS 레지스터에 설정된 IO 특권 레벨(IOPL)의 값을 비교하여, CPL이 더 낮으면 1로 설정된 포트 어드레스의 I/O를 제한하는 역할을 합니다.

 

비트맵의 바이트 순서와 비트 인덱스하위 바이트/비트에서 상위 바이트/비트의 순서로 증가하며, 각 비트맵은 해당 위치의 I/O 포트 주소와 1:1 대응하고, 비트맵의 길이는 제어할 I/O 어드레스에 따라 달라지며, 비트맵의 끝을 나타내는 마지막 byte는 반드시 0xFF로 설정해야 합니다.

 

만약 애플리케이션이 9번 I/O 포트에 접근하는 것을 막고 싶다면 먼저 I/O 비트맵을 위해 3byte를 할당한 다음 마지막 바이트를 0xFF로 설정한 뒤 비트맵의 두 번째 byte에서 비트 2를 1로 설정하고 나서 RFLAGS 레지스터의 IOPL 필드를 최고 특권 레벨인 Ring 0으로 설정하면 됩니다.

 

FS64 OS에서는 애플리케이션이 I/O 포트에 접근하는 것을 막지 않기 때문에 I/O 비트맵을 사용하지 않습니다.

 

 

사실 구조는 거창하지만 TSS 세그먼트는 메모리 어드레스에 있는 단순한 데이터일 뿐이기 때문에 프로세서가 TSS 세그먼트를 참조할 수 있게 TSS 세그먼트의 주소를 알려줘야 합니다.

 

프로세서에 TSS 세그먼트에 대한 정보를 알려주는 역할을 하는 것이 TSS 디스크립터(TSS Descriptor)와 LTR 어셈블리어 명령어입니다.

 

TSS 디스크립터는 GDT 테이블에 있으며 다른 디스크립터와 달리 16byte를 차지합니다.

 

TSS 디스크립터의 하위 8byte는 기존의 세그먼트 디스크립터와 필드 구성이 같고 상위 8byte는 64bit 어드레스를 위한 공간으로 사용됩니다.

 

아래의 그림은 IA-32e 모드의 TSS 디스크립터 구조를 나타낸 것입니다.

 

IA-32e 모드의 TSS 디스크립터

 

TSS 디스크립터의 상위 8byte를 제외한 나머지 하위 8byte는 이전에 봤던 세그먼트 디스크립터와 필드가 거의 같으며, 하위 8byte에 새롭게 등장한 B 필드는 Busy를 나타내는 필드로 1로 설정되면 현재 TSS 디스크립터가 가리키는 태스크가 실행 중임을 나타냅니다.

 

B 필드TSS 세그먼트와 TSS 디스크립터가 프로세서 상태와 밀접한 관계가 있던 보호 모드에서 유용했던 필드이며, IA-32e 모드에서 동작하는 FS64 OS는 B 필드를 참조하지 않습니다.

 

 

GDT 테이블에 위와 같은 구조의 TSS 디스크립터를 생성했다면, 이제 프로세서에 현재 수행 중인 태스크의 TSS가 어떤 것인지 알려줘야 합니다.

 

이 작업은 LTR 어셈블리 명령어로 수행할 수 있으며, LTR은 LGDT나 LIDT 명령어와 달리 GDT 테이블 내의 TSS 디스크립터의 오프셋을 사용합니다.

 

LTR 명령어를 수행하는 순간 프로세서에 TSS 세그먼트의 정보가 로드되므로 모든 작업이 완료된 후 LTR 명령을 마지막에 수행해야 합니다.

반응형

+ Recent posts