가상 메모리에 대한 이론적인 배경과 구현(?)에 대한 내용들은 웬만치 다 보았습니다. 그렇다면, 우리가 사실상 활용하게 되는 windows Memory API에 대해서 오늘은 다루어 보도록 합시다.
사실, 여기서 부터는 OS의 내용이라고 하기에는 애매~합니다.
시스템 프로그래밍..?이라고 할까요. 하지만 OS를 만드는것도 결국은 시스템 프로그래밍이기 때문에 그렇게 칼로 자르듯이 나누는건 지양해야한다고 생각합니다.

window에서는 메모리를 직접 할당받을 수 있게 VirtualAlloc()이라는 함수를 지원합니다.
LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress, // 활용할 주소
[in] SIZE_T dwSize, // 크기
[in] DWORD flAllocationType, // 메모리 상태
[in] DWORD flProtect // 페이지 속성
);
예를들어, 1MB를 할당받고 싶다면, 아래와 같이 활용하면 됩니다.
VirtualAlloc(NULL, 1 * 1024 * 1024, MEM_COMMIT, PAGE_READWRITE));
여기서, lpAddress에 NULL을 넣는 의미는 Window에게 알아서 너가 빈 공간을 찾아서, 내가 원하는 만큼의 크기를 내놔! 라는 의미입니다. Window는 해당 프로세스의 VAD를 살펴보면서, Virtual Address 중 가용가능한 Virtual Address 주소를 주게됩니다.
특정 포인터를 지정해서 쓸 수 있습니다만, 그 가상 주소공간이 누가 쓸지는 프로그래밍 할 시점에는 파악이 어려우므로, 대부분 NULL을 씁니다.
메모리를 해제할 때는 쉽습니다. VirtualFree()함수를 이용하면 됩니다.
BOOL VirtualFree(
[in] LPVOID lpAddress, // 주소
[in] SIZE_T dwSize, // 사이즈
[in] DWORD dwFreeType // decommit or Release
);
예를들어, 아까 받은 1MB를 해제한다면, 아래와 같이 활용이 됩니다.
VirtualFree(pCommitMemory, 0, MEM_RELEASE);
주의할 점이, 해제시에는 할당받은 사이즈만큼 해제가 가능하기 때문에, 할당받은 일부를 해제한다는 개념은 없습니다. dwSize는 Decommit(Commit -> Reserve)할 때 사이즈를 입력하기 위해서 있는 매개변수입니다.
지연할당
VirtualAlloc()으로 1MB를 Call한다고 진짜 Ram 1MB를 그대로 쓰지 않습니다. PTE를 만들어놓고, 실제로 접근시(실제로 활용되는 시점)에 page Fault가 발생되면서 OS가 Trap으로 처리하게 됩니다.
윈도우는 메모리를 Reserve -> Commit 상태를 거쳐서 활용합니다.
프로그램은 기본적으로, 연속된 메모리 공간을 활용할 확률이 높습니다. 따라서, 메모리 예약은 1페이지보다 큰 16Page 단위인 64KB를 예약으로 걸어놓습니다. 따라서, 가상 메모리 공간은 결국 64KB단위로 Reserve가 되게 됩니다.

실제로 가상 메모리 공간에서 활용한다고 결정한 공간 입니다. 페이지 단위(4K)로 커밋이 가능합니다.
읽기,쓰기 모두 못하는 NoAccess 구간이 되버림.
DWORD* pReservedMemory = reinterpret_cast<DWORD*>(VirtualAlloc(NULL, 1 * 1024 * 1024, MEM_RESERVE, PAGE_READWRITE));
DWORD* pCommitMemory = reinterpret_cast<DWORD*>(VirtualAlloc(pReservedMemory, 4 * 1024, MEM_COMMIT, PAGE_READWRITE));
*pCommitMemory = 100;
*((char*)pCommitMemory + 4095) = 'a';
*((char*)pCommitMemory + 4096) = 'b';


