Step 70. 프로젝트 — C로 메모리 관찰 도구 만들기

Step 70. 프로젝트 — C로 메모리 관찰 도구 만들기

Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★★★☆ | 예상 소요 시간 4시간

전제: Step 56~69를 마쳤다. 포인터와 주소 출력, malloc/free, 메모리의 네 영역, 실행 파일의 구조, 가상 메모리의 개념을 안다.

  • 준비물: 리눅스 터미널(WSL 또는 우분투), gcc, 그리고 지난 챕터들에서 쌓은 모든 것.
  • 주의: 이 챕터는 배움이 아니라 종합 시험입니다. 오늘은 막혀도 좋습니다. 프로젝트는 원래 막히면서 완성하는 것입니다. 실습 자체는 100% 안전합니다.

지금까지 우리는 메모리를 "배우는 대상"으로만 다뤘습니다. 오늘은 위치를 바꿉니다. 메모리를 관찰하는 도구를 직접 만드는 것입니다. 의사가 될 필요는 없지만, 적어도 청진기를 직접 만들어 본 사람이 환자의 소리를 다르게 듣습니다. 만들 것은 단순합니다 — 전역 변수, 정적 변수, 지역 변수, malloc으로 받은 힙, 함수. 다섯 종류의 존재가 메모리 어디에 사는지 주소를 찍고, 주소 순서대로 정렬해 "지도"로 보여 주는 프로그램입니다. 여기에 영역 사이의 거리까지 계산해 주면, 여러분은 눈으로 보는 메모리 지도를 갖게 됩니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • 자기 자신의 메모리 주소 지도를 그려 주는 C 프로그램 memmap을 완성한다
  • 주소를 uintptr_t로 바꿔 비교·정렬·거리 계산을 한다
  • 버블 정렬로 주소를 오름차순 정렬한다
  • 실행할 때마다 주소가 바뀌는 현상(ASLR)을 데이터로 확인하고 설명한다
  • 파일 속 주소(readelf)와 실행 중 주소의 관계를 설명한다

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 C 언어 + 리눅스 터미널 (WSL 우분투 24.04, gcc 13.3.0에서 실측)
오늘의 명령어 gcc -o memmap memmap.c, readelf -h memmap | grep Entry(진입점 비교), pmap PID(프로 도구 맛보기)
오늘의 C 요소 &변수·함수명으로 주소 수집, uintptr_t(주소를 담는 정수형), %p 서식, 구조체 배열, 버블 정렬
필요한 개념 메모리 네 영역(코드/데이터/힙/스택), 가상 주소, ASLR, Step 66의 실행 파일 구조
오늘의 산출물 memmap — 내 프로세스의 메모리 지도를 그려 주는 관찰 도구 + 관찰 일지

2-1. 오늘의 설계도

프로그램의 뼈대는 이렇습니다.

  1. 관찰 대상을 만든다: 전역 변수, 정적(static) 변수, 지역 변수, malloc으로 받은 힙 블록, 함수(main 말고 하나 더).
  2. 각각의 주소를 수집한다. C에서는 &변수명으로 변수의 주소를, 함수명으로 함수의 주소를 얻습니다.
  3. 주소와 이름을 짝지어 배열에 담고, 주소 오름차순으로 정렬한다.
  4. 정렬된 순서대로 이름과 주소, 이웃과의 거리(주소 차이)를 출력한다.

2-2. 주소를 다루는 기술적 디테일

주소를 정렬하려면 주소를 "비교 가능한 숫자"로 다뤄야 합니다. 종류가 다른 포인터(변수 주소와 함수 주소)를 섞어 비교하면 컴파일러가 경고를 낼 수 있으므로, 오늘은 모든 주소를 uintptr_t라는 정수형으로 바꿔 담습니다. stdint.h에 정의된 이 타입은 "포인터를 손실 없이 담을 수 있는 정수"라고 약속된 타입입니다. 정수니까 비교도, 뺄셈(거리 계산)도 자유롭습니다. 함수 주소는 (uintptr_t)(void*)main처럼 void*를 한 단계 거치면 경고 없이 변환됩니다.

