<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>confidence</title>
        <link>https://velog.io/</link>
        <description>잘할 수밖에 없는 자신감</description>
        <lastBuildDate>Fri, 09 Oct 2026 08:24:54 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>confidence</title>
            <url>https://velog.velcdn.com/images/jae_yun/profile/d52d7b42-798a-4823-a53b-f202ec38062b/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. confidence. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jae_yun" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[alarm 클럭이 뭔지랑 alarm clock에서 스레드는 어떻게 사용되는가]]></title>
            <link>https://velog.io/@jae_yun/alarm-%ED%81%B4%EB%9F%AD%EC%9D%B4-%EB%AD%94%EC%A7%80%EB%9E%91-alarm-clock%EC%97%90%EC%84%9C-%EC%8A%A4%EB%A0%88%EB%93%9C%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%AC%EC%9A%A9%EB%90%98%EB%8A%94%EA%B0%80</link>
            <guid>https://velog.io/@jae_yun/alarm-%ED%81%B4%EB%9F%AD%EC%9D%B4-%EB%AD%94%EC%A7%80%EB%9E%91-alarm-clock%EC%97%90%EC%84%9C-%EC%8A%A4%EB%A0%88%EB%93%9C%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%AC%EC%9A%A9%EB%90%98%EB%8A%94%EA%B0%80</guid>
            <pubDate>Fri, 09 Oct 2026 08:24:54 GMT</pubDate>
            <description><![CDATA[<h1 id="1-alarm-clock이란-무엇인가">1. Alarm Clock이란 무엇인가?</h1>
<h3 id="1-본질적-정의">(1) 본질적 정의</h3>
<p>운영체제(OS)에서 실행 중인 스레드를 &quot;정해진 시간(ticks) 동안 잠재우고(Sleep/Blocked), 해당 시간이 경과하면 다시 깨워 실행 가능한 상태(Ready)로 만드는 기능&quot;입니다.</p>
<h3 id="2-기존-pintos의-문제점-바쁜-대기-busy-waiting">(2) 기존 Pintos의 문제점: 바쁜 대기 (Busy Waiting)</h3>
<p>Pintos의 초기 <code>timer_sleep()</code> 코드는 다음과 같이 작성되어 있습니다.</p>
<pre><code class="language-c">/* 기존 Pintos 코드의 Busy Waiting 방식 */
void timer_sleep (int64_t ticks) {
    int64_t start = timer_ticks ();
    while (timer_elapsed (start) &lt; ticks) 
        thread_yield (); // &quot;아직 시간 안 됐네? 일단 CPU 양보할게!&quot;[cite: 3]
}
</code></pre>
<ul>
<li><strong>동작 방식</strong>: 스레드가 CPU를 잡고 루프를 돌면서 &quot;지금 몇 시지? 아직 안 됐네? 그럼 양보(<code>thread_yield</code>)할게&quot;를 무한 반복합니다.</li>
</ul>
<ul>
<li><strong>문제점</strong>:</li>
</ul>
<ol>
<li><code>thread_yield()</code>를 호출하면 스레드는 여전히 준비 큐(<code>ready_list</code>)에 남습니다.</li>
<li>스케줄러는 준비 큐에 있는 이 스레드를 계속 뽑아서 CPU에 올려줍니다.</li>
<li>스레드는 아무런 유의미한 작업도 하지 않으면서 <strong>CPU 점유율을 100% 낭비</strong>하고, 다른 스레드의 실행을 방해합니다.</li>
</ol>
<h3 id="3-개선-목표-sleep--wakeup-방식">(3) 개선 목표: Sleep / Wakeup 방식</h3>
<ul>
<li>스레드가 잠들어야 할 때 <strong>스스로 CPU 제어권을 완전히 반납</strong>하고 대기 큐(<code>sleep_list</code>)로 들어갑니다.</li>
<li>스케줄러의 탐색 대상(<code>ready_list</code>)에서 완전히 배제되어 CPU 점유율이 0%가 됩니다.</li>
<li>목표 시간에 도달했을 때 <strong>하드웨어 타이머 인터럽트가 이 스레드를 깨워서</strong> 준비 큐(<code>ready_list</code>)로 복귀시킵니다.</li>
</ul>
<hr>
<h1 id="2-스레드는-물리적으로논리적으로-어디에-있는가">2. 스레드는 물리적으로/논리적으로 어디에 있는가?</h1>
<h3 id="1-물리적-메모리-관점-ram-4kb-커널-페이지">(1) 물리적 메모리 관점 (RAM: 4KB 커널 페이지)</h3>
<p>스레드가 실행 중이든, 잠들어 있든(Blocked) 스레드의 물리적 실체는 <strong>RAM 커널 풀에 할당된 4KB 크기의 메모리 페이지 안에 그대로 박제</strong>되어 있습니다.</p>
<pre><code class="language-text">[ RAM 상의 단일 스레드 (4KB 커널 페이지 구조) ][cite: 1]

주소 높음 (0xXXXX_1000)
┌────────────────────────────────────────────────────────┐
│  Kernel Stack (커널 스택)                              │
│   - 함수 호출 기록, 지역 변수                          │
│   - ★ 잠들기 직전 CPU 레지스터 백업 본 (Context)      │
│     (rip: 다음 실행할 코드 주소, rsp, rax, rbx 등)      │
│                        │                               │
│                        ▼ (스택은 아래로 자람)          │
│                                                        │
│                        ▲ (위로 넘치면 커널 패닉)        │
│  uint32_t magic;       │ (스택 오버플로우 감지용 매직넘버)[cite: 1] │
├────────────────────────────────────────────────────────┤
│  struct thread (스레드 제어 블록 - TCB)                │
│   - tid_t tid                 : 스레드 ID              │
│   - enum thread_status status : THREAD_BLOCKED         │
│   - int64_t wakeup_tick       : 일어날 목표 시각       │
│   - struct list_elem elem     : 리스트 연결 고리       │
└────────────────────────────────────────────────────────┘
주소 낮음 (0xXXXX_0000)
</code></pre>
<blockquote>
<p><strong>핵심 원리</strong>:
잠든 스레드는 메모리에서 삭제되거나 사라진 것이 아닙니다.
CPU 레지스터 값(Context)을 커널 스택에 그대로 저장해 둔 채, 메모리 4KB 구역 안에서 <strong>동결(Freeze)</strong> 상태로 가만히 대기하고 있는 것입니다.</p>
</blockquote>
<hr>
<h3 id="2-논리적-큐-관점-os-스케줄러-시각">(2) 논리적 큐 관점 (OS 스케줄러 시각)</h3>
<p>스레드의 &quot;상태&quot;는 스레드가 <strong>운영체제의 어떤 연결 리스트에 걸려있는가</strong>로 결정됩니다.</p>
<pre><code class="language-text">[ 1. 기존 Busy Waiting 구조 ]
                 ┌── (CPU 실행) ──┐
                 ▼                │
            [ ready_list ] ───────┘[cite: 3]
            (스레드가 계속 준비 큐를 맴돌며 CPU를 낭비)


[ 2. 개선된 Sleep / Wakeup 구조 ]
  [ CPU ]
     ▲
     │ (스케줄러는 여기 있는 스레드만 실행시킴)
  [ ready_list ] (준비 큐)
     Thread B  ───►  Thread C
  ─────────────────────────────────────────────────────
  [ sleep_list ] (수면 큐 - 격리 구역)
     Thread A (깨어날 시간: 100) ───► Thread D (깨어날 시간: 250)
</code></pre>
<ul>
<li><strong>수면 큐(<code>sleep_list</code>)</strong>: 잠든 스레드들이 묶여있는 대기실입니다. 스케줄러는 이 큐를 전혀 쳐다보지 않으므로, 수면 큐에 있는 스레드는 CPU를 단 1cycle도 쓰지 않습니다.</li>
</ul>
<hr>
<h1 id="3-alarm-clock에서-스레드의-동작-흐름-a-to-z">3. Alarm Clock에서 스레드의 동작 흐름 (A to Z)</h1>
<p>스레드가 잠들고 깨어나는 전체 과정은 4단계의 상태 전이로 이루어집니다.</p>
<pre><code class="language-text">[THREAD_RUNNING] 
      │
      │ 1. timer_sleep() 호출 -&gt; sleep_list 삽입[cite: 3]
      ▼
[THREAD_BLOCKED] (기절 상태: RAM에 Context 박제)
      │
      │ 2. 10ms 주기 타이머 인터럽트 감지 (wakeup_tick 도달)[cite: 1, 3]
      ▼
[THREAD_READY] (ready_list 삽입)
      │
      │ 3. 스케줄러가 선택 -&gt; Context 복원
      ▼
[THREAD_RUNNING] (timer_sleep 바로 다음 줄부터 코드 재개)[cite: 3]
</code></pre>
<hr>
<h3 id="step-1-잠들기-thread-관점">Step 1. 잠들기 (Thread 관점)</h3>
<ol>
<li>스레드가 코드 실행 도중 <code>timer_sleep(50)</code>을 호출합니다.</li>
</ol>
<ol start="2">
<li>현재 하드웨어 틱(예: 100)에 50을 더해 깨어날 시각(<code>wakeup_tick = 150</code>)을 계산합니다.</li>
<li>리스트 조작 중 인터럽트로 인한 데이터 오염(Race Condition)을 막기 위해 인터럽트를 비활성화(<code>intr_disable()</code>)합니다.</li>
<li>스레드 TCB의 <code>wakeup_tick</code> 필드에 <code>150</code>을 기록합니다.</li>
<li>스레드를 <strong><code>sleep_list</code>에 삽입</strong>합니다.</li>
<li><code>thread_block()</code>을 호출합니다:</li>
</ol>
<ul>
<li>스레드의 상태를 <code>THREAD_BLOCKED</code>로 변경합니다.</li>
<li>CPU 레지스터 내용(Context)을 현재 스레드의 커널 스택에 저장합니다.</li>
</ul>
<ul>
<li><code>schedule()</code> 함수가 다른 실행 가능한 스레드를 골라 CPU를 넘깁니다.</li>
<li><strong>이 시점부터 해당 스레드는 실행이 완전히 정지(Freeze)됩니다.</strong></li>
</ul>
<hr>
<h3 id="step-2-시간의-경과-hardware-timer-관점">Step 2. 시간의 경과 (Hardware Timer 관점)</h3>
<ul>
<li>스레드는 자고 있으므로 스스로 시간을 재거나 코드를 실행할 수 없습니다.</li>
<li>컴퓨터 메인보드의 타이머 칩(PIT)이 주기적으로(Pintos 기준 1초에 100번, 즉 10ms마다) CPU에 전기적 펄스(하드웨어 인터럽트)를 보냅니다.</li>
</ul>
<ul>
<li>CPU는 하던 작업을 잠시 멈추고 타이머 인터럽트 핸들러(<code>timer_interrupt()</code>)를 강제로 실행합니다.</li>
</ul>
<ul>
<li>전역 변수 <code>ticks</code>가 1씩 증가합니다.</li>
</ul>
<hr>
<h3 id="step-3-깨우기-os-타이머-인터럽트-핸들러-관점">Step 3. 깨우기 (OS 타이머 인터럽트 핸들러 관점)</h3>
<ol>
<li>매 틱마다 <code>timer_interrupt()</code>가 호출되면 커널 함수(<code>thread_awake()</code>)를 실행합니다.</li>
</ol>
<ol start="2">
<li>커널이 <strong><code>sleep_list</code>를 순회</strong>하며 각 스레드의 <code>wakeup_tick</code>을 검사합니다.</li>
</ol>
<ol start="3">
<li><code>현재 ticks &gt;= 스레드의 wakeup_tick</code> 조건을 만족하는 스레드를 발견하면:</li>
</ol>
<ul>
<li>해당 스레드를 <code>sleep_list</code>에서 꺼냅니다.</li>
<li><code>thread_unblock()</code>을 호출하여 스레드 상태를 <code>THREAD_READY</code>로 변경합니다.</li>
<li>스레드를 <strong><code>ready_list</code>에 삽입</strong>합니다.</li>
</ul>
<hr>
<h3 id="step-4-실행-재개-scheduler-관점">Step 4. 실행 재개 (Scheduler 관점)</h3>
<ol>
<li>스케줄러가 <code>ready_list</code>에서 대기 중이던 해당 스레드를 선택합니다.</li>
<li><code>switch_threads()</code> 어셈블리 함수가 스레드의 커널 스택에 저장되어 있던 레지스터 값(Context)을 CPU로 다시 복원합니다.</li>
<li>스레드는 자신이 잠들었던 지점(<code>thread_block()</code> 내부의 문맥 전환 직후)으로 정확히 복귀하여, <code>timer_sleep()</code> 함수를 빠져나와 다음 코드를 정상 실행합니다.</li>
</ol>
<hr>
<h1 id="4-전체-시스템-통합-구조도">4. 전체 시스템 통합 구조도</h1>
<pre><code class="language-text">[ Hardware ]
   ┌──────────────────────────────────────────────┐
   │ 하드웨어 타이머 (PIT) - 매 10ms마다 펄스 발생[cite: 1] │
   └──────────────────────┬───────────────────────┘
                          │ (Hardware Interrupt)
                          ▼
[ OS Kernel Space ]
   ┌────────────────────────────────────────────────────────────────────────┐
   │ timer_interrupt() 실행 (ticks++)[cite: 1, 3]                                  │
   │   │                                                                    │
   │   ▼                                                                    │
   │ thread_awake(ticks) 호출: sleep_list 검사[cite: 1]                          │
   └───────┬────────────────────────────────────────────────────────────────┘
           │
           │ (깨울 시간이 된 스레드 추출: wakeup_tick &lt;= ticks)
           ▼
   ┌─────────────────┐       thread_unblock()        ┌─────────────────┐
   │   sleep_list    │ ────────────────────────────► │   ready_list    │
   │ (THREAD_BLOCKED)│                               │ (THREAD_READY)  │
   └─────────────────┘                               └────────┬────────┘
           ▲                                                  │
           │                                                  │ schedule()
           │ timer_sleep() 호출[cite: 3]                              │ CPU 할당
           │ thread_block()                                   ▼
   ┌───────┴─────────┐                               ┌─────────────────┐
   │ Current Thread  │ ◄──────────────────────────── │      CPU        │
   │ (THREAD_RUNNING)│         실행권 획득           │(스레드 코드 실행)│
   └─────────────────┘                               └─────────────────┘
</code></pre>
<hr>
<h1 id="5-3년-뒤-기억-소환-체크포인트-질문-방어용-faq">5. 3년 뒤 기억 소환 체크포인트 (질문 방어용 FAQ)</h1>
<ul>
<li><strong>Q1. 잠든 스레드가 자력으로 일어나는가?</strong></li>
<li><strong>A:</strong> 절대 아닙니다. 스레드는 <code>THREAD_BLOCKED</code> 상태에서 CPU를 쓰지 못하므로 죽어있는 것과 같습니다. 스레드를 깨우는 것은 외부 하드웨어 타이머 인터럽트를 감지한 운영체제(OS)입니다.</li>
</ul>
<ul>
<li><strong>Q2. 스레드는 수면 중에 어디에 존재하는가?</strong></li>
<li><strong>A:</strong> 물리적으로는 <strong>RAM의 4KB 커널 페이지</strong> 안에 실행 문맥(레지스터)을 스택에 보존한 채 머물러 있고, 논리적으로는 준비 큐에서 빠져나와 <code>sleep_list</code>에 연결되어 있습니다.</li>
</ul>
<ul>
<li><strong>Q3. <code>sleep_list</code>는 어떻게 관리해야 성능상 가장 유리한가?</strong></li>
<li><strong>A:</strong></li>
</ul>
<ol>
<li><strong>정렬 삽입</strong>: <code>sleep_list</code>에 넣을 때 <code>wakeup_tick</code> 오름차순으로 정렬해 두면, 인터럽트 핸들러가 맨 앞의 원소만 확인하고 즉시 루프를 탈출할 수 있어 $O(1)$의 기상 검사가 가능합니다.</li>
<li><strong>최소 틱 추적</strong>: 전역 변수 <code>next_tick_to_awake</code>를 두어 현재 잠든 스레드 중 가장 빠른 기상 시각을 기록해 두면, 그 시각이 되기 전까지는 <code>sleep_list</code>를 탐색조차 하지 않고 스킵할 수 있습니다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[핀토스/환경구축/Git] Docker DevContainer 기반 핀토스(Pintos) 개발 환경 구축과 Project 1 스레드 채점 명령어 및 Git 4단계 영역 구조도 총정리]]></title>
            <link>https://velog.io/@jae_yun/%ED%95%80%ED%86%A0%EC%8A%A4git-pintos-project-1-%EC%8A%A4%EB%A0%88%EB%93%9Cthreads-%EA%B0%9C%EB%B0%9C%EC%B1%84%EC%A0%90-%EB%AA%85%EB%A0%B9%EC%96%B4-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%99%80-git-4%EB%8B%A8%EA%B3%84-%EC%98%81%EC%97%ADworking-treeindexloc</link>
            <guid>https://velog.io/@jae_yun/%ED%95%80%ED%86%A0%EC%8A%A4git-pintos-project-1-%EC%8A%A4%EB%A0%88%EB%93%9Cthreads-%EA%B0%9C%EB%B0%9C%EC%B1%84%EC%A0%90-%EB%AA%85%EB%A0%B9%EC%96%B4-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%99%80-git-4%EB%8B%A8%EA%B3%84-%EC%98%81%EC%97%ADworking-treeindexloc</guid>
            <pubDate>Thu, 08 Oct 2026 13:22:50 GMT</pubDate>
            <description><![CDATA[<h2 id="소주제">소주제</h2>
<ol>
<li>Docker 및 VSCode DevContainer 기반 핀토스(Pintos) x86-64 가상화 개발 환경 구축 원리</li>
<li>템플릿 리포지토리 연결 해제 및 독립된 신규 Git 원격 저장소 재초기화(git init) 메커니즘</li>
<li>핀토스 커널 개발 아키텍처: C 소스코드 컴파일, QEMU 가상머신 적재 및 채점 파이프라인 구조도</li>
<li>Git 4단계 저장 영역(Working Directory, Staging Area, Local Repository, Remote Repository)과 GitHub 아키텍처 구조도</li>
<li>일과 중 수십 번 반복 실행하는 핵심 개발 명령어: 디렉토리 이동, 빌드, 단일 테스트 실행 및 스냅샷 저장</li>
<li>특정 상황 및 팀 협업 시 사용하는 필수 관리 명령어: 빌드 캐시 초기화, 브랜치 분기, 원격 동기화, 임시 보관 및 복구</li>
<li>커널 패닉 및 버그 추적을 위한 디버깅 인프라: GDB 커널 원격 디버깅과 채점 파일(.output, .result) 분석법</li>
<li>빌드 부산물 격리를 위한 .gitignore 설정 원리와 팀 프로젝트 코드 충돌(Conflict) 해결 메커니즘</li>
<li>Pintos Project 1 스레드(Threads) 과제 구현 1사이클 표준 워크플로</li>
</ol>
<hr>
<h2 id="1-docker-및-vscode-devcontainer-기반-핀토스pintos-x86-64-가상화-개발-환경-구축-원리">1. Docker 및 VSCode DevContainer 기반 핀토스(Pintos) x86-64 가상화 개발 환경 구축 원리</h2>
<p>핀토스(Pintos)는 하드웨어를 직접 제어하는 운영체제 커널이므로, 컴파일러(GCC) 버전, C 표준 라이브러리 의존성, 에뮬레이터(QEMU) 환경이 호스트 운영체제(Windows, macOS, Linux 배포판)마다 다르면 컴파일 실패나 런타임 오류가 발생합니다.</p>
<p>크래프톤 정글 핀토스 과정(KAIST Pintos 기반)은 오리지널 32비트 Pintos와 달리 <strong>64비트 x86-64 아키텍처</strong>를 채택하고 있으며, 호스트 환경의 파편화를 방지하기 위해 <strong>Docker</strong>와 <strong>VSCode DevContainer</strong>를 결합한 표준화된 리눅스 컨테이너 개발 환경(<code>ubuntu:22.04</code> 기반)을 사용합니다.</p>
<h3 id="11-docker-컨테이너-가상화와-하이퍼바이저-기반-가상머신vm의-하드웨어-차이점">1.1 Docker 컨테이너 가상화와 하이퍼바이저 기반 가상머신(VM)의 하드웨어 차이점</h3>
<table>
<thead>
<tr>
<th align="left">비교 항목</th>
<th align="left">하이퍼바이저 기반 가상머신 (EC2, VirtualBox)</th>
<th align="left">Docker 컨테이너 (Container)</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>가상화 계층</strong></td>
<td align="left">하이퍼바이저가 가상 하드웨어를 생성하고 독립된 게스트 OS 커널 전체를 구동</td>
<td align="left">호스트 OS 커널을 직접 공유하며 리눅스 네임스페이스와 cgroups로 프로세스 격리</td>
</tr>
<tr>
<td align="left"><strong>실행 단위</strong></td>
<td align="left">운영체제 커널 전체 (Guest OS + Application)</td>
<td align="left">애플리케이션 실행 프로세스 단위 격리</td>
</tr>
<tr>
<td align="left"><strong>기동 속도</strong></td>
<td align="left">커널 부팅 프로세스를 거치므로 수십 초 이상 소요</td>
<td align="left">호스트 커널이 이미 기동되어 있으므로 밀리초(ms) 단위 즉시 실행</td>
</tr>
<tr>
<td align="left"><strong>자원 점유</strong></td>
<td align="left">가상 메모리 및 CPU 코어를 정적으로 선점하여 오버헤드가 큼</td>
<td align="left">필요한 메모리와 CPU만 동적으로 소비하여 오버헤드가 극히 적음</td>
</tr>
</tbody></table>
<h3 id="12-docker-핵심-3대-구성요소의-기술적-정의">1.2 Docker 핵심 3대 구성요소의 기술적 정의</h3>
<ol>
<li><strong>Docker Engine</strong>: 호스트 운영체제 백그라운드에서 데몬 형태로 동작하며, 리눅스 커널의 프로세스 격리 기술(Namespace, Control Groups)을 제어하여 컨테이너 생성과 라이프사이클을 총괄하는 핵심 런타임 서비스입니다.</li>
<li><strong>Docker Image</strong>: 파일 시스템 스냅샷의 불변 읽기 전용 템플릿입니다. OS 파일 시스템 구조(<code>ubuntu:22.04</code>), 빌드 도구(<code>gcc</code>, <code>make</code>), 에뮬레이터(<code>qemu</code>), 디버거(<code>gdb</code>)가 레이어(Layer) 형태로 패키징되어 있습니다.</li>
<li><strong>Docker Container</strong>: Docker 이미지를 기반으로 읽기/쓰기 레이어를 추가하여 실제 메모리에 인스턴스화된 독립 격리 실행 프로세스입니다.</li>
</ol>
<h3 id="13-vscode-devcontainer-연동-구조와-디렉토리-레이아웃">1.3 VSCode DevContainer 연동 구조와 디렉토리 레이아웃</h3>
<p>VSCode DevContainer는 호스트 머신의 VSCode 에디터가 컨테이너 내부의 개발 서버 프로세스와 IPC(프로세스 간 통신) 채널을 연결하여, 컨테이너 내부의 파일 시스템과 컴파일러를 로컬 에디터처럼 원격 제어할 수 있게 해 주는 기능입니다.</p>
<pre><code class="language-text">pintos_22.04_lab_docker/
├── .devcontainer/
│   ├── devcontainer.json      # VSCode 컨테이너 확장 설정, 터미널 자동 실행 스크립트 지정
│   └── Dockerfile             # 64비트 ubuntu:22.04 기반 핀토스 빌드 도구 설치 스크립트
├── pintos/
│   ├── threads/               # Project 1: 스레드 과제 소스코드
│   ├── userprog/              # Project 2: 유저 프로그램 및 시스템 콜 과제 소스코드
│   └── vm/                    # Project 3: 가상 메모리 과제 소스코드
└── README.md</code></pre>
<h3 id="14-환경-구축-절차">1.4 환경 구축 절차</h3>
<h4 id="1-docker-desktop-설치-및-백그라운드-엔진-실행">1) Docker Desktop 설치 및 백그라운드 엔진 실행</h4>
<ul>
<li>Docker 공식 사이트에서 Windows 또는 macOS용 Docker Desktop을 설치하고 백그라운드 서비스를 실행합니다.</li>
<li>Windows 작업 표시줄 트레이 또는 macOS 상단 메뉴바에 Docker 고래 아이콘이 정상 상태(Running)로 유지되어야 합니다.</li>
</ul>
<h4 id="2-프로젝트-저장소-얕은-복제shallow-clone">2) 프로젝트 저장소 얕은 복제(Shallow Clone)</h4>
<p>호스트 터미널(PowerShell, CMD, Terminal)에서 과거 불필요한 Git 커밋 히스토리를 제외하고 최신 파일 스냅샷만 신속하게 내려받기 위해 <code>--depth=1</code> 옵션을 지정하여 복제합니다.</p>
<pre><code class="language-bash">git clone --depth=1 https://github.com/krafton-jungle/pintos_22.04_lab_docker.git</code></pre>
<ul>
<li><code>--depth=1</code>: 깃 히스토리 그래프 전체를 내려받지 않고 최신 커밋 1단계의 데이터만 가져와 네트워크 대역폭과 디스크 용량을 절약합니다.</li>
</ul>
<h4 id="3-vscode에서-폴더-열기-및-컨테이너-내부-재기동">3) VSCode에서 폴더 열기 및 컨테이너 내부 재기동</h4>
<ol>
<li>VSCode를 실행하고 <code>파일 -&gt; 폴더 열기</code> 메뉴를 통해 복제한 <code>pintos_22.04_lab_docker</code> 폴더를 엽니다.</li>
<li><code>Ctrl + Shift + P</code> (macOS는 <code>Cmd + Shift + P</code>) 단축키를 눌러 명령어 팔레트를 호출합니다.</li>
<li><code>Dev Containers: Reopen in Container</code> 명령을 선택합니다.</li>
<li>VSCode가 <code>devcontainer.json</code>과 <code>Dockerfile</code>을 분석하여 Docker 이미지를 빌드하고 컨테이너를 구동한 뒤 컨테이너 내부 환경으로 전환합니다 (최초 실행 시 빌드에 수 분 소요).</li>
<li>컨테이너 내부에서 VSCode 통합 터미널을 열면 <code>.devcontainer/devcontainer.json</code> 설정에 따라 환경 변수 활성화 스크립트인 <code>source /workspaces/pintos_22.04_lab_docker/pintos/activate</code>가 자동으로 실행되어 핀토스 실행 경로(<code>PATH</code>)가 세팅됩니다.</li>
</ol>
<blockquote>
<p>[!NOTE]
핀토스 컨테이너 환경은 GUI 기반의 VSCode 단축키 디버깅(F5)을 지원하지 않으므로, 커널 디버깅은 본문 7장에서 다루는 CLI 기반 <code>pintos-gdb</code>를 사용해야 합니다.</p>
</blockquote>
<hr>
<h2 id="2-템플릿-리포지토리-연결-해제-및-독립된-신규-git-원격-저장소-재초기화git-init-메커니즘">2. 템플릿 리포지토리 연결 해제 및 독립된 신규 Git 원격 저장소 재초기화(git init) 메커니즘</h2>
<p>배포받은 <code>pintos_22.04_lab_docker</code>는 공개 템플릿 저장소와 연결되어 있으므로, 과제 코드를 커밋하고 푸시하려면 기존 Git 연결을 완전히 끊어내고 팀 또는 개인의 신규 GitHub 원격 저장소로 재초기화해야 합니다.</p>
<pre><code class="language-text">[기존 템플릿 연결 상태]
  pintos_22.04_lab_docker/
    └── .git/ (템플릿 저장소 메타데이터) ──▶ https://github.com/krafton-jungle/... (푸시 권한 없음)

[1단계: 기존 .git 디렉토리 영구 삭제]
  $ rm -rf .git
  (과거 커밋 로그, 원격 브랜치 참조, 설정 파일이 완전히 파괴되어 일반 파일들만 남음)

[2단계: 신규 로컬 저장소 초기화 및 새 원격 저장소 연결]
  $ git init
  $ git remote add origin https://github.com/내계정/내-새-저장소.git
  $ git add .
  $ git commit -m &quot;feat: 핀토스 개발 환경 초기 커밋&quot;
  $ git push -u origin main</code></pre>
<h3 id="21-저장소-초기화-명령어-실행-흐름">2.1 저장소 초기화 명령어 실행 흐름</h3>
<pre><code class="language-bash"># 1. 기존 Git 메타데이터 디렉토리 강제 삭제
rm -rf .git

# 2. 현재 디렉토리를 빈 Git 로컬 저장소로 초기화
git init

# 3. 팀 또는 개인의 신규 GitHub 저장소를 origin 원격 저장소로 등록
git remote add origin https://github.com/내계정/내-새-저장소.git

# 4. 전체 프로젝트 파일을 스테이징 영역에 등록
git add .

# 5. 베이스라인 최초 커밋 생성
git commit -m &quot;feat: 핀토스 개발 환경 초기 커밋&quot;

