본문 바로가기
C

[C] 메모리 할당 - 정적 메모리 할당 (김성엽의 기초 C언어 강좌 16장 1)

by Lizzie Oh 2022. 10. 28.

🔔 김성엽 님의 Do it! C언어 입문 동영상 강의를 보면서 정리한 내용입니다. 모든 그림과 코드 예제는 해당 강의에서 발췌한 내용입니다.

 

🗒️ terminology

프로그램

프로그래머가 만든 프로그램 실행 파일 e.g. .exe 파일 

 

프로세스 

CPU가 프로그램의 명령등을 실행할 수 있도록, 운영체제가 프로그램의 명령들을 읽어서 메모리에 재구성한 것.
'실행 중인 프로그램' 이라고도 한다

세그먼트*의 집합으로 구성

 

세그먼트

64kb 이하의 메모리 그룹으로서, 여러가지 정보나 사용자가 입력한 데이터를 기억하는 메모리 공간

코드 세그먼트, 데이터 세그먼트, 스택 세그먼트 등으로 구성 (이 세가지 이외에도 다양한 세그먼트들이 존재)

 

메모리 할당

메모리 할당이란 내 프로그램이 운영체제로 배정받은 메모리 공간 내에서, 데이터를 저장할 공간을 적정하게 나누는 작업. 

정적 메모리 할당과 동적 메모리 할당으로 나뉜다. 

 

정적 메모리 할당 (static Memory Allocation) 

- 컴파일러가 소스코드를 기계어로 번역하는 시점(컴파일 타임)에, 크기에 맞는 메모리를 변수에 할당하는 것 

- 변수가 가질 메모리 크기가 이미 컴파일 시 결정되기 때문에 프로그램 실행 중에 할당된 메모리 크기는 변경 불가.
- 메모리 크기를 바꾸려면 컴파일을 다시 해야하므로 정적메모리 할당을 많이 사용 시 코드의 유지 보수가 어려워 짐

- 메모리 크기만 컴파일 시 결정되고, 실제 메모리 위치(주소)는 프로그램이 실행될 때 결정됨 

- 정적 메모리 할당의 예 : 전역변수, 지역변수
   → 컴파일 시 전역/지역변수의 메모리는 이미 정해짐 → 프로그램 실행 중에 전역/지역변수의 개수나 크기를 변경 불가

메모리 유지 시간 전역변수 지역변수
시작 시점 프로그램 시작 함수 호출
종료 시점 프로그램 종료 함수 끝
저장 영역 프로세스의 데이터 세그먼트 프로세스의 스택 세그먼트

 

📖 정적으로 할당된 메모리를 관리하는 법

지역 변수는 함수가 호출될 때마다 생긴다. 게다가 지역변수에 접근하기 위해서는 지역 변수의 개수만큼 포인터 변수가 필요하기 때문에 그만큼 메모리가 더 필요하므로 지역변수에 너무 많은 메모리가 사용된다.

 

이를 해결하기 위해 C언어에서는 같은 함수에 선언한 지역변수들을 하나의 메모리 '그룹'으로 관리한다. 지역 변수는 스택 세그먼트(스택 자료구조를 사용)에 저장되기 때문에 모든 변수들에 대한 포인터를 다 사용하는게 아닌, 스택의 Base Pointer(start 포인터)와 Stack Pointer(end 포인터)를 사용하여 메모리에 접근하여 메모리를 관리한다. 

 

컴파일은 컴파일시 함수에서 사용하는 모든 변수의 크기와 갯수를 알고있으므로 총 메모리 용량을 알고 있다.
필요한 모든 변수들을 모아 하나의 메모리 그룹으로 만든 후 각 지역변수에 접근하기 위해서 start 포인터와 end 포인터를 사용

int Test() 
{
	int a,b,c,d ; // 컴파일 시 4바이트 변수 4개 -> 16바이트 크기의 메모리가 필요함을 알 수 있음
    
    a=5 ;
    c=3 ;
    new_test() ; // new_test 라는 새로운 함수 호출 
}

 

함수 내에 a,b,c,d 의 4개의 정수형 변수가 선언되어있다. 컴파일러는 컴파일 시 총 16바이트 용량의 메모리가 필요하다는 것을 알고 총 16바이트의 지역변수 메모리 그룹을 스택 세그먼트에 순서대로 넣는다. 이때 각각의 변수들을 가리키기 위해 start 포인터를 사용하고, 다음 함수 호출 시 다음 함수의 지역변수들에 할당될 메모리 주소를 표시하기 위해 end 포인터가 사용된다.

 

아래 왼쪽 그림을 보면 start 포인터는 test 함수 지역변수의 시작위치(첫 지역변수 위치)를 가리키고 있고 end 포인터는 test 함수 지역변수의 마지막 위치(마지막 지역변수 위치)를 가리키고 있다. a,b,c,d는 4바이트이므로 start+0, start+4, start+8, start+12 는 각각 a,b,c,d를 가리키게 된다.  코드의 a=5; a=3;이 실행되면 아래 오른쪽 그림과 같이 a와 c의 값이 저장된다. 

 

새로운 함수가 호출되었을 때는 end 포인터를 기준으로 다음 호출될 함수의 메모리 시작 위치가 결정된다. 즉, 위의 그림에서 end 포인터가 Test 함수의 마지막 변수인 d의 위치를 가리키고 있는데, 새로운 함수 new_test가 호출되면 end 포인터의 다음 위치부터 new_test 함수의 지역변수 메모리 그룹이 시작하게 된다. 

 

💡 이렇게 변수(와 각 변수를 가리키는 포인터)를 개별적으로 관리하지 않고 지역변수 메모리 그룹으로 묶고 start와 end포인터를 사용해서 관리함으로써 메모리를 효과적으로 사용할 수 있게 된다.

 