출력할 때는 반대로 (void*)로 되돌려 %p로 찍습니다. %p는 포인터를 0x… 형태로 출력해 주는 서식입니다.

2-3. 정렬 알고리즘 — 버블 정렬

오늘 정렬 대상은 겨우 6개이므로 가장 단순한 버블 정렬(bubble sort)이면 충분합니다. 이웃한 두 원소를 비교해 순서가 틀렸으면 맞바꾸기를 반복하는 알고리즘으로, 거품이 떠오르듯 큰 값이 뒤로 밀려납니다. 원소 n개에 대해 최대 n×n번 비교하므로 느리지만, n이 6이면 36번이라 눈 깜짝할 새에 끝납니다.


3. 따라 하기

3-1. 뼈대 만들기 — 여섯 대상의 주소 수집

먼저 관찰 대상을 만들고 주소를 찍어 봅니다. 아래 코드를 memmap.c로 저장하세요.

입력 (memmap.c)

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

int g_var = 10;              /* 전역 변수: 데이터 영역 */
static int s_var = 20;       /* 정적 변수: 데이터 영역 */

void helper(void) {          /* 함수: 코드 영역 */
}

int main(void) {
    int l_var = 30;                       /* 지역 변수: 스택 */
    int *h_var = malloc(sizeof(int));     /* 힙 블록 */
    if (h_var == NULL) { return 1; }
    *h_var = 40;

    printf("전역 변수  : %p\n", (void*)&g_var);
    printf("정적 변수  : %p\n", (void*)&s_var);
    printf("힙(malloc): %p\n", (void*)h_var);
    printf("지역 변수  : %p\n", (void*)&l_var);
    printf("main 함수  : %p\n", (void*)main);
    printf("helper 함수: %p\n", (void*)helper);

    free(h_var);
    return 0;
}

입력

gcc -o memmap memmap.c && ./memmap

출력 (2026-09-09 실측):

전역 변수  : 0x59d2d21d1010
정적 변수  : 0x59d2d21d1014
힙(malloc): 0x59d3120c72a0
지역 변수  : 0x7ffc8e8b9e9c
main 함수  : 0x59d2d21ce1b4
helper 함수: 0x59d2d21ce1a9

읽는 법: 주소를 자세히 보면 묶음이 보입니다. 함수들(0x…e1b4, 0x…e1a9)과 전역·정적 변수들(0x…1010, 0x…1014)은 0x59d2d2… 대역에 모여 있고, 힙도 0x59d3…로 비슷한 대역의 조금 더 큰 곳에, 지역 변수만 0x7ffc…라는 전혀 다른 높은 곳에 있습니다. Step 60에서 배운 "낮은 쪽에 코드·데이터, 그 위에 힙, 높은 곳에 스택" 지도의 실측값입니다. 재미있는 점 하나 — helper가 main보다 낮은 주소에 있습니다. 소스에 먼저 쓴 함수가 먼저 배치된 것입니다.

: 정렬 없이도 지도의 윤곽이 보인다는 것을 먼저 확인하는 단계입니다. 여러분의 눈이 이미 메모리 지도를 읽기 시작했습니다.

3-2. 예측해 보기 — 정렬하면 무슨 순서가 될까

본격적으로 지도를 만들기 전에, 위 출력을 보고 예측하세요. 여섯 대상을 주소 오름차순(낮은 것부터)으로 줄 세우면 어떤 순서가 될까요? 종이에 "helper → main → …" 식으로 적어 두세요. 힌트는 위 실측 출력에 있습니다.

3-3. 지도 완성 — 수집, 정렬, 거리 계산

예측을 적었으면 프로그램을 업그레이드합니다. memmap2.c로 저장하세요.

입력 (memmap2.c)

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

int g_var = 10;
static int s_var = 20;
void helper(void) { }

typedef struct {
    const char *name;
    uintptr_t addr;
} Entry;