# 6. 기본 브랜치 이름을 main으로 확인하고 원격 저장소로 업로드 (-u: 업스트림 추적 설정)
git branch -M main
git push -u origin main</code></pre>
<h3 id="22-각-단계의-내부-동작-원리">2.2 각 단계의 내부 동작 원리</h3>
<ul>
<li><code>rm -rf .git</code>: Git의 모든 이력, 원격 저장소 URL 정보, 인덱스 파일은 루트 디렉토리의 <code>.git</code> 하위 폴더에만 존재합니다. 이 디렉토리를 삭제하면 파일 시스템의 소스 파일은 보존된 채 템플릿 리포지토리와의 연결이 즉시 단절됩니다.</li>
<li><code>git init</code>: 빈 <code>.git</code> 디렉토리를 생성하고 <code>objects</code>, <code>refs</code> 디렉토리와 기본 <code>config</code>, <code>HEAD</code> 파일을 생성합니다.</li>
<li><code>git remote add origin &lt;URL&gt;</code>: 로컬 저장소의 <code>.git/config</code> 파일에 <code>[remote &quot;origin&quot;]</code> 섹션을 생성하여 원격 네트워크 주소를 등록합니다.</li>
<li><code>git push -u origin main</code>: 로컬 <code>main</code> 브랜치의 커밋 객체들을 원격 저장소로 전송하고, 로컬 <code>main</code> 브랜치가 원격의 <code>origin/main</code>을 지속적으로 추적하도록 기준점을 바인딩합니다.</li>
</ul>
<hr>
<h2 id="3-핀토스-커널-개발-아키텍처-c-소스코드-컴파일-qemu-가상머신-적재-및-채점-파이프라인-구조도">3. 핀토스 커널 개발 아키텍처: C 소스코드 컴파일, QEMU 가상머신 적재 및 채점 파이프라인 구조도</h2>
<p>핀토스는 x86 하드웨어를 직접 제어하는 운영체제 커널입니다. 일반 응용 프로그램과 달리 호스트 컴퓨터의 실제 물리 CPU에서 미완성 커널을 직접 부팅하면 운영체제 충돌로 하드웨어가 정지합니다. 따라서 호스트 OS 위에 가상 CPU, 4MB 가상 물리 메모리(RAM), 프로그래머블 인터럽트 컨트롤러(PIC), 시리얼 포트를 에뮬레이션하는 <strong>QEMU 가상머신</strong>을 구동하고 그 위에서 핀토스 커널을 실행합니다.</p>
<h3 id="31-핀토스-빌드-및-테스트-자동화-파이프라인-구조도">3.1 핀토스 빌드 및 테스트 자동화 파이프라인 구조도</h3>
<pre><code class="language-text">[개발자 소스코드 수정]
  │  threads/thread.c, devices/timer.c 등 C 소스코드 및 헤더파일
  ▼
[GCC 크로스 컴파일러 &amp; 링커: make 실행]
  │  1. gcc: C 텍스트 코드를 x86-64 목적 파일(*.o)로 컴파일
  │  2. ld: 목적 파일들과 런타임 라이브러리를 결합(Link)
  │  3. 결과물: 커널 바이너리(kernel.bin) 및 부트로더(loader.bin) 생성
  ▼
[QEMU 가상머신 구동: pintos 스크립트 실행]
  │  1. 가상 CPU, 가상 물리 RAM, 가상 타이머 하드웨어 초기화
  │  2. 가상 디스크에 loader.bin과 kernel.bin 적재 후 부팅 시작
  │  3. 지정된 스레드 테스트 함수(예: test_alarm_multiple) 호출
  ▼
[출력 가로채기: 가상 시리얼 포트(UART)]
  │  커널 내부의 printf() 호출 결과가 QEMU 가상 시리얼 포트(COM1)로 전송됨
  │  핀토스 런처가 시리얼 포트 바이트 스트림을 터미널 콘솔 및 *.output 파일로 기록
  ▼
[Perl 채점기 비교 검증: *.ck 스크립트]
  │  채점 프로그램(tests/threads/&lt;테스트명&gt;.ck)이 정규표현식으로 *.output 내용 분석
  │  - 스레드 깨어난 순서와 틱(tick) 오차 범위 검증
  │  - 일치 시: tests/threads/&lt;테스트명&gt;.result 파일에 &quot;PASS&quot; 기록
  │  - 불일치 시: tests/threads/&lt;테스트명&gt;.result 파일에 &quot;FAIL&quot; 및 실패 원인 기록</code></pre>
<hr>
<h2 id="4-git-4단계-저장-영역working-directory-staging-area-local-repository-remote-repository과-github-아키텍처-구조도">4. Git 4단계 저장 영역(Working Directory, Staging Area, Local Repository, Remote Repository)과 GitHub 아키텍처 구조도</h2>
<p>Git은 파일 시스템의 변경 사항을 4개의 논리적 및 물리적 영역으로 분리하여 통제합니다.</p>
<h3 id="41-git-4단계-내부-저장-영역과-데이터-흐름-구조도">4.1 Git 4단계 내부 저장 영역과 데이터 흐름 구조도</h3>
<pre><code class="language-text">┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                       로컬 컴퓨터 (Local PC)                                     │
│                                                                                                 │
│  [1. Working Directory]    [2. Staging Area (Index)]      [3. Local Repository (.git)]          │
│   (작업 디렉토리)           (스테이징 영역)                 (로컬 저장소)                          │
│                                                                                                 │
│   실제 편집 중인 파일        다음 커밋에 기록될 파일 목록      영구 보관된 커밋 히스토리(HEAD)          │
│   (thread.c, timer.c)      (.git/index 바이너리 파일)     (.git/objects 객체 데이터베이스)         │
│            │                           │                                  │                     │
│            │─── git add &lt;파일&gt; ───────▶│                                  │                     │
│            │                           │─── git commit -m ───────────────▶│                     │
│            │◀── git restore &lt;파일&gt; ────│                                  │                     │
│            │◀── git restore --source=HEAD &lt;파일&gt; ─────────────────────────│                     │
│            │                                                              │                     │
│            │─── git diff (작업 디렉토리 vs 스테이징 영역 차이 비교)            │                     │
│            │─── git diff --staged (스테이징 영역 vs 최신 커밋 HEAD 차이 비교)  │                     │
└────────────┼──────────────────────────────────────────────────────────────┼─────────────────────┘
             │                                                              │
             │                   git push origin &lt;브랜치명&gt;                  ▼
             │               ────────────────────────────────────────▶ ┌──────────────────────────┐
             │                                                         │  [4. Remote Repository]  │
             │                   git pull origin &lt;브랜치명&gt;             │    (GitHub 원격 저장소)   │
             │               ◀──────────────────────────────────────── │                          │
             │                  (git fetch + git merge 자동 수행)        │  팀원 공유 원격 브랜치   │
             └─────────────────────────────────────────────────────────┴──────────────────────────┘</code></pre>
<h3 id="42-각-저장-영역의-기술적-정의">4.2 각 저장 영역의 기술적 정의</h3>
<ol>
<li><strong>Working Directory (작업 디렉토리)</strong>: 개발자가 에디터로 직접 수정하는 파일 시스템 상의 실제 디렉토리 및 파일입니다.</li>
<li><strong>Staging Area / Index (스테이징 영역)</strong>: 다음 커밋 스냅샷에 포함할 파일들의 메타데이터와 파일 해시 목록을 보관하는 단일 바이너리 파일(<code>.git/index</code>)입니다.</li>
<li><strong>Local Repository (로컬 저장소)</strong>: <code>git commit</code> 실행 시 생성되는 스냅샷 객체(Tree, Commit, Blob)가 고유 SHA-1 해시 키로 저장되는 데이터베이스(<code>.git/objects/</code>)입니다.</li>
<li><strong>Remote Repository (원격 저장소 / GitHub)</strong>: 네트워크를 통해 팀원들과 변경 이력을 공유하는 원격 Git 서버의 중앙 저장소입니다.</li>
</ol>
<h3 id="43-github-협업-아키텍처-구조도-branch-remote-pull-request">4.3 GitHub 협업 아키텍처 구조도 (Branch, Remote, Pull Request)</h3>
<pre><code class="language-text">[GitHub 원격 중앙 저장소 (Remote: origin)]
  │
  ├─▶ main / dev 브랜치 (안정화된 기준 코드 베이스)
  │      ▲
  │      │ Pull Request (코드 리뷰 후 병합 / Merge)
  │      │
  ├─▶ feat/alarm-clock 브랜치 (팀원 A 작업 브랜치)
  └─▶ feat/priority-scheduling 브랜치 (팀원 B 작업 브랜치)
         ▲
         │ git push origin feat/priority-scheduling (네트워크 커밋 전송)
         │ git pull origin dev (최신 공용 커밋 수신 및 병합)
         │
[로컬 컴퓨터 (Local PC)]
  └─ Working Tree -&gt; Index -&gt; Local Repository (.git)</code></pre>
<hr>
<h2 id="5-일과-중-수십-번-반복-실행하는-핵심-개발-명령어-디렉토리-이동-빌드-단일-테스트-실행-및-스냅샷-저장">5. 일과 중 수십 번 반복 실행하는 핵심 개발 명령어: 디렉토리 이동, 빌드, 단일 테스트 실행 및 스냅샷 저장</h2>
<p>핀토스 과제를 진행하는 동안 &quot;코드 수정 -&gt; 컴파일 -&gt; 가상머신 단일 테스트 실행 -&gt; 상태 확인 -&gt; Git 스냅샷 저장&quot;의 루프를 하루에도 수십 번 실행하게 됩니다.</p>
<h3 id="51-핀토스-실행-및-빌드-명령어">5.1 핀토스 실행 및 빌드 명령어</h3>
<h4 id="1-cd-srcthreadsbuild-또는-cd-threadsbuild">1) <code>cd src/threads/build</code> (또는 <code>cd threads/build</code>)</h4>
<ul>
<li><strong>동작 방식</strong>: 핀토스 커널의 빌드 및 실행 전용 하위 디렉토리로 작업 경로를 이동합니다.</li>
<li><strong>물리적 이유</strong>: 핀토스는 소스코드 디렉토리가 컴파일 부산물(<code>*.o</code>, <code>*.d</code>, <code>kernel.bin</code>, <code>*.output</code>)로 뒤섞이는 것을 차단하기 위해, 모든 컴파일 및 QEMU 실행을 <code>build</code> 디렉토리 내부에서만 수행하도록 <code>Makefile</code>이 설계되어 있습니다. 이 경로를 벗어나면 타깃 규칙을 찾지 못해 빌드가 실패합니다.</li>
</ul>
<h4 id="2-make">2) <code>make</code></h4>
<ul>
<li><strong>동작 방식</strong>: 수정된 C 파일의 수정 시간(mtime)을 감지하여 변경된 파일만 GCC 컴파일러로 재컴파일하고 커널 이미지(<code>kernel.bin</code>)를 다시 링크합니다.</li>
<li><strong>동작 원리</strong>: C 코드는 CPU가 직접 실행할 수 없는 텍스트입니다. <code>make</code> 명령은 <code>Makefile</code>에 기술된 의존성 트리를 기반으로, 소스 파일(<code>thread.c</code>)의 타임스탬프가 목적 파일(<code>thread.o</code>)보다 새로운 경우에만 <code>gcc -c thread.c</code>를 실행하고 링커(<code>ld</code>)를 통해 실행 가능한 단일 커널 바이너리(<code>kernel.bin</code>)를 생성합니다.</li>
</ul>
<h4 id="3-pintos----run-테스트이름-예-pintos----run-alarm-multiple">3) <code>pintos -- run &lt;테스트이름&gt;</code> (예: <code>pintos -- run alarm-multiple</code>)</h4>
<ul>
<li><strong>동작 방식</strong>: QEMU 가상머신을 기동하여 가상 디스크에 <code>kernel.bin</code>을 적재하고, 지정된 단일 테스트 함수를 실행하여 콘솔 로그를 터미널로 실시간 스트리밍합니다.</li>
<li><strong>상세 원리</strong>:<ul>
<li><code>pintos</code> 스크립트는 내부적으로 <code>qemu-system-x86_64</code> (또는 i386) 에뮬레이터 프로세스를 호출합니다.</li>
<li>가상 메모리와 가상 타이머 하드웨어를 초기화한 뒤 커널 main 함수를 실행하고 테스트 파라미터를 넘깁니다.</li>
<li>전체 테스트를 돌리지 않고 현재 수정 중인 단일 기능만 1~2초 내에 고속 검증할 때 사용합니다.</li>
</ul>
</li>
</ul>
<h4 id="4-make-check">4) <code>make check</code></h4>
<ul>
<li><strong>동작 방식</strong>: 스레드 과제에 등록된 27개 전체 단위 테스트를 순차적으로 자동 실행하고 종합 채점 결과 리포트를 터미널에 렌더링합니다.</li>
<li><strong>상세 원리</strong>:<ul>
<li><code>alarm-single</code>, <code>alarm-multiple</code>, <code>priority-preempt</code>, <code>priority-donate-one</code>, <code>mlfqs-load-1</code> 등 전체 테스트 목록에 대해 각각 QEMU 가상머신을 실행합니다.</li>
<li>각 테스트의 출력 로그(<code>*.output</code>)를 채점 기준 스크립트(<code>*.ck</code>)와 비교하여 각각 <code>PASS</code> 또는 <code>FAIL</code> 여부를 집계합니다.</li>
<li>전체 회귀 테스트(Regression Test)용이므로 수 분의 시간이 소요됩니다.</li>
</ul>
</li>
</ul>
<h3 id="52-git-상태-확인-및-스냅샷-저장-명령어">5.2 Git 상태 확인 및 스냅샷 저장 명령어</h3>
<h4 id="5-git-status">5) <code>git status</code></h4>
<ul>
<li><strong>동작 방식</strong>: 작업 디렉토리, 스테이징 영역, 로컬 저장소 최신 커밋(HEAD) 간의 파일 변경 상태를 비교하여 출력합니다.</li>
<li><strong>상세 원리</strong>:<ul>
<li>Git은 디렉토리 내 각 파일의 수정 시간(mtime)과 크기를 <code>.git/index</code>에 캐시된 정보와 대조합니다.</li>
<li>수정되었으나 스테이징되지 않은 파일은 빨간색 <code>modified</code>, <code>git add</code>된 파일은 초록색 <code>Changes to be committed</code>, 추적되지 않는 새 파일은 <code>Untracked files</code>로 분류합니다.</li>
</ul>
</li>
</ul>
<h4 id="6-git-diff">6) <code>git diff</code></h4>
<ul>
<li><strong>동작 방식</strong>: 작업 디렉토리에서 수정한 내용과 스테이징 영역(Index) 간의 줄 단위 텍스트 차이를 비교 출력합니다.</li>
<li><strong>상세 원리</strong>:<ul>
<li>삭제된 라인은 빨간색 <code>-</code>, 추가된 라인은 초록색 <code>+</code>로 표기합니다.</li>
<li>실수로 코드를 잘못 삭제했거나 디버깅용 <code>printf</code>가 남아있는지 커밋 전 라인 단위로 점검할 때 사용합니다.</li>
<li>이미 <code>git add</code>된 변경 사항을 비교할 때는 <code>git diff --staged</code>를 사용합니다.</li>
</ul>
</li>
</ul>
<h4 id="7-git-add-파일명-또는-git-add-">7) <code>git add &lt;파일명&gt;</code> (또는 <code>git add .</code>)</h4>
<ul>
<li><strong>동작 방식</strong>: 작업 디렉토리의 파일 내용을 읽어 SHA-1 해시 기반 압축 Blob 객체로 변환하여 <code>.git/objects/</code>에 기록하고, 해당 경로와 해시를 <code>.git/index</code>에 등록합니다.</li>
<li><strong>상세 원리</strong>: 점(<code>.</code>)을 지정하면 <code>.gitignore</code> 대상을 제외한 현재 디렉토리 이하의 모든 변경 파일이 일괄 스테이징 영역에 반영됩니다.</li>
</ul>
<h4 id="8-git-commit--m-커밋-메시지">8) <code>git commit -m &quot;커밋 메시지&quot;</code></h4>
<ul>
<li><strong>동작 방식</strong>: 스테이징 영역의 파일 트리 상태를 영구적인 Commit 객체로 패키징하여 로컬 저장소에 불변 스냅샷 객체로 영구 기록합니다.</li>
<li><strong>상세 원리</strong>: 새로운 40자리 고유 SHA-1 해시가 부여되며, 현재 브랜치 참조 포인터와 <code>HEAD</code> 포인터가 새 커밋을 가리키도록 이동합니다.</li>
</ul>
<hr>
<h2 id="6-특정-상황-및-팀-협업-시-사용하는-필수-관리-명령어-빌드-캐시-초기화-브랜치-분기-원격-동기화-임시-보관-및-복구">6. 특정 상황 및 팀 협업 시 사용하는 필수 관리 명령어: 빌드 캐시 초기화, 브랜치 분기, 원격 동기화, 임시 보관 및 복구</h2>
<h3 id="61-핀토스-유지보수-명령어">6.1 핀토스 유지보수 명령어</h3>
<h4 id="1-make-clean">1) <code>make clean</code></h4>
<ul>
<li><strong>동작 방식</strong>: <code>build</code> 디렉토리 내부에 생성된 모든 컴파일 목적 파일(<code>*.o</code>), 디펜던시 파일(<code>*.d</code>), 커널 이미지(<code>kernel.bin</code>), 테스트 출력 결과물(<code>*.output</code>, <code>*.result</code>)을 파일 시스템에서 일괄 삭제합니다.</li>
<li><strong>사용 시점</strong>: C 헤더 파일(<code>thread.h</code>)의 구조체 멤버 크기나 오프셋을 수정했으나 의존성 검사 누락으로 일부 소스가 재컴파일되지 않아 바이너리 불일치(ABI mismatch) 버그가 발생할 때 전체 클린 빌드를 수행하기 위해 사용합니다.</li>
</ul>
<h4 id="2-make-teststhreads테스트이름result-예-make-teststhreadsalarm-multipleresult">2) <code>make tests/threads/&lt;테스트이름&gt;.result</code> (예: <code>make tests/threads/alarm-multiple.result</code>)</h4>
<ul>
<li><strong>동작 방식</strong>: 지정한 단일 테스트의 결과 파일(<code>.result</code>) 하나만을 대상으로 Makefile 타깃 규칙을 실행하여 채점 결과를 갱신합니다.</li>
<li><strong>상세 원리</strong>: <code>pintos -- run</code>은 콘솔 화면에 커널 로그를 출력하는 반면, 이 명령어는 백그라운드에서 QEMU를 구동하여 <code>.output</code>을 생성하고 Perl 채점기(<code>.ck</code>)까지 자동 실행하여 PASS/FAIL 결과를 <code>.result</code> 파일에 기록한 뒤 결과 한 줄만 화면에 표시합니다.</li>
</ul>
<h3 id="62-git-브랜치-협업-및-복구-명령어">6.2 Git 브랜치, 협업 및 복구 명령어</h3>
<h4 id="3-git-checkout--b-새브랜치명-또는-git-switch--c-새브랜치명">3) <code>git checkout -b &lt;새브랜치명&gt;</code> (또는 <code>git switch -c &lt;새브랜치명&gt;</code>)</h4>
<ul>
<li><strong>동작 방식</strong>: 현재 커밋 위치(HEAD)를 기반으로 새로운 브랜치 참조 파일(<code>.git/refs/heads/&lt;새브랜치명&gt;</code>)을 생성하고 작업 브랜치를 즉시 전환합니다.</li>
<li><strong>물리적 원리</strong>: Git에서 브랜치는 특정 커밋 해시를 담은 41바이트 텍스트 포인터 파일에 불과하므로 분기 연산이 즉각 완료됩니다.</li>
</ul>
<h4 id="4-git-push-origin-브랜치명">4) <code>git push origin &lt;브랜치명&gt;</code></h4>
<ul>
<li><strong>동작 방식</strong>: 로컬 저장소에 누적된 새 커밋 객체들과 데이터들을 네트워크를 통해 GitHub 원격 저장소(<code>origin</code>)로 전송하고 원격 브랜치 참조를 갱신합니다.</li>
</ul>
<h4 id="5-git-pull-origin-공용브랜치명-예-git-pull-origin-dev">5) <code>git pull origin &lt;공용브랜치명&gt;</code> (예: <code>git pull origin dev</code>)</h4>
<ul>
<li><strong>동작 방식</strong>: GitHub 원격 저장소의 최신 커밋 이력을 내려받는 <code>git fetch</code>와, 이를 로컬 브랜치에 병합하는 <code>git merge</code>를 연속으로 실행합니다.</li>
</ul>
<h4 id="6-git-restore-파일명-작업-취소-및-롤백">6) <code>git restore &lt;파일명&gt;</code> (작업 취소 및 롤백)</h4>
<ul>
<li><strong>동작 방식</strong>: 작업 디렉토리의 특정 파일 수정을 폐기하고 스테이징 영역(Index) 또는 최신 커밋(HEAD)의 상태로 덮어써서 복원합니다.</li>
<li>스테이징된 파일을 언스테이징하려면 <code>git restore --staged &lt;파일명&gt;</code>을 실행합니다.</li>
</ul>
<h4 id="7-git-stash-및-git-stash-pop-작업-내용-임시-스택-보관">7) <code>git stash</code> 및 <code>git stash pop</code> (작업 내용 임시 스택 보관)</h4>
<ul>
<li><strong>동작 방식</strong>:<ul>
<li><code>git stash</code>: 미완성 작업 내역을 커밋하지 않고 내부 임시 스택 참조(<code>.git/refs/stash</code>)에 특수 커밋으로 저장한 뒤 작업 디렉토리를 깨끗한 HEAD 상태로 되돌립니다.</li>
<li><code>git stash pop</code>: 스택 최상단에 보관된 수정 사항을 현재 작업 디렉토리에 다시 복원하고 스택 엔트리를 삭제합니다.</li>
</ul>
</li>
</ul>
<h4 id="8-git-log---oneline---graph">8) <code>git log --oneline --graph</code></h4>
<ul>
<li><strong>동작 방식</strong>: 로컬 저장소의 커밋 히스토리를 단축 해시, 커밋 메시지, ASCII 텍스트 브랜치 그래프로 출력합니다. 터미널 뷰어 종료는 <code>q</code> 키를 입력합니다.</li>
</ul>
<hr>
<h2 id="7-커널-패닉-및-버그-추적을-위한-디버깅-인프라-gdb-커널-원격-디버깅과-채점-파일output-result-분석법">7. 커널 패닉 및 버그 추적을 위한 디버깅 인프라: GDB 커널 원격 디버깅과 채점 파일(.output, .result) 분석법</h2>
<h3 id="71-채점-파일의-구조와-디버깅-접근법">7.1 채점 파일의 구조와 디버깅 접근법</h3>
<p><code>src/threads/build/tests/threads/</code> 디렉토리 아래에는 각 테스트마다 2개의 결과 파일이 생성됩니다.</p>
<ol>
<li><strong><code>*.output</code> (실제 커널 실행 전체 콘솔 로그)</strong>:<ul>
<li>가상머신 구동 시 시리얼 포트로 전송된 모든 콘솔 바이트 텍스트가 저장됩니다.</li>
<li>테스트 실패 시 이 파일을 텍스트 에디터로 열어보면 커널 패닉 지점, <code>ASSERTION FAILED</code>가 발생한 소스 파일과 라인 번호, 스레드 호출 스택이 그대로 기록되어 있습니다.</li>
</ul>
</li>
<li><strong><code>*.result</code> (채점 결과 판정 파일)</strong>:<ul>
<li>채점기(<code>.ck</code>)가 <code>.output</code> 내용을 정규표현식으로 대조한 최종 결과이며, <code>PASS</code> 또는 기대 출력과의 차이점(Diff)이 기록됩니다.</li>
</ul>
</li>
</ol>
<h3 id="72-gdb를-이용한-qemu-커널-원격-디버깅-명령어">7.2 GDB를 이용한 QEMU 커널 원격 디버깅 명령어</h3>
<p>무한 루프나 메모리 침범으로 커널이 멈출 때 변수와 레지스터 상태를 추적하기 위해 GDB를 QEMU에 원격 연결합니다.</p>
<pre><code class="language-bash"># 터미널 1: QEMU 가상머신을 GDB 대기 모드로 기동 (1234번 포트 오픈)
pintos --gdb -- run alarm-single

# 터미널 2: 동일 빌드 디렉토리에서 핀토스 전용 GDB 디버거 실행
pintos-gdb kernel.o</code></pre>
<ul>
<li><strong>GDB 콘솔 내부 조작 흐름</strong>:<ol>
<li><code>target remote localhost:1234</code>: QEMU 가상머신의 디버깅 포트에 연결합니다.</li>
<li><code>b timer_sleep</code>: C 소스코드의 <code>timer_sleep</code> 함수 시작 주소에 하드웨어 중단점(Breakpoint)을 설정합니다.</li>
<li><code>c</code> (continue): 커널 실행을 재개하여 중단점까지 진행시킵니다.</li>
<li><code>p thread_current()-&gt;priority</code>: 멈춘 시점의 스레드 우선순위 구조체 멤버 값을 직접 검사합니다.</li>
</ol>
</li>
</ul>
<hr>
<h2 id="8-빌드-부산물-격리를-위한-gitignore-설정-원리와-팀-프로젝트-코드-충돌conflict-해결-메커니즘">8. 빌드 부산물 격리를 위한 .gitignore 설정 원리와 팀 프로젝트 코드 충돌(Conflict) 해결 메커니즘</h2>
<h3 id="81-gitignore-설정의-물리적-필요성">8.1 .gitignore 설정의 물리적 필요성</h3>
<p>C 프로젝트를 빌드하면 소스코드보다 수십 배 큰 크기의 기계어 목적 파일(<code>*.o</code>), 커널 바이너리(<code>kernel.bin</code>), 테스트 로그(<code>*.output</code>, <code>*.result</code>)가 생성됩니다.</p>
<ul>
<li><strong>이진 파일(Binary)의 Git 추적 문제점</strong>: 이진 파일은 1바이트만 변경되어도 파일 전체가 새로운 Blob 객체로 저장되어 <code>.git</code> 용량이 폭증하며, 컴파일러 환경 차이로 인한 자동 병합 불가(Binary Conflict)가 발생합니다.</li>
<li>따라서 프로젝트 루트의 <code>.gitignore</code> 파일에 빌드 디렉토리를 반드시 등록해야 합니다.</li>
</ul>
<pre><code class="language-text"># Pintos .gitignore 필수 등록 패턴
src/threads/build/
*.o
*.d
kernel.bin
loader.bin
*.output
*.result</code></pre>
<h3 id="82-팀-프로젝트-브랜치-병합-시-코드-충돌conflict-해결-원리">8.2 팀 프로젝트 브랜치 병합 시 코드 충돌(Conflict) 해결 원리</h3>
<p>팀원 간 동일 파일의 동일 라인을 수정하고 병합을 시도하면 Git은 자동 병합을 중단하고 충돌 경계 마커를 삽입합니다.</p>
<pre><code class="language-text">&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD (내가 작성한 로컬 커밋 코드)
    t-&gt;priority = PRI_DEFAULT;
=======
    t-&gt;priority = new_priority;
&gt;&gt;&gt;&gt;&gt;&gt;&gt; dev (팀원이 작성하여 원격에서 병합하려는 코드)</code></pre>
<ol>
<li>소스코드 파일에서 충돌 경계 마커(<code>&lt;&lt;&lt;&lt;&lt;&lt;&lt;</code>, <code>=======</code>, <code>&gt;&gt;&gt;&gt;&gt;&gt;&gt;</code>)를 확인합니다.</li>
<li>개발자가 올바른 구현을 선택하거나 두 로직을 병합 수정한 뒤 충돌 마커를 완전히 삭제합니다.</li>
<li><code>git add src/threads/thread.c</code> 및 <code>git commit</code>을 실행하여 병합 커밋(Merge Commit)을 생성함으로써 충돌을 최종 해결합니다.</li>
</ol>
<hr>
<h2 id="9-pintos-project-1-스레드threads-과제-구현-1사이클-표준-워크플로">9. Pintos Project 1 스레드(Threads) 과제 구현 1사이클 표준 워크플로</h2>
<p>실제 프로젝트 진행 중 매일 반복 수행하는 표준 개발 사이클은 다음과 같은 일관된 파이프라인으로 정립됩니다.</p>
<pre><code class="language-text">[단계 1: 최신 코드 동기화 및 작업 브랜치 확인]
  $ git pull origin dev
  $ git switch feat/alarm-clock

[단계 2: C 소스코드 수정]
  $ vim src/devices/timer.c        (timer_sleep 함수를 슬립 큐 방식으로 수정)
  $ vim src/threads/thread.c       (스레드 깨우기 로직 구현)

[단계 3: 빌드 디렉토리 이동 및 재컴파일]
  $ cd src/threads/build
  $ make

[단계 4: 수정 기능 단일 단위 테스트 고속 실행]
  $ pintos -- run alarm-multiple

[단계 5: 실행 결과 분기]
  ├─ [실패 시]:
  │    $ cat tests/threads/alarm-multiple.output (오류 로그 확인)
  │    ──▶ 단계 2로 복귀하여 소스코드 디버깅 및 수정
  │
  └─ [성공 시 (PASS)]:
       $ make tests/threads/alarm-multiple.result (채점 결과 PASS 갱신 확인)
       $ git status                              (수정된 파일 목록 확인)
       $ git diff                                (변경 코드 라인 검증)
       $ git add src/devices/timer.c src/threads/thread.c
       $ git commit -m &quot;feat(threads): timer_sleep 슬립 큐 기반 대기 및 알람 시계 통과&quot;
       $ git push origin feat/alarm-clock         (GitHub 원격 백업)

