C언어 메모리 세그먼트

C & C++

2020. 1. 6. 16:07

C를 공부하기 전에 C언어의 메모리 구조를 알면 C언어를 이해하는데 큰 도움이 되며, C이외에도 Java 등의 언어도 이러한 구조와 굉장히 비슷하게 설계되어있다. (아주 약간의 차이만 존재한다) 때문에 가장 먼저 메모리구조에 대해서 공부한다. 가장 먼저, C의 메모리 구조는 텍스트(코드)영역, 데이터 영역, BSS영역, 힙영역, 스택영역 등 다섯개의 영역으로 분리된다. 하나하나 자세히 알아보자.

 

[그림] C언어의 메모리 구조

 

1. 텍스트(코드) 세그먼트

텍스트 영역에는 코드들이 바이너리화 되어 저장된다. 프로세서가 여기에서 명령어를 하나씩 가져와서 실행한다. 텍스트 세그먼트의 명령 실행 순서는 순차적이지는 않다. 왜냐하면 하이레벨의 언어 제어구조가 어셈블리어의 branch, jump, call 등으로 변형되기 때문이다. 프로그램이 실행되면 EIP 레지스터는 텍스트 세그먼트의 맨 처음 위치로 설정되게 된다. 그리고 프로세서는 다음 작업을 반복한다.

 

EIP 레지스터는 다음에 실행할 명령의 메모리 주소가 저장되어있는 레지스터로, 현재 실행 중인 명령어가 종료되면 프로세서는 자동으로 EIP레지스터에 저장된 명령어를 실행한다. 또한 프로세서는 EIP에 있는 명령어 실행하기 전에 다음 명령어를 미리 EIP에 저장한다.

 


1. EIP의 명령 읽기

2. EIP 해당 명령 길이를 더함

3. 1단계에서 읽은 명령을 수행

4. 1단계로 돌아감

 


 

텍스트 (코드)영역은 변수가 아닌 코드만 저장하고있기 때문에, 쓰기가 금지되어있다. 텍스트 세그먼트에 쓰려는 시도가 있을 경우, 운영체제가 그 사실을 사용자에게 알리고 프로그램은 종료된다. 추가적으로 텍스트 세그먼트가 읽기 전용일때 장점으로는 한 종류의 프로그램을 여러번, 여러개 실행해도 텍스트 세그먼트를 공유할 수 있다는 것이다. (예를 들어 우리가 윈도우즈 메모장을 3개, 4개 켜더라도 RAM에 텍스트 세그먼트를 3개, 4개씩 올릴필요가 없다) 또한 텍스트 세그먼트는 바뀌는 것이 없으므로 크기도 고정되어있다.

 

2. 데이터 세그먼트

#include<stdio.h>

struct Man{
    int age;
    char sex;
    int id;
    char[12] name;
}


/**
* 아래의 전역 변수들은 초기값이 할당되어있기 때문에,
* Data Segment에 할당되게 된다.
*/
int globalVariable = 10;
Man man = {19, 'M', 201112010, "홍길동"};
int szman = sizeof(Man);


int main(){
	
	// .. 작업 수행 ..

	return 0;
}

 

데이터 세그먼트는 전역변수 (Global Variable)과 정적변수 (Static Variable)이 저장되는 메모리 공간이다. 이 공간에 있는 데이터들은 특히나 초기값이 있는 전역변수, 배열, static으로 선언된 변수가 들어간다. 이 공간은 추후 일정 메모리 접근 공간으로 사용가능하다. 즉, 프로그램 런타임에 자유롭게 수정 및 변경이 가능하다.

 

3. BSS (Block Stated Symbol) 세그먼트

#include<stdio.h>

struct Man{
    int age;
    char sex;
    int id;
    char[12] name;
}


/**
* 아래의 전역 변수들은 초기값이 할당되어있지 않기 때문에,
* BSS Segment에 할당되게 된다.
*/
int uninitializedVar;
Man man;


int main(){
	
	// .. 작업 수행 ..

	return 0;
}

 

BSS란 정적 / 전역 변수 중 초기화 되지 않은 변수들이 저장되는 공간이다. Unix나 Windows를 포함한 많은 컴파일러나 링커에서 이러한 이름을 사용한다. BSS 섹션 혹은 BSS 세그먼트라고 불리는 경우도 많다. C언어에서는 초기화없이 정적으로 할당된 변수는 0(수치데이터의 경우)이나 NULL(포인터의 경우)로 초기화가 된다. 0과 NULL 모두 일반적으로 컴파일러에서는 모든 비트가 0인 비트패턴으로 표현한다. 이러한 작업을 BSS 섹션에서 담당하게 된다. 

 

