16비트 Windows의 메모리 관리가 어떻게 작동하는지, 세그먼트, 핸들, DLL, 프롤로그/에필로그, 테스트 도구를 중심으로 설명하는 글입니다.
이 글은 16비트 Windows에서 메모리 관리가 정확히 어떻게 작동하는지 이해하려는 시도의 결과물인 일종의 지식 기반 문서입니다. 완전히 문서화되지 않은 것은 아니지만, 그렇다고 잘 문서화되어 있는 것도 아닙니다. Windows 3.0이 등장하기 전부터도, 사실상 모든 애플리케이션 개발자가 고급 언어를 사용하고 개발 도구가 저수준 세부 사항을 처리해 줄 것이라는 가정이 있었습니다.
게다가 초보 Windows 개발자를 위한 거의 모든 자료는 Windows 프로그래밍의 더 눈에 띄는 측면, 즉 창, 아이콘, 메뉴 등에 초점을 맞추었습니다. 메모리 관리는 대충 넘어가곤 했지만, Hello World 프로그램보다 조금만 더 복잡한 견고한 Windows 애플리케이션을 작성하는 데 절대적으로 중요했습니다.

Windows 3.0 SDK HeapWalker 메모리 분석 도구
메모리 관리의 세부 사항과 메커니즘은 Windows 1.x와 2.x의 8086 실모드 역사에 뿌리를 두고 있으며, Windows 3.1부터 Windows가 보호 모드에서만 실행되기 시작한 뒤에도 그 복잡성의 상당 부분이 지속되었습니다.
별도 언급이 없는 한, 이 글에서 “Windows”는 Windows NT가 아니라 Microsoft 제품의 16비트 계열을 가리킵니다.
Windows 메모리 관리를 이해하는 핵심은, Windows가 처음부터 여러 역할 중 하나로서 화려한 오버레이 관리자였다는 점입니다. 오랜 기간 동안 Windows는 당시의 일반적인 PC에 비해 너무 컸고, 가장 활발히 사용되는 메모리 세그먼트만 물리 RAM에 유지하면서 덜 자주 필요한 세그먼트는 필요할 때 버리고 다시 불러오는 메커니즘이 필요했습니다. 8086과 80286 시스템에는 페이징 지원이 없었기 때문에 당연히 페이징은 사용되지 않았습니다. 그리고 Windows 3.0 이전에는 그런 시스템이 거의 설치 기반의 전부였습니다.
코드 세그먼트 하나와 데이터 세그먼트 하나를 가진 애플리케이션의 가장 단순한 경우에는, Windows 세그먼트의 이동 가능성은 거의 완전히 투명합니다. 애플리케이션이 실행 중일 때 CS(코드) 세그먼트 레지스터는 코드 세그먼트를 가리키고 DS(데이터) 및 SS(스택) 세그먼트 레지스터는 데이터 세그먼트를 가리킵니다. 애플리케이션이 코드 세그먼트 내부에서는 근거리 호출/점프만 사용하고 데이터/스택 세그먼트에 대해서는 근거리 포인터만 사용하는 한, 세그먼트가 메모리의 정확히 어디에 있는지, 즉 CS/DS/SS 레지스터에 실제로 어떤 값이 적재되는지는 전혀 신경 쓸 필요가 없습니다. Windows가 세그먼트를 이리저리 옮겨도 모든 것은 정상적으로 동작합니다.
하지만 Hello World 스타일의 예제를 따라가는 초보 Windows 프로그래머조차도 16비트 Windows 세계가 그렇게 단순하지 않다는 의심을 아주 빨리 하게 됩니다. 윈도우 프로시저는 FAR PASCAL로 선언되어야 하는데, Windows 호출 규약을 따라야 하므로 그 자체는 충분히 이해할 만합니다. 그러나 그것은 애플리케이션의 실행 파일에서 반드시 export 되어야 하며, 그렇지 않으면 프로그램이 제대로 동작하지 않습니다. 이것은 비 Windows 개발자에게는 완전히 낯선 개념입니다.
메모리 관리 방식을 구현하는 데 도움을 주기 위해, Windows는 “DOS 4”에서 처음 사용된 “New Executable”(NE) 형식을 채택하고 확장했습니다. 이 “DOS 4”는 Multitasking DOS 4.0으로 더 잘 알려져 있으며, PC DOS 및 MS-DOS 4.0/4.01과는 상당히 다릅니다. 애플리케이션이 사실상 하나의 이진 덩어리인 DOS MZ 실행 파일 형식과 달리, NE 형식은 세그먼트 지향적이며 각 세그먼트가 디스크에 별도로 저장됩니다. 이것은 Windows에 개별 세그먼트를 적재(또는 재적재)하고 메모리 안에서 이동시킬 수 있는 능력을 제공합니다.
NE 형식은 또한 _imports_와 _exports_를 지원합니다. imports는 애플리케이션이 운영체제 자체 같은 외부 코드를 호출해야 할 때 사용됩니다. exports는 외부에서 호출되는 애플리케이션 코드에 사용됩니다.
윈도우 프로시저가 바로 그런 외부 호출 코드의 한 예입니다. Windows가 그 위에서 마법을 부릴 수 있도록, 그것은 export되어야 합니다. 그 마법이란 Windows가 윈도우 프로시저의 prolog(진입 시퀀스)를 수정하여 DS 레지스터에 애플리케이션 자체의 데이터 세그먼트를 적재할 수 있게 해 주는 것입니다.
Windows 메모리 관리의 모든 것은 세그먼트를 중심으로 돌아갑니다. 세그먼트는 최대 64KB 크기의 연속적인 메모리 블록입니다. 일반적인 8086 프로그래밍에서는 각 세그먼트가 세그먼트 주소로 식별되며, 이는 물리 메모리상의 주소와 직접 대응합니다. Windows의 대부분의 세그먼트는 이동되거나 버려질 수 있으므로, 대신 _핸들_로 식별됩니다. 핸들은 16비트 값이며, 실제로는 어떤 테이블의 단순한 인덱스일 수도 있지만 불투명한 값으로 취급해야 합니다.
x86 보호 모드에 익숙한 프로그래머에게 Windows 세그먼트 핸들은 보호 모드 셀렉터와 매우 비슷합니다. 즉, 메모리 세그먼트를 유일하게 식별하는 16비트 값이지만, 시스템 메모리 내 세그먼트의 위치와는 독립적입니다. 이 유사성은 우연이 아닙니다. Windows 1.0 메모리 관리 설계자인 Steve Wood는 Windows 메모리 관리자를 설계할 때 Intel 286 보호 모드를 영감의 원천으로 사용했습니다1. 286은 1982년에 출시되었고 Windows 개발은 1983년에 시작되었습니다.
핸들은 메모리 안에서 어디에 있든, 즉 8086 세그먼트 주소가 무엇이든 상관없이 메모리 세그먼트를 가리킵니다. GlobalAlloc API는 전역 힙에서 연속 메모리(64K를 넘을 수도 있음)를 할당하고 세그먼트 핸들을 반환합니다.
8086은 보호 모드를 지원하지 않기 때문에, 보호 모드 기능을 흉내 내려면 상당한 추가 작업과 규율이 필요합니다. 핸들은 세그먼트 주소가 아니므로 원거리 16:16 포인터의 세그먼트 부분으로 사용할 수 없습니다. 다른 세그먼트의 무언가를 주소 지정하려면, 애플리케이션은 원거리 포인터를 형성해야 합니다.
이를 위해 애플리케이션은 GlobalLock API를 호출해야 하며, 이 API는 세그먼트 주소를 반환하고 세그먼트를 메모리에 고정합니다(잠금 횟수를 증가시킴). 잠겨 있는 동안에는 세그먼트가 이동되지 않으므로 세그먼트 주소는 계속 유효합니다.
세그먼트 안의 메모리 접근이 끝나면 애플리케이션은 GlobalUnlock을 호출합니다. 그러면 세그먼트의 잠금 횟수가 감소하고, 그 값이 0이 되면 세그먼트는 다시 이동될 수 있습니다.
말할 필요도 없이, GlobalUnlock을 호출한 뒤에는 GlobalLock이 반환한 세그먼트 주소를 무효한 것으로 간주해야 합니다. 이것이 교묘한 버그의 원인이 될 수 있다는 점에 주의해야 합니다. GlobalUnlock 호출 후 세그먼트는 대개 즉시 이동하지 않습니다. 애플리케이션이 잠금 해제 후 이전에 잠겨 있던 세그먼트에 잘못 접근해도 눈에 띄는 문제가 전혀 발생하지 않을 수 있습니다.
실제로도 Windows는 필요하지 않으면 세그먼트를 이동하거나 버리지 않습니다. 다시 사용될 가능성이 있기 때문입니다. 그러나 세그먼트가 잠금 해제되면, Windows는 언제든 그것들을 이동하거나 폐기할 수 있습니다.
이제 가능한 세그먼트 유형을 좀 더 자세히 살펴보겠습니다.
Windows 세그먼트에는 Windows 메모리 관리자가 그것들을 어떻게 다룰지 결정하는 몇 가지 중요한 속성이 있습니다.
세그먼트는 고정(fixed) 이거나 이동 가능(movable) 할 수 있습니다. 이름 그대로, 이동 가능한 세그먼트는 잠겨 있지 않은 한 Windows에 의해 이리저리 이동될 수 있고, 고정 세그먼트는 제자리에 머뭅니다. 예를 들어 인터럽트 핸들러 루틴을 담고 있는 세그먼트는 인터럽트 벡터가 계속 유효해야 하므로 고정되어야 합니다. 이상적으로는 애플리케이션의 코드와 데이터 세그먼트 대부분이 이동 가능해야 하며, 그래야 Windows가 메모리를 효율적으로 관리할 기회를 얻습니다. 세그먼트를 이동할 수 있는 능력은 필요합니다. 세그먼트를 해제하거나 폐기하면 메모리에 “구멍”이 생겨 메모리가 빠르게 단편화될 수 있기 때문입니다. Windows는 세그먼트를 이동하여 압축할 수 있어야 자유 메모리를 하나 이상의 더 큰 덩어리로 통합할 수 있습니다.
세그먼트는 또한 폐기 가능(discardable) 하거나 비폐기(nondiscardable) 일 수 있습니다. 코드 세그먼트는 일반적으로 쓰기 가능하지 않으므로 폐기 가능합니다. 사용하지 않는 코드 세그먼트를 제거했다가 나중에 다시 필요하게 되면, Windows는 원래 실행 파일에서 그것을 쉽게 다시 불러올 수 있습니다. 읽기 전용인 리소스도 마찬가지입니다. 반면 데이터 세그먼트는 대개 비폐기성입니다. 보통 쓰기 가능하고, 한 번 수정되면 디스크에서 그냥 다시 불러올 수 없기 때문입니다. 그렇다고 해도, 세그먼트가 폐기된 뒤 다시 필요해질 경우 그 내용을 재생성할 의향이 있다면 애플리케이션은 쓰기 가능한 데이터 세그먼트도 폐기 가능하게 둘 수 있습니다.
동적 링크는 1980년대 중반에는 아직 널리 퍼진 기법이 아니었고, Microsoft Windows는 동적 링크 라이브러리(DLL), 즉 공유 라이브러리를 지원한 최초의 시스템 중 하나였습니다. 더 큰 규모의 일부 시스템은 1970년대부터 동적 링크를 사용했지만, UNIX 시스템이 공유 라이브러리를 도입하기 시작한 것은 1980년대 중후반이었습니다.
Windows DLL은 Windows 애플리케이션과 마찬가지로 NE 형식 이미지이지만, DLL은 애플리케이션이 아닙니다. DLL은 직접 실행할 수 없고, 다른 프로세스(Windows 용어로는 태스크)에 의해 적재되어 호출될 수 있을 뿐입니다. 실제로 Windows의 대부분은 DLL(KERNEL, USER, GDI)로 구현되었습니다.
DLL은 애플리케이션이 호출할 수 있는 루틴(진입점)을 export합니다. 애플리케이션은 링크 시점에 DLL에 대해 링크될 수 있으며, imports는 DLL 이름과 진입점을 참조합니다. DLL은 완전히 동적으로도 적재될 수 있고, 그 진입점은 ordinal(번호) 또는 이름으로 조회할 수 있습니다.
UNIX 시스템과 달리, Windows에는 동적 심볼 해석을 위한 전역 이름 공간이 한 번도 없었다는 점에 주의하십시오. DLL의 심볼은 항상 먼저 모듈 이름으로, 그다음 이름 또는 ordinal로 import되었습니다. 이 2단계 이름 공간은 관리에 약간 더 많은 노력이 필요하지만 이름 충돌을 피할 수 있습니다. 따라서 두 DLL이 모두 Alloc이라는 심볼을 export하더라도 어떤 것이 필요한지 혼동이 없습니다. 모듈 이름이 둘을 구별해 주기 때문입니다. 물론 이런 2단계 이름 공간이 없었다면 ordinal에 의한 import는(약간 더 빠르고 메모리도 덜 소비하지만) 완전히 비실용적이었을 것입니다.
Windows 프로그래밍과 관련하여 애플리케이션과 DLL 사이의 한 가지 중요한 차이는, DLL은 자체 스택이 없고 항상 호출자의 스택으로 실행된다는 점입니다. DLL은 거의 항상 자체 데이터 세그먼트를 가지지만, 그것은 스택 세그먼트와 다릅니다. 즉, SS != DS입니다.
이 차이는 DLL이 애플리케이션과 다르게 빌드되어야 함을 의미합니다. 컴파일러는 DLL용 코드를 생성하라는 지시를 받아야 하며, 더 구체적으로는 DS와 SS 레지스터가 같은 메모리를 주소 지정한다고 가정할 수 없다는 것을 알아야 합니다.
초기의 Windows에서는 DLL 진입점의 prolog와 epilog가 애플리케이션 prolog/epilog와 동일했습니다. 컴파일러 작성자들은 결국 애플리케이션의 prolog는 SS가 DS와 같으므로 단순화할 수 있다는 것을 알아냈습니다. 하지만 DLL에서는 그렇지 않으며, DLL은 여전히 Windows 모듈 로더가 수정해야 하는 옛 스타일의 “뚱뚱한” prolog를 사용해야 합니다.
Microsoft C는 아주 초기부터 Windows 개발을 지원했습니다. 즉, 버전 3.0부터였습니다. 그 이전 Microsoft C 버전은 이름만 바꾼 제3자 제품이었고, Microsoft C 3.0은 Microsoft가 직접 개발한 최초의 C 컴파일러로 처음에는 XENIX와 DOS용이었습니다.
하지만 오랫동안 이 지원은 거의 비밀에 가까웠습니다. Windows 전용 스위치는 컴파일러 문서에서 완전히 빠져 있거나, 나열되어 있더라도 사용자는 Windows SDK를 참조하라는 안내만 받았습니다. Microsoft C 5.1까지도 그랬습니다. 이 버전은 /Gw와 /Aw 스위치가 존재한다는 사실은 문서화했지만, 그것들이 무엇을 하고 어떻게 사용하는지는 설명하지 않았고 대신 Windows SDK 문서를 참조하라고 했습니다. 이는 Windows 개발 그룹과 Microsoft 언어 그룹 사이의 다소 근친적인 관계를 어쩌면 잘 보여 주는 예일지도 모릅니다.
Microsoft C 3.0(1985) 이후로 컴파일러에는 /Aw와 /Gw 스위치가 있었고(/Au 스위치도 있었습니다).
/Aw 스위치는 메모리 모델 수정자이며 SS != DS임을 지정하지만, 함수 진입 시 DS를 다시 적재하지는 말라고 지시합니다(왜냐하면 Windows가 그것을 처리하기 때문입니다). /Aw 스위치는 DLL 생성 시 사용하도록 되어 있습니다.
/Gw 스위치는 원거리 함수에 대해 Windows prolog와 epilog를 생성합니다. 이것은 애플리케이션과 DLL 모두에서 export된 함수에 필요하며, 매우 Windows 특유의 기능입니다.
그렇다면 Windows 전용 함수 prolog와 epilog는 정확히 어떤 모습일까요? 모든 것은 Windows SDK에 포함된 CMACROS.INC 파일에 명시되어 있습니다. 불행히도 CMACROS.INC는 MASM 조건부 어셈블리의 뒤죽박죽이라 사람이 읽기에는 거의 불가능합니다. C 컴파일러가 실제로 어떤 코드를 생성하는지, 또는 CMACROS.INC를 사용하는 어셈블리 코드가 정확히 무엇으로 바뀌는지를 보는 편이 훨씬 쉽습니다.
다음은 Microsoft C 3.0이 생성하는 코드로, 컴파일러가 만든 목록 파일에 표시된 내용이며 주석을 추가했습니다:
PUBLIC Proc
Proc PROC FAR
*** 000 1e push ds ; 거의
*** 001 58 pop ax ; no-op
*** 002 90 xchg ax,ax ; NOP
*** 003 45 inc bp ; marker
*** 004 55 push bp ; save BP
*** 005 8b ec mov bp,sp
*** 007 1e push ds
*** 008 8e d8 mov ds,ax ; reload DS
; Line 4
*** 00a 8b 46 06 mov ax,[bp+6]
*** 00d 03 46 08 add ax,[bp+8]
*** 010 83 ed 02 sub bp,2
*** 013 8b e5 mov sp,bp
*** 015 1f pop ds
*** 016 5d pop bp ; restore BP
*** 017 4d dec bp ; recover value
*** 018 cb ret
Proc ENDP
우선, 이 prolog는 실제로는 거의 아무 일도 하지 않으면서 많은 명령을 소비하는 것처럼 보입니다. DS를 푸시하고, 그것을 AX로 옮긴 다음, DS를 저장한 후 AX를 다시 DS로 옮깁니다. 또 BP를 증가시킨 뒤 스택에 푸시하고, 팝한 뒤 다시 감소시킵니다.
전체적으로 보면, 겉보기에는 아무것도 하지 않기 위해 많은 노력을 들이는 셈입니다. 하지만 실제로 그게 핵심입니다. Windows prolog와 epilog 코드는 필요하지 않을 때는 무해하도록 설계되어 있습니다.
만약 그 함수가 실제로 Windows NE 모듈에서 export된다면, Windows 로더는 처음 세 바이트를 패치하여 모듈의 기본 데이터 세그먼트를 AX에 적재하게 만듭니다. SYMDEB에서 보면 다음과 같으며, 예시는 임의의 GDI 함수에서 가져온 것입니다:
_TEXT:SELECTOBJECT:
5BC1:1840 B80591 MOV AX,9105
5BC1:1843 45 INC BP
5BC1:1844 55 PUSH BP
5BC1:1845 8BEC MOV BP,SP
5BC1:1847 1E PUSH DS
5BC1:1848 8ED8 MOV DS,AX
5BC1:184A 83EC04 SUB SP,+04
위의 경우 5BC1h는 GDI 모듈의 _TEXT 코드 세그먼트이고, 9105h는 GDI 모듈의 기본 데이터 세그먼트입니다.
Windows 메모리 관리자는 데이터 세그먼트가 이동하면 그것을 참조하는 export된 함수들이 새 주소를 가리키도록 다시 수정될 수 있게 이 prolog를 최신 상태로 유지합니다.
Windows .DEF 파일의 NODATA 키워드는 Windows에 함수 prolog를 패치하지 말라고 지시한다는 점에 주의하십시오. 이는 예를 들어 export된 진입점이 단순히 다른 export 함수로 점프하는 경우나, 함수가 데이터 세그먼트에 접근할 필요가 없는 경우에 필요합니다.
그렇다면 BP의 증가와 감소는 무엇을 위한 것일까요? Windows는 스택을 따라 올라갈 수 있어야 하며, 따라서 애플리케이션과 라이브러리는 Windows가 이해할 수 있는 형식으로 스택 프레임을 유지해야 합니다.
Windows 메모리 관리자가 세그먼트를 이리저리 이동할 때, 이미 스택에 푸시된 스택 프레임 안에서 그 세그먼트가 참조되고 있는지를 알아야 합니다. 예를 들어 Windows가 현재 실행 중인 코드에 직간접적으로 호출한 코드 세그먼트를 이동하려고 한다면, 그 상황을 감지하여 세그먼트를 이동하지 않거나, 이동한 뒤 스택을 조정해야 합니다. Windows가 할 수 없는 것은 세그먼트를 옮기고 스택은 그대로 두는 것입니다. 기본 데이터 세그먼트에 대해서도 마찬가지입니다.
비기본 데이터 세그먼트는 문제가 되지 않습니다. 그런 세그먼트는 잠겨 있어서 이동할 수 없거나, 잠금이 해제되어 있다면 올바르게 작성된 Windows 애플리케이션은 그런 세그먼트 안의 포인터를 유지하지 않기 때문입니다.
푸시 전에 BP를 증가시키는 것은 중요한 목적이 있습니다. 그것은 Windows에 그 BP 값이 원거리 함수에 의해 푸시되었음을 알려 주며, 즉 스택에는 오프셋과 세그먼트가 모두 존재하게 된다는 뜻입니다. 분명히 이 방식이 작동하려면 스택은 항상 워드 정렬되어 있어야 합니다. 다행히 Windows는 초기 상태에서 스택이 정렬되도록 보장하고, 그것을 어긋나게 만드는 데는 어느 정도 노력이 필요합니다(스택에 홀수 바이트 수를 푸시하는 쉬운 방법이 없기 때문입니다).
16비트 Windows를 16비트 OS/2와 비교해 보는 것은 유익합니다. 두 시스템은 여러 면에서 매우 가까운 친척이었습니다. 둘 다 같은 실행 파일 형식(NE)을 사용했고 차이는 사소한 수준이었습니다. 둘 다 세그먼트 기반 메모리 관리를 사용했습니다. 둘 다 Microsoft의 동일한 개발 도구를 사용했습니다.
보호 모드를 사용한다는 덕분에, OS/2는 프로그래머의 협력을 덜 필요로 했습니다. 보호 모드에서 세그먼트 셀렉터는 동시에 Windows 핸들의 등가물이자 세그먼트 주소였습니다. 따라서 프로그래머는 세그먼트를 조심스럽게 잠그고 푸는 데 신경 쓸 필요가 없었습니다.
OS/2 애플리케이션 역시 외부 호출 가능한 함수에 대해 특별한 prolog나 epilog 코드가 필요하지 않았고, NE 모듈에서 윈도우 프로시저 등을 명시적으로 export할 필요도 없었습니다. MakeProcInstance에 해당하는 것도 없었고, 그럴 필요도 없었습니다. 다시 말해 운영체제가 애플리케이션 스택을 풀어 올라갈 필요가 없었고, 진입점을 패치할 필요도 없었습니다.
80286 메모리 관리 하드웨어 덕분에, 세그먼트는 애플리케이션이 전혀 모르는 사이에 완전히 이동되고, 폐기되고, 재적재될 수 있었습니다. GlobalLock/GlobalUnlock이 필요 없었고, 따라서 프로그래밍 오류의 한 원천도 제거되었습니다.
Windows DLL과 마찬가지로, OS/2 DLL 진입점은 DS 레지스터를 DLL의 데이터 세그먼트로 설정하기 위한 특별한 prolog가 필요하긴 했습니다. 하지만 OS/2에서는 운영체제의 특별한 지원이 필요하지 않았습니다. 물론 OS/2 DLL도 마찬가지로 SS != DS를 나타내기 위해 /Aw 스위치 또는 그에 상응하는 옵션으로 빌드되어야 했습니다.
전체적으로 보면, 286 하드웨어가 무거운 작업의 상당 부분을 맡았고, 메모리 관리는 운영체제와 프로그래머 모두에게 더 적은 작업이었으며(버그가 들어갈 여지도 더 적었습니다).
Windows SDK는 Windows 메모리 관리를 강하게 시험하기 위해 설계된 도구들을 제공했습니다. 예를 들어 세그먼트 잠금/해제 오류와 관련된 문제는 메모리 압박이 없고 잘못 관리된 세그먼트가 제자리에 계속 머무르면 드러나지 않습니다. 이런 버그는 숨어 있을 수 있으며, 최악의 경우 재현하기 어려운 상황에서만 나타납니다.
Windows 1.0 SDK의 SHAKER 도구는 메모리를 “흔들어” 세그먼트가 폐기되고 이리저리 이동되도록 강제하는 데 사용되었습니다. 이는 메모리 관리를 압박하여, 일반적인 조건에서는 잠복해 있을 메모리 관리 버그를 드러내기 위한 것이었습니다.

