공부

메모리 구조(프로세스 주소공간 전체 구조)

CodingNabi 2026. 7. 4. 16:38

들어가기 앞서 필자는 앞으로 모든 OS에 대한 기준을 윈도우에 맞춰서 설명할 것이다.

 

우선 이것부터 정의하겠다.

프로세스란 무언인가? 위키피디아에는 이런식으로 정의되어있다.

프로세스(process)는 컴퓨터에서 연속적으로 실행되고 있는 프로그램이다. 종종 스케줄링의 대상이 되는 작업(task)이라는 용어와 거의 같은 의미로 쓰인다. 여러 개의 프로세서를 사용하는 것을 멀티프로세싱이라고 하며 같은 시간에 여러 개의 프로그램을 띄우는 시분할 방식을 멀티태스킹이라고 한다. 프로세스 관리 운영 체제의 중요한 부분이 되었다.

 

오늘은 메모리에 대한 설명을 위해서이니 정의를 더 파보진 않겠다.

한마디로 프로세스는 즉, 실행 중인 프로그램의 인스턴스인 것이다. 그 실행중인 프로그램의 인스턴스(프로세스)은 가상주소라는공간을 사용하는데, 오늘은 이 가상주소 공간의 영역에 대해 알아볼 것이다.

x86(전통적인 32비트 운영체제)

32비트 윈도우에서 전체 가상주소 공간은 2^32바이트(4GB)이고, 기본적으로 하위 2GB는 유저공간, 상위 2GB는 시스템(커널)공간으로 나뉜다. 

x86-64(64 비트 운영체제)

현대의 64비트 윈도우에서 전체 가상주소 공간은 2^64(약 16EB) 이지만 실제로 사용되는 공간은 훨신 작은 크기이다.

윈도우 10/11기준에선 유저모드 프로세스에게 최대 128TB, 커널모드에도 128TB의 가상주소 공간이 주어진다.

둘 사이에는 접근 불가능한 가드 영역이 있어서 실수나 악의적인 접근을 막는다. 오늘은 유저 영역만을 다룰 것이다.

자 그럼 다시 돌아와서 아래 이미지에서 우리는 코드, 데이터, 힙, 스텍 영역이 유저 영역이란걸 유추할 수 있다.

이제 각 영역이 어떤 내부 구조를 가지는지 살펴보자.

메모리 -> 디스크에 있는 .exe 파일의 내용이 아니라, 지금 이 순간 그 프로세스의 "가상주소공간" 상에 존재하는 값을 보여주는 것. 즉, 실행 중인 프로세스의 (그 순간의) 스냅샷이라고 생각하면 편하다. 여기서 "값이 실제 물리 RAM에 있느냐"는 별개 문제다 — 아직 물리 프레임이 안 붙은 가상주소를 조회하면, 그 순간 Page Fault가 유발되어 프레임이 붙은 뒤 값이 보여진다. 즉 프로세스의 한 순간의 "가상주소공간" = 메모리 창이 보여주는 대상이고, 이는 앞서 그린 가상주소공간 그림을 실시간으로 들여다보는 창이다.

우선 우리가 작성한 코드는 언제 저런 영역으로 나뉠까? 이를 정확하게 답하려면 코드/데이터(rdata,data,bss)영역힙/스택 영역을 완전 분리해서 봐야한다. 둘이 나눠지는 시점이 다르기 때문이다.

우선 코드/데이터(rdata,data,bss)영역이 언제 나뉘는가이다.

1단계: 전처리

-> #include, #define 처리

2단계: 컴파일 - 윈도우에선 IDE가 내부적으로 호출하는 별개의 실행파일(cl.exe)이 담당.

->소스코드(.c/.cpp) -> 어셈블리어로 변환. 그리고 바로 오브젝트 파일(.obj)까지 생성

이 시점에 이미 .text/.data/.bss로 섹션이 나뉘어 오브젝트 파일 안에 존재한다. 단 아직 최종 주소는 미정이고, 각 섹션 시작점 기준의 임시 오프셋으로만 기록된다.

3단계: 링킹 - 윈도우에선 IDE가 내부적으로 호출하는 별개의 실행파일(link.exe)이 담당.

->여러 .obj파일들 + 라이브러리(.lib)를 하나로 합친다. 링커가 가정한 베이스 주소 기준으로 상대 주소인 RVA를 배치. 결과물로 최종 PE 실행파일(.exe/.dll)

=> 즉, 코드/데이터/BSS 영역은 프로그램이 실행되기도 전에, 디스크 실행파일 안에 이미 물리적으로 확정되어 있음.

 

