0. 페이징

가상기억장치를 같은 크기의 블록, 즉 페이지(page)로 나누어 사용하는 기법입니다.

  • 프레임과 페이지

프레임은 물리 메모리를 일정한 크기로 나눈 블록이고, 페이지는 가상 메모리를 일정한 크기로 나눈 블록입니다. 페이지가 하나의 프레임을 할당 받아 물리 메모리에 위치함으로, 페이지는 알맹이, 프레임은 알맹이가 들어갈 틀이라고 생각하시면 쉽습니다. 프레임을 할당 받지 못한 페이지는 외부 저장장치, (하드디스크 등등)에 저장됩니다. 프레임과 페이지는 같은 크기로 관리됩니다.

  • 페이지 테이블(page table)

페이지 테이블은 프로세스의 페이지 정보를 저장하고 있는 테이블입니다. 하나의 프로세스는 하나의 페이지 테이블을 가지고, 테이블은 페이지 번호를 뜻하는 색인과, 해당 페이지에 할당된 물리 메모리(프레임)의 시작주소를 의미하는 내용인 내용으로 구성됩니다.

  • 페이지 테이블 엔트리

페이지 테이블 엔트리는(PTE : PageTable Entry)는 페이지 테이블의 레코드, 즉 각 항목으로 페이지 기본주소(Page base address)와 플래그 비트의 내용이 기록됩니다. 플래그 비트에는 접근 비트(페이지에 대한 접근이 있었는가?), 변경 비트(Dirty bit – 페이지 내용의 변경이 있었는가?), 현재비트(Present bit – 현재 페이지에 할당된 프레임이 있는가?), 읽기/쓰기 비트(Read/Write bit – 읽기/쓰기에 대한 권한을 표시)와 같은 내용이 기록됩니다.

  • 페이지의 크기

페이지의 크기는 X86과 amd64에서는 4kb, ia64에서는 8kb를 가집니다.

  • 동적 주소 변환

페이징 기법에서 동적 주소 변화는 다음과 같은 과정을 거칩니다.

페이징 기법이 적용된 시스템에서 가상주소는 순써쌍 v(p, d)로 나타나는데, p는 가상기억장치 내에서 참조될 항목이 속해있는 페이지 번호이고, d는 페이지 p 내에서 참조될 항목이 위치하고 있는 곳의 변위입니다.

1) 수행중인 프로세스가 가상 주소 V(p,d) 를 참조

2) 페이징 기법을 통해 페이지 p가 페이지 프레임 P' 에 있음을 알림

3) 실 주소 r = p' + d

 

 

1. 페이지 디렉토리와 페이지 테이블

페이징 기능을 사용하지 않았을 때 선형 주소는 물리주소와 같습니다. 그러나 페이징 기능을 사용하면, 논리 주소와 물리 주소의 사이에 선형 주소가 존재하고 따라서 주소를 변경해 주어야 합니다.

 

페이징을 사용하기 위해서 커널은 미리 페이지 디렉토리와 페이지 테이블을 만들어 놓아야 합니다.

1) 페이지 디렉토리

페이지 디렉토리는 1024개의 디렉토리 엔트리로 구성된 데이터 입니다. 시스템에 하나만 존재합니다. 각각의 디렉토리 엔트리에는 페이지 테이블의 첫 주소, 페이지 테이블의 포인터를 가지고 있습니다. 따라서 시스템에는 1024개의 페이지 테이블이 존재한다고 볼수 있습니다.

 

2) 페이지 테이블

각각의 페이지 테이블은 또 1024개의 페이지 테이블 엔트리를 가지고 있고 디렉토리 엔트리와 비슷한 구조를 가지고 있습니다. 페이지 테이블의 엔트리는 4KB의 물리 주소, 즉 페이지의 포인터를 가지고 있습니다.

 

페이지 디렉토리와 페이지 테이블은 다음과 같은 C언어 구문으로 나타낼 수 있습니다.

DWORD page[1024][1024];

 

전체 페이지 엔트리 수는 1024*1024 = 1MB 이고, 각각 4KB의 크기를 가지고 있어, 1mb*4kb = 4GB가 됩니다. 따라서 페이징을 사용하여 4GB 영역의 주소를 지정할 수 있습니다.

4GB영역을 모두 지정하기 위해서는 1024*4+1024*4= 16MB(각 엔트리가 32비트)의 영역이 메모리에 할당되고 유지되어야 합니다. 낭비가 될 수 있으므로, 페이지 테이블의 수를 조절합니다.

 

2. 선형주소를 물리주소로

