티스토리 뷰

ATmega128에서 CALL 명령어를 만났을 때 발생하는 일
ATmega128 · AVR Architecture

CALL 명령어를 만났을 때, 내부에서 무슨 일이 일어날까?

CPU가 함수를 호출하는 순간, PC 레지스터와 스택 메모리에서 벌어지는 일을 단계별로 살펴봅니다.

1. CALL 명령어를 만나기 전 — Fetch, Decode, Execute 반복

ATmega128의 CPU는 아래 세 단계를 끊임없이 반복하며 프로그램을 실행합니다. 이를 명령어 파이프라인 사이클이라고 합니다.

📥
Fetch
PC가 가리키는 Flash 메모리 주소에서 명령어를 읽어 IR에 저장
🔍
Decode
Instruction Decoder가 IR에 저장된 명령어를 해석
Execute
ALU 연산, 레지스터 이동 등 실질적인 작업 수행
↻ 다음 명령어로 반복
핵심 포인트: Fetch 단계에서 명령어를 읽어온 직후, PC(Program Counter) 레지스터는 다음 명령어의 주소를 가리키도록 명령어 크기만큼 자동으로 증가합니다.
구체적인 예시로 이해하기 — PC 레지스터의 변화

아래와 같은 간단한 어셈블리 코드가 있다고 가정합니다. 각 명령어는 2바이트(Word) 크기입니다.

0x0200 LDI R16, 0x42 ; R16 ← 0x42
0x0202 ADD R16, R17 ; R16 ← R16 + R17
0x0204 MOV R18, R16 ; R18 ← R16
PC = 0x0200
Fetch: 0x0200 주소의 LDI 명령어를 읽음 → PC가 자동으로 0x0202로 증가
PC = 0x0202
Fetch: 0x0202 주소의 ADD 명령어를 읽음 → PC가 자동으로 0x0204로 증가
PC = 0x0204
Fetch: 0x0204 주소의 MOV 명령어를 읽음 → PC가 자동으로 0x0206으로 증가

이처럼 CALL 명령어가 없는 경우 PC는 순차적으로 증가하며 코드를 실행합니다.

2. CALL 명령어를 만난 경우 — 스택과 PC의 대변화

Instruction Decoder가 해석한 명령어가 CALL임을 인식하면, 단순히 PC를 증가시키는 것이 아니라 두 가지 중요한 작업이 순서대로 발생합니다.

구체적인 예시 — CALL 명령어가 있는 코드
0x0200 LDI R16, 5 ; 준비 작업
0x0202 LDI R17, 3 ; 준비 작업
0x0204 CALL add_func ; ← CPU가 이 명령어를 만남! (4Byte) 0x0208 MOV R18, R16 ; CALL 이후 실행될 다음 명령어 (복귀 주소)
...
0x0350 ; add_func 함수의 시작 주소
0x0350 ADD R16, R17 ; 함수 내부
0x0352 RET ; 함수 종료 후 복귀

CALL 명령어는 4바이트이므로, 0x0204에서 읽으면 PC는 0x0208로 증가합니다. 이 0x0208이 바로 복귀 주소입니다.

2-1. 복귀 주소(PC 값)를 스택 메모리에 저장

함수 실행이 끝난 뒤 원래 흐름으로 돌아오려면, 이동하기 직전에 복귀 주소를 어딘가에 저장해야 합니다. ATmega128은 이를 스택(Stack)에 Push합니다.

1
CALL 명령어 Fetch 완료 → PC = 0x0208
0x0204의 CALL 명령어(4Byte)를 읽은 직후, PC는 자동으로 0x0208로 증가합니다. 이것이 함수 호출 후 돌아올 복귀 주소입니다.
2
PC 값(0x0208)을 Stack에 Push
현재 PC 값 0x0208을 SRAM 스택 영역에 저장합니다. ATmega128은 16비트 PC를 2바이트로 나눠 순서대로 Push하며, Stack Pointer(SP)는 2 감소합니다.
3
PC ← 함수 시작 주소(0x0350)로 점프
이제 PC에 CALL 명령어의 피연산자인 add_func의 주소 0x0350이 들어가고, CPU는 그 주소부터 실행을 시작합니다.

