Skip to content

Instantly share code, notes, and snippets.

@YangSiJun528
Last active January 20, 2026 06:38
Show Gist options
  • Select an option

  • Save YangSiJun528/29d81bea78ab4c09a75027fd380f9cb6 to your computer and use it in GitHub Desktop.

Select an option

Save YangSiJun528/29d81bea78ab4c09a75027fd380f9cb6 to your computer and use it in GitHub Desktop.
Linux 스레드와 메모리 구조.md

Linux 스레드와 메모리 구조

러스트의 코루틴 개념을 공부하다가
스레드의 스택은 메모리 레이아웃에서 stack이 아니라는 사실을 알게 되서,
찾아본 내용을 클로드로 정리한 글, 그래도 참고한 자료들이 많아서 신뢰성이 없진 않다고 봄.

1. 스레드 스택: 메인 vs 생성된 스레드

핵심 차이

구분 메인 스레드 생성된 스레드 (pthread)
스택 할당 커널 (execve 시) pthread (mmap)
스택 관리 커널 (VM_GROWSDOWN) pthread (고정 크기)
크기 동적 확장 가능 고정 (생성 시 결정)
최대 크기 RLIMIT_STACK (기본 8MB) pthread_attr_setstacksize()로 지정
위치 주소 공간 상단 mmap 영역 (중간)
/proc/PID/maps [stack] 레이블 레이블 없음 (익명 매핑)

메인 스레드 스택의 동적 확장

메인 스레드만 동적으로 스택이 확장된다:

"A thread's stack size is fixed at the time of thread creation. Only the main thread can dynamically grow its stack." — pthread_attr_setstacksize(3) man page1

동작 방식:

  1. 스택 포인터가 매핑 안 된 영역에 접근
  2. Page fault 발생
  3. 커널이 "스택 확장"으로 인식 (VM_GROWSDOWN 플래그)
  4. 새 페이지 할당

생성된 스레드는 mmap으로 할당된 고정 크기 영역이라 이 메커니즘이 적용되지 않는다.


2. 스레드 관리: pthread의 역할

pthread는 유저 레벨 코드다

시스템콜처럼 커널에서 제공하는 게 아니다. 시스템콜 호출과 연관된 작업(스택 공간 할당 및 관리)을 수행하는 POSIX 표준 함수이다.

pthread는 스레드 실행 중에는 아무것도 안 한다. 생성/종료/동기화 시점에만 작동:

pthread_create():
├── mmap()으로 스택 할당
├── mprotect()로 guard page 설정
├── TLS(Thread Local Storage) 초기화
└── clone() 시스템콜 호출

스레드 실행 중:
└── pthread 코드 실행 안 됨 (커널이 스케줄링)

pthread_exit():
├── cleanup handler 실행
├── TLS 소멸자 호출
├── 스택 munmap()
└── exit() 시스템콜

스레드 스택 크기 설정

pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 64 * 1024);  // 64KB
pthread_create(&thread, &attr, func, NULL);

기본값은 glibc가 RLIMIT_STACK을 참조해 결정한다 (일반적으로 8MB). 정확한 값은 pthread_attr_getstacksize()로 확인.

pthread 없이도 스레드 생성 가능

pthread는 편의 라이브러리일 뿐, 직접 시스템콜로 스레드를 만들 수 있다.2

컨텍스트 스위칭

컨텍스트 스위칭은 100% 커널이 한다. pthread는 관여하지 않는다.

커널이 저장/복원하는 것:

  • 범용 레지스터 (RAX, RBX, ... R15)
  • 스택 포인터 (RSP)
  • 명령어 포인터 (RIP)
  • 플래그 (RFLAGS)
  • 세그먼트 레지스터 (FS, GS) — TLS 포인터 포함
  • FPU/SSE/AVX 레지스터

pthread가 하는 일은 생성 시 초기 설정동기화 함수 래핑뿐이다.

TLS (Thread Local Storage)

__thread int my_errno;  // 각 스레드가 독립적인 복사본 가짐
  • FS 레지스터가 TLS 베이스 주소를 가리킴
  • 컨텍스트 스위칭 시 커널이 FS 레지스터를 저장/복원
  • TLS 메모리 영역 할당과 초기화는 pthread가 담당

3. 동기화

동기화는 프로그래머가 명시적으로 요청할 때만 발생한다.

Mutex

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock(&lock);    // 경합 시 futex()로 sleep
counter++;
pthread_mutex_unlock(&lock);  // 대기자 있으면 futex()로 wake