세그먼트를 이용해 산출된 선형 주소는 1)최상위 10비트는 페이지 디렉토리에서 몇 번째 엔트리를 사용할지 결정하고, 2)그 다음 10비트는 페이지 테이블에서 몇 번재 엔트리를 사용할지 나타내며, 3)하위 12비트는 4KB 물리 페이지상에서 오프셋을 나타냅니다.

CR3 레지스터에는 페이지 디렉토리의 포인터가 저장되어 있습니다. 페이지 디렉토리의 포인터는 처음 페이지를 설정할 때 CR3 레지스터에 설정해 줍니다.

 

CPU는 프로그램에서 주소 지정을 하면 자동으로 세그먼테이션과 페이징을 수행하여 물리 주소를 계산 합니다. 위의 그림을 참고해주세요.

(1) 물리주소 계산

1) CR3를 참조하여 페이지 디렉토리 찾기

2) 주어진 선형 주소의 최상위 10비트를 참조하여 페이지 디렉토리의 엔트리 번호를 구해, 이 수에 4를 곱한 후 페이지 디렉토리의 포인터와 더해 페이지 디렉토리 내의 엔트리를 찾기

디렉토리 엔트리의 주소 = 선형주소의 최상위 10비트*4 + 페이지 디렉토리의 포인터

3) 찾아낸 엔트리를 참조하여 페이지 테이블의 물리 주소 찾기

4) 선형 주소의 중간 10비트를 이용하여 페이지 테이블 내의 엔트리 번호 찾기

5) 페이지 테이블에서 물리 페이지의 주소를 구하기

6) 선형 주소의 하위 12비트로 물리 페이지 내에서의 오프셋을 구하여 물리 주소에 접근

 

페이지 디렉토리 엔트리

31

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

12

11

 

9

8

7

6

5

4

3

2

1

0

페이지 테이블의 포인터

커널이 사용

G

P

S

0

A

P

C

D

P

W

T

U

/

S

R

/

W

P

 

페이지 테이블 엔트리

31

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

12

11

 

9

8

7

6

5

4

3

2

1

0

4KB  물리 페이지의 포인터

커널이 사용

G

P

A

T

D

A

P

C

D

P

W

T

U

/

S

R

/

W

P

(1) 포인터

페이지 테이블의 포인터와 4KB 물리 페이지의 포인터는 각각 해당하는 물리주소의 4kb를 나눈 값이 들어갑니다. 페이지는 4kb 단위를 기본으로 동작하기 때문입니다. 따라서 페이지 디렉토리나 페이지 테이블, 4KB 물리 페이지는 0x1000단위, 즉 하위 12비트를 0으로 하는 주소에 위치해야 합니다.

(2) A 비트

커널 프로그램이 모든 엔트리의 A 비트를 조사하여 최근에 접근한 페이지나 접근되지 않은 페이지를 찾아냅니다. 페이지에 접근되면 CPU가 자동적으로 해당 엔트리의 A 비트를 1로 세트합니다(클리어는 커널 프로그램의 몫입니다.)

(3) U/S 비트

0으로 클리어 되어 있으면 페이지 테이블도, 4KB 물리 페이지도 커널만 사용할 수 있다는 뜻입니다.

(4) R/W

0으로 클리어 되어 있으면 페이지 테이블이나 4KB 물리 페이지는 읽기만 가능하다는 의미입니다.

(5)P비트

0으로 클리어 되어 있으면 물리 주소상에 페이지가 위치하고 있지 않다는 의미이고, 1이면 물리 주소에 위치하고 있다는 의미입니다.

 

3. 예시

(1) 선형 주소 0x10065

0x10065를 32비트 이진수로 나타내면 다음과 같습니다.

00000000000000010000000001100101

파란색 부분은 페이지 디렉토리 엔트리 번호이며,

붉은 색 부분은 페이지 테이블의 물리 페이지 엔트리 번호이고,

초록색 부분은 페이지에서의 오프셋입니다.

 

각각을 16진수로 나누면 다음과 같습니다.

0x0

0x10

0x65

 

1) CR3에서 페이지 디렉토리의 시작 주소를 구합니다.

2) 페이지 디렉토리의 주소에 페이지 디렉토리 엔트리 번호(선형주소의 상위 10비트)*4한 값을 더해줍니다. 0이므로 첫번째 엔트리가 될 것입니다.

3) 페이지 디렉토리에서 0번째 엔트리에 포함된 페이지 테이블의 주소를 참조하여 페이지 테이블을 찾습니다.

4) 페이지 테이블의 첫 주소에 0x10*4=64를 더하여 페이지 테이블 엔트리를 찾습니다. 0x10은 선형 주소의 가운데 10비트로, 페이지 테이블의 물리 페이지 엔트리 번호입니다.

