보호

CPU는 일부 커널과 함께 모든 유저레벨의 프로그램이 잘 동작하고, 원하는 결과를 얻을 수 있도록 해준다.
Protected Mode로 넘어온 후에는 위에서 이야기한 바대로 여러 유저 프로그램이 같이 동작하기 때문에 CPU는 각 명령어마다 여러 가지 체크를 하고 합당한 명령만 실행시키고, 합당하지 않으면 폴트를 발생시켜 커널이나 유저 프로그램 자신이 이 에러를 다시 조정하도록 한다.
이러한 체크에는 4가지가 존재한다.
    1. Limit 체크
    2. Type 체크
    3. 특권 레벨 체크
    4. 명령 세트 체크


1. CPU의 체크 포인트

1.1 Limit 체크

세그먼트 디스크립터 내 G비트가 0일 때에는 Limit를 0xFFFFF(1MB)까지 선택할 수 있으며, 세그먼트 영역의 크기를 0으로 지정해도 되고, 0xFFFFF로 지정해도 된다는 의미이다.
세그먼트 디스크립터 내 G비트가 1일 때에는 Limit를 0xFFFFFFFF(4GB)까지 선택할 수 있으며, 세그먼트 영역의 크기를 0xFFF로 지정해도 되고, 0xFFFFFFFF로 지정해도 된다는 의미이다.
세그먼트 영역에서 유효 Limit는 전체 세그먼트 영역의 크기에서 1바이트를 뺀것과 같다. 아래의 경우 CPU는 일반 보호 예외(#GP)를 발생시킨다.
    1. 오프셋이 유효 Limit보다 큰 바이트
    2. 오프셋이 유효 Limit-1보다 큰 워드
    3. 오프셋이 유효 Limit-3보다 큰 더블워드
    4. 오프셋이 유효 Limit-7보다 큰 쿼드워드

1.2 Type 체크

세그먼트 디스크립터에는 S비트와 Type 필드가 있다. S비트가 0이면 Type 필드가 시스템 Type이라는 것이고, 1이면 코드나 데이터 세그먼트의 Type이라는 뜻이다.


예를 들어 P비트가 1, DPL이 00, S가 0이고, 32비트 인터럽트 게이트일때 디스크립터의 한 부분에 0x8E가 들어가고, 32비트 TSS였다면 0x89, 32비트 TSS의 B비트가 1로 세트되었을 때 0x8B가 되는 것을 기억하면 된다.

CPU는 세그먼트 셀렉터와 세그먼트 디스크립터를 조작할 때 여러가지 시점에서 Type 정보를 조사한다. 예를 들어 아래와 같은 경우가 Type 체크되는 대표적인 예이다.

1. 세그먼트 셀렉터가 세그먼트 레지스터에 로드될 때 CS에는 코드 세그먼트의 셀렉터만이 로드될 수 있다.
    MOV AX, 0x08
    MOV CS, AX

2. 디스크립터가 세그먼트 레지스터에 이미 로드되어 있는 세그먼트에 명령이 액세스 할 때
    -> 코드 세그먼트 영역에는 데이터의 쓰기가 금지된다.
    -> 읽기 전용 데이터 세그먼트에 데이터의 쓰기가 금지된다.
    -> 코드 세그먼트 영역에 읽기 가능한 플래그가 설정되지 않은 상태에서는 이 영역을 읽어 들일 수 없다.

3. CALL JMP 명령의 오퍼랜드에 셀렉터가 있을 때 그 셀렉터에 대한 디스크립터의 Type 필드를 조사한다.

TSS 셀렉터를 오퍼랜드로 한 CALL JMP 명령에서는 그 셀렉터에 대한 디스크립터가 TSS용인지를 자동 체크한다. 또는 IRET 명령이 내려졌을 때 현재 실행중인 태스크의 NT비트가 1로 세트되어 있으면 이에 대한 TSS영역에서 "이전 태스크로의 백링크"가 TSS용인지를 체크한다.

GDT의 맨 처음 디스크립터인 NULL 디스크립터를 CS나 SS에 로드하려고 할 때 일반보호 예외 발생

DS, ES, FS, GS에 로드는 가능하나 로드된 셀렉터를 사용하여 세그먼트 영역에 접근하려고 해도 일반 보호 예외(#GP)가 발생


2. 특권레벨

CPU의 특권레벨은 0~3으로 4개가 있으며, 0과 3의 2개 레벨만을 사용한다. 낮은 레벨의 태스크가 높은 레벨의 데이터를 참조하려고 하면 일반 보호 에러(#GP)가 발생한다.

특권레벨을 이해하기 위해서는 CPL, DPL, RPL 세 개의 레벨을 알아야 한다.

CPL(Current Privilege Level)
CPL은 현재 실행되고 있는 태스크의 특권 레벨로, CS, SS 셀렉터 레지스터의 0, 1번째 비트에 있는 수이다. 프로그램이 다른 특권 레벨의 코드 세그먼트로 제어 이행되면 CPU는 CPL을 변경한다.

DPL(Description Privilege Level)
DPL은 디스크립터에 기재된 DPL 값이고 2비트로 표현된다. 디스크립터를 통한 세그먼트 영역으로의 모든 접근에서 항상 CPL과 DPL의 관계가 체크된다. 예를 들어 현재 낮은 레벨의 CPL을 가지고 있는데, 높은 레벨의 데이터 세그먼트 영역에 접근을 시도한다면 CS의 0, 1비트(CPL)와 세그먼트 디스크립터에 있는 DPL 값을 비교할 것이다. 이러한 상황에서는 체크 후 물론 불가능하다는 판단이 내려진다.

RPL(Requested Privilege Level)
특권레벨 3인 프로세스가 콜게이트(낮은 특권 레벨에서 높은 특권 레벨의 루틴을 사용할 수 있게 하는 관문)를 통해 특권레벨 0에 있는 루틴을 실행 할 때가 있다. 이때 일시적으로 특권 레벨 0으로 들어가기 때문에 특권레벨 0의 데이터 영역에 접근 가능하다. 이러한 현상을 교모히 이용하는 프로그램도 존재할 가능성이 있다. RPL 값은 이러한 상황을 막기 위해 OS 커널이 사용하는 값이다. 예를 들어 콜게이트에 연결되어 있는 루틴에 이 루틴이 어디에서 불려졌는지를 기록하는 방법이 있다. 특권레벨 3인 프로세스가 콜게이트를 통해 이 루틴을 불렀다면 데이터 세그먼트 셀렉터에 특권레벨 3에서 불려졌다는 것을 표시한다. RPL은 누가 요구 했는가를 나타내는 값이다.


콜게이트

낮은 특권 레벨의 프로그램이 실행 도중 높은 특권 레벨로 변경되는 수단은 대표적으로 인터럽트, 예외, 콜게이트가 있다. 이 중 하드웨어 인터럽트와 예외에서는 낮은 특권 레벨 프로그램이 자신의 의지와는 상관없이 특권 레벨의 변경이 이루어지고, 소프트웨어 인터럽트와 콜게이트는 낮은 특권 레벨의 프로그램이 자신의 의지에 의해 높은 특권 레벨의 루틴을 잠깐 사용하는 것이다.
콜게이트도 다른 세그먼트 디스크립터와 함께 GDT 테이블에 디스크립터로서 포함되며, 또 하나의 세그먼트 정의라고 생각하면 된다. 낮은 특권 레벨의 프로그램이 높은 특권 레벨의 프로그램의 일부분을 사용하기 위한 창구 세그먼트이다. 디스크립터의 생김새는 아래와 같으며 "인수의 개수" 칸과 Type 이외의 부분은 다른 디스크립터와 비슷하다.

위 디스크립터를 GDT에 지정해 놓고, 유저 프로그램은 코드 셀렉터의 사용법과 마찬가지로 이 디스크립터를 셀렉터로 선택하여 JMP나 CALL 명령을 내린다. 오프셋은 디스크립터에 지정되어 있으므로 아무 숫자나 사용하여도 디스크립터에 지정된 오프셋의 루틴이 수행된다.


코드와 데이터의 특권레벨 관계

특권 레벨 간 JMP 명령은 불가(굳이 사용해야 한다면 콜게이트(Call Gate)를 사용하여 이루어 져야 한다.
CALL 명령은 특권 레벨 간의 이동 가능, 항상 낮은 특권 레벨에서 높은 특권 레벨에 대하여 이루어져야 하고, RET 명령은 높은 특권 레벨에서 낮은 특권 레벨에 대하여 이루어져야 한다. 특권 레벨 0의 코드가 특권 레벨 3의 코드를 불러낼 이유가 없기 때문이다.

특권레벨 0에서 동작하는 동안 CS(CPL) 셀렉터의 0, 1비트에는 00이 들어가 있다. 특권 레벨 3에서 동작하는 동안의 CS 셀렉터에는 0, 1비트에 11(10진수 3)이 들어가 있게 된다.

특권레벨 3에서 CALL 명령으로 특권레벨 0의 루틴을 불러내면 CS 셀렉터의 0, 1비트에 00, CPL은 0이며
특권레벨 0에서 RET 명령으로 특권레벨 3의 루틴으로 돌아갈 시 CS 셀렉터의 0, 1비트에 11(3), CPL은 3이다. (같음)
한가지 예외가 있다면 Conforming 세그먼트 일 경우, 특권레벨 0의 루틴을 불러내도 CPL의 값이 그대로 3인 채로 실행 되었다가 RET 명령으로 돌아온다. Conforming 세그먼트는 콜게이트와 관련있다.


유저 테스크가 콜 게이트 등을 사용하여 커널모드의 루틴을 불러내는 경우

1. CPU는 이 태스크의 TSS 영역에 있는 SS0, ESP0의 값을 참조하여 커널모드의 스택에 현재 유저 태스크가 사용하고 있는 SS, ESP, 그리고 유저 태스크가 현재 진행 중인 루틴의 주소인 CS, EIP를 차례로 PUSH한다. 그리고 CPU의 SS, ESP 레지스터에는 SS0, ESP0의 값을 각각 넣는다.
2. 콜게이트에 지정된 커널 모드의 루틴 주소로 점프 후 실행한다.
3. RET 명령이 내려질 시 현재 커널 모드의 스택에 저장된 SS, ESP, CS, EIP를 POP하여 CPU의 각각 레지스터에 복원하여 유저모드 태스크로 돌아간다. 이때 먼저 CS를 확인한 후 현재 커널 모드보다 낮은 특권 레벨이면 SS와 ESP를 POP한다.
호출 구문에 인수가 2개 있을 시 인수의 개수에 2를 기입해야 한다. GDT에 있는 콜게이트의 디스크립터의 "인수의 개수" 칸에 2가 들어있다면 유저 레벨의 스택에서 2개를 커널 모드의 스택에 복사한다.


인터럽트 예외가 발생하였을 때의 스택

EFLAGS가 추가된 것만 빼면 콜게이트의 경우와 비슷하다.
먼저 커널모드인 특권레벨 0에서 인터럽트 발생 시 스택 변경이 일어나지 않는다. 특권 레벨 0에서는 굳이 시스템을 보호할 필요가 없다. 그러므로 특권 레벨 0에서 사용하고 있던 SS, ESP 그대로의 값을 사용하여 스택에 EFLAGS, CS, EIP를 PUSH 한다.

유저레벨 태스크의 동작 중에 인터럽트나 예외가 발생하면 유저레벨 태스크의 TSS 영역에서 SS0, ESP0을 CPU의 SS, ESP 레지스터에 복사한다. 이로써 커널 모드의 스택을 사용하게 되는데, 커널모드 스택에 콜게이트의 경우와 같이 유저모드의 태스크가 사용하던 SS, ESP, EFLAGS, CS, EIP 등의 레지스터 값을 PUSH 하여 저장한다. 그 후 커널모드에 있는 인터럽트 핸들러 루틴으로 점프하고, 인터럽트 처리가 끝나면 IRET을 사용하여 인터럽트가 걸린 유저모드로 돌아오는데, 이때 스택에 있는 값들을 POP하여 CPU의 각각의 레지스터에 다시 복원한다.

TSS 영역에 SS0, ESP0, SS1, ESP1 등 레벨 별로 존재(SS3, ESP3은 존재하지 않음)하며, 커널모드 스택, 유저모드 스택 조정이 가능하다.(5장 Task Switcing 참조)


+ Recent posts