▸C언어/기본 상식

C언어의 메모리_스택 메모리 [2/2]

코데방 2019. 12. 5.
728x90

C언어로 만든 프로그램은 함수 호출로 시작해서 함수 호출로 끝난다고 해도 무방합니다. 다른 언어도 근본적으로는 모두 똑같습니다. 함수를 효율적으로 처리하기 위해 사용되는 메모리가 바로 스택 메모리입니다.

 


 

[ 스택 메모리 ]

  • 앞서 말한 네 가지 영역 중 스택 세그먼트(Stack Segment) 전체를 의미
  • 함수 실행을 위해 동적 할당 되는 메모리 공간
  • 후입선출(LIFO - Last Input First Out) 방식으로 작동

[ 스택 프레임 (Stack Frame)]

  • 스택 영역(Stack Segment) 내에서 하나의 함수 실행을 위해 할당받는 메모리 덩어리
  • 후입선출이란, 이 스택 프레임이 순서대로 쌓이고 마지막 스택 프레임이 먼저 빠지는 것을 의미

 

 


 

[ C언어의 함수란? ]

프로그램 안에서 자주 쓰이는 기능을 별도의 코드로 작성해두고 필요할 때마다 이 코드를 불러서 쓰는 것을 "프로시저(Procedure)"라고 합니다. 또한 함수를 호출했을 때 일정한 규칙에 의해 반환(Return)되는 결과값이 있는 것을 "함수(Function)"이라고 합니다. 이 프로시저와 함수를 함수의 개념을 합한 개념이 C언어의 함수입니다. C언어의 Void함수가 이 프로시저를 대신합니다.

또한 독립적으로 사용되지 않고 메인 루틴(메인 함수)과 결합하여 사용되며, 특정 기능을 반복 수행할 때 이용하는 것을 서브 루틴(함수)이라고 합니다. C언어에서 메인 함수를 제외하면 모두 어떤 함수의 서브 루틴으로써 동작하게 됩니다.

 


 

컴퓨터는 뭐든지 순서대로 실행하기 때문에 하나의 함수 내에서 다른 함수를 실행하고, 또 그 함수 안에서 다른 함수를 실행하는 등 함수를 중복 호출할 경우 다양한 상황에 대해 고민해야합니다. 크게 보면 아래 두 가지의 문제가 생깁니다.

  • 함수 간 매개변수를 어떻게 전달할 것인가.
  • 호출된 함수가 종료되면 어떻게 함수 호출 직전의 명령어(코드) 위치로 돌아갈 것인가.

이런 고민을 해결해줄 수 있는 메모리 구조가 바로 스택 메모리입니다.

 


 

먼저, 프로그램을 실행하면 코드 영역에 바이너리 형태로 컴파일된 명령어 코드들이 디스크에서 메모리로 옮겨갑니다. 예시는 메인함수 1개에 서브루틴 함수 2개입니다.

 

 

 


 

CPU는 명령어 위치를 주면 순서대로 읽어나갑니다. 그냥 적힌 내용대로 수행만 할 뿐 아무 생각이 없습니다. 프로그램 실행의 준비가 끝나면 프로그램 종료를 담당하는 명령어가 들어 있는 위치를 스택에 하나 넣고 메인함수를 읽기 시작합니다. 스택 포인터 레지스터는 항상 스택의 마지막 주소를 가리키고 있습니다.

 

 

 


 

CPU가 현재 읽고 있는 명령어 위치는 메인함수 시작 지점인 1번 주소입니다. 먼저 변수 a를 하나 스택에 쌓습니다. double형이기 때문에 8byte 공간을 할당합니다. 스택 레지스터는 항상 마지막 값을 기리키고 있습니다.

 

 

 


 

다음은 변수 a를 출력하는 명령어입니다. a를 출력하기 위해서는 cpu가 a의 값을 가져와야 합니다. 왜 C언어에서 변수의 타입을 정확하게 지정해줘야 하는지 이유가 여기 있습니다. 현재 스택 포인터 레지스터가 가리키고 있는 스택의 마지막 값이 88번 주소이기 때문에, CPU가 a변수의 값을 가져오기 위해서는 88번 주소에서 8을 더한 96번 주소의 값을 가져오게 됩니다. 해당 값은 다른 레지터에다가 넣어서 작업합니다.

 

 

 


 

이제 다음 명령어인 함수 1번을 호출합니다. 호출할 때는 argument로 int 타입 변수 2개를 제공합니다. 먼저 스택에 차례대로 값 2개를 넣습니다.

 

