Exception Control Flow 소개

mydailylogs
|2022. 12. 6. 22:52

프로세서에 전원을 처음 공급하는 시점부터 전원을 끌 때까지 프로그램 카운터는 연속된 값들을 가정한다. 프로그램 카운터는 계속해서 인스트럭션 I에 해당하는 주소를 받게 되고, 이러한 제어 이동의 배열을 제어 흐름 혹은 프로세서의 제어 흐름이라고 부른다.

대부분의 상황에서는 우리가 생각하는 점진적인 순서로 프로그램을 실행하기 위한 제어 흐름만이 필요하겠지만, 시스템들은 내부 프로그램 변수에 의해 표시되지 않으며 프로그램 실행과는 상관 없이 시스템의 상태 변화를 추척하고 반응할 수 있는 방법이 필요하다. 

예를 들어 하드웨어 타이머는 규칙적인 간격으로 꺼지며, 시스템은 이를 반드시 처리해야만 한다. 또한 프로그램은 디스크로부터 데이터를 요청하며, 그 후에 데이터가 준비되었다는 통지를 받을 때까지 다른 일을 수행한다. 이를 DMA라고한다. 자식 프로세스를 생성하는 부모 프로세스는 자신의 자식이 종료되었을 때 SIG_CHLD 시그널을 받는다는 것도 기존 제어 흐름만으로는 설명이 불가능하다.

현대의 시스템들은 제어흐름의 갑작스런 변화를 반드는 방법으로 이러한 상황에 반응한다. 일반적으로 이와 같은 급격한 변화를 예외적인 제어흐름(Exceptional control flow, ECF)라고 한다. 예외적인 제어 흐름은 컴퓨터 시스템의 모든 수준에서 발생한다. 예를 들어 하드웨어 수준에서 하드웨어에 의해 검출되는 이벤트들은 예외 핸들러로 갑작스런 제어의 이동을 발생시킨다. 

운영체제 커널 수준의 문맥전환을 통해 사용자 프로세스에서 다른 프로세스로의 제어가 이동하는 것도 이러한 ECF의 예시이다. 또한 응용 수준에서 프로세스는 시그널을 수신하고 있는 시그널 핸들러로 제어를 급격히 이동하는 다른 프로세스로 시그널을 보낼 수 있다. 개별 프로그램은 일반적인 스택 운영을 회피하고 다른 함수 내의 임의의 위치로 비지역성 점프를 하는 방법으로 에러에 대응할 수 있다.

예외 상황

예외 상황은 어떤 프로세스 상에 상태의 변화에 대한 대응으로 발생하는 제어흐름의 갑작스런 변화이다. 

그림 1 예외 상황의 분석 (프로세서 상태의 변화는 응용 프로그램에서 예외처리 핸들러로 급격한 제어 이동을 발생시킨다. 처리가 끝나면 제어를 다시 중단되었던 프로그램으로 돌려주거나 실행을 중단한다.)

위의 그림은 예외 상황에 대한 기본 아이디어를 보여준다. 그림에서 프로세서는 현재 어던 명령어 I를 실행하고 있으며, 이때 프로세서 상태의 중요한 변화가 일어나고 있다. 이 상태는 프로세서 내 다양한 비트들과 신호들로 인코드된다.

이러한 상태 변화를 이벤트라고 하며, 이 이벤트는 현재 인스트럭션의 실행에 직접적으로 관련되는 경우가 있다. 예를 들어, 가상 메모리의 페이지 오류, 산술 오버플로우, divide by zero를 시도하는 경우가 이러한 이벤트의 예시이다. 

또 반대로 이벤트가 현재 인스트럭션의 실행과 관련이 없을 수 있다. 예를 들어 시스템 타이머가 정지하거나 I/O 요청이 완료되는 경우가 그렇다.

어느 경우이던지 프로세서가 이벤트의 발생을 감지하면 예외 테이블이라고 부리는 점프 테이블을 통해 이 특정 종류의 이벤트를 처리하기 위해 특별히 설계된 운영체제의 서브 루틴으로 간접 프로시져 콜을 하게 된다.

the indirect call uses an instruction call with a register as argument (here rax). The register is previously loaded either directly with the fixed address of the subroutine that is to be called, or with a value fetched from somewhere else, such as another register or a place in memory where the subroutine’s address was previously stored.

