64비트 멀티코어 OS 만들기 - 운영 모드, 레지스터, 메모리 관리

170802 2주차


운영 모드

이번 포스트에서는 64비트 프로세서에 대해 알아보도록 하겠다.

인텔 64비트 호환 프로세서(x86_64 프로세서)에서는 크게 다섯 가지 운영 모드가 있다.

운영 모드 설명
리얼 모드 프로세서의 초기 상태(16비트로 동작하며 8086 프로세서와 호환되는 모드)
보호 모드 32비트로 동작하며 세그먼트, 페이징, 보호, 멀티태스킹 등의 기능 제공
IA-32e 모드 32비트 호환 모드와 64비트 모드의 두 가지 서브 모드로 구성
시스템 관리 모드 전원 관리나 하드웨어 제어 같은 특수 기능을 제공하는 모드
가상 8086 모드 보호 모두 내부에서 가상의 환경을 설정하여 리얼 모드처럼 동작하는 모드

참고로, 64비트를 지원하는 IA-32e 모드는 AMD에서 Long Mode로 표기한다.

64비트 OS가 반드시 지원해야 하는 세 가지 모드는 리얼 모드, 보호 모드, IA-32e 모드(64비트 서브 모드)이다. 시스템 관리 모드나 가상 8086 모드는 특수한 상황에서 사용되므로 OS에서 해당 기능을 사용하지 않는다면 구현하지 않아도 된다.


프로세서가 어떤 상태 또는 모드에 있든 전원이 켜지거나 리셋되면 프로세서는 리얼 모드로 진입한다. 리얼 모드는 BIOS(Basic Input Ouput System)의 여러 기능을 사용할 수 있으며, BIOS는 디스크 읽기 및 쓰기 기능, 그래픽 모드 전환 기능 등 제공한다. 리얼 모드의 역할은 OS 이미지를 디스크에서 메모리로 복사하여 보호 모드로 변경하는 것이지만 대부분 어셈블리어로 처리해야 한다.


보호 모드는 IA-32e 모드로 전환하는 과정에서 반드시 거쳐야 하는 모드이다. 32비트 윈도우나 리눅스 OS가 동작하는 기본 모드이며, 최대 4G(2^32) 주소 공간을 제공한다. OS의 필수 기능으로 여겨지는 보호, 멀티태스킹, 세그먼테이션, 페이징 등의 기능을 하드웨어적으로 지원한다. 보호 모드에서 사용하는 레지스터는 대부분 IA-32e 모드에서도 같이 사용한다.


IA-32e 모드에서는 서브 모드로 32비트 호환 모드와 64비트 모드가 있다. 32비트 호환 모드는 보호 모드와 같은 기능을 수행하므로, 64비트 모드를 위주로 살펴보겠다. IA-32e 모드는 최대 16E(2^64)의 주소 공간을 제공하며 레지스터 수도 보호 모드보다 많다. 32비트 호환 모드일 때, 보호 모드처럼 동작하고 32비트 코드를 그대로 실행할 수 있다. 그리고 64비트 OS에서도 별다른 처리 없이 서브 모드만 변경해서 동일하게 실행할 수 있다.


운영 모드와 레지스터

