|
이전 포스팅에서 Protected Mode로 가기 전의 Real Mode에 대해 알아보았다. Real Mode에서의 세그먼트주소지정방식과 그의 한계에 대해서 알아봤다. 16 bit 세그먼트 주소지정 방식은 아래와 같은 한계가 있다.
1. 메모리 보호기능이 없다.
2. 메모리 가용범위가 적다.
3. 0x10단위로 세그먼트 주소를 지정해야한다.[원하는 위치에 시작주소를 둘 수 없다]
이러한 한계에 있는 16bit Real Mode에서 32비트 Protected Mode 변하면서 메모리 보호기능 추가, 메모리 가용범위의 증가, 원하는 위치에 세그먼트시작주소 지정가능의 3가지 한계가 모두 극복 가능하다.
DPL이라는 2비트에 권한에 따라 다른 값을 지정한 후, 메모리를 부를 때 지금 부르려는 세그먼트셀렉터가 가진 DPL과 세그먼트 디스크립터가 가진 DPL을 확인하여 세그먼트디스크립터를 가져 올지 말지에 대해 결정한다.
기존의 [0xFFFF:FFFF]인 0xFFFFF만큼만 주소지정을 할 수 있었다면, 이제는 0xFFFFF * 0xFFF이 되어 0xFFFFFFFF만큼의 크기를 4GB만큼의 영역을 주소지정하고 활용할 수 있다. 16bit의 1MB의 크기의 약 4000배의 값이 커져 엄청난 공간확보가 가능해졌다.
16bit에서 세그먼트 지정하는 방법이 [세그먼트:오프셋]이라는 방법으로 했는데, 세그먼트부분에 *0x10(곱하기 0x10)을하여 주소를 0x10단위로만 지정했어야 했지만, 32bit Protected Mode의 세그먼트 디스크립터에는 단순히 원하는 베이스 주소를 넣어 접근 가능하다.
Protected Mode로 바뀌면서 새롭게 생긴 것이 GDT와 셀렉터 레지스터, 디스크립터 레지스터이다. 천천히 이들에 대해 알아보도록 하겠다. 먼저 큰 그림을 약간만 말해준다면 GDT는 메모리에 올라와 있어서 운영체제에서 참고하는 부분이고, 바이너리에서 특정 세그먼트에 접근 할 때 그 해당 셀렉터 레지스터에 설정된 인덱스를 확인하여 GDT에서 해당 세그먼트 디스크립터를 찾는다. 하지만 여기서 셀렉터 레지스터의 DPL과 세그먼트 디스크립터의 DPL을 확인하여 같은 권한을 가지고 있을 때만 디스크립터 레지스터로 세그먼트 디스크립터를 불러온다. 자 이제 GDT에 대해 알아보도록 하자.
GDT(Global Descriptor Table)
GDT는 Table of GD라는 말이다. 다시 말하자면 글로벌 디스크립터(Global Descriptor)들은 모아둔 테이블이라고 하면 되겠다. 디스크립터라면 어떤 것에 대해 설명하거나 가리킨다는 뜻(File Descriptor)으로 생각하고 이번 경우에는 전자에 해당한다. 32bit protected mode에서 사용할 세그먼트들을 설명하는 디스크립터라는 말이다. 이 디스크립터는 시작주소, 한계크기, 권한등을 가지고 있으며 GDT는 총 8192개의 디스크립터를 가질 수 있다. 후에 세그먼트 셀렉터의 인덱스 비트를 보면 알 수 있을 것이다. 여기서 권한을 이용하여 이전의 16bit real mode에서 권한에 상관없이 모든 메모리에 접근 가능했던 것을 지정된 권한을 확인하여 잘못된 접근을 막아준다. 이러한 디스크립터의 모음인 GDT를 참고하여서 해당 세그먼트에 접근 후 매핑되어 있는 물리 메모리에 접근한다(여기서는 페이징이 없지만 있다면 변환이후)
이제 GDT을 구성하는 하나의 디스크립터에 대해 알아보도록 하자. 처음에 GDT에 알아 볼 때 어떤 블로그는 Limit[15~0비트]부분을 아래쪽에 두고 어떤 블로그는 위쪽에 두어서 보는데 헷갈렸다. 구조를 쉽게 표현하기 아래 그림을 참고하고, 실제로 이미지에 들어가는 값은 hxd로 본 값을 참고하면 되겠다.
▲디스크립터 구조
[Limit 15 ~ 0 비트]
세그먼트의 크기를 나타낸다. 세그먼트가 어느정도의 크기를 가질 수 있는 지에 대한 것을 표현하는 데 이는 19 ~ 0 인 20비트로 표현된다. 그 중에서 15 ~ 0의 16비트를 나타낸다. 만약 이 크기를 넘어간다면 GP fault (Protected Mode 규약 위반)가 나타난다.
[Base Address 15 ~ 0 비트]
위의 16비트가 Limit인 크기를 나타냈다면, 이 부분은 세그먼트가 가리키는 첫 주소를 말한다. Base 주소의 경우에는 32bit 환경이므로 정확하게 해당 주소를 표현하기 위해서는 32bit를 모두 사용해야 한다. 만약 16bit의 세그먼트 주소지정방식처럼 특정 위치를 지정한 후 곱하는 형식으로 한다면 또다시 원하는 주소에 세그먼트의 베이스 주소를 지정할 수 없다. 그래서 세그먼트 베이스 주소를 표현하는 32bit에서 15 ~0비트 부분이다.
[Base Address 23 ~ 16 비트]
위의 Address와 같이 32bit의 세그먼트 베이스 주소지만 23 ~ 16bit이다.
[TYPE]
이 부분은 4bit로 16가지로 표현가능하다. 이 중 가장 앞 비트가 0인 경우는 Data, 1인 경우는 Code이다.
Data는 디폴트로 읽을 수 있도록 되어 있어 하위 3비트는 각각 Expand, Write, Access를 나타낸다. Expand는 아래로 확장되는 지 위쪽으로 확장되는 지를 말하며, 스택 세그먼트의 경우 이 비트가 1로 되어 있고 Expand Down을 의미한다. Write는 해당 데이터 세그먼트에 쓸 수 있냐 없느냐를 의미하고, Accessed는 접근한 세그먼트 인지를 의미한다.
Code는 디폴트로 실행 될 수 있도록 되어 있고, 하위 3비트는 Conform, Read, Access를 나타낸다. Conform인 설정되어 있으면 다른 권한, 그러니까 여기에서는 0인 커널레벨과 3인 유저레벨이 있는데 유저레벨에서 커널레벨로 변환이 가능하고, 만약 설정이 안되어서 non-conform이면 다른 레벨의 세그먼트로 이동할 때 general-protection exception(#GP)가 발생한다. 다음 Read와 Accessed는 차레대로 읽을 수 있는지, 접근했었던 지를 의미한다.
▲ 세그먼트 디스크립터에서의 TYPE 표 - 인텔 메뉴얼
[S]
이 비트가 0로 셋팅되면 세그먼트 디스크립터가 시스템 세그먼트로 사용된다는 것이고, 1으로 셋팅되면 code나 data 세그먼트로 사용된다. 시그템 세그먼트는 LDT, TSS, Call gate descriptor, Interrupt-gate descriptor, Trap-gate descriptor, Task-gate descriptor가 있다. 이에 대한 설명은 현재 포스팅의 주제에 벗어난 것이므로 인텔 아키텍쳐 소프트웨어 디벨로퍼 메뉴얼을 참고하거나 구글신의 힘을 빌리도록 하자.
[DPL - Descriptor Privilege Level]
세그먼트의 권한(특권)을 말해주며 2 bit로 표현된다. 2 bit이면 당연히 0~3까지 4개까지 표현가능하며 일반적으로 운영체제의 링에서 사용되는 것처럼 커널레벨에 0, 사용자 레벨에 3을 쓴다.
http://ko.wikipedia.org/wiki/링_(컴퓨터_보안)
[P]
처음에 이 비트는 P라고 되어 있어서 Paging에 관련된 비트인줄 알았는데, 여기서 P는 Paging로 쓰이는게 아니라 Present로 쓰인다. 이 비트가 1로 셋팅되면 세그먼트 디스크립터가 가리키는 세그먼트가 현재 메모리에 존재하는 것이고, 0으로 셋팅되어 있으면 현재 이 세그먼트가 메모리에 존재하지 않아서 segment-not-present execeptoin(#NP)라는 예외가 발생한다.
[Limit 19 ~ 16비트]
세그먼트에서 20bit의 limit에서 상위 4비트를 나타낸다.
[AVL]
시스템 소프트웨어에 사용되는 bit이다.
[0]
0으로 예약되어 있음
[D/B ( defulat operation size / default stack pointer size and/or upper bound) flag ]
1로 셋팅 되면 해당 세그먼트가 32bit로 사용되는 것을 뜻하고, 0으로 셋팅되면 16bit로 사용되는 것을 뜻한다. 그래서 1로 셋팅되면 code segment를 가리키는 ESP(32bit)와 upper bound가 FFFFFFFFH(4 GBytes)도 함께 뜻한다. 한편 0으로 셋팅되면 SP(16bit)와 upper bound가 FFFFH (64 KBytes)를 뜻한다.
[G]
이 비트가 0로 셋팅되면 byte 단위로 세그먼트 limit가 정해지고, 1로 셋팅되면 4-KByte 단위로 세그먼트 limit가 정해진다. 만약 limit이 0x1111인데 G bit가 1로 셋팅되어 있으면 0x1111*0xFFF를 하여 0x1111FFF가 된다. 0xFFF가 2^12( 4Kbyte)이므로 해당 단위인 4Kbyte가 곱해진다.
[Base Address 31 ~ 24 비트] 00
이 비트는 세그먼트 베이스 주소의 상위 8bit를 나타낸다.
소스와 이미지에서 GDT
Protected 모드에서 중요한 GDT에 대한 소스를 가져왔다. 먼저 nasm으로 gdtr: 이라는 레이블을 지정하고 그 부분에 크기(limit)과 베이스 주소를 넣는다. 그 후에 차례대로 널 디스크립터와 코드셀렉터, 데이터 셀렉터, 비디오 셀렉터를 넣는다. 소스를 설명할 때 세그먼트 디스크립터에 대한 부분은 코드셀렉터만 대표로 설명하도록 하겠다.
▲ GDT 소스코드(.asm)
[gdtr :
dw gdt_ end - gdt - 1
dd gdt + 0x10000]
gdt의 크기(2바이트 = word)만큼 저장하여 limit을 지정하고, 커널소스가 0x10000부터 시작이기 때문에 gdt레이블에서 +0x10000을 하여 4바이트(double word)로 저장한다.
[SysCodeSelector :
dw 0xFFFF
dw 0x0000
db 0x01
db 0x9A
db 0xCF
db 0x00 ]
이 부분이 위에서 열심히 플래그 하나하나 설명했던 부분이라고 보면 되겠다.
0xFFFF는
limit 0~15bit를 뜻한다.
0x0000은
base address의 0 ~ 15bit를 뜻하고
0x01은
base address의 16 ~ 24bit 뜻한다.
0x9A는
0x9인 1001은 P = 1로 세그먼트가 메모리에 있다는 뜻이고 DPL이 00로 커널 레벨의 권한을 , S =1 시스템 세그먼트로 사용된다는 뜻이다.
0xA인 1010은 코드세그먼트이면서 non-conforming이고, Readable 이라는 것을 뜻한다.
0xCF는
0xC는 1100으로 limit의 기본 단위가 4Kbyte이면서, 이 세그먼트는 32bit로 사용된다.
0xF는 1111으로 limit의 상위 16 ~ 19bit가 1111을 뜻하는 것을 말한다.
마지막 0x00은
base address의 24 ~ 32bit가 0인 것을 의미한다.
각 selector들 앞에 0x08, 0x10, 0x18이 기입되어 있는데 이는 전에 설명했던 것과 마찬가지로 하나의 디스크립터의 크기가 8바이트이므로 그 크기에 따른 위치를 지정해주는 것으로 보면 되겠다. 그래서 GDT에서 0x0부분에는 널 디스크립터 0x8부분에는 SysCodeSeletor 순으로 지정했다.
▲ .img파일에서 세그먼트 디스크립터 값
8바이트(64bit)인 디스크립터들을 각각 색으로 표시했다. 이런 디스크립터들을 모여 있는 것들이니 GDT라고 부르며, 빨간색 네모 앞 부분에 00이 8개가 채워진 것을 알 수 있다. 이는 GDT의 가장 첫 부분인 널 디스크립터인데 세그먼트들을 초기화하는데 사용한다. 그리고 빨간색 네모로 표시되어 있는 부분이 위에 소스와 함꼐 설명한 SysCodeSeletor이다.
Segment Register(Segment Selector 와 Segment descriptor)
GDT에 알아봤으니 GDT를 있는 정보를 어떻게 저장할 지에 대해 이야기한다. 처음 프로그램에서 원하는 세그먼트를 부른다. 한 프로그램마다 아래 그림과 같이 6개의 세그먼트가 사용된다. 이는 프로그램에서 지정하는 것이 아니라 컴파일러나 링커, 로더나 운영체제에서 해주는 일이다. 우리가 볼 수 있는 부분은 Visible Part은 세그먼트 셀렉터이고, Hidden Part가 지금까지 설명했던 GDT에서 있던 하나의 디스크립터를 저장할 공간이다. 그래서 이 부분에 Bass Address나 Limit등 각종 정보들이 들어 있다.
그러면 Visible Part인 세그먼트 셀럭터에 대해 알아보도록 하자. 우리가 볼 수 있는 부분은 이곳이니 셀렉터라는 이름을 있는 걸 봐서는 어떤 것을 가리키는 포인터라고 예상할 수 있다. 아까 GDT를 나타낸 소스코드에서 말했듯이 하나의 디스크립터는 8바이트이다. 그래서 index의 가장 하위 비트는 3bit부터 사용하여도 가능한 것이다. 그래서 index비트에 0000000000001이면 아까 처음 0x08로 지정하였던 SysCodeSelector를 가르킨다. TI는 이 세그먼트 셀렉터가 GDT를 참고할 것인지 LDT를 참고할 것 인지를 말한다. GDT와 LDT에 대해서는 아래에서 다시 설명하도록 하겠다. RPL은 요청 권한을 의미하는데 만약 세그먼트 셀렉터의 권한이 03으로 user권한인데 GDT에서 Index으로 참고하여 접근한 디스크립터의 DPL이 00이어서 kernel 권한이면 접근이 불가한 것을 의미한다.
GDT, LDT
처음에는 GDT, LDT라고하여 둘이 동등한 관계에 있는 테이블이라고 착각하였다. 그래서 왜 LDT에 접근하기 위해서 GDT를 참고해야 하는지 이해가 가지 않았다. 하지만 그것이 아니라 16bi real mode에서 시스템 초기화할 때 GDTR이라는 레지스터에 등록되어 있는 것이 GDT이고, LDT는 GDT가 포함하고 있는 디스크립터중 하나이다. 그래서 이 LDT는 위의 세그먼트 레지스터에 저장되어 있고, 이 중 세그먼트 셀렉터에는 GDT에서의 자신의 인덱스를 가지고 있다. 그래서 GDT는 시스템 레지스터에 등록되어 하나만 존재하지만, LDT의 경우에는 각 프로그램마다 하나이상을 존재할 수 있다. GDT를 GDTR에 등록하는 부분을 아래에서 다시 설명하도록 하겠다.
▲GDT와 LDT
16비트 Real mode -> 32비트 Protected mode
이제 막바지다. 16 bit real mode에서 32bit protected mode를 설명하기 위해 GDT, 세그먼트 레지스터를 살펴봤다. 아까 소스에서 GDT의 크기와 베이스 주소를 저장하는 것이 기억나는가? 오른쪽 아래 그림과 같이 크기와 주소가 gdtr이라는 레이블에 저장되고 이 것을 lgdt를 통해 GDTR에 GDT를 등록 시키는 것을 왼쪽 그림 12번째 줄에서 볼 수 있다.
▲real mode에서 protected mode로 가는 핵심 소스(왼), GDT(크기 및 베이스 주소)를 GDTR에 등록(오른)
[cli]
interrupt flag를 초기화하여 interrupt를 disable한다.
[lgdt[gdtr]]
위에서 설명했듯이 gdtr이라는 레이블에 저장된 값( 크기 및 물리 주소)를 GDTR에 등록한다.
[mov eax, cr0
or eax, 0x00000001
mov cr0, eax]
cr0(control register 0)의 최하위 bit가 1이면 32bit, 0이면 16bit를 의미하는데 1로 셋팅하여 32bit로 바꾸는 것을 의미한다.
[jmp $+2
nop
nop]
저번 포스팅에서 $는 현재 위치를 가리킨다고 했다. 그러면 jmp $+2는 nop으로 가는 것인데 왜 이런 것을 할까? 그 이유는 CPU는 읽고, 해석하고, 실행을 처리하는 부분이 다 따로 나뉘어져있다. 그러면 내가 지금 32bit로 바꾸더라도 아직 이 유닛들안에 16bit모드인 명령어나 코드가 들어있을 수 있으므로 2byte 점프하면서 이런 유닛에 있는 명령어들을 지운다.
세그먼트가 불려지는 구조
두가지를 살펴보려고 한다. 먼저 특정 세그먼트에 값을 넣었을때 어떻게 CPU가 세그먼트 셀렉터를 참고하여 세그먼트 디스크립터에 값을 가져오는 지에 대한 것을 아래 그림을 통해 보면 되겠다.
1. CS(Code Segment)에 0x8을 넣으면 1000이 들어가서 TI, RPL은 둘다 0으로 초기화되고 Index부분은 1이 될 것이다. 그래서 이 Index와 GDTR의 BaseAddress를 참고하여 GDT의 주소를 이용하여 세그먼트 디스크립터를 찾는다.
2. 세그먼트 셀렉터의 RPL과 세그먼트 디스크립터의 DPL을 비교한다.
3. 권한이 같다면 세그먼트 레지스터의 세그먼트 디스크립터에 해당 정보를 옮긴다.
▲ cs(Code Segment)에 값을 넣었을 때 CPU 의 동작
그 다음으로 특정 세그먼트에서 데이터를 가져 올 때 구조에 대해서 보도록 하자. 아래와 같은 어셈블리 명령어가 있다. 이는 ds(data segment)의 msgPmode라는 오프셋의 주소를 가져와서 esi에 저장하는 것이다.여기서 아래 그림처럼 컴파일된 기게어와 소스코드를 같이 보여주는데 msgPMode의 오프셋이 0x63인 것을 알 수 있다.
▲ 세그먼트 접근 어셈블리어
▲ msgPMode의 오프셋 = 0x63
▲ 레지스터 확인
위 그림과 같이 0x63은 소스코드에 정해준 레이블? 변수라고 생각하면 되고 이것이 기계어로 들어가면 오프셋 0x63이 된다. 그래서 결국 ds:0x63을 부른 구조를 보여준다. 0x63이 ds세그먼트 디스크립터 레지스터에 limit가 넘지 않는 것을 확인하고 Base Address와 오프셋인 0x63을 더하여 0x10063인 선형주소를 만든다. 여기서 페이징을 고려하지 않았기 때문에 실질적으로 물리메모리 주소 0x10063에 "We are in Protected Mode"가 있다.
마치면서
생각보다 내가 공부했을 때는 몰랐는데 정리하면서 보니 생각보다 많은 양의 내용이 Protected 모드에 관련 되 어있다. 물론 내가 관련된 모든 것을 정리하지는 못했지만 말이다. 어쨋든 가장 핵심은 real mode의 한계와 GDT, 세그먼트 레지스터에 대해 이해다. 그리고 세그먼트 셀렉터에 값을 넣었을 때 CPU의 플로우와 어떻게 메모리에 세그먼트 레지스터를 통해서 매핑된 물리메모리 주소에 접근하는 지에 대한 구조를 알면 되겠다.
아래는 소스코드이며 이전 포스팅에서 사용한 boot.asm 파일과 아래 파일을 컴파일 한 이미지를 copy로 합쳐서 vmware에 올리면 된다.
참조
http://sangki19.tistory.com/22
http://anster.egloos.com/2135644
http://cotkdrl1.blog.me/10152722953
http://taesun1114.tistory.com/
http://64bitos.tistory.com/category/CLKH64%20OS?page=16
http://parkjunehyun.tistory.com/entry/GDTLDT-Segment-Descriptor
출처: https://kcats.tistory.com/156?category=554568 [kcats's mindstory]
|