📌 스택 메모리란?

1. 정의

함수 호출 시 필요한 데이터를 임시로 저장하는 메모리 영역
후입선출(LIFO: Last In First Out) 구조
높은 주소 → 낮은 주소 방향으로 데이터 저장


2. 스택의 역할

구분역할
매개변수 전달함수 호출 시 인자(매개변수) 저장
반환주소 저장함수 실행 후 돌아갈 위치 저장
지역변수 저장함수 내부에서 사용하는 변수 저장
레지스터 보존호출자/피호출자 레지스터 값 임시 저장

3. 주요 레지스터

레지스터역할
rspStack Pointer, 스택 최상단 가리킴
rbpBase Pointer, 스택 프레임의 기준점
ripInstruction Pointer, 다음 명령어 위치

📌 스택 프레임이란?

각 함수가 독립적으로 사용할 수 있도록 구성된 스택 영역
함수 호출 시 생성 → 함수 종료 시 정리

구분위치
반환주소호출한 위치 주소
이전 rbp이전 함수 스택 프레임 기준점
매개변수호출자가 넘긴 인자값들
지역변수해당 함수가 사용하는 변수들

📂 전체 코드 및 상세 분석


📂 헤더 및 섹션 설정

%include "io64.inc"
  • 입출력 매크로 포함 (PRINT_DEC, PRINT_STRING, NEWLINE 사용 가능)
section .text
global CMAIN
  • section .text: 실행 코드 영역
  • global CMAIN: 프로그램 진입점 지정

📂 메인 함수 시작

CMAIN:
    mov rbp, rsp ; 현재 스택 위치를 RBP에 저장 (디버깅용)
  • rbp, rsp 관계:
    ⭕️ 호출자 프레임 (이전 rbp)
    ⭕️ 나의 프레임 (현재 rbp = 현재 rsp)

📂 스택 동작 연습 (push & pop)

;push 1
;push 2
;push 3

;pop rax
;pop rbx
;pop rcx
  • push: 스택에 값 저장 (8바이트씩)
  • pop: 스택에서 값 꺼내기 (8바이트씩)
  • 순서 확인:
    1. push 1
    2. push 2
    3. push 3
      => pop 순서: 3 → 2 → 1 (후입선출)

📂 스택 기반 MAX 함수 호출

push 1        ; 두 번째 인자 (1)
push 2        ; 첫 번째 인자 (2)
call MAX      ; MAX 함수 호출
  • 스택에 값 저장 후 call MAX
  • 호출 규약: 마지막 인자부터 push
  • 호출하면:
    • call이 자동으로 ret 주소 push
    • MAX 진입

📂 반환값 출력 & 스택 정리

PRINT_DEC 8, rax
NEWLINE
add rsp, 16   ; 인자 2개 pop (8+8=16)
  • rax에 반환된 결과 출력
  • 스택에 남은 매개변수 영역 정리 (add rsp, 16)

📂 기존 레지스터 복원 예제 (비활성화)

;pop rbx
;pop rax
  • 원래는 push rax, push rbx로 보존 후 복원 가능

📂 종료 처리

xor rax, rax
ret
  • 반환값 0 설정 후 종료

📂 MAX 함수 정의


📂 진입 시 스택 프레임 구성

MAX:
    push rbp          ; 이전 rbp 저장 (호출자 프레임 보존)
    mov rbp, rsp      ; 나의 프레임 시작점 설정
  • 새로운 스택 프레임 구성
  • rbp+16, rbp+24로 인자 접근 가능 (64bit 환경)

📂 매개변수 읽기

mov rax, [rbp+16]  ; 첫 번째 인자 (2)
mov rbx, [rbp+24]  ; 두 번째 인자 (1)
  • rbp+16: call 직후 push된 첫 인자
  • rbp+24: 그 위에 push된 두 번째 인자

📂 비교 및 큰 값 반환

cmp rax, rbx
jg L1
mov rax, rbx      ; rbx가 더 크면 rax에 저장
  • 두 값 비교
  • 큰 값 선택 (rax = max(a, b))

📂 정리 및 복귀

L1:
    pop rbp
    ret
  • 이전 rbp 복원 (호출자 프레임으로 복귀 준비)
  • 원래 위치로 돌아감 (call했던 다음 명령어로)

📂 데이터 섹션

section .data
msg db 'Hello World', 0x00
  • 메시지 데이터 (필요시 활용)

📂 전체 스택 흐름도

스택 구성 예시 (64비트 환경)

  [rbp+24] → 두 번째 인자 (1)
  [rbp+16] → 첫 번째 인자 (2)
  [rbp+8]  → 반환주소 (call 자동 push)
  [rbp]    → 이전 함수의 rbp

   ↑
  rsp (현재 스택 top)

