C 언어로 작성한 소스 파일(*.c)은 컴파일 작업과 링크 작업을 거쳐 기계어로 이루어진 파일(*exe)이 된다. 이렇게 프로그래머가 만든 프로그램 실행 파일을 컴퓨터 사용자들은 프로그램(program)이라고 부른다.
그런데 실행 파일에 있는 명령들은 CPU가 직접 실행할 수 없다. CPU가 실행하기 위해선 운영체제가 명령들을 읽어 메모리에 재구성 해야하는데, 이것을 프로세스(Process)라고 한다.
프로세스가 구성되면 CPU는 프로세스에 저장된 명령들을 실행할 수 있다.
프로세스는 단순히 실행할 명령들로만 이루어져 있는 것이 아니라, 사용자가 입력한 데이터를 기억하는 메모리 공간도 포함하고 있다.
이러한 공간을 세그먼트(Segment)라고 한다. 프로세스는 세그먼트의 집합으로 구성되어 있으며, 코드 세그먼트(Code Segment, CS), 데이터 세그먼트(Data Segment, DS), 스택 세그먼트(Stack Segment, SS)는 각각 한 개 이상의 세그먼트로 구성된다.
코드 세그먼트 (Code Segment) : C 언어를 컴파일러가 기계어로 된 명령문으로 번역해서 실행파일을 만드는데, 이 파일이 실행되어 프로세스가 만들어질 때 기계어 명령들이 복사되는 곳이다.
데이터 세그먼트 (Data Segment) : 프로그램이 시작해서 끝날 때까지 계속 사용되는 데이터가 보관되는 곳. 문자열 상수, 전역 변수, static 변수가 이 영역을 사용한다.
스택 세그먼트 (Stack Segment) : 프로그램 실행 중에 필요한 임시 데이터를 저장하는 데 사용하는 메모리 영역이다. 함수가 호출될 때 함수 안에 선언한 지역 변수 또한 이 영역에 할당된다. 지역 변수가 놓이는 스택(Stack)과 동적 메모리 할당을 담당하는 힙(Heap)으로 나뉜다.
컴파일러가 코드를 기계어로 번역하는 시점에 변수를 저장할 메모리 위치를 배정하는 것을 정적 메모리 할당(Static Memory Allocation)이라고 한다.
메모리 할당 기준이 C 언어 코드가 기계어로 변역되는 시점에 결정되기 때문에 정적 할당된 메모리 크기나 개수를 변경하려면 코드를 변경하고 다시 컴파일해야 한다.
정적 메모리 할당은 프로그램이 실행될 때 메모리의 크기가 이미 결정되어 있다. 또한 실행되는 중간에 크기를 변경할 수 없다.
할당된 메모리는 지역 변수인지 전역 변수인지에 따라 유지되는 시간도 다르다.
전역 변수는 프로그램이 시작해서 종료할 때까지 할당 받은 메모리 상태를 계속 유지해야 하고, 프로그램이 실행되는 도중에는 새롭게 전역 변수를 추가하거나 삭제할 수 없다. 데이터 세그먼트 < 구역에 별도로 마련되어 있다.
지역 변수는 함수가 호출될 때 메모리에 할당되었다가 함수가 끝나면 할당된 메모리가 해제된다. 따라서 함수의 호출과 종료가 반복될 때마다 지역 변수의 메모리 할당과 해제 또한 반복된다. 스택 세그먼트 < 구역을 사용한다.
프로그램에서 변수를 사용하려면 자신이 사용할 변수의 주소를 알아야 한다.
전역 변수는 프로그램이 시작해서 끝날 때까지 할당된 메모리 크기나 주소가 바뀌지 않기 때문에 전역 변수가 어디에 할당되어 있는지에 대한 추가 정보가 필요하지 않다.
지역 변수는 함수 호출에 따라 메모리 할당과 해제가 반복되기 때문에 변수 주소가 계속해서 바뀐다. 따라서 지역 변수를 사용하려면 그 변수의 현재 주소를 알아야 한다.
스택(Stack)은 자료 구조(Data Structure)의 한 종류이며 두 개의 포인터로 많은 양의 데이터를 효과적으로 관리하는 이론이다.
스택은 베이스 포인터 (Base Pointer, BP)를 기준으로 데이터가 추가될 때마다 순서대로 쌓아 올리는 구조이며 새로운 데이터가 추가될 위치를 스택 포인터(Stack Pointer, SP)가 가리키게 된다.
스택에 데이터를 추가하면 스택 포인터가 가리키는 주소의 메모리에 대입되고
스택 포인터의 주소는 4(32비트 운영체제)만큼 증가한다.
이렇게 데이터를 추가하는 작업을 PUSH 라고 한다.
데이터를 꺼낼 때는 가장 마지막에 추가된 값을 제거하고
스택 포인터 주소가 4만큼 감소한다. 이것을 POP이라고 한다.
지역 변수를 관리할 때 스택(Stack)을 사용한다.
스택 세그먼트의 지역 변수 영역은 앞에서 설명한 스택 이론으로 메모리를 관리할 수 있으며, PUSH와 POP을 이용해 데이터를 추가하거나 삭제할 수 있다.
그런데 스택에서 한 가지 주의해야 할 점이 있다.
이론적으로 스택을 표현할 때는 PUSH가 스택에 데이터를 추가한다는 뜻이라서 스택 포인터(SP)에 저장된 주소가 증가하도록 한다. 하지만 실제 컴퓨터에서는 PUSH 명령은 SP에 저장된 주소가 감소하도록 만들어져있다.
따라서 스택 포인터 주소는 PUSH할 때마다 감소하고, POP을 실행할 때마다 증가한다고 할 수 있다. (실제 컴퓨터 시스템에서)
void Test() /* Test 함수 */
{
int a, b, c; /* Test함수가 시작되는 시점에 a,b,c 세 개의 지역변수 사용 */
...
}
이 코드를 보면 Test 함수의 시작 시점에 a,b,c 세 개의 지역변수를 사용하는데, 이 세 변수를 저장할 메모리를 확보해야 하므로 컴파일러는 변수를 위한 메모리 공간 확보를 위해 세 번 push (주소 감소) 하는 코드를 기계어로 만든다.
이렇게 하면 프로그램이 실행될 때 push가 3번 수행되어 4바이트 크기의 메모리 공간 3개가 스택에 추가된다. 즉 SP의 주소가 4씩 세 번 감소하여 BP와 SP 사이에 12바이트의 메모리 공간이 생긴다.
함수가 종료될 때엔 pop 명령을 세 번 호출하여 저장했던 12바이트의 공간을 제거하면서 함수가 종료된다.
하지만 컴파일러는 이 방법을 사용하지 않는다.
push, pop 코드가 너무 많아지기 때문이다.
push를 세 번 하면 SP에 저장된 주소가 12만큼 감소한다. pop을 세 번 하면 12만큼 증가한다. 따라서 이 명령을 단순화하여 sub SP, 12와 같이 sub, add 명령을 이용해 간소화할 수 있다.
C 언어 컴파일러는 지역 변수가 선언된 순서대로 메모리를 할당하며, 스택에 저장된 데이터를 꺼내려면 가장 최근에 저장된 SP 바로 아래의 데이터부터 차례대로 꺼내야 한다.
(p. 394 참고)
스택 메모리도 결국 메모리이기 때문에 컴파일러가 해당 변수의 주소를 알면 간접 지정 방식을 사용하는 포인터의 개념을 이용해서 지역 변수의 값을 읽거나 저장할 수 있기 때문에, 지역 변수를 BP 기준으로 표시해 보면
BP - 4, BP - 8, BP - 12 ...
처럼 BP를 기준으로 하는 메모리 주소로 사용할 수 있다.
사진을 보면서 이해를 하는 것이 쉽기 때문에 교재 398쪽을 참고하면 된다.
IP - Instruction Pointer