간접 호출은 레지스터를 인수(여기서 rax)로 사용하는 명령 호출을 사용합니다. 레지스터는 호출될 서브루틴의 고정 주소로 직접 로드되거나 다른 레지스터 또는 서브루틴의 주소가 이전에 저장된 메모리의 위치와 같은 다른 위치에서 가져온 값으로 로드됩니다.

 

예외 처리 핸들러가 처리를 끝마치면 예외 상황을 발생시킨 이벤트의 종류에 따라 다음과 같은 세 가지 중에 한 가지 일이 발생한다.

  1. 핸들러는 제어를 중단된 기존 인스트럭션으로 다시 돌려준다.
  2. 핸들러는 제어를 예외 상황이 발생하지 않았다면 다음에 실행되었을 명령어로 돌려준다.
  3. 핸들러는 중단된 프로그램을 종료한다.

예외 처리

한 시스템 내에서는 가능한 예외 상황의 종류마다 중복되지 않는 양의 정수를 예외번호로 할당하고 있다. 이 숫자들의 일부는 프로세서 설계자가 부여한 것으로, 나머지 번호는 운영체제 커널의 설계자가 할당한다. 전자의 예로는 divide by zero, 페이지 오류, 메모리 접근 위반, breakpoint, 산술연산 오버플로우가 포함된다. 후자의 예에는 시스템 콜, 외부 I/O 디바이스로부터의 시그널이 포함된다.

그림 2. 예외 테이블의 예시 (Interrupt Vector Table)

 시스템 부팅 시 운영체제는 예외 테이블이라고 하는 점프 테이블을 할당하고 초기화하여 엔트리 k가 예외 상황 k에 대한 핸들러의 주소를 갖는다. 위의 그림에서 각각의 예외 상황에 대해 번호가 붙어있는 것을 확인할 수 있다. 시스템이 부팅되는 경우 운영체제는 예외 테이블이라고 하는 점프 테이블을 할당하고 초기화하여 각각의 엔트리 k가 예외상황 k에 대한 핸들러의 주소를 갖는다. 예를들어 divide error 가 발생할 경우, 예외 테이블의 0 번으로 접근하여 해당 엔트리에 연결된 divide error를 처리하기 위한 핸들러의 주소를 가지고 적절한 대응이 가능할 것이다.

즉 런타임 시에 프로세서는 이벤트의 발생을 감지하고 대응되는 예외 번호 k를 결정한다. 프로세서는 그 후에 예외 테이블의 엔트리 k를 통해 간접 프로시져 콜을 하는 방법으로 예외 상황을 발생시킨다.

예외의 종류

앞서 계속 설명한 예외 상황은 총 4 가지의 종류로 구분이 가능하다.

그림 3. 예외의 종류. 비동기 예외는 프로세서 외부에 있는 입출력 디바이스 내 이벤트의 결과로 발생한다. 동기형 예외는 인스트럭션을 실행한 직접적인 결과로 발생한다.

  1. 인터럽트 (Interrupt)
  2. 트랩 (trap)
  3. 오류 (fault)
  4. 중단 (abort)


인터럽트

인터럽트는 프로세서 외부에 있는 입출력 디바이스로부터의 시그널의 결과로 비동기적(Asynchronous)으로 발생한다. 하드웨어 인터럽트는 비동기적이며, 이러한 예시로 DMA를 생각할 수 있다. 이러한 하드웨어 인터럽트를 위한 예외 핸들러는 인터럽트 핸들러라고 불리며, 위 그림 2와 같은 예외 테이블 중 인터럽트 테이블을 거쳐 동작한다.

그림 3 인터럽트의 처리

위의 그림은 인터럽트의 처리 과정을 요약한 것이다. 네트워크 어뎁터, 디스크 컨트롤러, 타이머 칩과 같은 입출력 디바이스들은 프로세서 칩의 핀에 시그널을 보내서 인터럽트를 발생시키고, 인터럽트에 대한 예외 번호를 시스템 버스에 보낸다

현재의 인스트럭션이 실행을 완료한 후에 프로세서는 인터럽트 핀이 high로 올라갔다는 것을 인지하고, 시스템 버스에서 예외 번호를 읽으며 적당한 인터럽트 핸들러를 호출한다. 이후 핸들러가 리턴할 때, 제어를 다음 인스트럭션으로 돌려주게 되며, 그 이후 프로그램이 마치 인터럽트가 발생하지 않았던 것처럼 계속해서 실행된다.

