Programming/Win32 API2008. 12. 1. 15:35
Win32 Global API Hook - 제2강 다른 프로세스의 주소공간으로 !! (1)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

 지난 강좌의 내용이 어땠는지 모르겠군요. 첨부터 반말로 써서 혹시 기분이 상하셨을까봐, 이번 강좌부터
는 처음에 간단한 인사를 드리고 시작하겠습니다. ^^ 생각보다 많은 사람들이 보신것 같은데요. 실제로 코
딩을 해가면서 디버깅까지 해보셨다면 별로 어려울게 없었을거라고 믿습니다.
 자 이제 이번 강좌부터는 약간의 레벨업이 필요할듯 한데요. 이번 강좌에서 다루는 내용은 SOFTICE,
BOUNDS CHECKER 등의 디버깅툴로 유명한 NUMEGA SOFTWARE의 시스템 엔지니어인 Matt Pietrek의 아이디어에
서 빌어온 것임을 밝히며, API HOOKING의 원리를 이해할수 있을것으로 생각됩니다. 또 기운이 빠지는 얘기
일지도 모르겠지만 이번 강좌의 내용으로도 우리가 원하는것(첫 강좌에서 밝혔죠?)을 완벽하게 이룰수는 없
다는점입니다. 그렇지만 이 내용을 모르고는 다음 강좌로 넘어갈수는 없다는 판단에 두회에 걸쳐 강좌를 진
행하도록 하겠습니다. 그럼, 담배한대 피우고 가보기로 하죠 ^^

(강좌의 진행상 존칭을 생략합니다.)

1. CPU

 시스템프로그래밍에서의 CPU의 중요성은 말할필요가 없을것이다. CPU 아키텍쳐는 무엇보다도 중요하고 기
본적인 내용이지만 이 강좌에서 그것을 다룰수는 없다. 자세한것은 각자 자료를 찾거나 책을 보면서 익히도
록하고 우리는 여기서 CPU가 프로그램을 어떻게 실행하는지에 관해서만 언급하고자 한다. 운영체제가 실행
파일을 메모리에 로드하게 되면 정해진규칙에 따라 실행시작주소(보통 엔트리포인트라고 말한다.)를 찾고
그곳으로 제어를 넘긴다. (여기서 제어를 넘긴다는 말은 IP(인스트럭션 포인터)가 지정된곳으로 세팅된다
는 말과 같다.) 그렇게 되면 CPU는 그곳에서부터 정해진 바이트씩 읽어오면서 그것을 해석하며 실행해나간
다. 이러한 작업을 두고 명령어가 패치된다고 말한다. 당연히 CPU가 해석하는 명령어는 기계어코드이며 어
셈블리로 1:1 대응시킬수 있다. 이것만 기억하고 넘어가자, CPU는 기계어로 된 명령어를 해석해서 순차적으
로 실행해 나간다. 혹시 OOP 프로그래밍이나 이벤트드리븐방식(Windows같은)의 프로그래밍에만 익숙하다
면, 프로그래밍 방법론은 변했어도 CPU가 명령어를 처리하는 그순간은 그 옛날 도스시절이나 지금이나 별
반 다를게 없다는것을 명심하기 바란다.

2. Win32의 메모리 관리

 지난강좌에서도 잠깐 언급한적이 있지만, 시스템프로그래밍에서 메모리관리는 빠질수 없는 주제이다. 대부
분의 CPU와 운영체제가 보호모드를 지원하는 최근에 와서는 더더욱 중요한 주제가 되었고 우리도 당연히 짚
고 넘어가야 하겠다. 지면과 시간의 한계로 메모리 관리의 많은 부분을 다룰수 없는것을 안타깝게 생각한
다. 역시 이부분도 우리가 다루려는 핵심만 짚고 넘어가야 할듯하다. CPU는 메모리에 있는 데이터를 다룬
다. CPU가 디스크를 엑세스한다? 말도 안되는 얘기다. CPU는 모든것이 메모리라고 생각하면서 작업한다.
(물론 이말에 대해서 반론의 여지가 있는 사람도 있겠지만, 일단은 이렇게 생각하고 강좌를 진행하는 것이
이해가 빠를것 같다.)
 
 CPU의 실행모드에는 리얼모드와 보호모드의 두가지가 있는데, 리얼모드는 본 강좌와 무관하니 언급하지 않