내부 동작:

  • 경합 없음: atomic CAS로 유저 공간에서 처리 (커널 개입 없음)
  • 경합 있음: futex() 시스템콜로 커널이 sleep/wake 처리

Atomic 연산

atomic_fetch_add(&counter, 1);  // CPU 명령어 하나 (lock xadd)

커널 개입 없이 하드웨어가 보장.

역할 분담

동기화 방식 유저 공간 커널
Atomic 연산 CPU 명령어 관여 안 함
Mutex (경합 없음) atomic CAS 관여 안 함
Mutex (경합 있음) futex() 호출 sleep/wake

4. 메모리 레이아웃

전체 구조 (64비트, flexible mmap layout)

High Address (0x7FFFFFFFFFFF)
┌─────────────────────┐
│ Main Thread Stack   │ [stack] - kernel managed, dynamic growth
│      grows down     │
├─────────────────────┤
│   (guard gap)       │ 기본 1MB, 부팅 시 변경 가능
├─────────────────────┤
│                     │
│   mmap region       │ grows down from below stack
│   - shared libs     │ (.so files)
│   - thread stacks   │ (allocated by pthread)
│   - large malloc    │ (> MMAP_THRESHOLD)
│   - file mappings   │
│   - shared memory   │
│                     │
│                     │
│    (free space)     │ ← 64-bit: 약 127TB
│                     │
│                     │
├─────────────────────┤
│      grows up       │
│ Heap (brk)          │ managed by malloc internally
├─────────────────────┤
│ BSS                 │ uninitialized globals
├─────────────────────┤
│ Data                │ initialized globals
├─────────────────────┤
│ Text                │ program code
└─────────────────────┘
Low Address (0x400000)

Heap vs mmap

Heap (brk) mmap
시스템콜 brk() mmap()
위치 Data/BSS 바로 위 주소 공간 중간-상단
성장 방향 위로 (↑) 아래로 (↓)
할당 brk 포인터를 위로 이동 가상 주소 공간 어디든 가능
반환 맨 위가 비어야만 brk를 내릴 수 있음 munmap()으로 즉시 OS에 반환 가능
관리 malloc (유저 공간) 커널
용도 작은 동적 할당 큰 할당, 파일 매핑, 스레드 스택 등

brk()와 malloc

brk()는 heap의 끝 주소를 이동하는 시스템콜:

sbrk(4096);  // heap 4KB 확장 (바이트 값을 받지만 실제론 페이지 단위로 올림)

malloc은 brk()로 받은 영역을 내부적으로 쪼개서 관리:

Heap from brk():
┌────┬────┬────┬────┐
│ 32 │FREE│ 64 │USED│
└────┴────┴────┴────┘
      ↑
   reused via free list

큰 할당 (기본 128KB 이상)은 mmap으로 처리해서 munmap()으로 바로 OS에 반환 가능.

malloc 또한 시스템콜이 아니라 C 표준 라이브러리 함수이며, 내부에 메모리 관리 알고리즘이 들어간다. 때문에 glibc의 기본 구현(ptmalloc2) 외에도 jemalloc, tcmalloc, mimalloc 등 더 나은 성능이나 메모리 효율을 제공하는 대체 구현체들이 존재한다.3

Flexible mmap layout

2004년 Ingo Molnar 패치로 도입4:

  • 이전: heap(↑)과 mmap(↑)이 같은 방향 → 충돌 위험
  • 이후: heap(↑)과 mmap(↓)이 반대 방향 → 가운데서 만남

64비트에서는 사이 공간이 약 127TB라 사실상 충돌 불가능. 만약 공간이 부족해지면 mmap() 호출 시 ENOMEM 에러가 반환된다.

32비트 vs 64비트: 가상 주소 공간 한계

32비트 환경에서는 물리 메모리가 아닌 가상 주소 공간 자체가 고갈되는 문제가 있었다.5

32비트 64비트
가상 주소 공간 4GB (고정) 약 128TB
스레드 스택 8MB 기준 약 500개 생성 가능 사실상 무제한
주소 공간 고갈 현실적 문제 사실상 불가능

32비트에서는 주소 공간이 4GB로 고정되어 있어서, 실제로 메모리를 안 쓰더라도 "예약"만으로 주소 공간을 다 써버리면 mmap()이 연속된 8MB 영역을 찾지 못해 실패한다. 64비트에서는 128TB 주소 공간에서 8MB씩 예약하는 건 사실상 무한대라 이 문제가 없다.


5. 보안 기능

Guard Page / Guard Gap

