[C++] C++에서 사용되는 개념 4탄

Patrick!·2022년 7월 24일
0
post-thumbnail

스텍 메모리와 스택 프레임 ?

C++ 프로그래밍을 하는데 있어 나중에 버그가 일어났을떄 도움이 되는 부분이라고 생각한다.

1. 스택 메모리는 무엇인가?!

엔지니어가 코드를 실행하면 해당 코드는 Ram에 올라가게 된다 하지만 올라가는 data는 조금 다르게 작동하게 된다

a. 컴파일 시 크기가 결정되는 Code, Data, Bss
b. Run Time 시에 크기가 결정되는 Heap, STACK 으로 나뉘게 된다.

Stack 은 점차 커졌다가 작아지는 원리로 작용한다?!

Stack 은 높은 주소에서 낮은 주소로 사용된다.
기본적으로 어떤 프로그렘을 실행 시킬 때, main() func1() func2() 가 차례대로 실행된다고 해보자.

그럼 main()의 매개변수, 변환주소값, 지역변수 등이 먼저 Stack에 쌓이고 func1()을 호출한다.
호출된 func1()의 매개변수, 변환주소값, 지역변수 등이 Stack에 쌓이고 이후 func2()가 실행되면서 Stack이 점차 늘어나는 구조이다.

이후 func2() -> func1() -> main() 순으로 종료가 된다면 Stack도 차례대로 줄어들게 된다.

스택 메모리, 스택 프레임
다음의 개념들을 이번에 알아야 한다.

  • 포인터 레지스터 (포인터 = 위치를 가리키는 개념)
  • ip (Instruction Pointer) : 다음 수행 명령어의 위치
  • sp (Stack Pointer) : 현재 스택 top 위치 (일종의 cursor)
  • bp (Base Pointer) : 스택 상대 주소 계산용
mov rbp, rsp;

push 1
push 2
push 3

위의 코드를 실행해보면 Register 에 rip 가 있는 것을 확인할 수 있고, 다음에 실행될 명령어의 주소를 가리키고 있다.
ex) rip - 0x60fe38 (HEX)

여기서 그럼 rip는 어디서 이 값을 가져오는 걸까?

이는 우리가 Code 를 실행할 때, 코드의 영역을 rip가 가리키고 있다는 것을 알 수 있다.

이제 위의 코드에서 push 1을 실행시키면 어떻게 될까?

Register의 rsp를 살펴보면 push 1 의 명령어를 실행 하였기에 data가 들어간 위치에서 cursor가 깜빡이고 있는 상태이다.
rsp = 0x60fe30 (HEX) 를 가리키고 있다.

이후, push 2, 3 을 진행하면 마지막에 실행한 push 3 위치에서 sp가 가리키고 있는 것을 볼 수 있다.

pop rax
pop rbx
pop rcx

이번에는 push한 data를 꺼내보자

pop rax rbx rcx 를 차례대로 실행해본 결과.
rax = 3 , rbx = 2, rcx = 1 순으로 뽑힌 것을 확인할 수 있다.

2. 스택 프레임은 무엇인가?!

여기서 중요한 것은 rsp가 pop를 진행할 때도 변화한다는 것이다!

pop rax rbx rcx를 진행하면서 sp(cursor) 의 값은 지속적으로 변화하게 된다.
마지막으로 실행을 했다면 rsp = 0x60fe38 (HEX) 를 가리키고 있게 된다는 것이다.

자 이번엔 단순하게 데이터를 push 를 하는게 아닌 함수도 넣어서 해보자

mov rbp, rsp;

push 1
push 2
call MAX

xor rax, rax
ret

MAX: 
	push rbp
    mov rbp, rsp
    
    pop rbp
	ret

함수 MAX를 생성했다. 그리고 이를 call MAX 를 실행하게 되면, func MAX 를 실행하게 된다.
이후 ret를 통해 함수가 끝나면 call MAX 주소로 돌아가게 된다.

여기서 궁금한 점은 어떤 원리로 call MAX에 돌아가게 되는 것인가?

우선 call MAX 를 실행하게 되면 위의 개념 중 Instruction Pointer 가 돌아갈 주소 값을 기억하고 있기에 함수가 종료 이후에도 진행했던 위치로 돌아가게 되는 것이다.