int main(void) {
    int l_var = 30;
    int *h_var = malloc(sizeof(int));
    if (h_var == NULL) { return 1; }
    *h_var = 40;

    Entry entries[] = {
        {"전역 변수",   (uintptr_t)(void*)&g_var},
        {"정적 변수",   (uintptr_t)(void*)&s_var},
        {"힙(malloc)", (uintptr_t)(void*)h_var},
        {"지역 변수",   (uintptr_t)(void*)&l_var},
        {"main 함수",   (uintptr_t)(void*)main},
        {"helper 함수", (uintptr_t)(void*)helper},
    };
    int n = sizeof(entries) / sizeof(entries[0]);

    /* 버블 정렬: 주소 오름차순 */
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - 1 - i; j++) {
            if (entries[j].addr > entries[j + 1].addr) {
                Entry tmp = entries[j];
                entries[j] = entries[j + 1];
                entries[j + 1] = tmp;
            }
        }
    }

    printf("===== 내 프로그램의 메모리 지도 =====\n");
    for (int i = 0; i < n; i++) {
        printf("%-12s %p", entries[i].name, (void*)entries[i].addr);
        if (i + 1 < n) {
            uintptr_t gap = entries[i + 1].addr - entries[i].addr;
            printf("  (다음까지 거리: %lu 바이트)", (unsigned long)gap);
        }
        printf("\n");
    }

    free(h_var);
    return 0;
}

입력

gcc -o memmap2 memmap2.c && ./memmap2

출력 (2026-09-09 실측):

===== 내 프로그램의 메모리 지도 =====
helper 함수 0x5f7cd1b8b1e9  (다음까지 거리: 11 바이트)
main 함수  0x5f7cd1b8b1f4  (다음까지 거리: 11804 바이트)
전역 변수 0x5f7cd1b8e010  (다음까지 거리: 4 바이트)
정적 변수 0x5f7cd1b8e014  (다음까지 거리: 720413324 바이트)
힙(malloc)  0x5f7cfca982a0  (다음까지 거리: 35732726228156 바이트)
지역 변수 0x7ffca91da75c

읽는 법: 주소 순서대로 정렬된 지도가 나왔습니다. 읽을 것은 네 가지입니다. ① helper와 main은 11바이트 차이로 붙어 있습니다(코드 영역). ② 전역과 정적 변수는 4바이트(int 크기) 차이입니다(데이터 영역). ③ 정적 변수에서 힙까지는 7.2억 바이트가 뚫려 있습니다. ④ 힙에서 지역 변수(스택)까지는 약 35조 바이트(32.5TB)라는 어마어마한 거리가 있습니다. 이 빈 공간이 가상 주소 공간의 여유분입니다. 실제 RAM이 아니라 "약속된 주소 공간"이기에 이렇게 넓을 수 있습니다.

: 수집 → 정렬 → 시각화라는 데이터 처리의 기본 흐름을 메모리 주소에 적용한 것이 이 프로그램의 전부입니다. 그런데 이 전부가 시스템 도구들이 하는 일의 축소판입니다.

예측 확인: 아까 적은 순서와 비교하세요. 예측이 틀린 부분이 있다면, 그 이유를 /proc/self/maps(Step 69)와 비교하며 찾아보세요. 관찰 도구의 존재 이유가 "교과서 그림과 현실의 차이를 보는 것"입니다.

3-4. 예측해 보기 — 세 번 실행하면 주소는?

방금 만든 memmap2를 세 번 연속 실행하면, 출력되는 주소는 매번 같을까요 다를까요? 거리는 어떨까요? Step 69의 ASLR 실험을 떠올리며 예측해 보세요.

입력

./memmap2 | head -n 3; ./memmap2 | head -n 3; ./memmap2 | head -n 3

출력 (2026-09-09 실측):

===== 내 프로그램의 메모리 지도 =====
helper 함수 0x592c11eb91e9  (다음까지 거리: 11 바이트)
main 함수  0x592c11eb91f4  (다음까지 거리: 11804 바이트)
===== 내 프로그램의 메모리 지도 =====
helper 함수 0x59162e5691e9  (다음까지 거리: 11 바이트)
main 함수  0x59162e5691f4  (다음까지 거리: 11804 바이트)
===== 내 프로그램의 메모리 지도 =====
helper 함수 0x5b7ee516d1e9  (다음까지 거리: 11 바이트)
main 함수  0x5b7ee516d1f4  (다음까지 거리: 11804 바이트)