CALL 명령어 전후의 스택 변화:

CALL 실행 전

SRAM Stack SP = 0x10FF
← SP
— (비어있음) top
SP+1
SP+2
스택이 비어있는 상태

CALL 실행 후

SRAM Stack SP = 0x10FD
SP
0x02 (복귀 주소 상위) ← top
SP+1
0x08 (복귀 주소 하위)
SP+2
복귀 주소 0x0208이 2바이트로 나뉘어 저장됨
스택은 위에서 아래로 자랍니다!
ATmega128의 스택은 높은 주소 → 낮은 주소 방향으로 자랍니다. Push 할 때마다 SP 값이 감소하고, Pop 할 때 SP 값이 증가합니다.

2-2. 이전 Frame Pointer를 스택에 저장 (함수 프롤로그)

CALL 명령어가 복귀 주소를 스택에 Push하는 것은 하드웨어가 자동으로 수행합니다. 이후에는 컴파일러가 생성한 함수 프롤로그(Prologue) 코드가 실행되어, 현재 Frame Pointer(FP) 값을 스택에 추가로 저장합니다.

컴파일러가 생성하는 함수 프롤로그 어셈블리 예시
0x0350 ; === add_func 시작 (프롤로그) ===
0x0350 PUSH R29 ; ← 이전 FP(Y 레지스터 상위) 저장 0x0352 PUSH R28 ; ← 이전 FP(Y 레지스터 하위) 저장 0x0354 IN R28, SPL ; 새 FP ← 현재 SP로 설정
0x0356 IN R29, SPH ; (Y 레지스터 = Frame Pointer)
0x0358 ; 이제 함수 본문 실행...
0x0358 ADD R16, R17 ; 함수 본문

AVR에서 Frame Pointer는 Y 레지스터(R28:R29)를 사용합니다.

프롤로그 완료 후 스택의 최종 상태:

SRAM Stack (프롤로그 완료 후) SP = 0x10FB
SP
R28 (이전 FP 하위) ← top
SP+1
R29 (이전 FP 상위)
SP+2
0x02 (복귀 주소 상위)
SP+3
0x08 (복귀 주소 하위)
🔵 복귀 주소(Return Address)    🟣 이전 Frame Pointer — 함수 종료 시 이 순서의 역순으로 Pop됩니다
Frame Pointer를 저장하는 이유:
함수 내에서 지역 변수나 매개변수에 접근할 때 Frame Pointer를 기준으로 상대 주소를 계산합니다. 이전 Frame Pointer를 스택에 기억해두지 않으면, 함수 종료 후 이전 함수의 스택 프레임으로 올바르게 복원할 방법이 없어집니다.

3. 함수가 끝나면 — RET 명령어로 복귀

함수의 마지막에는 반드시 RET 명령어가 있습니다. CALL과 정확히 반대 과정을 수행합니다.

에필로그(Epilogue): FP 복원
컴파일러가 생성한 에필로그 코드가 스택에서 이전 Frame Pointer 값을 Pop하여 Y 레지스터를 복원합니다. (POP R28, POP R29)
RET 명령어: 복귀 주소 Pop → PC에 적재
스택에 저장해두었던 복귀 주소 0x0208을 꺼내어(Pop) PC에 넣습니다. CPU는 0x0208 주소부터 실행을 재개합니다.
원래 흐름 복귀 완료
CPU는 CALL 명령어 바로 다음인 0x0208MOV R18, R16 명령어를 이어서 실행합니다.

전체 Flash 메모리 흐름 한눈에 보기:

0x0200
LDI R16, 5준비
0x0202
LDI R17, 3준비
0x0204
CALL add_func🔵 함수 호출 (4Byte) — PC → 0x0350
0x0208
MOV R18, R16🟢 복귀 주소 — RET 후 여기서 재개
···
···
0x0350
PUSH R29 (프롤로그)🟣 add_func 시작
0x0352
PUSH R28 (프롤로그)🟣
0x0354
ADD R16, R17🟣 함수 본문
0x0356
RET🟣 복귀 → 스택에서 0x0208 꺼내어 PC에 적재