힙과 스택 영역은 - 코드/데이터(rdata,data,bss)와는 완전 다른 타이밍이다.

실행파일을 열어봐도 힙, 스텍 섹션은 존재하지 않는다.

힙(heap)

->프로세스 시작 시 OS/C런타임이 virtualloc(RESERVE)로 가상주소 일부를 힙 용도로 예약한다. malloc/new 호출될 때마다 실시간으로 내용을 채운다. 자세한 내용은 힙 게시글을 따로 작성하겠다.

스택(stack)

->프로세스 생성 시 OS가 스택용 가상주소 범위를 예약한다(기본 1MB). 함수 호출/리턴마다 내용이 실시간으로 바뀌고 가드 페이지로 자동 확장한다. 스택 게시글에 따로 작성하겠다.

=> 요지는 힙/스택 영역은 동적으로 실행파일엔 위치/크기정도만(또는 아무 정보도 없이) 예약되고, 실제 내용은 프로세스 실행 중(런타임)에 계속 채워지고 바뀐다.

 

그럼 여기서 하나 의문이 들 것이다. 우리가 VS같은  IDE로 코드를 작성하고 실행할때, 보이는 디스어셈블리와 메모리 창은 무엇일까? 라는 의문 말이다.

디스어셈블리와 메모리 창

디스어셈블리

-> 핵심은 여기서 나오는 주소는 항상 .text영역 안의 가상주소이다. 그럼 다음 질문은 이 주소가 어느 단계까지 완성된 값이냐이다.

-> 링크 타임: link.exe가지 계산된 값 = RVA(이미지 베이스 기준 상대주소, 아직 실행 되기 전이니 가정된 값)

-> 로드 타임:(OS 로더, 실행 시점)에 최종 확정된 값 = VA = 실제 베이스 주소 + RVA

=>결론적으로 VS디스어셈블리 창은 디버깅 중에 보는 창이므로 이미 프로세스가 실행되어 로드까지 끝난 시점의 주소이다. 즉 RVA가 아니라 완성된 절대 가상주소 VA를 보여준다.

 

메모리

-> 디스크에 있는 .exe 파일의 내용이 아니라, 지금 이 순간 그 프로세스의 "가상주소공간" 상에 존재하는 값을 보여주는 것.

즉, 실행 중인 프로세스의 (그 순간의) 스냅샷이라고 생각하면 편하다. 여기서 "값이 실제 물리 RAM에 있느냐"는 별개 문제다(추후 페이지 시간에 다루겠다). 프로세스의 한 순간의 "가상주소공간" = 메모리 창이 보여주는 대상 범위이고, 이는 앞서 그린 가상주소공간 그림을 실시간으로 들여다보는 창이다.

주의해야 할 점은 저 모든 영역을 정말로 스냅샷처럼 찍어서 보여주는것이 아니라는것이다. 우리가 스크롤을 내리며 끝없이 다 보여준다고 생각하지만 스크롤 될때마다 매 번 새로 그 구간을 읽어오는 것이다.

 

자 서론이 길었다 이제 본격적으로 저 가상주소 공간의 각 영역의 내용과 특징에 대해 알아보자.

1. 코드 영역(.text)

저장 내용: 컴파일된 실행 가능 명령어(기계어)를 저장한다.

권한(해당 메모리에 어떤 종류의 접근이 허용되는가): 읽기 + 실행, 쓰기 불가.

->왜? 실행중인 코드에 우연, 혹은 악의적으로 변조되는것을 막기 위해. 만약 여기에 쓰기를 시도하면 Access Violation(접근 위반) 이 즉시 발생한다.

크기 결정: 컴파일/링크 시점에 완전히 고정.

공유 여부: 같은 실행파일/DLL을 여러 프로세스가 실행하면, 물리 메모리상 코드 영역은 하나만 있어도 되로록 공유 가능.

변화 여부: 프로세스 실행 중 절대 바뀌지 않음.

2. 데이터 영역(.rdata, .data, .bss)

.rdata (읽기 전용 데이터)

저장 내용: 문자열 리터럴, const 상수, 디버그 디렉터리 정보, 임포트/ 익스포트 테이블 등.

권한: 읽기 전용(read-only, 쓰기 금지), 쓰기 시도 시 즉시 Access Violation

크기 결정: 컴파일/링크 시점에 완전히 고정

공유 여부: .text와 마찬가지로  읽기 전용이라 여러 프로세스가 물리 메모리상 공유 가능

변화 여부: 프로세스 실행 중 절대 안 바뀜 (상수이므로 애초에 값이 고정)

예: const char s[] = "Hello"; 