읽는 법: 실행할 때마다 주소가 바뀝니다(0x592c…, 0x5916…, 0x5b7e…). 그런데 자세히 보면 끝자리(1e9, 1f4)와 거리(11, 11804)는 세 번 모두 똑같습니다. 바뀌는 것은 앞쪽의 시작점뿐입니다. 이것이 ASLR(Address Space Layout Randomization, 주소 공간 배치 무작위화)입니다. 운영체제가 실행할 때마다 메모리 지도 전체를 무작위 위치로 통째로 옮기되, 지도 안의 상대 위치는 그대로 두기 때문입니다.

: 공격자가 "변수 X는 주소 0x…e010에 있다"를 미리 알아내지 못하게 만드는 것이 ASLR의 목적입니다. 메모리 공격의 대부분은 정확한 주소를 필요로 하기 때문입니다. 여러분은 방금, 세계에서 가장 널리 쓰이는 메모리 방어 기술을 자기가 만든 도구로 관측한 것입니다.

3-5. 실행 파일의 진입점과 비교

마지막으로 Step 66의 readelf를 꺼내, 파일 속 주소와 실행 중 주소를 비교합니다.

입력

readelf -h memmap2 | grep -E "Type|Entry"

출력 (2026-09-09 실측):

  Type:                              DYN (Position-Independent Executable file)
  Entry point address:               0x1100

읽는 법: 실행 파일 안에 적힌 진입점은 0x1100처럼 작은 숫자인데, 실행 중에 찍힌 main 함수 주소는 0x5f7c…로 시작하는 큰 숫자입니다. Type이 DYN(위치 독립 실행 형식)이기에, 파일 속 주소는 "지도의 원점을 0으로 둔 상대 좌표"이고, 실행 중 주소는 ASLR로 정해진 원점을 더한 "절대 좌표"인 셈입니다. 3-4에서 끝자리가 보존된 것도 이 구조 때문입니다 — 원점만 바뀌고 상대 좌표는 그대로니까요.

: 실행 파일의 구조(Step 66), 가상 메모리(Step 69), ASLR(오늘)이 하나의 그림으로 맞물리는 순간입니다. 이 연결고리가 보이면 Level 1의 메모리 대장정은 사실상 끝난 것입니다.

3-6. 프로 도구 맛보기 — pmap

여러분의 memmap은 사실 프로 도구들의 아기 버전입니다. pmap PID는 임의 프로세스의 메모리 지도를 보여 줍니다.

입력

sleep 20 &
pmap 1495

(sleep 20 &을 실행하면 화면에 [1] 1495처럼 방금 띄운 프로세스의 PID가 표시됩니다. 그 번호를 pmap 뒤에 적습니다 — 여러분의 번호는 다릅니다.)

출력 (2026-09-09 실측, 상단 일부):

1495:   sleep 20
00005e6ca2776000      8K r---- sleep
00005e6ca2778000     16K r-x-- sleep
00005e6ca277c000      4K r---- sleep
00005e6ca277d000      4K r---- sleep
00005e6ca277e000      4K rw--- sleep
00005e6cd3c2e000    132K rw---   [ anon ]
0000709210400000    160K r---- libc.so.6

읽는 법: sleep이라는 작은 프로그램에도 지도가 있습니다. r-x–(실행 가능, 코드)와 rw—(읽기 쓰기, 데이터)가 분리되어 있고, libc(표준 라이브러리)가 공유되어 올라와 있습니다. memmap이 여섯 점으로 그린 지도를, pmap은 구간 전체로 그려 주는 것입니다. 둘의 원천 데이터는 같습니다 — Step 69의 /proc/PID/maps입니다.

: 오늘 만든 도구가 있어서, 앞으로 만날 프로 도구들(gdb, pmap)의 출력이 "아, 나도 아는 그 지도구나"로 읽히게 됩니다.


4. 미션과 연습문제