📂 스택 기반 함수 호출 흐름 정리

단계동작
인자 push (역순)
call로 진입 (ret 주소 push)
rbp push (이전 프레임 보존)
매개변수 읽기 (rbp+16, rbp+24)
로직 처리 (비교 & 반환값 저장)
rbp pop (프레임 복원)
ret (호출자 복귀)
add rsp로 인자 정리

좋아요! 주신 어셈블리 코드, 블로그 노트(스택/스택 프레임 정리), 강의 텍스트 본을 한데 모아, 실습·개념·디버깅·퀴즈까지 포함한 “한 번에 끝내는 x86-64 스택 & 스택 프레임 학습자료”로 재구성했습니다.
(모두 NASM + io64.inc 기준, 64비트 관점으로 설명합니다.)


1) 학습 목표

  • 스택 메모리/스택 프레임의 구조흐름을 그림 없이 머릿속에 그릴 수 있다.
  • push/pop, call/ret, rbp/rsp로 매개변수·반환주소·지역변수 위치를 정확히 계산한다.
  • 프로로그/에필로그(function prologue/epilogue) 패턴을 이해하고 직접 구현한다.
  • 호출 규약(ABI) 차이, 정렬(align), 보존(callee-saved) 레지스터 규칙을 안다.
  • 디버거로 RSP/RBP 추적, 스택 오염(crash) 원인 진단을 할 수 있다.

2) 개념 핵심 요약

2.1 스택(⚙️ 실행 중 임시 저장소)

  • LIFO(후입선출), 높은 주소 → 낮은 주소 방향으로 “확장”.

  • 함수 호출 시

    • 반환주소(return address)
    • 이전 RBP
    • 매개변수
    • 지역변수/임시 보관 레지스터
      등이 순차적으로 쌓였다가 함수 종료 시 함께 정리.

2.2 스택 포인터와 프레임 포인터

  • RSP: “커서” 같은 현재 스택 최상단. push하면 8바이트 감소, pop하면 8바이트 증가.
  • RBP: 현재 함수의 기준점. 스택이 오르내려도, 매개변수/지역변수에 안정적 상대주소로 접근.

2.3 전형적 함수 골격(프로로그/에필로그)

push rbp       ; 이전 프레임의 기준점 저장
mov  rbp, rsp  ; 내 프레임의 기준점 고정
; (필요 시) sub rsp, locals   ; 지역변수 공간 확보

; ... 함수 본문 ...

; (필요 시) add rsp, locals   ; 지역변수 반납
pop  rbp
ret

2.4 콜/리턴과 매개변수 위치

  • call func반환주소 8바이트를 자동 push.

  • 진입 직후 프레임 기준(64비트, RBP 사용 시) 주소:

    • [rbp + 8] : 반환주소
    • [rbp + 16] : 1번째 인자
    • [rbp + 24] : 2번째 인자
  • 지역변수는 rbp - 8, rbp - 16 … 처럼 음수 오프셋으로 잡는다.


3) 주신 코드 – 주석 달린 정독본 & 핵심 포인트

%include "io64.inc"

section .text
global CMAIN

CMAIN:
    mov rbp, rsp     ; (디버깅용) 현재 스택 위치를 RBP에 맞춰보기

    ; [데모] 스택으로 인자 전달하여 MAX 호출
    push 1           ; 두 번째 인자
    push 2           ; 첫 번째 인자
    call MAX         ; call이 반환주소를 push
    PRINT_DEC 8, rax ; 반환값 출력 (rax에 최대값)
    NEWLINE

    add rsp, 16      ; 호출자 정리(caller clean-up): 인자 2개 반납(2 * 8B)

    xor rax, rax
    ret

MAX:
    push rbp
    mov  rbp, rsp

    ; [rbp+16] = 첫 번째 인자, [rbp+24] = 두 번째 인자
    mov rax, [rbp+16]
    mov rbx, [rbp+24]
    cmp rax, rbx
    jg  .L1
    mov rax, rbx
.L1:
    pop rbp
    ret

section .data
msg db 'Hello World', 0

이 코드로 꼭 짚을 점

  • 직접 스택으로 인자 전달: x86-64의 표준 호출규약과 달라도, 호출자와 피호출자 합의만 맞으면 동작합니다. 본 예제는 학습용으로 스택 오프셋을 선명하게 보여주기 위함.
  • 정리(add rsp, 16) 누락 시 크래시: call 직후 스택엔 [반환주소][인자1][인자2]…가 쌓여 있음. 인자 정리를 안 하면 RET가 엉뚱한 주소로 돌아가서 크래시.
  • RBX 사용: x64에서 RBX는 callee-saved(보존 레지스터)입니다(Windows/System V 공통). 학습 코드에선 문제가 없지만, 일반적으로는 함수에서 push rbx / … / pop rbx보존하는 습관 권장.