대상 방식 크기
메인 스택 Guard gap 기본 1MB (커널 4.12+)
pthread 스택 Guard page 기본 4KB (PROT_NONE)

메인 스택은 동적 확장이 필요해서 guard page가 아닌 guard gap 사용.6

메인 스택의 guard gap 크기는 부팅 시 커널 파라미터로 변경 가능하다:

# 페이지 단위로 지정 (예: 512페이지 = 2MB)
stack_guard_gap=512

pthread 스택의 guard 크기는 pthread_attr_setguardsize()로 스레드 생성 전에 설정 가능.

ASLR (Address Space Layout Randomization)

heap, stack, mmap 영역의 시작 주소가 랜덤화됨.

# 확인
$ cat /proc/sys/kernel/randomize_va_space
2  # 0=off, 1=stack/mmap만, 2=전체

6. 기술 관리 주체 구분

분류 예시 특징
ISO C 표준 malloc, free, printf 언어 표준, 모든 C 구현에 존재
POSIX 표준 pthread_create, pthread_mutex_lock OS 인터페이스 표준, Unix 계열
시스템콜 clone, brk, mmap, futex OS마다 다름, 커널 직접 호출

Linux, macOS, BSD 모두 POSIX 표준 함수를 제공하지만 내부 시스템콜은 다르다:

pthread_create()
├── Linux: clone()
├── macOS: bsdthread_create()
└── FreeBSD: thr_new()

7. 핵심 요약

  1. 메인 스레드 스택: 커널이 관리, 동적 확장, RLIMIT_STACK 제한7
  2. 생성된 스레드 스택: pthread가 mmap으로 할당, 고정 크기
  3. pthread: 시스템콜 래퍼 + 편의 기능 (TLS, cancellation 등), 없어도 직접 clone() 가능2
  4. 컨텍스트 스위칭: 커널이 전담, pthread는 관여 안 함
  5. 동기화: 경합 없으면 유저 공간, 경합 있으면 커널 (futex)
  6. Heap: brk()로 확장, malloc이 내부 관리
  7. mmap: 커널이 빈 자리에 매핑, 큰 할당/파일/스레드 스택 등에 사용
  8. 메모리 레이아웃: heap(↑)과 mmap(↓)이 반대 방향으로 성장

부록: /proc/PID/maps로 보는 메모리 레이아웃 분석

이 섹션은 CMCDragonkai의 "Understanding the Memory Layout of Linux Executables"를 기반으로 핵심 내용을 요약한 것이다.

/proc/PID/maps 읽는 법

00400000-00401000 r-xp 00000000 fc:00 1457150  /home/user/program
7f849c31b000-7f849c4d6000 r-xp 00000000 fc:00 1579071  /lib/x86_64-linux-gnu/libc-2.19.so
7fffb5d61000-7fffb5d82000 rw-p 00000000 00:00 0        [stack]
필드 의미
주소 범위 시작-끝 (left inclusive, right exclusive)
권한 r(read), w(write), x(execute), p(private)/s(shared)
오프셋 파일 매핑 시 파일 내 오프셋
장치 major:minor 장치 번호
inode 파일 inode 번호
경로 파일 경로 또는 [heap], [stack], [vdso]

File-backed mmap vs Anonymous mmap

참고: mmap - Wikipedia

File-backed mmap Anonymous mmap
/proc/PID/maps 파일 경로 표시 경로 없음 (00:00 0)
생성 mmap(fd, ...) mmap(..., MAP_ANONYMOUS, -1, ...)
물리 메모리 공유 같은 파일 매핑 시 공유 불가
메모리 부족 시 버림 (디스크에서 재로드) swap에 저장
용도 공유 라이브러리 (.so) 스레드 스택, 큰 malloc

File-backed의 장점: libc.so를 100개 프로세스가 써도 물리 메모리엔 한 번만 로드됨.

Demand Paging

file-backed mmap은 매핑 시점에 전체를 로드하지 않는다:

  1. mmap() → 가상 주소만 예약 (물리 메모리 할당 없음)
  2. 첫 접근 → page fault → 해당 페이지만 디스크에서 로드
  3. 이후 접근 → 물리 메모리에서 직접 읽음

"파일에 연결"은 매번 디스크를 읽는 게 아니라, 파일이 백업 저장소 역할을 한다는 의미다.

64비트 ELF 실행 파일의 메모리 구조

0x000000000000     ← 사용 안 함 (링커가 0x400000부터 시작하도록 설정)
0x400000           ← ELF 헤더, 프로그램 헤더
0x400720 (예시)    ← 실제 진입점 (_start), readelf --file-header로 확인
                   ← .text (실행 코드)
