윈도우 인터널즈

메모리 관리 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는 요청된 크기를 포함할 수 있는 가장 작은 버킷에 대응하는 버킷을 선택
  • 첫 번째 버킷 18바이트, 두 번째 버킷 916바이트 ~ 8바이트 단위의 크기로 32번째 버킷 249256바이트까지 늘어남
  • 33번째 버킷 257272바이트부터는 16바이트 단위의 크기로 늘어나고 마지막 128번째 버킷은 1587316384바이트 크기까지 할당할 수 있음
    • Binary buddy 시스템이라 알려짐

만약 할당이 16384바이트보다 크다면?

  • LFH는 그냥 이 요청을 하부의 힙 백앤드로 전달함

캐시라인 경쟁 문제 처리 방법

여러 개의 프로세서가 같은 위치의 메모리에 동시 쓰기 동작을 할 때 캐시라인 경쟁 문제 발생

  • LFH는 코어 힙 관리자와 룩 어사이드 리스트를 통해 처리
  • 윈도우는 LFH를 활성화하면 더 좋은 상황에서 기본적으로 LFH를 활성화할 수 있는 튜닝 알고리즘을 구현함
    • 락 경쟁, 자주 사용되는 크기의 할당 등의 상황

LFH의 할당 전략

  • 큰 크기의 힙에서 대부분의 할당은 특정 크기를 갖는 상대적으로 작은 수의 버킷으로 그룹화
  • 동일한 크기를 갖는 블록을 효율적으로 관리하여 최적화하는 것이 LFH의 할당 전략

LFH가 확장성 문제를 해결하는 방법

  • 현재 프로세서 개수보다 두 배 많은 슬롯(slot) 개수만큼 자주 접근되는 내부 구조체를 늘림
  • 슬롯에 대한 스레드의 할당은 Affinity 관리자라는 LFH의 구성 요소에 의해 이뤄짐
  • 최초 LFH는 힙 할당을 위해 첫번째 슬롯 사용
  • 하지만 내부 데이터 접근에 경쟁이 발생하면 현재 스레드가 다른 슬롯을 사용하게 변경됨
    • 경쟁이 일어날수록 스레드는 더 많은 슬롯으로 골고루 분산됨
  • 슬롯이 갖는 버킷의 크기는 지역성을 향상시키고 메모리 사용량을 최소화하도록 적절히 조절됨

LFH가 프론트엔드 힙으로 활성화 되어 있어도

  • 여전히 코어 힙 함수로 자주 사용되지 않는 크기 할당 가능, 자주 할당되는 크기는 LFH 사용 가능
  • LFH가 특정 힙에 대해 활성화된다면 비활성화 불가

세그먼트 힙

세그먼트 힙의 아키텍처

5-4

할당을 관리하는 실제 계층은 할당 크기에 따라 달라짐

  • 작은 크기이고 크기가 일반적이라면 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는 일반적으로 서비스에는 동작하지 않음

  • 윈도우 서버 시스템에서는 성능 문제로 동작하지 않음
  • 시스템 관리자가 실행 파일에 수동으로 적용할 수 있음
반응형