기로 하고, 보호모드에 대해서만 얘기해보자. 메모리관리에서 웬 보호모드냐 하겠지만, 말하고자 하는것은
보호모드의 특징중의 하나인 가상메모리 메카니즘을 말하려고 함이다. 가상메모리라고 하는것은 말그대로
진짜메모리가 아니다. 단언하건데 여러분이 디바이스드라이버나 하드웨어를 제작하는 사람이 아니라면 여러
분이 지금까지 알고 있었던, 사용해왔던 메모리는 모두 가상메모리였을것이다. 보호모드에서 가상메모리는
연속된다는 의미로 선형메모리(leaner 또는 flat memory)라고도 한다. (앞으로 선형메모리와 가상메모리는
같은 의미로 사용하겠다.) 가상메모리로 인해서 실제로 시스템에 장착된 메모리보다 큰 메모리를 우리는 사
용할수 있었던 것이다. 이런 얘기는 들었을것이다. Windows는 모두 4기가바이트의 메모리를 사용할 수 있는
데, 그중 응용프로그램이 사용하는 영역은 0-2기가바이트이며, 그 이상은 운영체제가 사용한다. 이것 역시
가상메모리이며, 윈도 운영체제는 가상메모리를 페이징메카니즘이란 방법으로 관리한다.
 
 그럼 페이징메카니즘이란 도대체 무얼 말하는것인가? 페이지는 시스템이 인식하는 가상메모리의 단위이
다. 페이지는 CPU와 운영체제에 따라 그 크기가 다양한데 보통 Intel CPU의 윈도운영체제일 경우 보통 4K
의 크기를 갖는다. 가상메모리는 페이지 단위로 스왑되거나 맵된다. 여러분이 단지 한바이트의 메모리만 할
당한다하더라도 시스템은 하나의 페이지를 준비한다. 또 하나의 중요한 사실은 하나의 페이지는 연속된다
는 것이다. 이말은 하나 이상의 페이지는 실제로는 연속되지 않을수도 있다는 말이다. 보호모드에서의 가상
메모리는 리얼모드와 달리 산술적인 연산으로 물리주소로 연결되지 않으며, 페이지디렉토리와 페이지테이블
이라는 자료구조를 통해서 실제메모리(물리메모리)로 연결된다. 실제로 가상메모리상에서는 연속되는 메모
리영역이라 하더라도 실제 물리메모리상에서는 연속되지 않을수도 있다.
 
 이러한 개념은 매우 중요한데, 예를 들면 이러하다. 내가 만약 1메가바이트의 메모리를 할당했다고 하더라
도 실제로 그 메모리는 물리주소상에서 연속된다고 보장할수는 없다. 가상메모리가 3페이지에 걸쳐서 존재
한다면 그에 따르는 물리주소도 3페이지만큼이 존재하지만 그들의 위치는 우리가 예상하는대로 배치되지 않
을수 있다는것이다.
 
3. 프로세스와 메모리

 내가 만든 A라는 프로그램이 있다고 치자. 그놈이 어떤 메모리를 할당했는데 그것의 시작주소가
0x70000000 이었다고 가정하자. 그런데 B라는 프로그램도 메모리를 할당하는데 그것의 시작주소 또한
0x70000000 이었다고 한다면, A, B 두 프로세스가 가지고 있는 이 두개의 메모리 영역은 과연 실제로는 어
디에 존재할까? 주소는 같지만 실제로 둘은 전혀다른 메모리이다. 둘다 같은 가상주소값을 가지고 있지만
둘은 엄연히 물리주소상에서는 다른 곳을 가리키고 있다. 이것이 어떻게 가능할까? 시스템은 현재프로세스
가 변할때(태스크 스위칭이 일어날때)마다 페이지디렉토리의 내용을 갱신한다. 윗부분에서 가상주소는 페이
지디렉토리와 페이지테이블를 통해 물리주소와 연결된다고 했다. 따라서 이들이 변한다는것은 실제 가상주
소가 가리키는 실제주소(물리주소)가 변한다는 말과 같다. 이러한 원리로 프로세스 A와 B는 서로의 공간을
전혀 알수가 없으며 이로 인해서 운영체제가 더욱 견고해지는 것이다.
 
 그렇다면 만약 10메가 바이트의 메모리를 사용하는 프로세스 10개가 동시에 동작한다면 100메가 바이트의