5) 찾은 페이지 테이블 엔트리에서 4kb 물리 페이지의 첫 주소를 찾습니다.

6) 4KB 물리 페이지의 첫 주소에 0x65를 더하여 실제 물리 주소를 구합니다.

 

페이지 디렉토리와 페이지 테이블을 10비트로 접근 하는 이유는 1024*4=4069=4KB이기 때문입니다. 반면 페이지의 오프셋을 12비트를 이용하여 접근 하는 이유는 1byte 단위로 접근해야 하기 때문입니다. 따라서 다른 경우 (페이지 디렉토리 엔트리의 페이지 테이블 포인터와 페이지 테이블 엔트리의 4KB 물리 페이지 포인터)와 다르게 4도 곱하지 않습니다.

 

4. 페이지 폴트

RAM 상에 프로그램이 데이터를 쓸 장소가 없는 경우, 페이지 폴트가 발생됩니다. 이때 14번 인터럽트 핸들러가 실행되고, 커널은 페이지 교체 알고리즘에 따라 디스크의 특정 공간에 저장하고, IRET 명령으로 핸들러를 마친뒤 페이지 폴트가 발생했든 프로그램에게 돌아갑니다.

마찬가지로 RAM상에 프로그램이 찾는 페이지가 없을 경우 (4KB 물리 페이지 엔트리의 p 비트가 0일 때) 페이지 폴트를 발생시키고, 14번 인터럽트를 실행 시켜 디스크의 스왑 영역에서 해당 페이지를 찾아 RAM 상에 옮긴뒤, 다시 해당 프로그램으로 돌아갑니다.

A20 혹은 addressing line 20는 x86 컴퓨터 시스템에서 전자적인 시스템 버스로 이루어져있습니다.
8086에는 어드레스 라인이 20개 뿐이여서 0xFFFF:FFFF에서 최상위 1비트를 표현할 수 없었습니다.( 0xFFFF:FFFF, 즉 0x10FFEF가 100001111111111101111로 21자리이기 때문에)

따라서 8086에서 1MB(0x100000) 초과 범위의 주소는 0번지부터 다시 시작되어야 하고 주소가 겹치게 됩니다.

80286때부터 CPU에 Protected Mode가 도입되고, 어드레스 라인이 늘어나게 됩니다. 8086과 호환하게 위해, REAL 모드를 만들고, 20번을 8042(8086 당시의 키보드 컨트롤러)와 AND 게이트로 접속시킵니다.

 

 

A20과 연결된 키보드 컨트롤러의 핀이 0이 라면 항상 0이 되어, 주소가 제대로 전달되지 않을 것이며, 홀수 값인 주소(단위 MB)는 제대로 전달 되지 않아 사용할 수 없을 것입니다.

예) 키보드 컨트롤러의 A20 GATE가 항상 0일 때

0x100000 -> 100000000000000000000(2) -> 000000000000000000000(2) -> 0x0

0x200000 -> 1000000000000000000000(2) -> 1000000000000000000000(2) -> 0x200000

주소값의 20번째 비트를 0으로 바꿔보면 다음과 같이 값이 변경되는 것을 볼 수 있습니다. 짝수 MB 단위의 주소의 경우 20번째 비트가 항상 0이므로 변화가 없습니다.

 

따라서 32비트 커널에서 A20 게이트를 켜 주어야, 제대로 주소 전달을 할 수 있습니다.

 

A20 게이트를 켜 주기 위하여, 먼저 1)int 0x15를 사용하여 BIOS 함수를 호출하거나 2)8042 키보드 컨트롤러를 제어하거나 3) I/O 포트를 제어하는 방법이 있습니다.

1. 스택 영역을 사용한 태스크 스위칭

(1) 인터럽트를 위한 스택

1) 인터럽트가 시작되었을 ,  커널 스택 영역은 다음과 같이 데이터를 쌓습니다.

 

SS0

 

 

ESP0

 

SS

ESP

 

EFLAGS

 

 

CS

 

EIP

 

 

2) 유저 데이터 세그먼트 셀렉터 값을 스택에 저장

DS, ES, FS, GS 유저 데이터 세그먼트 셀렉터 값이 존재하여, 값을 커널 데이터 세그먼트 셀렉터 값으로 바꾸어야 인터럽트 핸들러에서 데이터를 사용할 있습니다. 유저 데이터 세그먼트 셀렉터 값을 스택에 저장 한후, 커널 데이터 세그먼트 셀렉터 값으로 바꾸어 줍니다.

 

SS0

 

 

ESP0

 

SS

