[크래프톤정글/WIL] 4주차 회고: C 포인터와 x86-64 어셈블리, 기피하던 발표와 어려운 CS의 직면, 뼈아픈 자기반성

코딩이 어려운 사람·2026년 10월 22일

WIL

목록 보기
4/8

소주제

  1. 4주차 학습 개요와 기술 스택: C 언어 포인터 메모리 모델과 x86-64 시스템 프로그래밍(CS:APP)
  2. 내가 직면했던 현실과 한계: 기피하던 발표와 어려운 CS의 직면, 그리고 뼈아픈 자기반성
  3. 핵심 기술 개념과 코드 분석: C 포인터의 주소 연산(*p vs &p)과 배열 감쇄(Array Decay)
  4. x86-64 레지스터 호출 규약(Calling Convention)과 Caller/Callee-saved 스택 보존 원칙
  5. 협업과 소통의 성찰: 준비되지 않은 두려움의 결과와 정직한 학습 태도로의 대전환
  6. 실행 가능한 액션 플랜: 백지 도식화와 3분 설명 연습
  7. 4주차 핵심 엔지니어링 Q&A 3선

1. 4주차 학습 개요와 기술 스택: C 언어 포인터 메모리 모델과 x86-64 시스템 프로그래밍(CS:APP)

4주차는 고수준 인터프리터 언어(Python)의 안락함을 벗어나, 하드웨어 메모리를 직접 제어하는 C 언어의 포인터 체계와 컴퓨터 시스템의 바이블인 CS:APP 3장(x86-64 머신 레벨 프로그램 표현)을 본격적으로 학습한 주차였습니다. 컴파일러가 고급 언어를 어떤 어셈블리 명령어와 레지스터 연산으로 변환하는지 그 물리적 밑바닥을 마주했습니다.

[고급 언어에서 x86-64 머신 코드로의 하향 변환 계층]

고급 C 코드: int sum(int a, int b) { return a + b; }
        │
        ▼ 컴파일러 (GCC) 번역
x86-64 어셈블리:
        lea (%rdi, %rsi), %eax   ; 첫 번째 인자(%rdi)와 두 번째 인자(%rsi)를 더해 %eax에 저장
        ret                      ; 호출자(Caller)로 복귀
        │
        ▼ 어셈블러 & 링커
기계어 바이너리: 8d 04 37 c3 (CPU 제어 유닛이 직접 실행하는 16진수 바이트)

2. 내가 직면했던 현실과 한계: 기피하던 발표와 어려운 CS의 직면, 그리고 뼈아픈 자기반성

"내가 싫어하던 발표와 어려운 CS의 대한 모든 것을 합친 한주, 두려움이 가장 컸고, 내 스스로도 준비를 안해서 많이 반성했다"

살면서 가장 기피해 왔던 두 가지가 한꺼번에 정면으로 닥쳤습니다. 바로 '여러 사람 앞에서의 공식 기술 발표'와 '악명 높은 저수준 컴퓨터 시스템(CS:APP)과 어셈블리'였습니다.

처음 접하는 어셈블리 명령어와 C 포인터의 복잡성에 압도당하자, 마음속 깊은 곳에서 막연한 공포가 피어올랐습니다. "이걸 내가 어떻게 남들 앞에서 설명하지?"라는 두려움에 사로잡혀 오히려 문제에 깊이 몰입하지 못하고 시간을 흘려보냈으며, 발표 준비를 철저히 끝내지 못했습니다. 준비 부족은 발표 당일의 자신감 상실과 불완전한 전달로 고스란히 드러났고, 팀원들과 동료들에게 큰 미안함을 남겼습니다. 제 자신의 나태함과 두려움에 도망치려 했던 태도를 뼈저리게 반성한 한 주였습니다.


3. 핵심 기술 개념과 코드 분석: C 포인터의 주소 연산(*p vs &p)과 배열 감쇄(Array Decay)

3.1 C 포인터의 물리적 실체

포인터(Pointer)는 복잡한 마법이 아닙니다. 컴퓨터 아키텍처 관점에서 포인터 변수는 '메모리 주소 값을 담고 있는 8바이트(x86-64 기준) 크기의 단순 정수형 변수'에 불과합니다.

int x = 42;
int *p = &x;  // p는 x의 시작 메모리 주소를 저장
*p = 100;     // p가 가리키는 주소로 직접 찾아가(역참조) 4바이트 정수 100을 덮어씀
  • Call-by-Value의 한계 극복: C 언어는 기본적으로 모든 인자를 값으로 복사(Call-by-Value)하므로, 피호출 함수에서 원본 변수의 값을 변경하기 위해서는 반드시 변수의 메모리 주소(&x)를 포인터로 전달해야 합니다.
  • 배열 감쇄(Array Decay): 배열의 이름 arr은 함수의 인자로 전달되는 순간 배열 전체가 복사되지 않고, 첫 번째 원소의 시작 주소를 가리키는 포인터(&arr[0])로 강제 퇴화(Decay)합니다. 따라서 함수 내부에서 sizeof(arr)를 호출하면 배열의 전체 크기가 아닌 포인터 크기(8바이트)만 반환됩니다.

4. x86-64 레지스터 호출 규약(Calling Convention)과 Caller/Callee-saved 스택 보존 원칙

4.1 System V AMD64 ABI 6대 인자 레지스터