4) 강의 내용 → 실전 감각 포인트 정리

  • 메모리 뷰에서 push 결과 확인: 리틀엔디안으로 낮은 바이트부터 저장됨을 직접 본다.
  • pop은 메모리를 “지우지” 않는다: 단지 RSP만 이동. 이 영역은 “유효 범위 밖”이 될 뿐.
  • RSP로 주소 계산은 위험: 중간에 push/pop이 생기면 기준점이 흔들림 → RBP를 기준으로 오프셋 접근.
  • 스택을 스크래치 패드처럼 쓰기: 지역변수/임시 저장용으로 rbp - N 구간 확보 후 사용.
  • 정렬(Alignment): x86-64에선 16바이트 정렬 요구가 일반적. 호출 시점에 정렬이 어긋나면 SSE/ABI 규칙 위반으로 크래시/성능저하 가능.

5) 호출 규약(ABI) 간단 비교 (학습 코드와의 차이 이해)

항목Windows x64System V AMD64 (리눅스/맥)본 학습 코드
1~4번째 정수 인자RCX, RDX, R8, R9RDI, RSI, RDX, RCX (그다음 R8, R9)스택으로 직접 push
호출자 “쉐도우 스페이스”32바이트 필요없음사용 안 함
보존 레지스터(예)RBX, RBP, RDI, RSI, R12~R15RBX, RBP, R12~R15RBX를 사용했으면 보존 권장
스택 정렬호출 시 16B 정렬 유지호출 직전 rsp % 16 == 8 (call 후 함수 진입 시 16B 정렬)데모라 엄격히 안 맞췄지만, 실전은 정렬 고려

TIP: 학습 목적으론 “스택으로 인자 전달”이 오프셋 구조를 배우기에 가장 쉽습니다.
실전(특히 C/C++ 연동)에서는 각 OS의 ABI를 반드시 준수하세요.


6) 디버깅 체크리스트 (크래시 방지)

  1. 인자 정리 확인

    • 스택으로 인자 push했다면, 호출자add rsp, 8*n으로 반드시 반납.
  2. 프로로그/에필로그 쌍

    • push rbppop rbp 짝 맞는지, 중간에 ret 경로 누락 없는지.
  3. 보존 레지스터

    • RBX, RBP 등 callee-saved는 함수 내부에서 사용 시 push/pop으로 보존.
  4. 스택 정렬

    • 호출 전/진입 후 정렬 상태 점검. (SSE/AVX 사용 시 특히 중요)
  5. RSP를 기준으로 주소계산 금지

    • 임시 push가 끼면 모든 오프셋이 틀어짐 → RBP 기준으로만.

7) 실습: 난이도별 과제

7.1 과제 A — MIN 함수 만들기 (워밍업)

  • MAX를 복제해 MIN으로 바꾸고, 비교 분기만 반대로 작성.
  • 호출: push 7, push 3, call MIN → 결과 3 확인.
  • 해설 포인트: [rbp+16], [rbp+24] 위치는 그대로, 비교만 반대로.

7.2 과제 B — 3개 인자 MAX3(a,b,c)

  • 호출부: push cpush bpush acall MAX3
  • 피호출자: [rbp+16]=a, [rbp+24]=b, [rbp+32]=c
  • 비교 흐름: rax ← max(a,b)raxc 비교.

7.3 과제 C — 지역변수 사용(스택을 메모장처럼)

  • sub rsp, 16으로 16바이트 지역영역 확보.
  • [rbp-8]에 중간 최대값을 저장했다가 다시 꺼내 비교.
  • 종료 전 add rsp, 16로 반납.

7.4 과제 D — RBX 보존 규칙 적용

  • MAX 시작 시 push rbx → 끝에 pop rbx.
  • 중간에 rbx 사용 로직을 넣어도 호출자 레지스터 오염 없음.

8) 개선 버전 코드(권장 습관 반영)

8.1 스택 인자 전달 + 보존/지역영역 + 정리