ESP

 

EFLAGS

 

 

CS

 

EIP

 

 

GS

 

 

FS

 

 

ES

 

 

DS

ESP

 

 

 

 

 

 

 

3) PIC 초기화

4) 인터럽트 루틴 수행

여기서는 .This is the timer interrupt 라는 문자열을 표시하고, 첫번째 문자의 값을 1 증가 시킵니다.

여기 까지의 스택 모습은 다음과 같습니다

 

SS0

 

 

ESP0

 

SS

ESP

 

EFLAGS

 

 

CS

 

EIP

 

 

GS

 

 

FS

 

 

ES

 

 

DS

 

EAX

EBX

 

ECX

 

EDX

 

EBX

 

EBP

 

ESI

 

EDI

ESP

 

 

5) 인터럽트를 마치고 ret_from_int 함수로

Ret_from_int 함수에서는 인터럽트 핸들러가 유저 영역 실행 중에 발생 했는지 커널 영역 실행 중에 발생 했는지 알아보는 루틴으로 되어 있습니다.

                  mov eax, [esp+52]

                  and eax, 0x00000003

ESP+52 (ESP+4*13) 인터럽트 실행 전의 CS 값을 참조합니다. 값에 and 3 하면 CS RPL 값을 있습니다.

                  mov bx, cs

                  and ebx, 0x00000003

                  cmp eax, ebx

마찬가지로 인터럽트 핸들러가 실행하고 뒤의 CS 값의 RPL 구합니다.

값을 구하여 비교하고, 만약 스택에서 뽑아낸 CS 값이 크다면 이전에 실행되던 태스크는 유저모드 태스크 이므로 태스크 스위칭을 위한 루틴 scheduler: 점프하고작거나 같다면 커널 모드 루틴 실행 중에 인터럽트이므로 스택에 저장해 두었던 범용 레지스터, 데이터 세그먼트 셀렉터 모두를 POP하여 스택에서 꺼내고, IRET 통해 EIP, Cs, Eflag 스택에서 꺼낸다음, 인터럽트가 발생한 시점으로 돌아갑니다.

 

(2) 유저 모드 태스크를 위한 스택

유저 모드 태스크에 각각 실행하던 레지스터 값을 저장하기 위해 RAM 상에 스택을 가지고 있습니다. 스택은 커널 모드 스택이며, 유저 모드에서 실행되는 루틴은 실행되선 안됩니다.

1) 유저 프로그램을 위한 레지스터 저장 영역 예시

times 63 dd 0

User1Stack:

User1regs:

                  dd 0, 0, 0, 0, 0, 0, 0, 0

                  dw UserDataSelector, 0

                  dw UserDataSelector, 0

                  dw UserDataSelector, 0

                  dw UserDataSelector, 0

                  dd user_process1

                  dw UserCodeSelector, 0

                  dd 0x200

                  dd User1Stack

                  dw UserDataSelector, 0

 

Times 63 dd 0 유저 태스크의 스택을 만들어 주는 영역입니다. 다음과 같이 스택이 생성됩니다.

0

UserDataSelector(SS)

User1Stack(ESP)

0x200(EFLAGS)

UserCodeSelector(CS)

0

User_process1(EIP)

0

UserDataSelector(GS)

0

UserDataSelector(FS)

0

UserDataSelector(ES)

0

UserDataSelector(DS)

0(EAX)

0(EBX)

0(ECX)

0(EDX)

0(EBX)

0(EBP)

0(ESI)

0(EDI)

 

라벨이 아래쪽에 지정되어 있는 것은 스택이여서, PUSH 될때 메모리의 작은 번지수를 향하여 진행되기 때문입니다스택의 아래부분에는 범용 레지스터들이 저장되는 공간이 있으며, POPAD 명령을 사용하여 메모리의 번지수방향으로 내려가며 복구하게 됩니다.

데이터 세그먼트 셀렉터 부분은 순서대로 pop 하며 복구합니다.

위의 EIP, CS, EFLGAS, ESP, SS iret명령을 통해 한번에 CPU 복구되게 됩니다.

소스에는 초기값으로 EIP 부분에 user_process1 지정되어 있으며, 프로그램이 실행되면서 EIP 영역의 값이 계속 변할 것입니다. 영역은 태스크스위칭이 일어났을 처음으로 실행 해야하는 명령어가 저장되어 있는 부분입니다.

2) 유저 프로세스

다음 코드는 유저 프로세스의 예시입니다.

user_process1:

                  mov eax, 80*2*2+2*5

                  lea ebx, [msg_user_process1_1]

                  int 0x80

                  mov eax, 80*2*3+2*5

                  lea ebx, [msg_user_process1_2]

                  int 0x80

                  inc byte [msg_user_process1_2]

                  jmp user_process1

 

