Home 레지스터는 어떻게 이전 문맥을 기억할까?
Post
Cancel

레지스터는 어떻게 이전 문맥을 기억할까?


아직 작성 중입니다.




1. 레지스터


CPU 레지스터는 프로그램 실행 상태 그 자체입니다. 지금 어떤 명령어를 실행 중인지, 다음에 어디로 이동해야 하는지, 계산 중인 값이 무엇인지가 모두 레지스터에 들어 있습니다. CPU는 메모리를 읽지 않고도 즉시 실행하기 위해, 현재 실행에 필요한 모든 정보를 레지스터에 올려 둔 상태로 동작합니다. 따라서 레지스터의 내용이 곧 “지금 이 프로그램이 어디까지 실행되었는가”를 의미합니다.

하지만 레지스터는 개수가 제한되어 있고, 함수 호출이나 인터럽트, 스레드 전환이 발생하면 다른 실행 흐름이 같은 레지스터를 사용해야 합니다. 이때 기존 레지스터 값을 유지하지 않으면, 원래 실행 중이던 코드로 되돌아올 수 없습니다. 그래서 CPU와 운영체제는 레지스터 값을 메모리에 저장해 두었다가 다시 복원하는 방식으로 문맥을 기억합니다.




함수 호출에서의 레지스터 문맥 보존

함수 호출은 실행 흐름이 잠시 다른 코드로 이동했다가 다시 돌아오는 동작입니다. 이 “다시 돌아오기”가 정확히 이루어지려면, 함수 호출 이전의 레지스터 상태가 반드시 보존되어야 합니다. 이를 위해 함수 호출 규약에서는 일부 레지스터를 보존 대상으로 지정하고, 해당 레지스터를 사용하는 함수가 진입 시 그 값을 스택에 저장하도록 규정합니다. 이 방식 덕분에 함수 호출 전후의 실행 상태는 끊김 없이 이어질 수 있습니다.

1
2
3
4
5
6
# 함수 호출 직전

Registers
RIP = 0x100        ← 다음에 실행할 명령 주소
RSP = 0x8000       ← 스택 꼭대기
RBX = 10           ← 중요한 값 (caller가 계속 쓰고 싶음)




함수가 호출되면, 가장 먼저 호출 이후 다시 돌아오기 위한 복귀 위치가 스택에 저장됩니다. 이어서 함수 내부에서 사용될 가능성이 있는 보존 대상 레지스터의 값이 스택에 복사됩니다. 이 시점에서 중요한 점은, 레지스터 값이 소멸되는 것이 아니라 다른 저장 공간인 스택으로 옮겨져 보관된다는 사실입니다.

1
2
3
4
5
6
# 함수 진입 시 스택
┌────────────────────┐
│ return address     │  ← 복귀 위치
├────────────────────┤
│ saved RBX          │  ← 레지스터 백업
└────────────────────┘




레지스터 값이 스택에 저장된 이후에는, 함수 내부에서 해당 레지스터를 자유롭게 사용할 수 있습니다. 함수는 자신의 로직을 수행하면서 레지스터 값을 변경할 수 있으며, 이 변경은 호출자의 실행 상태에 영향을 주지 않습니다.

스택은 함수 호출 흐름에 따라 자동으로 확장·축소되며, 각 함수 호출마다 독립적인 스택 프레임이 생성됩니다. 이 스택 프레임은 현재 실행 중인 함수만 사용하며, 함수가 종료되면 즉시 폐기되기 때문에 레지스터 값이 다른 함수에 의해 덮어쓰이거나 변경되지 않고 안전하게 보존됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
# 함수 호출에 따른 스택 프레임 분리

caller stack frame
┌────────────────────┐
│ caller local data  │
└────────────────────┘
----------------------
callee stack frame   ← 현재 함수 전용
┌────────────────────┐
│ saved registers    │
│ local variables    │
└────────────────────┘




즉, 호출자가 사용하던 레지스터 값은 이미 스택에 안전하게 저장되어 있기 때문입니다.

1
2
3
4
5
6
7
8
9
10
11
12
# 함수 진입 직후 (레지스터 문맥 백업 완료)

Registers
RBX = 10          ← 아직 원래 값
RSP = 0x7FF0

Stack
┌────────────────────┐
│ saved RBX = 10     │  ← 호출자 레지스터 값 보관
├────────────────────┤
│ return address     │
└────────────────────┘




1
2
3
4
5
6
7
8
9
10
11
12
# 함수 내부 실행 중

Registers
RBX = 999         ← 함수 로직에서 마음대로 변경
RSP = 0x7FF0

