한동안 이리치이고 저리치이면서 OS공부를 못했다. 일단 페이징에 개념을 잡기 위해서 미리 어느정도는 봤지만 만족스럽게 포스팅 할 정도로 자세히 알지 못했었다. 그래서 차일피일 미뤘다. 하지만 이러다가 한도 끝도 없이 뒤로 미뤄질 것 같아서 바닥에 축 늘어져 있던 내 의지를 다시 세우며 포스팅하기 위해 책을 펼쳤다.

 이번 포스팅은 운영체제를 배우면서 귀에 딱지가 앉을 정도로 들었던 세그멘테이션과 페이징에서 그 페이징이다. 이 책의 마지막 장은 C언어로 커널을 만들기 인 것을 고려하면 개념적인 부분은 막바지라는 것을 알 수 있다. 항상 들어왔던 운영체제에서 각 프로세스에게 독립적인 가상의 4GB를 제공한다는 말을 확실하게 이해하지 못하고 있는 사람들에게 이 포스팅을 바친다.

이 포스팅은 크게 2가지로 나뉘어 있다. CPU가 특정 메모리의 주소에서 값을 받아오기 위해서 사용하는 어드레스 라인 A20에 대한 이야기와 CR3레지스터에 저장되어 있는 페이지 디렉토리의 주소부터 시작되는 페이징에 대한 이야기이다. 지금은 약간 생소한 단어이지만 일단 뒤에서 설명할 테니 이런 것이 있구나!라는 느낌만 가지고 넘어가도록 하자.


A20이 무엇일까


  CPU가 8086이던 시절, CPU가 특정 메모리에 접근하기 위해서는 메모리의 주소를 이용해야 하는 것은 당연히 받아 들일 수 있을 것이다.  헌데, 이 메모리의 주소를 표현하는 어드레스 라인이라는 것이 20개밖에 없다. A0~A19까지 20개의 어드레스 라인이 있는데  A20이라 하면 실질적으로 21번째의 어드레스 라인이라는 것을 알 수 있다.  A20을 기준으로 해당 메모리가 지금 어드레스 라인이 20개를 사용하는  real mode인지 20개 이상을 사용하는  protected 모드인지를 판단할 수 있다. 

 이전 포스팅에서 설명한 real mode에서 주소지정 방식을 알아보자. 혹시 이해가 잘 안된다면 이 포스팅을 참조하면 되겠다.

 OS 만들기[4] - [1] Protected Mode로 가기 위한 16bit real mode

 16bit 주소가 [FFFF:0011]이라고 해보자. 앞에 16비트 0xFFFF는 왼족으로 4비트 쉬프트하여 0xFFFF0으로 된 후에 뒤의 16비트 00111과 합쳐진다. 그러면 0xFFFF0+0x0011이 더해져서 0x100001이 된다.  F부분들에 0x10이 더해져 차례대로 캐리가 발생해 결국 0x100000이 되는 것을 알 수 있다. 그러면 이 0x100000을 자세히 보면 어?! 총 21비트가 쓰이는 것을 알 수 있다. 그래 지금 1로 셋팅되어 있는 부분이 A20이라는 것을 알 수 있다. 그러면 아까 말했듯이 총 20개로 표현한 범위를 넘어섰다. 그렇다. 16비트 주소시정방식의 최대 표현범위는 0xFFFF:FFFF을 고려하여 0x10FFEF까지 가능하지만 A19까지 가능하므로 0x100000부터는 참조할 수가 없다.. OTL..  

  

▲ A20이 사용되지 않아서 1MB(100000)부분이상 초과하면 겹쳐지는 부분

 80286이 되면서 어드레스 라인이 24개가 되어 A20뿐만 아니라 A23까지 사용이 가능하다. 하지만 여전히 8086만을 지원하던 소프트웨어와 호환이 되어야 하는 것을 잊지 말아야 한다. 그러므로 1MB이상에서 0번지로 겹쳐지는 기존의 8086의 주소 지정 방법까지도 호환이 되어야 한다. 그래서 A20게이트가 사용되고 있는지 없는 지 확인하는 방법중 세가지를 아래에 보인다. 방법이 3가지더라도 결국에는 겹치는 부분을 체크하여 초과하는 부분의 값인지 확인하는 것이다.

1. 키보드 컨트롤러 8042

2. 소프트웨어 인터럽트 0x15를 사용하여 BIOS 함수 호출

3. I/O 포트를 이용하는 방법

  아래 사진은 지금까지 16비트 주소지정방식에 대해 말했던 [0xFFFF:0x11]에 0x1234를 저장한 후에 [0x0000:1]과 비교 하는 부분이다. A20이 없다면 아래 1MB 초과되는 메모리주소가 중복되어 0x1234가 저장되어 있을 것이기 때문이다. 아래 그림은 책에서 나오는 소스로 컴파일 했을 때 나오는 것이고, 당연히 내가 사용하는 VM은 protected모드를 지원했기 때문에 A20이 on되어 있는 것을 알 수 있다.

▲ A20 확인 소스(좌), A20 확인 결과 화면(우)

A20 참고


http://en.wikipedia.org/wiki/A20_line

http://anster.egloos.com/v/52607


페이징


 대망의 페이징을 설명하는 시간이 왔소이다. 페이징은 한 프로세스당 4GB만큼의 메모리를 제공하기 위해 자주 사용하지 않는 영역을 하드디스크에 저장하여 필요할 때 불러주는 메모리 관리 기법을 말한다. 메모리와 하드디스크 사이에서 스왑하는 정보가 pagefile.sys에 저장된다.이때 저장되는 기본 단위를 페이지라고 불리며 일반적으로 4KB로 저장된다. 또한 이 페이징은 세그멘테이션으로 인해 논리주소가 선형주소로 바뀐 것을 실제 물리주소로 매핑 시켜준다. 방금 내가 한 말이 이해가 잘 안되는 게 당연한 것이다. 먼저 논리주소에서 선형주소로, 선형주소에서 물리주소로 바뀌는 과정에 대해 알아보고 그와 관련된 사항을 보도록 하겠다. 일단 페이징을 쉽게 설명한 블로그가 있어 링크를 첨부하니 확인하시고 실제적으로 관련된 정보와 Flag들을 보면 더욱 이해가 쉽겠다. 

http://cappleblog.co.kr/247

 위 블로그는 메모리와 하드디스크사이에 저장공간을 교환하는 스왑에 초점을 맞춘 것이라면 이 포스팅은 앞서 말했듯이 실제적으로 메모리 주소가 어떻게 변환하는 지에 대해 설명한다. 실제적으로 메모리 주소가 변화하는 과정을 보면서 필자는 소름이 돋았다. 아.. 이게 이렇게 변하는 거였구나. 이래서 4GB를 표현할 수 있구나라는 것을 알게 됬다.

 운영체제에서 실행파일(윈도우 PE, 리눅스 ELF 등)을 실행시켜주는 역할을 로더가 한다. 윈도우기준으로 설명하자면, 윈도우는 flat memory model을 이용하고 이 모델은 코드 세그먼트와 데이터 세그먼트가 같은 base address와 limit을 가진다. 그래서 로더가 6개의 세그먼트 셀렉터를 이용하는 데 DS:[0]과 CS:[0]이 같은 곳을 가리킨다. 



왜 갑자기 세그먼트 셀렉터가 나오고 로더가 나오는 것일까? 그 이유는 아까전에 언급한 물리주소로 맵핑되기 전에 논리주소에서 선형주소 바뀌는 과정을 설명하기 위해서이다. 디버거(Ollydbg, Immunity debugger, ida pro 등)을 이용하여 동적분석을 할 때 6개의 세그먼트 셀렉터가 보일 것이다. 이 셀렉터를 이용하여 아래와 같이 논리주소를 이용하여 선형주소를 만든다. 계속 해서 설명하려고 했지만 이미 포스팅한 내용이고 페이징을 설명하는 목적의 포스팅이므로 나머지 아래 글에서 확인하도록 하자!

OS 만들기[4] - [2] Protected Mode로 변환과 GDT 세그먼트 레지스터


 위 글을 참고 했으면 논리주소에서 선형주소로 변경된 과정을 알 수 있다. DS:[7D000707]이라는 논리주소에서 0x10065와 같은 32bit의 선형주소로 변경된다.  이 32bit가 10bit, 10bit, 12bit단위로 나뉘어서 각 테이블의 오프셋으로 사용된다. 0x10065의 경우 아래와 같이 첫 10bit는 0으로 채워있고, 두번째 10bit는 00000 10000 으로 되어 있고 마지막 12bit는 0000 0100 0011인 것을 아래 그림을 통해 알 수 있다.


 여기서부터 순서대로 [베이스 주소]에 [오프셋]을 더하는 방법의 주소지정 방식이 계속 사용된다. 그 첫 걸음이 CR3 레지스터부터 시작된다. CR3 레지스터에는 해당 프로세스의 [페이지 디렉토리의 물리주소]를 가지고 있다. 페이지 디렉토리에서 [10bit의 오프셋] 더해서 [페이지 테이블의 주소]를 가져온다. 여기서는 첫 10bit의 값인 0을 의미한다. 그 후 다시 페이지 테이블의 주소에 두번째 [10bit의 오프셋]이 더해져 [실제 물리주소]를 가져오고 위에서 말했듯이 기본 페이지의 크기는 4KB이므로 이를 12bit를 이용하여 가리킨다. 풀어서 말한 것을 아래와 같이 정리하였고 밑의 그림도 참고하면서 보도록 하자.

베이스 주소 + 오프셋 방법 정리

CR3 레지스터 : 페이지 디렉토리의 물리주소 

페이지 디렉토리의 주소 + 첫 10bit offset : 페이지 테이블 주소

페이지 테이블의 주소 + 두번째 10bit offset : 4KB 한 페이지의 주소

페이지의 주소 + 마지막 12bit offset : 12bit를 이용하여 4KB내에서 원하는 주소 지정 가능


 페이지 디렉토리도 4KB이고 페이지 테이블도 4KB이다. 그런데 어떻게 10bit를 이용하여 이런 4KB에 접근이 가능할까? 이는 배열을 생각하면 쉽게 이해 될 수 있다. int형 배열 Arr이 아래와 같이 있다고 하자. Arr[2]에 접근하고 싶을 땐 우리는 그저 인덱스인 '2'를 이용한다 그러면 실제적으로 int는 4바이트 단위이므로 2*8 이 되어 [베이스 주소]인 0x100에 [오프셋 2*4 인 8]이 더해져 0x108을 구할 수 있다. 0,1,2인 3개의 값을 가지고 0x100 ~ 0x108까지 접근 가능하다. 이처럼 10bit를 가지고 표현할 수 있는 단위인 2^10이므로 접근하는 주소의 단위가 4바이트 이면 2^10 * 2^2 이므로 2^12인 4KB까지 접근 가능한 것이다.


 지금까지 페이징을 이용해서 논리주소에서 선형주소, 선형주소에서 물리주소로 변환하는 과정을 봤다. 하지만 아직 말하지 않은 부분이 있다. 페이지 디렉토리의 엔트리와 페이지 테이블의 엔트리에 대해서 언급하지 않았다. 여기서 참고할 만한 사항은 31와 12비트인 20개의 비트가 사용되냐는 것이다. 이는 지금까지 말한 [베이스 주소] + [오프셋]의 개념으로 봐도 된다. 위에서 배열을 말할 때 오프셋(인덱스)에 배열의 단위인 Int의 크기인 4만큼 곱하기를 했던 것 처럼 페이지의 단위인 4KB를 12bit를 곱한다는 것을 의미한다. 페이지 디렉토리도 4KB, 페이지 테이블도 4KB, 하나의 페이지도 4KB인 것을 차례대로 생각하면 된다. 





 그러면 여기서 사용되는 중요한 플래그들을 알아보도록 하자. 

[A]

커널프로그램이 사용하는 것으로 모든 엔트리의 이 A비트를 조사하여 최근에 접근한 페이지나 접근되지 않은 페이지를 찾아낼 수 있다. 페이지에 접근되면 CPU에 의해 자동으로 A비트가 1로 세트 되고, 접근하지 않으면 커널프로그램이 0으로 셋팅한다.

[U/S]

0으로 셋팅되어 있으면 페이지테이블, 4KB 물리페이지를 커널만이 사용가능 한 것이고, 1로 셋팅되어 있으면 유저도 사용가능한 것을 의미한다.

[R/W] 

0으로 셋팅되어 있으면 페이지 테입르이나 4KB 물리 페이지는 읽기만 가능하고 1로 셋팅되어 있으면 쓰기도 가능하다.

[P]

0으로 클리어 되어 있으면 페이지가 물리 주소상에 로드되어 있지 않다는 뜻이고, 1로 셋트되어 있으면 페이지가 물리 주소상에 로드되어 있다는 뜻이다. 이는 스왑되어 하드디스크에 저장되어 있는지 아닌지를 말한다.


마치면서


 페이징을 공부하면서 A20비트로 인해 16bit real mode에서 지원가능한 address line은 20개이며, 이 때문에 겹치는 부분이 발생한다는 것을 알 수 있었다. 그래서 A20이상의 address line이 사용되고 있는지 아닌 지를 확인해 보는 것을 알 수 있었다. 그 이후에 페이징을 통해 논리주소에서 선형주소, 선형주소에서 물리주소로 변환하는 과정을 볼 수 있었다. 뿐만 아니라 각 엔트리에 저장되어 있는 값이 모두 주소를 지정하는 데 쓰이지 않고 플래그들이 존재하며, 이 플래그들 중 하나는 하드디스크에 스왑되어 저장되어 있는지 아닌 지 여부등을 아는데 사용되는 것을 알 수 있었다.



cent OS 7과 win7 samba 설정


1. samba install


2. conf파일 설정


3. user 추가 및 기존의 user 사용


4. 방화벽 설정 ( 런타임인지, 영구적인지 확인해야 함)


5. selinux 설정


6. 네트워크 대역 및 워크그룹 확인


7. smbpasswd로 conf파일에 설정해둔 id 설정

예) share라는 아이디를 conf에서 사용한다고 설정해두었다면

smbpasswd share



'시스템' 카테고리의 다른 글

cent OS 7과 win7 사이 samba 설정  (0) 2014.10.01
GDB 및 쉘에서 "$()" 이용하기  (0) 2014.09.24



-실행 인자 값을 넣을 때gdb에서 파이썬 사용

r $(python -c 'print "A"*264')


-쉘에서 바이너리 헥스 덤프한 것을 다시 바이너리(.ko)로 만들기

echo -ne “$(cat [hex dump 파일])” > m.ko


'시스템' 카테고리의 다른 글

cent OS 7과 win7 사이 samba 설정  (0) 2014.10.01
GDB 및 쉘에서 "$()" 이용하기  (0) 2014.09.24



CPU가 Protected Mode에서 행하는 체크 포인트는 아래와 같다. 체크 포인트에서 합당하지 명령이나 상태가 나타나면 에러를 발생하여 조정한다.

  • Limit 체크
  • Type 체크
  • 특권 레벨 체크
  • 명령 세트 체크


CPU 체크 포인트


Limit 체크