🎁 추가적으로 알면 좋은 부분 (심화 학습용)

CALL vs RCALL — 무엇이 다를까?

CALL

  • Flash 메모리 어느 주소든 호출 가능
  • 명령어 크기 4 Byte
  • 절대 주소(Absolute Address) 사용
  • 큰 프로젝트, 원거리 함수 호출에 적합

RCALL

  • 현재 PC 기준 ±2K 범위 내 함수만 호출
  • 명령어 크기 2 Byte
  • 상대 주소(Relative Address) 사용
  • 작은 프로젝트, 실행 속도 중요 시 적합

하버드 아키텍처 — Flash와 SRAM의 분리

ATmega128은 하버드 아키텍처를 채택하여, 프로그램 메모리와 데이터 메모리를 물리적으로 분리합니다.

💾

Flash 메모리

프로그램(명령어)이 저장되는 공간
PC가 항상 이 영역을 가리킴
읽기 전용, 비휘발성

🖥️

CPU (ATmega128)

별도의 버스로
Flash와 SRAM에
동시 접근 가능

📝

SRAM

데이터(변수, 스택)가 저장되는 공간
SP가 항상 이 영역을 가리킴
읽기/쓰기, 휘발성

요약: Program Counter(PC)는 항상 Flash 메모리를 가리키고, Stack Pointer(SP)는 항상 SRAM 영역을 가리킵니다. CALL 명령어는 PC를 Flash 내에서 이동시키고, 복귀 주소는 SRAM 스택에 저장합니다.

핵심 정리

📍

복귀 주소 Push

CALL 실행 시 다음 명령어 주소(PC+4)가 SRAM 스택에 자동 저장됩니다.

🖼️

Frame Pointer 저장

컴파일러의 프롤로그 코드가 이전 FP를 스택에 Push하여 이전 스택 프레임 기준점을 보존합니다.

↕️

스택은 아래로 자람

Push 시 SP 감소, Pop 시 SP 증가. 높은 주소 → 낮은 주소 방향입니다.

↩️

RET로 복귀

함수 끝의 RET 명령어가 스택에서 복귀 주소를 Pop하여 PC에 적재, 원래 흐름으로 돌아옵니다.

🎬 CALL 명령어 전체 흐름 — 인터랙티브 시뮬레이션

아래 버튼을 눌러 CALL 명령어가 실행되는 과정을 단계별로 직접 확인해보세요.

Step 0 / 6 ▶ [시작] 시뮬레이션을 시작하려면 Next를 누르세요
Flash Memory (Program)
0x0200LDI R16, 5
0x0202LDI R17, 3
0x0204CALL add_func
0x0208MOV R18, R16
···
0x0350PUSH R29
0x0352PUSH R28
0x0354ADD R16, R17
0x0356RET
CPU Registers
PC 0x0204
SP 0x10FF
FP (Y) 0x10FF
IR CALL add_func
General Purpose
R160x05
R170x03
R180x??
SRAM Stack (Top)
0x10FF
0x10FE
0x10FD
0x10FC
0x10FB
0x10FA
↓ 스택 성장 방향 ↓
SP (Stack Pointer)
0x10FF
시뮬레이션을 시작하면 각 단계에서 일어나는 일을 여기서 설명합니다.

📊 CALL ~ RET 전체 흐름 인포그래픽

CALL 명령어가 실행되고 RET로 복귀하기까지의 전체 과정을 한 장으로 정리했습니다.