🚨 자료구조 이론의 스택과 실제 컴퓨터 시스템의 스택의 차이 (중요)

이론에서의 스택은 push 후 스택포인터 주소가 증가하고 pop 후 스택포인터 주소가 감소하지만,
실제 컴퓨터 시스템에서는 push 후 스택포인터 주소가 감소하고 pop 후 스택포인터 주소가 증가한다 (이론과 반대!) 

 

📖 컴파일러가 지역변수를 저장할 메모리 공간을 확보하는 방법 

 

[이론] 실제와 다름 주의. push & pop 사용

C언어 소스에 함수 내에서  int a, b, c; 와 같이 지역 변수를 선언
어셈블리어 push ax  push ax  push ax 로 번역되고 함수 호출 시 4바이트 크기의 메모리 공간 3개가 스택에 추가 
 어셈블리어 pop ax  pop ax  pop ax 로 번역되고   4바이트 크기의 메모리 공간 3개가 스택에 추가 

 

[실제] 이론과 다름 주의. sub 명령과 add 명령 사용 

C언어 소스에 함수 내에서  int a, b, c; 와 같이 지역 변수를 선언
 어셈블리어 sub SP, 12 로 번역:  함수 호출 시 SP(Stack Pointer)값을 12만큼 줄여서(subtraction) 메모리 공간을 확보 
 어셈블리어 add SP, 12로 번역: 함수 종료 시 SP(Stack Pointer)값을 12만큼 늘여서(add) 메모리 공간을 제거

 

 

📖 컴파일러가 스택에 할당된 지역변수를 사용하는 원리 

[이론] 변수 b의 값을 5로 변경할 때. 실제와 다름 주의.

실제로 위와 같이 push, pop을 사용한다고 하면 변수가 많아지면 끝없이 느려질 수 있음 (즉 실제로는 이렇게 안 씀) 

 

[실제] 변수 b의 값을 5로 변경할 때. 이론과 다름 주의.

스택을 사용한다고 꼭 push & pop을 써야 하는게 아니라 간접 주소 지정 방식(pointer)으로도 값을 읽거나 저장할 수 있다.
즉, Base Pointer-4 로 접근해서 바로 값 5를 대입할 수 있다.

 

📖 함수를 호출할 때 스택 메모리가 변화하는 과정 (스택프레임)

매번 함수가 호출될 때마다 스택포인터와 베이스 포인터가 넘쳐나지 않도록 (=스택포인터 1개, 베이스포인터 1개만 사용할 수 있도록) 스택 프레임 이라는 기술을 사용  

 

새로운 함수가 호출될 때

- 현재 함수의 IP(instruction point) 즉, 현재 함수의 어디까지 실행했는지를 알려주는 명령어 포인터를 저장(*push IP)하고 SP +1 
   * 실제로는 push IP가 아닌 함수 호출 시 call이라는 기계어 명령어를 실행하게 되는데 이때 자동으로 IP가 push 되는 것! 
- 현재 함수의 BP값을 저장(push BP) 하고 SP+ 1 

- BP에 SP값을 대입 (BP = SP) 

- 새로운 함수에 필요한 메모리 크기 만큼 SP주소값을 감소 → 이제 새로 실행되는 함수에 맞는 BP와 SP가 모두 갖춰짐

기존 함수가 main이고 새로운 함수의 변수가 y라고 할 때, 새로운 함수 호출 시 스택메모리가 변화하는 과정 

이처럼 스택 메모리에서는 하나의 BP와 하나의 SP를 사용하고 이를 '현재 실행 중인 함수에 맞춰' 사용한다
→ C언어에서 다른 함수의 지역변수를 참조할 수 없는 이유이기도 함 

 

호출된 새로운 함수가 종료될 때 

- SP에 BP의 주소를 대입 (SP=BP)

- 다시 돌아가야 할 함수의 BP를 pop해서(이때 SP +1) BP에 대입 

- 다시 돌아가야 할 함수를 어디까지 실행했는지 알기 위해 IP를 꺼내서 확인(*pop IP)
  * 실제로는 pop IP가 아닌 ret라는 기계어 명령에 따라 내부적으로 pop이 실행되는 것 

 

스택 프레임

스택 프레임은 함수를 호출할 때 일어나는 스택(메모리?)의 변화를 의미하고, 이는 컴파일러가 C언어 소스를 기계어로 번역하는 시점, 즉 컴파일 타임에 결정된다. 어떤 식으로 스택이 변화할지는 컴파일 타임에 이미 다 결정되는 것.

 

스택프레임을 결정하는 요소는 지역변수의 자료형이나 갯수이기 때문에 지역변수가 변경되면 스택 프레임이 바뀌어야 한다. 즉 컴파일을 새로 해야 한다. 

 

함수가 종료될 때 기존의 값들은 SP,BP가 움직여서 쓸 수 없는 없게 된 것이지 실제로 메모리에서 사라지는 것은 아니다! 위의 세네번째 그림에서 n,m,k가 실제로 지워진 것은 아님!

 

정적메모리 할당의 한계

프로세스 안에서 지역변수가 저장되는 기본 스택 메모리의 크기는 1MB. 

따라서 단일 변수의 크기가 1MB를 넘는 경우에 컴파일 시 오류가 발생하며, 함수 내 모든 변수의 크기 합산이 1MB를 넘는 경우에도 프로그램 실행 시 오류가 발생한다. (아래 오류 메세지 참고)

 

 

 

 

 

 

Reference

https://youtu.be/5_p2WxmHuyM
https://www.youtube.com/watch?v=qw78mV28ROo&list=PLiZvlxkcLhakQwbPjkyfuHFy1IVG-VXrP&index=23  

 

반응형

댓글