물리메모리가 필요할까? 반드시 그렇지는 않다. 왜냐면 위에서 말한 페이지 스왑이라는 기법을 운영체제가
지원하기 때문이다. 운영체제는 어떠한 페이지가 현재 필요하지 않다고 판단되면 그것을 디스크에 기록한
후, 물리메모리에서 해제한다. 그러다가 그 페이지가 다시 필요한 시점에서 디스크에 보관된 페이지를 다
시 물리메모리로 로드한다. 이러한 일련의 작업들로써 응용프로그램들은 현재 시스템에 장착된 메모리보다
더 큰 메모리를 사용할수 있는것이다. 우리가 자주보는 시스템 오류중 하나인 페이지 폴트(page fault)는
바로 이러한 페이징에 오류가 생겼을때 발생하는데 대표적인 경우는 현재 물리메모리가 할당되어지지 않은
(디스크 스왑된) 페이지를 마치 메모리에 존재하는 페이지처럼 접근하려고 할때이다. 시스템은 기본적으로
이러한 에러를 예외핸들러를 설치해서 복구하게된다.
 
그렇다면 이제 프로세스간에 주소공간을 공유할수 있는가라는 문제에 대해 생각해보자. 계속 이론만 늘어놓
았으니 이번에는 간단한 예제를 통해서 확인해보도록 하자. 아래에 리스트된 코드를 보자.

PVOID p1 = malloc(16);
if(NULL == p1)
    return -1;
memset(p1, "A", 16);
*((char*)p1 + 15) = "\0";

HANDLE hMap = CreateFileMapping(INVALID_HANDLE_VALUE, NULL,
    PAGE_READWRITE|SEC_RESERVE, 0, 16, NULL);
PVOID pMap = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 16);
PVOID p2 = VirtualAlloc(pMap, 16, MEM_COMMIT, PAGE_READWRITE);
if(NULL == p1)
    return -1;
memset(p2, "B", 16);
*((char*)p2 + 15) = "\0";

printf("p1:0x%08x \n", p1);
printf("dump:%s \n", p1);
printf("p2:0x%08x \n", p2);
printf("dump:%s \n", p2);

printf("press any key... \n");
getch();

if(NULL != p1)
    free(p1);
if(NULL != p2)
    VirtualFree(p2, 16, MEM_DECOMMIT);
if(NULL != pMap)
    UnmapViewOfFile(pMap);
if(NULL != hMap)
    CloseHandle(hMap);

간단히 설명하면 일단 malloc()으로 16바이트만큼 메모리를 할당한다음 문자 "A"로 채운다음 그 주소와 내
용을 화면에 출력한다. 그런다음 메모리맵파일을 생성한후 문자 "B"로 채운다음 그 주소와 내용을 화면에
출력한다. 그리고 잠시 사용자 키입력을 기다린후, 입력이 들어오면 메모리를 해제하고 프로그램을 종료한
다. 메모리맵파일에 관한 자세한 설명은 MSDN을 참고하길 바라며, 일단 컴파일한후 실행시켜보자.

p1:0x00780eb0
dump:AAAAAAAAAAAAAAA
p2:0x85536000
dump:BBBBBBBBBBBBBBB
press any key...

필자의 시스템에서는 위와같이 출력되고 사용자 입력을 기다리는 상태가 되었다. 키입력을 하게되면 메모리
를 해제하고 프로그램을 종료하게 되므로 일단 저상태로 내버려 두고 p1과 p2의 주소만 잘 적어두고 두번
째 프로그램을 작성하자.

printf("dump:%s \n", 0x00780eb0);
printf("dump:%s \n", 0x85536000);

먼저 작성한 프로그램의 p1, p2의 주소를 출력하는 코드이다. 혹시 그냥 Cop & Paste하는 사람이 있을까봐
얘기하는데, 하드코딩된 주소는 당연히 앞서 작성한 프로그램에서 출력된 주소를 적어주어야 할것이다.
자, 두번째 프로그램을 컴파일한후 실행시켜보자.

dump:emTest2.exe
dump:BBBBBBBBBBBBBBB