Windows 1.x SDK의 Shaker와 HeapWalker 도구
또 다른 도구는 HEAPWALK였는데, 주로 현재 할당된 세그먼트와 그 소유자를 표시할 수 있는 진단 유틸리티였습니다. 하지만 HEAPWALK는 사용 가능한 모든 메모리를 할당한 뒤 1K 단위로 해제하여, 메모리 부족 상황을 시뮬레이션할 수도 있었습니다.

Windows 3.0 SDK 버전의 Shaker
Shaker와 HeapWalker는 Windows 3.0 SDK에도 여전히 포함되어 있었습니다. 적어도 메모리 관리 측면에서 보면 실모드로 실행되는 Windows 3.0은 Windows 1.0과 최소한의 차이만 있었기 때문입니다.
이 도구들은 필요했습니다. Windows의 메모리 관리는 정교했지만, 이를 뒷받침할 하드웨어가 부족했기 때문입니다(확실히 보호 모드에서 실행되는 Windows 3.0 이전에는 그랬습니다). 할당되지 않은 메모리에 접근하려는 시도 같은 오류를 하드웨어가 잡아 주는 대신, 프로그래머는 특수한 도구를 사용해 오류를 유도하려고 시도하고 버그가 눈에 보이는 방식으로 드러나기를 기대해야 했습니다. 이것은 정확한 과학이 아니었습니다. 8086 아키텍처에서는 모든 메모리 주소가 유효했고, 읽기와 쓰기는 항상 성공했기 때문입니다.
Windows 3.1 SDK는 Shaker 도구를 Stress라는 새 유틸리티로 대체했습니다. 이 도구는 다양한 Windows 내부 힙의 제한된 메모리, 디스크 공간 고갈, 파일 핸들 고갈 등 저자원 조건에서의 애플리케이션 동작을 시험하도록 설계되었습니다.

Windows 3.1 SDK Stress 도구
Windows 3.1은 보호 모드에서만 실행되었기 때문에 이전의 몇몇 메모리 관리 문제는 더 이상 해당되지 않았지만, 저자원 조건은 여전히 그 어느 때보다 중요했습니다.
16비트 Windows는 상당히 정교한 메모리 관리 시스템을 도입했습니다. 하드웨어 지원이 부족했기 때문에 애플리케이션 프로그래머에게는 상당한 규율이 요구되었습니다. 잘못된 컴파일러 스위치를 사용했거나, 함수가 올바르게 export되지 않았거나, 세그먼트가 정확히 잠기고 해제되지 않았다면… 그다음은 아무도 장담할 수 없었습니다.
1. Peter Norton’s Windows 3.0 Power Programming Techniques, Peter Norton and Paul Yao, 1990, page 613.