msg_user_process1_1 db "User Process1", 0

msg_user_process1_2 db ".I'm running now", 0

 

Int 0x80 IDT 0x80번째 있는 트랩 게이트를 이용하겠다는 의미입니다.

                  push eax

                  mov ax, SysDataSelector

                  mov ds, ax

                  mov es, ax

                  mov fs, ax

                  mov gs, ax

                  pop eax

 

                  mov edi, eax

                  lea esi, [ebx]

                  call printf

 

인터럽트 서비스 루틴의 코드의 일부입니다. 세그먼트 셀렉터 레지스터를 커널의 데이터 세그먼트 셀렉터로 설정하였습니다. 과정에서 eax 사용하기 때문에, 스택에 eax 옮겨놓고 작업이 끝나면 다시 꺼내서 eax 저장합니다.

 

3) 태스크 스위칭

1) 태스크 스위칭 준비

CurrentTask dd 0 ; 현재 실행 중인 태스크 번호

NumTask dd 20    ; 모든 태스크의

TaskList :

                  times 5 dd 0  ; 태스크 저장 영역의 포인터 배역

 

다음 코드는 태스크 스위칭을 위해서 정의한 변수 선언입니다. 다음의 C언어 코드와 유사합니다.

int CurrentTask = 0;

int NumTask = 20;

int TaskList[5] = {0,};

 

TaskList 유저 영역 태스크가 사용하는 레지스터 저장 영역의 포인터를 계속해서 넣어줍니다. (4byte 포인터이므로 4 더하면서 넣습니다)

                  mov eax, [CurrentTask]

                  add eax, TaskList

                  lea edx, [User1regs]

                  mov [eax], edx

                  add eax, 4

                  lea edx, [User2regs]

                  mov [eax], edx

                  add eax, 4

 

2) 태스크 스위칭

scheduler:

                  lea esi, [esp]

 

                  xor eax, eax

                  mov eax, [CurrentTask]

                  add eax, TaskList

 

                  mov edi, [eax]

 

                  mov ecx, 17

                  rep movsd

                  add esp, 68

 

현재 커널 영역에 저장된 모든 레지스터 값을 유저 모드 태스크의 스택 위에 있는 레지스터 저장 공간으로 복사를 합니다.  이를 구현하기 위해서 ESP (스택 포인터) ESI 넣고 레지스터 저장 공간의 주소 포인터를 EDI 넣은뒤 rep movsd 값을 복사합니다.

이때, 커널 스택 영역에 저장된 레지스터 값은 이제 필요가 없어, ESP+68, 스택 포인터를 옮겨 원래대로 되돌립니다. 나중에 값은 덮어 씌여져 없어질 것입니다.

 

                  add dword [CurrentTask], 4

                  mov eax, [NumTask]

                  mov ebx, [CurrentTask]

                  cmp eax, ebx

                  jne yet

                  mov byte [CurrentTask], 0

 

yet:

                  xor eax, eax

                  mov eax, [CurrentTask]

                  add eax, TaskList

                  mov ebx, [eax]

 

CurrentTask 변수는 포인터로 사용되는데, 값에 4 ,  NumTask 비교하여, 같으면 모든 태스크를 번씩 수행한 것으로 보아, CurrentTask 변수를 0으로 만들어 다시 처음부터 수행합니다. 다음 다음에 실행할 태스크의 레지스터영역의 주소를 ebx 넣어줍니다.

 

3)TSS

TSS 영역을 하나만 사용하여, 인터럽트로 커널로 진입했을 , ESP 있는 포인터로 스택을 사용합니다. TSS 영역의 ESP0 현재 ESP 값을 넣습니다.

ESP 값은 스택을 이용하여 태스크 스위칭을 하고 레지스터 값을 복원하기 위해 사용됩니다.

이와 같이 스택에 레지스터를 저장하고 복원하는 것을 반복하면서 태스크 스위칭이 이루어 집니다.

(POPAD, POP, iret 등등등)

 

3. 태스크 실행

                  mov eax, [CurrentTask]

                  add eax, TaskList

                  mov ebx, [eax]

                  jmp sched

 

CurrentTask 변수를 이용하여 레지스터 저장 영역의 포인터를 꺼내, EBX 넣고 sched 번지로 점프하여 레지스터 값을 복원하여 IRET 명령을 통해 태스크를 실행합니다.

 하여 레지스터 값을 복원하여 IRET 명령을 통해 첫 태스크를 실행합니다.

+ Recent posts