나머지 예외의 종류들은 (트랩, Fault, Abort) 지금의 인스트럭션을 실행한 결과로 동기적으로 일어난다. 이를 묶어서 오류 인스트럭션(faulting instruction)이라고 부른다.

트랩과 시스템 콜

트랩은 의도적인 예외 상황으로 어떤 인스트럭션을 실행한 결과로 발생한다. 인터럽트 핸들러와 마찬가지로 트랩 핸들러는 제어를 다음 인스트럭션으로 리턴한다. 트랩의 가장 중요한 사용은 시스템 콜이라고 알려진 사용자 프로그램과 커널 사이의 프로시져와 유사한 인터페이스를 제공하는 것이다.

사용자 프로그램은 파일을 읽거나 (read), 새로운 프로세스를 만들거나 (fork), 새 프로그램을 로드하거나 (execve), 현재 프로세스를 종료하는 등의 서비스를 종종 커널로부터 요청할 필요가 있다. 이러한 커널 서비스에 제한된 접근을 하기 위해 프로세서는 system call이라는 인스트럭션을 제공하며, 사용자가 system call 인스트럭션을 실행하면 트랩이 인자들을 해독하고 적절한 커널 루틴을 호출하는 예외 핸들러로 가게 한다. 

그림 4. 트랩의 처리

위의 그림은 시스템 콜에 대한 처리작업을 요약한 것이다. 프로그래머 관점에서 시스템 콜은 보통의 함수 호출과 동일하다. 그러나 실제 구현 상에서 시스템 콜은 커널 모드로의 진입이 포함된다는 점이 기존 함수와 차이가 있다.

기존 함수를 생각해보자. 시스템 콜이 배제된 채, 유저가 만든 함수는 철저하게 유저 모드에서 돌아간다. 이해가 잘 되지 않는다면 시스템 콜이 호출되는 경우를 살펴보자. 시스템 콜의 호출은 결국 하드웨어 혹은 OS 레벨에서 특정 작업을 수행해달라는 유저의 요청이다. 이러한 요청을 받게 되면 OS는 명시적으로 이를 처리하는 대신, 시스템 콜 호출 시 유저에게서 잠시 제어권을 가져온 채, 커널 모드에서 작업을 수행한 후 다시 유저에게 제어권을 넘긴다.

이때 제어권을 가져오고 넘기는 것을 가능하게 하는 예외 처리가 바로 트랩이다.

그림 5. 예외 상황 시의 제어권

Fault

Fault는 핸들러가 정정할 수 없을 지도 모르는 에러 상태를 말한다. Fault가 발생하면 프로세서는 제어를 Fault 핸들러로 이동해준다. 만일 핸들러가 에러 조건을 정정할 수 있다면, 오류를 발생시킨 인스트럭션으로 제어를 다시 돌려주며 거기서부터 재실행한다. 그렇지 않다면 핸들러는 커널 내부의 abort 루틴으로 리턴해서 오류를 발생시킨 응용 프로그램을 종료한다. 

그림 6. Fault의 처리

Fault의 고전적인 예시는 페이지 폴트로, 인스트럭션이 가상 메모리 테이블을 참조했을 때 대응되어지는 실제 메모리 Page가 존재하지 않는 상황에서 디스크로부터 데이터를 가져와야 하는 상황을 의미한다. 페이지 폴트 핸들러는 디스크에서 적절한 페이지를 로드해서 제어를 오류가 발생한 인스트럭션으로 다시 넘겨준다. 이 인스트럭션이 다시 실행될 때 적절한 물리페이지는 메모리에 있게 되므로, 인스트럭션은 오류를 발생시키지 않고 완료할 때까지 실행이 가능해진다.

Abort

Aborts는 대개 DRAM이나 SRAM이 고장날 떄 발생하는 패리티 에러와 하드웨어와 같은 복구할 수 없는 치명적인 에러에서 발생한다. Abort 핸들러는 절대로 응용프로그램으로 제어를 리턴하지 않는다. 핸들러는 제어를 응용프로그램을 종료하는 중단 루틴으로 넘겨준다.

그림 7. Abort의 처리

앞서 설명한 fault와 abort를 총칭하는 개념으로 Exception을 사용하기도 한다