MAX: 
	push rbp
    mov rbp, rsp
    
    pop rbp
	ret

push rbp -> mov rbp, rsp 가 실행되게 되는데

mov rbp, rsp 왜 실행해야 하는 것인가?

Stack은 계속해서 높은 메모리에서 낮은 메모리로 이동하는 구조를 가지고 있다.
그렇기에 지금은 MAX()의 Stack Memory에 rsp 를 rbp에 넣어 해당 영역을 지정하는 개념이다.

이 부분이 상당히 이해하기에는 힘들다... (처음에는 말이다.)

하지만 쉽게 생각을 해보자.

MAX()가 ret를 실행한 이후, 시스템은 call MAX 위치로 ret하게 된다.
이는 이전의 rip가 해당 주소값을 기억하고 있기 때문이다.

그럼 함수 안에서 다른 함수를 호출한다면 이는 어떻게 되는걸까?

rip는 이미 사용하고 있기에 MAX() 에서 MIN() 이라는 함수를 호출했다.
이후 MIN()이 종료된 이후, MAX() 로 돌아가야 하는데, 이때의 MAX() 주소값을

mov rbp, rsp 로 지정하는 것이다. 

MAX() 함수에 들어왔을때, rsp는 최초의 MAX() 함수에 배정된 Memory 초기값을 지정하고 있다. 그렇기에 위의 코드를 통해서 여기서 부터는 MAX() 에 해당하는 Memory 영역이다 를 지정하는 것이다. (매우 중요한 개념이다.)

이렇게 한다면 MIN()이 종료된 이후에도, MAX()로 ret이 될 수 있는 것이다.

그렇다. rbp는 해당 메모리의 영역을 나타내는 역할을 할 수 있다는 것이다.

자 그럼, 이제 Stack 에서 해당 영역을 나누는 개념을 알아봤으니 ... 다음 개념을 알아보자.

Stack에 저장된 메모리의 값을 가져오기 위해서는 어떻게 해야하는 걸까?

여기서도 ! rbp의 개념이 사용된다. 위에서도 명시했던 것처럼

  • bp (Base Pointer) : 스택 상대 주소 계산용 이라는 개념을 가지고 있다.
Stack 구조
1
2
ret
rsp = rbp (현재위치)

지금 현재위치에서 1 혹은 2에 접근하려면 어떻게 해야하는건가 ...
(처음에는 다른 방법이 있나 생각했다.)

하지만 의외로(?) 간단한 방법이 있다. 현재 각 Stack 의 8bit 로 나누어져 있다.
그럼 여기서 rbp + 8의 배수 값으로 지정하면 되는 것이다.

mov rax, [rbp+16]
mov rbx, [rbp+24]

를 실행하면 rax = 2, rbx = 1 가 담기게 된다.

스택 프레임은 각 영역에 배정된 Stack Memory 영역을 의미한다는 것이 된다.

3. 스택 메모리 초기화

모든 함수가 실행되고 차례대로 ret를 통해 main() 까지 다시 도달 했다 ! (이제 끝나는 것인가!?)

push 1
push 2
call MAX
print_dec 8, rax (함수들의 결과물을 출력해보자!!)
NEWLINE

xor rax, rax
ret

하지만 호락호락하게 넘어가지 않는 Stack Memory ...

The porgram crashed! Execution Time

다음 Error Message를 출력하고 있다... 왜인가 ...

Stack Memory를 사용할 때는 초기화가 매우 중요한 개념이다.

push 1 
push 2 를 고정적으로 박아넣었기 때문에 해당 에러 메세지를 출력하는 것이다.

모든 함수들이 끝나고 ret 로 돌아올때 Stack Memory의 위치가 뒤틀려 있을 수 있다.
그렇기에 Stack을 사용했다면 깨끗하게 정리를 해야한다는 것이다.

Stack 을 정리하는 방법

  1. push 한 수 만큼 pop을 진행한다.
  2. add rsp, [Stack 구조 bit * push 한 수] 를 진행한다.
profile
C++와 Unreal Engine / C#과 Unity / Katalon Studio를 통한 자동화 테스트 등을 하루하루 공부한 기록

0개의 댓글