내밥줄/리눅스

[펌] Intel 80386 Protected Mode

jjoell 2012. 11. 14. 10:23

글쓴이 : 이호 (i@flyduck.com

최신 글이 있는 곳 : http://linuxkernel.net/
v0.1.0 2000년 4월 4일

0. 서문

현 재 Linux가 돌고 있는 시스템 중의 대부분은 Intel IA32 CPU이다. Linux는 Intel 80386부터 시작하여 80486, Pentium 계열의 CPU에서 실행이 된다. 운영체제의 기능을 구현하려면 CPU의 지원을 필요로 하는데, Linux는 80386부터 등장한 32-bit 보호모드(protected mode)의 지원을 이용하여, 메모리 관리, 프로세스 관리 등을 하고 있다. 여기서는 Linux를 구현하기 위해 필요한 보호모드의 기능들을 간단히 살펴보도록 한다.

1. 실제모드(Real Mode)

실 제모드는 x86 계열로 처음 등장한 Intel 8086 CPU와 같은 동작 모드를 말하는 것으로, 16 bit CPU인 8086, 80286에서뿐만 아니라, 80386 이후의 모든 32-bit 프로세서에서도 이를 지원한다. x86 계열의 모든 CPU는 처음 시작할 때는 실제모드로 동작한다.

실제모드에서는 20 bit address bus를 사용하여 총 1MB의 메모리를 사용할 수 있으며, 16 bit register를 사용한다. 레지스터의 크기가 16 bit이기 때문에, 20 bit 주소를 나타내기 위해 segment register라는 것을 도입하였다. 이는 16 bit segment register와 16 bit offset을 중첩시켜서 20 bit의 주소를 만들어내는 것이다. 모든 메모리 접근에는 segment와 offset이 같이 필요하며, 하나의 segment를 사용하면 64K(0×10000)만큼의 메모리를 사용할 수 있다. 실제모드에서는 가상 메모리라는 개념이 존재하지 않으며, segment와 offset으로 만들어지는 주소는 바로 물리적인 메모리 주소이다. 또한 이 모드에서 동작하는 모든 프로그램은 메모리의 어떤 영역이든지 맘대로 접근할 수 있으며, cli (clear interrupt), sti (set interrupt)같은 명령어를 포함하여 실제모드에서 사용할 수 있는 모든 명령어들을 모두 사용할 수 있다.

IBM-PC는 8086 CPU를 이용하여 만들어졌는데, 이것이 처음 등장하던 당시엔 1MB의 메모리는 상당히 큰 것이었다. 그래서 IBM은 앞의 640KB(0 – 0x9ffff)만을 프로그램이 사용할 수 있게 하고, 나머지 384KB(0xa0000 – 0xfffff)는 BIOS와 ISA 장치용으로 사용하게 하였는데, 이는 실제모드로 동작하는 각종 프로그램들이 640KB의 메모리만을 사용할 수 밖에 없는 제약을 만들었다. 일반적으로 리눅스 부팅을 할 때 사용되는 LILO도 실제모드로 시작하여 리눅스 커널을 로드하기 때문에, 마찬가지로 640KB의 제약을 받게 된다.


그림 1-1. 실제모드에서의 메모리 변환과정 (주1)


그림 1-2. 실제모드에서의 메모리 계산방법 (주2)

2. 보호모드(Protected Mode)

Intel 80286부터 처음 도입된 보호모드는 80386에 이르러서 완성된 모습을 보여 지금에 이르게 된다. 80286의 보호모드를 간단히 살펴보면, 우선 24 bit address bus를 사용하여 총 16MB(0×1000000)의 메모리를 사용할 수 있게 하였다. segment register는 selector라는 명칭으로 바뀌었고, descriptor table이라는 것을 통하여 16 bit segment를 24 bit base address로 바꾸게 하였다. 이 base address에 offset을 더함으로써 모두 16MB의 메모리를 사용할 수 있었지만, 80286 역시 16 bit CPU였기 때문에 각 segment의 크기는 여전히 64KB의 제한을 가지게 되었다.

이런 제한을 없애고 완전한 32 bit address space와 32 bit register set을 제공하는 80386이 등장하였고, 이후에 운영체제들은 80386에서 제공하는 보호모드의 기능을 활용하게 되었다. 보호모드에서는 CPU가 제공하는 모든 기능과 명령어들을 활용할 수 있다. 보호모드에서는 privilege level이라는 것이 등장하는데, 이는 프로세서를 활용할 수 있는 권한정도를 말한다. 이는 0부터 3까지 있는데, 0이 모든 일을 할 수 있는 모드로 일반적인 운영체제 구현에서 커널모드가 이 상태이며, 3은 응용프로그램처럼 사용자 권한의 실행상태를 나타낸다. 이 privilege level을 통하여 커널모드/사용자모드를 구현하게 된다. 보호모드에서는 32 bit address bus를 통하여 4GB의 메모리를 사용할 수 있으며, 메모리 보호기능과, 페이징(paging) 메커니즘 등을 통해 가상 메모리를 효율적으로 구현할 수 있다. 인터럽트나 예외처리, task switching 등도 모두 보호모드에서 지원하는 기능을 활용한다.


그림 2-1. 보호모드 레지스터와 자료구조 (주3)

3. Intel 80386 Registers

  • 일반 목적의 data register (32 bits) :
    • EAX, EBX, ECX, EDX
    • ESI (source pointer), EDI (destination pointer)
    • ESP (stack pointer), EBP (base pointer)
  • Segment Register / Selector (16 bits) :
    • CS (code segment), DS (data segment), SS (stack segment)
    • ES, FS, GS
  • Flag Register (32 bits) : EFLAGS
  • Instruction pointer : EIP
  • Control Register : 시스템의 동작들을 제어하기 위한 레지스터
    • CR0 : 프로세서의 상태와 동작모드를 제어하는 여러가지 제어 flag를 가지고 있다. 대표적인 flag로는 PG (Paging. paging 사용여부 설정), PE (Protection Enable, 보호모드를 사용하는지 여부)가 있다.
    • CR1 : reserved
    • CR2 : page fault가 발생하였을 때 이것이 발생한 linear address를 가지고 있다.
    • CR3 : page directory가 시작하는 physical address를 가지고 있다.
    • CR4 : 아키텍쳐별로 확장한 여러가지 flag들을 가지고 있다.
  • Descriptor Table Register :
    • GDTR (Global Descriptor Table Register) : GDT의 위치를 가리키는 register. LGDT (Load GDT), SGDT (Store GDT) 명령으로 참조하고 설정한다.
    • LDTR (Local Descriptor Table Register) : GDT안에 LDT descriptor를 가리키는 selector. LLDT (Load LDT), SLDT (Store LDT) 명령으로 참조하고 설정한다. 내부적으로 segment의 base address와 segment limit도 가지고 있지만 외부에서는 selector 값만을 참조할 수 있다.
    • IDTR (Interrupt Descriptor Table Register) : IDT의 위치를 가리키는 register LIDT (Load IDT), SIDT (Store IDT) 명령으로 참조하고 설정한다.
  • Task Register : TR

TSS (Task State Segment)가 있는 위치를 가리키는 GDT 내의 TSS descriptor를 가리키는 selector. 내부적으로 segment의 base address와 segment limit도 가지고 있지만 외부에서는 selector 값만을 참조할 수 있다. LTR (Load TR), STR (Save TR) 명령으로 참조하고 설정한다.

  • Test Register :
    • TR1 : test parity check
    • TR2, TR3, TR4, TR5 : cache test register
    • TR6, TR7 : TLB (Translation Look-aside Buffers) test registers
    • TR9, TR10, TR11 : BTB (Branch Target Buffers) test registers
    • TR12 : new feature control
  • Debug Register :
    • DR0, DR1, DR2, DR3 : debug address register. breakpoint의 linear address를 가지고 있다.
    • DR4, DR5 : reserved
    • DR6 : debug status register
    • DR7 : debug control register
  • Model Specific Registers (MSRs) :

4. 보호모드에서의 메모리 관리

보 호모드에서는 segmentation과 paging 메커니즘을 이용하여 메모리를 관리한다. segmentation은 4GB의 메모리를 segment라는 단위로 쪼개는 것을 말한다. 여기서는 16 bit의 selector와 32 bit의 offset을 이용하여 4GB 범위안에 있는 32 bit의 선형주소(linear address)를 만드는 일을 한다. 이렇게 만들어진 선형주소가 물리주소(physical address)가 되는 것이 아니라 메모리를 4KB 단위로 쪼개서 관리하는 paging mechanism을 거쳐서 물리주소로 변환된다. (control register CR0의 PG flag를 수정함으로써 paging 메커니즘을 사용하지 않을 수도 있다)


그림 4-1. 보호모드에서의 메모리 변환과정 (주4)

segment는 segment descriptor라는 것으로 정의가 된다. 여기에는 segment의 base address와 segment limit (크기), 그리고 DPL (descriptor privilege level)을 비롯한 정보가 들어간다. (여기에 보면 segment type이라는 것이 있다. 이것은 이 descriptor가 나타내는 것이 무엇인지를 말하는 것이다. segment는 일반적인 code/data/stack일 수도 있고, 뒤에 나오는 TSS 일수도, gate일 수도 있는데 이 type을 가지고 무엇을 가리키는 descriptor인지 구별할 수 있으며, 이에 따라 descriptor의 구조가 달라진다.) segment descriptor table은 이들 segment descriptor를 모아두고 있는 것을 말하는데, 여기에는 시스템 전체에 대한 descriptor table인 GDT (global descriptor table)과 프로세스마다 개별적으로 정의하고 있는 LDT (local descriptor table)이 있다. 이들은 모두 메모리 상에 존재하게 되는데, GDT의 위치는 GDTR (GDT register)가 가리키고 있다. LDT의 위치는 LDTR (LDT register)가 가리키고 있는데 이 LDTR은 원래 GDT에 있는 한 segment descriptor를 가리키고 있는 selector이다. 즉 GDT에는 시스템에 있는 모든 LDT에 대한 segment descriptor가 들어있으며, LDTR은 이 중 현재 프로세스의 LDT에 대한 segment descriptor를 가리키는 selector이다.

16 bit인 selector에는 이 descriptor table에서의 index 값과, 이것이 이것이 GDT에서의 index인지, LDT에서의 index인지를 나타내는 TI (table indicator), 권한을 나타내는 RPL (requestor privilege level)이 들어있다. 이 TI와 index를 가지고 해당하는 table에서 segment descriptor를 찾아서 base address를 구하게 된다. 이 값에 offset을 합하면 지정한 selector와 offset에 해당하는 linear address가 만들어지게 된다.


그림 4-2. Selector (주5)


그림 4-3. Segment Descriptor (주6)


그림 4-4. descriptor table 참조 (주7)


그림 4-5. 가상주소에서 선형주소로의 변환 (주8)

이렇게 segmentation을 거쳐 나온 linear address는 paging 메커니즘을 통하여 physical address로 변환이 된다. paging은 요즘의 운영체제에서 모두 구현하고 있는 paging을 지원하기 위한 것이다. 이를 이용하여 실제로 하드웨어적으로 있는 메모리보다 많은 메모리를 사용할 수 있으며, 메모리를 효율적으로 사용할 수 있으며, swapping을 쉽게 구현할 수 있게 된다. 메모리는 4KB 단위의 page로 쪼개지며, 이들은 page directory와 page table로 체계화된다. 각 page들의 시작 위치는 page table entry에 기록이 되며, 각 page table들의 위치는 page directory entry에 들어있다. page directory의 시작 위치는 control register중의 하나인 CR3이 가지고 있다. 앞에서 넘어온 linear address는 page directory에서의 index, page table에서의 index, offset 세가지로 쪼개지며, 이들 table들을 차례로 따라가서 실제 page의 주소를 얻고, 여기에 offset을 더함으로써 실제 physical address가 나오게 된다.


그림 4-6. 선형주소에서 물리주소로의 변환 (주9)

이렇게 가상주소가 물리주소로 변환되는 과정을 정리하면 다음과 같다.


그림 4-7. 가상주소에서 물리주소로의 변환 (주10)

5. 보호모드에서의 태스크 관리

보 호모드에서는 각 task별로 TSS (Task State Segment)라는 것을 관리한다. 여기에는 하나의 task의 상태 – 일반 register, segment register, flag, EIP, stack segment selector, stack pointer, LDT selector, page directory base address 등 – 가 저장이 되며, task switching이 일어날 때 이전의 상태를 자동으로 여기에 저장을 하며, 새로운 TSS에 있는 상태가 현재 상태로 복구가 된다. task register인 TR은 현재 task의 TSS를 가리키는 selector이다. 각 TSS는 GDT에서 TSS descriptor로 기술되며, TR은 이 중에서 현재 TSS의 descriptor를 가리키는 것이다.


그림 5-1. Task Register (주11)

task switching을 하는 방법으로 우선 call이나 jmp 명령에 전환할 TSS selector를 지정하는 것이 있다. 이렇게 하면 프로세서는 현재 상태를 현재 TSS에 저장을 하고, 새로운 task의 TSS로 상태를 모두 바꾼후, 새로운 task를 실행한다.

task gate는 task switching을 일으키는 특별한 descriptor로서 LDT나 IDT (Interrupt Descriptor Table)에 들어가는 descriptor이다. 여기에는 TSS descriptor에 대한 selector가 들어있다. task gate는 interrupt나 trap이 발생하였을 때 task switching이 일어나게 하는 것처럼 간접적으로 task switching이 일어나게 하거나, 특정한 task들이 TSS에 접근할 수 있도록 하기 위해서 사용한다. 앞의 경우에서처럼 call이나 jmp에 task gate의 selector를 지정을 해주면 여기서 가리키는 TSS로 task switching이 일어난다. 또한 task gate가 IDT에 있을 때 해당하는 interrupt가 발생하면 이 task gate를 통하여 해당하는 TSS로 task switching이 일어나게 된다.


그림 5-1. Task Gate (주12)

6. 보호모드에서의 인터럽트/예외 처리

인 터럽트나 예외가 발생하면 어떤 것이 발생하였는지 식별하는 번호가 나오게 된다. (인터럽트는 CPU의 인터럽트 핀에 의하여 발생하며, 예외는 프로세서가 작업을 하는 중에 잘못된 일을 발견하거나 – 여기에는 fault, trap, abort가 있다 -, 프로그래밍으로 들어가 있는 코드에 의하여 – INT 3, INT n, BOUND 등 – 에 의하여 발생하는 것이다) 이 번호를 vector라고 부르는데 이를 index 삼아, IDT (interrupt descriptor table)에 있는 descriptor를 찾아서 이를 처리하게 된다. IDT는 interrupt 처리에 관련된 descriptor들을 모아둔 것으로 이의 위치는 IDTR (IDT register)가 가지고 있다. 여기에 들어가는 descriptor로는 task gate, interrupt gate, trap gate가 있다. task gate는 앞에서 이야기한 것처럼 TSS selector를 가지고 있는 gate로서, 해당하는 TSS로 task switching이 일어나게 된다. interrupt gate와 trap gate는 GDT/LDT에 있는 segment descriptor에 대한 selector와 offset을 가지고 있는 gate로서, selector를 가지고 segment의 시작주소를 얻고, 여기에 offset을 더하여 이를 처리할 handler의 주소를 얻게 된다. 그리고 이 handler의 위치로 건너가서 해당하는 처리를 한 후 이를 마친후에 복귀하게 된다. 이 과정에서 privilege level이 바뀔 수도 있으며, 처리 중에는 현재 TSS에 있는 stack을 그대로 사용한다. interrupt gate와 trap gate를 통하는 경우의 차이는, interrupt gate를 통하는 경우 처리하는 동안 interrupt를 금지하고 (flag의 IF를 clear) 처리를 마치고 복귀할 때 다시 이를 복구하지만, trap gate를 통하는 경우에는 interrupt를 금지하지 않는다.


그림 6-1. Interrupt Gate와 Trap Gate (주13)


그림 6-2. Interrupt 발생시 Task Gate (주14)

7. 보호모드에서의 시스템콜

시 스템콜은 사용자모드에서 커널모드에서 실행되는 코드를 부르기 위한 방법이다. 이를 위해서 위해서 보호모드에서는 call gate라는 것을 제공한다. call gate는 다른 privilege level 사이로 제어권을 넘기는 방법으로, segment selector와 offset을 가지고 있다. 이 selector는 GDT/LDT에 있는 segment descriptor를 가리키고, 여기서 얻어지는 base address에 offset을 더하여 실행할 함수의 위치를 얻게 된다. call이나 jmp 명령에 목적하는 call gate의 segment selector를 지정함으로써 call gate를 통하여 커널모드로 진입하게 된다. 이 때 CPL (current privilege level)과 call gate descriptor의 DPL, call gate selector의 RPL을 비교하여 권한을 검사한다. 시스템콜에 진입하였을 때에는 현재 task의 TSS에 있는 stack을 사용하며, ret 명령을 통해서 결과값과 함께 시스템콜을 부르기 이전의 상태로 되돌아가게 된다.


그림 7-1. Call Gate (주15)

A. Reference


출처:http://digitalangelmaster.wordpress.com/2008/03/13/%ED%8E%8C-intel-80386-protected-mode/


'내밥줄 > 리눅스' 카테고리의 다른 글

vmlinuz를 vmlinux로 바꾸기  (0) 2012.11.21
[펌][Linux] samba 설치하기  (0) 2012.11.14
리눅스 보안 프로그램들...  (0) 2012.10.23
vim Tex 명령  (0) 2012.10.22
[펌]screen 명령  (0) 2012.10.22