[단계 6: 마일스톤 완료 시 전체 회귀 검증]
  $ make clean
  $ make
  $ make check                                   (Project 1 전체 27개 테스트 종합 채점)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C언어/말록랩] calloc이 malloc과 memset 조합보다 압도적으로 빠른 가상 메모리 Zero Page 공유와 CoW(Copy-on-Write) 지연 할당 원리]]></title>
            <link>https://velog.io/@jae_yun/calloc%EC%9D%B4-malloc-memset%EB%B3%B4%EB%8B%A4-%EC%95%95%EB%8F%84%EC%A0%81%EC%9C%BC%EB%A1%9C-%EB%B9%A0%EB%A5%B8-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@jae_yun/calloc%EC%9D%B4-malloc-memset%EB%B3%B4%EB%8B%A4-%EC%95%95%EB%8F%84%EC%A0%81%EC%9C%BC%EB%A1%9C-%EB%B9%A0%EB%A5%B8-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Thu, 08 Oct 2026 05:33:13 GMT</pubDate>
            <description><![CDATA[<h2 id="소주제">소주제</h2>
<ol>
<li>DRAM 물리 셀의 전하 상태와 운영체제 커널의 프로세스 간 정보 유출 차단용 Zero-out 보안 규약</li>
<li>malloc과 memset 조합 실행 시 CPU 명령어(rep stosq / SIMD), 페이지 폴트 연쇄 발생과 캐시 스래싱(Cache Thrashing) 메커니즘</li>
<li>calloc의 가상 메모리 최적화: x86-64 PTE 구조(PFN, Present, R/W, Dirty)와 커널 ZERO_PAGE CoW(Copy-on-Write) 지연 할당 원리</li>
<li>컴파일/링크 타임 바이너리 구조와 런타임 힙 할당: 실행 파일 ELF(.bss vs .data)와 calloc의 물리 메모리 절약 차이</li>
<li>메모리 공급 출처에 따른 성능 분기: 대형 할당(mmap 시스템 콜) vs 소형 할당(힙 가용 리스트, 경계 태그 및 Dirty 청크 오버헤드)</li>
<li>상위 소프트웨어 레벨의 초기화 최적화: 세대 번호 기반 타임스탬프(Epoch) 기법의 O(1) 논리적 리셋과 메모리 추상화 계층 구분</li>
<li>프로젝트 로드맵 연계: 사용자 공간 말록랩(Malloc Lab)에서 커널 공간 핀토스(Pintos Project 3 가상 메모리)까지의 아키텍처 관통</li>
</ol>
<hr>
<h2 id="1-dram-물리-셀의-전하-상태와-운영체제-커널의-프로세스-간-정보-유출-차단용-zero-out-보안-규약">1. DRAM 물리 셀의 전하 상태와 운영체제 커널의 프로세스 간 정보 유출 차단용 Zero-out 보안 규약</h2>
<h3 id="11-dram-반도체-소자의-물리적-상태">1.1 DRAM 반도체 소자의 물리적 상태</h3>
<p>컴퓨터의 주기억장치(DRAM)는 1비트마다 트랜지스터 1개와 커패시터 1개로 구성된 미세 물리 셀들의 집합체입니다.</p>
<ul>
<li>커패시터에 전하가 충전되어 있으면 1, 방전되어 있으면 0의 전압 신호로 판독됩니다.</li>
<li>전원이 공급되는 동안 컴퓨터 하드웨어 내부에는 비어 있는 상태라는 개념이 물리적으로 존재하지 않으며, 모든 번지수에는 0 또는 1의 전압 상태가 항시 잔존합니다.</li>
</ul>
<h3 id="12-프로세스-간-메모리-공유와-정보-유출information-leak-방지">1.2 프로세스 간 메모리 공유와 정보 유출(Information Leak) 방지</h3>
<p>운영체제(OS)는 한정된 물리 RAM을 여러 프로세스가 번갈아 할당받고 해제(free)하도록 중계합니다.</p>
<ul>
<li>프로세스 A가 실행을 마치거나 동적 메모리를 해제했을 때, 해당 물리 메모리 셀들에는 프로세스 A가 다루던 암호화 키, 로그인 세션 토큰, 개인 식별 데이터가 물리적 비트 형태로 그대로 잔존합니다.</li>
<li>만약 커널이 이 물리 메모리를 0으로 초기화하지 않은 채 프로세스 B에게 할당하면, 프로세스 B는 포인터 역참조를 통해 프로세스 A의 기밀 데이터를 메모리 덤프로 탈취할 수 있는 보안 취약점이 발생합니다.</li>
<li>따라서 모든 현대 운영체제 커널은 <strong>&quot;유저 모드 프로세스에게 새로운 물리 메모리 페이지를 할당할 때는, 반드시 커널 모드에서 모든 바이트를 0으로 덮어써서(Zero-out) 전달해야 한다&quot;</strong>는 하드웨어 및 운영체제 보안 불변식을 강제합니다.</li>
</ul>
<hr>
<h2 id="2-malloc과-memset-조합-실행-시-cpu-명령어rep-stosq--simd-페이지-폴트-연쇄-발생과-캐시-스래싱cache-thrashing-메커니즘">2. malloc과 memset 조합 실행 시 CPU 명령어(rep stosq / SIMD), 페이지 폴트 연쇄 발생과 캐시 스래싱(Cache Thrashing) 메커니즘</h2>
<p>0으로 초기화된 대형 메모리를 확보하기 위해 malloc 직후 memset을 수행하는 방식은 하드웨어 자원을 극심하게 낭비합니다.</p>
<pre><code class="language-c">void *ptr = malloc(1024 * 1024 * 100); // 100MB 가상 메모리 할당
memset(ptr, 0, 1024 * 1024 * 100);     // 100MB 전체에 물리적 0 쓰기 루프 실행</code></pre>
<h3 id="21-저수준-명령어-실행-rep-stosq-및-simd-레지스터">2.1 저수준 명령어 실행: rep stosq 및 SIMD 레지스터</h3>
<ul>
<li>컴파일러와 C 라이브러리의 memset 함수는 x86-64 아키텍처에서 고속 반복 명령어인 rep stosq 또는 128비트/256비트 SIMD(AVX) 벡터 레지스터를 호출하도록 어셈블리 수준에서 최적화되어 있습니다.</li>
<li>이는 CPU 코어가 메모리 버스의 최대 대역폭을 점유하며 순차적으로 물리 주소 공간에 0을 강제 기록하는 하드웨어 동작입니다.</li>
</ul>
<h3 id="22-지연-할당-무력화와-연쇄적-페이지-폴트page-fault">2.2 지연 할당 무력화와 연쇄적 페이지 폴트(Page Fault)</h3>
<ul>
<li>malloc 호출 시점에는 운영체제가 가상 주소 공간(VMA, Virtual Memory Area)의 연속된 주소 범위만 예약할 뿐, 실제 물리 RAM 프레임을 전혀 연결하지 않습니다.</li>
<li>그러나 직후 실행되는 memset 루프는 할당받은 영역의 첫 1바이트부터 마지막 바이트까지 모든 페이지(4KB 단위)에 순차 쓰기를 시도합니다.</li>
<li>이로 인해 운영체제의 지연 할당(Lazy Allocation) 최적화가 전면 무력화되며, 100MB 할당 기준 총 25,600회(100MB / 4KB)에 달하는 하드웨어 인터럽트(Page Fault)가 연쇄적으로 발생합니다.</li>
<li>CPU 제어권이 유저 모드와 커널 모드를 25,600번 오가며 문맥 교환 오버헤드와 물리 프레임 할당 연산이 즉각 강제됩니다.</li>
</ul>
<h3 id="23-이중-0-쓰기double-zeroing와-캐시-스래싱cache-thrashing">2.3 이중 0 쓰기(Double Zeroing)와 캐시 스래싱(Cache Thrashing)</h3>
<ol>
<li><strong>이중 0 쓰기 오버헤드</strong>:<ul>
<li>커널은 보안 규약에 따라 새 물리 프레임을 할당할 때마다 커널 내부에서 이미 4KB 전체를 0으로 밀어버립니다(1차 0 쓰기).</li>
<li>유저 공간으로 복귀한 직후, memset 루프가 동일한 물리 메모리에 다시 0을 쓰는 중복 명령을 수행합니다(2차 0 쓰기).</li>
<li>동일한 물리 셀에 0을 두 번 연속 기록하는 물리 버스 낭비가 발생합니다.</li>
</ul>
</li>
<li><strong>캐시 스래싱 (Cache Thrashing)</strong>:<ul>
<li>CPU가 대규모 메모리 영역에 순차적으로 0 데이터를 쏟아붓는 동안, CPU 내부의 초고속 SRAM인 L1, L2, L3 캐시 라인이 전부 0 데이터로 가득 찹니다.</li>
<li>이 과정에서 애플리케이션의 핵심 코드와 실제 작업 데이터가 캐시에서 강제 방출(Eviction)되어, 시스템 전체의 캐시 적중률(Cache Hit Ratio)이 급락합니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="3-calloc의-가상-메모리-최적화-x86-64-pte-구조pfn-present-rw-dirty와-커널-zero_page-cowcopy-on-write-지연-할당-원리">3. calloc의 가상 메모리 최적화: x86-64 PTE 구조(PFN, Present, R/W, Dirty)와 커널 ZERO_PAGE CoW(Copy-on-Write) 지연 할당 원리</h2>
<p>calloc은 메모리 요청 시점의 실제 물리 메모리 기록을 생략하고, MMU(Memory Management Unit)와 페이징 하드웨어를 제어하여 할당 속도를 상수 시간 O(1)로 단축합니다.</p>
<h3 id="31-64비트-x86-64-페이지-테이블-엔트리pte-구조">3.1 64비트 x86-64 페이지 테이블 엔트리(PTE) 구조</h3>
<p>가상 주소를 물리 주소로 변환하기 위해 프로세스마다 존재하는 다단계 페이지 테이블의 최하위 행(Entry)을 PTE(Page Table Entry)라고 부르며, 8바이트(64비트) 크기를 갖습니다.</p>
<table>
<thead>
<tr>
<th align="left">비트 필드</th>
<th align="left">명칭</th>
<th align="left">기능 및 상태</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>Bit 0</strong></td>
<td align="left"><strong>Present (P)</strong></td>
<td align="left">1: 물리 RAM에 프레임 존재 / 0: 미할당 또는 스왑 디스크 상태 (접근 시 Page Fault 유발)</td>
</tr>
<tr>
<td align="left"><strong>Bit 1</strong></td>
<td align="left"><strong>Read/Write (R/W)</strong></td>
<td align="left">1: 읽기 및 쓰기 모두 허용 / 0: 읽기 전용 (쓰기 시도 시 Page Fault 유발)</td>
</tr>
<tr>
<td align="left"><strong>Bit 2</strong></td>
<td align="left"><strong>User/Supervisor (U/S)</strong></td>
<td align="left">1: 사용자 모드 접근 허용 / 0: 커널 모드 전용 접근</td>
</tr>
<tr>
<td align="left"><strong>Bit 5</strong></td>
<td align="left"><strong>Accessed (A)</strong></td>
<td align="left">해당 페이지의 읽기/쓰기 참조 발생 시 하드웨어가 1로 설정 (페이지 교체 시 참조)</td>
</tr>
<tr>
<td align="left"><strong>Bit 6</strong></td>
<td align="left"><strong>Dirty (D)</strong></td>
<td align="left">해당 페이지에 쓰기 연산 발생 시 하드웨어가 1로 설정 (스왑 동기화 판단)</td>
</tr>
<tr>
<td align="left"><strong>Bit 12~51</strong></td>
<td align="left"><strong>PFN (Page Frame Number)</strong></td>
<td align="left">매핑된 실제 물리 RAM 프레임의 4KB 단위 물리 기준 주소 비트열</td>
</tr>
</tbody></table>
<h3 id="32-커널-단일-zero_page의-읽기-전용-공유">3.2 커널 단일 ZERO_PAGE의 읽기 전용 공유</h3>
<p>운영체제 커널은 부팅 시 모든 바이트가 0으로 채워진 단 하나의 물리 4KB 페이지 프레임인 ZERO_PAGE를 물리 RAM에 상주시켜 둡니다.</p>
<pre><code class="language-text">[프로세스 가상 메모리 공간]                    [물리 RAM]
가상 페이지 0 (0x1000) ──┐
가상 페이지 1 (0x2000) ──┼──(PTE: Present=1, R/W=0)──▶ [물리 ZERO_PAGE (4KB 단일 프레임)]
가상 페이지 2 (0x3000) ──┘</code></pre>
<ul>
<li>calloc으로 100MB(25,600개 페이지)를 요청하면, 커널은 25,600개의 가상 페이지 전체가 이 <strong>단 하나의 물리 ZERO_PAGE를 가리키도록 페이지 테이블의 PFN을 매핑</strong>합니다.</li>
<li>이때 PTE의 <strong>R/W 비트는 0(읽기 전용)</strong>으로 설정됩니다.</li>
<li>소비되는 실제 물리 메모리는 단 4KB이며, CPU가 메모리에 0을 기록하는 루프는 단 한 번도 실행되지 않으므로 할당 연산이 즉각 완료됩니다.</li>
</ul>
<h3 id="33-cowcopy-on-write-하드웨어-발동-상세-메커니즘">3.3 CoW(Copy-on-Write) 하드웨어 발동 상세 메커니즘</h3>
<p>프로세스가 할당받은 주소 공간을 읽기(Read)만 할 때는 수만 개의 가상 페이지가 단 하나의 물리 ZERO_PAGE를 공유하며 항상 0을 읽어옵니다.</p>
<p>실제 쓰기(Write) 연산이 발생하는 순간의 하드웨어 및 커널 제어 흐름은 다음과 같습니다.</p>
<pre><code class="language-text">1. 프로그램: 특정 가상 주소에 데이터 쓰기(Write) 명령어 실행
   │
2. CPU MMU: 페이지 테이블 조회 ──▶ PTE의 R/W 비트가 0(읽기 전용)임을 감지
   │
3. 하드웨어 인터럽트: CPU가 #PF (Page Fault 인터럽트 벡터 14) 발생
   │
4. 제어권 이관: 커널의 Page Fault 핸들러로 전환
   │
5. 예외 원인 판별: 핸들러가 VMA를 검사하여 정당한 쓰기 영역(CoW 대상)인지 불법 주소(Segmentation Fault)인지 확인
   │
6. 물리 프레임 분리: 커널이 빈 물리 메모리에서 새 4KB 프레임 1장을 즉각 할당
   │
7. 데이터 복제: 기존 ZERO_PAGE의 내용(4KB의 0)을 새로 할당된 물리 프레임으로 1회 복사
   │
8. PTE 재설정: 해당 가상 페이지의 PFN을 새 물리 프레임 주소로 교체하고, R/W 비트를 1(쓰기 허용)로 갱신
   │
9. 복귀 및 재실행: Page Fault 핸들러 종료 후, CPU가 중단되었던 쓰기 명령어를 정상 재실행</code></pre>
<ul>
<li><strong>희소 접근(Sparse Access) 최적화</strong>: 100MB를 할당받고 실제로는 특정 위치 4KB 페이지만 수정하는 경우, malloc + memset은 100MB 전체에 물리 RAM을 강제 할당하고 2회 0 쓰기를 수행하지만, calloc은 수정된 단 4KB 페이지만 물리 프레임을 분리하고 나머지 99.996MB에 대해서는 물리 메모리 소모와 쓰기 연산이 0으로 유지됩니다.</li>
</ul>
<hr>
<h2 id="4-컴파일링크-타임-바이너리-구조와-런타임-힙-할당-실행-파일-elfbss-vs-data와-calloc의-물리-메모리-절약-차이">4. 컴파일/링크 타임 바이너리 구조와 런타임 힙 할당: 실행 파일 ELF(.bss vs .data)와 calloc의 물리 메모리 절약 차이</h2>
<p>&#39;0으로 초기화된 메모리가 디스크 또는 RAM 공간을 차지하지 않는다&#39;는 개념은 컴파일/링크 타임의 정적 바이너리 구조와 런타임 운영체제 가상 메모리 관리의 차이에서 비롯됩니다.</p>
<pre><code class="language-text">+--------------------------------------------------+
|                    ELF 실행 파일                  |
|  +--------------------------------------------+  |
|  | .text 섹션: 컴파일된 기계어 코드            |  |
|  +--------------------------------------------+  |
|  | .data 섹션: 초기화된 전역/정적 변수         |  | ◀── 실제 디스크 용량 차지 (초기화 데이터 기록)
|  +--------------------------------------------+  |
|  | .bss 섹션:  0 또는 미초기화 전역 변수       |  | ◀── 메타데이터(크기 정보)만 기록 (용량 차지 0)
|  +--------------------------------------------+  |
+--------------------------------------------------+</code></pre>
<h3 id="41-정적-데이터-섹션-data-vs-bss">4.1 정적 데이터 섹션: .data vs .bss</h3>
<ul>
<li><strong>.data 섹션</strong>: <code>int arr[1000] = {1, 2, ...};</code>처럼 0이 아닌 유의미한 값으로 초기화된 전역 변수입니다. 초기화된 실제 바이트 데이터가 실행 파일(바이너리) 내부에 물리적으로 기록되므로 파일의 디스크 용량을 직접 차지합니다.</li>
<li><strong>.bss 섹션</strong>: <code>int arr[1000] = {0};</code>처럼 0으로 초기화되었거나 초기화되지 않은 전역 변수입니다. 컴파일러는 4000바이트의 0을 파일에 기록하지 않고, &quot;프로그램 적재 시 4000바이트 크기의 0 공간이 필요함&quot;이라는 메타데이터(크기 정보)만 기록합니다. 따라서 실행 파일의 디스크 크기가 늘어나지 않습니다.</li>
</ul>
<h3 id="42-bss-섹션과-calloc의-핵심-차이점">4.2 .bss 섹션과 calloc의 핵심 차이점</h3>
<ul>
<li><strong>.bss (정적 전역 변수)</strong>: <strong>컴파일 및 링크 타임 최적화</strong>입니다. 하드디스크의 실행 파일 크기를 절약하는 것이 목적이며, 프로그램 실행 시 운영체제 로더가 메모리에 적재하면서 가상 메모리에 매핑합니다.</li>
<li><strong>calloc (동적 힙 할당)</strong>: <strong>런타임(Runtime) OS 가상 메모리 최적화</strong>입니다. 컴파일러는 실행 시점에 calloc에 어떤 크기가 들어올지 알 수 없으므로 단순히 함수 호출 코드(call calloc)만 생성합니다. 실제 메모리 매핑과 ZERO_PAGE 공유, CoW 처리는 100% 실행 중에 운영체제 커널에 의해 수행되어 <strong>물리 RAM 용량을 절약</strong>합니다.</li>
</ul>
<hr>
<h2 id="5-메모리-공급-출처에-따른-성능-분기-대형-할당mmap-시스템-콜-vs-소형-할당힙-가용-리스트-경계-태그-및-dirty-청크-오버헤드">5. 메모리 공급 출처에 따른 성능 분기: 대형 할당(mmap 시스템 콜) vs 소형 할당(힙 가용 리스트, 경계 태그 및 Dirty 청크 오버헤드)</h2>
<p>calloc의 Zero Page CoW 최적화는 모든 크기의 할당에 일괄 적용되지 않으며, 할당기가 메모리를 가져오는 공급 경로에 따라 물리적 동작이 분기됩니다.</p>
<table>
<thead>
<tr>
<th align="left">비교 항목</th>
<th align="left">대형 할당 (Large Allocation)</th>
<th align="left">소형 할당 (Small Allocation)</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>기준 크기</strong></td>
<td align="left">glibc 기준 대략 128KB 이상 (MMAP_THRESHOLD)</td>
<td align="left">수 바이트 ~ 수십 KB 단위</td>
</tr>
<tr>
<td align="left"><strong>기저 메커니즘</strong></td>
<td align="left">커널 mmap 시스템 콜 직접 호출</td>
<td align="left">사용자 공간 힙 내부 가용 리스트(Free List) 탐색 및 분할</td>
</tr>
<tr>
<td align="left"><strong>메모리 상태</strong></td>
<td align="left">OS가 새로 매핑한 Clean(0 보장) 페이지</td>
<td align="left">이전에 프로세스가 쓰다 해제한 Dirty(쓰레기 값 잔존) 블록</td>
</tr>
<tr>
<td align="left"><strong>calloc 내부 동작</strong></td>
<td align="left">커널의 0 보장을 신뢰하여 유저 레벨 0 쓰기 전면 생략</td>
<td align="left">기존 잔존 데이터를 지우기 위해 내부에서 직접 memset 루프 실행</td>
</tr>
<tr>
<td align="left"><strong>속도 차이</strong></td>
<td align="left">Zero Page 공유로 <strong>calloc이 수십~수백 배 압도적으로 빠름</strong></td>
<td align="left">내부에서 memset을 돌리므로 <strong>calloc과 malloc + memset의 속도가 기계적으로 동일</strong></td>
</tr>
</tbody></table>
<h3 id="51-대형-할당-mmap-시스템-콜">5.1 대형 할당: mmap 시스템 콜</h3>
<ul>
<li>요청 크기가 MMAP_THRESHOLD(기본 128KB) 이상이면, 힙 영역(brk)을 확장하지 않고 커널의 mmap 시스템 콜을 통해 독립된 익명 페이지(Anonymous Page)를 매핑받습니다.</li>
<li>커널이 새로 제공하는 페이지는 앞서 설명한 커널 보안 규약에 의해 0 초기화가 보장되므로, calloc은 별도의 바이트 쓰기를 전혀 하지 않고 Zero Page와 CoW를 적용합니다.</li>
</ul>
<h3 id="52-소형-할당-가용-리스트free-list와-dirty-청크">5.2 소형 할당: 가용 리스트(Free List)와 Dirty 청크</h3>
<ul>
<li>잦은 소형 할당마다 시스템 콜을 호출하면 문맥 교환 오버헤드로 인해 시스템 성능이 저하됩니다.</li>
<li>따라서 할당기는 과거에 free되어 보관 중인 가용 블록(Free Block)을 쪼개어 재할당합니다.</li>
<li>이 블록 내부에는 이전에 프로그램이 기록했던 정수, 문자열, 포인터 등의 과거 데이터(Dirty Data)가 그대로 남아 있습니다.</li>
<li>이는 OS 커널이 새로 매핑해 준 페이지가 아니므로 Zero Page 메커니즘을 적용할 수 없습니다.</li>
<li>따라서 calloc 라이브러리 함수 역시 사용자에게 반환하기 전에 <strong>자체 루프를 돌아 블록 페이로드를 바이트 단위로 0으로 덮어써야(memset) 합니다.</strong></li>
</ul>
<h3 id="53-힙-블록-메타데이터-구조-경계-태그boundary-tag와-단편화">5.3 힙 블록 메타데이터 구조: 경계 태그(Boundary Tag)와 단편화</h3>
<p>말록랩에서 구현하는 동적 메모리 할당기의 힙 블록은 다음과 같은 구조로 동작합니다.</p>
<pre><code class="language-text">[할당 블록 메모리 구조]               [가용 블록(Free Block) 메모리 구조]
┌───────────────────────────┐         ┌───────────────────────────┐
│ Header (크기 + 할당 비트 1)  │         │ Header (크기 + 할당 비트 0)  │
├───────────────────────────┤         ├───────────────────────────┤
│                           │         │ NEXT 가용 포인터 (8바이트) │ ──▶ 다음 가용 블록 주소
│ User Payload 데이터 영역    │         ├───────────────────────────┤
│                           │         │ PREV 가용 포인터 (8바이트) │ ◀── 이전 가용 블록 주소
│                           │         ├───────────────────────────┤
│                           │         │ 미사용 여백 공간            │
├───────────────────────────┤         ├───────────────────────────┤
│ Footer (크기 + 할당 비트 1)  │         │ Footer (크기 + 할당 비트 0)  │
└───────────────────────────┘         └───────────────────────────┘</code></pre>
<ol>
<li><strong>헤더(Header)와 푸터(Footer) 경계 태그</strong>:<ul>
<li>블록의 양 끝에 위치하는 4바이트 또는 8바이트 메타데이터입니다.</li>
<li>블록의 전체 크기(Size)와 할당 여부(Allocated Bit, 최하위 비트 a/f)를 비트 패킹으로 저장합니다.</li>
<li>현재 블록 직전의 푸터를 읽어 이전 블록의 가용 여부를 즉시 확인하므로, 인접 가용 블록과의 병합(Coalescing)을 상수 시간 O(1)에 수행합니다.</li>
</ul>
</li>
<li><strong>Explicit Free List의 포인터 오버레이(Overlay)</strong>:<ul>
<li>가용 블록들을 이중 연결 리스트로 연결할 때, 별도의 추가 메모리를 동적 할당하지 않습니다.</li>
<li>블록이 free 상태일 때는 사용자의 페이로드가 비어 있으므로, 페이로드의 첫 16바이트 공간에 NEXT와 PREV 포인터를 직접 덮어씁니다(Overlay).</li>
</ul>
</li>
<li><strong>단편화(Fragmentation)</strong>:<ul>
<li><strong>내부 단편화(Internal Fragmentation)</strong>: 하드웨어 정렬 규약(8바이트 또는 16바이트)과 헤더/푸터 메타데이터로 인해 사용자가 요청한 크기보다 큰 블록이 할당되어 낭비되는 바이트 공간입니다.</li>
<li><strong>외부 단편화(External Fragmentation)</strong>: 가용 메모리의 총합은 요청 크기보다 크지만, 가용 블록들이 작은 조각들로 분할되어 있어 단일 연속 공간을 할당할 수 없는 상태입니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="6-상위-소프트웨어-레벨의-초기화-최적화-세대-번호-기반-타임스탬프epoch-기법의-o1-논리적-리셋과-메모리-추상화-계층-구분">6. 상위 소프트웨어 레벨의 초기화 최적화: 세대 번호 기반 타임스탬프(Epoch) 기법의 O(1) 논리적 리셋과 메모리 추상화 계층 구분</h2>
<p>물리 메모리의 전압이나 OS 가상 메모리 조작과 무관하게, 배열이나 테이블 자료구조의 논리적 초기화 비용을 항상 상수 시간 O(1)로 유지하는 소프트웨어 알고리즘 기법이 존재합니다.</p>
<h3 id="61-동작-원리-및-c-구현-코드">6.1 동작 원리 및 C 구현 코드</h3>
<p>데이터를 저장하는 각 슬롯에 실제 값과 함께 해당 데이터가 기록된 세대 번호(generation)를 구조체로 묶어 관리합니다. 시스템 전역에는 단 하나의 정수 카운터인 global_epoch를 유지합니다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdint.h&gt;

#define MAX_SIZE 1000000

typedef struct {
    int value;
    uint32_t generation;
} Slot;

Slot table[MAX_SIZE];
uint32_t global_epoch = 1;

// O(1) 논리적 초기화 연산
void clear_table(void) {
    global_epoch++; // 수백만 개 요소를 0으로 미는 루프 없이 카운터 1 증가로 전체 초기화
}

// 읽기 연산: 세대 번호 일치 여부 판별
int read_slot(int index) {
    if (table[index].generation != global_epoch) {
        return 0; // 현재 유효 세대의 값이 아니므로 초기화된 상태(0)로 취급
    }
    return table[index].value;
}

// 쓰기 연산: 현재 세대 번호를 각인
void write_slot(int index, int val) {
    table[index].value = val;
    table[index].generation = global_epoch; // 최신 세대 기록
}</code></pre>
<h3 id="62-특징-및-계층layer-구분">6.2 특징 및 계층(Layer) 구분</h3>
<ol>
<li><strong>장점과 한계</strong>:<ul>
<li>배열의 크기가 수억 개에 달해도 clear_table() 호출 시 루프 순회 없이 단 한 번의 정수 덧셈으로 즉각 리셋이 완료됩니다.</li>
<li>단, 슬롯마다 4바이트의 generation 필드를 추가로 유지해야 하므로 메모리 사용량이 증가하며, 32비트 정수 오버플로우 발생 시의 리셋 처리가 요구됩니다.</li>
</ul>
</li>
<li><strong>추상화 계층의 본질적 차이</strong>:<ul>
<li>타임스탬프(Epoch) 기법은 애플리케이션 레벨에서 논리적 유효성만을 판별하는 <strong>소프트웨어 알고리즘 기법</strong>입니다.</li>
<li>반면 calloc의 Zero Page 매핑과 말록랩/OS 커널의 메모리 관리는 <strong>하드웨어 MMU, DRAM 물리 셀, 바이트 단위 주소 버스, 페이지 테이블 비트를 직접 제어하는 물리 및 시스템 계층 기법</strong>입니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="7-프로젝트-로드맵-연계-사용자-공간-말록랩malloc-lab에서-커널-공간-핀토스pintos-project-3-가상-메모리까지의-아키텍처-관통">7. 프로젝트 로드맵 연계: 사용자 공간 말록랩(Malloc Lab)에서 커널 공간 핀토스(Pintos Project 3 가상 메모리)까지의 아키텍처 관통</h2>
<p>컴퓨터 시스템의 메모리 관리 아키텍처는 유저 레벨과 커널 레벨로 명확히 분리되며, 말록랩과 핀토스는 이 거대한 구조의 상부와 하부를 각각 직접 구현하는 과정입니다.</p>
<pre><code class="language-text">[사용자 공간 (User Space)]
  │  애플리케이션 프로그램 코드 (User Application)
  │  C 표준 라이브러리 (malloc, calloc, realloc, free)
  │
  └─▶ [6주차 말록랩 (Malloc Lab)]: 사용자 레벨 힙 할당기 직접 구현
        - mem_sbrk로 연속된 가상 힙 메모리 공간 확보
        - 헤더/푸터 경계 태그(Boundary Tag) 비트 연산
        - Explicit Free List 관리 (페이로드 포인터 오버레이)
        - 가용 블록 탐색(First-fit / Next-fit / Best-fit) 및 분할(place)
        - 상수 시간 인접 가용 블록 병합(Coalescing) 및 단편화 제어

──────────────── 시스템 콜 경계 (Syscall Interface: brk, mmap) ────────────────

[커널 공간 (Kernel Space)]
  │
  └─▶ [7주차 핀토스 (Pintos Project 3: Virtual Memory)]: OS 커널 메모리 서브시스템 직접 구현
        - 하드웨어 다단계 페이지 테이블(Page Table) 및 64비트 PTE 비트 직접 조작
        - CPU 하드웨어 인터럽트 #PF를 수신하는 Page Fault 핸들러 구현
        - 익명 페이지(Anonymous Page) 지연 할당(Lazy Allocation)
        - Frame Table을 통한 물리 RAM 프레임 점유 관리
        - 물리 RAM 부족 시 페이지 교체(Clock 알고리즘, Eviction) 수행
        - 스왑 디스크(Swap Table) 읽기/쓰기 및 파일 매핑(Memory-Mapped Files) 구현</code></pre>
<h3 id="71-말록랩malloc-lab과-mm_calloc">7.1 말록랩(Malloc Lab)과 mm_calloc</h3>
<ul>
<li>말록랩에서 구현하는 할당기는 OS가 이미 가상 메모리를 매핑해 주었다는 전제하에 작동합니다.</li>
<li>mm_calloc 구현 시 학생 레벨 할당기가 다음과 같이 작성되는 이유가 바로 메모리 출처가 유저 힙 가용 리스트이기 때문입니다.</li>
</ul>
<pre><code class="language-c">void *mm_calloc(size_t nmemb, size_t size) {
    size_t total_size = nmemb * size;
    void *ptr = mm_malloc(total_size);
    if (ptr != NULL) {
        memset(ptr, 0, total_size); // 힙 가용 블록 재사용이므로 사용자 레벨 memset 필수
    }
    return ptr;
}</code></pre>
<h3 id="72-7주차-핀토스pintos-커널의-palloc_get_pagepal_zero-연계">7.2 7주차 핀토스(Pintos) 커널의 palloc_get_page(PAL_ZERO) 연계</h3>
<ul>
<li>7주차 핀토스 커널의 페이지 할당자(threads/palloc.c) 역시 플래그를 통해 0 초기화 여부를 하드웨어 수준에서 제어합니다.</li>
<li>palloc_get_page(PAL_ZERO) 호출 시, 커널은 물리 4KB 페이지 프레임을 할당한 직후 커널 모드에서 memset(page, 0, PGSIZE)를 직접 실행하여 0으로 초기화한 뒤 반환합니다.</li>
<li>PAL_ZERO 플래그가 없으면 0 초기화 연산을 건너뛰어 페이지 할당 지연을 최소화합니다.</li>
</ul>
<h3 id="73-시스템-요약">7.3 시스템 요약</h3>
<ul>
<li>calloc이 malloc + memset보다 압도적으로 빠른 이유는 CPU의 물리적 0 쓰기 루프를 커널의 <strong>단일 ZERO_PAGE 매핑과 CoW 지연 할당</strong>이라는 하드웨어 페이징 메커니즘으로 대체했기 때문입니다.</li>
<li>이 원리는 사용자 레벨 힙 메모리 관리자(6주차 말록랩)의 포인터 제어부터, 시스템 콜 경계를 넘어 운영체제 커널(7주차 핀토스 가상 메모리)의 페이지 테이블 및 인터럽트 핸들러 설계로 이어지는 컴퓨터 시스템의 핵심 동작 원리입니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[컴퓨터시스템/말록랩] 주소 버스 물리 한계(2의 N승 바이트), 가상 주소 공간 분할(스택 하향·힙 상향)과 가용 블록 탐색·병합 메커니즘]]></title>
            <link>https://velog.io/@jae_yun/%EC%BB%B4%ED%93%A8%ED%84%B0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%98-%EB%AC%BC%EB%A6%AC%EC%A0%81-%ED%95%98%EB%93%9C%EC%9B%A8%EC%96%B4-%EC%A0%9C%EC%95%BD%EA%B3%BC-%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EB%8F%99%EC%A0%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EA%B4%80%EB%A6%AC%EC%9D%98-%EC%9C%A0%EA%B8%B0%EC%A0%81-%ED%86%B5%ED%95%A9-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98</link>
            <guid>https://velog.io/@jae_yun/%EC%BB%B4%ED%93%A8%ED%84%B0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%98-%EB%AC%BC%EB%A6%AC%EC%A0%81-%ED%95%98%EB%93%9C%EC%9B%A8%EC%96%B4-%EC%A0%9C%EC%95%BD%EA%B3%BC-%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EB%8F%99%EC%A0%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EA%B4%80%EB%A6%AC%EC%9D%98-%EC%9C%A0%EA%B8%B0%EC%A0%81-%ED%86%B5%ED%95%A9-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98</guid>
            <pubDate>Tue, 06 Oct 2026 07:57:04 GMT</pubDate>
            <description><![CDATA[<h2 id="소주제">소주제</h2>
<ol>
<li>CPU 물리 주소 버스 가닥 수 N개와 2의 N승 바이트 절대 메모리 한계선 및 1바이트 주소 매핑 원리</li>
<li>프로세스 가상 메모리 4대 세그먼트: 코드, 데이터(BSS), 힙(상향 성장), 스택(하향 성장)의 마주 보는 대칭 구조</li>
<li>동적 메모리 가용 블록 탐색 3대 정책(First-fit, Next-fit, Best-fit)의 물리적 주소 이동 추적</li>
<li>경계 태그(푸터)를 통한 직전 블록 메타데이터 즉각 조회 및 O(1) 상수 시간 양방향 병합 메커니즘</li>
<li>6주차 말록랩에서 7주차 핀토스(Pintos)로의 연결: PHYS_BASE 가상 메모리 분할과 스택 동적 확장(Stack Growth)</li>
</ol>
<hr>
<h2 id="1-cpu-물리-주소-버스-가닥-수-n개와-2의-n승-바이트-절대-메모리-한계선-및-1바이트-주소-매핑-원리">1. CPU 물리 주소 버스 가닥 수 N개와 2의 N승 바이트 절대 메모리 한계선 및 1바이트 주소 매핑 원리</h2>
<p>컴퓨터 하드웨어에서 CPU와 주기억장치(RAM) 사이를 연결하는 전선 묶음을 시스템 버스라고 합니다.</p>
<ul>
<li><strong>주소 버스의 물리 가닥 수</strong>: CPU 메인보드 칩셋에서 메모리로 뻗어 있는 물리 구리선의 개수(N)를 의미합니다.</li>
<li>각 전선은 0볼트(0) 또는 고전압(1)의 2가지 상태만을 표현할 수 있으므로, N가닥의 전선으로 조합할 수 있는 고유 번지수 숫자의 총개수는 <strong>2의 N승 개</strong>가 됩니다.</li>
<li>컴퓨터 아키텍처는 RAM의 메모리 셀을 <strong>1바이트(8비트) 단위마다 고유 주소 번호 1개를 1대1 매핑</strong>하도록 물리 규격화되어 있습니다.</li>
<li>따라서 시스템이 장착하고 인식할 수 있는 최대 물리 RAM 크기는 하드웨어적으로 <strong>2의 N승 바이트</strong>로 절대 고정됩니다 (32비트 버스는 2의 32승 바이트 = 4GB, 64비트 아키텍처는 테라바이트급 이상).</li>
</ul>
<hr>
<h2 id="2-프로세스-가상-메모리-4대-세그먼트-코드-데이터bss-힙상향-성장-스택하향-성장의-마주-보는-대칭-구조">2. 프로세스 가상 메모리 4대 세그먼트: 코드, 데이터(BSS), 힙(상향 성장), 스택(하향 성장)의 마주 보는 대칭 구조</h2>
<p>운영체제는 프로세스가 실행될 때 가상 주소 공간을 4대 세그먼트로 분할하여 배치합니다.</p>
<pre><code class="language-text">[프로세스 가상 주소 공간 구조]
높은 주소 (PHYS_BASE)
  │  ▼ [스택 세그먼트 (Stack)]  (함수 호출 시 낮은 주소로 하향 성장)
  │
  │      중앙 빈 여백 공간 (동적 공유)
  │
  │  ▲ [힙 세그먼트 (Heap)]      (malloc 호출 시 높은 주소로 상향 성장)
  │  [데이터 세그먼트 (Data/BSS)] (전역 변수, 정적 변수 고정 할당)
  ▼  [코드 세그먼트 (Code/Text)]  (기계어 명령어 읽기 전용 고정 할당)
0x0 (접근 금지 NULL 영역)</code></pre>
<ul>
<li><strong>스택(Stack)</strong>: 함수 호출 시 생성되는 지역 변수와 복귀 주소가 저장되며, 높은 주소에서 낮은 주소로 자라납니다.</li>
<li><strong>힙(Heap)</strong>: <code>malloc</code>에 의해 할당되며, 낮은 주소에서 높은 주소로 자라납니다.</li>
<li><strong>마주 보며 자라는 이유</strong>: 프로그램 실행 전에는 스택이 깊어질지(재귀 호출), 힙이 커질지(데이터 할당) 예측이 불가능합니다. 두 영역이 중앙의 거대한 빈 가상 메모리 공간을 서로 마주 보며 공유함으로써 메모리 공간 낭비 없이 유연하게 확장할 수 있습니다.</li>
</ul>
<hr>
<h2 id="3-동적-메모리-가용-블록-탐색-3대-정책first-fit-next-fit-best-fit의-물리적-주소-이동-추적">3. 동적 메모리 가용 블록 탐색 3대 정책(First-fit, Next-fit, Best-fit)의 물리적 주소 이동 추적</h2>
<p>프로그래머가 <code>malloc</code>으로 크기를 요구했을 때 힙 관리자가 블록을 선택하는 물리적 알고리즘 비교입니다.</p>
<ul>
<li><strong>First-fit (최초 적합)</strong>: 매번 힙 시작점부터 순차 스캔하여 요청 크기를 수용하는 첫 가용 블록을 선택합니다.</li>
<li><strong>Next-fit (다음 적합)</strong>: 직전 할당 위치의 다음 번지수부터 이어서 탐색하므로 순회 속도는 빠르지만, 힙 뒷부분의 큰 블록을 빠르게 파편화합니다.</li>
<li><strong>Best-fit (최적 적합)</strong>: 모든 가용 블록을 전수 조사하여 남는 자투리 크기가 가장 작은 블록을 선택하므로 메모리 이용률은 극대화되나 탐색 시간이 O(N)으로 가장 느립니다.</li>
</ul>
<hr>
<h2 id="4-경계-태그푸터를-통한-직전-블록-메타데이터-즉각-조회-및-o1-상수-시간-양방향-병합-메커니즘">4. 경계 태그(푸터)를 통한 직전 블록 메타데이터 즉각 조회 및 O(1) 상수 시간 양방향 병합 메커니즘</h2>
<ul>
<li>블록을 <code>free</code>할 때 직전 블록의 가용 여부를 모르면 힙 시작점부터 선형 탐색해야 하므로 O(N)의 지연이 발생합니다.</li>
<li>각 블록의 끝에 4바이트 푸터(Footer)를 배치하면, 현재 블록 헤더 바로 앞 4바이트 번지수를 역참조하는 것만으로 직전 블록의 크기와 할당 여부를 즉시 파악할 수 있습니다.</li>
<li>이로써 블록의 개수에 상관없이 <strong>단 1회의 주소 참조만으로 이전 블록과 합치는 O(1) 상수 시간 양방향 병합</strong>이 완성됩니다.</li>
</ul>
<hr>
<h2 id="5-6주차-말록랩에서-7주차-핀토스pintos로의-연결-phys_base-가상-메모리-분할과-스택-동적-확장stack-growth">5. 6주차 말록랩에서 7주차 핀토스(Pintos)로의 연결: PHYS_BASE 가상 메모리 분할과 스택 동적 확장(Stack Growth)</h2>
<p>6주차 말록랩에서 학습한 가상 메모리 분할 개념은 7주차 핀토스 Project 3(Virtual Memory)의 핵심 구현 과제인 <strong>스택 동적 확장(Stack Growth)</strong>으로 자연스럽게 이어집니다.</p>
<h3 id="51-phys_base-가상-메모리-분할선">5.1 PHYS_BASE 가상 메모리 분할선</h3>
<ul>
<li>핀토스는 가상 주소를 <code>PHYS_BASE</code>를 경계로 양분합니다.</li>
<li><code>PHYS_BASE</code> 이상의 주소는 <strong>커널 가상 메모리</strong>로, 유저 프로세스가 접근하면 즉시 하드웨어 보호 위반(Page Fault)으로 프로세스를 강제 종료합니다.</li>
<li><code>PHYS_BASE</code> 미만의 주소는 <strong>유저 가상 메모리</strong>이며, 유저 프로세스의 코드, 데이터, 힙, 스택이 배치됩니다.</li>
</ul>
<h3 id="52-유저-스택-동적-확장-stack-growth-메커니즘">5.2 유저 스택 동적 확장 (Stack Growth) 메커니즘</h3>
<ol>
<li>프로그램 실행 초기에는 유저 스택에 4KB 단 1개의 페이지만 할당됩니다.</li>
<li>유저 함수 호출이 깊어지거나 지역 배열이 커지면 스택 포인터 <code>rsp</code>가 아래로 내려가면서 아직 물리 메모리가 할당되지 않은 페이지 번지수에 접근하게 됩니다.</li>
<li>CPU MMU는 즉시 14번 인터럽트인 <strong>페이지 폴트(Page Fault)</strong>를 발생시키고 실패 주소를 <code>fault_addr</code>에 기록합니다.</li>
<li>핀토스의 <code>page_fault</code> 핸들러는 이 접근이 정상적인 스택 확장인지 판정합니다:<ul>
<li>조건 1: <code>fault_addr &gt;= rsp - 32</code> (x86 PUSHA 명령어 오프셋 고려).</li>
<li>조건 2: <code>fault_addr &lt; PHYS_BASE</code> (유저 영역 내부).</li>
<li>조건 3: 현재 스택 총 크기가 최대 제한(보통 8MB) 이내.</li>
</ul>
</li>
<li>위 조건이 참이면 커널은 새 물리 페이지를 <code>palloc_get_page</code>로 즉시 할당하고 페이지 테이블에 매핑한 뒤, 유저 프로그램을 중단 지점부터 정상 재개시킵니다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C언어/시스템] C 포인터의 본질부터 7주차 핀토스(Pintos) 커널까지의 메모리 연결 원리]]></title>
            <link>https://velog.io/@jae_yun/C-%ED%8F%AC%EC%9D%B8%ED%84%B0%EC%9D%98-%EB%B3%B8%EC%A7%88%EB%B6%80%ED%84%B0-%ED%95%80%ED%86%A0%EC%8A%A4Pintos-%EC%BB%A4%EB%84%90%EA%B9%8C%EC%A7%80%EC%9D%98-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%9B%90%EB%A6%AC</link>
            <guid>https://velog.io/@jae_yun/C-%ED%8F%AC%EC%9D%B8%ED%84%B0%EC%9D%98-%EB%B3%B8%EC%A7%88%EB%B6%80%ED%84%B0-%ED%95%80%ED%86%A0%EC%8A%A4Pintos-%EC%BB%A4%EB%84%90%EA%B9%8C%EC%A7%80%EC%9D%98-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%9B%90%EB%A6%AC</guid>
            <pubDate>Tue, 06 Oct 2026 06:15:47 GMT</pubDate>
            <description><![CDATA[<h2 id="소주제">소주제</h2>
<ol>
<li>포인터 주소 연산(ALU/AGU)과 역참조(*p의 메모리 버스 전송 및 캐시 미스)의 하드웨어 유닛 동작 차이</li>
<li>4KB 단위 가상 메모리 페이징과 MMU 페이지 폴트(Page Fault) 예외 발생 및 처리 순서</li>
<li>핀토스 침투형 연결 리스트: struct list_elem과 offsetof 매크로를 이용한 부모 구조체 시작 주소 역산 공식</li>
<li>CPU 하드웨어 권한 링: 커널 모드(Ring 0)와 유저 모드(Ring 3) 및 시스템 콜 인터럽트 전이</li>
<li>핀토스(Pintos) 커널 적용: 시스템 콜 핸들러에서 유저가 넘긴 가상 주소 검증(is_user_vaddr, pagedir_get_page)과 커널 패닉 방지법</li>
</ol>
<hr>
<h2 id="1-포인터-주소-연산aluagu과-역참조p의-메모리-버스-전송-및-캐시-미스의-하드웨어-유닛-동작-차이">1. 포인터 주소 연산(ALU/AGU)과 역참조(*p의 메모리 버스 전송 및 캐시 미스)의 하드웨어 유닛 동작 차이</h2>
<p>C 코드에서 포인터를 다룰 때 CPU 내부에서는 서로 다른 하드웨어 유닛이 작동합니다.</p>
<ol>
<li><strong>포인터 주소 연산 (<code>p++</code>, <code>p + 4</code>)</strong>:<ul>
<li>실행 유닛: CPU 내부의 <strong>ALU(산술논리연산장치)</strong> 또는 <strong>AGU(주소생성장치)</strong>.</li>
<li>동작: RAM이나 메모리 버스로 신호를 일절 보내지 않고, CPU 내부 레지스터의 번지수 숫자만 덧셈/뺄셈하므로 <strong>1클럭 사이클 내에 즉시 완료</strong>됩니다 (<code>leaq</code> 또는 <code>addq</code> 명령어).</li>
</ul>
</li>
<li><strong>역참조 (<code>*p</code>)</strong>:<ul>
<li>실행 유닛: 메모리 컨트롤러(Memory Controller), L1/L2/L3 캐시, 시스템 버스.</li>
<li>동작: CPU가 주소 버스에 번지수 신호를 실어 보내어 RAM의 실제 데이터 비트를 읽어옵니다 (<code>movq</code> 명령어).</li>
<li>캐시 미스(Cache Miss)가 발생하면 물리 RAM 칩의 커패시터 전하를 감지해 올 때까지 CPU 파이프라인이 수십~수백 클록 동안 대기(Stall)합니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="2-4kb-단위-가상-메모리-페이징과-mmu-페이지-폴트page-fault-예외-발생-및-처리-순서">2. 4KB 단위 가상 메모리 페이징과 MMU 페이지 폴트(Page Fault) 예외 발생 및 처리 순서</h2>
<p>현대 x86 아키텍처는 가상 주소를 물리 RAM 주소로 변환하기 위해 <strong>4KB($2^{12} = 4096\text{바이트}$)</strong> 단위 페이징을 하드웨어로 강제합니다.</p>
<ol>
<li><strong>가상 주소 변환</strong>: CPU가 가상 주소로 메모리를 참조하면, 하드웨어 <strong>MMU(Memory Management Unit)</strong>가 페이지 테이블을 조회하여 물리 주소로 자동 변환합니다.</li>
<li><strong>페이지 폴트 발생 조건</strong>:<ul>
<li>가상 주소에 대응하는 페이지 테이블 엔트리의 &#39;존재(Present)&#39; 비트가 0인 경우 (물리 RAM에 미적재).</li>
<li>유저 모드(Ring 3)에서 커널 전용 페이지(Supervisor bit=1)에 접근을 시도한 경우.</li>
<li>읽기 전용(Read-only) 페이지에 쓰기(Write) 명령어를 수행한 경우.</li>
</ul>
</li>
<li><strong>CPU의 하드웨어 인터럽트 전이</strong>:<ul>
<li>CPU는 발생한 주소를 <code>%cr2</code> 레지스터에 기록하고, 14번 예외 인터럽트(Page Fault)를 발생시켜 커널 모드로 강제 전환한 뒤 운영체제의 페이지 폴트 핸들러를 실행합니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="3-핀토스-침투형-연결-리스트-struct-list_elem과-offsetof-매크로를-이용한-부모-구조체-시작-주소-역산-공식">3. 핀토스 침투형 연결 리스트: struct list_elem과 offsetof 매크로를 이용한 부모 구조체 시작 주소 역산 공식</h2>
<p>핀토스는 노드 메모리를 동적 할당(<code>malloc</code>)하지 않고 커널 오버헤드를 없애기 위해, 리눅스 커널과 동일한 <strong>침투형 연결 리스트(Intrusive Linked List)</strong>를 사용합니다.</p>
<pre><code class="language-c">struct thread {
    tid_t tid;
    struct list_elem elem;  // 구조체 내부에 삽입된 연결 요소 (16바이트 오프셋 위치)
    int priority;
};</code></pre>
<p>리스트를 순회할 때 커널이 알고 있는 것은 오직 내부에 박혀 있는 <code>elem</code>의 포인터 주소뿐입니다. 이때 부모 <code>struct thread</code>의 시작 주소를 찾아내는 매크로가 <strong><code>list_entry</code></strong>입니다.</p>
<pre><code class="language-c">#define list_entry(LIST_ELEM, STRUCT, MEMBER) \
    ((STRUCT *) ((uint8_t *)(LIST_ELEM) - offsetof(STRUCT, MEMBER)))</code></pre>
<h3 id="역산의-3단계-물리-과정">역산의 3단계 물리 과정</h3>
<ol>
<li><strong><code>offsetof(struct thread, elem)</code></strong>: 컴파일러가 구조체의 0번지 기준에서 멤버 <code>elem</code>이 물리적으로 몇 바이트 떨어져 있는지 상수 바이트 오프셋(예: 16)을 계산합니다.</li>
<li><strong><code>(uint8_t *)(LIST_ELEM) - 16</code></strong>: 보폭을 1바이트로 만들기 위해 <code>uint8_t *</code>로 캐스팅한 뒤, 정확히 16바이트를 뒤로 차감합니다.</li>
<li><strong><code>(struct thread *)</code></strong>: 차감되어 도출된 주소는 정확히 부모 구조체의 시작 번지수가 되므로, 이를 <code>struct thread *</code>로 타입 캐스팅하여 스레드 멤버 변수들에 접근합니다.</li>
</ol>
<hr>
<h2 id="4-cpu-하드웨어-권한-링-커널-모드ring-0와-유저-모드ring-3-및-시스템-콜-인터럽트-전이">4. CPU 하드웨어 권한 링: 커널 모드(Ring 0)와 유저 모드(Ring 3) 및 시스템 콜 인터럽트 전이</h2>
<ul>
<li><strong>Ring 0 (커널 모드)</strong>: 모든 하드웨어 I/O 포트, 인터럽트 제어 레지스터, 물리 메모리 전체에 무제한 접근할 수 있는 CPU 최고 권한 모드입니다.</li>
<li><strong>Ring 3 (유저 모드)</strong>: 일반 애플리케이션이 실행되는 제한 모드로, I/O 명령어나 커널 메모리 주소 접근 시 CPU 하드웨어 예외를 발생시킵니다.</li>
<li><strong>시스템 콜 메커니즘</strong>: 유저 프로세스가 파일 입출력이나 화면 출력을 수행하려면, CPU의 <code>syscall</code>(또는 <code>int 0x30</code>) 명령어를 실행하여 하드웨어 인터럽트를 유발하고, CPU 모드를 Ring 0으로 전환시켜 커널 핸들러로 진입해야 합니다.</li>
</ul>
<hr>
<h2 id="5-핀토스pintos-커널-적용-시스템-콜-핸들러에서-유저가-넘긴-가상-주소-검증is_user_vaddr-pagedir_get_page과-커널-패닉-방지법">5. 핀토스(Pintos) 커널 적용: 시스템 콜 핸들러에서 유저가 넘긴 가상 주소 검증(is_user_vaddr, pagedir_get_page)과 커널 패닉 방지법</h2>
<p>핀토스 Project 2(User Programs)의 시스템 콜 구현 시 가장 치명적인 취약점은 <strong>검증되지 않은 유저 포인터 역참조</strong>입니다.</p>
<h3 id="51-악의적이거나-잘못된-포인터의-위험성">5.1 악의적이거나 잘못된 포인터의 위험성</h3>
<p>유저 프로세스가 시스템 콜 인자로 <code>NULL</code>(0번지), 커널 가상 메모리 영역(<code>PHYS_BASE</code> 이상 번지), 또는 매핑되지 않은 미할당 주소를 넘겼을 때, 커널이 이를 검증 없이 <code>*ptr</code>로 역참조하면 <strong>커널 모드(Ring 0) 내부에서 페이지 폴트가 발생하여 운영체제 전체가 뻗는 커널 패닉(Kernel Panic)</strong>이 일어납니다.</p>
<h3 id="52-핀토스-3단계-물리-검증-함수-구현-userprogsyscallc">5.2 핀토스 3단계 물리 검증 함수 구현 (<code>userprog/syscall.c</code>)</h3>
<pre><code class="language-c">void check_address(const uint64_t *uaddr) {
    struct thread *curr = thread_current();

    // 1. 포인터 주소가 0(NULL)인지 검사
    if (uaddr == NULL)
        exit(-1);

    // 2. 주소가 유저 가상 주소 영역(PHYS_BASE 미만)인지 검사
    if (!is_user_vaddr(uaddr))
        exit(-1);

    // 3. 현재 스레드의 페이지 디렉토리에 실제 물리 메모리로 매핑되어 있는지 검사
    if (pagedir_get_page(curr-&gt;pagedir, uaddr) == NULL)
        exit(-1);
}</code></pre>
<ul>
<li>시스템 콜 핸들러는 유저가 넘긴 모든 포인터와 버퍼 메모리에 대해 위 3가지 하드웨어 조건을 사전에 통과한 경우에만 안전하게 역참조를 수행하고, 위반 시 프로세스를 즉시 종료(<code>exit(-1)</code>)시켜 커널을 보호합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CS:APP/말록랩] 명시적·암묵적 가용 리스트 할당기(malloc·free·sbrk) 동작 원리와 가용 블록 병합(coalesce)·분할(place) 메커니즘]]></title>
            <link>https://velog.io/@jae_yun/%EB%8F%99%EC%A0%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%ED%95%A0%EB%8B%B9</link>
            <guid>https://velog.io/@jae_yun/%EB%8F%99%EC%A0%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%ED%95%A0%EB%8B%B9</guid>
            <pubDate>Mon, 05 Oct 2026 14:49:47 GMT</pubDate>
            <description><![CDATA[<h2 id="소주제">소주제</h2>
<ol>
<li>동적 메모리 할당의 물리적 필요성: 런타임 크기 결정, 스택 프레임 수명 극복, 힙 경계 확장(sbrk)</li>
<li>가용 블록 탐색 3대 정책: First-fit, Next-fit, Best-fit의 처리 속도 및 외부 단편화 비교</li>
<li>가용 블록 병합(Coalescing) 4가지 물리 케이스와 시작 포인터 bp의 재조정 규칙</li>
<li>블록 배치 및 분할(Splitting, place): 최소 블록 크기(16바이트) 기준 내부 단편화 방지</li>
<li>핀토스(Pintos) 커널 적용: threads/malloc.c의 페이지 기반 아레나(Arena) 구조와 블록 디스크립터(Descriptor) 동작 메커니즘</li>
</ol>
<hr>
<h2 id="1-동적-메모리-할당의-물리적-필요성-런타임-크기-결정-스택-프레임-수명-극복-힙-경계-확장sbrk">1. 동적 메모리 할당의 물리적 필요성: 런타임 크기 결정, 스택 프레임 수명 극복, 힙 경계 확장(sbrk)</h2>
<h3 id="11-동적-할당이-필요한-하드웨어-이유">1.1 동적 할당이 필요한 하드웨어 이유</h3>
<ol>
<li><strong>스택 용량 한계 (Stack Overflow)</strong>: 프로세스의 스택 세그먼트는 보통 수 MB(Linux 기본 8MB)로 제한되어 있어 대용량 버퍼를 선언하면 스택 경계를 침범해 즉시 충돌합니다.</li>
<li><strong>함수 스택 프레임 수명(Lifetime) 한계</strong>: 함수 내부에서 선언된 지역 변수는 함수가 반환(<code>ret</code>)하는 즉시 스택 포인터가 원복되어 파괴되므로, 외부로 주소를 넘기면 댕글링 포인터가 발생합니다.</li>
<li><strong>런타임 크기 가변성</strong>: 사용자 입력이나 네트워크 패킷 크기는 컴파일 시점에 알 수 없으므로, 거대한 힙(Heap) 세그먼트에서 필요한 바이트를 동적으로 요청해야 합니다.</li>
</ol>
<h3 id="12-저수준-힙-확장-시스템-콜-sbrk">1.2 저수준 힙 확장 시스템 콜 sbrk</h3>
<pre><code class="language-c">void *sbrk(intptr_t incr);</code></pre>
<ul>
<li>힙 영역의 최상단 경계 주소를 <code>brk</code>(Program Break)라고 부릅니다.</li>
<li><code>sbrk(incr)</code>는 커널에 요청하여 <code>brk</code> 주소를 <code>incr</code> 바이트만큼 증가시키고, 확장되기 직전의 시작 주소를 반환합니다. 할당 실패 시 <code>(void *)-1</code>을 반환합니다.</li>
</ul>
<hr>
<h2 id="2-가용-블록-탐색-3대-정책-first-fit-next-fit-best-fit의-처리-속도-및-외부-단편화-비교">2. 가용 블록 탐색 3대 정책: First-fit, Next-fit, Best-fit의 처리 속도 및 외부 단편화 비교</h2>
<p>프로그래머가 <code>malloc(asize)</code>를 호출했을 때, 가용 블록 목록에서 요청 크기를 수용할 수 있는 빈 블록을 선택하는 정책 3가지입니다.</p>
<ol>
<li><strong>First-fit (최초 적합)</strong>:<ul>
<li>힙의 맨 처음(<code>heap_listp</code>)부터 순차적으로 탐색하여 <code>크기 &gt;= asize</code>인 첫 번째 빈 블록을 즉시 반환합니다.</li>
<li>장점: 힙 뒷부분에 거대한 가용 블록이 보존됩니다.</li>
<li>단점: 힙 앞쪽에 자잘한 자투리 블록들이 누적되어 탐색 시간이 갈수록 길어집니다.</li>
</ul>
</li>
<li><strong>Next-fit (다음 적합)</strong>:<ul>
<li>매번 처음부터 찾지 않고, 직전 탐색이 종료된 포인터 위치부터 이어서 탐색합니다.</li>
<li>장점: First-fit보다 탐색 속도가 빠릅니다.</li>
<li>단점: 힙 뒷부분의 거대한 빈 메모리 덩어리들을 빠르게 쪼개어 단편화를 가속시킵니다.</li>
</ul>
</li>
<li><strong>Best-fit (최적 적합)</strong>:<ul>
<li>힙 전체의 모든 가용 블록을 검사하여, <code>크기 &gt;= asize</code>를 만족하면서 남는 자투리 크기가 가장 작은 블록을 선택합니다.</li>
<li>장점: 자투리 크기를 최소화하여 메모리 활용도(외부 단편화 방지)가 가장 뛰어납니다.</li>
<li>단점: 매 할당마다 힙 전체를 끝까지 순회해야 하므로 $O(N)$ 시간 지연이 발생합니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="3-가용-블록-병합coalescing-4가지-물리-케이스와-시작-포인터-bp의-재조정-규칙">3. 가용 블록 병합(Coalescing) 4가지 물리 케이스와 시작 포인터 bp의 재조정 규칙</h2>
<p>메모리를 해제(<code>free</code>)할 때 인접한 빈 블록들을 하나로 합치지 않으면 거대한 연속 메모리를 할당할 수 없는 외부 단편화가 발생합니다.</p>
<pre><code class="language-c">static void *coalesce(void *bp)</code></pre>
<ol>
<li><strong>Case 1 (직전 블록: 할당 / 다음 블록: 할당)</strong>:<ul>
<li>인접 블록이 모두 사용 중이므로 병합 불가. <code>bp</code>를 그대로 반환.</li>
</ul>
</li>
<li><strong>Case 2 (직전 블록: 할당 / 다음 블록: 가용)</strong>:<ul>
<li>현재 블록 크기에 다음 블록 크기를 합산.</li>
<li>현재 블록의 헤더와 다음 블록의 푸터 크기를 합산값으로 갱신. <code>bp</code> 유지.</li>
</ul>
</li>
<li><strong>Case 3 (직전 블록: 가용 / 다음 블록: 할당)</strong>:<ul>
<li>직전 블록 크기에 현재 블록 크기를 합산.</li>
<li>직전 블록의 헤더와 현재 블록의 푸터 크기를 합산값으로 갱신.</li>
<li><strong>중요</strong>: 시작 포인터를 직전 블록의 시작 번지수로 이동 (<code>bp = PREV_BLKP(bp)</code>).</li>
</ul>
</li>
<li><strong>Case 4 (직전 블록: 가용 / 다음 블록: 가용)</strong>:<ul>
<li>직전 크기 + 현재 크기 + 다음 크기 3개를 합산.</li>
<li>직전 블록의 헤더와 다음 블록의 푸터 크기를 3개 합산값으로 갱신.</li>
<li>시작 포인터를 직전 블록 시작 번지수로 이동 (<code>bp = PREV_BLKP(bp)</code>).</li>
</ul>
</li>
</ol>
<hr>
<h2 id="4-블록-배치-및-분할splitting-place-최소-블록-크기16바이트-기준-내부-단편화-방지">4. 블록 배치 및 분할(Splitting, place): 최소 블록 크기(16바이트) 기준 내부 단편화 방지</h2>
<p>가용 블록을 찾았을 때, 요청 크기보다 블록이 훨씬 크다면 잉여 공간을 잘라내어 새 가용 블록으로 만들어야 합니다.</p>
<pre><code class="language-c">static void place(void *bp, size_t asize) {
    size_t csize = GET_SIZE(HDRP(bp));
    if ((csize - asize) &gt;= (2 * DSIZE)) { // 잉여 공간이 최소 블록 크기(16바이트) 이상인 경우
        // 앞부분은 할당 블록으로 세팅
        PUT(HDRP(bp), PACK(asize, 1));
        PUT(FTRP(bp), PACK(asize, 1));
        // 뒷부분은 쪼개서 새로운 가용 블록으로 등록
        bp = NEXT_BLKP(bp);
        PUT(HDRP(bp), PACK(csize - asize, 0));
        PUT(FTRP(bp), PACK(csize - asize, 0));
    } else {
        // 16바이트 미만 자투리는 쪼갤 수 없으므로 통째로 할당 (내부 단편화 허용)
        PUT(HDRP(bp), PACK(csize, 1));
        PUT(FTRP(bp), PACK(csize, 1));
    }
}</code></pre>
<hr>
<h2 id="5-핀토스pintos-커널-적용-threadsmallocc의-페이지-기반-아레나arena-구조와-블록-디스크립터descriptor-동작-메커니즘">5. 핀토스(Pintos) 커널 적용: threads/malloc.c의 페이지 기반 아레나(Arena) 구조와 블록 디스크립터(Descriptor) 동작 메커니즘</h2>
<p>핀토스는 CS:APP의 암묵적 가용 리스트보다 한 단계 더 진화한 <strong>아레나(Arena) &amp; 슬랩(Slab) 기반 디스크립터 할당기</strong>를 커널 내부에 탑재하고 있습니다.</p>
<h3 id="51-블록-디스크립터-block-descriptor">5.1 블록 디스크립터 (Block Descriptor)</h3>
<ul>
<li>핀토스 커널은 16, 32, 64, 128, 256, 512, 1024바이트 크기별로 디스크립터 구조체(<code>struct desc</code>)를 배열로 유지합니다.</li>
<li>각 디스크립터는 같은 크기의 메모리 블록들만 모아둔 페이지 목록(<code>free_list</code>)을 관리합니다.</li>
</ul>
<h3 id="52-아레나-구조-arena">5.2 아레나 구조 (Arena)</h3>
<ul>
<li>핀토스는 4KB 페이지를 할당받으면 페이지 맨 앞부분에 <strong>아레나 헤더(<code>struct arena</code>)</strong>를 심습니다.</li>
<li>아레나 헤더에는 해당 페이지가 몇 바이트 크기 블록들로 쪼개져 있는지, 남은 가용 블록 수는 몇 개인지 기록됩니다.</li>
<li><code>free(ptr)</code> 호출 시, 핀토스는 복잡한 탐색 없이 <code>ptr</code>의 하위 12비트를 잘라내어(<code>pg_round_down(ptr)</code>) 단 1번의 비트 연산으로 4KB 페이지 시작점의 아레나 헤더를 즉시 찾아내고 블록을 반환합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C언어/말록랩] 포인터 증감 연산자(*p++ vs (*p)--) 우선순위, 보폭(Stride) 공식과 1바이트 주소 캐스팅 원리]]></title>
            <link>https://velog.io/@jae_yun/C%EC%96%B8%EC%96%B4%EB%A7%90%EB%A1%9D%EB%9E%A9-%ED%8F%AC%EC%9D%B8%ED%84%B0-%EC%A6%9D%EA%B0%90-%EC%97%B0%EC%82%B0%EC%9E%90p-vs-p-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%EB%B3%B4%ED%8F%ADStride-%EA%B3%B5%EC%8B%9D%EA%B3%BC-1%EB%B0%94%EC%9D%B4%ED%8A%B8-%EC%A3%BC%EC%86%8C-%EC%BA%90%EC%8A%A4%ED%8C%85-%EC%9B%90%EB%A6%AC</link>
            <guid>https://velog.io/@jae_yun/C%EC%96%B8%EC%96%B4%EB%A7%90%EB%A1%9D%EB%9E%A9-%ED%8F%AC%EC%9D%B8%ED%84%B0-%EC%A6%9D%EA%B0%90-%EC%97%B0%EC%82%B0%EC%9E%90p-vs-p-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%EB%B3%B4%ED%8F%ADStride-%EA%B3%B5%EC%8B%9D%EA%B3%BC-1%EB%B0%94%EC%9D%B4%ED%8A%B8-%EC%A3%BC%EC%86%8C-%EC%BA%90%EC%8A%A4%ED%8C%85-%EC%9B%90%EB%A6%AC</guid>
            <pubDate>Mon, 05 Oct 2026 07:36:31 GMT</pubDate>
            <description><![CDATA[<h2 id="1-p---vs-p---vs-p---후위-증감-연산자가-역참조보다-우선-결합하여-발생하는-주소-이동과-값-변경의-차이">1. <em>p-- vs (</em>p)-- vs *(p--): 후위 증감 연산자가 역참조보다 우선 결합하여 발생하는 주소 이동과 값 변경의 차이</h2>
<p>포인터와 증감 연산자가 결합할 때, 괄호 유무에 따라 물리적으로 조작되는 대상이 &#39;주소 번지수&#39;인지 &#39;메모리 내부 값&#39;인지 완전히 갈립니다.</p>
<table>
<thead>
<tr>
<th align="left">표현식</th>
<th align="left">조작 대상</th>
<th align="left">주소(p)의 위치</th>
<th align="left">실행 순서 및 결과</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong><code>(*p)--</code></strong></td>
<td align="left"><strong>메모리 내부 값</strong></td>
<td align="left"><strong>제자리 유지</strong></td>
<td align="left">괄호로 역참조를 먼저 수행하여, 해당 주소에 들어있는 데이터를 읽어 값을 1 감소시킵니다. 포인터 주소는 전혀 변하지 않습니다.</td>
</tr>
<tr>
<td align="left"><strong><code>*p--</code></strong></td>
<td align="left"><strong>포인터 주소</strong></td>
<td align="left"><strong>이전 번지수로 이동</strong></td>
<td align="left">후위 감소 연산자(<code>--</code>)가 역참조(<code>*</code>)보다 우선 결합하여 <code>*(p--)</code>로 처리됩니다. 현재 주소의 값을 먼저 평가한 뒤, <strong>주소 자체가 이전 단위로 이동</strong>합니다.</td>
</tr>
<tr>
<td align="left"><strong><code>*(p--)</code></strong></td>
<td align="left"><strong>포인터 주소</strong></td>
<td align="left"><strong>이전 번지수로 이동</strong></td>
<td align="left"><code>*p--</code>에 괄호를 명시한 것으로, 동작이 100% 동일합니다.</td>
</tr>
</tbody></table>
<hr>
<h2 id="2-포인터-4대-증감-조합p-p-p-p의-평가-순서와-기계어-명령어-변환-흐름">2. 포인터 4대 증감 조합(<em>p++, *++p, (</em>p)++, ++*p)의 평가 순서와 기계어 명령어 변환 흐름</h2>
<p>포인터 변수 <code>p</code>가 가리키는 배열을 순회할 때 4가지 조합의 동작 흐름은 다음과 같습니다.</p>
<ol>
<li><strong><code>*p++</code></strong>: 현재 주소 <code>p</code>에 있는 값을 먼저 가져와 식 전체의 결과로 사용한 뒤, 포인터 주소를 다음 칸으로 증가시킵니다. (반복문 순회에서 표준적으로 사용).</li>
<li><strong><code>*++p</code></strong>: 주소 <code>p</code>를 먼저 다음 칸으로 증가시킨 뒤, 이동한 새 번지수의 값을 가져옵니다.</li>
<li><strong><code>(*p)++</code></strong>: 주소는 가만히 두고, 현재 주소에 저장된 메모리 데이터 숫자를 1 증가시킵니다.</li>
<li><strong><code>++*p</code></strong>: 전위 연산자와 역참조는 우선순위가 같아 우측에서 좌측으로 결합하므로 <code>++(*p)</code>가 됩니다. 현재 주소의 데이터 숫자를 먼저 1 증가시킨 뒤 그 값을 평가합니다.</li>
</ol>
<hr>
<h2 id="3-포인터-자료형-크기에-따른-물리적-이동-보폭stride-공식-정수--sizeofp">3. 포인터 자료형 크기에 따른 물리적 이동 보폭(Stride) 공식: 정수 * sizeof(*p)</h2>
<p>C 언어에서 포인터 덧셈·뺄셈은 1바이트 단위 연산이 아니며, 가리키는 자료형의 크기 단위로 이동합니다.</p>
<p>$$\text{실제 물리 이동 바이트 수} = \text{연산 정수} \times \text{sizeof(*포인터 타입)}$$</p>
<ul>
<li><strong><code>char *p</code></strong> ($1\text{바이트}$): <code>p + 1</code> 실행 시 물리 주소 <strong>1바이트</strong> 증가.</li>
<li><strong><code>int *p</code></strong> ($4\text{바이트}$): <code>p + 1</code> 실행 시 물리 주소 <strong>4바이트</strong> 증가.</li>
<li><strong><code>double *p</code></strong> ($8\text{바이트}$): <code>p + 1</code> 실행 시 물리 주소 <strong>8바이트</strong> 증가.</li>
<li><strong><code>struct thread *p</code></strong> ($512\text{바이트}$ 가정): <code>p + 1</code> 실행 시 물리 주소 <strong>512바이트</strong> 증가.</li>
</ul>
<p>컴파일러는 오직 포인터 선언문 앞에 명시된 자료형 크기만을 참조하여 CPU 주소 가산 명령어에 들어갈 즉시값을 결정합니다.</p>
<hr>
<h2 id="4-void-의-연산-불가-이유와-1바이트-단위-주소-연산을-위한-uint8_t--캐스팅-규칙">4. void *의 연산 불가 이유와 1바이트 단위 주소 연산을 위한 uint8_t * 캐스팅 규칙</h2>
<ul>
<li><code>void *</code>는 대상의 메모리 주소 번지수만 저장할 뿐, 해당 위치의 데이터 크기 정보가 0입니다.</li>
<li>따라서 <code>sizeof(*void_ptr)</code>을 계산할 수 없으므로, C 표준 규격상 <code>void_ptr + 1</code> 연산이나 역참조 <code>*void_ptr</code>은 컴파일 오류를 발생시킵니다.</li>
<li>메모리를 임의의 오프셋만큼 바이트 단위로 정확히 이동시키려면 반드시 <strong><code>(uint8_t *)</code></strong> 또는 <strong><code>(char *)</code></strong>로 명시적 형변환(Casting)을 거쳐 보폭을 1바이트로 고정해야 합니다.</li>
</ul>
<hr>
<h2 id="5-핀토스pintos-커널-적용-process_exec-유저-스택-인자-전달-시-하향-성장rsp---size과-16바이트-정렬-규약">5. 핀토스(Pintos) 커널 적용: process_exec 유저 스택 인자 전달 시 하향 성장(rsp -= size)과 16바이트 정렬 규약</h2>
<p>핀토스 Project 2(User Programs)의 핵심 과제인 <strong>인자 전달(Argument Passing)</strong> 구현 시 포인터 연산과 스택 규약이 직결됩니다.</p>
<h3 id="51-x86-64-유저-스택의-하향-성장-grow-down">5.1 x86-64 유저 스택의 하향 성장 (Grow Down)</h3>
<ul>
<li>x86-64 아키텍처에서 스택 포인터 레지스터 <code>%rsp</code>는 높은 주소(유저 영역 최상단 <code>USER_STACK</code>)에서 낮은 주소로 자라납니다.</li>
<li>데이터를 스택에 넣으려면 주소를 더하는 것이 아니라, 반드시 크기만큼 주소를 빼야(<code>if_rsp -= size</code>) 합니다.</li>
</ul>
<h3 id="52-인자-문자열-적재와-주소-포인터-배열-배치-순서">5.2 인자 문자열 적재와 주소 포인터 배열 배치 순서</h3>
<pre><code class="language-c">// 핀토스 인자 전달 스택 구축 4단계
// 1단계: 문자열 데이터 적재 (문자열 길이 + 널문자 1바이트만큼 rsp 차감)
for (int i = argc - 1; i &gt;= 0; i--) {
    if_rsp -= strlen(argv[i]) + 1;
    memcpy(if_rsp, argv[i], strlen(argv[i]) + 1);
    argv_addr[i] = if_rsp; // 적재된 물리 주소 기록
}

// 2단계: 8바이트 정렬 패딩 (라운드 다운)
while ((uintptr_t)if_rsp % 8 != 0) {
    if_rsp--;
    *(uint8_t *)if_rsp = 0;
}

// 3단계: argv[argc] NULL 포인터 (8바이트 0) 및 주소 배열 역순 푸시
if_rsp -= sizeof(char *);
*(char **)if_rsp = NULL;

for (int i = argc - 1; i &gt;= 0; i--) {
    if_rsp -= sizeof(char *);
    *(char **)if_rsp = argv_addr[i];
}

// 4단계: 16바이트 스택 정렬 (ABI 규약)
// 함수 진입 시점의 %rsp는 반드시 16의 배수여야 함</code></pre>
<ul>
<li>x86-64 System V ABI 규약에 따라 <code>call</code> 명령어가 실행되는 순간 스택 포인터는 16의 배수(<code>rsp % 16 == 0</code>)여야 하며, 이를 위반하면 유저 프로그램 진입 즉시 세그멘테이션 오류로 비정상 종료됩니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CS:APP/말록랩] 동적 메모리 할당기 힙 블록 구조(헤더, 푸터, 8바이트 정렬)와 Implicit Free List 비트 패킹 원리]]></title>
            <link>https://velog.io/@jae_yun/dd</link>
            <guid>https://velog.io/@jae_yun/dd</guid>
            <pubDate>Sun, 04 Oct 2026 16:58:44 GMT</pubDate>
            <description><![CDATA[<h2 id="1-8바이트16바이트-주소-정렬의-하드웨어-버스-전송-이유와-하위-3비트000의-가용-여부-플래그-활용법">1. 8바이트/16바이트 주소 정렬의 하드웨어 버스 전송 이유와 하위 3비트(000)의 가용 여부 플래그 활용법</h2>
<h3 id="11-하드웨어-정렬alignment의-물리적-이유">1.1 하드웨어 정렬(Alignment)의 물리적 이유</h3>
<p>CPU가 메모리 버스를 통해 RAM과 데이터를 주고받을 때, 데이터 버스는 64비트(8바이트) 폭으로 묶여 전송됩니다.</p>
<ul>
<li>만약 8바이트 정수가 8의 배수 번지수(예: 0x1000)에 위치하면, 메모리 컨트롤러는 단 1번의 버스 사이클로 데이터를 완벽히 읽어옵니다.</li>
<li>만약 주소가 8의 배수가 아니어서 두 개의 버스 블록에 걸쳐 있으면(Misaligned), CPU는 메모리 버스를 2번 작동시키고 비트를 이어 붙여야 하므로 성능이 반토막 납니다.</li>
</ul>
<h3 id="12-하위-3비트-비트-패킹bit-packing-트릭">1.2 하위 3비트 비트 패킹(Bit Packing) 트릭</h3>
<ul>
<li>8바이트 정렬 규칙에 따라, 모든 할당 블록의 크기는 반드시 8의 배수(8, 16, 24, 32...)가 됩니다.</li>
<li>2진수로 8의 배수는 최하위 3개 비트가 항상 <code>000</code>입니다 ($8 = 1000_{(2)}$, $16 = 10000_{(2)}$, $24 = 11000_{(2)}$).</li>
<li>이 하위 3비트는 블록 크기를 나타내는 데 전혀 쓰이지 않고 항상 0이므로, 할당기는 최하위 1개 비트를 <strong>할당 상태 플래그(1=사용 중, 0=가용 빈 블록)</strong>로 재활용합니다.</li>
</ul>
<hr>
<h2 id="2-4바이트-블록-헤더-설계-블록-크기와-할당-상태-비트1할당-0가용를-비트합으로-패킹하는-원리">2. 4바이트 블록 헤더 설계: 블록 크기와 할당 상태 비트(1=할당, 0=가용)를 비트합(|)으로 패킹하는 원리</h2>
<p>동적 메모리 할당기(Implicit Free List)의 핵심 메타데이터 매크로는 비트 연산으로 구현됩니다.</p>
<pre><code class="language-c">// 블록 크기(size)와 할당 여부(alloc)를 1개의 4바이트 워드로 결합
#define PACK(size, alloc)  ((size) | (alloc))

// 하위 3비트를 0으로 마스킹하여 순수 블록 크기(8의 배수) 추출 (~0x7 = ...11111000)
#define GET_SIZE(p)        (GET(p) &amp; ~0x7)

// 최하위 1비트만 마스킹하여 할당 여부(0 또는 1) 추출
#define GET_ALLOC(p)       (GET(p) &amp; 0x1)</code></pre>
<ul>
<li>예시: 24바이트 크기의 블록이 할당(1)된 경우<ul>
<li><code>PACK(24, 1)</code> = $00011000_{(2)} \mid 00000001_{(2)} = 00011001_{(2)}$ (10진수 25)</li>
<li>헤더에는 정수 25가 기록되지만, <code>GET_SIZE</code>를 거치면 하위 3비트가 잘려 순수 크기 24가 추출되고, <code>GET_ALLOC</code>을 거치면 1이 추출됩니다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="3-프롤로그-블록-에필로그-블록-정렬-패딩의-물리적-배치와-힙-경계-검사-제거-목적">3. 프롤로그 블록, 에필로그 블록, 정렬 패딩의 물리적 배치와 힙 경계 검사 제거 목적</h2>
<p>힙 초기화 함수 <code>mm_init</code>은 힙의 시작과 끝에 특수한 더미 블록들을 심어 경계 조건 예외 처리를 제거합니다.</p>
<pre><code class="language-text">[초기 힙 물리 배치 구조]
0x1000: [ 패딩 4B (0) ]           &lt;-- 8바이트 정렬을 맞추기 위한 여백
0x1004: [ 프롤로그 헤더 4B (8/1) ] &lt;-- 크기 8, 할당됨(1)
0x1008: [ 프롤로그 푸터 4B (8/1) ] &lt;-- 크기 8, 할당됨(1)  &lt;-- heap_listp 시작점
0x100C: [ 에필로그 헤더 4B (0/1) ] &lt;-- 크기 0, 할당됨(1)  &lt;-- 힙의 끝 표시 벽</code></pre>
<ul>
<li><strong>에필로그 블록 (크기 0, 할당됨 1)</strong>: 다음 블록을 순회하다가 크기가 0인 헤더를 만나는 즉시 힙의 끝임을 감지하고 순회를 안전하게 종료시킵니다.</li>
<li><strong>프롤로그 블록 (크기 8, 할당됨 1)</strong>: 맨 앞 블록이 이전 블록과 병합을 시도할 때, 물리 메모리 시작 번지수 이전으로 주소가 넘어가지 않도록 &#39;항상 할당된 벽&#39; 역할을 합니다.</li>
</ul>
<hr>
<h2 id="4-경계-태그푸터-footer가-이전-블록-역방향-탐색-및-o1-상수-시간-병합을-가능하게-하는-기계적-구조">4. 경계 태그(푸터, Footer)가 이전 블록 역방향 탐색 및 O(1) 상수 시간 병합을 가능하게 하는 기계적 구조</h2>
<ul>
<li><strong>다음 블록 주소 계산</strong>: 현재 페이로드 포인터 <code>bp</code>에서 내 헤더에 적힌 내 크기만큼 앞으로 더하면(<code>bp + GET_SIZE(HDRP(bp))</code>) 즉시 나옵니다.</li>
<li><strong>이전 블록 주소 계산의 한계</strong>: 내 바로 앞 블록의 크기를 모르면, 그 블록의 시작 번지수를 알 수 없어 힙 맨 처음부터 $O(N)$ 시간으로 순회해야 합니다.</li>
<li><strong>경계 태그(푸터)의 해결책</strong>: 각 블록의 마지막 4바이트에 헤더와 완전히 똑같은 크기/할당 정보를 복사해 둡니다.</li>
<li>내 헤더의 바로 4바이트 뒤(<code>bp - DSIZE</code>)를 읽으면 직전 블록의 푸터를 단 1회의 역참조로 즉시 읽을 수 있으며, 그 크기만큼 주소를 뒤로 빼면 이전 블록의 시작 위치를 $O(1)$ 상수 시간에 찾아내어 양방향 병합을 완료합니다.</li>
</ul>
<hr>
<h2 id="5-핀토스pintos-커널-적용-4kb-단위-물리-페이지-할당자palloc와-블록-기반-동적-할당자malloc의-2단계-계층-구조">5. 핀토스(Pintos) 커널 적용: 4KB 단위 물리 페이지 할당자(palloc)와 블록 기반 동적 할당자(malloc)의 2단계 계층 구조</h2>
<p>핀토스 커널(<code>threads/palloc.c</code>, <code>threads/malloc.c</code>)은 하드웨어 MMU 페이징과 소프트웨어 동적 할당을 2단계 계층으로 분리하여 관리합니다.</p>
<h3 id="51-1단계-페이지-할당자-palloc">5.1 1단계: 페이지 할당자 (<code>palloc</code>)</h3>
<ul>
<li>물리 RAM을 4KB($4096\text{바이트}$) 단위의 페이지 프레임으로 쪼개어 관리합니다.</li>
<li>비트맵(Bitmap) 자료구조를 사용하여, 각 비트 1개가 4KB 물리 메모리 1개의 사용 여부를 나타냅니다.</li>
<li>커널 전용 메모리 풀(Kernel Pool)과 유저 프로세스 전용 메모리 풀(User Pool)로 분리되어 독립적으로 할당됩니다.</li>
</ul>
<h3 id="52-2단계-블록-할당자-malloc">5.2 2단계: 블록 할당자 (<code>malloc</code>)</h3>
<ul>
<li>개발자가 <code>malloc(64)</code>처럼 작은 바이트를 요구할 때 매번 4KB 페이지를 줄 수 없으므로, <code>palloc</code>으로 4KB 페이지를 먼저 받아온 뒤 그 내부를 잘게 쪼개어 할당합니다.</li>
<li>CS:APP의 동적 할당 원리가 핀토스 커널의 기본 메모리 공급 엔진으로 작동합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C언어/말록랩] 연산자 우선순위에 따른 포인터 배열·배열 포인터 판독법과 힙 메모리 제어 포인터 연산 원리]]></title>
            <link>https://velog.io/@jae_yun/%ED%8F%AC%EC%9D%B8%ED%84%B0-%EB%BF%8C%EC%8B%A4%EA%B1%B0%EC%9E%84</link>
            <guid>https://velog.io/@jae_yun/%ED%8F%AC%EC%9D%B8%ED%84%B0-%EB%BF%8C%EC%8B%A4%EA%B1%B0%EC%9E%84</guid>
            <pubDate>Fri, 02 Oct 2026 06:53:44 GMT</pubDate>
            <description><![CDATA[<h2 id="1-c-언어-연산자-결합-우선순위-배열-첨자-와-함수-호출-가-역참조-보다-먼저-결합하는-하드웨어-규칙">1. C 언어 연산자 결합 우선순위: 배열 첨자 []와 함수 호출 ()가 역참조 *보다 먼저 결합하는 하드웨어 규칙</h2>
<p>C 언어 문법에서 기호의 결합 우선순위는 컴파일러가 변수의 정체(타입)를 결정하는 최우선 기준입니다.</p>
<ol>
<li><strong>최우선 결합 기호</strong>: 배열 첨자 <code>[]</code>와 함수 호출 소괄호 <code>()</code>는 우선순위가 1등급으로 가장 높습니다. 결합 방향은 왼쪽에서 오른쪽(Left-to-Right)입니다.</li>
<li><strong>후순위 결합 기호</strong>: 단항 역참조 연산자 <code>*</code>는 우선순위가 이보다 낮으며, 결합 방향은 오른쪽에서 왼쪽(Right-to-Left)입니다.</li>
<li><strong>타입 해석의 절대 원칙</strong>: <ul>
<li>변수 식별자 이름 바로 오른쪽에 <code>[]</code>나 <code>()</code>가 붙어 있다면, 그 변수는 무조건 <strong>배열</strong> 또는 <strong>함수</strong>로 먼저 판정됩니다.</li>
<li>변수 이름에 역참조 기호 <code>*</code>를 먼저 결합시켜 <strong>포인터</strong>로 선언하려면, 반드시 변수와 <code>*</code>를 소괄호 <code>(*변수명)</code>으로 감싸야 합니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="2-포인터-배열int-p5과-배열을-가리키는-포인터int-p5의-메모리-구조-및-크기바이트-차이">2. 포인터 배열(int <em>p[5])과 배열을 가리키는 포인터(int (</em>p)[5])의 메모리 구조 및 크기(바이트) 차이</h2>
<h3 id="21-포인터-배열-int-p5">2.1 포인터 배열: int *p[5]</h3>
<pre><code class="language-c">int *p[5];</code></pre>
<ul>
<li>식별자 <code>p</code> 오른쪽에 <code>[5]</code>가 먼저 결합합니다. 따라서 <code>p</code>는 <strong>원소 5개를 담는 연속된 배열</strong>입니다.</li>
<li>배열의 각 원소 타입은 <code>int *</code>, 즉 8바이트 메모리 주소입니다.</li>
<li><strong>물리적 메모리 크기</strong>: 64비트 시스템에서 8바이트 주소 5개가 연속 할당되므로 $5 \times 8 = 40\text{바이트}$를 차지합니다.</li>
</ul>
<h3 id="22-배열을-가리키는-포인터-int-p5">2.2 배열을 가리키는 포인터: int (*p)[5]</h3>
<pre><code class="language-c">int (*p)[5];</code></pre>
<ul>
<li>괄호에 의해 <code>(*p)</code>가 먼저 결합합니다. 따라서 <code>p</code>는 배열이 아니라 <strong>단 하나의 8바이트 포인터 변수</strong>입니다.</li>
<li><code>p</code>가 가리키는 대상은 &#39;정수 5개로 이루어진 int[5] 배열&#39;입니다.</li>
<li><strong>물리적 메모리 크기</strong>: <code>p</code> 자체는 메모리 주소 1개만을 저장하므로 시스템 규격상 정확히 <strong>8바이트</strong>만 차지합니다.</li>
<li><code>p + 1</code>을 실행했을 때 이동하는 메모리 보폭(Stride)은 단일 정수(4바이트)가 아니라, 가리키는 대상 전체 크기인 $5 \times 4 = 20\text{바이트}$만큼 물리 번지수가 증가합니다.</li>
</ul>
<hr>
<h2 id="3-이중-포인터-배열int-x5과-포인터-배열을-가리키는-포인터int-x5의-다차원-메모리-주소-해석법">3. 이중 포인터 배열(int *<em>x[5])과 포인터 배열을 가리키는 포인터(int *(</em>x)[5])의 다차원 메모리 주소 해석법</h2>
<h3 id="31-int-x5">3.1 int **x[5]</h3>
<ul>
<li><code>x</code>에 <code>[5]</code>가 먼저 결합하므로 <strong>크기 5인 배열</strong>입니다.</li>
<li>각 원소의 타입은 <code>int **</code>(이중 포인터)입니다.</li>
<li>즉, 정수의 주소를 담고 있는 8바이트 포인터 변수들의 번지수 5개를 일렬로 저장하는 40바이트 메모리 블록입니다.</li>
</ul>
<h3 id="32-int-x5">3.2 int <em>(</em>x)[5]</h3>
<ul>
<li><code>(*x)</code>가 먼저 결합하므로 <code>x</code>는 <strong>단 하나의 8바이트 포인터 변수</strong>입니다.</li>
<li><code>x</code>가 가리키는 대상은 &#39;8바이트 int* 포인터 5개로 이루어진 배열&#39;입니다.</li>
<li>역참조 <code>*x</code>를 수행하면 5개의 포인터를 담고 있는 배열의 시작 주소가 나오며, <code>x + 1</code> 연산 시의 이동 보폭은 $5 \times 8 = 40\text{바이트}$입니다.</li>
</ul>
<hr>
<h2 id="4-배열-원소의-주소-추출arr2과-2차원-배열-전체-크기-계산-공식">4. 배열 원소의 주소 추출(&amp;arr[2])과 2차원 배열 전체 크기 계산 공식</h2>
<h3 id="41-arr2의-물리적-타입">4.1 &amp;arr[2]의 물리적 타입</h3>
<pre><code class="language-c">int arr[5] = {10, 20, 30, 40, 50};
int *p = &amp;arr[2];</code></pre>
<ul>
<li><code>arr[2]</code>는 배열의 3번째 칸에 들어있는 4바이트 정수 값(30)을 의미합니다.</li>
<li>여기에 주소 연산자 <code>&amp;</code>를 붙이면 <code>&amp;arr[2]</code>는 해당 4바이트 메모리 공간의 물리 시작 번지수가 됩니다.</li>
<li>따라서 타입은 정확히 <code>int *</code>가 되며, <code>int *</code> 타입 포인터 변수에 직접 대입할 수 있습니다.</li>
</ul>
<h3 id="42-2차원-배열-전체-메모리-바이트-계산">4.2 2차원 배열 전체 메모리 바이트 계산</h3>
<pre><code class="language-c">int matrix[2][5];</code></pre>
<ul>
<li>물리적 크기 공식: $\text{sizeof(int)} \times \text{행 개수} \times \text{열 개수}$</li>
<li>$4\text{바이트} \times 2 \times 5 = 40\text{바이트}$ 연속 공간이 RAM에 할당됩니다.</li>
</ul>
<hr>
<h2 id="5-핀토스pintos-커널-적용-인터럽트-핸들러-테이블intr_handlers256과-시스템-콜-디스패치-배열의-함수-포인터-구조">5. 핀토스(Pintos) 커널 적용: 인터럽트 핸들러 테이블(intr_handlers[256])과 시스템 콜 디스패치 배열의 함수 포인터 구조</h2>
<p>핀토스 운영체제는 하드웨어 인터럽트와 유저 시스템 콜을 처리할 때 복합 포인터 문법을 핵심 자료구조로 사용합니다.</p>
<h3 id="51-인터럽트-핸들러-함수-포인터-배열-threadsinterruptc">5.1 인터럽트 핸들러 함수 포인터 배열 (<code>threads/interrupt.c</code>)</h3>
<pre><code class="language-c">typedef void intr_handler_func (struct intr_frame *);
static intr_handler_func *intr_handlers[256];</code></pre>
<ul>
<li><code>intr_handlers</code>는 크기 256인 <strong>포인터 배열</strong>입니다.</li>
<li>0번부터 255번까지 각 인터럽트 벡터 번호마다 실행할 함수의 메모리 진입점 주소(8바이트)를 보관합니다.</li>
<li>CPU가 0x20(타이머 인터럽트) 번호를 보내오면, 커널은 <code>intr_handlers[0x20](frame)</code> 코드를 통해 조건문 분기(if-else) 없이 해당 주소로 1클럭 내에 즉시 점프하여 핸들러를 호출합니다.</li>
</ul>
<h3 id="52-시스템-콜-핸들러-디스패치-userprogsyscallc">5.2 시스템 콜 핸들러 디스패치 (<code>userprog/syscall.c</code>)</h3>
<ul>
<li>유저 프로그램이 <code>SYS_WRITE</code>, <code>SYS_READ</code> 등을 호출할 때, 시스템 콜 번호를 인덱스로 사용하여 함수 포인터 테이블 <code>syscall_handlers[syscall_num](f)</code> 형태로 커널 진입점을 분기 실행합니다.</li>
<li>함수 포인터 배열의 선언과 우선순위를 정확히 파악해야 커널 세그멘테이션 폴트 없이 안전한 디스패처를 구축할 수 있습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CS:APP/x86-64] ABI 6개 레지스터 함수 인자 전달 규약, CPU 상태 플래그(ZF·SF·OF·CF)와 조건부 이동(cmov) 최적화 및 leaq 주소 연산 원리]]></title>
            <link>https://velog.io/@jae_yun/csappx86-64-abi-6%EA%B0%9C-%EB%A0%88%EC%A7%80%EC%8A%A4%ED%84%B0-%ED%95%A8%EC%88%98-%EC%9D%B8%EC%9E%90-%EC%A0%84%EB%8B%AC-%EA%B7%9C%EC%95%BD-cpu-%EC%83%81%ED%83%9C-%ED%94%8C%EB%9E%98%EA%B7%B8zfsfofcf%EC%99%80-%EC%A1%B0%EA%B1%B4%EB%B6%80-%EC%9D%B4%EB%8F%99cmov-%EC%B5%9C%EC%A0%81%ED%99%94-%EB%B0%8F-leaq-%EC%A3%BC%EC%86%8C</link>
            <guid>https://velog.io/@jae_yun/csappx86-64-abi-6%EA%B0%9C-%EB%A0%88%EC%A7%80%EC%8A%A4%ED%84%B0-%ED%95%A8%EC%88%98-%EC%9D%B8%EC%9E%90-%EC%A0%84%EB%8B%AC-%EA%B7%9C%EC%95%BD-cpu-%EC%83%81%ED%83%9C-%ED%94%8C%EB%9E%98%EA%B7%B8zfsfofcf%EC%99%80-%EC%A1%B0%EA%B1%B4%EB%B6%80-%EC%9D%B4%EB%8F%99cmov-%EC%B5%9C%EC%A0%81%ED%99%94-%EB%B0%8F-leaq-%EC%A3%BC%EC%86%8C</guid>
            <pubDate>Thu, 01 Oct 2026 15:10:10 GMT</pubDate>
            <description><![CDATA[<h2 id="1-x86-64-함수-호출-규약abi-6개-전용-레지스터-인자-전달과-7번째-이상-매개변수의-스택-메모리-푸시-순서">1. x86-64 함수 호출 규약(ABI): 6개 전용 레지스터 인자 전달과 7번째 이상 매개변수의 스택 메모리 푸시 순서</h2>
<p>C언어에서 함수를 호출할 때 전달하는 매개변수들은 x86-64 하드웨어 아키텍처 규약(System V AMD64 ABI)에 의해 정해진 위치에 배치된다.</p>
<h3 id="11-1번째부터-6번째-인자-전용-하드웨어-레지스터-사용">1.1 1번째부터 6번째 인자: 전용 하드웨어 레지스터 사용</h3>
<p>CPU 내부의 레지스터는 RAM보다 약 100배 이상 빠르게 접근할 수 있다. 따라서 x86-64는 최대 6개까지의 정수 및 포인터 매개변수를 RAM에 쓰지 않고 다음 6개의 레지스터에 순서대로 복사하여 전달한다.</p>
<ol>
<li>첫 번째 매개변수: <code>%rdi</code></li>
<li>두 번째 매개변수: <code>%rsi</code></li>
<li>세 번째 매개변수: <code>%rdx</code></li>
<li>네 번째 매개변수: <code>%rcx</code></li>
<li>다섯 번째 매개변수: <code>%r8</code></li>
<li>여섯 번째 매개변수: <code>%r9</code></li>
</ol>
<h3 id="12-7번째-이후-매개변수-스택stack-메모리-사용">1.2 7번째 이후 매개변수: 스택(Stack) 메모리 사용</h3>
<p>만약 함수의 매개변수가 7개 이상이면 레지스터 공간이 부족하므로, 7번째 매개변수부터는 스택 메모리를 사용한다.</p>
<ul>
<li>호출하는 함수는 7번째 이상의 인자들을 <strong>가장 마지막 인자부터 역순으로 스택 메모리에 push</strong>한다.</li>
<li>스택 포인터 레지스터인 <code>%rsp</code>는 인자가 푸시될 때마다 8바이트씩 주소가 감소하며, 피호출 함수는 <code>%rsp + 8</code>, <code>%rsp + 16</code> 등의 오프셋 주소를 통해 스택 메모리에서 7번째 이후의 인자 값을 읽어간다.</li>
</ul>
<hr>
<h2 id="2-call과-ret-명령어의-하드웨어-동작-스택에-8바이트-복귀-주소rip-자동-저장과-복원-메커니즘">2. call과 ret 명령어의 하드웨어 동작: 스택에 8바이트 복귀 주소(RIP) 자동 저장과 복원 메커니즘</h2>
<p>함수 호출은 단순히 다음 줄로 점프하는 것이 아니며, 실행이 끝난 뒤 원래 자리로 되돌아올 물리 주소를 반드시 보존해야 한다.</p>
<pre><code class="language-text">[호출 직전 메모리 상태]
코드 영역:
0x401000: callq 0x402000   &lt;-- 함수 호출 명령 (크기: 5바이트)
0x401005: movq %rax, %rbx  &lt;-- 함수 종료 후 복귀해야 할 다음 명령어 위치</code></pre>
<h3 id="21-callq-명령어의-2단계-하드웨어-동작">2.1 callq 명령어의 2단계 하드웨어 동작</h3>
<ol>
<li>CPU는 현재 실행 중인 <code>callq</code> 명령어 바로 다음 줄의 메모리 주소(위 예시에서 0x401005)를 계산한다.</li>
<li>스택 포인터 레지스터 <code>%rsp</code>의 값을 8 감소시키고, 스택 메모리의 그 번지수에 계산한 <strong>복귀 주소 0x401005 8바이트를 복사(Push)</strong>한다.</li>
<li>명령어 포인터 레지스터 <code>%rip</code>에 목표 함수의 시작 주소 0x402000을 대입하여 실행 흐름을 점프시킨다.</li>
</ol>
<h3 id="22-retq-명령어의-2단계-하드웨어-동작">2.2 retq 명령어의 2단계 하드웨어 동작</h3>
<ol>
<li>피호출 함수의 실행이 모두 끝나고 <code>retq</code> 명령어를 만나면, CPU는 현재 스택 포인터 <code>%rsp</code>가 가리키고 있는 메모리 최상단에서 8바이트 데이터(0x401005)를 꺼낸다(Pop).</li>
<li><code>%rsp</code> 값을 다시 8 증가시켜 스택을 원복한다.</li>
<li>꺼내온 0x401005 번지수를 <code>%rip</code> 레지스터에 즉시 대입한다.</li>
<li>CPU는 호출 직후의 다음 명령어(0x401005)부터 다시 정상 실행을 이어간다.</li>
</ol>
<hr>
<h2 id="3-산술-비교-연산cmptest과-cpu-4대-상태-플래그zf-sf-of-cf의-비트-세팅-기준">3. 산술 비교 연산(cmp/test)과 CPU 4대 상태 플래그(ZF, SF, OF, CF)의 비트 세팅 기준</h2>
<p>x86-64 CPU 내부에는 ALU의 연산 결과 상태를 1비트 단위로 기록하는 FLAGS 레지스터(상태 플래그)가 존재한다.</p>
<h3 id="31-cmp-명령어와-test-명령어의-하드웨어-동작-차이">3.1 cmp 명령어와 test 명령어의 하드웨어 동작 차이</h3>
<ul>
<li><code>cmp S2, S1</code>: 내부적으로 <code>S1 - S2</code> 뺄셈 연산을 수행한다. 단, 뺄셈한 결과값은 어디에도 저장하지 않고 버리며, <strong>오직 4대 상태 플래그의 비트만 세팅</strong>한다.</li>
<li><code>test S2, S1</code>: 내부적으로 <code>S1 &amp; S2</code> 비트 단위 AND 연산을 수행한다. 결과값은 버리고 플래그 비트만 세팅한다. (보통 어떤 변수가 0인지 음수인지 확인할 때 <code>testq %rax, %rax</code> 형태로 사용).</li>
</ul>
<h3 id="32-4대-핵심-상태-플래그의-비트-판정-조건">3.2 4대 핵심 상태 플래그의 비트 판정 조건</h3>
<ol>
<li><strong>ZF (Zero Flag - 0 플래그)</strong>:<ul>
<li>연산 결과가 정확히 숫자 0이면 1로 세팅되고, 0이 아니면 0이 된다.</li>
<li>예: <code>cmp %rax, %rbx</code>에서 두 값이 같으면 뺀 결과가 0이므로 <code>ZF = 1</code>이 된다. 조건부 점프 <code>je</code>(Jump if Equal)는 <code>ZF == 1</code>일 때 분기한다.</li>
</ul>
</li>
<li><strong>SF (Sign Flag - 부호 플래그)</strong>:<ul>
<li>연산 결과의 최상위 비트(MSB, 부호 비트)가 1이면 1로 세팅된다. 즉, 결과가 음수이면 <code>SF = 1</code>, 양수이면 0이다.</li>
</ul>
</li>
<li><strong>OF (Overflow Flag - 부호 오버플로우 플래그)</strong>:<ul>
<li>2의 보수 부호 있는 정수 연산에서, 양수와 양수를 더했는데 음수가 되거나 음수와 음수를 더했는데 양수가 되어 표현 범위를 초과했을 때 1로 세팅된다.</li>
</ul>
</li>
<li><strong>CF (Carry Flag - 올림/빌림 플래그)</strong>:<ul>
<li>부호 없는 정수(Unsigned) 연산에서, 최상위 비트 밖으로 자리올림(Carry)이 발생했거나 뺄셈 시 빌림(Borrow)이 발생했을 때 1로 세팅된다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="4-조건부-점프jcc의-분기-예측-실패-패널티와-이를-제거하는-조건부-이동cmov-최적화-메커니즘">4. 조건부 점프(jcc)의 분기 예측 실패 패널티와 이를 제거하는 조건부 이동(cmov) 최적화 메커니즘</h2>
<h3 id="41-cpu-파이프라인과-분기-예측branch-prediction">4.1 CPU 파이프라인과 분기 예측(Branch Prediction)</h3>
<p>현대 CPU는 속도를 높이기 위해 하나의 명령어가 끝나기 전에 다음 명령어 수십 개를 미리 읽어와 해석하는 명령어 파이프라인(Pipeline) 구조를 사용한다.</p>
<ul>
<li><code>if (x &gt; 0)</code> 같은 코드가 어셈블리로 변환되면 <code>jle</code>(작거나 같으면 점프) 같은 조건부 점프 명령어가 된다.</li>
<li>CPU는 조건 연산이 완전히 끝나기 전에, 분기가 점프할 것인지 아니면 다음 줄로 갈 것인지를 미리 추측(Branch Prediction)하여 해당 방향의 명령어들을 파이프라인에 미리 채워 넣는다.</li>
<li>만약 이 추측이 빗나가면(Branch Misprediction), 파이프라인에 미리 적재해 둔 수십 개의 명령어들을 <strong>전부 폐기(Pipeline Flush)</strong>하고 처음부터 다시 읽어와야 한다. 이때 약 15~30 클럭 사이클의 막대한 시간 지연(Penalty)이 발생한다.</li>
</ul>
<h3 id="42-조건부-이동cmov의-하드웨어-최적화-원리">4.2 조건부 이동(cmov)의 하드웨어 최적화 원리</h3>
<p>컴파일러는 이러한 패널티를 제거하기 위해 분기 점프(<code>jcc</code>) 대신 <strong>조건부 이동(cmov)</strong> 명령어로 코드를 최적화한다.</p>
<pre><code class="language-c">// C 코드
int result = (a &gt; b) ? val1 : val2;</code></pre>
<pre><code class="language-text">[조건부 점프 jcc 방식 - 분기 예측 실패 위험]
    cmpq %rsi, %rdi
    jle .L_ELSE        &lt;-- 분기 예측 실패 시 클럭 손실 발생
    movq %rdx, %rax
    ret
.L_ELSE:
    movq %rcx, %rax
    ret

[조건부 이동 cmov 방식 - 분기 자체가 없음]
    cmpq %rsi, %rdi
    movq %rcx, %rax    &lt;-- 먼저 val2를 rax에 대입해 둠
    cmovg %rdx, %rax   &lt;-- 플래그 조건(a &gt; b)이 참일 때만 rax를 val1로 덮어씀 (분기 점프 없음)
    ret</code></pre>
<ul>
<li>cmov 방식은 점프 명령어 자체가 없으므로 실행 흐름이 갈라지지 않고 일직선으로 진행된다.</li>
<li>CPU 파이프라인이 중단되거나 비워질 일이 물리적으로 전혀 발생하지 않으므로, 예측 불가능한 조건문에서도 일정한 고속 실행 성능을 보장한다.</li>
</ul>
<hr>
<h2 id="5-leaqload-effective-address와-movq-명령어의-본질적-차이-메모리-버스-접근-유무와-alu-산술-연산">5. leaq(Load Effective Address)와 movq 명령어의 본질적 차이: 메모리 버스 접근 유무와 ALU 산술 연산</h2>
<p><code>leaq</code>와 <code>movq</code>는 둘 다 괄호 문법(<code>D(Rb, Ri, S)</code>)을 사용하지만, 하드웨어 내부에서 동작하는 물리적 경로는 완전히 다르다.</p>
<h3 id="51-주소-계산-공식">5.1 주소 계산 공식</h3>
<p>두 명령어 모두 괄호 안에 적힌 유효 주소를 다음과 같은 수학 공식으로 계산한다:
<code>유효 주소 = Base(Rb) + Index(Ri) * Scale(S) + Displacement(D)</code>
(여기서 Scale은 1, 2, 4, 8 중 하나).</p>
<h3 id="52-movq-명령어의-물리적-동작-메모리-버스-데이터-역참조">5.2 movq 명령어의 물리적 동작: 메모리 버스 데이터 역참조</h3>
<pre><code class="language-assembly">movq 8(%rbx, %rcx, 4), %rax</code></pre>
<ol>
<li>CPU가 <code>%rbx + %rcx * 4 + 8</code>을 연산하여 주소 번지수(예: 0x2038)를 도출한다.</li>
<li>CPU는 이 0x2038 번지수를 <strong>주소 버스(Address Bus)에 실어 RAM 메모리로 전송</strong>한다.</li>
<li>RAM은 해당 0x2038 번지수에 저장되어 있는 실제 데이터 8바이트를 꺼내 <strong>데이터 버스(Data Bus)를 통해 CPU로 반환</strong>한다.</li>
<li>CPU는 반환받은 데이터를 <code>%rax</code> 레지스터에 저장한다. 즉, <strong>실제 메모리 읽기(역참조)가 발생</strong>한다.</li>
</ol>
<h3 id="53-leaq-명령어의-물리적-동작-메모리-접근이-없는-순수-alu-고속-연산">5.3 leaq 명령어의 물리적 동작: 메모리 접근이 없는 순수 ALU 고속 연산</h3>
<pre><code class="language-assembly">leaq 8(%rbx, %rcx, 4), %rax</code></pre>
<ol>
<li>CPU 내부의 연산 장치(ALU)가 <code>%rbx + %rcx * 4 + 8</code>을 계산하여 결과 숫자 0x2038을 도출한다.</li>
<li><strong>주소 버스나 데이터 버스로 신호를 일절 보내지 않으며, RAM 메모리를 전혀 참조하지 않는다.</strong></li>
<li>계산된 숫자 결과인 0x2038 그 자체를 레지스터 <code>%rax</code>에 즉시 기록한다.</li>
<li>용도: 포인터의 실제 메모리 번지수 자체를 얻고자 할 때 사용될 뿐만 아니라, <code>x * 5 + 7</code>과 같이 덧셈과 곱셈이 결합된 일반 정수 산술 연산을 단 1개의 클럭 사이클로 초고속 처리할 때 컴파일러에 의해 광범위하게 활용된다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C언어/디버깅] 동적 메모리 오류(Double Free, realloc 실패, NULL 역참조)의 물리적 발생 원인과 GDB 0xCC 중단점 실행 추적 원리]]></title>
            <link>https://velog.io/@jae_yun/c%EC%96%B8%EC%96%B4%EB%94%94%EB%B2%84%EA%B9%85-%EB%8F%99%EC%A0%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%98%A4%EB%A5%98double-free-realloc-%EC%8B%A4%ED%8C%A8-null-%EC%97%AD%EC%B0%B8%EC%A1%B0%EC%9D%98-%EB%AC%BC%EB%A6%AC%EC%A0%81-%EB%B0%9C%EC%83%9D-%EC%9B%90%EC%9D%B8%EA%B3%BC-gdb-0xcc-%EC%A4%91%EB%8B%A8%EC%A0%90-%EC%8B%A4%ED%96%89-%EC%B6%94%EC%A0%81-%EC%9B%90</link>
            <guid>https://velog.io/@jae_yun/c%EC%96%B8%EC%96%B4%EB%94%94%EB%B2%84%EA%B9%85-%EB%8F%99%EC%A0%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%98%A4%EB%A5%98double-free-realloc-%EC%8B%A4%ED%8C%A8-null-%EC%97%AD%EC%B0%B8%EC%A1%B0%EC%9D%98-%EB%AC%BC%EB%A6%AC%EC%A0%81-%EB%B0%9C%EC%83%9D-%EC%9B%90%EC%9D%B8%EA%B3%BC-gdb-0xcc-%EC%A4%91%EB%8B%A8%EC%A0%90-%EC%8B%A4%ED%96%89-%EC%B6%94%EC%A0%81-%EC%9B%90</guid>
            <pubDate>Thu, 01 Oct 2026 15:10:08 GMT</pubDate>
            <description><![CDATA[<h2 id="1-포인터-에일리어싱aliasing과-힙-메타데이터를-파괴하는-더블-프리double-free의-하드웨어-원인">1. 포인터 에일리어싱(Aliasing)과 힙 메타데이터를 파괴하는 더블 프리(Double Free)의 하드웨어 원인</h2>
<h3 id="11-포인터-에일리어싱의-물리적-상태">1.1 포인터 에일리어싱의 물리적 상태</h3>
<p>C언어에서 두 개의 포인터 변수 p와 q를 선언하고 다음과 같이 코드를 작성할 수 있다.</p>
<pre><code class="language-c">int *p = (int *)malloc(sizeof(int)); // 힙 주소 0x5000 할당
int *q = p;                          // q에 p가 가진 주소 숫자 0x5000을 복사</code></pre>
<ul>
<li>스택 메모리에 위치한 포인터 변수 p는 8바이트 공간 안에 0x5000이라는 번지수를 숫자로 저장한다.</li>
<li>포인터 변수 q 역시 8바이트 공간 안에 똑같은 0x5000이라는 번지수를 숫자로 저장한다.</li>
<li>이처럼 서로 다른 두 개 이상의 포인터 변수가 메모리 상의 동일한 물리 주소(0x5000)를 가리키고 있는 상태를 <strong>에일리어싱(Aliasing)</strong>이라고 부른다.</li>
</ul>
<h3 id="12-free-호출-시-힙-관리자의-동작과-double-free-충돌">1.2 free() 호출 시 힙 관리자의 동작과 Double Free 충돌</h3>
<p>운영체제의 힙 관리자는 메모리를 할당할 때 요청받은 4바이트 직전 위치(예: 0x4FF8~0x4FFF)에 <strong>메타데이터(할당된 크기, 사용 중 여부를 나타내는 플래그 비트)</strong>를 기록해 둔다.</p>
<ol>
<li><code>free(p);</code> 실행:<ul>
<li>힙 관리자는 p에 들어있는 주소 0x5000의 메타데이터 위치로 이동하여, &#39;사용 중&#39; 비트를 &#39;해제됨(0)&#39;으로 변경하고 가용 블록 목록(Free List)에 해당 주소를 등록한다.</li>
<li>이때 포인터 변수 p와 q 내부의 8바이트 주소값 0x5000은 자동으로 0으로 바뀌지 않고 그대로 유지된다.</li>
</ul>
</li>
<li><code>free(q);</code> 재실행:<ul>
<li>힙 관리자는 q에 들어있는 주소 0x5000의 메타데이터를 다시 확인한다.</li>
<li>이미 &#39;해제됨&#39;으로 표시되어 가용 목록에 들어가 있는 블록을 다시 가용 목록에 등록하려고 시도하게 된다.</li>
<li>이 과정에서 힙 관리 자료구조(연결 링크)가 꼬이거나 메모리가 오염되는 <strong>Heap Corruption</strong>이 감지된다.</li>
<li>운영체제는 메모리 보안 공격 및 파괴를 방지하기 위해 프로그램에 SIGABRT 신호를 보내 강제 종료(Crash)시킨다. 이것이 <strong>더블 프리(Double Free)</strong> 오류의 물리적 원인이다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="2-realloc-메모리-재할당-시-발생하는-2가지-물리적-동작-경로와-메모리-누수-방지-패턴">2. realloc() 메모리 재할당 시 발생하는 2가지 물리적 동작 경로와 메모리 누수 방지 패턴</h2>
<p>동적 배열의 크기를 늘릴 때 사용하는 realloc(ptr, new_size) 함수는 하드웨어 메모리 상태에 따라 서로 다른 두 가지 동작 경로를 수행한다.</p>
<h3 id="21-경로-a-제자리-확장-in-place-expansion">2.1 경로 A: 제자리 확장 (In-place Expansion)</h3>
<ul>
<li>기존에 할당된 힙 블록 0x5000의 바로 뒤 인접 번지수(예: 0x5010 이후)가 아직 다른 변수에 할당되지 않고 비어 있는 경우.</li>
<li>힙 관리자는 데이터를 다른 곳으로 옮기지 않고, 메타데이터에 기록된 블록 크기 숫자만 기존 크기에서 new_size로 수정한 뒤 원래 주소인 0x5000을 그대로 반환한다.</li>
</ul>
<h3 id="22-경로-b-새-주소-할당-및-바이트-복사-relocation">2.2 경로 B: 새 주소 할당 및 바이트 복사 (Relocation)</h3>
<ul>
<li>기존 블록 0x5000의 바로 뒤 인접 번지수가 이미 다른 malloc에 의해 사용 중인 경우, 연속된 공간을 확보할 수 없다.</li>
<li>힙 관리자는 1) 힙의 완전히 다른 위치(예: 0x9000)에 new_size만큼의 연속 바이트를 새로 할당한다.</li>
<li>2) CPU 메모리 복사 명령을 통해 기존 0x5000에 있던 데이터를 0x9000으로 1바이트씩 전부 복사한다.</li>
<li>3) 이전 주소인 0x5000 블록에 대해 내부적으로 free(0x5000)을 수행하여 운영체제에 반납한다.</li>
<li>4) 새로 할당된 메모리의 시작 주소인 0x9000을 반환한다.</li>
</ul>
<h3 id="23-realloc-실패-시-메모리-누수memory-leak-발생-메커니즘">2.3 realloc 실패 시 메모리 누수(Memory Leak) 발생 메커니즘</h3>
<p>힙 메모리가 부족하여 new_size만큼의 공간을 확보하지 못하면 realloc()은 실패하고 숫자 0(NULL)을 반환한다. 이때 다음과 같이 코드를 작성하면 문제가 발생한다.</p>
<pre><code class="language-c">p = (int *)realloc(p, new_size); // 위험한 코드</code></pre>
<ul>
<li>만약 재할당에 실패하여 NULL이 반환되면, 변수 p에 들어있던 기존 유효 주소 0x5000이 지워지고 0이 대입된다.</li>
<li>하지만 메모리 0x5000 번지에 있던 기존 데이터는 해제되지 않고 힙에 그대로 남아 있다.</li>
<li>개발자는 0x5000이라는 번지수를 잃어버렸기 때문에 해당 메모리에 접근할 수도 없고 free()를 호출할 수도 없게 되어, 프로그램이 종료될 때까지 메모리를 낭비하는 <strong>메모리 누수(Memory Leak)</strong>가 확정된다.</li>
<li>올바른 해결법: 임시 포인터 <code>tmp = realloc(p, new_size)</code>에 먼저 결과를 받고, <code>tmp != NULL</code>임을 검증한 뒤에만 <code>p = tmp</code>로 갱신해야 한다.</li>
</ul>
<hr>
<h2 id="3-null-포인터0x0-번지-역참조와-mmu-가상-메모리-보호-오류페이지-폴트-메커니즘">3. NULL 포인터(0x0 번지) 역참조와 MMU 가상 메모리 보호 오류(페이지 폴트) 메커니즘</h2>
<h3 id="31-null-포인터의-물리적-정의">3.1 NULL 포인터의 물리적 정의</h3>
<p>C언어에서 NULL은 64비트 시스템 기준으로 <strong>64개의 비트가 전부 0으로 채워진 8바이트 정수 숫자 0(0x0000000000000000)</strong>을 의미한다.</p>
<pre><code class="language-c">int *p = NULL; // p 변수에 숫자 0을 저장
*p = 10;       // 0번지 메모리에 정수 10을 쓰려고 시도</code></pre>
<h3 id="32-하드웨어-mmu의-보호-메커니즘">3.2 하드웨어 MMU의 보호 메커니즘</h3>
<ol>
<li>프로그램이 사용하는 모든 주소는 가상 메모리 주소(Virtual Address)이며, CPU 내부의 하드웨어 장치인 MMU(Memory Management Unit)가 페이지 테이블(Page Table)을 참조하여 실제 물리 RAM 주소로 변환한다.</li>
<li>현대 운영체제(Linux, Windows 등)는 가상 주소 0x0을 포함하는 최하위 메모리 페이지(보통 0x0000 ~ 0x0FFF, 4KB 공간)를 물리 RAM에 전혀 매핑하지 않고 접근 불가(Inaccessible) 보호 구역으로 설정해 둔다.</li>
<li>CPU가 <code>*p = 10</code> 명령어를 실행하여 주소 버스에 0x0 번지수를 올리면, MMU는 페이지 테이블을 확인하고 유효하지 않은 매핑임을 즉시 감지한다.</li>
<li>MMU는 하드웨어 예외 신호인 페이지 폴트(Page Fault)를 발생시키고 CPU 실행을 중단한다.</li>
<li>운영체제 커널의 페이지 폴트 핸들러는 이 접근이 허용되지 않은 보호 구역 침범임을 확인하고, 해당 프로세스에 SIGSEGV(Segmentation Fault) 신호를 전달하여 프로그램을 강제 종료시킨다.</li>
</ol>
<hr>
<h2 id="4-gdb-디버거가-실행-중인-프로그램을-멈추는-원리-기계어-명령어의-0xccint-3-인터럽트-치환과-레지스터-조회">4. GDB 디버거가 실행 중인 프로그램을 멈추는 원리: 기계어 명령어의 0xCC(int 3) 인터럽트 치환과 레지스터 조회</h2>
<p>디버깅은 감으로 코드를 추측하는 것이 아니라, 실행 중인 CPU 레지스터와 RAM의 실제 바이트 상태를 관측하는 과정이다. GDB는 이를 하드웨어 인터럽트를 통해 수행한다.</p>
<h3 id="41-중단점breakpoint의-물리적-동작-0xcc-명령어-덮어쓰기">4.1 중단점(Breakpoint)의 물리적 동작: 0xCC 명령어 덮어쓰기</h3>
<ol>
<li>사용자가 GDB에서 <code>break main</code> 또는 특정 행에 중단점을 설정한다.</li>
<li>GDB는 해당 행에 대응하는 기계어 명령어의 메모리 주소를 찾아낸다.</li>
<li>GDB는 그 주소에 원래 저장되어 있던 기계어 1바이트를 자신의 내부 메모리에 백업해 둔다.</li>
<li>원래 기계어 첫 바이트 자리에 x86-64 아키텍처의 디버그 인터럽트 명령어인 0xCC(int 3) 1바이트를 강제로 덮어쓴다.</li>
</ol>
<h3 id="42-실행-정지와-레지스터-상태-관측">4.2 실행 정지와 레지스터 상태 관측</h3>
<ol>
<li>CPU가 프로그램을 실행하다가 주소 카운터(RIP)가 해당 중단점에 도달하여 0xCC 명령어를 읽는다.</li>
<li>CPU는 하드웨어 인터럽트를 발생시키고 실행을 즉시 멈추며, 운영체제는 제어권을 GDB에게 넘긴다.</li>
<li>이때 GDB는 CPU 내부의 모든 레지스터 값(RIP, RSP, RAX, RDI 등)을 그대로 보존하여 읽어온다.</li>
<li>개발자가 GDB 콘솔에서 <code>info registers</code>, <code>print p</code>, <code>x/10xw 주소</code> 명령을 입력하면, GDB는 레지스터에 들어있는 실제 숫자와 RAM의 물리 주소에 기록된 16진수 바이트 데이터를 화면에 그대로 표시한다.</li>
<li>사용자가 <code>continue</code> 또는 <code>next</code>를 입력하면, GDB는 0xCC를 지우고 백업해 두었던 원래 기계어 바이트를 복원한 뒤 CPU 실행을 한 단계 재개한다.</li>
</ol>
<hr>
<h2 id="5-strtok의-정적static-내부-포인터-한계와-strchr의-1바이트-선형-메모리-탐색-비교">5. strtok의 정적(static) 내부 포인터 한계와 strchr의 1바이트 선형 메모리 탐색 비교</h2>
<h3 id="51-strtok의-정적static-포인터-유지-메커니즘">5.1 strtok의 정적(static) 포인터 유지 메커니즘</h3>
<p><code>strtok(str, delim)</code> 함수는 첫 호출 시 전달받은 문자열 주소에서 구분자를 찾아 <code>\0</code>으로 변경한 뒤 토큰의 시작 주소를 반환한다.</p>
<ul>
<li>그 다음 호출부터는 첫 번째 인자로 NULL을 넘겨받아도 이전에 멈췄던 위치를 기억하고 파싱을 이어간다.</li>
<li>이를 가능하게 하는 물리적 구조는 함수 내부에 선언된 정적 변수(<code>static char *next_token</code>)이다.</li>
<li>정적 변수는 스택이 아니라 프로그램의 데이터 세그먼트(Data Segment)에 단 하나만 물리적으로 고정 할당된다.</li>
<li>따라서 두 개 이상의 문자열을 번갈아가며 파싱하거나, 여러 스레드가 동시에 <code>strtok</code>을 호출하면 데이터 세그먼트에 있는 단 하나의 <code>next_token</code> 주소값이 덮어씌워져 파싱이 파괴된다.</li>
</ul>
<h3 id="52-strchr의-1바이트-주소-증가-선형-탐색">5.2 strchr의 1바이트 주소 증가 선형 탐색</h3>
<p><code>strchr(s, c)</code> 함수는 정적 변수를 일절 사용하지 않는다.</p>
<ul>
<li>인자로 전달받은 메모리 주소 <code>s</code>부터 시작하여, 포인터를 1바이트씩(<code>s++</code>) 메모리 번지수를 증가시킨다.</li>
<li>각 번지수의 1바이트 값을 CPU 레지스터로 읽어와 찾고자 하는 문자 바이트 <code>c</code>와 직접 비교한다.</li>
<li>만약 문자열의 끝을 알리는 바이트 값 0(<code>\0</code>)을 만날 때까지 <code>c</code>와 일치하는 바이트가 없으면 숫자 0(NULL)을 반환한다.</li>
<li>이 방식은 메모리 내부 상태를 오염시키지 않으므로 다중 루프나 멀티스레드 환경에서도 안전하게 메모리 주소를 분리해 낼 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C언어] 문자열의 메모리 구조(char 배열과 널 문자 \0)와 포인터를 사용한 구분자 자르기(In-Place 파싱) 원리]]></title>
            <link>https://velog.io/@jae_yun/C%EC%96%B8%EC%96%B4-%EB%AC%B8%EC%9E%90%EC%97%B4-%ED%8C%8C%EC%8B%B1%EC%9D%98-%EB%B3%B8%EC%A7%88-%EB%B0%B0%EC%97%B4-%EB%B2%84%ED%8D%BC%EB%A9%94%EB%AA%A8%EB%A6%AC-%ED%8F%AC%EC%9D%B8%ED%84%B0-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%ED%86%A0%ED%81%B0</link>
            <guid>https://velog.io/@jae_yun/C%EC%96%B8%EC%96%B4-%EB%AC%B8%EC%9E%90%EC%97%B4-%ED%8C%8C%EC%8B%B1%EC%9D%98-%EB%B3%B8%EC%A7%88-%EB%B0%B0%EC%97%B4-%EB%B2%84%ED%8D%BC%EB%A9%94%EB%AA%A8%EB%A6%AC-%ED%8F%AC%EC%9D%B8%ED%84%B0-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%ED%86%A0%ED%81%B0</guid>
            <pubDate>Tue, 29 Sep 2026 16:36:54 GMT</pubDate>
            <description><![CDATA[<h2 id="1-c언어에는-문자열이라는-데이터-타입이-없습니다">1. C언어에는 문자열이라는 데이터 타입이 없습니다</h2>
<p>컴퓨터 메모리는 숫자를 저장하는 공간입니다. 파이썬이나 자바와 달리 C언어에는 <code>string</code>이라는 별도의 데이터 타입이 없습니다.
대신 영문자 한 글자를 저장하는 1바이트 크기의 정수 타입인 <code>char</code>를 연속해서 나열한 <strong>배열</strong>을 문자열로 사용합니다.</p>
<p>컴퓨터가 &quot;문자열이 여기서 끝났다&quot;는 것을 알기 위해, C언어는 문자열의 맨 마지막 칸에 숫자 0을 넣습니다. 이 숫자 0을 코드에서는 <code>&#39;\0&#39;</code>(널 문자)이라고 적습니다.</p>
<pre><code class="language-text">메모리 주소:   1000   1001   1002   1003   1004
저장된 문자:   &#39;T&#39;    &#39;E&#39;    &#39;S&#39;    &#39;T&#39;    &#39;\0&#39;
저장된 숫자:    84     69     83     84      0</code></pre>
<hr>
<h2 id="2-버퍼buffer와-메모리-주소address의-뜻">2. 버퍼(Buffer)와 메모리 주소(Address)의 뜻</h2>
<ul>
<li><strong>메모리(RAM)</strong>: 1바이트마다 0, 1, 2, 3처럼 1씩 증가하는 일련번호가 붙어 있습니다. 이 번호를 <strong>메모리 주소</strong>라고 부릅니다.</li>
<li><strong>버퍼 (Buffer)</strong>: 특정 데이터를 담기 위해 연속적으로 붙어 있는 메모리 공간 자체를 부르는 기술 용어입니다.</li>
<li><strong>포인터 (Pointer)</strong>: 그 메모리 주소 번호(숫자)를 저장하고 있는 변수입니다.</li>
</ul>
<hr>
<h2 id="3-파싱parsing과-토큰token의-뜻">3. 파싱(Parsing)과 토큰(Token)의 뜻</h2>
<ul>
<li><strong>파싱 (Parsing)</strong>: 긴 문자열을 읽어가면서 특정한 기준 기호(구분자, 예를 들어 쉼표나 콜론)를 찾아내어 분리하는 작업입니다.</li>
<li><strong>토큰 (Token)</strong>: 구분자를 기준으로 잘라낸 각 단어 조각을 의미합니다.</li>
</ul>
<p>예시:</p>
<ul>
<li>원본 문자열: <code>&quot;USER:1001&quot;</code></li>
<li>구분자: <code>&#39;:&#39;</code></li>
<li>나뉘어진 토큰 1: <code>&quot;USER&quot;</code></li>
<li>나뉘어진 토큰 2: <code>&quot;1001&quot;</code></li>
</ul>
<hr>
<h2 id="4-in-place-파싱의-동작-과정-새-메모리-없이-원본의-구분자에-00을-덮어쓰기">4. In-Place 파싱의 동작 과정: 새 메모리 없이 원본의 구분자에 0(\0)을 덮어쓰기</h2>
<p>새로운 메모리를 따로 만들지 않고, 원본 문자열이 들어있는 메모리를 그대로 수정해서 토큰을 나누는 방식을 <strong>In-Place 파싱</strong>이라고 합니다.</p>
<pre><code class="language-text">[실행 전]
메모리 주소:   1000  1001  1002  1003  1004  1005  1006  1007  1008
데이터:         &#39;U&#39;   &#39;S&#39;   &#39;E&#39;   &#39;R&#39;   &#39;:&#39;   &#39;1&#39;   &#39;0&#39;   &#39;0&#39;   &#39;1&#39;   &#39;\0&#39;

[실행 후: 구분자 &#39;:&#39; 위치인 1004번지에 숫자 0(\0)을 덮어씀]
메모리 주소:   1000  1001  1002  1003  1004  1005  1006  1007  1008
데이터:         &#39;U&#39;   &#39;S&#39;   &#39;E&#39;   &#39;R&#39;   &#39;\0&#39;  &#39;1&#39;   &#39;0&#39;   &#39;0&#39;   &#39;1&#39;   &#39;\0&#39;</code></pre>
<ol>
<li>주소 1004번지의 <code>&#39;:&#39;</code> 자리에 숫자 0(<code>&#39;\0&#39;</code>)을 강제로 기록합니다.</li>
<li>이제 C언어 함수들은 주소 1000번지부터 읽기 시작하다가 1004번지의 0을 만나면 멈추므로, 첫 번째 단어를 <code>&quot;USER&quot;</code>로 인식합니다.</li>
<li>그 다음 주소인 1005번지부터 읽으면 1008번지의 0에서 멈추므로, 두 번째 단어를 <code>&quot;1001&quot;</code>로 인식합니다.</li>
<li>메모리를 새로 복사하지 않기 때문에 컴퓨터 실행 속도가 빠릅니다. 단, 원본 데이터의 <code>&#39;:&#39;</code> 문자가 0으로 지워집니다.</li>
</ol>
<hr>
<h2 id="5-c언어-표준-파싱-함수-비교-strtok-vs-strtok_r-vs-strsep">5. C언어 표준 파싱 함수 비교 (strtok vs strtok_r vs strsep)</h2>
<ol>
<li><strong><code>strtok</code></strong>:<ul>
<li>구분자를 찾아 0(<code>&#39;\0&#39;</code>)으로 바꾸고 다음 시작 주소를 함수 내부의 고정 변수(static)에 저장합니다.</li>
<li>동시에 여러 코드가 실행되는 환경(멀티스레드)에서는 내부 고정 변수 값이 서로 엉켜서 에러가 납니다.</li>
</ul>
</li>
<li><strong><code>strtok_r</code></strong>:<ul>
<li>다음 시작 주소를 함수 내부에 두지 않고, 사용자가 만든 변수(<code>saveptr</code>)에 직접 적어둡니다.</li>
<li>변수가 섞이지 않으므로 여러 코드가 동시에 실행되어도 안전합니다.</li>
</ul>
</li>
<li><strong><code>strsep</code></strong>:<ul>
<li>전달받은 포인터 변수의 주소를 다음 단어의 시작 주소로 직접 옮겨줍니다.</li>
<li>쉼표가 두 번 연속 나오는 경우(<code>&quot;a,,b&quot;</code>) 중간의 빈 문자열도 빼놓지 않고 찾아냅니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="6-포인터-연산자p를-사용한-수동-파싱-코드-동작-순서">6. 포인터 연산자(p++)를 사용한 수동 파싱 코드 동작 순서</h2>
<pre><code class="language-c">char line[] = &quot;AGE:25&quot;;
char *p = line; // p는 &#39;A&#39;가 있는 1000번지 주소를 가짐

// 1단계: 콜론(:) 문자가 나올 때까지 주소를 1씩 증가시키며 전진
while (*p != &#39;\0&#39; &amp;&amp; *p != &#39;:&#39;) {
    p++; // 주소가 1000 -&gt; 1001 -&gt; 1002 -&gt; 1003으로 증가
}

// 2단계: 콜론을 찾았으면 그 자리에 숫자 0(\0)을 대입
if (*p == &#39;:&#39;) {
    *p = &#39;\0&#39;; // 1003번지가 &#39;:&#39;에서 &#39;\0&#39;으로 변경됨
    p++;        // p는 이제 &#39;2&#39;가 있는 1004번지 주소를 가짐
}

char *key = line; // key는 1000번지 (&quot;AGE&quot; 시작)
char *val = p;    // val은 1004번지 (&quot;25&quot; 시작)</code></pre>
<ul>
<li><code>p</code>는 메모리 주소 번호입니다.</li>
<li><code>p++</code>는 메모리 주소 번호를 1 더해서 바로 다음 바이트 위치로 이동하라는 뜻입니다.</li>
<li><code>*p</code>는 현재 <code>p</code>에 적힌 주소 번지로 직접 찾아가서 거기에 들어있는 글자를 읽거나 바꾸라는 뜻입니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C언어] 힙 메모리 동적 배열(IntList)의 구조와 할당 크기 계산 실수로 인한 Heap Buffer Overflow 발생 원인]]></title>
            <link>https://velog.io/@jae_yun/heapbufferoverflow-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@jae_yun/heapbufferoverflow-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 28 Sep 2026 14:32:59 GMT</pubDate>
            <description><![CDATA[<h2 id="1-정적-배열과-동적-배열의-물리적-차이">1. 정적 배열과 동적 배열의 물리적 차이</h2>
<ul>
<li><strong>정적 배열 (<code>int arr[10];</code>)</strong>: 프로그램을 만들 때 크기를 10개로 고정합니다. 프로그램을 켜서 실행하는 도중에 11개로 크기를 늘릴 수 없습니다.</li>
<li><strong>동적 배열</strong>: 처음에는 작은 크기로 시작했다가, 데이터가 꽉 차면 실행 중에 운영체제에 요청하여 더 큰 메모리를 받아와 크기를 늘립니다.</li>
</ul>
<hr>
<h2 id="2-동적-배열을-만드는-구조체intlist의-3가지-멤버-변수">2. 동적 배열을 만드는 구조체(IntList)의 3가지 멤버 변수</h2>
<pre><code class="language-c">typedef struct {
    int    *data;  // 힙 메모리에 만든 배열의 시작 주소 번호 (8바이트)
    size_t  len;   // 현재 배열에 실제로 들어있는 숫자 개수 (정수)
    size_t  cap;   // 현재 할당받은 메모리에 최대 담을 수 있는 숫자 개수 (정수)
} IntList;</code></pre>
<ol>
<li><strong><code>data</code></strong>: 숫자들이 저장된 힙 메모리의 시작 주소 번호입니다.</li>
<li><strong><code>len</code></strong>: 현재까지 사용자가 배열에 집어넣은 숫자의 개수입니다.</li>
<li><strong><code>cap</code> (Capacity, 용량)</strong>: 현재 운영체제로부터 받아둔 메모리에 숫자를 최대 몇 개까지 넣을 수 있는지를 적어둔 숫자입니다.</li>
</ol>
<hr>
<h2 id="3-스택stack과-힙heap-메모리의-차이">3. 스택(Stack)과 힙(Heap) 메모리의 차이</h2>
<ul>
<li><strong>스택 (Stack)</strong>: <code>IntList list;</code>처럼 함수 안에서 만든 변수 자체가 자리 잡는 메모리 영역입니다. 함수가 끝나면 자동으로 사라집니다.</li>
<li><strong>힙 (Heap)</strong>: <code>malloc()</code> 함수를 호출했을 때 운영체제가 따로 떼어주는 메모리 영역입니다. 사용자가 직접 해제하기 전까지 계속 유지됩니다.</li>
<li>구조체 변수 <code>list</code>는 스택에 있고, <code>list.data</code> 안에 적힌 주소가 가리키는 실제 숫자 공간들은 힙에 있습니다.</li>
</ul>
<hr>
<h2 id="4-원소가-꽉-찼을-때-realloc으로-크기를-늘리는-과정">4. 원소가 꽉 찼을 때 realloc()으로 크기를 늘리는 과정</h2>
<pre><code class="language-c">static void list_push(IntList *l, int x) {
    // 1단계: 현재 개수(len)와 최대 용량(cap)이 같은지 검사
    if (l-&gt;len == l-&gt;cap) {
        size_t newcap = l-&gt;cap * 2; // 용량을 2배로 계산 (예: 8개 -&gt; 16개)

        // 2단계: realloc으로 16개 * 4바이트(int 크기) = 64바이트 힙 메모리를 새로 확보
        int *p = realloc(l-&gt;data, newcap * sizeof(int));
        if (p == NULL) {
            exit(1); // 메모리 부족 시 종료
        }

        l-&gt;data = p;      // 새 주소로 변경
        l-&gt;cap = newcap;  // 최대 용량을 16으로 변경
    }

    // 3단계: 데이터를 넣고 개수를 1 증가시킴
    l-&gt;data[l-&gt;len] = x;
    l-&gt;len++;
}</code></pre>
<hr>
<h2 id="5-heap-buffer-overflow힙-버퍼-오버플로우가-발생하는-기계적-원인">5. Heap Buffer Overflow(힙 버퍼 오버플로우)가 발생하는 기계적 원인</h2>
<p>이 오류는 <strong>변수에 적어둔 용량 숫자(<code>cap</code>)와 실제 힙에 빌려둔 물리 바이트 크기가 맞지 않을 때</strong> 일어납니다.</p>
<ol>
<li>개발자가 실수로 <code>sizeof(int)</code>(4바이트)를 곱하지 않고 <code>realloc(l-&gt;data, 16)</code>을 실행했다고 가정합니다.</li>
<li>컴퓨터는 <code>int</code> 16개(64바이트)가 아니라, 오직 16바이트(<code>int</code> 4개 분량)만 힙에 할당합니다.</li>
<li>하지만 구조체 변수에는 <code>l-&gt;cap = 16;</code>이라고 적혀 있습니다.</li>
<li><code>list_push</code> 코드는 <code>cap</code>이 16이므로 5번째, 6번째, 7번째 숫자를 넣을 때도 공간이 충분하다고 판단하여 힙 메모리에 값을 씁니다.</li>
<li>이때 4개 크기를 넘어서는 위치(남의 메모리 영역)에 숫자를 덮어쓰게 됩니다.</li>
<li>이 현상을 <strong>Heap Buffer Overflow</strong>라고 부르며, 다른 데이터가 깨지거나 프로그램이 강제 종료됩니다.</li>
</ol>
<hr>
<h2 id="6-결론-요약">6. 결론 요약</h2>
<ul>
<li><code>cap</code> 변수 값은 실제 운영체제로부터 할당받은 <code>int</code> 개수와 반드시 일치해야 합니다.</li>
<li><code>malloc</code>이나 <code>realloc</code>을 호출할 때는 반드시 원소 개수에 데이터 1개의 크기(<code>sizeof(int)</code>)를 곱한 총 바이트 수를 넘겨야 메모리 침범이 발생하지 않습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[GitHub] Projects 보드의 이슈 검색 패널에서 등록된 이슈가 나타나지 않는 이유와 자동 추가(Auto-add) 워크플로 필터링 원리]]></title>
            <link>https://velog.io/@jae_yun/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B3%B4%EB%93%9C%EC%9D%98-Add-items-to-project-%ED%8C%A8%EB%84%90%EC%97%90%EC%84%9C-%EC%9D%B4%EC%8A%88%EB%A5%BC-%EA%B2%80%EC%83%89%ED%95%98%EB%A9%B4-%EB%AA%A9%EB%A1%9D%EC%97%90-%EB%82%98%ED%83%80%EB%82%98%EC%A7%80-%EC%95%8A%EC%9D%8C</link>
            <guid>https://velog.io/@jae_yun/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B3%B4%EB%93%9C%EC%9D%98-Add-items-to-project-%ED%8C%A8%EB%84%90%EC%97%90%EC%84%9C-%EC%9D%B4%EC%8A%88%EB%A5%BC-%EA%B2%80%EC%83%89%ED%95%98%EB%A9%B4-%EB%AA%A9%EB%A1%9D%EC%97%90-%EB%82%98%ED%83%80%EB%82%98%EC%A7%80-%EC%95%8A%EC%9D%8C</guid>
            <pubDate>Wed, 23 Sep 2026 14:18:18 GMT</pubDate>
            <description><![CDATA[<h2 id="1-발생한-현상">1. 발생한 현상</h2>
<p>GitHub에서 새로운 이슈(Issue)를 등록한 후, 프로젝트(Projects) 화면으로 이동하여 &#39;+ Add items to project&#39; 검색창에 해당 이슈 번호나 제목을 입력했으나 검색 결과에 아무것도 나오지 않는 현상이 발생했습니다.</p>
<hr>
<h2 id="2--add-items-to-project-화면의-원래-기능">2. &#39;+ Add items to project&#39; 화면의 원래 기능</h2>
<p>GitHub Projects 화면 우측 상단의 &#39;+ Add items to project&#39; 패널은 <strong>아직 이 프로젝트에 등록되지 않은 외부 이슈들만 검색해서 수동으로 집어넣기 위해 만든 화면</strong>입니다.</p>
<hr>
<h2 id="3-검색-결과가-비어있던-원인-auto-add-워크플로">3. 검색 결과가 비어있던 원인: Auto-add 워크플로</h2>
<p>해당 저장소(Repository)에는 <strong>Auto-add to project</strong> 자동화 설정이 켜져 있었습니다.</p>
<ul>
<li>자동화 규칙: <code>is:issue is:open</code> (열려 있는 모든 이슈 대상)</li>
<li>동작 방식: 사용자가 이슈를 생성하는 순간, GitHub 시스템 봇(<code>github-project-automation</code>)이 즉시 그 이슈를 해당 프로젝트 보드 안에 등록합니다.</li>
<li>결과: <strong>해당 이슈는 이미 프로젝트에 등록된 상태</strong>였습니다.</li>
<li>검색 창에서 제외된 이유: 이미 프로젝트에 들어와 있는 항목은 중복해서 추가할 수 없으므로, 검색 패널의 필터링 기능이 검색 결과 목록에서 자동으로 숨겼기 때문입니다. 오류가 아니라 정상적인 화면 동작입니다.</li>
</ul>
<hr>
<h2 id="4-이슈가-프로젝트에-잘-등록되었는지-확인하는-2가지-위치">4. 이슈가 프로젝트에 잘 등록되었는지 확인하는 2가지 위치</h2>
<ol>
<li><strong>이슈 상세 페이지</strong>: 이슈 화면 우측 사이드바의 <strong>Projects</strong> 항목을 확인합니다. 해당 프로젝트 이름이 표시되어 있으면 정상 등록된 것입니다.</li>
<li><strong>프로젝트 보드 화면</strong>: 보드의 첫 번째 열(예: <strong>No Status</strong> 또는 <strong>Todo</strong> 컬럼) 목록에 해당 이슈 카드가 들어와 있는지 직접 확인합니다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CS:APP/어셈블리] 실수(부동소수점) 숫자를 명령어 안에 직접 적지 못하고 메모리(.rodata) 주소에서 읽어와야 하는 x86-64 아키텍처 이유]]></title>
            <link>https://velog.io/@jae_yun/%EB%B6%80%EB%8F%99%EC%86%8C%EC%88%98%EC%A0%90-%EC%83%81%EC%88%98%EC%9D%98-%EC%A0%95%EC%9D%98-%EB%B0%8F-%EC%9D%B4%EC%9A%A9</link>
            <guid>https://velog.io/@jae_yun/%EB%B6%80%EB%8F%99%EC%86%8C%EC%88%98%EC%A0%90-%EC%83%81%EC%88%98%EC%9D%98-%EC%A0%95%EC%9D%98-%EB%B0%8F-%EC%9D%B4%EC%9A%A9</guid>
            <pubDate>Tue, 22 Sep 2026 14:18:57 GMT</pubDate>
            <description><![CDATA[<h2 id="1-정수와-실수의-컴퓨터-저장-방식-차이">1. 정수와 실수의 컴퓨터 저장 방식 차이</h2>
<ul>
<li><strong>정수 (Integer)</strong>: 1, 2, 32 같은 숫자는 2진수로 바꾸어 비트에 순서대로 0과 1을 채웁니다.</li>
<li><strong>실수 (Floating Point)</strong>: 3.14 같은 소수점이 있는 숫자는 컴퓨터에서 부호(1비트), 지수(11비트), 가수(52비트)로 나누어 복잡하게 인코딩하는 <strong>IEEE 754 규격</strong>을 사용합니다.</li>
</ul>
<hr>
<h2 id="2-정수-연산과-실수-연산의-어셈블리-명령어-차이">2. 정수 연산과 실수 연산의 어셈블리 명령어 차이</h2>
<p>정수 연산 어셈블리 명령어는 명령어 자체에 숫자를 바로 적어 넣을 수 있습니다.</p>
<pre><code class="language-asm">addq $32, %rax   # %rax 레지스터에 숫자 32를 더하라 (32가 명령어 코드 안에 직접 들어감)</code></pre>
<p>명령어 바이트 안에 숫자를 직접 넣는 방식을 <strong>즉시값 (Immediate value)</strong>이라고 부릅니다.</p>
<p>하지만 <strong>실수(부동소수점) 연산 명령어는 즉시값을 지원하지 않습니다.</strong></p>
<pre><code class="language-asm"># 문법 에러가 발생하는 불가능한 명령어
vmulsd $3.14, %xmm0, %xmm0  # CPU에 이런 형태의 기계어 형식이 아예 존재하지 않음!</code></pre>
<hr>
<h2 id="3-실수를-명령어-안에-직접-적지-못하는-이유">3. 실수를 명령어 안에 직접 적지 못하는 이유</h2>
<p>x86-64 CPU를 설계할 때, 64비트 실수(<code>double</code>)의 복잡한 0과 1 비트 패턴을 명령어 코드 안에 직접 집어넣는 하드웨어 회로 규격을 만들지 않았기 때문입니다. 실수 연산 장치(AVX/SSE)는 오직 <strong>레지스터끼리 계산</strong>하거나 <strong>메모리 주소에서 숫자를 읽어와서 계산</strong>하도록만 만들어졌습니다.</p>
<hr>
<h2 id="4-해결-방법-rodata-메모리에-실수를-미리-저장해두고-주소로-읽어오기">4. 해결 방법: .rodata 메모리에 실수를 미리 저장해두고 주소로 읽어오기</h2>
<p>컴파일러는 C 코드에 적힌 실수 숫자(<code>3.14</code>)를 프로그램의 읽기 전용 데이터 영역(<code>.rodata</code>) 메모리에 미리 8바이트 숫자로 기록해 둡니다.</p>
<pre><code class="language-asm">.section .rodata
.LC0:
    .long 1413754136    # 3.141592... 의 하위 4바이트 정수 표현
    .long 1074340347    # 3.141592... 의 상위 4바이트 정수 표현

.text
    # 메모리 주소 .LC0에서 8바이트를 읽어와서 실수 전용 레지스터 %xmm0에 복사
    vmovsd .LC0(%rip), %xmm0

    # 레지스터에 들어있는 값끼리 곱셈 실행
    vmulsd %xmm0, %xmm1, %xmm0</code></pre>
<hr>
<h2 id="5-결론-요약">5. 결론 요약</h2>
<ul>
<li>x86-64 CPU의 실수 연산 명령어는 명령어 코드 안에 직접 실수 값을 적는 기능이 없습니다.</li>
<li>따라서 모든 실수 상수는 메모리의 <code>.rodata</code> 영역에 먼저 저장됩니다.</li>
<li>CPU는 실행 중에 그 메모리 주소를 찾아가서 레지스터로 값을 읽어온(Load) 뒤에 곱셈이나 덧셈을 수행합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CS 기초/시스템] 시스템 버스 3종류(데이터, 주소, 제어), 포인터와 메모리 주소, x86-64 레지스터와 함수 포인터 원리]]></title>
            <link>https://velog.io/@jae_yun/5%EC%A3%BC%EC%B0%A8-%ED%80%B4%EC%A6%88-%EC%A0%95%EB%A6%AC9%EC%9B%94-22%EC%9D%BC</link>
            <guid>https://velog.io/@jae_yun/5%EC%A3%BC%EC%B0%A8-%ED%80%B4%EC%A6%88-%EC%A0%95%EB%A6%AC9%EC%9B%94-22%EC%9D%BC</guid>
            <pubDate>Tue, 22 Sep 2026 07:23:24 GMT</pubDate>
            <description><![CDATA[<h2 id="1-시스템-버스bus의-물리적-정의와-3가지-종류">1. 시스템 버스(Bus)의 물리적 정의와 3가지 종류</h2>
<p>컴퓨터 메인보드 위에는 CPU 칩, RAM(메모리) 칩, 하드디스크/키보드 같은 I/O 장치들이 장착되어 있습니다. 이 칩들 사이에 0V(전압 없음=0)와 3.3V(전압 있음=1)의 전기 신호를 주고받기 위해 연결된 수십 가닥의 미세한 구리 전선 묶음을 <strong>시스템 버스</strong>라고 부릅니다.</p>
<ol>
<li><strong>데이터 버스 (Data Bus, 양방향)</strong>:<ul>
<li>숫자, 문자, 기계어 명령어 비트가 실제로 오고 가는 전선입니다.</li>
<li>CPU가 메모리에서 값을 읽어올 수도 있고, 메모리에 값을 쓸 수도 있으므로 양방향으로 전기가 흐릅니다.</li>
</ul>
</li>
<li><strong>주소 버스 (Address Bus, 단방향: CPU -&gt; 메모리/IO)</strong>:<ul>
<li>CPU가 &quot;몇 번지 메모리를 읽을 것인가?&quot; 또는 &quot;몇 번지에 쓸 것인가?&quot;를 지정하는 전선입니다.</li>
<li>CPU만 주소를 지정하여 출력하므로 단방향입니다.</li>
</ul>
</li>
<li><strong>제어 버스 (Control Bus, 개별 신호선)</strong>:<ul>
<li>현재 신호가 읽기(Read)인지 쓰기(Write)인지 알려주는 제어 전선입니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="2-포인터pointer의-물리적-실체">2. 포인터(Pointer)의 물리적 실체</h2>
<pre><code class="language-c">int x = 10;
int *p = &amp;x;</code></pre>
<ul>
<li><strong><code>x</code> 변수</strong>: RAM의 1000번지부터 1003번지까지의 4바이트 공간에 2진수로 숫자 <code>10</code>이 들어 있습니다.</li>
<li><strong><code>&amp;x</code></strong>: <code>x</code>가 위치한 시작 주소 번호인 숫자 <code>1000</code>을 뜻합니다.</li>
<li><strong><code>p</code> 포인터 변수</strong>: RAM의 2000번지부터 2007번지까지의 8바이트 공간이며, 그 안에 데이터 내용물로 숫자 <code>1000</code>이 저장됩니다.</li>
<li><strong><code>*p</code></strong>: <code>p</code> 변수 안에 들어있는 숫자 1000번지로 직접 가서 거기에 저장된 숫자 <code>10</code>을 읽거나 변경하는 동작입니다.</li>
</ul>
<hr>
<h2 id="3-함수-포인터function-pointer란-무엇인가">3. 함수 포인터(Function Pointer)란 무엇인가?</h2>
<p>우리가 작성한 C 코드는 컴파일되면 0과 1의 기계어 바이너리로 바뀌어 메모리의 코드 영역(Text Segment)에 순서대로 배치됩니다.</p>
<pre><code class="language-c">int add(int a, int b) { return a + b; }
int (*fp)(int, int) = add;</code></pre>
<ul>
<li><code>add</code> 함수는 메모리 어딘가(예: 5000번지)에 적재된 기계어 코드들의 묶음입니다.</li>
<li><code>fp</code> 변수는 그 <code>add</code> 함수의 시작 주소 번호인 숫자 <code>5000</code>을 저장하는 8바이트 변수입니다.</li>
<li><code>fp(10, 20)</code>을 실행하면 CPU는 명령어 포인터를 5000번지로 이동시켜 <code>add</code> 함수의 기계어를 실행합니다.</li>
</ul>
<hr>
<h2 id="4-x86-64-아키텍처의-함수-인자-전달-레지스터-규칙">4. x86-64 아키텍처의 함수 인자 전달 레지스터 규칙</h2>
<p>함수를 호출할 때 넘겨주는 값(인자)들은 메모리가 아니라 CPU 내부의 임시 고속 저장소인 <strong>레지스터</strong>에 아래 순서대로 들어갑니다:</p>
<pre><code class="language-text">1번째 인자: %rdi 레지스터
2번째 인자: %rsi 레지스터
3번째 인자: %rdx 레지스터
4번째 인자: %rcx 레지스터
5번째 인자: %r8  레지스터
6번째 인자: %r9  레지스터
7번째 이후 인자: 스택 메모리에 저장
함수 반환값: %rax 레지스터</code></pre>
<hr>
<h2 id="5-64비트-레지스터rax의-크기별-분할-접근">5. 64비트 레지스터(%rax)의 크기별 분할 접근</h2>
<p>과거 8비트, 16비트, 32비트 프로그램과의 호환성을 위해 x86-64 CPU는 하나의 64비트 레지스터를 크기별로 나누어 부를 수 있습니다:</p>
<ul>
<li><strong><code>%rax</code></strong>: 64비트 (8바이트 전체)</li>
<li><strong><code>%eax</code></strong>: 하위 32비트 (4바이트)</li>
<li><strong><code>%ax</code></strong>: 하위 16비트 (2바이트)</li>
<li><strong><code>%al</code></strong>: 하위 8비트 (1바이트)</li>
</ul>
<p>규칙: 32비트 레지스터인 <code>%eax</code>에 값을 쓰면, 상위 32비트 영역은 CPU에 의해 자동으로 0으로 채워집니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C언어] 힙 메모리 할당(malloc/free) 동작 원리와 free 후 주소가 남아 발생하는 댕글링 포인터(Use-After-Free) 방어법]]></title>
            <link>https://velog.io/@jae_yun/9%EC%9B%94-21%EC%9D%BC-%EC%A0%95%EB%A6%AC%EB%B3%B8-USERAFTERFREE</link>
            <guid>https://velog.io/@jae_yun/9%EC%9B%94-21%EC%9D%BC-%EC%A0%95%EB%A6%AC%EB%B3%B8-USERAFTERFREE</guid>
            <pubDate>Mon, 21 Sep 2026 08:05:22 GMT</pubDate>
            <description><![CDATA[<h2 id="1-메모리ram와-힙heap-영역의-물리적-정의">1. 메모리(RAM)와 힙(Heap) 영역의 물리적 정의</h2>
<p>컴퓨터의 RAM은 1바이트마다 0, 1, 2, 3처럼 1씩 증가하는 일련번호(메모리 주소)가 붙어 있는 물리적 반도체 공간입니다.</p>
<ul>
<li><strong>스택 (Stack)</strong>: 함수 안에서 선언된 일반 지역 변수가 사용하는 메모리입니다. 함수가 끝나면 CPU가 스택 포인터 레지스터를 이동시켜 자동으로 지워버립니다.</li>
<li><strong>힙 (Heap)</strong>: 프로그램 실행 중에 크기가 유동적으로 변하거나, 함수가 끝나도 메모리를 계속 보존해야 할 때 운영체제에 요청하여 할당받는 메모리 영역입니다. 사용자가 직접 해제하기 전까지 메모리에 유지됩니다.</li>
</ul>
<hr>
<h2 id="2-malloc과-free의-실제-실행-동작">2. malloc()과 free()의 실제 실행 동작</h2>
<pre><code class="language-c">int *ptr = (int *)malloc(sizeof(int)); // 1단계: 힙 메모리 4바이트 할당
*ptr = 42;                             // 2단계: 주소에 숫자 42 저장
free(ptr);                             // 3단계: 힙 메모리 사용 권한 반납</code></pre>
<ol>
<li><strong><code>malloc(4)</code></strong>: 운영체제가 힙 영역에서 비어 있는 연속된 4바이트 공간(예: 주소 <code>0x5000</code>번지)의 사용 권한을 부여하고, 그 시작 주소 번호인 숫자 <code>0x5000</code>을 반환합니다.</li>
<li><strong><code>*ptr = 42</code></strong>: 포인터 변수 <code>ptr</code>에 저장된 주소 <code>0x5000</code>번지로 이동하여 4바이트 공간에 숫자 42를 2진수로 기록합니다.</li>
<li><strong><code>free(ptr)</code></strong>: 운영체제에 &quot;주소 <code>0x5000</code>번지의 4바이트 공간을 이제 다른 변수나 코드가 써도 된다&quot;고 알립니다.</li>
</ol>
<hr>
<h2 id="3-댕글링-포인터dangling-pointer가-발생하는-기계적-원인">3. 댕글링 포인터(Dangling Pointer)가 발생하는 기계적 원인</h2>
<ul>
<li><code>free(ptr);</code>을 실행하면 <code>0x5000</code>번지의 힙 메모리 사용 권한만 반납됩니다.</li>
<li><strong>포인터 변수 <code>ptr</code> 안에 들어있던 숫자 <code>0x5000</code>은 컴퓨터가 0으로 자동으로 지워주지 않으므로 그대로 남아 있습니다.</strong></li>
<li>이미 반납되어 쓸 권한이 없는 주소 번호를 여전히 갖고 있는 포인터를 <strong>댕글링 포인터</strong>라고 부릅니다.</li>
</ul>
<hr>
<h2 id="4-use-after-free해제-후-재사용-버그의-위험성">4. Use-After-Free(해제 후 재사용) 버그의 위험성</h2>
<pre><code class="language-c">free(ptr);   // 0x5000 주소를 반납함
*ptr = 100;  // 반납된 0x5000 주소에 숫자 100을 덮어씀 (심각한 오류)</code></pre>
<ul>
<li>반납된 주소 <code>0x5000</code>은 나중에 호출된 다른 <code>malloc</code>에 의해 전혀 다른 데이터나 함수 주소가 배치될 수 있습니다.</li>
<li>이때 댕글링 포인터 <code>ptr</code>을 통해 숫자를 덮어쓰면, 새로 할당된 다른 변수의 데이터가 깨지거나 프로그램이 비정상 종료(Segmentation Fault)됩니다.</li>
</ul>
<hr>
<h2 id="5-해결-방법-free-직후-null-대입">5. 해결 방법: free 직후 NULL 대입</h2>
<pre><code class="language-c">free(ptr);
ptr = NULL; // ptr 내부의 숫자를 0x0(접근 불가 주소)으로 변경</code></pre>
<ul>
<li>포인터 변수를 <code>NULL</code>(주소 0)로 바꿔두면, 이후 실수로 <code>*ptr</code>을 실행하더라도 운영체제가 즉시 에러를 발생시키며 프로그램을 안전하게 멈추어 주므로 다른 메모리가 오염되는 것을 차단합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CS:APP] 가변크기 배열(VLA)의 2차원 메모리 주소 곱셈(imulq)과 GCC 컴파일러의 덧셈 치환 루프 최적화]]></title>
            <link>https://velog.io/@jae_yun/9%EC%9B%94-20%EC%9D%BC-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@jae_yun/9%EC%9B%94-20%EC%9D%BC-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 21 Sep 2026 07:59:32 GMT</pubDate>
            <description><![CDATA[<h2 id="1-2차원-배열은-메모리에-일렬1차원로-저장됩니다">1. 2차원 배열은 메모리에 일렬(1차원)로 저장됩니다</h2>
<p>컴퓨터 메모리는 가로세로 바둑판 모양이 아니라, 0번지부터 1씩 증가하는 1차원 직선 형태입니다.
따라서 2차원 배열 <code>int A[n][m]</code>은 1번째 행, 2번째 행, 3번째 행이 메모리에 빈틈없이 연속해서 이어 붙어 저장됩니다.</p>
<hr>
<h2 id="2-2차원-배열-원소-aij의-메모리-주소-계산-공식">2. 2차원 배열 원소 A[i][j]의 메모리 주소 계산 공식</h2>
<p>원소 1개의 크기가 4바이트(<code>int</code>)이고, 열 개수가 <code>m</code>개일 때:</p>
<pre><code class="language-text">A[i][j]의 메모리 주소 = 배열 시작 주소 + 4바이트 * (m * i + j)</code></pre>
<ol>
<li><code>i</code>번째 행을 건너뛰기 위해서는 앞서 있는 행들의 원소 개수인 <code>m * i</code>개를 건너뛰어야 합니다.</li>
<li>그 행 안에서 <code>j</code>번째 원소로 가기 위해 <code>+ j</code>를 더합니다.</li>
<li>원소 1개가 4바이트이므로 전체에 4를 곱합니다.</li>
</ol>
<hr>
<h2 id="3-고정-크기-배열과-가변-크기-배열vla의-어셈블리-명령어-차이">3. 고정 크기 배열과 가변 크기 배열(VLA)의 어셈블리 명령어 차이</h2>
<ul>
<li><strong>고정 크기 배열 (<code>int A[10][8]</code>)</strong>:<ul>
<li>열 개수 <code>m</code>이 숫자 8로 고정되어 있습니다.</li>
<li>$8     imes i$는 2진수 비트를 왼쪽으로 3번 이동하는 시프트 연산과 덧셈(<code>leaq</code> 명령어)만으로 1클럭 사이클 만에 빠르게 계산됩니다.</li>
</ul>
</li>
<li><strong>가변 크기 배열 (<code>int A[n][m]</code>)</strong>:<ul>
<li>열 개수 <code>m</code>이 실행 중에 사용자가 입력한 변수입니다.</li>
<li>비트 시프트로 바꿀 수 없으므로, CPU는 무거운 정수 곱셈 명령어인 <strong><code>imulq</code></strong>를 실행해야 합니다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="4-gcc-컴파일러의-루프-최적화-곱셈을-덧셈으로-바꾸기-강도-감쇄">4. GCC 컴파일러의 루프 최적화: 곱셈을 덧셈으로 바꾸기 (강도 감쇄)</h2>
<p>이중 루프 안에서 <code>A[i][j]</code>를 순회할 때 매번 <code>m * i</code> 곱셈을 하면 CPU 연산 시간이 크게 낭비됩니다.
GCC 컴파일러는 이를 다음과 같이 최적화합니다:</p>
<pre><code class="language-text">[최적화 전]: 매 반복마다 4 * (m * i + j)를 계산 (imulq 곱셈이 매번 실행됨)
[최적화 후]: 루프 시작 전 ptr = A[0] 주소를 잡아둠
             루프가 돌 때마다 ptr = ptr + 4 (다음 칸으로 4바이트 더하기만 수행)
             행이 바뀔 때는 ptr = ptr + (4 * m) (다음 행으로 덧셈만 수행)</code></pre>
<ul>
<li>무거운 곱셈 연산(<code>imulq</code>)을 가벼운 덧셈 연산(<code>addq</code>)으로 바꾸는 기법을 <strong>강도 감쇄(Strength Reduction)</strong> 최적화라고 부릅니다.</li>
</ul>
<hr>
<h2 id="5-어셈블리에서-4n과-n을-따로-사용하는-이유">5. 어셈블리에서 4n과 n을 따로 사용하는 이유</h2>
<ul>
<li><strong><code>4n</code></strong>: 포인터를 다음 행으로 건너뛰게 할 때 메모리 바이트 주소 전진용으로 사용됩니다 (<code>int</code>가 4바이트이므로 $n     imes 4$ 바이트 필요).</li>
<li><strong><code>n</code></strong>: 루프가 $n$번 반복되었는지 반복문 탈출 조건 검사 카운터로 사용됩니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CS:APP] 함수 호출 시 변수 보존 규칙(Caller-saved, Callee-saved)과 레지스터 부족으로 스택 메모리에 저장하는 원리]]></title>
            <link>https://velog.io/@jae_yun/%EB%A0%88%EC%A7%80%EC%8A%A4%ED%84%B0%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EB%8A%94-%EC%A7%80%EC%97%AD%EC%A0%80%EC%9E%A5%EC%86%8C</link>
            <guid>https://velog.io/@jae_yun/%EB%A0%88%EC%A7%80%EC%8A%A4%ED%84%B0%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EB%8A%94-%EC%A7%80%EC%97%AD%EC%A0%80%EC%9E%A5%EC%86%8C</guid>
            <pubDate>Sat, 19 Sep 2026 11:23:26 GMT</pubDate>
            <description><![CDATA[<h2 id="1-문제의-배경-cpu-레지스터는-모든-함수가-공유하는-단-1세트의-공간입니다">1. 문제의 배경: CPU 레지스터는 모든 함수가 공유하는 단 1세트의 공간입니다</h2>
<p>CPU 안에 존재하는 연산용 레지스터는 함수마다 따로 주어지는 것이 아닙니다.
컴퓨터에 존재하는 단 1개의 레지스터 세트를 함수 $P$도 쓰고, 함수 $P$가 호출한 다른 함수 $Q$도 똑같이 사용합니다.</p>
<p>따라서 함수 $P$가 레지스터에 중요한 값을 넣어두었는데, 함수 $Q$를 호출하면 $Q$가 자기 계산을 하느라 그 레지스터의 값을 덮어써서 지워버리는 문제가 발생합니다.</p>
<hr>
<h2 id="2-abi-규약이-정한-2가지-레지스터-보존-규칙">2. ABI 규약이 정한 2가지 레지스터 보존 규칙</h2>
<ol>
<li><strong>Caller-saved 레지스터 (호출한 쪽이 알아서 챙기기)</strong>:<ul>
<li><code>%rax</code>, <code>%rdi</code>, <code>%rsi</code>, <code>%rdx</code>, <code>%rcx</code>, <code>%r8 ~ %r11</code></li>
<li>호출받은 함수 $Q$는 이 레지스터들을 마음대로 덮어써도 됩니다.</li>
<li>따라서 $P$는 $Q$를 호출한 뒤에도 이 레지스터 값을 써야 한다면, $Q$를 호출하기 전에 스택 메모리에 자기가 직접 저장해 두어야 합니다.</li>
</ul>
</li>
<li><strong>Callee-saved 레지스터 (호출받은 쪽이 원상복구해 주기)</strong>:<ul>
<li><code>%rbx</code>, <code>%rbp</code>, <code>%r12</code>, <code>%r13</code>, <code>%r14</code>, <code>%r15</code> (총 6개)</li>
<li>호출받은 함수 $Q$는 이 레지스터의 원래 값을 손상시키면 안 됩니다.</li>
<li>$Q$가 이 레지스터를 사용하려면 함수 시작 시 스택 메모리에 <code>pushq</code>로 백업해 두고, 함수가 끝나서 복귀하기 직전에 <code>popq</code>로 원래 값으로 복구해 주어야 합니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="3-csapp-연습문제-334-지역-변수가-스택에-저장되는-계산-과정">3. CS:APP 연습문제 3.34: 지역 변수가 스택에 저장되는 계산 과정</h2>
<p>함수 $P$가 지역 변수 7개(<code>a1 ~ a7</code>)와 입력 변수 <code>x</code>를 다루는 코드:</p>
<ol>
<li>사용할 수 있는 Callee-saved 레지스터는 총 6개입니다.</li>
<li>그중 <code>%rbx</code> 레지스터는 입력 인자 변수 <code>x</code>를 보존하는 데 먼저 할당되었습니다.</li>
<li>남은 Callee-saved 레지스터는 <strong>5개</strong>뿐입니다.</li>
<li>지역 변수 7개 중 5개(<code>a1 ~ a5</code>)는 남은 5개의 레지스터에 할당됩니다.</li>
<li><strong>자리(레지스터)가 부족해서 남은 2개의 변수(<code>a6</code>, <code>a7</code>)는 RAM 메모리인 스택 프레임 공간(<code>(%rsp)</code>, <code>8(%rsp)</code>)에 저장됩니다.</strong></li>
</ol>
<hr>
<h2 id="4-결론-요약">4. 결론 요약</h2>
<ul>
<li>CPU 레지스터 개수는 한정되어 있습니다.</li>
<li>보존해야 할 변수 개수가 레지스터 개수보다 많아지면, 넘쳐나는 변수들은 스택 메모리에 저장합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CS:APP] 점프 명령어(jmp, je, ja) 제어 흐름과 RIP 레지스터 기준 상대 주소(PC-relative) 오프셋 계산 공식]]></title>
            <link>https://velog.io/@jae_yun/CSAPP-3.6.4-%EC%A0%90%ED%94%84-%EB%AA%85%EB%A0%B9%EC%96%B4%EC%99%80-PC-relative-%EC%A3%BC%EC%86%8C-%EA%B3%84%EC%82%B0</link>
            <guid>https://velog.io/@jae_yun/CSAPP-3.6.4-%EC%A0%90%ED%94%84-%EB%AA%85%EB%A0%B9%EC%96%B4%EC%99%80-PC-relative-%EC%A3%BC%EC%86%8C-%EA%B3%84%EC%82%B0</guid>
            <pubDate>Sat, 19 Sep 2026 06:40:31 GMT</pubDate>
            <description><![CDATA[<h2 id="1-cpu의-기본-실행-순서와-점프jump의-정의">1. CPU의 기본 실행 순서와 점프(Jump)의 정의</h2>
<ul>
<li><strong>기본 동작</strong>: CPU 내부에는 다음에 실행할 명령어의 메모리 주소를 저장하는 <strong><code>%rip</code> (Instruction Pointer 레지스터)</strong>가 있습니다. CPU는 한 줄의 명령어를 실행할 때마다 <code>%rip</code>의 숫자를 다음 명령어 바이트 크기만큼 자동으로 증가시키며 순서대로 실행합니다.</li>
<li><strong>점프 (Jump)</strong>: <code>if</code> 조건문이나 <code>while</code> 반복문을 만나면 순서대로 내려가지 않고 특정 주소로 건너뛰어야 합니다. 이처럼 <strong><code>%rip</code> 레지스터 안에 들어있는 주소 숫자를 다른 번호로 강제로 바꾸는 기계어 명령어</strong>를 점프라고 합니다.</li>
</ul>
<hr>
<h2 id="2-cmp-명령어와-cpu-상태-플래그-세팅-원리">2. cmp 명령어와 CPU 상태 플래그 세팅 원리</h2>
<pre><code class="language-asm">cmpq %rsi, %rdi   # %rdi에 들어있는 숫자에서 %rsi 숫자를 뺍니다 (%rdi - %rsi)</code></pre>
<ul>
<li><code>cmp</code>는 뺄셈 결과 숫자를 어딘가에 저장하지 않고 버립니다.</li>
<li>대신 뺄셈 결과에 따라 CPU 내부의 <strong>EFLAGS 레지스터 안의 1비트 스위치(플래그)</strong>들을 0 또는 1로 켭니다:<ul>
<li><strong><code>ZF</code> (Zero Flag)</strong>: 뺄셈 결과가 정확히 0이면 1로 켜집니다 (즉, 두 숫자가 같음).</li>
<li><strong><code>SF</code> (Sign Flag)</strong>: 뺄셈 결과가 음수이면 1로 켜집니다.</li>
<li><strong><code>CF</code> (Carry Flag)</strong>: 부호 없는 양수 연산에서 뺄셈 시 빌림수가 발생하면 1로 켜집니다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="3-조건부-점프-명령어와-플래그-확인-규칙">3. 조건부 점프 명령어와 플래그 확인 규칙</h2>
<ul>
<li><strong><code>jmp</code></strong>: 플래그를 보지 않고 무조건 지정한 주소로 이동합니다.</li>
<li><strong><code>je</code> (Jump if Equal)</strong>: <code>ZF</code> 플래그가 1이면(두 값이 같으면) 점프합니다.</li>
<li><strong><code>jne</code> (Jump if Not Equal)</strong>: <code>ZF</code> 플래그가 0이면(두 값이 다르면) 점프합니다.</li>
<li><strong><code>jg</code> / <code>jl</code></strong>: 부호 있는 정수 크기 비교 점프 (<code>SF</code>와 <code>OF</code> 플래그 확인).</li>
<li><strong><code>ja</code> / <code>jb</code></strong>: 부호 없는 정수 크기 비교 점프 (<code>CF</code>와 <code>ZF</code> 플래그 확인).</li>
</ul>
<hr>
<h2 id="4-pc-relative-주소-지정-방식의-계산-공식">4. PC-relative 주소 지정 방식의 계산 공식</h2>
<p>x86-64 아키텍처는 점프할 목적지 주소를 기계어 안에 8바이트 절대 주소 번호로 적지 않습니다.
대신 <strong>&quot;점프 명령어 바로 다음 줄의 시작 주소(%rip)로부터 목적지까지 몇 바이트 떨어져 있는가(상대 거리)&quot;</strong>를 계산해서 적습니다.</p>
<pre><code class="language-text">[계산 공식]
목적지 주소 = 다음 명령어 시작 주소(%rip) + 상대 오프셋(거리)
기계어에 기록될 상대 오프셋 = 목적지 주소 - 다음 명령어 시작 주소(%rip)</code></pre>
<h3 id="실제-예제-계산-je">실제 예제 계산 (je)</h3>
<pre><code class="language-text">메모리 주소   기계어 코드        어셈블리 코드
0x400540:    74 05             je 0x400547
0x400542:    e8 12 00 00 00    callq 0x400559
...
0x400547:    8b 05 ...         mov ...</code></pre>
<ol>
<li><code>je</code> 명령어는 0x400540 번지에 있고 크기가 2바이트입니다.</li>
<li>CPU가 <code>je</code> 명령어를 읽은 직후, <code>%rip</code>는 바로 다음 줄의 주소인 <strong><code>0x400542</code></strong>를 가리킵니다.</li>
<li>점프하려는 목표 주소는 <strong><code>0x400547</code></strong>입니다.</li>
<li>상대 거리 계산: <code>0x400547 - 0x400542 = +5</code> (16진수로 <code>0x05</code>).</li>
<li>따라서 기계어 코드는 <code>74</code>(je 옵코드) 뒤에 거리 <code>05</code>가 붙어 <code>74 05</code> 2바이트가 됩니다.</li>
</ol>
<hr>
<h2 id="5-음수-거리와-2의-보수twos-complement-루프-역점프">5. 음수 거리와 2의 보수(Two&#39;s Complement) 루프 역점프</h2>
<p>루프문처럼 이전 주소로 거슬러 올라가는 점프는 거리가 음수가 됩니다:</p>
<pre><code class="language-text">0x400560:    eb f4             jmp 0x400556
0x400562:    다음 명령어...</code></pre>
<ul>
<li>다음 줄 주소(%rip) = <code>0x400562</code></li>
<li>목표 주소 = <code>0x400556</code></li>
<li>상대 거리: <code>0x400556 - 0x400562 = -12</code> (10진수)</li>
<li>1바이트 2의 보수로 -12를 16진수로 바꾸면 <code>0xF4</code>가 됩니다.</li>
<li>따라서 기계어는 <code>eb f4</code>가 됩니다.</li>
</ul>
<hr>
<h2 id="6-pc-relative-방식을-사용하는-2가지-이유">6. PC-relative 방식을 사용하는 2가지 이유</h2>
<ol>
<li><strong>명령어 바이트 크기 절약</strong>: 8바이트(64비트) 주소를 통째로 적는 대신, 1바이트(-128 ~ +127 바이트 범위) 또는 4바이트 상대 거리만 적으므로 프로그램 용량이 크게 줄어듭니다.</li>
<li><strong>어느 메모리에 적재되어도 실행 가능 (위치 독립 코드, PIE)</strong>: 프로그램 전체가 RAM의 다른 주소 번지로 통째로 옮겨져도, 명령어들 사이의 상대적인 거리 차이는 변하지 않으므로 코드를 수정하지 않고 바로 실행할 수 있습니다.</li>
</ol>
]]></description>
        </item>
    </channel>
</rss>