윈도우 인터널즈
메모리 관리 02 - 힙
묭묭.cpp
2024. 10. 11. 11:57
메모리 관리 02 - 힙
힙 관리자(Heap manager)
- VirtualAlloc은 큰 페이지 단위를 할당하는 함수
- 미리 예약된 큰 영역의 메모리 내에서 크기가 작은 메모리 할당을 지원하는 역할
- 힙 관리자 내에서 할당 단위
- 32비트 시스템 8바이트
- 64비트 시스템 16바이트
- 작은 영역의 메모리 사용 시 메모리 사용성과 성능을 최적화하기 위해 만들어짐
대표적인 윈도우 힙 함수
- HeapCreate, HeapDestroy : 생성 삭제
- HeapAlloc, HeapFree : 할당 해제
- HeapReAlloc : 기존 할당된 영역의 크기를 변경
- HeapLock, HeapUnLock : 힙 조작에 대한 상호 배제(Mutual Exclusion)을 제어
- HeapWalk : 힙 내의 엔트리와 영역을 열거함
- GetProcessHeaps : 프로세스의 모든 힙을 담고 있는 배열을 구함
프로세스 힙
- 프로세스는 기본 힙이라는 최소 하나의 힙을 가짐
- 프로세스 시작 시 생성되고 종료될 때까지 사라지지 않음
- 기본 크기는 1MB, /HEAP 링커 플래그를 통해 크기 변경 가능
힙은 메모리 맵 파일 객체의 할당 또한 관리할 수 있음
- 윈도우 API에 노출되어 있지 않음
- 두 프로세스 간 혹은 커널 모드 유저 모드 컴포넌트 간의 블록 내용의 공유가 필요할 때 용이
- Win32k.sys는 GDI 객체와 유저 객체를 유저 모드와 공유하기 위해 사용
메모리 맵 파일 기반의 힙이 가지는 제약
- 내부 힙 구조체는 포인터를 사용하므로 다른 프로세스의 다른 주소로 재매핑을 허용하지 않음
- 멀티프로세스 간 또는 커널 컴포넌트와 유저 프로세스 간 동기화는 힙 함수에서 제공되지 않음
- 유저 모드와 커널 모드간 힙이 공유되는 경우 유저 모드 매핑은 읽기 전용으로 하여 힙의 내부 구조를 덮어쓰는 것을 방지해야 함
- 만약 커널 모드가 사용하는 메모리를 덮어쓰게 되면 시스템 크래시 발생
힙 유형
윈도우 10 이전에는 NT 힙이라는 하나의 힙 유형만 존재
- NT 힙은 사용된다면 저단편화 힙(LFH, Low Fragmentation Heap)을 구성하는 Front-end 계층에 의해 확장됨
- 모든 프로세스에 의해 사용됨
윈도우 10부터는 세그먼트 힙(Segment heap)이라는 새로운 힙 유형 도입
- 모든 UWP 앱과 일부 시스템 프로세스에 의해 사용됨
NT 힙
NT 힙은 힙 프론트엔드와 힙 백엔드(Core) 두 계층으로 구성
힙 백엔드 계층
- 기본 기능을 처리하며 세그먼트 내부의 블록 관리와 세그먼트의 관리, 힙 확장 정책, 메모리 커밋과 디커밋, 큰 블록 관리를 포함함
힙 프론트엔드 계층
- 유저 모드 힙에서만 핵심 기능 위에 존재할 수 있음
- 윈도우에서 제공되는 유일한 프론트엔드 기능은 저단편화 힙
힙 동기화
힙 관리자는 기본적으로 멀티스레드의 동시 접근을 지원
- 만약 프로세스가 하나의 스레드로만 동작하거나 다른 동기화 메커니즘을 사용하면?
- HEAP_NO_SERIALIZE 플래그를 통해 동기화 오버헤드를 방지할 수 있음
HeapLock, HeapUnlock
- 힙 Lock을 통해 모든 스레드의 힙 관련 작업을 막을 수 있음
저단편화 힙
윈도우에서 많은 애플리케이션은 보통 1MB 이하의 작은 힙 메모리를 사용
- 이런 경우 Best-fit(가장 사이즈에 잘 맞는) 정책을 통해 프로세스가 작은 메모리 사용량을 유지하게 도움을 줌
- 그러나 큰 프로세서나 멀티프로세서 시스템에 적용할 수 없음
- 이런 경우 힙 단편화로 인해 힙 낭비가 발생
LFH(저단편화 힙)이 단편화를 해결하는 방법
- 버킷(Bucket)이라는 미리 정의된 서로 다른 크기의 범위를 갖는 블록을 관리함으로 해결
- 프로세스가 힙에서 메모리를 할당하려 하면 LFH는 요청된 크기를 포함할 수 있는 가장 작은 버킷에 대응하는 버킷을 선택
- 첫 번째 버킷 1
8바이트, 두 번째 버킷 916바이트~ 8바이트 단위의 크기로 32번째 버킷 249256바이트까지 늘어남 - 33번째 버킷 257
272바이트부터는 16바이트 단위의 크기로 늘어나고 마지막 128번째 버킷은 1587316384바이트 크기까지 할당할 수 있음- Binary buddy 시스템이라 알려짐
만약 할당이 16384바이트보다 크다면?
- LFH는 그냥 이 요청을 하부의 힙 백앤드로 전달함
캐시라인 경쟁 문제 처리 방법
여러 개의 프로세서가 같은 위치의 메모리에 동시 쓰기 동작을 할 때 캐시라인 경쟁 문제 발생
- LFH는 코어 힙 관리자와 룩 어사이드 리스트를 통해 처리
- 윈도우는 LFH를 활성화하면 더 좋은 상황에서 기본적으로 LFH를 활성화할 수 있는 튜닝 알고리즘을 구현함
- 락 경쟁, 자주 사용되는 크기의 할당 등의 상황
LFH의 할당 전략
- 큰 크기의 힙에서 대부분의 할당은 특정 크기를 갖는 상대적으로 작은 수의 버킷으로 그룹화
- 동일한 크기를 갖는 블록을 효율적으로 관리하여 최적화하는 것이 LFH의 할당 전략
LFH가 확장성 문제를 해결하는 방법
- 현재 프로세서 개수보다 두 배 많은 슬롯(slot) 개수만큼 자주 접근되는 내부 구조체를 늘림
- 슬롯에 대한 스레드의 할당은 Affinity 관리자라는 LFH의 구성 요소에 의해 이뤄짐
- 최초 LFH는 힙 할당을 위해 첫번째 슬롯 사용
- 하지만 내부 데이터 접근에 경쟁이 발생하면 현재 스레드가 다른 슬롯을 사용하게 변경됨
- 경쟁이 일어날수록 스레드는 더 많은 슬롯으로 골고루 분산됨
- 슬롯이 갖는 버킷의 크기는 지역성을 향상시키고 메모리 사용량을 최소화하도록 적절히 조절됨
LFH가 프론트엔드 힙으로 활성화 되어 있어도
- 여전히 코어 힙 함수로 자주 사용되지 않는 크기 할당 가능, 자주 할당되는 크기는 LFH 사용 가능
- LFH가 특정 힙에 대해 활성화된다면 비활성화 불가
세그먼트 힙
세그먼트 힙의 아키텍처
할당을 관리하는 실제 계층은 할당 크기에 따라 달라짐
- 작은 크기이고 크기가 일반적이라면 LFH 할당자가 적용
- 만약 LFH가 아직 처리하지 않았다면 가변 크기(VS) 할당자를 사용
- 128KB보다 작거나 같은 크기(LFH에 의해 처리되지 않은) 가변 크기 할당자가 사용됨
- VS와 LFH는 힙 서브세그먼트를 생성하기 위해 백엔드를 사용
- 128KB보다 크고 508KB보다 작거나 같은 할당은 힙 백엔드에 의해 처리
- 508KB보다 큰 할당은 메모리 관리자를 직접 호출하여 처리(VirtualAlloc)
두 힙 구현의 비교
- 일부 시나리오에서 세그먼트 힙은 NT 힙보다 다소 느릴 수 있음
- 세그먼트 힙은 메타데이터 용도로 좀 더 작은 메모리 사용을 함
- 세그먼트 힙의 메타데이터는 실제 데이터와는 구분됨
- NT 힙의 경우에는 메타데이터와 실제 데이터가 함께 존재
- 세그먼트 힙의 이런 특징 메타데이터 획득을 어렵게 함으로 좀 더 안전하게 함
- 두 힙 모두 LFH 할당을 지원하지만 내부 구현은 완전히 다름
- 세그먼트 힙이 메모리 소비와 성능 측면에서 좀 더 효율적임
세그먼트 힙을 사용하는 예시
- 일부 UWP 앱
- 작은 메모리 장치에 적합한 앱의 작은 메모리 사용에 기인
- 시스템 프로세스
- csrss.exe, services.exe, svchost.exe 등
기존 어플래케이션의 호환성 문제로 세그먼트 힙은 데스크톱 앱의 기본 힙이 아님
특정 실행 파일의 세그먼트 힙 활성화/비활성화 옵션
- FrontEndHeapDebugOptions(DWORD)로 불리는 이미지 파일 실행 옵션 값 설정 가능
- 비트 2(4)는 비활성화, 비트 3(8)은 활성화
힙 보안 특징
메모리 취약점을 줄여주는 메커니즘
- 힙의 메타데이터는 난수화 되어 있어 내부 데이터를 패치하여 공격 시 발생하는 크래시 방지, 익스플로잇이 공격 시도 흔적을 지우는 것을 어렵게 만듬
- 버퍼 오버런과 같은 간단한 손상을 진단하기 위해 헤더 무결성 검증 메커니즘도 제공
- 베이스 주소나 핸들에 대해 낮은 수준의 난수화 제공
- 힙 무결성 손상이 탐지될 경우 프로세스가 자동으로 종료될 수 있음
세그먼트 힙 한정 보안 기능
- 링크드 리스트 노드 손상에 대한 빠른 실패
- 세그먼트 힙은 세그먼트와 서브 세그먼트 추적을 위해 링크드 리스트 사용
- 손상된 노드가 탐지되면 RtlFailFast를 호출하여 프로세스 종료
- 레드블랙(RB) 트리 노드 손상에 대한 빠른 실패
- 세그먼트 힙은 RB트리를 통해 프리 백엔드와 VS 할당을 추적
- 삽입 삭제 함수에서 노드의 유효성 검사
- 노드 손상 시 빠른 손상 메커니즘 호출
- 함수 포인터 디코딩
- 세그먼트 힙의 일부분은 콜백을 허용
- 공격자가 이 콜백을 자신의 코드로 덮을 수 있음
- 하지만 함수 포인터는 내부 난수화 힙 키와 컨텍스트 주소로 XOR 하여 인코딩됨
- 가드 페이지
- LFH와 VS 서브 세그먼트, 큰 블록이 할당될 때 가드 페이지가 추가됨
- 이는 오버플로우와 인접 데이터 손상 탐지에 도움됨
페이지 힙
애플리케이션 실패의 가장 흔한 원인 - 힙 메타데이터 손상
- 이런 문제를 완화시키고 더 나은 문제 해결 자원을 제공 하기 위해 폴트 톨러런트 힙(FTH, Fault Tolerant Heap) 기능을 포함
폴트 톨러런트 힙의 구성 요소
탐지 컴포넌트(FTH 서버)
- Fthsvc.dll - Wscsvc.dll에 의해 로드됨
- 윈도우 오류 보고 서비스에 의해 애플리케이션 크래시를 통지받음
완화 컴포넌트(FTH 클라이언트)
- 애플리케이션 호환성 심(shim)
- 윈도우 XP부터 예전 윈도우 시스템의 특정 동작에 의존적인 애플리케이션이 나중에 나온 시스템에서 수행되게 하기 위해 사용됨
- 심 메커니즘은 힙 루틴에 대한 호출을 가로채 이를 자신의 코드로 돌림
- FTH 코드는 애플리케이션이 다양한 힙 관련 에러에도 불구하고 살아남을 수 있게 하기 위해 많은 완화 기능을 구현함
FTH는 일반적으로 서비스에는 동작하지 않음
- 윈도우 서버 시스템에서는 성능 문제로 동작하지 않음
- 시스템 관리자가 실행 파일에 수동으로 적용할 수 있음
반응형