운영 모드는 크게 16비트 모드, 32비트 모드, 64비트 모드 세 가지로 나눌 수 있으며 앞에 붙은 숫자는 커질수록 레지스터의 개수도 많아진다. x86_64 프로세서에는 많은 [레지스터][http://ljhblog.tistory.com/60]가 있지만, OS 개발에 큰 비중을 차지하는 녀석들은 범용 레지스터, 세그먼트 레지스터, 컨트롤 레지스터 세 가지다.

레지스터는 프로세서 내부에 있는 작은 공간이며 연산, 제어, 상태 표시, 디버깅 등의 목적으로 사용한다.


범용 레지스터(General Purpose Register)는 계산, 메모리 주소 지정, 임시 저장 공간 등의 목적으로 사용된다. 만약에, 범용 레지스터의 수가 늘어나면 수행 속도가 개선되고 레지스터에 모두 올려서 계산할 수 있어 메모리 접근 시간을 줄일 수 있다. 또한 함수를 호출할 때, 호출하는 쪽에서 호출되는 함수가 필요한 파라미터 정보들을 레지스터에 넣어서 넘겨줌으로써 스택 영역 메모리에 접근 시간과 스택을 정리하는 시간을 줄일 수 있다.

범용 레지스터 이름 용도
AX 산술 연산을 수행할 때 누산기로 사용
BX 데이터의 주소를 지정할 때 데이터 포인터로 사용
CX 루프 또는 문자열의 카운터로 사용
DX I/O 주소를 지정할 때 사용되며, 산술 연살을 수행할 때 보조 레지스터로 사용
SI 문자열에 관련된 작업을 수행할 때 원본 문자열의 인덱스로 사용
DI 문자열에 관련된 작업을 수행할 때 목적지 문자열의 인덱스로 사용
SP 스택의 포인터로 사용
BP 스택의 데이터에 접근할 때 데이터의 포인터로 사용
R8 ~ R15 x86_64 프로세서에 추가된 범용 레지스터로, 다양한 용도로 사용 가능

실제로 x86_64 프로세서는 64비트 크기의 범용 레지스터만 있으며 보호 모드나 리얼 모드일 때 레지스터의 일부 영역만 사용하도록 처리한다. 대체로 64비트 레지스터에는 RAX, RBX 처럼 R 접두사가 붙고, 32비트 레지스터에는 EAX, EBX, R8D, R9D 처럼 E 접두사나 D 접미사가 붙는다. 16비트 레지스터에는 AX, R8W 처럼 접두사가 붙지 않거나 W 접미사가 붙고, 16비트 이하 레지스터는 상위 8비트와 하위 8비트로 다시 구분되며 상위 8비트는 AH, BH 처럼 끝자리가 H 로 변경되고, 하위 8비트는 AL, BL 처럼 끝자리가 L 로 변경되거나 접미사 L 이 붙는다.

리얼 모드에서는 16비트 크기 이하의 레지스터만 접근할 수 있지만 오퍼랜드 크기 접두사(Operand-size Prefix, 0x66)를 사용하거나 어드레스 크기 접두사(Address-size Prefix 0x67)를 사용하면 32비트 레지스터도 접근할 수 있다. 보호 모드에서 16비트 레지스터에 접근할 때도 동일하게 사용하여 접근할 수 있고, IA-32e 모드의 경우 기존의 두 접두사와 함께 새로 추가된 REX 접두사를 사용하여 다양한 크기의 레지스터를 사용할 수 있다.

RIP 레지스터는 현재 수행 중인 명령의 주소를 가리키는 레지스터다. 크기는 64비트고, RIP 레지스터의 값과 32비트 오퍼랜드를 통해 64비트 주소 공간을 나타낼 수 있다. 이것이 RIP 상대 주소 방식의 주소 지정 방법이다. 하지만 기본 오퍼랜드의 크기가 32비트이므로 RIP 레지스터의 값에 상위 2G, 하위 2G 범위까지만 표현할 수 밖에 없다. 그래서 접두사 중에 REX 접두사를 사용한다. REX 접두사를 사용하면 오퍼랜드의 크기가 64비트가 되고, 따라서 64비트 주소를 모두 표현할 수 있게 된다.


세그먼트 레지스터(Segment Register)는 16비트 레지스터로 주소 영역을 다양한 크기로 구분하는 역할을 한다.

세그먼트 레지스터 이름 설명
CS - 코드 영역을 가리키는 레지스터
- 데이터 이동 명령으로 값을 변경할 수 없으며, 점프 명령이나 인터립트 관련 명령으로 변경 가능
DS
ES
FS
- 데이터 영역을 가리키는 레지스터
- 데이터 이동 명령으로 값을 변경할 수 있음
- DS 레지스터는 데이터 영역에 접근할 때 암시적으로 사용됨
- ES 레지스터는 문자열과 관련된 작업을 처리할 때 암시적으로 사용됨
- 데이터 영역에 접근하면서 DS 레지스터 이외의 세그먼트 레지스터를 사용하려면 세그먼트 레지스터 접두사 사용
SS - 스택 영역을 가리키는 레지스터
- 데이터 이동 명령으로 값을 변경할 수 있음
- 스택 관련 레지스터(SP, BP)를 통해 스택에 접근할 때 암시적으로 사용됨


컨트롤 레지스터(Control Register)는 운영 모드를 변경하고, 현재 운영 중인 모드의 특정 기능을 제어하는 레지스터다.

컨트롤 레지스터 이름 설명
CR0 - 운영 모드를 제어하는 레지스터
- 리얼 모드에서 보호 모드로 전환하는 역할과 캐시, 페이징 기능 등을 활성화 시킴
CR1 - 프로세서에 의해 예약된 레지스터
CR2 - 페이지 폴트 발생 시 페이지 폴트가 발생한 선형 주소가 저장되는 레지스터
- 페이징 기법을 활성화한 후에는 페이지 폴트 발생 시만 유효한 값을 가짐
CR3 - 페이지 디렉토리의 물리 주소와 페이지 캐시에 관련된 기능을 설정하는 레지스터
CR4 - 프로세서에서 지원하는 각종 확장 기능을 제어하는 레지스터
페이지 크기 확장이나 메모리 영역 확장 등의 기능을 활성화시킴
CR8 - 태스크 우선 순위 레지스터의 값을 제어하는 레지스터
- 프로세스 외부에서 발생하는 인터립트를 걸러주는 필터의 역할
- IA-32e 모드에서만 접근 가능

컨트롤 레지스터는 리얼 모드와 보호 모드일 때 32비트 크기이며, IA-32e 모드에서는 64비트로 확장되지만 일부 제약이 있다. CR0와 CR4, CR8 레지스터에서는 64비트 중 상위 32비트를 0으로 설정해야 한다. CR2 레지스터의 경우, 64비트 영역을 모두 사용할 수 있고 CR3 레지스터부터는 비트 40부터 비트 51까지 모두 0으로 설정해야 한다.

운영 모드에 따라 필수 필드와 옵션 필드가 달라지는데, PC를 가장 쉽게 재부팅하는 방법이라면 CR0 레지스터에 아무 값을 설정해 보거나 비트 0의 값을 변경하면 될 것 같다. 비트 0은 리얼 모드와 보호 모드를 전환하는 플래그(Flag)이며, 값을 변경하기 전후에 별도로 처리해야 한다.


운영 모드와 메모리 관리 기법

x86와 x86_64 프로세서에서 지원하는 메모리 관리 기법은 세그먼테이션(Segmentation)과 페이징(Paging)이다. 세그먼테이션과 페이징은 주소 공간을 특정 영역으로 나눈다는 공통점이 있지만, 나누는 방식에 차이가 있다. 세그먼테이션은 전체 영역을 원하는 크기로 나누어 관리하는 방식이고, 페이징은 일정한 단위로 잘라진 조각을 모아 원하는 크기로 관리하는 방식이다.

세그먼테이션은 세그먼트 레지스터에 세그먼트의 시작 주소 혹은 디스크립터(Descriptor)라고 불리는 자료구조의 위치를 설정해야 하고, 페이징은 컨트롤 레지스터 중 CR3 레지스터에 페이지 디렉토리라고 불리는 자료구조의 물리 주소를 설정해야 사용할 수 있다. 하지만, 모든 운영모드가 세그먼트와 페이징을 지원하는 것은 아니다. 왜냐하면 x86_64 프로세서가 기존 프로세서와 호환성을 유지하면서 발전했기 때문이다.


리얼 모드는 최대 1MB까지 주소 공간을 사용하며 세그먼테이션만 지원한다. 세그먼트 크기는 64K로 고정이고, 세그먼트의 시작 주소는 코드나 메모리에 접근할 때 기준 주소(Base Address)로 사용된다. 세그먼테이션을 거쳐 나온 주소가 바로 물리 주소다. 리얼 모드의 세그먼테이션은 세그먼트 레지스터의 값에 범용 레지스터의 값에 범용 레지스터의 값을 더하는 방식으로 동작한다. 그대로 더하는 것이 아닌, 세그먼트 레지스터의 값에 16 곱한 값을 세그먼트의 기준 주소로 사용하는 것이다. 16비트의 최댓값은 65535(2^16-1)이며, 여기에 16(2^4)를 곱하면 세그먼트의 기준 주소는 거의 1MB에 근접하는 1048560(2^20-16)가 된다.

세그먼트의 크기가 64KB인 이유는 범용 레지스터의 크기 때문이다. 16비트 프로세서에는 32비트 레지스터가 없으며 범용 레지스터는 모두 16비트 크기이다. 따라서 16비트로 접근할 수 있는 범위가 0~65535(0xffff)이므로 세그먼트 크기도 64KB가 된 것이다.


보호 모드는 세그먼테이션과 페이징을 모두 지원한다. 세그먼트 레지스터에 세그먼트의 기준 주소를 직접 설정하는 대신 디스크립터 자료구조의 위치(Offset)를 설정해서 세그먼트 레지스터의 명칭도 세그먼트 셀렉터로 변경된다.

디스크럽터 는 메모리 영역의 정보를 저장하는 자료구조로 여러 종류가 있는데, 그 중에 세그먼트에 대한 정보를 나타내는 세그먼트 디스크립터라고 한다. 세그먼트 디스크립터에는 세그먼트의 시작 주소와 크기, 권한, 타입 등의 정보가 있다. 그 중에 특권 레벨(DPL, Descriptor Privilege Level)은 해당 세그먼트에 접근하기 위한 최소한의 권한을 나타낸다. 특권 레벨은 0~3 사이의 값을 가지며, 숫자가 작을 수록 권한이 높다. 디스크립터에 설정된 권한과 같거나 높아야 세그먼트에 접근할 수 있고, 조건을 만족하지 않으면 프로세서는 예외를 발생시켜 문제가 발생했음을 알린다.

세그먼트 디스크립터는 메모리에 위치하는 자료구조의 일종으로 GDT(Global Descriptor Table)라는 곳에 모여있다. GDT 역시 메모리의 위치하는 자료구조이므로 프로세서에 GDT의 위치를 알려야 하며, GDT의 위치와 관련 있는 레지스터는 GDTR(Global Descriptor Table Register)라고 불린다. 이 레지스터는 16비트 GDT 크기 필드와 32비트 기준 주소 필드로 구성된 자료구조의 물리 주소를 넘겨받는다. 프로세서는 이 값을 내부에 저장했다가 세그먼트 셀럭터를 통해 주소에 접근할 때마다 GDT의 위치를 찾는데 참조한다.

리얼 모드와 달리 세그먼테이션을 거쳐 나온 선형 주소는 물리 주소와 일치하지 않을 수 있다. 페이징은 물리 메모리를 페이지(Page)라고 불리는 일정한 크기로 나누고, 선형 주소와 물리 주소를 나눠 놓은 페이지로 연결하는 방식을 말한다. 같은 물리 페이지를 여러 선형 주소에 연결함으로써 응용 프로그램끼리 공유하는 메모리를 손쉽게 처리할 수 있다. 반대로 응용 프로그램마다 독립적인 주소 공간을 보장하고 싶다면 페이징 자료구조를 따로 생성하고 물리 메모리에 중복되지 않게 연결하면 된다.

보호 모드에서 페이징 방식은 두 가지다. 하나는 물리 메모리를 4KB 크기로 나누고 선형 주소를 3단계로 구분하는 방식이고, 다른 하나는 물리 메모리를 4MB 크기로 나누고 선형 주소를 2단계로 구분하는 방식이다. 특히, 3단계 페이징은 선형 주소를 디렉토리, 테이블, 오프셋 세 부분으로 나누며 물리 메모리를 4KB 페이지로 나누어 관리하는 방식이다. 페이지 디렉토리와 테이블은 GDT와 마찬가지로 메모리 공간에 있는 자료구조고, GDT에는 GDTR 레지스터가 있듯이 페이징에는 CR3 컨트롤 레지스터가 있다. CR3 컨트롤 레지스터는 페이지 디렉토리의 시작 주소를 가리키며 페이지 디렉토리 엔트리의 위치 계산에 사용한다. 페이지 디렉토리 엔트리와 페이지 테이블 엔트리 크기는 4바이트고, 페이지 크기가 최소 4KB이므로 비트 12 ~ 비트 31로 기준 주소를 나타내고 비트 11 ~ 비트 0은 속성 필드로 사용한다. 속성 필드 중 U/S(User/Supervisor) 필드다. U/S 필드는 해당 페이지에 접근할 수 있는 권한을 나타내고, 0으로 설정하면 유저 애플리케이션 레벨(3)을 제외한 모든 레벨에서 접근 가능, 1로 설정하면 유저 애플리케이션 레벨 이상에서 접근 가능하다. 선형 주소는 최상위 비트부터 디렉토리 오프셋 10비트, 테이블 오프셋 10비트, 페이지 오프셋 12비트로 구분한다. 디렉토리와 테이블의 오프셋이 10비트면 페이지 디렉토리와 페이지 테이블의 엔트리 수는 총 1024(2^10)개, 선형 주소의 마지막에 있는 페이지 오프셋은 12비트이므로 최댓값은 4KB(4096, 2^12)이며, 12비트인 이유는 4KB 페이지의 오프셋을 나타내기 때문이다.


보호 모드에서 3단계 페이징과 주소 변환 과정

  1. CR3 레지스터에 설정된 주소로 페이지 디렉토리의 주소를 찾는다.

  2. 페이지 디렉토리의 시작 주소에 선형 주소의 디렉토리 오프셋을 이용해서 해당 디렉토리 엔트리를 찾는다(디렉토리 엔트리에 설정된 값이 페이지 테이블의 시작 주소).

  3. 페이지 테이블의 시작 주소에 선형 주소의 테이블 오프셋을 이용해서 테이블 엔트리를 찾는다(페이지 테이블 엔트리에 설정된 값이 4KB 페이지의 시작 주소).

  4. 페이지의 시작 주소에 선형 주소의 페이지 오프셋 값을 더해 실제 물리 주소로 변환한다.


IA-32e 모드 중 호환 모드는 보호 모드와 동작이 같고, 64비트 모드는 64비트이므로 사용 가능한 최대 주소는 2^64(16EB)까지다. IA-32e 모드의 세그먼테이션은 보호 모드의 세그먼테이션과 큰 차이가 없다. 세그먼트 디스크립터에 설정된 기준 주소와 크기에 관계없이 모든 세그먼트가 기준 주소는 0, 크기는 64비트 전체로 설정된다는 것이다. 64비트 주소 지원을 위해 디스크립터를 확장하지 않고 주소만 저장하게 고정했기 때문인데, IA-32e 모드에서는 선형 주소를 기준 주소가 다른 여러 개의 세그먼트로 구분할 수 없음을 고려해서 OS를 설계해야 한다. IA-32e 모드에서 두 서브 모드를 구분하고자 코드 세그먼트 디스크립터에 L 필드(비트 21)가 추가됐다. 이 필드를 0으로 설정하면 호환 모드, 1로 설정하면 64비트 모드로 동작하므로 IA-32e 모드에서는 보호 모드로 돌아가지 않고도 32비트 코드를 실행할 수 있다.

IA-32e 모드의 페이지 테이블 엔트리는 비트 40에서 비트 51까지 모두 0으로 예약되어 있다. 40비트 물리 메모리를 지원하는 프로세서는 주로 서버용 프로세서이므로 PC에 사용하는 프로세서는 보통 1TB 이하 (64GB, 128GB 등)만 지원한다. 64비트 크기의 주소 공간이 필요한 경우가 거의 없어서 솔직히 40비트 주소만으로도 충분하다..

IA-32e 모드의 페이지 엔트리는 64비트로 늘어난 주소로 인해 8바이트로 늘어났고, 하위 4바이트는 보호 모드와 구조 같고 상위 4바이트는 기준 주소 필드와 예약된 영역, 임의로 사용 가능한 영역, EXB로 구성된다. EXB 필드는 해당 페이지에서 명령어가 실행되는 것을 막는 필드다.

정상적인 프로그램은 데이터 영역에서 명령어가 실행되지 않는다. 프로그램은 코드 영역과 데이터 영역으로 구분되는데, 버퍼나 스택에 프로그램 코드를 삽입한 후 해당 영역의 최댓값 이상으로 데이터를 밀어 넣어 강제로 코드를 실행할 수 있다. 이 공격이 버퍼 오버플로우와 스택 오버플로우다. 데이터 영역의 페이지에 EXB 속성을 활성화했다면 이를 막을 수 있다.

보호 모드와 달리 IA-32e 모드는 페이징이 필수다!