가장 먼저 BSS섹션은 초기화가 되지 않은 정적 / 전역변수 혹은 모든 비트패턴이 0으로 초기화된 변수(NULL로 초기화된 변수 등)BSS 섹션에 할당한다. 통상적으로, 컴파일타임에는 BSS 섹션에 할당된 메모리는 메모리만 잡아놓고 초기화시키지 않는다. BSS영역에는 단지 어느정도의 공간을 할당할 것이라는 정보만을 저장한다. 이후, 런타임에 링크되어 올라가고 프로그램 로더가 프로그램을 로드할 때 값이 0으로 초기화된다. 즉, 런타임에 시스템이 BSS 섹션에 맵핑된 메모리 영역을 0으로 초기화한다. 게다가 OS가 필요시기까지 0으로 초기화 하는 작업을 지연하는 테크닉을 사용해서 BSS 섹션을 효율적으로 구현한다. 결론적으로 데이터 영역은 초기에 사용할 메모리를 확보하는 반면, BSS 영역은 어느정도의 메모리를 확보할것인지에 대한 정보만 미리 할당해놓고 있다가, 런타임 이후에나 메모리 영역이 확보된다. 즉, 메모리 사용면에서 BSS가 더욱 효율적이다. (전역변수를 선언한다면, 초기화를 하지 않는 것이 좋다)

 

4. 스택 세그먼트

#include<stdio.h>

struct Man{
    int age;
    char sex;
    int id;
    char[12] name;
}

int func1(int a, int b);
void func2(int n);

int main(){
/* 
* 스택프레임 Step 1시작 
* 매개변수는 없음
* 반환주소값 할당 (아래의 "return 0" 이 들어갈 주소)
*/

    int a = 10;
    int b = 4;
    // 지역변수 할당
    
    int result = func1(a, b); 
    // 스택프레임 Step 2로 이동

    return 0;
    // 스택프레임 Step 1종료, Step 1 영역 반환
}

int func1(int a, int b){
    /*
    * 스택프레임 Step 2시작
    * 매개변수 할당 (a, b의 복사본이 들어갈 주소)
    * 반환주소값 할당 (main 함수의 result의 대입연산자 우항의 리턴주소 할당, 
    * 이 주소는 result의 주소가 아님, result는 아직 할당되지 않았으며,
    * 이 주소는 result의 우항에 있는 리턴주소임!)
    */
	
    int c = a + b;
    c = c * 100;
    // 매개변수 할당
    
    func2(c);
    // 스택프레임 Step 3로 이동
    
    return c;
    // 스택 프레임 Step 2종료, Step 2영역 반환
}

void func2(int n){
    /*
    * 스택프레임 Step 3시작
    * 매개변수 할당 (n의 복사본이 들어갈 주소)
    * 반환주소 없음 (void 타입)
    */
    
    printf("%d\n", n);
    printf("%d\n", n * 100);
    // 스택프레임 Step 3종료, Step 3영역 반환
}

 

스택 세그먼트에서는 프로그램이 실행하고 있는 동안에 만들어지는 데이터 (지역변수, 매개변수 등)을 스택처럼 쌓아서 저장하는 세그먼트 영역이다. 일반적으로 쓰레드 1개가 1개의 스택을 보유하며, 함수가 호출될 때마다 스택의 공간이 일정부분 할당된다. 이를 스택프레임 (Stack Frame)이라고 하는데, 아래의 그림을 보면 스택 프레임의 동작을 자세히 알 수 있다.

 

[그림] 스택프레임 (1)

 

위의 그림처럼 처음  main 함수가 호출되면, 매개변수, 반환주소를 위한 공간이 스택에 할당된다. 그리고 함수가 진행되면서 지역변수들이 할당이 된다. 이 때, 또 다른 함수 func1(a, b)가 호출되면 func1(a, b)를 위한 매개변수, 반환주소를 위한 공간이 스택에 할당되고 함수가 진행되며, 지역변수들이 할당된다. 또, func2(n)이 호출되면, 매개변수, 반환주소를 위한 공간이 스택에 할당되고 함수가 진행되며, 지역변수들이 할당된다. 

 

[그림] 스택프레임 (2)

 

func2(n)가 종료되면서 func2(n)이 사용하고 있던 공간이 반환된다. 그리고 func1(a, b)가 종료되면서 func1(a, b)가 사용하던 공간이 반환되고 마지막으로 main함수가 종료되면서 main함수가 사용하고 있던 공간까지 리턴이 되고 메모리에서 해당 프로그램이 완전히 해제된다. 만약 이 Stack 공간이 다른 공간을 침범하면 어떻게 될까?

 

[그림] 스택 오버플로우

 