즉, 64KB로 Reserve 한뒤, 16Page 중 활용하지 못하는 Page들은 그냥 .. 버려지는 공간으로 생각하면 된다. 가상주소 공간이 낭비되는거라, 실제 Ram이 낭비되는건 아니라서 메모리 소모랑은 관련이 없지만 연속된 가상주소공간은 CacheIndex도 넓게넓게 펼쳐서 쓸 수 있기 때문에, cache Hit율을 높일 수 있는 수단 중 하나일 수도 있는데.. 아깝다는 생각이 들수있습니다.
(물론, 캐시라인은 64byte라서 한 페이지 안에서도 cacheLine이 여러개 나오긴하지만..)
어.. 우리가 흔히 이용하는 Stack은 어떻게 그럼..연속적인 공간을 보장받는거지? 라는 의문이 들 순 있지만 그건 뒤에 다루겠습니다.
PTE에서 관리하는 페이지에 관한 속성입니다. 접근금지, 읽기만가능, 가드, Read&Write(수정가능) 등의 속성이 있습니다.
호출 프로세스의 가상 주소 공간에서 커밋된 페이지의 영역에 대한 보호를 변경하는 함수입니다.
BOOL VirtualProtect(
[in] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flNewProtect,
[out] PDWORD lpflOldProtect
);
ReadOnly영역으로 바꾸고 싶다면 아래와 같이 활용하면 됩니다.
DWORD* pCommitMemory;
DWORD oldProtect;
VirtualProtect(pCommitMemory, 4 * 1024, PAGE_READONLY, &OldProtect);
운영체제에서 Stack 공간은 특정가드 영역을 갖고, 해당 가드 영역에 접근시 OS가 예외처리를 하면서 해당 영역을 쓸 수 있도록 계속 확장해가면서 1MB공간을 보장하도록 합니다.
esp가 0x00cffa34 나와서, 뒤를 000으로 해주면서 위로 올라간 결과
0x00CFD000 윗 부분이 Guard로 나왔습니다. 이때, 0x00CFD000 윗부분을 건드리면 원래라면 엑서스 금지가 발생해야하는데, 건드리면 ??로 나오던 부분이 0으로 초기화 되면서 줄겁니다.

0x00CFCFF8을 찌르면, ?? 가 뿅하고 없어지고, 성공적으로 실행되는 모습을 볼 수 있습니다.

왜 0을 보장?
저번글에서 Zero demanding을 보장한다고 했습니다.
이런식으로, Stack은 Guard영역을 통해서 스택 위쪽을 건드리면 OS가 Pagefault가 발생해도 그건 정상적인거야~ 하면서 처리를 해주게됩니다. Guard의 크기는 8K정도라고 알려져있고, 64bit에서는 12K로 알려져있습니다.
여기서, 가드보다 큰 곳을 내가 찌른다면, 지역변수를 가드보다 큰 사이즈를 내가 선언한 후 활용한다면 어떻게 될까요? 그건 에러일까요?
직접 확인 해보시면 흥미로운 결과를 얻을 수 있습니다.
VirtualAlloc()은 정말 정직한 System에게 부탁하는 것이기 때문에, SystemCall입니다.

따라서, 매번 SysCall을 하기에는 부담스럽기 때문에 Heap이라는 메모리 관리자가 OS에는 기본적으로 있습니다. Malloc()을 하면 사실 Heap(메모리 관리자)가 자기가 관리하고 있는 메모리의 일부분을 주는 개념입니다.
따라서, New -> Malloc -> HeapAlloc() -> VirtualAlloc() 4단계를 거치면서 메모리를 할당시키고, 이 과정에서 HeapAlloc()은 ntdll에 구현되어있기 때문에, KernelMode까지 가지 않고 메모리를 관리하게 됩니다.
DLL은 동적 라이브러리라는 친구인데, 런타임에 다양한 프로세스들이 동시에(?) 공용으로 사용하는 코드입니다. 예를들어, 윈도우에 CreateFile()이라는 함수를 Call했을 때, CreateFile은 윈도우 커널의 어떤 코드가 돌아가면서 파일에 접근하고 파일을 생성할겁니다.
근데, 다양한 프로세스들이 해당 CreateFile을 이용할 때마다 해당 코드를 실행하는 것보다, 공통된 공간 1개를 잡아놓고 여러 프로세스들이 Read/Only만 하겠다고 약속하고 서로 읽기만 한다면 메모리 공간을 효율적으로 쓸 수 있다는 생각입니다.
Copy on Write는 DLL이 하나의 코드 파일만 바라보는 상태에서, DLL도 코드니까 내부적으로 전역변수와 같은 걸 갖고있는데, 다른 프로세스에서 해당 전역변수를 어쩔 수 없이 건드리는 상황일 때, 어떻게 동작하는지를 설명해주는 부분입니다.
예를들어, 출력하는 함수는 각 프로세스마다 값이 달라질텐데.. 하나의 프로세스에서 값을 변조했다고 다른 프로세스까지 영향을 받으면 안됩니다.

ReadOnly이기 때문에, PageFault가 발생하고 이 에러는 OS가 Trap으로 받아서 다른 메모리 공간에 해당 페이지를 복사 해준뒤, write요청된 부분만 변경한 뒤 요청한 프로세스를 연결해줍니다.

이걸 Copy on Write라고 합니다.
저희가 프로그래밍을 할 때, Paging이 되는 대상들이 대부분이지만, 고정된 메모리 주소에 항상 떠있어야하는 친구들이 있습니다.