; MAX(a, b): rax에 최대값 반환
MAX:
    push rbp
    mov  rbp, rsp

    ; 보존 레지스터 사용 시 보존
    push rbx

    ; (옵션) 지역변수 16바이트 확보
    sub  rsp, 16

    mov  rax, [rbp+16]    ; a
    mov  rbx, [rbp+24]    ; b
    cmp  rax, rbx
    cmovl rax, rbx        ; rax < rbx 이면 rax ← rbx (분기 없는 조건이동)

    ; 지역변수 반납
    add  rsp, 16

    pop  rbx
    pop  rbp
    ret

cmov*는 분기 오염(branch mispredict) 줄이는 데 유리하고, 코드도 간결합니다.

8.2 (참고) Windows x64 ABI 스타일로 인자 받기

실전 C/C++ 연동 시 필수. (RCX=첫째, RDX=둘째)

; MAX_RCX_RDX(rcx=a, rdx=b) → rax = max(a,b)
MAX_RCX_RDX:
    push rbp
    mov  rbp, rsp
    push rbx               ; 보존
    ; 쉐도우 스페이스(필요 시) sub rsp, 32  ; 호출자 쪽에서 확보하는 게 규약이지만
                                 ; 독립 테스트용으로 잡아도 됨(호출 규약에 유의)
    mov  rax, rcx
    mov  rbx, rdx
    cmp  rax, rbx
    cmovl rax, rbx
    ; add rsp, 32
    pop  rbx
    pop  rbp
    ret

9) 스택 흐름 그림(텍스트 버전)

함수 MAX 진입 직후 (64-bit, RBP 고정 프레임)

   ↑ 높은 주소
   |  [rbp+24]  = b (두 번째 인자)
   |  [rbp+16]  = a (첫 번째 인자)
   |  [rbp+8]   = 반환주소
rbp→|  [rbp]     = 이전 RBP (push rbp로 저장됨)
   |  [rbp-8]   = 지역변수1 (예시)
   |  [rbp-16]  = 지역변수2 (예시)
   ↓ 낮은 주소

10) 퀴즈 & 해설

  1. Q. MAX에서 [rbp+8]엔 무엇이 있나요?
    A. 반환주소. call이 자동으로 push한 값.

  2. Q. push 1push 2call MAX를 했는데, MAX 내부에서 첫 번째 인자는 어디?
    A. [rbp+16] (그다음 인자 [rbp+24]).

  3. Q. add rsp, 16을 빼먹으면 왜 크래시가 날 수 있나요?
    A. 인자들이 스택에 남아 RET가 잘못된 주소로 복귀하려 하기 때문.

  4. Q. RSP 기준으로 매개변수 주소를 잡으면 왜 위험한가요?
    A. 중간에 push/pop이 끼면 RSP가 변해서 오프셋이 뒤틀림.

  5. Q. x64에서 보존해야 할 레지스터 예시는?
    A. RBX, RBP(공통). (Windows: RDI, RSI, R12~R15도 보존 대상)


11) 자주 터지는 오류 모음(원인 ➜ 해결)

  • 증상: 함수 끝에서 ret 직후 크래시
    원인: 인자 정리 누락, RSP 어긋남 ➜ add rsp, 8*n 또는 호출 규약 준수
  • 증상: 다른 함수 호출 후 레지스터 값이 이상
    원인: callee-saved 레지스터 미보존 ➜ push/pop으로 보존
  • 증상: SSE 사용 함수에서 이상 동작
    원인: 스택 16바이트 정렬 불일치 ➜ 호출 전 정렬 확인(패딩 삽입)
  • 증상: 매개변수 값이 가끔씩만 이상
    원인: RSP 기준 주소계산 ➜ RBP 고정 후 [rbp+offset] 사용

12) 체크리스트(프로젝트 적용 전)

  • 함수마다 프로로그/에필로그 올바르게 짝 맞춤
  • 보존 레지스터 사용 시 push/pop
  • 인자 정리(caller 또는 callee) 책임 일관성
  • 16B 정렬 유지
  • RBP 기준으로 매개변수/지역변수 접근
  • 디버거에서 RSP/RBP와 스택 메모리 패널로 흐름 확인

13) 확장 과제(심화)

  • 가변 인자(예: sum(n, a1, ..., an))를 스택에서 순회하며 합산.
  • 재귀 호출(팩토리얼/피보나치)로 프레임 체인(이전 RBP)을 눈으로 확인.
  • 예외 상황: 인자 없이 ret만 호출하면? (반환주소가 없으니 즉시 크래시!)

필요하시면 위 자료를 노션/블로그용 마크다운 템플릿으로도 정리해 드릴게요.
또, 현재 코드로 Windows x64 / System V AMD64 ABI 준수 버전(레지스터 인자 전달)도 바로 만들어 드릴 수 있어요.

profile
李家네_공부방

0개의 댓글