Stack의 모든 영역이 모두 할당되어 더이상 공간이 없어 다른 공간을 침범하게 되는 경우를 스택 오버플로우 (Stack Overflow)라고 하며, 이 경우 운영체제가 이상을 감지하고 프로그램을 종료시킨다. 지금까지 텍스트영역, 데이터 영역, BSS영역, 스택 영역까지 알아보았다. 그럼 힙영역은 왜 필요할까? 

 

5. 힙 세그먼트

#include<stdio.h>

int main() { 

  // 정상적인 배열선언 
  int arr[10]; 

  // 비 정상적인 배열선언 (C99 이전버전에 해당)
  // C99가 가변길이배열(VLA)을 지원하고부터는 가능
  int i = 0; 
  scanf("%d", &i); 
  int arr[i]; 

  return 0; 
}

 

우리는 배열을 선언할 때, 상수로 선언을 한다. 배열의 길이를 사용자가 입력한 숫자로 잡아주는 것은 비 정상적인 배열 선언이다. 아래 그림을 보면, 스택영역은 컴파일 타임에 크기가 결정되고, 힙영역은 런타임에 동적(Dynamic Allocation)으로 크기가 결정된다. 정상적인 배열 선언의 경우, 컴파일타임에 arr이라는 배열의 크기가 40 byte임을 알 수 있다. 하지만 비 정상적인 배열 선언의 경우 컴파일타임에 i의 크기가 4 byte라는 것을 알수는 있으나, arr라는 배열의 크기는 알 수가 없다. (사용자가 아직 입력하지 않았기 때문)

 

[그림] 실행시간에 따른 메모리 영역 크기 경정

 

 

즉, 사용자의 요구에 맞게 메모리를 할당하기 위해서는 메모리를 동적으로 런타임에 할당해야한다. 힙영역은 동적으로 런타임에 사용자의 요구에 맞게 메모리를 할당하기 위한 영역이다. 

 

#include<stdio.h>
#include<stdlib.h>


int main(){

    int i = 0;
    scanf("%d", &i);
    
    int *arr = (int*)malloc(sizeof(int) * i);
    // 데이터가 힙영역에 할당됨.
    
    free(arr);
    // 데이터가 해제됨.
    
    return 0;
}

 

힙영역은 프로그램 실행 중에 동적으로 메모리를 할당하기 위한 공간으로, 동적할당 함수인 malloc, calloc, realloc 등을 사용하여 할당 했을 경우, 힙 영역에 데이터를 할당하게 된다. 이렇게 동적으로 할당한 메모리의 경우 Java등의 언어에서는 Garbage Collecting 알고리즘에 의해 자동으로 해제되지만, C언어에서는 자동으로 해제되지 않아서 free()함수를 이용해서 직접 해제해줘야한다. (현대의 운영체제는 프로그램 종료시 모든 메모리를 해제해주지만, MCU나 임베디드 시스템 또는 운영체제에 따라 그렇제 않은 것도 있다) 그렇지 않으면 사용하지 않는 메모리가 영원히 메모리에 잡혀있는 메모리 누수 (Memory Leak)이 발생할 수도 있다.

 

6. Reference

 

SPARC Assembler Memory Map

어셈블러를 수월하기 위해서는 사실 절차적인 면에서, Register Set과 Memory Map에 관한 이해가 가장 필수적이다. 지난번에 Register Set에 대해서는 여러번 언급을 하였고, 이번에는 Memory Map에 대하여 이야기..

shinluckyarchive.tistory.com

 

메모리 세그먼트-(1)이론

주관적인 생각이지만, 이 개념을 잘 알고 있어야 BOF, ROP 등등의 여러기법을 이해하는데 수월합니다. 또한 설명에 있어서 이하 존칭은 생략하겟습니다. <큰 그림> 환경변수 스택 힙 데이터 코드(=text) 컴파일된..

symnoisy.tistory.com

 

BSS(Block Started by Symbol)란?

BBS와는 다르다. BBS와는! 한국어로는 나오지 않기 때문에.. 어쩔 수 없이 일어 버전으로 보게되었다. .bss 또는 bss란, 정적으로 할당된 변수 중 프로그램의 시작 전에 0으로 초기화 시키는 것을 포함한 데이터..

genesis8.tistory.com

 

[C] 스택(Stack), 힙(Heap), 데이터(Data)영역

C언어의 메모리 구조 프로그램을 실행시키면 운영체제는 우리가 실행시킨 프로그램을 위해 메모리 공간을 할당해준다. 할당되는 메모리 공간은 크게 스택(Stack), 힙(Heap), 데이터(Data)영역으로 나뉘어진다. 이..

dsnight.tistory.com