AddressSanitizer(ASan)를 Linux에서 쓰다가 Windows로 옮기면, 혹은 그 반대로 옮기면 “같은 도구인데 왜 다르게 구는가” 하는 순간을 만난다. 대부분은 같지만 몇 군데서 정확히 갈리고, 그 몇 군데가 하필 실무에서 데이는 자리다.
같은 버그 프로그램을 **WSL의 g++ 11.4(Ubuntu 22.04)**와 Visual Studio 2026의 MSVC(cl 19.51, 툴셋 14.51) 양쪽에서 직접 돌려 확인한 기록이다.
ASan이 하는 일
ASan은 메모리 오류를 실행 중에 잡는 도구다. 원리는 두 부분이다.
- 컴파일 계측 —
-fsanitize=address(GCC/Clang) 또는/fsanitize=address(MSVC)를 주면 컴파일러가 모든 메모리 접근 앞에 검사 코드를 심는다. “이 주소를 지금 써도 되는가”를 매번 묻는 코드로 바뀐다. - 그림자 메모리(shadow memory) — 실제 메모리 8바이트마다 그림자 1바이트를 두고 접근 가능 여부를 기록한다. 할당 블록 둘레에는 **레드존(redzone)**을 깔아, 한 칸만 넘어가도 그림자가 “여긴 금지”라고 답한다.
잡는 것은 힙·스택·전역 버퍼 오버플로, use-after-free, double-free, 그리고 (딸린 LeakSanitizer로) 메모리 누수다. 대가는 대략 2배 느려지고 메모리를 2~3배 쓴다. 배포용이 아니라 테스트·디버그용인 이유다.
new int[10]에 a[10]을 쓰는 오버플로를 양쪽에서 돌리면 리포트가 사실상 똑같이 나온다.
==ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 4 ... thread T0
#0 ... in main asan_test.cpp:4
0x... is located 0 bytes after 40-byte region [..., ...)
heap-use-after-free도 마찬가지로 양쪽 다 잡는다. 여기까지는 플랫폼을 가리지 않는다.
왜 리포트가 같은가 — 근거가 리포트에 찍혀 있다
Windows에서 나온 오버플로 리포트의 할당 스택을 보면 내부 경로가 이렇게 찍힌다.
operator new[] ... D:\a\_work\1\s\src\vctools\asan\llvm\compiler-rt\lib\asan\asan_win_new_array_thunk.cpp
llvm\compiler-rt\lib\asan — MSVC ASan이 Linux·Clang과 같은 LLVM compiler-rt 코드를 쓴다는 뜻이다. Microsoft는 2024년 무렵부터 MSVC ASan 런타임을 LLVM 상류에 올리고 LLVM main의 변경을 매주 통합해왔는데, 그 결과가 리포트 형식·shadow byte 범례·기본 동작의 일치로 나타난다.
“대체로 같다”가 말뿐인 인상이 되지 않도록, 버그 유형을 다섯 가지로 늘려 양쪽 다 기본 옵션으로 돌렸다.
| 버그 유형 | Linux (g++ 11.4) | Windows (MSVC) |
|---|---|---|
| 스택 버퍼 오버플로 | 잡음 | 잡음 |
| use-after-scope | 잡음 | 잡음 |
| 전역 버퍼 오버플로 | 잡음 | 잡음 |
| use-after-return | 못 잡음 | 못 잡음 |
| 메모리 누수 | 잡음 | 못 잡음 |
오버플로·use-after-scope 계열은 완전히 같고, use-after-return은 양쪽 다 기본으로 놓친다(계측을 따로 켜야 잡힌다). 딱 하나, 누수에서 갈린다. “리눅스가 더 예민하게 잡더라”는 인상의 실체가 대개 이 한 칸이다.
use-after-return에는 각주가 붙는다. Clang은 16부터 이 검사를 기본으로 켠다. clang 18로 확인하니 환경변수 없이도 stack-use-after-return을 잡았고, ASAN_OPTIONS=detect_stack_use_after_return=0으로 꺼야 놓쳤다. 반면 GCC(g++ 11.4)는 기본으로 못 잡고, 런타임 detect_stack_use_after_return=1을 줘도 여전히 못 잡는다 — 이 검사는 컴파일 시 계측이 필요한데 GCC는 그걸 기본으로 심지 않아 런타임 스위치만으로는 켤 수 없기 때문이다. MSVC도 기본으로는 못 잡는다. 정리하면 위 표의 “양쪽 다 못 잡음”은 g++·MSVC 기준이고, Clang 기반 리눅스라면 이 항목에서도 리눅스가 더 잡는다.
함정 1 — 누수는 Windows에서 조용히 통과한다
가장 크게 갈리는 곳이다. delete 없이 400바이트를 흘리는 프로그램을 양쪽에서 돌렸다.
Linux (g++):
==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 400 byte(s) in 1 object(s) allocated from:
#0 operator new[](unsigned long)
#1 main /tmp/leak.cpp:3
SUMMARY: AddressSanitizer: 400 byte(s) leaked in 1 allocation(s).
Windows (MSVC):
leaked 1
=== EXIT 0 ===
Windows 쪽은 누수 리포트가 아예 없다. 정상 종료로 처리하고 exit 0을 반환한다.
이유는 단순하다. MSVC에는 LeakSanitizer가 없다. 누수 탐지는 함께 딸려 오는 별도 도구(LSan)가 하는데, LSan은 Linux에서 기본으로 켜지고(macOS는 ASAN_OPTIONS=detect_leaks=1로) Windows에는 이식되지 않았다. Microsoft 문서도 이를 명시한다. Linux에서 통과하던 누수 검사를 Windows CI에 그대로 옮기면 누수를 못 잡는데 통과한 것처럼 보인다.
이 차이는 arena 할당 같은 패턴에서 특히 크게 데인다. protobuf의 Arena처럼 객체를 한 블록에 몰아 담고 개별 delete 없이 arena 수명으로 한꺼번에 회수하는 구조에서는, arena 하나만 새도 그 위에 얹힌 게 통째로 누수된다. 게다가 개별 객체는 잡을 포인터조차 없다. arena를 힙에 만들고 회수하지 않는 최소 예제를 양쪽에서 돌리면 이렇게 갈린다.
struct Arena { // protobuf Arena를 흉내 낸 범프 할당자
char* buf;
size_t off = 0;
explicit Arena(size_t cap) : buf((char*)malloc(cap)) {}
void* Alloc(size_t n) { void* p = buf + off; off += n; return p; }
// 소멸자 없음 — buf를 free하지 않는다
};
struct Msg { int data[64]; };
int main() {
Arena* a = new Arena(1 << 20); // arena를 힙에 생성 (1MB)
Msg* m = (Msg*)a->Alloc(sizeof(Msg)); // arena 위에 할당
m->data[0] = 42;
printf("%d\n", m->data[0]);
// a를 delete 안 함 → Arena + 1MB 버퍼가 통째로 누수
}- Linux:
SUMMARY: AddressSanitizer: 1048592 byte(s) leaked in 2 allocation(s)— 1MB 버퍼와 Arena 객체를 정확히 짚는다. - Windows:
42를 출력하고 exit 0. 아무 말이 없다.
같은 코드가 Linux CI에서는 빨갛게 서고 Windows CI에서는 초록으로 통과한다. arena 오용으로 생긴 누수를 Windows에서 못 잡고 Linux에서 잡는 건 도구가 예민해서가 아니라 한쪽에만 LeakSanitizer가 있어서다.
함정 2 — Windows 실행 파일은 ASan DLL을 PATH에서 찾아야 뜬다
계측한 실행 파일의 의존성을 뜯어보면 배포 방식이 갈린다.
Windows (dumpbin /dependents):
asan_msvc.exe
Image has the following dependencies:
clang_rt.asan_dynamic-x86_64.dll
KERNEL32.dll
ASan 런타임이 DLL로 분리돼 있다. 이 clang_rt.asan_dynamic-x86_64.dll을 로더가 PATH에서 못 찾으면 실행 자체가 안 된다. 개발 명령 프롬프트(vcvars) 안에서는 이 DLL 폴더가 PATH에 들어가 있어 잘 돌지만, 그 밖으로 실행 파일만 꺼내 돌리면 “DLL을 찾을 수 없음”으로 죽는다. 배포하거나 CI 아티팩트로 옮길 때 이 DLL을 같이 챙겨야 한다.
Linux (ldd):
libasan.so.6 => /lib/x86_64-linux-gnu/libasan.so.6
기본은 libasan.so에 동적으로 의존하지만, -static-libasan을 주면 런타임 의존성이 사라진다.
$ g++ -fsanitize=address -static-libasan ...
$ ldd a.out | grep asan
(출력 없음 — .so 불필요)
즉 Linux(GCC)는 런타임을 실행 파일에 정적으로 넣는 선택지가 있고, Windows는 DLL 동봉이 기본 경로다. 옮겨 실행할 바이너리라면 이 차이가 “왜 저쪽 PC에서 안 뜨지”의 원인이 된다. (Clang은 Linux에서도 ASan 런타임을 기본으로 정적 링크하므로 GCC와 또 다르다. 여기서 확인한 건 GCC 쪽이다.)
함정 3 — Windows에서 함께 못 켜는 것들
이건 직접 돌려 확인한 게 아니라 MSVC 공식 문서에 명시된 제약이다. /fsanitize=address와 다음은 함께 쓸 수 없어 꺼야 한다.
/RTC(런타임 검사) — 충돌- 증분 링크(
/INCREMENTAL) — 미지원 - 편집 후 계속(Edit and Continue) — 미지원
- 코루틴 — 계측에서 제외됨(resumable 함수는 검사 안 됨)
- OpenMP, 관리 코드(C++/CLI), C++ AMP, UWP 앱 — 미지원
Linux(GCC/Clang)에는 이런 조합 제약이 사실상 없다. Visual Studio 프로젝트에서 ASan을 처음 켤 때 링크가 깨지면 대개 증분 링크나 /RTC가 켜져 있어서다.
반대로 Windows의 이점도 있다. Visual Studio는 ASan 오류를 편집기에 예외로 띄워 해당 줄에 바로 멈춰준다. 명령줄 리포트를 읽는 것보다 원인 줄로 가기가 빠르다.
함정 4 — ARM64는 아직 preview
ASan은 2019년 x86/x64로 처음 들어왔고, x64는 양쪽 다 성숙했다. 갈리는 건 ARM이다. Visual Studio 2026(18.0, 2025-11 GA)에서 ARM64 지원이 추가됐지만 preview 상태다 — Microsoft 스스로 “계속 다듬고 들어오는 버그를 고치겠다”고 밝혔다. Apple Silicon·ARM 리눅스에서 성숙하게 쓰이는 것과 온도차가 있다. ARM64 Windows에서 ASan에 프로덕션 수준을 기대하기엔 이르다.
정리
같은 LLVM 코드를 공유하므로 탐지 능력과 리포트는 대체로 같다. 갈리는 건 주변부다. 같은 뿌리에서 나와 가장자리만 갈라지는 구조다.
flowchart TD Core["공유 코어 · LLVM compiler-rt<br/>→ 오버플로·use-after-free 탐지<br/>→ 리포트 형식·shadow byte 동일"] Core --> L["Linux · g++/clang"] Core --> W["Windows · MSVC"] L --> LD["누수: LeakSanitizer 있음<br/>런타임: libasan.so / 정적 링크 가능<br/>조합 제약: 거의 없음<br/>ARM64: 성숙"] W --> WD["누수: 없음 → 조용히 통과<br/>런타임: clang_rt.asan_dynamic.dll 동봉<br/>조합 제약: /RTC·증분 링크·코루틴·UWP 불가<br/>ARM64: preview (VS 2026)"]
| 항목 | Linux (g++ 11.4) | Windows (MSVC 19.51, VS 2026) |
|---|---|---|
| 오버플로·use-after-free | 잡음 | 잡음 (리포트 형식 동일) |
| 누수 탐지 | 자동(LeakSanitizer) | 없음 — 조용히 통과 |
| 런타임 배포 | libasan.so 또는 정적 링크 | clang_rt.asan_dynamic-*.dll 동봉 필요 |
| 조합 제약 | 거의 없음 | /RTC·증분 링크·E&C·코루틴·OpenMP·UWP 불가 |
| IDE 통합 | 명령줄 중심 | 편집기에 오류 표시 |
| ARM64 | 성숙 | preview(VS 2026) |
실무로 옮기면 세 가지다.
- Linux에서 통과한 누수 검사를 Windows가 대신해준다고 믿지 않는다. 누수 게이트는 Linux ASan에 둔다.
- Windows ASan 바이너리를 옮겨 실행하려면
clang_rt.asan_dynamic-*.dll을 같이 챙긴다. - Visual Studio에서 링크가 깨지면 증분 링크와
/RTC부터 끈다.
참고
- AddressSanitizer — Clang 문서 — 계측·그림자 메모리 원리와 탐지 대상, LeakSanitizer가 Linux 기본·다른 플랫폼 미지원이라는 서술
- MSVC AddressSanitizer 알려진 문제와 제약 — Microsoft Learn —
/RTC·증분 링크·Edit-and-Continue·코루틴·OpenMP·UWP 비호환 목록의 출처 - MSVC Address Sanitizer updates in VS 2022 17.13 — C++ Team Blog — LLVM 상류 합류와 main 매주 통합 전략(리포트의 compiler-rt 경로가 이걸 뒷받침한다)
- What’s New for C++ Developers in Visual Studio 2026 18.0 — C++ Team Blog — ARM64 ASan이 preview로 추가됐다는 근거