함수 호출 시 메모리 스택 대신 초고속 CPU 레지스터를 통해 인자를 전달하여 속도를 극대화합니다.

  • 1번째 인자: %rdi
  • 2번째 인자: %rsi
  • 3번째 인자: %rdx
  • 4번째 인자: %rcx
  • 5번째 인자: %r8
  • 6번째 인자: %r9
  • 반환값: %rax

4.2 레지스터 보존 규약의 하드웨어 원칙

CPU의 레지스터 개수는 한정되어 있으므로 함수 호출 시 레지스터 덮어쓰기 충돌이 발생합니다.

  • 호출자 보존 레지스터 (Caller-saved: %rax, %rcx, %rdx, %rsi, %rdi, %r8~%r11): 함수를 부르는 쪽(Caller)에서 값이 필요하다면 호출 전에 스택에 직접 백업해야 합니다. 피호출 함수는 이 레지스터들을 마음대로 덮어쓸 수 있습니다.
  • 피호출자 보존 레지스터 (Callee-saved: %rbx, %rbp, %r12~%r15): 호출당하는 쪽(Callee)에서 이 레지스터를 사용하려면 반드시 기존 값을 스택에 push해 두고, 함수를 반환(ret)하기 직전에 원래 값으로 pop하여 복원해 놓아야 합니다.

5. 협업과 소통의 성찰: 준비되지 않은 두려움의 결과와 정직한 학습 태도로의 대전환

4주차의 실패를 통해 인생의 가장 큰 교훈 중 하나를 얻었습니다. "두려움은 피한다고 사라지지 않으며, 준비되지 않은 두려움은 반드시 처참한 결과로 돌아온다"는 사실이었습니다.

어렵고 싫어하는 주제일수록 더 일찍 시작하고, 더 집요하게 준비했어야 했습니다. 나의 무지와 게으름을 포장하려 하지 않고 투명하게 인정하는 것, "내가 이 부분을 완벽히 이해하지 못했다"는 것을 정직하게 인정하는 것에서부터 진짜 엔지니어의 성장이 시작된다는 것을 깨달았습니다. 이 반성은 5주차 이후 저를 근본적으로 변화시키는 결정적인 분수령(Turning Point)이 되었습니다.


6. 실행 가능한 액션 플랜: 백지 도식화와 3분 설명 연습

  1. 백지 도식화 훈련: 새로운 시스템 개념을 배울 때 에디터를 끄고 백지에 메모리 주소, 레지스터 전송 흐름, 스택 프레임을 내 손으로 완벽히 그릴 수 있을 때까지 시각화하기.
  2. 동료 대상 3분 피치: 이해한 CS 주제를 동료에게 3분 이내로 비전공자도 알아들을 수 있을 만큼 쉽고 명확하게 구두로 설명하는 연습하기.
  3. 선제적 발표 준비: 다음 발표 기회가 주어지면 마감 전날이 아니라 최소 3일 전에 슬라이드와 대본 구성을 완료하고 실전 리허설을 3회 이상 거치기.

7. 4주차 핵심 엔지니어링 Q&A 3선

Q1. C 언어에서 포인터 역참조(*p) 연산 시 컴파일러는 메모리에서 몇 바이트를 읽어야 하는지 어떻게 아는가?

답변: 포인터 변수 자체는 단순히 8바이트 메모리 주소 값만 저장하지만, 컴파일러는 포인터 선언 시 지정된 기저 자료형(Base Type)의 메타데이터를 추적합니다. 예를 들어 char *p는 역참조 시 해당 주소로부터 1바이트를 읽는 기계어(movzbl)를 생성하고, int *p는 4바이트(movl), double *p나 구조체 포인터는 해당 자료형의 크기만큼 데이터를 읽거나 쓰는 명령어를 생성합니다.

Q2. x86-64 아키텍처에서 leaq 명령어가 단순 덧셈·곱셈 산술 연산에 널리 사용되는 하드웨어적 이유는?

답변: leaq(Load Effective Address) 명령어는 원래 메모리 피연산자의 주소 계산(Base + Index * Scale + Disp)을 위해 하드웨어 주소 생성 유닛(AGU)에서 실행되도록 설계되었습니다. 이 명령어는 메모리를 역참조하지 않고 주소 계산 결과만을 목적지 레지스터에 저장하므로, 단 한 클럭 사이클 내에 y = x * 4 + z + 8과 같은 복합 산술 연산을 플래그 레지스터(%rflags) 변경 없이 고속으로 처리할 수 있어 컴파일러가 범용 산술 연산 최적화에 적극 채택합니다.

Q3. C 언어에서 댕글링 포인터(Dangling Pointer)가 시스템 보안에 치명적인 이유는?

답변: free()를 통해 힙 메모리를 운영체제에 반환한 이후에도 포인터 변수는 여전히 과거의 메모리 주소를 가리키고 있습니다. 이 상태에서 해당 포인터를 다시 역참조하거나 값을 쓰는 경우, 이미 다른 객체나 공격자가 제어하는 데이터가 적재된 메모리를 무단 덮어쓰게 되어 Use-After-Free(UAF) 취약점이 발생하며, 함수 포인터 변조를 통한 임의 코드 실행 공격으로 이어질 수 있습니다.


크래프톤 정글 4주차 C 포인터 메모리 주소 역참조 및 x86-64 어셈블리 아키텍처 다이어그램

profile
그사세가 아닌 내세상으로 만들기

0개의 댓글