미션 — memmap 완성과 관찰 일지

  1. 기본 지도: 3-3의 코드를 완성해 여섯 대상의 주소가 정렬되어 출력되게 합니다
  2. 영역 이름표: 출력 각 줄에 "(코드 영역)", "(데이터 영역)", "(힙)", "(스택)" 같은 영역 라벨을 붙입니다. 주소 대역으로 판별해도 되고, 직접 표기해도 됩니다
  3. ASLR 기록: 프로그램을 3번 실행해 helper 함수의 주소를 적고, "무엇이 바뀌고 무엇이 보존되는가"를 두 문장으로 정리합니다
  4. 거리 해석: 힙과 스택 사이의 거리가 왜 그렇게 큰지 가상 메모리 관점에서 한 문단으로 설명합니다
  5. 진입점 비교: readelf의 Entry point와 실행 중 main 주소의 차이를 관찰 일지에 기록합니다

연습문제

문제 1. 포인터를 uintptr_t로 바꿔 담는 이유를 "비교와 뺄셈"의 관점에서 설명해 보세요.

문제 2. 버블 정렬의 한 번의 맞바꾸기(3줄짜리 교환 코드)가 하는 일을 설명해 보세요.

문제 3. ASLR 실행 기록에서 "끝자리와 거리는 보존되고 앞자리만 바뀌는" 이유를 상대 좌표와 원점이라는 말로 설명해 보세요.

문제 4. 실행 파일 속 진입점(0x1100)과 실행 중 main 주소(0x5f7c…)가 다른 이유를 Type: DYN과 연결해 설명해 보세요.


5. 모범 답안과 완료 기준

미션 모범 답안

영역 이름표는 Entry 구조체에 영역 이름을 하나 더 넣는 것이 가장 간단합니다:

typedef struct {
    const char *name;
    const char *region;   /* 영역 이름표 */
    uintptr_t addr;
} Entry;

Entry entries[] = {
    {"전역 변수",  "(데이터 영역)", (uintptr_t)(void*)&g_var},
    {"정적 변수",  "(데이터 영역)", (uintptr_t)(void*)&s_var},
    {"힙(malloc)", "(힙)",        (uintptr_t)(void*)h_var},
    {"지역 변수",  "(스택)",       (uintptr_t)(void*)&l_var},
    {"main 함수",  "(코드 영역)",  (uintptr_t)(void*)main},
    {"helper 함수", "(코드 영역)", (uintptr_t)(void*)helper},
};
/* 출력부: printf("%-12s %-10s %p", name, region, addr); 식으로 한 칸 추가 */

관찰 일지 예시 (2026-09-09 실측 근거):

3. ASLR 3회: helper = 0x592c11eb91e9 / 0x59162e5691e9 / 0x5b7ee516d1e9
   → 바뀌는 것: 앞쪽 시작점(원점). 보존되는 것: 끝자리(1e9)와 이웃과의 거리(11, 11804).
4. 힙-스택 거리 35,732,726,228,156바이트(약 32.5TB): 이 공간은 실제 RAM이 아니라
   프로세스에게 약속된 가상 주소 공간이다. 약속이기에 RAM 크기와 무관하게 넓을 수 있다.
5. 파일 속 진입점 0x1100 vs 실행 중 main 0x5f7c...: DYN 형식이라 파일 속 주소는
   상대 좌표, 실행 중에는 ASLR 원점이 더해진 절대 좌표.

검증하는 법: ① 출력이 주소 오름차순인가. ② 전역-정적 거리가 4바이트(int 크기)인가. ③ 3회 실행 기록에서 끝자리 보존이 확인되는가. ④ 일지에 "가상 주소 공간"이라는 단어로 거리 해석이 쓰였는가. 넷이 ‘예’이면 완성입니다. 완성한 memmap2.c는 여러분의 첫 "시스템 도구"이니 잘 보관해 두세요.

연습문제 해답

문제 1 해답. 정수로 바꾸면 종류가 다른 포인터(변수 주소와 함수 주소)라도 대소 비교와 뺄셈이 가능하기 때문입니다. uintptr_t는 "포인터를 손실 없이 담는 정수"라고 약속된 타입(stdint.h)이라, 정렬(비교)과 거리 계산(뺄셈)을 안전하게 할 수 있습니다.