Flash Memory CPU SRAM (Stack) 범례 명령어 복귀주소 Frame Ptr PC 이동 스택 Push 0x0204 CALL add_func 0x0208 MOV R18, R16 ← 복귀 주소 ··· 0x0350 PUSH R29 (프롤로그) 0x0352 PUSH R28 (프롤로그) 0x0354 ADD R16, R17 0x0356 RET ① Fetch 0x0204 읽음 → IR ← CALL add_func PC 자동 증가: 0x0204 → 0x0208 ② Decode Instruction Decoder: "CALL 명령어 감지!" ③ Execute (하드웨어) 복귀주소 0x0208을 Stack에 Push ④ PC ← 0x0350 (Jump) PC가 add_func 시작점으로 이동 ⑤ 함수 프롤로그 (소프트웨어) 이전 FP(Y 레지스터)를 Stack에 Push, 새 FP 설정 ⑥ 함수 본문 실행 ADD R16, R17 등 수행 ⑦ 에필로그 + RET FP 복원 → Stack에서 0x0208 Pop → PC에 적재 ⑧ 원래 흐름 복귀 0x0208 MOV R18, R16 재개 초기 상태 (비어있음) — (empty) ← SP CALL 실행 후 0x02 (복귀주소 상위) ← SP 0x08 (복귀주소 하위) 프롤로그 후 R29 (이전 FP 상위) ← SP R28 (이전 FP 하위) 0x02 (복귀주소 상위) 0x08 (복귀주소 하위) RET 실행 후 (Pop 완료) — (empty, 복원됨) ← SP Push Push PC Jump Pop 복귀

🔗 중첩 함수 호출 — 스택이 계속 쌓이면?

함수 안에서 또 다른 함수를 호출하면, 스택에 스택 프레임이 차곡차곡 쌓입니다. 이것이 바로 재귀 호출이 가능한 이유이고, 너무 깊이 중첩되면 스택 오버플로우가 발생하는 이유이기도 합니다.

중첩 호출 예시 — main → func_A → func_B
; main에서 func_A 호출
main:
  CALL func_A ; 스택에 복귀주소①(main+) Push
  ...

; func_A에서 func_B 호출
func_A:
  ; 프롤로그: 이전 FP(main의 FP) Push
  CALL func_B ; 스택에 복귀주소②(func_A+) Push
  ...
  RET ; 복귀주소① 꺼내어 main으로 복귀

func_B:
  ; 프롤로그: 이전 FP(func_A의 FP) Push
  ...
  RET ; 복귀주소② 꺼내어 func_A로 복귀

func_B 실행 중 스택 상태 (위 = 낮은 주소, 스택 top):

SRAM Stack — func_B 실행 중 SP = 최솟값
SP →
func_A의 FP (이전 FP)func_B 프레임 ↑
복귀주소② (func_A+N)func_B 프레임
main의 FP (이전 FP)func_A 프레임 ↑
복귀주소① (main+M)func_A 프레임
main의 지역변수 등main 프레임
RET를 만날 때마다 프레임 하나씩 해제되며 위에서부터 순서대로 Pop됩니다
스택 오버플로우(Stack Overflow)란?
ATmega128의 SRAM은 4KB로 제한되어 있습니다. 함수 호출이 너무 깊이 중첩되거나 재귀 호출이 끝없이 반복되면, SP가 계속 감소하여 다른 데이터 영역을 침범하게 됩니다. 이것이 스택 오버플로우이며 임베디드 시스템에서 매우 치명적인 버그입니다.

📋 전체 요약 — 한눈에 보기

단계 수행 주체 PC 변화 SP 변화 설명
① Fetch 하드웨어 (CU) 0x0204 → 0x0208 변화 없음 CALL 명령어 읽기, PC 자동 증가 (+4)
② Decode 하드웨어 (CU) 유지 (0x0208) 변화 없음 IR의 명령어 해석 → CALL 확인
③ Push 복귀주소 하드웨어 유지 (0x0208) −2 감소 0x0208을 SRAM 스택에 Push
④ PC Jump 하드웨어 0x0208 → 0x0350 변화 없음 PC ← add_func 시작 주소
⑤ 프롤로그 소프트웨어 (컴파일러) 0x0350 → 0x0354 −2 감소 이전 FP(Y reg) Stack에 Push, 새 FP 설정
⑥ 함수 본문 소프트웨어 순차 증가 지역변수 따라 변화 실제 기능 수행
⑦ 에필로그+RET 소프트웨어+하드웨어 → 0x0208 (복귀) +4 증가 FP 복원, Stack에서 복귀주소 Pop → PC 적재
⑧ 복귀 완료 하드웨어 0x0208 →순차증가 CALL 전과 동일 원래 흐름 재개 (MOV R18, R16)
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/06   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함