그리고 함수 1을 실행하기 전에, 현재 실행되고 있는(메인) 함수에서 다음 실행해야 할 명령어 주소를 스택에다가 하나 넣어 둡니다. 음영 처리된 부분이 하나의 스택 프레임이 끝나면 CPU가 다시 읽을 명령어 주소를 저장한 부분입니다. 그 의미는 계속 보시다보면 알 수 있습니다.

 

 

 


 

함수 1을 호출한다는 의미는, 현재 CPU가 읽고 있는 명령어 위치에서 함수1이 있는 명령어의 위치로 이동시킨다는 것입니다. 함수 1을 호출해서 이제 CPU가 읽을 명령어 위치는 6번으로 점프했습니다. 예시가 좀 짧아서 체감이 안되지만 긴 코드에서는 그만큼 먼 곳으로 점프하게 됩니다.

 

 


 

그리고 다시 함수 1번의 첫 번째 줄을 수행합니다. 여기서는 바로 결과값을 리턴시킵니다. 리턴시키기 위해서는 함수 1번이 받은 1과 2의 정보를 CPU가 받아와서 연산작업을 해야합니다. 이것 역시, 아까와 마찬가지로 현재 스택 레지스터가 가지고 있는 76번 주소에서, 8byte를 더한 곳의 값과 12byte 값을 더한 곳의 값을 가져가면 됩니다. 80번의 포인터 4byte도 같이 더한 값입니다.

 

 

 


 

이렇게 CPU에서 연산을 위한 레지스터로 1,2의 값을 순서대로 가져가서 계산한 후, 3이라는 결과를 도출합니다. CPU나 컴파일러 종류에 따라 다를 수 있지만 대부분 이 결과 값은 다른 레지스터에 저장합니다.

리턴값은 하나 밖에 없으니 레지스터를 하나만 쓰면 됩니다. 하지만 함수에 전달해야 하는 매개변수(argument)의 갯수는 매우 많을 수도 있으므로 레지스터에 바로 저장하지 않고 스택 메모리에 저장 합니다. CPU 레지스터의 공간과 갯수는 메모리처럼 많지 않기 때문이죠. 위의 1,2 값을 가져갈 때도 한번에 다 가져가는게 아니라 1을 가져가서 작업하고 다시 2를 가져와서 그 값에 더해주는 방식으로 연산하게 됩니다. CPU 작동원리도 언젠가 다시 다루겠습니다.

 

 

 


 

제 메인함수에 그 값을 다시 리턴해줘야 합니다. 함수 1이 종료 되었으므로 이제 용도를 다 한 함수 1의 스택 프레임은 제거합니다. 이 과정이 바로 후입 선출의 스택 구조가 생겨난 가장 큰 이유입니다. 이 스택 프레임을 제거하다 보면 80번 위치에서 다음 CPU가 읽어야 할 명령어 위치인 5가 있기 때문에 다시 CPU는 5번 위치로 이동해서 다음 명령어를 읽기 시작합니다.

 

 

 


 

이제 결과가 저장된 레지스터에서 값을 다시 스택 메모리에 옮겨줍니다. 제가 빼먹고 리턴 값을 담을 변수를 안만들어놨는데 리턴 값을 변수 b에 담았다고 가정해 보겠습니다. 물론 변수에 담지 않고 바로 리턴 값을 사용할 때는 레지스터의 값을 그대로 쓰면 됩니다.

 

 

 


 

마지막 명령어인 출력을 하면 메인 함수도 끝납니다. 메인 함수 리턴도 빼먹었네요. 어쨌든 메인함수도 끝났으므로 해당 스택 프레임을 모두 지워줍니다. 그럼 CPU는 프로그램 종료 명령어가 있는 0번 위치로 가서 거기 있는 명령대로 프로그램을 안전하게 종료합니다. 이 부분의 명령어는 컴파일러가 알아서 작성해서 넣어둡니다.

 

 

 


 

위 과정에서 볼 수 있듯이, 함수가 계속 호출되는 과정을 위해 후입선출의 스택 메모리 구조가 사용됩니다. 아무리 복잡한 과정이라도 매개변수만 잘 전달되고 함수가 종료되면 다시 돌아갈 위치만 주면 알아서 잘 잘동되는 것이죠.

728x90

'▸C언어 > 기본 상식' 카테고리의 다른 글

C언어의 메모리_기본 구조 [1/2]  (1) 2019.12.05

댓글

💲 추천 글