어떤가? 확실히 이해가 되는가? 결론부터 말하자면 malloc으로 할당한 메모리의 주소는 다른 프로세스에서
는 쓸모없는 무효한 주소가 된다. 그러나 메모리맵파일로 할당한 메모리의 주소는 다른 프로세스의 주소공
간에서도 여전히 유효한것을 볼수 있다. (혹시 운이 없는 사람은 두번째 프로그램을 실행시키다가 시스템
이 죽거나 블루스크린을 만났었을지도 모른다. 아마 대부분 그러지 않았을거라 확신하지만... ^^) 여기서
중요한것은 바로 다른 프로세스간에도 유효한 메모리와 무효한 메모리의 주소이다. 주소는 시스템마다 약간
씩 차이가 있었겠지만 분명한것은 malloc으로 할당한 메모리의 주소는 0x80000000보다 작았을것이고, 메모
리맵파일로 할당한 메모리주소는 분명히 0x80000000보다 큰 주소로 할당되었을것이다. 0x80000000은 10진수
로 2147483648, 즉 정확히 2기가바이트이다. 우리는 이제 주소만 보고도 이것이 프로세스 전용메모리인지
아니면 시스템에서 공유되는 메모리인지를 구별할 수 있을것이다.

4. 끝으로

 필자의 생각으로 이번 강좌는 여러분들에게 정말로 지루하고 재미없는 강좌였을것이다. 대부분 이론적인
내용뿐이니 말이다. 하지만 이번에 다룬내용은 모두 우리가 앞으로 해야할 작업의 기초가 되는 내용들이니
지루하더라도 꼭 이해하고 넘어가기 바란다. CPU와 메모리, 프로세스에 관한 내용은 이것말고도 굉장히 중
요한 내용들을 포함하고 있으니 다른자료나 참고서적을 통해서라도 꼭 살펴보길 바란다. 참고서적을 추천해
달라는 분이 계셨는데 지금은 시스템프로그래밍에 관한 책이 여러권 나와있지만 필자가 살펴본 바로는 1강
에서도 언급한 Jeffrey Ritcher의 Advanced Windows라는 책이 볼만할것이다. 한글번역본도 있으니 꼭 구해
다가 한번씩 읽어보기 바란다. 이책은 어플리케이션 프로그래머를 위한 시스템프로그래밍 서적이지만 워낙
유명한 책이니 책장에 꽂아놓는것만으로도 의미가 있을듯 싶다. 그밖에 역시 1강에서 언급한, NUMEGA
SOFTWARE의 시스템 엔지니어인 John Robbins의 Debugging Applications 라는 책 또한 참고할 내용이 많다.
나중에 NUMEGA SOFTWARE의 제품인 SOFTICE라는 디버거를 사용하게 될터인데(그때보면 알게되겠지만 정말 엄
청난 프로그램이라 하지않을수 없다. 필자는 SOFTICE가 없는 시스템프로그래밍 디버깅을 생각할수조차 없
을 정도이다.) 이책은 디버깅을 위한 책이라고 할수있지만 시스템에 관한 내용도 다루어지며, 아마 이책을
읽고나면 그럴듯한 디버거를 하나 만들수 있을것이다. 더 깊은 내용을 다루는 서적이나 자료를 알고싶다면
추후에 디바이스드라이버 강좌를 진행할때 소개하기로 하자.

5. 다음 강좌에서는...

 다음 강좌는 "제2강 다른 프로세스의 주소공간으로 !!" 두번째 시간이다. 원래 이번 강좌에서 다룰예정이
었던 Debugging API와 그것을 이용한 간단한 디버거를 작성해보고, 다른 프로세스 공간에 우리의 모듈을 삽
입하는 방법을 알아보자. 사실 강좌를 진행하다보니 욕심이 지나쳐 예상보다 진도가 늦어진 것같다. 애초 2
강을 두번에 걸쳐서 진행하려 했으니 다음 강좌는 좀 타이트한 진행이 될것 같다. 그렇지만 이번 강좌보다
는 덜 지루한 내용으로 채워질 예정이니 너무 걱정마시길...

(P.S) 추석연휴라서 고향에 내려왔다가 잠시 짬을 내서 올립니다. 이론위주의 강좌를 진행하다보니 끝도 없
이 진행될것 같아서 일단 필요하다고 생각되는 부분만 수박겉핡기 식으로 살펴보았습니다. 추석연휴에 고향
에 가시는 분들도 계실테고, 고향에 못가고 일하시는 분들도 계시겠지만 마음만이라도 즐거운 한가위가 되
시길 기원합니다. 
Posted by skensita