문제 2 해답. 이웃한 두 원소의 주소를 비교해, 앞이 뒤보다 크면 임시 변수(tmp)에 앞을 피신시키고, 뒤를 앞으로 옮기고, 피신시켰던 앞을 뒤에 놓는 세 걸음의 자리 바꿈입니다. 이 바꿈을 반복하면 큰 주소가 거품처럼 뒤로 밀려나 오름차순이 됩니다.

문제 3 해답. ASLR은 지도의 원점(시작 주소)만 무작위로 옮기고, 지도 안의 상대 좌표는 그대로이기 때문입니다. 파일 속에서 결정된 상대 위치(끝자리 1e9 같은)는 실행마다 보존되고, 원점(0x592c…, 0x5916…)만 바뀝니다. 상대 위치가 같으니 이웃과의 거리도 같습니다.

문제 4 해답. Type이 DYN(위치 독립 실행 파일)이기 때문입니다. 이 형식은 어느 주소에 실려도 실행되도록, 파일 속 주소를 "원점으로부터의 거리(상대 좌표)"로 적어 둡니다. 실행될 때 운영체제가 원점을 정하고(ASLR) 그 원점을 더한 값이 실행 중 주소(절대 좌표)가 됩니다. 그래서 파일 속 0x1100과 실행 중 0x5f7c…는 같은 곳의 두 표기입니다.

완료 기준 체크리스트

  • [ ] 여섯 대상의 주소를 수집해 출력할 수 있다
  • [ ] 주소를 uintptr_t로 바꿔 버블 정렬로 오름차순 정렬할 수 있다
  • [ ] 영역 간 거리를 계산해 출력할 수 있다
  • [ ] ASLR 현상을 3회 실행으로 관측하고 "끝자리 보존"을 설명할 수 있다
  • [ ] 파일 속 주소(상대)와 실행 중 주소(절대)의 관계를 설명할 수 있다
  • [ ] pmap으로 다른 프로세스의 지도를 열어 봤다
  • [ ] 미션: 관찰 일지 다섯 항목을 모두 완료했다

6. 흔한 실수와 해결

벽 1. 한글 이름의 출력 정렬이 어긋난다

증상 (2026-09-09 실측): %-12s로 폭을 맞췄는데 한글 이름 줄의 주소 위치가 들쭉날쭉합니다.

helper 함수 0x5f7cd1b8b1e9
main 함수  0x5f7cd1b8b1f4

원인: %-12s의 12는 "글자 12자"가 아니라 "바이트 12개"입니다. 한글은 UTF-8에서 한 글자가 3바이트(Step 50)라, 한글 네 글자면 12바이트를 다 써 버려 빈 칸이 남지 않습니다.
해결: 출력용으로는 신경 쓰지 않아도 됩니다(지도의 내용이 중요하니까요). 맞추고 싶다면 영문 이름으로 바꾸거나, 폭을 더 넉넉히 잡으세요.

벽 2. 함수 주소에서 컴파일 경고가 나온다

증상: 함수 주소를 변환하는 부분에서 포인터 변환 관련 경고가 뜹니다.
원인: 함수 포인터와 일반 포인터는 C 표준상 엄밀히 다른 존재라서, 캐스팅에 엄격한 컴파일러가 잔소리를 합니다.
해결: (uintptr_t)(void*)main처럼 void*를 한 단계 거치면 경고가 사라집니다. 3-3의 코드가 바로 그 형태입니다.

벽 3. malloc한 주소를 free한 뒤에 찍는다

증상: free 이후에 h_var을 출력하니 주소는 나오는데 뭔가 찜찜합니다.
원인: free는 메모리를 반납할 뿐, 변수 h_var 안의 숫자(주소 값)를 지우지는 않습니다. 그래서 주소 자체는 출력됩니다.
해결: 오늘처럼 "주소 값"만 필요한 경우는 괜찮지만, 그 주소에 접근하면 댕글링 포인터(반납된 자리를 가리키는 포인터) 오류입니다. 출력은 free 전에 끝내는 습관을 들이세요 — 3-3 코드가 그 순서입니다.