-> 이 경우엔 Hello 6바이트, s라는 배열 변수도 Hellow로 초기화된 const 배열이므로 s 자체도 .rdata에 저장됨(주의해야 할 점이 s라는 배열의 주소가 따로 저장된다는 것이 아니라 H.e.l.l.o.\0 데이터가 저장된 그 시작주소 자체를 가리키는것이 s라는 의미이다.)

 

예: const char* p  = Hello";

-> 이 경우엔 두 개의 별개 공간이 필요하다.

1. Hello\0 라는 문자열 데이터 자체 = .rdata에 저장

2. p라는 포인터 전역 변수(32bit 윈도우에선 4바이트, 64bit 윈도우에선 8바이트) -> .data에 저장. p안에 들어 있는 값은 Hello\0이 저장된 .rdata주소.

 

예: printf("hello world");

-> 여기서 저장되는 건 hellow world라는 문자열 리터럴 바이트 값임. printf(...)라는 함수 호출 자체는 call명령어라 .text영역에 저장됨.

 

.data(초기화된 읽기/쓰기 데이터)

저장 내용: 명시적으로 초기화된 전역/정적 변수

권한: 읽기 + 쓰기

크기 결정: 컴파일/링크 시점에 고정(초기값이 실행파일 안에 저장됨)

공유 여부: 프로세스마다 독립(쓰기 가능이라 공유하면, 한 프로세스의 값이 바뀌는 순간 다른 프로세스에 영향을 줘서 안됨. 단 DLL의 .data는 로드 시 프로세스별로 Copy-on-Write 방식으로 복사되어 각자 독립된 사본을 가짐)

변화 여부: 프로그램 실행 중 값이 바뀔 수 있음.

예시: int a = 0;

 

.bss(미초기화 데이터)

저장 내용: 초기화 안 했거나 0으로 초기화된 전역/정적 변수

권한: 읽기 +쓰기

크기 결정: 컴파일/링크 시점에 "크기"만 고정, 내용(0값,C++표준 상 초기화 안한 전역 변수는 0으로 초가화) 실행파일 안에 저장 안됨(크기 절약)

초기값: 앞서 말했듯 실행 시작 시 OS/로더가 자동으로 0으로 채움.

공유 여부: .data와 동일하게  프로세스 별 독립. 공유 안됨.

변화 여부: 실행 중 값이 바뀔 수 있음.

예시: int b; int b = 0;

 

3. 힙(Heap)

저장 내용: malloc/new/calloc으로 동적 할당 된 데이터

권한: 읽기 + 쓰기

크기 결정: 런타임에 동적으로 결정( 프로세스 시작 시 힙 메니저가 virtualAlloc(RESERVE)로 초기 영역 예약 후 malloc/new호출마다 필요시 추가.

공유 여부: 프로세스마다 완전 독립(다른 프로세스가 절대 접근 불가)

변화 여부: 실행중 값이 바뀔 수 있음.

 

4. 스택(Stack)

저장 내용: 함수 호출 프레임 ( 지역변수/매개변수/리턴주소/saved ebp/esp 등)

권한: 읽기 + 쓰기

크기 결정: 기본 예약 1MB(링커 기본값), 스레드 생성 시 결정.  CreateThread의 dwStackSize나 링커 옵션(/STACK)으로 변경 가능

공유 여부: 스레드마다 독립적으로 별도 예약(같은 프로세스 안에서도 스레드 개수만큼 스택이 각자 존재)

확장 방식: 가드 페이지로 자동 확장(추후 스택에서 다룰 것), 예약 크기 다 쓰면 스택 오버플로우 예외

변화 여부: 함수 호출/리턴마다 바뀜

 

5. 커널 영역(자세한건 커널에서 다룰 것)

저장 내용: OS 커널 코드, 드라이버, 페이지 캐시, 비페이징 풀/페이징 풀 등등

권한: 유저모드 접근 불가, 커널모드만 접근 가능

크기 결정: 시스템 부팅 시 결정(32비트: 기본2GB, 64비트: 128TB)

공유 여부: 모든 프로세스가 "동일한" 커널 공간을 공유. 프로세스 별 독립 X

변화 여부: 시스템 이벤트(페이지 캐시 갱신 등)에 따라 변함.

 

공유 여부에 따른 프로세스 시각화

종합

'공부' 카테고리의 다른 글

우리는 왜 UTF-8을 써야 하는가  (0) 2026.07.07
버블정렬  (0) 2026.07.06
C/C++ 연산자 우선순위  (0) 2026.07.05
이스케이프 시퀀스 / C 표준 입출력  (0) 2026.07.05
데이터 모델별 비트 차이  (0) 2026.07.04