Stack
┌────────────────────┐
│ saved RBX = 10     │  ← 호출자 값은 그대로 유지
├────────────────────┤
│ return address     │
└────────────────────┘
1
2
3
4
5
6
7
8
9
10
# pop RBX 실행 후

Registers
RBX = 10          ← 호출자 레지스터 문맥 복원
RSP = 0x7FF8

Stack
┌────────────────────┐
│ return address     │
└────────────────────┘




함수 실행이 끝나면, 저장해 두었던 레지스터 문맥을 다시 복원해야 합니다. 함수 종료 직전에는 스택에 보관된 레지스터 값을 다시 레지스터로 되돌리는 작업이 수행되며, 이를 통해 레지스터 상태는 함수 호출 이전과 동일한 상태가 됩니다. 이 시점의 스택 구조는 다음과 같습니다.

1
2
3
4
5
6
7
# 함수 종료 직전 스택 상태

┌────────────────────┐  ← RSP
│ saved RBX          │
├────────────────────┤
│ return address     │
└────────────────────┘




먼저 스택의 최상단에 저장되어 있던 레지스터 값을 레지스터로 복원합니다.

1
pop RBX




이 명령은 스택의 최상단에 저장된 값을 RBX 레지스터로 되돌리고, 스택 포인터를 한 단계 이동시킵니다. 이어서 반환 명령이 실행됩니다. ret 명령은 스택에 저장된 반환 주소를 명령 포인터로 복원하여, 실행 흐름을 호출자 코드로 되돌립니다. 이 과정이 완료되면 스택은 함수 호출 이전 상태로 정리되며, 함수 호출의 흔적은 남지 않습니다.

1
ret





인터럽트 발생 시의 레지스터 문맥 저장

인터럽트는 함수 호출과 달리, 현재 실행 중인 코드를 강제로 중단시킵니다. CPU는 인터럽트가 발생하면, 먼저 최소한의 실행 문맥을 하드웨어 수준에서 자동으로 저장합니다. 이 저장은 프로그램의 의사와 무관하게 즉시 이루어집니다.

1
2
3
4
5
6
7
8
# CPU 자동 저장 영역 (하드웨어)
┌────────────────────┐
│ RIP (다음 명령어)     │
├────────────────────┤
│ CS                 │
├────────────────────┤
│ RFLAGS             │
└────────────────────┘




이 정보는 “어디까지 실행했는지”를 기억하기 위한 최소 조건입니다. 이후 커널은 인터럽트 핸들러로 진입하면서, 나머지 일반 레지스터들을 소프트웨어적으로 스택에 저장합니다. 인터럽트 처리가 끝나면, 저장된 레지스터 값을 다시 CPU에 복원하고, 중단되었던 명령어부터 실행을 재개합니다. 프로그램 입장에서는 아무 일도 없었던 것처럼 보이지만, 실제로는 레지스터 문맥이 통째로 저장·복원된 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 커널 스택에 저장되는 전체 문맥
┌────────────────────┐
│ RIP / CS / FLAGS   │
├────────────────────┤
│ RAX                │
├────────────────────┤
│ RBX                │
├────────────────────┤
│ RCX                │
├────────────────────┤
│ RDX                │
├────────────────────┤
│ RSI, RDI, ...      │
└────────────────────┘




스레드 전환에서의 레지스터 문맥 교체

스레드 전환은 실행 흐름 자체가 완전히 바뀌는 경우입니다. 이때는 일부 레지스터가 아니라 CPU 레지스터 전체가 문맥이 됩니다. 운영체제는 각 스레드마다 레지스터 상태를 메모리에 보관합니다.

1
2
3
4
5
6
7
# Thread A 실행 중

Registers
RIP = 0xAAA
RSP = 0x1000
RAX = 5
RBX = 7



스케줄러가 개입하면, 현재 레지스터 값은 Thread A의 문맥으로 저장됩니다.

1
2
3
4
5
6
7
8
# Thread A Context (메모리)

┌────────────────────┐
│ RIP = 0xAAA        │
│ RSP = 0x1000       │
│ RAX = 5            │
│ RBX = 7            │
└────────────────────┘




이후 Thread B의 문맥이 레지스터로 복원됩니다. CPU는 즉시 Thread B의 실행을 이어갑니다. 이 과정에서 CPU는 “전환”을 인식하지 못하며, 단지 레지스터 값이 바뀐 상태로 실행을 계속할 뿐입니다.

1
2
3
4
5
6
# Thread B Context → Registers

RIP = 0xBBB
RSP = 0x2000
RAX = 42
RBX = 9

This post is licensed under CC BY 4.0 by the author.

메모리 배리어와 가시성: while 루프는 왜 멈추었을까?

Stack, Stack Frame