벽 4. 거리가 엉뚱한 숫자로 나온다

증상: 거리 출력이 음수처럼 보이거나 말도 안 되는 값입니다.
원인: 뺄셈 결과를 잘못된 서식(예: %d)으로 찍으면 부호 문제로 엉망이 됩니다.
해결: 거리는 부호 없는 큰 정수이므로 %lu(unsigned long) 캐스팅을 함께 쓰세요. 서식 지정자와 타입의 궁합은 C 출력의 영원한 함정입니다.

벽 5. 내 출력이 책과 순서가 다르다

증상: 함수들이 데이터보다 낮은 주소에 오는 등, 책의 실측과 배치가 다릅니다.
원인: 컴파일러와 링커의 배치는 환경에 따라 달라집니다. 책의 출력은 2026-09-09 gcc 13.3.0 실측 한 번의 기록입니다.
해결: 그것이 정답입니다. 관찰 도구의 존재 이유가 "교과서와 현실의 차이를 보는 것"입니다. 다르게 나온 순서를 관찰 일지에 기록하세요. 대신 바뀌지 않는 것 — 끝자리 보존과 네 영역의 대역 분리 — 은 확인되어야 합니다.


7. 정리

오늘의 개념

개념 한 줄 설명
uintptr_t 포인터를 손실 없이 담는 정수형 — 비교와 뺄셈을 가능하게
버블 정렬 이웃 비교·교환의 반복 — 작은 데이터에는 이것으로 충분
ASLR 실행마다 지도의 원점을 옮기는 방어 — 끝자리와 거리는 보존된다
상대 좌표 / 절대 좌표 파일 속 주소(DYN 형식) / 실행 중 주소(ASLR 원점 반영)
관찰 도구 교과서 그림과 현실의 차이를 보여 주는, 직접 만든 청진기

오늘의 명령어·구문

명령어·구문 하는 일
&변수 / 함수명 변수 / 함수의 주소 얻기
(uintptr_t)(void*)주소 포인터를 비교 가능한 정수로 변환
%p 포인터를 0x… 형태로 출력
readelf -h 파일 | grep Entry 파일 속 진입점(상대 좌표) 확인
pmap PID 임의 프로세스의 메모리 지도 보기
./memmap2 | head -n 3 (3회) ASLR 관측 — 끝자리 보존 확인

명령어보다 중요한 감각

Step 56에서 "주소라는 숫자"를 처음 찍어 본 것이 엊그제 같은데, 오늘 여러분은 그 숫자들로 지도를 그리는 도구를 만들었습니다. 이 도구와 함께라면 앞으로 만날 모든 메모리 이야기가 추상이 아니라 실측이 됩니다. "스택이 힙보다 높은 주소에 있다더니, 진짜?" 같은 의문이 들면 이 도구를 꺼내 들면 됩니다. 좋은 도구는 평생 씁니다.

보안과의 연결: 오늘 만든 memmap은 자기 자신만 보는 무해한 도구지만, 이 아이디어를 남의 프로세스로 확장하면 강력한 분석 도구가 됩니다. 침해사고 대응에서는 메모리 덤프로 악성코드가 숨은 곳의 지도를 그리고, 공격자도 같은 지도를 그려 공격 지점을 찾습니다. 도구 자체에 선악은 없고 어디를 향하느냐가 전부입니다. ASLR도 만능은 아닙니다 — 프로그램이 주소를 출력해 버리는 실수(정보 노출 취약점) 하나면 무작위화가 물거품이 되므로, 실전 공격은 "주소 하나만 새면 된다"는 식으로 정보 노출부터 노립니다. 방어가 어떻게 우회되는지를 아는 것이 방어를 더 잘하는 길입니다. 오늘 여러분의 도구가 관찰한 것은 철저히 여러분 자신의 프로세스였고, 모든 공격 실습은 내 랩·합법 플랫폼에서만입니다. 실서비스 무단 공격은 범죄입니다.


전부 체크되면 Step 70 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.