0x600000           ← .data (초기화된 전역 변수)
                   ← .bss (초기화 안 된 전역 변수)
                   ← [heap] - brk()로 확장, 위로 성장
        ...
                   ← 127TB 빈 공간 (64비트)
        ...
0x7f...            ← mmap 영역 (공유 라이브러리, 스레드 스택, 큰 malloc)
0x7fff...          ← [stack] - 아래로 성장
0x7fff...          ← [vdso] - 커널 제공 가상 공유 라이브러리
0xffffffffff600000 ← [vsyscall] - 레거시, 고정 주소

brk vs mmap: malloc의 내부 동작

조건 사용 시스템콜 특징
작은 할당 (< MMAP_THRESHOLD) brk()/sbrk() 힙 영역 연속 확장, [heap]으로 표시
큰 할당 (≥ MMAP_THRESHOLD, 기본 128KB) mmap() mmap 영역에 할당, 레이블 없음

brk() 호출 시 glibc는 기본적으로 128KB(M_TOP_PAD)를 추가로 확보해 시스템콜 횟수를 줄인다:

// 1000바이트 malloc → 실제 brk()는 1000 + 128KB + 페이지 정렬
addr = malloc(1000);
// /proc/PID/maps에서 [heap]이 132KB로 표시됨

brk() 힙의 한계: 맨 위 바이트가 해제되지 않으면 힙 크기를 줄일 수 없다. mmap()munmap()으로 즉시 반환 가능.

ASLR과 PIE

ASLR (Address Space Layout Randomization): 스택, mmap 영역, 힙의 시작 주소를 실행할 때마다 랜덤화. 보안 기능.

$ cat /proc/sys/kernel/randomize_va_space
2  # 0=off, 1=stack/mmap만, 2=전체(heap 포함)

PIE (Position Independent Executable): 실행 파일 자체의 주소도 랜덤화. gcc -fPIE -pie로 컴파일.

  • 일반 실행 파일: 항상 0x400000에서 시작
  • PIE 실행 파일: 매번 다른 주소에서 시작

vdso와 vsyscall

둘 다 컨텍스트 스위칭 없이 빠른 시스템콜을 제공:

vsyscall vdso
주소 고정 (0xffffffffff600000) ASLR 적용
제공 syscall gettimeofday, time, getcpu 동일 + 확장 가능
상태 레거시 (호환성 유지용) 현재 사용

분석 도구

도구 용도
cat /proc/PID/maps 메모리 영역 확인
cat /proc/PID/smaps 상세 메모리 정보 (RSS, PSS 등)
pmap -x PID 사람이 읽기 쉬운 형태로 출력
readelf -h ELF 헤더, 진입점 확인
objdump -d 디스어셈블
strace 시스템콜 추적 (brk, mmap 등)
gcore PID 프로세스 메모리 덤프

실습: malloc 전후 비교

# 터미널 1: 프로그램 실행 후 getchar()로 대기
$ ./program
Before malloc...

# 터미널 2: 메모리 맵 확인
$ cat /proc/$(pgrep program)/maps | grep heap
# (없음 - malloc 전)

# 터미널 1: Enter 입력 후 malloc 실행
After malloc...

# 터미널 2: 다시 확인
$ cat /proc/$(pgrep program)/maps | grep heap
019a3000-019c4000 rw-p 00000000 00:00 0  [heap]

참고 자료

Footnotes

  1. pthread_attr_setstacksize(3) man page

  2. pthread 없이 직접 clone() 시스템콜로 스레드를 생성하는 예제: Raw Linux Threads via System Calls 2

  3. jemalloc (FreeBSD, Firefox, Redis 등에서 사용), tcmalloc (Google), mimalloc (Microsoft) 등이 대표적이다. 멀티스레드 환경에서 lock contention 감소, 메모리 단편화 개선 등의 장점이 있다.

  4. Flexible mmap layout에 대한 LWN 기사: Reorganizing the address space (2004)

  5. 32비트 환경에서의 스레드 생성 한계에 대한 분석: The problem of thread creation

  6. Stack Clash 취약점과 guard gap 도입에 대한 LWN 기사: Preventing stack guard-page hopping (2017)

  7. RLIMIT_STACK은 프로세스 스택의 최대 크기(바이트)를 지정한다. 이 제한에 도달하면 SIGSEGV 시그널이 생성된다. getrlimit(2) man page 참조.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment