아놔 %^$#% 도데체 이거 쵸큼 하는데 하루 종일 걸린 이유가 뭐냔 말이다- _-
메모리 관리.. 후아..
간단한 언급을 하자면, 선형적인 가상 메모리는 실제 물리 메모리에서 보자면 분리된 페이지들로 존재할수도 있다. (가상 메모리<-세그멘테이션 처리<-선형 메모리<-페이징 처리<-물리 메모리)
에 또.. 페이지 풀에 할당된 유저 스페이스의 가상 메모리와 시스템 스페이스의 메모리는 말 그대로 항상 페이징 될수 있다. (물론 당연히, 실행 중에도 페이징 될수 있삼-0-)
아 또한, 가상 메모리는 현재 프로세스의 주소만 표시한다, 따라서 다른 프로세스의 메모리 주소는 액세스가 불가하다.
이건 약간 다른 이야기이긴 한데 드라이버에서 유저모드 어플리케이션의 요청을 처리하는 경우를 봐 보죠. 이런 경우가 있죠. 보통 유저모드 어플리케이션이 드라이버에 요청 메세지를 보내게 되죠. 실제로는 I/O매니저가 IRP를 만들어서 보내지만, 애초에 요청은 유저모드 어플리케이션이 하니깐요. 어쨌든, 이 IRP를 받는 WDM드라이버가 애초에 시작된 유저모드 어플리케이션의 쓰레드 컨텍스트에 위치해 있다. 라는 보장이 없습니다. 임의적인 쓰레드에서 실행될 가능성도 있죠. 따라서 임의의 쓰레드에서 실행되는 드라이버가 유저 모드의 메모리를 액세스하면 엉뚱한 쓰레드의 메모리를 건드리게 되는.. 불상사가 발생하죠. 따라서 드라이버 코드는 유저모드 메모리를 액세스하면 안된다는 사실. : )
에.. 하여간, 자 이제 시스템 스페이스에 추가적인 메모리 할당하는걸 배워보도록 합시다. 보통 우리(드라이버 개발자)는 드라이버 전역적으로 쓰이는 변수등을 디바이스 익스텐션에 저장을 합니다. 근데, 몇몇 드라이버들은 추가적으로 거대한 시스템 스페이스의 메모리를 할당해서 써야할 경우가 있답니다. 보통 I/O 버퍼 공간을 할당한다 거나 하는걸로요.
MnAllocateNonCachedMemory나 MnAllocateContiguousMemorySpecifyCache, ExAllocatePoolWithTag등의 함수로 시스템 스페이스의 메모리를 할당할수 있습니다.
근데, 넌-페이지드 풀은 운영체제가 동작하면서 단편화 되기 때문에, 드라이버의 초기화 루틴(DriverEntry)에서 할당을 해 주어야 하고, Unload에서 해제해 주어야 합니다. 왜냐하면 드라이버는 운영체제가 부팅되면서 초기화 루틴이 실행되기 때문이죠. 운영체제가 부팅할때부터 넌-페이지드 풀이 단편화 되어있을리는 없잖삼?
또한, 각 할당 함수를 사용해서 메모리를 할당할때는 매우 매우~ 경제적으로 메모리를 할당받아서 사용해야 합니다. 특히 넌-페이지드 메모리는 크기가 제한되어 있기 때문에 무턱대고 100MB의 용량을 할당해 버리면.. 할당이 안될수도 있습니다. 게다가 이 할당 함수들은 페이지 단위로 할당을 하기 때문에, 만약 1KB만 할당을 했어도, 1개의 페이지 프레임(보통 4KB)을 통째로 주게 됩니다. 그러면 결국 사용하는것은 1KB밖에 안되기 때문에, 3KB는 버려지는것이죠. 다른 커널 모드 코드에서도 액세스 할수 없습니다. 후우.. 잘~ 씁시다.
책에도 나와있는 유명한 함수 ExAllocatePoolWithTag에 대해.. 짬깐 봅시다.
얘가 보통 메모리 풀을 할당할때 쓰는 함수랩니다.
얘는 넌-페이지드/페이지드 풀을 할당할수가 있는데, 만약 넌-페이지드 메모리를 할당한다면 DISPATCH_LEVLE이하에서 콜 할수가 있고, 걍 페이지드 메모리를 할당한다면 APC_LEVEL이하에서 콜 할수가 있답니다. 한마디로 페이지드 메모리를 DISPATCH_LEVLE에서 콜하면 안된다는 거겠죠-.-;
얘도 역시 경제적으로 메모리를 할당해야 합니다. 5KB하면 3KB는 못 쓴다는 이야기죠.
근데, 갑작스레 궁금해 지는게... 만약 한 페이지 프레임 크기보다 작은 메모리를 할당하면 1페이지 프레임 이하의 크기에서도 남은 메모리는 버려지는 건가? 하는 생각이 드네요. 아니면 한 페이지 정도의 크기의 메모리 영역 보다 작은 메모리를 할당하는 경우는 없는건가? 하긴 보통은 디바이스 익스텐션에 박아 넣으면 별 무리는 없을거 같긴 하지만..흐음..- _;
...후아.. 거의 1시간을 비몽사몽으로 헤메고 다녔네..- _-;
음.. 이번에 관심을 갖을 분야는 맵 레지스터이다. 우선 맵 레지스터를 보기전에, DMA부터 보자. DMA의 사용 목적은 데이터를 전송하는데 있어 CPU와의 관련성을 최소화 시키는데 있다고 한다. CPU가 데이터 전송에 간섭하지 않는데, 어떻게 데이터 전송이 이루어질까? 적어도 어디에 있는놈을 어디에 보내라.. 정도는 계산해 줘야 하는데.. 어케...?
그 어케를 위해서 DMAC(DMA컨트롤러)라는 보조 프로세서를 이용한다.
자.. 근데 이런 문제가 있다. 우리가 보는 메모리는 물리 메모리 주소가 아니다. 가상 메모리 주소이다. 그리고, 가상 메모리 주소에서 선형적으로 인접한 메모리가 물리 메모리에서는 각기 다른 페이지 프레임에 속해 있을수도 있다.(물리 메모리는 페이징과 세그멘테이션을 통해서 가상 메모리로 변환된다는건 알지?). 근데, 문제는 이거다. DMAC..얘는 물리메모리에 순차적으로 바로 접근한다. 근데, 얘가 물리 메모리(페이지 프레임)을 주욱~ 읽어가다가 다음 페이지 프레임으로 넘어갔는데(페이지 경계를 지나감), 이게 가상 메모리에서는 순차적이지만, 물리 메모리에서는 순차적이지가 않을수도 있잖삼?.. 해서 다음 페이지 프레임이 내가 원하는(가상 메모리에서 순차적으로 액세스 하고 있는 주소)가 아닌거라. 어허..- _-;
그래서 어떻게 했겠어? 맵핑 레지스터라는걸 만들어서(실제 레지스터가 아닌 가상의 레지스터) 얘네가 DMA하드웨어에 의해 사용되는 논리적 주소라고 불리는 연속적인 주소들의 영역과 논리적인 공간 주소들을 물리적인 공간 주소들로 번역한...다는데, 뭔 말이 이리 어렵냐- _-; 깐딴하게는 DMAC가 순차적으로 읽는다고 했잖여-0-, DMAC가 맵핑 레지스터를 순차적으로 읽어, 근데, 맵핑 레지스터는 (순차적인 가상 메모리에 대응되는)물리 메모리의 페이지 테이블을 가리키고 있는거지.. R U understand?
MDL이랑 관련지어서 더 이야기 해야 할거 같긴 한데, 우선은 넘어가고, 다음으로 커널 스택에 대해 알아보자. 우선 커널 스택이 뭔지부터 보자. 어디에 쓰는지... 커널 스페이스의 코드들이 사용하는 스택인가 보다..- _-; 미안.. 책에 별 언급이 없다ㅠ_ㅜ;
하여간 이 커널 스택이란 놈은 상당히 제한적이다. 3개의 페이지 프레임 크기(3*4KB)밖에 안되기 때문에 큰 용량의 데이터를 내부적으로 주고받는데 사용하면 안된다.(100KB 크기의 배열을 값 전달 방식으로 넘긴다던지..- _;)... 만약 큰 데이터를 이리저리 주고 받거나 깊은 중첩 콜을 써서 스택이 넘쳤다... 그러면 fatal 시스템 에러가 난다고 한다. ..그게 뭐냐고? 아마.. 블루스크린일껄- .-
아.. 잠와서 미치겠다. 걍 번역해야겠다. 이해는 나중일이고.. 푸휴..
요청 I/O 처리를 수행하기 위해서 고정 크기의 버퍼를 동적으로 할당하려고 한다면, ExXxxLookasideList함수를 이용할수 있다. 드라이버가 룩어사이드 리스트를 초기화 하면, 운영체제는 동적으로 할당된 고정 크기의 버퍼를 드라이버를 위해 홀드(hold)한다. 운영체제는 현재 사용되는 넌-페이지드 와 페이지드 룩어사이드 리스트의 상태를 보관한다. 또한 동적으로 할당하거나 해제하는 작업을 한다.
넌-페이지드 룩어사이드 리스트를 만들려면 ExInitializeNPagedLookasideList함수를 사용하면 되고, DISPATCH_LEVEL이상에서 액세스 할수 있다.
페이지드 룩어사이드 리스트는 APC_LEVEL이하에서 액세스 될수 있다.
ExAllocateFrom(N)PagedLookasideList로 할당한 엔트리가 더이상 사용되지 않는다면 가능한 빨리 ExFreeTo(N)PagedLookasideList로 해제해야 한다.
아트 베이커의 책에는 룩어사이드 리스트와 관계된 모든 자원을 해제하고자 할 때에는 ExDelete(N)PagedLookasideList를 콜하면 된다. 이 함수들은 흔히 드라이버의 Unload, RemoveDevice루틴에서 호출된다고 한다.
후.. 드라이버를 pageable하게 만들기. 라는 서브 챕터인데.. 이게 먼 소리냐면, 드라이버 코드가 실행되는 IRQL이 여러개가 있잖어? PASSIVE/APC/DISPATCH_LEVEL, DIRQL...
근데, DISPATCH_LEVEL 미만(APC레벨 이하)이라면 드라이버 코드가 속한 메모리 영역이 페이지 아웃이 되어도 별 상관은 없그등... 게다가 넌-페이지드 영역은 제한되어 있잖냐. 그러니 별 쓸모없는 얘들까지 넌-페이지드 메모리에 올려서 메모리 낭비하는건 삽질이다 이거지. 그래서 드라이버를 pageable하게 만든다는거야. 그럼 그걸 언제 하느냐...
여기 세가지 해야 하는 경우와, 하지 말아야 하는 경우가 각각 3가지씩 나왔네.
페이지드 풀에 액세스 하는경우
다른 pageable 루틴을 콜 하는경우
유저 쓰레드 컨텍스트에서 유저 버퍼를 참조하는 경우
하지 말아야 하는 경우로는
DISPATCH_LEVEL이상에서 실행되는 경우
스핀락을 요청하는 경우
커널 오브젝트 지원루틴을 호출하는 경우
....둘다 애매하네.. 잘 모르겠으니 패스-.-;
실제로 코드를 pageable하게 만드는 방법
...
PAGED_CODE(); //first statement in a routine
...
얘는 DISPATCH_LEVEL이상에서 코드가 동작중인지 알려주는 매크로이다. 디버그 모드에서 메세지를 생성해 준다는데.. 흐음.. 한번 매크로를 까 봅시다....하려고 했는데 귀찮.. 근데 보니까 별거 없...
또한 코드의 일부 부분을 페이지드/넌-페이지드 메모리에 올릴수도 있는데, 이는 컴파일러 directive를 이용해서 지정할수 있다. 즉 다음과 같다.
#ifdef ALLOC_PRAGMA //pragma지원 여부를 알려줌
#pragma alloc_text(PAGE, DriverEntry) //해당 함수를 페이지 영역으로 지정
#endif
혹은
#pragma code_seg ("PAGE")
NTASTATUS DriverEntry(...){...}
#pragma code_seg ()
pragma 지시어 안에 있는 코드는 paged pool에 들어간다.
또한 런타임시에도 페이징 처리를 할수가 있는데 MnLockPagableData/CodeSection함수로 락을 걸고(넌-페이지드 풀에 위치) MnUnlockPagableImageSection을 통해 락을 해제(페이지드 풀에 위치)할수도 있다. 후에 다시 락을 걸고 싶다면 MnLockPagableData/CodeSection 함수가 리턴한 핸들을 인자로 받는 MnLockPagableSectionByHandle함수를 사용하면 된다.
흠.. 그럼 얘네를 언제 쓸수 있을까? 음.. 이런 경우가 있을수 있겠다. 이게 실제로 맞는지는 모르겠는데 어쨌든, 이런 느낌이라고 생각해 주기 바란다. 뭐냐면...
USB포트가 2개가 있다고 치자. 근데 USB가 하나도 사용중인게 없어-0-... 그러면 드라이버가 넌-페이지드 메모리를 차지하고 있을 꺼리가 없잖아.. 해서 언락을 하고 페이지드 풀로 옮기는거지. 그러다가 USB가 꼽히면, 락을 해서 다시 넌-페이지드 풀로 옮기는거고.. 뭐 이럴때 써먹을수 있을거 같지 않삼?
비슷한건데, MnPageEntireDriver랑 MnResetDriverPaging이라는 함수가 있어, 전자는 드라이버의 전체 섹션을 페이지드-풀로 옮기는거고, 후자는 드라이버를 컴파일시에 지정된 페이지 설정으로 돌려주는거야. 근데! 기억해야 할것 하나! 만약에 디바이스가 인터럽트를 쓴다면, 페이지드-풀 로 옮기면 안된다는거. 왜냐면, 드라이버 전체를 페이징하면 ISR도 페이징되 버리거든, 그러면.. 만약 인터럽트가 왔는데, 서비스할 루틴이.. 페이지 아웃?.. 이러면 안되겠지.. 그래서 페이지드 풀로 옮기기 전에 인터럽트를 끊어놔야 한다는 말씀 OKie?
MDL..인데.. 딱히 별 내용이 없네. 걍 사용법만 주루루룩..- _-; 간단히 언급하고 넘어가겠삼. (DMA랑 맵핑 레지스터랑 관련해서 이야기가 좀 나와줄줄 알았더니..- _-)
연속된 가상 메모리는 실제 물리 메모리에서는 여러 페이지에 분산되어있을수 있다. 해~서 시스템이 MDL(메모리 디스크립터 리스트) 구조체로 물리 메모리를 가상 메모리 버퍼에 표현한단 말이지.. 음 말이 어렵군..
아.. 내가 위 글을 쓰다가 갑자기 이런 생각이 들었었어.. 아니 왜 구찮게 물리 메모리를 참조하는거지- _-?, 간단하게 가상 메모리에 있는 유저 버퍼를 참조하면 되는거 아냐? 미안하다.. 내가 깜빡하고 있었다. 실제 I/O를 수행하는 드라이버 루틴은 임의적인 쓰레드에 존재할 가능성이 높다는걸... 임의적인 쓰레드에 존재하면.. 유저 버퍼의 내용이 임의의 쓰레드 내용으로 바뀌겠지.. 그 상태에서 참조하면..- ;;
어쨌든 그렇고, 우리들은 MDL멤버를 직접 참조하면 안된데-0-, 대신에 MmGetMdlVirtualAddress 가상 메모리 주소 반환
MmGetMdlByteCount 버퍼의 사이즈를 반환
MmGetMdlByteOffset 물리 페이지에서 버퍼의 시작 옵셋 반환
MmGetMdlPfnArray 실제 물리 메모리의 페이지 번호를 가리키는 포인터를 리턴
요런 매크로를 이용하고, MDL을 할당 하려면 IoAllocateMdl루틴을 콜 하면 될것이고, 반대로 해제하려면 IoFreeMdl을 콜 하면 된데. 또한, 이미 할당된 버퍼의 내용을 초기화(포멧) 할수도 있는데, 그럴때는 MmInitializeMdl을 콜 하라고 한담.
이번에는 객체 관리 챕터야. 짧으니까 빨리빨리 끝내고 싶은데 빨리 끝날지는 의문..- _;
커널 오브젝트는 이름을 가질수도 있고, 이름이 없을수도 있어. 오브젝트 네임은 유니코드 스트링이고 유저모드나 커널모드 양쪽 모두에서 참조할때 그 이름을 이용해서 참조하지.
응.. 여기 나오네, 유저모드와 커널모드 컴포넌트들은 핸들을 얻는데 오브젝트 이름을 사용한다. 모든 연산은 핸들을 통해서 이루어진다.
만약 오브젝트가 이름이 없다면(unnamed) 유저모드 컴포넌트는 핸들을 열지 못한다. 반면에 커널 모드 컴포넌터는 포인터나 핸들을 통해 참조할수 있다.
근데 커널 오브젝트라는게 개념이 잘 안오지..-.-; 한가지 예를 들어보자.
\DosDevices\C:\Directory\File
얘는 말이야. \DosDevices 라는 오브젝트 디렉토리 안에 있는 C:를 나타내는 \C:라는 디바이스 오브젝트에 Directory라는 디렉토리를 나타내는 파일 오브젝트, File이라는 파일을 나타내는 파일 오브젝트를 의미해.
간단히 C:\Directory\File 이라는 경로를 디바이스 오브젝트를 통해 보자면 위와 같다는 이야기야. 근데, 내가 써 놓고도 감이 안온다. 왜! 오브젝트라는걸 쓰는걸까?
내가 오브젝트 디렉토리를 이야기 했잖아. 근데 이 오브젝트 디렉토리는 하드 어느 구석에 짱박혀 있는게 아니야. 얘는 오브젝트 매니저가 가지고(?) 있는거야. 대충 다음과 같은 최 상위 오브젝트 디렉토리들이 있지.
\Callbacks
\Device
\KernelObjects
\DosDevices
오브젝트 매니저는 오브젝트에 대한 참조 카운트를 보관하고 있어. 오브젝트가 생성이 되면 오브젝트의 참조 카운트를 1로 설정하지. 만약 참조 카운트가 0으로 떨어지면 오브젝트는 해제되는거야.
드라이버는, 이 카운트를 잘 살펴야해. 왜냐면, 참조 카운트가 0으로 떨어져서 해제된 오브젝트를 다시 해제하면 나쁜경우 시스템 크래시를 발생시킬수도 있그등. (뭐.. 의미가 약간 다르긴 하지만.. 비슷한 말일거라고 생각하고.. 스킵)
아, 물론 알고 있겠지만, 오브젝트는 핸들에 의해서도 참조가 되잖아. 그래서 결과적으로 오브젝트 매니저는 오브젝트에 대해 핸들 카운트와 참조 카운트, 이 두가지의 카운트를 보관하고 있어. 만약 핸들을 오픈하면 핸들카운트와 참조 카운트가 모두 증가하게 되. 물론 객체의 핸들을 열었으면(얻었으면?) 다시 닫아주는(돌려주는?)게 예의야.
앞의 이야기는 유저모드의 이야기였고. (핸들을 연 만큼 닫아 준다는거)..
커널 모드에서는 오브젝트를 포인터로 참조할수도 있어. (유저 영역에서는 못하는거얌).
IoGetAttachedDeviceReference함수는 오브젝트의 포인터를 리턴해 주거등. 그러면, 오브젝트 매니저는 오브젝트의 참조값을 1 올리겠지? 내가 위에서 오브젝트가 어떨때 소멸된다고 했더라? 참조값이 0이 되야지 소멸한다고 그랬지?.. 따라서 오브젝트를 다 썼다면 반드시 ObDereferenceObject함수로 오브젝트 참조값을 -1해줘야해.
오브젝트 보안에 관한 내용이 쪼금 있는거 같아서 쵸큼 첨부할께.
오브젝트는 타입별로 특정한 액세스 권한이 있어. 이걸 specific access right라고 해. 뭐 이런거야. FILE_READ_DATA는 파일 타입의 오브젝트에 특정한 액세스 권한이고, KEY_QUERY_VALUE는 레지스트리 키 타입의 오브젝트에 특정한 액세스 권한이야.
반면에 비슷한 operation을 몽땅 합쳐버린 제네릭한 액세스 권한이 있어, 이걸 generic access right라고 하지. 뭐 이런거야. 위에서 FILE_READ_DATA랑 KEY_QUERY_VALUE를 언급했잖아. 근데, 얘네가 하는일이 둘다 READ거등. 따라서 하는일이 같은거야. 오브젝트 타입은 다르지만..쩝..
그래서 GENERIC_READ라고 설정하면, FILE_READ_DATA랑 KEY_QUERY_VALUE랑 기타 등등의 권한을 모두 가지고 있게 되는거지.
메모리 관리.. 후아..
간단한 언급을 하자면, 선형적인 가상 메모리는 실제 물리 메모리에서 보자면 분리된 페이지들로 존재할수도 있다. (가상 메모리<-세그멘테이션 처리<-선형 메모리<-페이징 처리<-물리 메모리)
에 또.. 페이지 풀에 할당된 유저 스페이스의 가상 메모리와 시스템 스페이스의 메모리는 말 그대로 항상 페이징 될수 있다. (물론 당연히, 실행 중에도 페이징 될수 있삼-0-)
아 또한, 가상 메모리는 현재 프로세스의 주소만 표시한다, 따라서 다른 프로세스의 메모리 주소는 액세스가 불가하다.
이건 약간 다른 이야기이긴 한데 드라이버에서 유저모드 어플리케이션의 요청을 처리하는 경우를 봐 보죠. 이런 경우가 있죠. 보통 유저모드 어플리케이션이 드라이버에 요청 메세지를 보내게 되죠. 실제로는 I/O매니저가 IRP를 만들어서 보내지만, 애초에 요청은 유저모드 어플리케이션이 하니깐요. 어쨌든, 이 IRP를 받는 WDM드라이버가 애초에 시작된 유저모드 어플리케이션의 쓰레드 컨텍스트에 위치해 있다. 라는 보장이 없습니다. 임의적인 쓰레드에서 실행될 가능성도 있죠. 따라서 임의의 쓰레드에서 실행되는 드라이버가 유저 모드의 메모리를 액세스하면 엉뚱한 쓰레드의 메모리를 건드리게 되는.. 불상사가 발생하죠. 따라서 드라이버 코드는 유저모드 메모리를 액세스하면 안된다는 사실. : )
에.. 하여간, 자 이제 시스템 스페이스에 추가적인 메모리 할당하는걸 배워보도록 합시다. 보통 우리(드라이버 개발자)는 드라이버 전역적으로 쓰이는 변수등을 디바이스 익스텐션에 저장을 합니다. 근데, 몇몇 드라이버들은 추가적으로 거대한 시스템 스페이스의 메모리를 할당해서 써야할 경우가 있답니다. 보통 I/O 버퍼 공간을 할당한다 거나 하는걸로요.
MnAllocateNonCachedMemory나 MnAllocateContiguousMemorySpecifyCache, ExAllocatePoolWithTag등의 함수로 시스템 스페이스의 메모리를 할당할수 있습니다.
근데, 넌-페이지드 풀은 운영체제가 동작하면서 단편화 되기 때문에, 드라이버의 초기화 루틴(DriverEntry)에서 할당을 해 주어야 하고, Unload에서 해제해 주어야 합니다. 왜냐하면 드라이버는 운영체제가 부팅되면서 초기화 루틴이 실행되기 때문이죠. 운영체제가 부팅할때부터 넌-페이지드 풀이 단편화 되어있을리는 없잖삼?
또한, 각 할당 함수를 사용해서 메모리를 할당할때는 매우 매우~ 경제적으로 메모리를 할당받아서 사용해야 합니다. 특히 넌-페이지드 메모리는 크기가 제한되어 있기 때문에 무턱대고 100MB의 용량을 할당해 버리면.. 할당이 안될수도 있습니다. 게다가 이 할당 함수들은 페이지 단위로 할당을 하기 때문에, 만약 1KB만 할당을 했어도, 1개의 페이지 프레임(보통 4KB)을 통째로 주게 됩니다. 그러면 결국 사용하는것은 1KB밖에 안되기 때문에, 3KB는 버려지는것이죠. 다른 커널 모드 코드에서도 액세스 할수 없습니다. 후우.. 잘~ 씁시다.
책에도 나와있는 유명한 함수 ExAllocatePoolWithTag에 대해.. 짬깐 봅시다.
얘가 보통 메모리 풀을 할당할때 쓰는 함수랩니다.
얘는 넌-페이지드/페이지드 풀을 할당할수가 있는데, 만약 넌-페이지드 메모리를 할당한다면 DISPATCH_LEVLE이하에서 콜 할수가 있고, 걍 페이지드 메모리를 할당한다면 APC_LEVEL이하에서 콜 할수가 있답니다. 한마디로 페이지드 메모리를 DISPATCH_LEVLE에서 콜하면 안된다는 거겠죠-.-;
얘도 역시 경제적으로 메모리를 할당해야 합니다. 5KB하면 3KB는 못 쓴다는 이야기죠.
근데, 갑작스레 궁금해 지는게... 만약 한 페이지 프레임 크기보다 작은 메모리를 할당하면 1페이지 프레임 이하의 크기에서도 남은 메모리는 버려지는 건가? 하는 생각이 드네요. 아니면 한 페이지 정도의 크기의 메모리 영역 보다 작은 메모리를 할당하는 경우는 없는건가? 하긴 보통은 디바이스 익스텐션에 박아 넣으면 별 무리는 없을거 같긴 하지만..흐음..- _;
...후아.. 거의 1시간을 비몽사몽으로 헤메고 다녔네..- _-;
음.. 이번에 관심을 갖을 분야는 맵 레지스터이다. 우선 맵 레지스터를 보기전에, DMA부터 보자. DMA의 사용 목적은 데이터를 전송하는데 있어 CPU와의 관련성을 최소화 시키는데 있다고 한다. CPU가 데이터 전송에 간섭하지 않는데, 어떻게 데이터 전송이 이루어질까? 적어도 어디에 있는놈을 어디에 보내라.. 정도는 계산해 줘야 하는데.. 어케...?
그 어케를 위해서 DMAC(DMA컨트롤러)라는 보조 프로세서를 이용한다.
자.. 근데 이런 문제가 있다. 우리가 보는 메모리는 물리 메모리 주소가 아니다. 가상 메모리 주소이다. 그리고, 가상 메모리 주소에서 선형적으로 인접한 메모리가 물리 메모리에서는 각기 다른 페이지 프레임에 속해 있을수도 있다.(물리 메모리는 페이징과 세그멘테이션을 통해서 가상 메모리로 변환된다는건 알지?). 근데, 문제는 이거다. DMAC..얘는 물리메모리에 순차적으로 바로 접근한다. 근데, 얘가 물리 메모리(페이지 프레임)을 주욱~ 읽어가다가 다음 페이지 프레임으로 넘어갔는데(페이지 경계를 지나감), 이게 가상 메모리에서는 순차적이지만, 물리 메모리에서는 순차적이지가 않을수도 있잖삼?.. 해서 다음 페이지 프레임이 내가 원하는(가상 메모리에서 순차적으로 액세스 하고 있는 주소)가 아닌거라. 어허..- _-;
그래서 어떻게 했겠어? 맵핑 레지스터라는걸 만들어서(실제 레지스터가 아닌 가상의 레지스터) 얘네가 DMA하드웨어에 의해 사용되는 논리적 주소라고 불리는 연속적인 주소들의 영역과 논리적인 공간 주소들을 물리적인 공간 주소들로 번역한...다는데, 뭔 말이 이리 어렵냐- _-; 깐딴하게는 DMAC가 순차적으로 읽는다고 했잖여-0-, DMAC가 맵핑 레지스터를 순차적으로 읽어, 근데, 맵핑 레지스터는 (순차적인 가상 메모리에 대응되는)물리 메모리의 페이지 테이블을 가리키고 있는거지.. R U understand?
MDL이랑 관련지어서 더 이야기 해야 할거 같긴 한데, 우선은 넘어가고, 다음으로 커널 스택에 대해 알아보자. 우선 커널 스택이 뭔지부터 보자. 어디에 쓰는지... 커널 스페이스의 코드들이 사용하는 스택인가 보다..- _-; 미안.. 책에 별 언급이 없다ㅠ_ㅜ;
하여간 이 커널 스택이란 놈은 상당히 제한적이다. 3개의 페이지 프레임 크기(3*4KB)밖에 안되기 때문에 큰 용량의 데이터를 내부적으로 주고받는데 사용하면 안된다.(100KB 크기의 배열을 값 전달 방식으로 넘긴다던지..- _;)... 만약 큰 데이터를 이리저리 주고 받거나 깊은 중첩 콜을 써서 스택이 넘쳤다... 그러면 fatal 시스템 에러가 난다고 한다. ..그게 뭐냐고? 아마.. 블루스크린일껄- .-
아.. 잠와서 미치겠다. 걍 번역해야겠다. 이해는 나중일이고.. 푸휴..
요청 I/O 처리를 수행하기 위해서 고정 크기의 버퍼를 동적으로 할당하려고 한다면, ExXxxLookasideList함수를 이용할수 있다. 드라이버가 룩어사이드 리스트를 초기화 하면, 운영체제는 동적으로 할당된 고정 크기의 버퍼를 드라이버를 위해 홀드(hold)한다. 운영체제는 현재 사용되는 넌-페이지드 와 페이지드 룩어사이드 리스트의 상태를 보관한다. 또한 동적으로 할당하거나 해제하는 작업을 한다.
넌-페이지드 룩어사이드 리스트를 만들려면 ExInitializeNPagedLookasideList함수를 사용하면 되고, DISPATCH_LEVEL이상에서 액세스 할수 있다.
페이지드 룩어사이드 리스트는 APC_LEVEL이하에서 액세스 될수 있다.
ExAllocateFrom(N)PagedLookasideList로 할당한 엔트리가 더이상 사용되지 않는다면 가능한 빨리 ExFreeTo(N)PagedLookasideList로 해제해야 한다.
아트 베이커의 책에는 룩어사이드 리스트와 관계된 모든 자원을 해제하고자 할 때에는 ExDelete(N)PagedLookasideList를 콜하면 된다. 이 함수들은 흔히 드라이버의 Unload, RemoveDevice루틴에서 호출된다고 한다.
후.. 드라이버를 pageable하게 만들기. 라는 서브 챕터인데.. 이게 먼 소리냐면, 드라이버 코드가 실행되는 IRQL이 여러개가 있잖어? PASSIVE/APC/DISPATCH_LEVEL, DIRQL...
근데, DISPATCH_LEVEL 미만(APC레벨 이하)이라면 드라이버 코드가 속한 메모리 영역이 페이지 아웃이 되어도 별 상관은 없그등... 게다가 넌-페이지드 영역은 제한되어 있잖냐. 그러니 별 쓸모없는 얘들까지 넌-페이지드 메모리에 올려서 메모리 낭비하는건 삽질이다 이거지. 그래서 드라이버를 pageable하게 만든다는거야. 그럼 그걸 언제 하느냐...
여기 세가지 해야 하는 경우와, 하지 말아야 하는 경우가 각각 3가지씩 나왔네.
페이지드 풀에 액세스 하는경우
다른 pageable 루틴을 콜 하는경우
유저 쓰레드 컨텍스트에서 유저 버퍼를 참조하는 경우
하지 말아야 하는 경우로는
DISPATCH_LEVEL이상에서 실행되는 경우
스핀락을 요청하는 경우
커널 오브젝트 지원루틴을 호출하는 경우
....둘다 애매하네.. 잘 모르겠으니 패스-.-;
실제로 코드를 pageable하게 만드는 방법
...
PAGED_CODE(); //first statement in a routine
...
얘는 DISPATCH_LEVEL이상에서 코드가 동작중인지 알려주는 매크로이다. 디버그 모드에서 메세지를 생성해 준다는데.. 흐음.. 한번 매크로를 까 봅시다....하려고 했는데 귀찮.. 근데 보니까 별거 없...
또한 코드의 일부 부분을 페이지드/넌-페이지드 메모리에 올릴수도 있는데, 이는 컴파일러 directive를 이용해서 지정할수 있다. 즉 다음과 같다.
#ifdef ALLOC_PRAGMA //pragma지원 여부를 알려줌
#pragma alloc_text(PAGE, DriverEntry) //해당 함수를 페이지 영역으로 지정
#endif
혹은
#pragma code_seg ("PAGE")
NTASTATUS DriverEntry(...){...}
#pragma code_seg ()
pragma 지시어 안에 있는 코드는 paged pool에 들어간다.
또한 런타임시에도 페이징 처리를 할수가 있는데 MnLockPagableData/CodeSection함수로 락을 걸고(넌-페이지드 풀에 위치) MnUnlockPagableImageSection을 통해 락을 해제(페이지드 풀에 위치)할수도 있다. 후에 다시 락을 걸고 싶다면 MnLockPagableData/CodeSection 함수가 리턴한 핸들을 인자로 받는 MnLockPagableSectionByHandle함수를 사용하면 된다.
흠.. 그럼 얘네를 언제 쓸수 있을까? 음.. 이런 경우가 있을수 있겠다. 이게 실제로 맞는지는 모르겠는데 어쨌든, 이런 느낌이라고 생각해 주기 바란다. 뭐냐면...
USB포트가 2개가 있다고 치자. 근데 USB가 하나도 사용중인게 없어-0-... 그러면 드라이버가 넌-페이지드 메모리를 차지하고 있을 꺼리가 없잖아.. 해서 언락을 하고 페이지드 풀로 옮기는거지. 그러다가 USB가 꼽히면, 락을 해서 다시 넌-페이지드 풀로 옮기는거고.. 뭐 이럴때 써먹을수 있을거 같지 않삼?
비슷한건데, MnPageEntireDriver랑 MnResetDriverPaging이라는 함수가 있어, 전자는 드라이버의 전체 섹션을 페이지드-풀로 옮기는거고, 후자는 드라이버를 컴파일시에 지정된 페이지 설정으로 돌려주는거야. 근데! 기억해야 할것 하나! 만약에 디바이스가 인터럽트를 쓴다면, 페이지드-풀 로 옮기면 안된다는거. 왜냐면, 드라이버 전체를 페이징하면 ISR도 페이징되 버리거든, 그러면.. 만약 인터럽트가 왔는데, 서비스할 루틴이.. 페이지 아웃?.. 이러면 안되겠지.. 그래서 페이지드 풀로 옮기기 전에 인터럽트를 끊어놔야 한다는 말씀 OKie?
MDL..인데.. 딱히 별 내용이 없네. 걍 사용법만 주루루룩..- _-; 간단히 언급하고 넘어가겠삼. (DMA랑 맵핑 레지스터랑 관련해서 이야기가 좀 나와줄줄 알았더니..- _-)
연속된 가상 메모리는 실제 물리 메모리에서는 여러 페이지에 분산되어있을수 있다. 해~서 시스템이 MDL(메모리 디스크립터 리스트) 구조체로 물리 메모리를 가상 메모리 버퍼에 표현한단 말이지.. 음 말이 어렵군..
아.. 내가 위 글을 쓰다가 갑자기 이런 생각이 들었었어.. 아니 왜 구찮게 물리 메모리를 참조하는거지- _-?, 간단하게 가상 메모리에 있는 유저 버퍼를 참조하면 되는거 아냐? 미안하다.. 내가 깜빡하고 있었다. 실제 I/O를 수행하는 드라이버 루틴은 임의적인 쓰레드에 존재할 가능성이 높다는걸... 임의적인 쓰레드에 존재하면.. 유저 버퍼의 내용이 임의의 쓰레드 내용으로 바뀌겠지.. 그 상태에서 참조하면..- ;;
어쨌든 그렇고, 우리들은 MDL멤버를 직접 참조하면 안된데-0-, 대신에 MmGetMdlVirtualAddress 가상 메모리 주소 반환
MmGetMdlByteCount 버퍼의 사이즈를 반환
MmGetMdlByteOffset 물리 페이지에서 버퍼의 시작 옵셋 반환
MmGetMdlPfnArray 실제 물리 메모리의 페이지 번호를 가리키는 포인터를 리턴
요런 매크로를 이용하고, MDL을 할당 하려면 IoAllocateMdl루틴을 콜 하면 될것이고, 반대로 해제하려면 IoFreeMdl을 콜 하면 된데. 또한, 이미 할당된 버퍼의 내용을 초기화(포멧) 할수도 있는데, 그럴때는 MmInitializeMdl을 콜 하라고 한담.
이번에는 객체 관리 챕터야. 짧으니까 빨리빨리 끝내고 싶은데 빨리 끝날지는 의문..- _;
커널 오브젝트는 이름을 가질수도 있고, 이름이 없을수도 있어. 오브젝트 네임은 유니코드 스트링이고 유저모드나 커널모드 양쪽 모두에서 참조할때 그 이름을 이용해서 참조하지.
응.. 여기 나오네, 유저모드와 커널모드 컴포넌트들은 핸들을 얻는데 오브젝트 이름을 사용한다. 모든 연산은 핸들을 통해서 이루어진다.
만약 오브젝트가 이름이 없다면(unnamed) 유저모드 컴포넌트는 핸들을 열지 못한다. 반면에 커널 모드 컴포넌터는 포인터나 핸들을 통해 참조할수 있다.
근데 커널 오브젝트라는게 개념이 잘 안오지..-.-; 한가지 예를 들어보자.
\DosDevices\C:\Directory\File
얘는 말이야. \DosDevices 라는 오브젝트 디렉토리 안에 있는 C:를 나타내는 \C:라는 디바이스 오브젝트에 Directory라는 디렉토리를 나타내는 파일 오브젝트, File이라는 파일을 나타내는 파일 오브젝트를 의미해.
간단히 C:\Directory\File 이라는 경로를 디바이스 오브젝트를 통해 보자면 위와 같다는 이야기야. 근데, 내가 써 놓고도 감이 안온다. 왜! 오브젝트라는걸 쓰는걸까?
내가 오브젝트 디렉토리를 이야기 했잖아. 근데 이 오브젝트 디렉토리는 하드 어느 구석에 짱박혀 있는게 아니야. 얘는 오브젝트 매니저가 가지고(?) 있는거야. 대충 다음과 같은 최 상위 오브젝트 디렉토리들이 있지.
\Callbacks
\Device
\KernelObjects
\DosDevices
오브젝트 매니저는 오브젝트에 대한 참조 카운트를 보관하고 있어. 오브젝트가 생성이 되면 오브젝트의 참조 카운트를 1로 설정하지. 만약 참조 카운트가 0으로 떨어지면 오브젝트는 해제되는거야.
드라이버는, 이 카운트를 잘 살펴야해. 왜냐면, 참조 카운트가 0으로 떨어져서 해제된 오브젝트를 다시 해제하면 나쁜경우 시스템 크래시를 발생시킬수도 있그등. (뭐.. 의미가 약간 다르긴 하지만.. 비슷한 말일거라고 생각하고.. 스킵)
아, 물론 알고 있겠지만, 오브젝트는 핸들에 의해서도 참조가 되잖아. 그래서 결과적으로 오브젝트 매니저는 오브젝트에 대해 핸들 카운트와 참조 카운트, 이 두가지의 카운트를 보관하고 있어. 만약 핸들을 오픈하면 핸들카운트와 참조 카운트가 모두 증가하게 되. 물론 객체의 핸들을 열었으면(얻었으면?) 다시 닫아주는(돌려주는?)게 예의야.
앞의 이야기는 유저모드의 이야기였고. (핸들을 연 만큼 닫아 준다는거)..
커널 모드에서는 오브젝트를 포인터로 참조할수도 있어. (유저 영역에서는 못하는거얌).
IoGetAttachedDeviceReference함수는 오브젝트의 포인터를 리턴해 주거등. 그러면, 오브젝트 매니저는 오브젝트의 참조값을 1 올리겠지? 내가 위에서 오브젝트가 어떨때 소멸된다고 했더라? 참조값이 0이 되야지 소멸한다고 그랬지?.. 따라서 오브젝트를 다 썼다면 반드시 ObDereferenceObject함수로 오브젝트 참조값을 -1해줘야해.
오브젝트 보안에 관한 내용이 쪼금 있는거 같아서 쵸큼 첨부할께.
오브젝트는 타입별로 특정한 액세스 권한이 있어. 이걸 specific access right라고 해. 뭐 이런거야. FILE_READ_DATA는 파일 타입의 오브젝트에 특정한 액세스 권한이고, KEY_QUERY_VALUE는 레지스트리 키 타입의 오브젝트에 특정한 액세스 권한이야.
반면에 비슷한 operation을 몽땅 합쳐버린 제네릭한 액세스 권한이 있어, 이걸 generic access right라고 하지. 뭐 이런거야. 위에서 FILE_READ_DATA랑 KEY_QUERY_VALUE를 언급했잖아. 근데, 얘네가 하는일이 둘다 READ거등. 따라서 하는일이 같은거야. 오브젝트 타입은 다르지만..쩝..
그래서 GENERIC_READ라고 설정하면, FILE_READ_DATA랑 KEY_QUERY_VALUE랑 기타 등등의 권한을 모두 가지고 있게 되는거지.


덧글
냉이 2009/10/26 16:44 # 삭제 답글
감사합니다, DMA의 가상 메모리 사용상의 문제점과 해결법 찾으러 돌아다니다가 여기까지 왔어요, 잘읽고 갑니다.ㅋㅋ 이제 저는 MDL찾으러 가야겠어요ㅋㅋ
승네군 2009/10/26 19:42 # 삭제
초금 읽었는데.... 뭔가 뭔 말인지 못 알아먹게 적어 놨었군요.한마디로, 저거 신뢰하지 마시라능.. ㅎㅎ (하긴, 신뢰가 안가는 말투긴 하지만 ㅎㅎ )
PS. 비로긴 유저지만, 제가 주인장 맞음. ㅎㅎ : )
PS. 비로깅 유저가 주인장 노릇한다고 까일까봐.
PS. 하긴, 까이나 마나 어차피 넷상...ㅎㅎ;