세그먼트 디스크립터 내의 G 비트가 0일 떄에는 Limit을 0~0xFFFFF(1MB)까지 선택할 수 있으며, G비트가 1일 때는 0xFFF(4KB) ~ 0xFFFFFFFF(4GB)로 선택할 수 있다. 그러므로 접근하려는 위치가 이 G비트를 고려한 Limit값을 초과한다면 CPU는 예외[일반 보호 예외(#GP)]를 발생시킨다. 이렇게 Limit체크를 하여 잘못된 주소를 가리키는 포인터에서 에러를 검출 할 수 있다. 이 에러는 발생했을때 바로 나타나므로원인을 검출하기 쉽다. 이렇게 Limit을 체크하기 때문에 현재 세크먼트 외의 다른 세그먼트의 접근을 막을 수 있으며, 특히 현재 프로그램에 악영향을 미칠 수 있는 코드 세그먼트나 스택 세그먼트로의 잘못된 접근을 막을 수 있다. 

 이제까지 포스팅을 했던 GDT, IDT의 base address(32bit)와 limit(16bit)를 가지고 있는 GDTR, IDTR에서도 해당 테이블에 접근할 때 limit을 초과하는 지 확인한다. 그리고 아래 그림과 같이 TR(Task Register)의 Invisible Part에서 세그먼테이션의 limit을 체크한다. 공부하다보니 프로그램에 포함된 6개의 세그먼트 레지스터와 GDTR, IDTR, TR의 구조가 같다고 생각했는데, 오해였다. 세그먼테 레지스터에는 세그먼트 셀렉터와 세그먼트 디스크립터로 구성되고 GDTR, IDTR, TR에는 가리키는 주소의 base address와 limit로 구성된다. 특히 TR은 셀렉터까지 포함된다는 사실을 다시 한번 확인했다.


TR레지스터를 이용한 TSS접근

Type 체크

 세그먼트 디스크립터에서 사용되는 type은 해당 세그먼트가 코드인지 데이터인지를 나타낸다. 뿐만 아니라 읽기, 쓰기, 실행등의 정보도 포함하고 있다. 세그먼트의 속성이 코드인지 데이터인지 나타낼 때 디스크립터의 S비트는 1로 되어 있는데, 이 S 비트가 0이면 Type필드가 시스템 Type으로 사용되어 가리키는 세그먼트가 시스템 세그먼트가 된다. S비트가 1일 때는 코드와 데이터 세그먼트에 대한 정보가 있다면, S비트가 0일 때는 어떤 시스템 Type의 디스크립터로 쓰이는지 나타난다.

 Type

게이트 종류 

0x0 

예약됨 

0x1 

16비트 

0x2 

LDT 

0x3 

Busy 16비트 TSS 

0x4 

16비트 콜게이트 

0x5 

태스크 게이트 

0x6 

16비트 인터럽트 게이트 

0x7 

16비트 트랩 게이트 

0x8 

예약됨 

0x9 

32비트 TSS 

0xA 

예약됨 

0xB 

Busy 32비트 TSS 

0xC 

32비트 콜게이트

0xD 

예약됨 

0xE 

32비트 인터럽트 게이트 

0xF 

32비트 트랩 게이트 

▲ S비트가 0일 때 타입

아래의 경우가 Type을 체크하는 대표적인 예이다.

  • 세그먼트 셀렉터가 세그먼트 레지스터에 로드될 때 CS에는 코드 세그먼트의 셀렉터만이 로드될 수 있다.
  • 디스크립터가 세그먼트 레지스터에 이미 로드되어 있는 세그먼트에 명령이 엑세스할 때

코드 세그먼트 영역에는 데이터의 쓰기가 금지

읽기 전용 데이터 세그먼트에 데이터의 쓰기가 금지

코드 세그먼트 영역에 읽기 가능 플래그가 설정되지 않은 상태에서 이 영역을 읽어 들일 수 없음

  • CALL JMP 명령의 오퍼랜드에 셀렉터가 있을 때 그 셀렉터에 대한 디스크립터의 Type 필드 조사한다. 예를 들어 Task Switching에서 다른 Task로 이동하기 위해 사용했던 CALL, JMP등과 같은 명령어의 오퍼랜드를 확인한다. 이 오퍼랜드가 TSS용인지를 확인한다. 
  • IRET 명령이 나왔을 때 CPU는 NT비트가 0인지 1인지를 확인하여 인터럽트 처리인지, Task Switching인지 판단한다. NT 비트가 1이면 Task Switching이며, 이때 TSS 영역에서 "이전 태스크로의 백링크"가 TSS용인지를 체크한다. 
  • GDT의 맨 처음 디스크립터는 NULL 디스크립터라고 하였다. 이는 초기화를 할 때 사용한다고 했는데, CS나 SS로드 할 때 일반 보호 예외(#GP)가 발생하고 데이터 세그먼트인 DS, GS, ES, FS를 초기화할 때만 사용한다. 하지만 초기화할 때만 사용하는 것이지, 이 세그먼트 영역에 접근하려고 해도 일반 보호 예외(#GP)가 발생한다.

특권 레벨


 CPU의 특권 레벨은 0~3으로 4개가 있지만, 그 중 0인 커널 레벨과 3인 유저레벨이라는 2개의 권한만 사용된다. 이 2개의 권한이 CPL, DPL, RPL이라는 3가지로 디스크립터에 나타난다. 

CPL(Current Priviledge Level)

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

 DPL(Descriptor Priviledge Level)

디스크립터에 기재된 DPL의 값을 말한다. 디스크립터를 통한 세그먼트 영역으로 모든 접근에 항상 CPL과 DPL의 값이 체크한다. 

 RPL(Requested Priviledge Level)

앞서 설명한 DPL과 CPL만 있으면 된다고 생각 할 수 있다. 하지만 아직 고려하지 못한 경우가 남아있다!! 그것은 바로 낮은 특권 레벨(유저)에서 높은 특권 레벨(커널)의 루틴을 사용하게 하는 방법이다. 이를 콜게이트라 말한다. 이 콜게이트를 이용하면 일시적으로 특권 레벨 0으로 들어가기 때문에 특권 레벨 0의 데이터 영역에도 접근 가능하다. 하지만 이를 악용하여 특권 레벨 0의 데이터에 접근 할 수 있으므로, 특권레벨 3인 프로세스가 콜게이트를 이용하여 특정 루틴을 불렀는 경우에 데이터 세그먼트 셀렉터에 특권 레벨 3에서 불러졌다는 것을 표시한다. 이 표시가 있다면 특권 레벨 0의 데이터에는 접근하지 못한다. 콜게이트 이외의 대부분의 경우 RPL은 CPL과 같다고 보면 된다.


콜게이트

 낮은 특권 레벨의 프로그램이 실행 도중 높은 특권 레벨로 변경되는 수단은 대표적으로 인터럽트, 예외, 콜게이트가 있다. 이 중에서 하드웨어 인터럽트와 예외는 프로그램의 의지와 상관없이 특권 레벨로 변경이 이루어지고, 소프트웨어 인터럽트와 콜게이트는 자신의 의지에 의해 높은 특권 레벨의 루틴을 잠깐 사용하는 것이다.

 유저 프로그램이 하드웨어를 관리하기 위해서는 커널 레벨을 잠시 빌려서 사용하고 다시 반납해야 한다. 이는 1마치 은행창구에 있는 직원에게 내 계좌의 돈을 빼달라고 하는 것으로 보면 된다. 그러면 직원은 해당 계좌번호가 내 계좌번호 인지 확인하고, 돈을 빼준다. 이것은 유저프로그램이 콜게이트를 불러 커널 레벨의 권한을 받아 처리하는 것과 같다고 보면 된다 .

 아래 그림은 콜게이트 디스크립터이다. 위의 표[S비트가 0일 때 타입]에서 나타난 것과 같이 32비트 콜게이트는 0xC이고 아래 그림의 Type에 1100으로 셋팅되어 있는 것을 볼 수 있다. 그 뒤에 나타난 인수의 개수[0비트~4비트]인 ParamCount가 있다. 결국 Type과 Param Count를 제외한 부분들은 다른 디스크립터와 비슷하다는 것을 알 수 있다.

 이런 콜게이트는 GDT에 다른 세그먼트와 같이 등록되어 있다. 다시 한번 콜게이트에 대해 말하자면 낮은 특권 레벨의 프로그램이 ㄴ높은 특권 레벨의 프로그램의 일부분을 사용하기 위한 창고 세그먼트이다. 이 또한 유저프로그램에서 FAR JMP나 CALL을 이용하여 명령을 내려 접근하다.

▲Call Gate Descriptor

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

 일반적으로 아래 그림과 같이 특권 레벨 간의 JMP명령은 불가능하다. 하지만 CALL 명령은 특권 레벨 간의 이동을 가능하게 해주는데, 이 CALL명령은 항상 낮은 특권 레벨에서 높은 특권 레벨에 대하여 이루어져야 하고, RET 명령은 높은 특권 레벨에서 낮은 특권 레벨에 대하여 이루어져야 한다. 특권 레벨 0의 코드특권 레벨 3의 코드를 불러낼 이유는 없기 때문이다. 그리고 만약 특권 레벨 간의 JMP 명령을 굳이 사용해야 한다면 콜게이트를 통하여 이루어지도록 한다. 


 특권 레벨 3에서 CALL 명령으로 특권 레벨 0의 루틴을 불러 내게 되면 CS 셀렉터의 0,1 비트에 00이 들어간다. 세그먼트 셀렉터의 가장 하위 2비트(0,1비트)에는 DPL이 있고, DPL은 권한을 표현하므로 00은 현재 셀렉터의 특권 레벨이 커널 레벨이라는 것을 의미한다. 그 후 RET 통해 다시 CALL 명령을 내린 코드로 온다면 코드 셀렉터의 0,1비트가 11로 바뀐다. 이런 규칙의 하나의 예외가 있는데 Conforming 세그먼트인 경우이다. 세그먼트 디스크립터에서 TYPE 비트에서 Conforming이 나온 적이 있었다. 이 Conforming 세그먼트는 콜게이트와 관련 있는데 위에서 콜게이트 디스크립터에 대해서만 이야기 했으므로 추후 이에 대해 포스팅 하도록 하겠다.

 특권 레벨 0에서는 어느 특권 레벨이든 모든 특권 레벨이 데이터 세그먼트 영역에 접근 가능하다. 커널 특권 레벨이기 때문이다. 하지만 낮은 특권 레벨의 프로세스에서 높은 특권 레벨의 데이터에 접근이 일어나면 CPU에서 폴트가 발생한다. 이를 통해 높은 레벨의 세그먼트로의 접근을 보호할 수 있다. 그러면 어쩔 수 없이 높은 레벨의 데이터에 접근해야 되는 경우에는 어떻게 할까? 높은 레벨의 데이터에 접근하기 위해서 CALL명령으로 높은 특권 레벨의 루틴을 불러내서 그 루틴에서 정해진 방법으로 데이터를 처리하고 RET로 돌아오는 방법밖에 없다. 이렇게 데이터에 접근하기 위해 높은 레벨의 루틴을 빌려 쓰는 방식을 구분하기 위해 RPL을 두고 체크한다.

 

특권 레벨 변동 시의 스택의 변화


 태스크가 실행 중인 환경에서 특권 레벨의 변동은 인터럽트나 예외가 발생하거나 콜게이트를 거칠 때 생긴다. 이런 특권 레벨의 변동할 때 스택의 변화는 높은 특권 레벨에서의 스택 부족으로 인한 크래쉬가 되지 않게 하는 것과 낮은 레벨의 스택의 조작으로 높은 레벨의 루틴에 영향을 주지 않게 하기 위함이다. 

CALL 명령이 내려졌을 때의 스택

 낮은 레벨에서 높은 레벨을 부르는 것이므로 CPU는 TSS의 SS0, ESP0의 값을 CPU의 SS, ESP레지스터에 복사한 후 SS, ESP, CS, EIP를 차례대로 PUSH한다. 앞에 2부분(SS, ESP)은 낮은 레벨의 스택 관련 레지스터이고, 뒤에 2부분(CS, EIP)은 높은 레벨의 스택 관련 레지스터다. 그 후에 CPU는 콜게이트에 지정된 주소로 점프하고 실행한다. 루틴을 마친 후에 호출하기 전에 저장했던 SS, ESP, CS, EIP를 차례대로 POP한다. 이때 순서대로 POP하므로 CS와 EIP가 먼저 POP 되는데, 이 CS를 체크하여 낮은 레벨인 경우에 SS, ESP를 POP한다. 같은 레벨(높은 레벨)이라면 같은 스택공간을 사용하기 때문에 POP할 필요없기 때문이다.

 낮은 레벨에서 높은 레벨을 호출할 때 파라미터가 있다면 어떻게 될까? PUSH되는 것은 SS, ESP, CS, EIP이므로 파라미터에 관한 정보가 없을 뿐더러, 이런 파라미터는 ESP, SS를 참조하여 가져와야 할 것이다. 하지만 만약 CPU가 파라미터가 몇개인지 몰라서 지정된 파라미터보다 더 많은 값을 POP할 수도 있다. 그러므로 이를 방지하기 위헤 콜게이트 디스크립터에 파라미터가 몇개인지에 대한 정보가 담겨있다. 그래서 이를 이용해서 낮은 레벨의 스택에서 2개의 파라미터를 높은 레벨의 스택에 복사한다. 

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

콜게이트와 인터럽트나 예외가 발생했을 떄의 차이는 EFLAG의 PUSH/POP하느냐 이다. EFLAG 의 사용하는 하나의 예로 EFLAG의 NT를 참고하여 인터럽트 발생인지 Task Switching인지 판단하는 것을 이전 포스팅에서 확인할 수 있다. 인터럽트나 예외는 낮은 레벨과 높은 레벨의 두가지 경우에서 모두 일어 날 수 있으므로 현재 레벨을 확인하여 SS와 ESP를 PUSH/POP할 것인지를 결정한다.

마치면서


 낮은 레벨과 높은 레벨, 유저 레벨과 커널레벨이 같은 것인데, 공부하는데 헷갈리게 혼용되는 것 같다. 어차피 ring 0과 ring3을 사용한다고 생각하면 되는데 말이다. 내가 알지 못한 곳에 ring1과 ring2가 쓰일려나 모르겠다. 지금까지 특정 세그먼트에 대한 접근과 특권 레벨 변동이 발생할 때 생각해야 할 보호에 대해서 알아봤다. 세그먼트 디스크립터에서 DPL, CPL, RPL과 limit을 체크하는 것과 특권 레벨 변동 할 때 SS, ESP, CS, EIP를 유지하기 위해 TSS의 SS0, ESP0 부분에 PUSH/POP을 했다. 특권 레벨 변동에서 알야야할 부분은 낮은 권한이 높은 권한의 데이터를 접근하기 위해서는 오직 콜게이트를 이용하여 높은 권한의 데이터에 접근하는 것이다.




커널 시스템 공부를 하다가 다시 한번 메모리에 대해 보게 되었다.

논리 주소 -> 선형 주소 -> 물리 주소의 변환과정을 다시 한번보면서 머리속으로 정리하는데

부트킷을 만들어서 MBR에서 조작하면 어떨까 하는 생각이 들었다. 이 조작을 통해 GDT를 변경하면 시스템에 막대한 영을 미칠 수 있지 않을까? 라는 생각만 했고, 실제적으로 GDT에 어떤 것이 들어가 있는 지에 대해 생각하지 못했다. 

 그런데 [리눅스 커널과 디바이스 드라이버 실습]책을 보다가 리눅스의 GDT를 봐서 신기했다. 오오오!!


▲리눅스 크로스 레퍼런스 참조한 GDT


실제로 적용되어 있는지 우분투14.04에서도 똑같이 되어 있는 것을 확인할 수 있었다.

find 명령어로 찾은 뒤에 x86의 segment.h를 확인했다.






http://www.kandroid.org/board/board.php?board=linux&command=body&no=2



 일반적으로 JMP명령어는 EIP를 바꿔주기 위해 사용한다. 물론 CALL, RET로도 가능하지만 여기서는 JMP만을 보도록 하겠다. JMP명령어가 특정 위치로 이동하여 코드로 이동하는 것이므로 엄밀하게 따지면 현재 실행되는 코드의 EIP를 바꾸는 것으므로 MOV EIP, [점프할 위치]다.

 이러한 JMP는 NEAR JMP와 FAR JMP으로 나뉜다. 

NEAR JMP라 함은 세그먼트의 limit보다 작은 범위내에서의 JMP를 의미한다. 

FAR JMP는 [세그먼트 인덱스:0]의 형식으로 다른 세그먼트로 JMP를 의미한다.

 처음에 JMP로 태스크 스위칭하는 것을 공부 할때 "그냥 세그먼트 인덱스로 JMP하네"라고 생각했는데 자세히 보니 FAR JMP로 태스크 스위칭하는 것을 알 수 있었다. 그래서 해당 세그먼트로 이동 할 테니 세그먼트 인덱스 뒤에 따라오는 오프셋은 상관 없다는 것을 알 수 있었다.




Task Swithching


 하나의 프로그램만 실행할 수 있었던 시절이 있었다. 그때는 CPU를 모두 사용하지 않더라도 하나의 프로그램이 끝나야지 다른 것들을 할 수 있었다. 얼마나 비효율적인가? 그래서 CPU의 시간을 잘게 쪼게어 돌아가면서 여러 프로그램들을 실행시켰다. 여기서 프로그램들(Task)간의 변경을 Task Switching이라고 한다. 이미 지금의 컴퓨터는 여러 프로그램들을 실행시킬 수 있으며, 그 단적인 예로 인터넷 서핑을 하면서 음악을 듣는 것이다.

 32비트 Protected mode에서 CPU레벨의 Task Switching을 지원한다. Task Swiching이 일어나려면 이전 태스크가 가지고 있는 정보들을 모두 저장시켜놓아야 한다. 만약 Task의 정보를 저장하지 않고 새 Task로 변경한다면 새 Task의 실행이 끝났을 때 정보가 없어서 이전 Task를 실행 할 수 없다. Task Switching을 위해 Task 관련 정보들이 저장되는 곳을 TSS(Task State Segment)라고 한다.

 이번 포스팅에서는 TSS에 정보를 저장하면서 JMP와 CALL라는 2가지 방법의 Task Switching에 대해 알아볼 것이다. JMP와 CALL의 Task Switching의 차이점과 그에 따라 EFLAG의 NT/B비트와의 상관관계에 대해 알아보도록 하겠다. 또, Task Switching을 하면서 저장되는 정보 중에 특히 스택에 대해 유념하면서 알아보도록 하자.

TSS(Task State Segment)


 TSS는 위에서 말했듯이 Task Switching중에 이전 Task에 대한 정보를 저장하는 곳이라고 했다. 아래 그림을 참고하자. 크게 다양한 레지스터와 각 권한에 대한 스택정보(ESP, SP)가 포함되어 있다.

▲ 32bit Task State Segment(TSS)

좀더 자세히 보도록 하자. 인텔 메뉴얼에서는 TSS에 들어가는 정보를 크게 2가지로 나누었다. 

먼저 Static Field부터 정리하도록 하겠다.

당연히 Static이라는 것은 정적인 것을 뜻하므로 변하지 않는 것을 알 수 있다. LDT는 해당 Task가 가지고 있는 Local Descriptor Table이고, 이 안에는 GDT처럼 여러 세그먼트에 대한 정보가 들어있다. CR3레지스터에는 해당 Task가 가지고 있는 페이지 디렉토리의 주소를 들어있다. Priviledge level stack pointer field에는 각 권한에 따른 스택정보가 들어가 있다. 다음에 포스팅 할 이야기지만, 다른 레벨간의 Task Switching에서는 각 스택들간의 정보가 겹쳐지지 않도록  하는 것이 중요하다. 만약 그렇지 않는다면(정보가 겹쳐지거나 접근이 가능하게 된다면) 유저모드에서 커널모드의 정보를 수정할 수 있게 되는 것이다. T비트는 해당 Task가 디버깅되고 있다면 1로 set된다. I/O map은 I/O permission bit map, interrupt redirection bitmap에 대한 정보가 있다고 한다.

남은 Dynamic Field는 아래와 같다. 

 당연히 동적으로 변하는 것을 의미하며 기계어의 프로그래밍된 코드에 영향을 받아 변하는 부분이라고 생각하면 되겠다. 어셈블리어로 EAX~EDX등과 같이 General-purpose register에 들어가는 부분들은 단순히 MOV, ADD등의 명령어로 값을 변경할 수 있다. 하나의 Task에서 가지고 있는 6종류의 세그먼트 셀렉터(CS, SS, DS, ES, GS, FS) 에 대한 정보가 Segment Selector에 들어간다. EFLAG에서는 CPU 상태나 제어관련된 부분에 대한 정보를 가지고 있다. EIP는 현재 실행되고 있는 명령어의 주소를 가리키고 있다. Previous task link field는 CALL명령으로 태스크 스위칭 한 후에 IRET로 이전 태스크로 돌아간다. 그때 이 부분을 참조하여 이전 태스크로 이동한다.(JMP에서는 직접 주소를 지정하여 이동하여 이 부분을 참조하지 않음)

  • Static Field
    • LDT segment selector field 
    • CR3 control register field
    • Privilege level-0, -1, and -2 stack pointer fields
    • T (debug trap) flag (byte 100, bit 0) 
    • I/O map base address field
  • Dynamic Field
    • General-purpose register fields
    • Segment selector fields
    • EFLAGS register field
    • EIP (instruction pointer) field 
    • Previous task link field


구조를 봤으니 실제로 어떤 식으로 코딩하여 메모리에 넣는지 알아보도록 하자. 아래 그림은 NASM로 TSS을 만든 소스이다. 위의 구조와 다르게 낮은 주소부터 순서대로 값을 넣기 때문에 구조가 반대인 것을 유념하면서 보면 되겠다. 

▲TSS 코딩


TSS 디스크립터


 인터럽트에서 인터럽트 디스크립터가 있었다면 테스크 스위칭의 부분에서는 TSS 디스크립터가 있다. TYPE에서 B부분만 다르고 나머지 부분들은 세그먼트 디스크립터와 동일하므로 생략하도록 하겠다. TYPE에서 2번쨰 비트는 B비트로 busy를 의미한다. busy는 Task Switching이라는 프로세스에서 Switching하기 전의 Task인지 Switching 한 후의 Task인지 판단하는 근거가 된다. 이는 JMP와 CALL을 이용한 Task Switching에서 쓰이는데 아래에서 설명하도록 하겟다.


JMP를 이용한 Task Switching


 도식화를 통해 JMP를 이용한 Task Switching에 대해 알아보도록 하자. Task Switching에서 CALL과 JMP의 차이는 CALL은 IRET를 이용하여 이전 Task로 복귀하는 것이고 JMP는 이동한 Task로 JMP한다. 지금은 JMP를 보는 것이니 아래 그림을 참고 하도록 하자.

1. JMP를 통해 TSS2로 이동하라는 명령이 내려짐

- TSS2Selector는 세그먼트 셀렉터로 GDT에 인덱스로 저장되어 있다.

- 또한 TSS2Selector:0은 세그먼트:오프셋의 형식으로 FAR JMP 명령어를 의미

2. TSS2로 이동 전에 현재 Task의 정보를 TR(Task Register)이 가리키는 TSS1 Selector를 찾음

3. TSS1 Selector의 인덱스 값을 이용하여 GDT에서 Base Address를 찾음

4. TSS1에 현재 현재 Task의 정보를 저장.    Task2로 점프 하면서 Task1의 B비트가 0으로 변경

5. 아까 내린 JMP명령을 수행하기 위해 해당 세그먼트 셀렉터인 TSS1 selector를 GDT에서 찾음

6. base address를 참조하여 TSS2의 정보를 찾음

7. TSS2의 정보를 CPU에 옮김. TSS의 세그먼트에 있는 B비트는 1이 되고 EFLAG의 NT는 0이 됨

8. TSS2에 있는 특정 루틴을 실행

▲JMP를 이용한 Task Switching

이론을 봤으니까 이제 소스를 보도록 하자. 아래의 소스는 JMP를 이용한 Task Switching 소스이다. ax에 기존의 Task의 TSSselector 값을 넣는다. 그리고 ax에 그 값을 넣는다. 이 ax를 ltr명령을 이용하여 tr레지스터에 TSS1Selector의 값을 넣는다. 이를 통해 Task Switching이 발생했을 때 저장할 TSS를 가리킨다. 그 후에 이동할 TSS2의 값들을 설정해 준다. 여기서는 eip와 esp를 설정해 주는 것을 알 수 있다. TSS2로 가면 Process2 부분으로 가는 것을 알 수 있는데, 오른쪽 그림에서 process2부분은 printf 함수를 이용하여 msg_process2를 화면에 출력해주는 역할을 한다. edi는 msg_process2가 출력 될 화면의 좌표이다. 그 후 다시 돌아와서 msg_process1을 출력해 주는 것을 알 수 있다. 

 정리해보면 이동하기 전에 TSS1Selector를 TR에 지정하여 Task Switching이 일어났을 때 저장할 공간을 마련해 두고 TSS2로 점프한다. 그러면 점프하기 전의 상태를 TSS1에 저장하고 TSS2Selector에 있는 값을 CPU로 가져와서 process2로 점프한다. process2가 끝나고 마지막에 다시 JMP TSS1Selector로 이동한다. 이때 아까 저장했던 값을들 가져와서 다시 실행하게 된다. 

▲JMP를 이용한 Task Switching 소스


▲TSS1과 TSS2에서 출력하는 메시지


CALL을 이용한 Task Switching


 JMP와 CALL과의 소스에서 다른 점은 CALL이 끝나고 난 뒤에 IRET로 원래의 task로 돌아가는 것이다. 그리고 실행 중 변경되는 부분은 Task Descriptor의 B 플래그뿐만 아니라 EFLAG의 NT비트 또한 변경된다. NT비트는 IRET가 task로 돌아가기 위해 인터럽트가 발생했는 지 CALL에 의해 불려졌는 지를 판단한다. NT비트가 1로 되어 있으면 CALL로 불려진 것이고, 0으로 되어 있으면 인터럽트가 발생한 것이다. 뿐만 아니라 JMP와 달리 Task Switching이 일어나기 전의 Task인 B비트가 1로 셋팅된다. Task Switching으로 인한 흐름은 JMP와 다른 것이 없으므로 비트의 차이에 대해 보이도록 하겠다. 




▲CALL로 발생하는 Task Switching


 위의 보라색 화살표는 CALL로 인해 다른 Task를 부르면서 그에 따라 변하는 비트를 보여준다. 그림에서 보듯이 CALL로 불려지는 Task는 B비트와 NT비트가 1이 된다. 그 후 B비트가 1인 Task2를 Task3을 부르는 것이 안되는 것을 볼 수 있다. Task3에서 Task2으로 가려면 IRET로 갈 수 있고, 이를 통해 B비트와 NT비트가 0이 되는 것을 볼 수 있다. 
















인터럽트란 무엇인가?


 인터럽트가 무엇일까? 우리가 컴퓨터를 할 때를 생각해보자. 컴퓨터로 영화를 보고 있다. 영화를 보다가 소리를 더 올리기 위해 마우스로 볼륨을 올렸다. 컴퓨터에서 명령어를 처리하는 CPU는 열심히 영화파일을 읽어서 열심히 모니터에 영화 화면을 전송시켜주고 있다가, 마우스가 움직인다. 그러면 마우스 움직임도 처리를 해줘야 될텐데, 이때 마우스의 움직임을 CPU에게 보내주는게 인터럽트이다. 뿐만 아니라 타이머, 키보드등 여러가지 인터럽트가 많다. 아래를 참고하자. 그래서 인터럽트가 있어야 CPU가 어떤 일을 하고 있더라도 급한 일이 있으면 도중에 멈추고 급한 일을 처리할 수 있다.

마이크로프로세서에서 인터럽트(interrupt, 문화어: 중단, 새치기)란 마이크로프로세서(CPU)가 프로그램을 실행하고 있을 때, 입출력 하드웨어 등의 장치나 또는 예외상황이 발생하여 처리가 필요할 경우에 마이크로프로세서에게 알려 처리할 수 있도록 하는 것을 말한다. 폴링이 대상을 주기적으로 감시하여 상황이 발생하면 해당처리 루틴을 실행해 처리한다면, 인터럽트는 상대가 마이크로프로세서에게 일을 처리해 달라고 요청하는 수단이다. 따라서 폴링과 대비대는 개념이다.

- 출처 : 위키백과


이 포스팅(OS의 인터럽트)에서 배울 것은 무엇인가?


 인터럽트가 발생했을 때 인터럽트 처리 루틴(ISR: Interrupt Service Routine)을 실행하기 위해서 IDT(Interrupt Descriptor Table)에 ISR의 주소와 관련된 정보를 저장한다. 뿐만 아니라 PIC(Programmable Interface Controller)에서 발생하는 하드웨어 인터럽트를 메인보드에 저장된 예외와 충돌나지 않게 매핑시키는 방법에 대해서 설명할 것이다. 만약 PIC에서 충돌나지 않는 설정을 하지 않는다면 다른 인터럽트가 같은 인터럽트 처리 루틴을 호출할 수 있기 때문에 이 설정은 꼭 필요하다. ISR은 하나의 코드(프로그램)이라고 봐서 이 또한 GDT에 저장되어 있으므로 IDT에서 ISR의 주소(오프셋)을 찾고 다시 GDT에서 해당 주소를 찾아 실행하는 큰 그림을 그리면 되겠다.

 이를 통해 타이머 인터럽트, 키보드 인터럽트, 예외( 0으로 나눈 예외)를 발생시켜 본다.


하드웨어 인터럽트, 소프트웨어 인터럽트, 예외


 IDT에서 처리하는 것은 인터럽트 뿐만 아니라 예외도 포함이 된다. 각각에 대한 설명은 아래와 같다.

하드웨어 인터럽트는 프로그램 실행중에 하드웨어로부터 받는 신호이이다.

소프트웨어 인터럽트는 INT N 명령어로 소프트웨어에서 만들어서 보내는 신호이다.

예외는 프로그램 실행중에 특정 조건에 만족하는 상황에 발생하는 것이다.

 이러한 예외와 인터럽트가 발생했을 떄 어떤 행동들을 해야 할지 IDT에 디스크립터로 저장되어 있다. 이처럼 IDT에 순서대로 저장이 되어 있을 것인데 메인보드에 저장된 예외와 CPU에 연결된 PIC의 하드웨어인터럽트가 각자 0부터 저장되어 있기 때문에 이대로 IDT가 만들어진다면 충돌이 나게 된다. 그래서 프로그래밍이 가능한 PIC에 명령어를 날려서 오프셋을 수정하여 충돌이 나지 않도록 해야한다. 하드웨어의 문제를 소프트웨어(어셈블리어)로 해결해야 한다.

PIC(Programmable Interrupt Controller)


 PC의 메인보드에는 8259A라는 이름의 칩이 있다. 이 칩이 메인보드 전체의 하드웨어 인터럽트를 관리하는데 이를 PIC라고 부른다. 요즘에는 칩이 있는 것이 아니라 메인보드의 칩셋에 로직화되어 포함되어 있다. 칩셋은 메인보드에 연결되는 장치들을 관리해주는 칩이라고 생각하면 쉬울 것 같다. 다시 말하면, 메인보드내에 관리해주는 칩이 CPU에 보내는 인터럽트와 예외도 처리해주는 것이다.

 이 PIC가 칩셋에 로직화 되기전의 모습은 아래와 같다. 각 PIC는 8개의 IRQ(Interrupt Request Line의 약자, 각 단어의 첫 글자로 줄인 것이 아니다.)핀을 가지고 있고, 마스터/슬레이브 형식으로 한 CPU를 기준으로 한 컨트롤러가 연결되어 있고 그 컨트롤에 다시 또 다른 하나의 컨트롤러가 연결되어 있다. 그리고 이 연결은 INT라는 핀으로 이루어져 있는데, 아래에서 빨간색으로 표시된 부분이 INT핀이다. 슬레이브의 PIC의 INT핀은 마스터 슬레이브의 2번 IRQ핀에 연결되어 있고, 마스터 PIC의 INT핀은 CPU에 연결되어 있다. 만약 슬레이브 PIC에서 INT가 발생한다면 마스터 PIC를 거쳐 CPU로 신호를 주게 된다.


▲CPU와 마스터/슬레이브 PIC - 참조

 각 IRQ핀에 매핑된 인터럽트는 아래 표와 같다. 본 포스팅에서는 타이머와 키보드 인터럽트를 이용하여 실습을 할 것이다. 아래 표에서는 IRQ번호 순서대로 나타나지 않았다는 것을 기억하길 바란다. 그리고 참고로 메인보드에 설정된 예외테이블을 아래에 두었다. 아래에서 다시 설명할 것이지만 CPU에게 PIC는 DATA 버스를 통해서 몇번쨰 인터럽트가 발생했는 지를 보낸다. 위 그림에서 파란색 라인이라고 보면 되겠다. IDT에서는 예외와 인터럽트에 대해 같이 처리를 해주므로 만약 CPU입장에서는 timer인터럽트와 Devide by Zero가 똑같이 Data 버스를 통해 0을 받게 된다. 그래서 프로그래밍 가능한 (Programmable) PIC에서 발생한 인터럽트 값에 특정 값을 더해 충돌이 나지 않도록 한다.


▲Hardware Interrupt Table

 

▲CPU Exception Table]

인터럽트 발생했을 때 PIC의 동작


 지금까지 PIC가 어떻게 생겼고 CPU에 어떻게 연결되어 있는지 보았다. 아까부터 PIC에서 발생하는 인터럽트와 예외가 중복되어 이를 처리해줘야 한다고 했다. 이는 PIC Flag 설정하기 부분에서 설명할 것이다. 그 전에!! PIC에서 인터럽트가 발생되었을 때 어떤식으로 CPU와 동작하는 지에 대해 집고 넘어가도록 한다.

 PIC 동작에 대해 확실하게 설명하기 위해 위에 첨부한 그림에 왼쪽에 빨강색으로 /INTA 선을 넣었다. 오른쪽에 있는 파란선은 DATA 버스 선이다. 마스터 PIC에서 인터럽트 발생하는 경우와 슬레이브 PIC에서 인터럽트 발생하는 경우로 나뉜다.

마스터 PIC에서 인터럽트 발생

  • 마스터 PIC는 자신의 INT핀에 신호를 실어 CPU의 INT 핀에 신호를 준다.

  • CPU는 EFLAG 레지스터의 IE 비트가 1로 세트되어 인터럽트를 받을 수 있는 상황이면 /INTA를 통해 마스터 PIC에게 신호를 보내 인터럽트를 잘 받았다고 신호를 보낸다.

  • 마스터 PIC는 /INTA 신호를 받으면 몇 번째 IRQ에 연결된 장치에서 인터럽트가 발생했는지를 숫자로 DATA 버스를 통해 CPU로 보낸다.( 이 경우 숫자는 0~7)

  • CPU는 이 데이터를 참고하여 Protected Mode로 실행 중이라면 IDT에서 그 번호에 맞는 디스크립터를 찾아 인터럽트 핸들러를 실행

슬레이브 PIC에서 인터럽트 발생

  • 슬레이브 PIC는 자신의 INT 핀에 신호를 실어 마스터 PIC의 IRQ 2번 핀에 인터럽트 신호를 보낸다

  • 마스터 PIC는 자신의 IRQ 핀에서 인터럽트가 발생했으므로 자신의 INT핀에 신호를 실어 CPU에게 보낸다.

  • CPU가 /INTA 신호를 줘서 잘 받았다는 것을 알면 DATA 버스에 숫자를 실어 CPU에게 몇번 째 IRQ에서 인터럽트가 발생 했는지 알림( 이 경우 숫자는 8~15)

  • 마스터 PIC의 경우와 같이 CPU가 데이터를 참고하여 인터럽트 핸들러 실행


▲ IRQ, /INTA, DATA를 이용한 CPU와 PIC의 인터럽트 동작

PIC 프로그래밍? PIC Flag 설정하기!


 처음엔 칩을 프로그래밍해야 된다고 해서 괜시리 겁을 먹었다. 응? 칩을 프로그래밍 해본 적은 없는데? 어려울까? 라는 생각이 들었다. 어셈블리어 코드를 보니깐 특정 레지스터에 값을 넣고 OUT 명령어를 통해 값을 전달하는 것 밖에 하지 않았다. 그러므로 전달하는 값이 무엇인지, 어떤 의미를 가지는 지에 안다면 프로그래밍을 이해한 것이나 다름없다. 내가 보기엔 프로그래밍이라고 하기에는 거창하고 설정이라고 하는게 맞을것 같다.

 이 프로그래밍을 FLAG를 설정한다고 표현하였다. 아래 그림과 같이 ICW(Initialization Command Word)라는 2Byte의 데이터를 포트 0x20/0x21번과 0xA0/0xA1번에 OUT 명령어로 신호를 보내 설정한다. 왜 프로그래밍보다 설정에 가깝다고 이야기 했는지 이해가 될 것이다.  이러한 데이터가 4개가 있어 ICW1~4를 설정해줘야 한다.


▲ICW1~4 셋팅에 따른 값

 위 그림을 참고하며 셋팅하면 되겠지만, 진짜 설정에 필요햇던 것들을 쉽게 정리했다. ICW1~4까지 셋팅해주며 ICW3 같은 경우에는 마스터PIC와 슬레이브PIC를 모두 설정해줘야 한다. 간략하게 각 ICW에 대해 정리하자면 ICW1은 인터럽트 신호 발생 인정 기준과 ICW4 사용여부에 대해서 설정하는 것이도 ICW2는 지금까지 그토록 강조해왔던 예외와 충돌나지 않게 하기 위해 얼마를 더해 줄지 정하는 부분이다. ICW3는 마스터 PIC, 슬레이브 PIC의 연결 방법에 대해 정하고, ICW4는 인터럽트 reset 시기 기준과 8086을 사용할지에 대한 것을 말한다.


    

▲ICW1(왼), ICW2(우)

[ICW1]

  • 7~4비트는 0001로 사용

  • 1로 설정되어 있으면 레벨 트리거 모드, 0으로 클리어 되어 있으면 엣지트리거 모드이다. 레벨 트리거는 특정 시점에 있을 때 event가 발생하는 것이고, 엣지 트리거는 신호의 변화가 있을때 event가 발생하는 것이다. 

  • PIC가 마스터/ 슬레이브로 구성되어 있는지 여부를 판단한다. 0이면 마스터/슬레이브 형식이고, 1이면 마스터 하나만 사용한다.

[ICW2]

  • IRQ의 번호에 얼마를 더해 줄지에 대해 값을 나타내는 것이고, 비트7~비트3 까지 사용하므로 최소 0x8을 더해 준다는 것을 알 수 있다.

    

ICW3 - 마스터 PIC(왼), ICW3 - 슬레이브  PIC(우)

[ICW3 마스터]

  • 어떤 IRQ 핀이 슬레이브 PIC와 연결되어 있는지 0~7번 위치 중에 1로 설정하여 표시한다.

[ICW3 슬레이브]

  • 0~2번 비트를 이용하여 8을 표현하여 마스터 PIC의 몇번째 IRQ 핀에 연결되어 있는지 표시한다. 일단 정한거긴 하지만 마스터는 각 비트에 맵핑하고 슬레이브는 0~2비트를 이용하여 8로 표현했는지 모르겠다.

ICW4

 [ICW4 마스터]

  • 7~5번 비트는 0으로 설정

  • 4~2번 비트는 각각 SFNM, BUF, M/S일 때 1로 설정되는데 이 포스팅에서는 사용하지 않아서 0으로 설정하겠다.

  • 1번 비트인 AEOI는 PIC reset을 자동으로 할 것인지, 수동으로 할 것인지 정하는 것이다. 리셋을 해야 다른 인터럽트를 받아들이는대 자동으로 리셋을 알지 수동으로 인터럽트 처리 루틴이 끝난 후 핸들러에서 PIC에 명령을 보내는지 정하는 것이다.

  • uPM는 비트에 0을 넣으면 MCS-80/85로 비트에 1을 넣으면 8086모드로 움직인다.


PIC를 설정하는 것을 소스(.asm)으로 보도록 하겠다. 처음 마스터 ICW1은 out 0x20, 슬레이브 ICW2는 out 0xA0으로 초기화를 하고 그 후 ICW2 ~ ICW4까지 0x21과 0xA1으로 초기화한다. 또한 0x00eb가 Jmp $+2라는 것으로 현재 위치에서 +2만큼 이동하는 것으로 다음 0x00eb로 갔다가 다시 out을 하는 순서를 취하고 있다.


▲PIC 설정 소스

IDT 설정


 지금까지 PIC 설정을 통해 메인보드의 예외부분과 하드웨어 인터럽트의 충돌을 막는 것을 봤다. PIC를 설정해서 ICW2에 0x20h를 넣어 증가시킨 값을 적용하는 것이 아래 표를 보면 이해가 갈 것이다. 0 ~ 31비트까지 32(0x20)개의 인터럽트를 예약해두었기 때문에 그 이후부터 추가하기 위해 0x20을 쓴다.

▲예약된 IDT 테이블 값들(예외, 인터럽트)

 이러한 IDT는 하나의 디스크립터가 256개로 이루어져 있다. IDT는 디스크립터가 딱 256개이다. 동적으로 처리 되는것이 아니라 예상되는 예외와 인터럽트를 처리하는 것이므로 256개가 정해져 있는 것이다. GDT에서 세그먼트 디스크립터로 사용한 것과 같은 크기(8Byte)의 디스크립터를 사용한다.  IDT에는 Task Gate와 Interrupt Gate,Trap Gate가 포함되지만 본 포스팅에서는 Interrupt Gate만 보도록 하겠다. 


▲인터럽트 디스크립터

 인터럽트 디스크립터에서는 세그먼트 셀렉터와 오프셋과 약간의 정보가 들어가 있다. DPL은 핸들러가 실행될 특권 레벨을 지정하는 부분인데 항상 커널 모드에서 동작하므로 00으로 설정한다. D비트는 1로 설정하면 현재 32bit에서 돌아가는 것이고 0으로 설정하면 16bit에서 돌아가는 것이다. P비트는 RAM 상에 존재하는 지에 대한 여부를 나타내는 것이므로 존재한다는 뜻인 1로 지정하면 되겠다.

대략적인 개념을 봤으니 이제 nasm으로 IDT를 설정하는 코드를 보도록 하자.


▲인터럽트 디스크립터

[mov ax, 256

 mov edi, 0 ]

이것은 IDT테이블에서 디스크립터의 수가 256개이므로 ax을 256으로 초기화하고 edi를 0으로 초기화한다.

[loop_idt:

lea esi, [idt_ignore]

mov cx, 8

rep movsb

dec ax

jnz loop_idt ]

loop_idt 라는 레이블에서 256개의 디스크립터를 모두 초기화하기 한다. 먼저 cx에 8을 설정하여 rep movsb로 8 Byte인 하나의 디스크립터를 초기화한 후에 ax 값을 하나씩 감소시키며 총 256개의 디스크립터를 0으로 모두 초기화한다. 

[     mov edi, 0

lea esi, [idt_zero_divide]

mov cx, 8

rep movsb ]

위의 예약된 IDT 표에서 봤듯이 0으로 나눴을 때 예외처리를 하는 루틴이 IDT의 첫번째에 들어간다. 

[ mov edi, 8*0x20

lea esi, [idt_timer]

mov cx, 8

rep movsb ]

PIC의 첫번째 IRQ 핀에 연결되어 있던 하드웨어 인터럽트인 타이머 인터럽트는 충돌을 피해주기 위해 0x20을 더해줘야 한다. 그러므로 0x20*8[한 디스크립터가 8바이트이므로 21번째라면 0x20*8]을 edi에 넣고 8바이트를 초기화하는 루틴이다.

[ lidt [idtr]

mov al, 0xFC ; 

out 0x21, al ; 

sti

mov edx, 0

mov eax, 0x100

mov ebx, 0

div ebx

jmp $ ]

lidt를 통해 idtr이라는 레이블(6Byte(limit(16Bit = 2Byte), base address(32Bit = 4Byte))에 담긴 값을 IDTR에 저장한다. 0xFC는 0xFF에서 0x2를 뺀 것이므로 PIC의 하드웨어 인터럽트 중에 첫번째인 타이머 인터럽트와 두번째인 키보드 인터럽트를 활성화 한다. 그리고 sti명령어를 통해서 인터럽트를 활성화 시킨다. 그후 devide by zero 예외를 만들기 위해 eax에 0x100을 넣고 ebx에 0을 넣어 예외를 발생 시킨다.

ISR(Interrupt Service Routine)


이제 IDT를 설정했으니 인터럽트나 예외가 발생했을 때 호출과정인 ISR에 대해 알아보도록 하자. 쉽게 설명하기 위해 열심히 그림을 그렸다!!



▲인터럽트 핸들러 호출과정

 코드 실행중에 타이머 인터럽트가 발생했다.

1. IDTR 레지스터에 저장된 base address를 확인

2. Base Address를 참조하여 IDT테이블 Base Address로 이동

3. 인터럽트 번호(0x20)를 이용하여 IDT 테이블에서 오프셋 구하기

4. IDT의 0x20번째에 저장된 세그먼트 오프셋을 이용하여 GDT에서 해당 위치로 접근

5. GDT에서 인터럽트 루틴이 저장된 디스크립터에서 Base Address의 메모리로 이동

6. IDT에 저장된 offset으로 메모리에서 base address + offset을 하여 인터럽트 핸들러 루틴으로 이동

마치면서


인터럽트와 예외에 대해서 구분하고 예외와 하드웨어 인터럽트가 충돌이 나지 않도록 0x20을 PIC에 추가했다. 그 후에 IDT 테이블을 설정하고 인터럽트 루틴으로 이동하는 과정을 봤다. 이에 대한 소스를 아래에 첨부하였다.이 소스는 PIC와 IDT를 셋팅한 후에 키보드 인터럽트, 타이머 인터럽트가 풀려 있지만 divide by zero 예외에 걸려 해당 ISR 루틴을 처리(대기)한다.


[4]boot.asm


[4]kernel.asm



CHS방식으로만 다음 섹터를 읽어서 LBA방식을 이용하여 디스크 읽기를 하는 부트코드를 만들려고 했다. 

하지만 이게 왠걸? 왜 안되지 ㅋㅋㅋ 분명히 필요한 부분이 다 줬는데 안된다.


▲LBA 어셈코드

에러코드를 보기 위해서 숫자의 아스키코드인 0x30을 더해줘서 에러코드를 받았는데 fail문에 들어가서 에러코드 1을 나타낸다. 그리고 다시 ah에 0x00을 넣어 초기화를 해주고 다시 해도 그대로다. 뿐만 아니라 reset에서는 에러코드가 나오지 않는 걸로 봐서 ah가 잘못됬거나, 파라미터가 잘못됬다는 말인데... 후 ㅋㅋ 왜 안되는지 모르겠네 한 3시간 삽질했는데 안된다ㅋㅋㅋ 혹시 이유 아는 사람 댓글 주세요


에러 처리 어셈코드

에러 출력 코드

▲에러상태 코드

+ Recent posts