[C#] 응용프로그램의 관점에서 바라본 메모리의 구조

Running boy·2023년 8월 11일
1

컴퓨터 공학

목록 보기
27/36

응용프로그램은 운영체제로부터 할당받은 메모리 공간에서 실행된다.
할당된 각각의 메모리 공간이 어떤 역할을 하는지 정리해보자.

메모리의 구조

메모리의 구조

코드(Code) 영역

컴파일된 응용프로그램의 네이티브 코드가 저장되는 영역이다.
코드 영역의 명령어는 CPU에 의해 레지스터로 옮겨지고 해석되어 실행된다.


데이터(Data) 영역

전역 변수, 정적 변수 등 프로그램 전반적으로 사용될 수 있는 데이터가 저장되는 영역이다.
응용프로그램이 실행되는 시점에 할당된다.


스택(Stack) 영역

지역 변수나 매개변수와 같은 일시적인 데이터가 저장되는 영역이다.
컴파일 타임에 크기가 결정되며 따로 설정하지 않는다면 보통 1MB만큼 자동 할당된다.

스택 자료구조와 유사하게 동작하기 때문에 라이프 사이클이 끝나면 자동으로 할당 해제된다.
스레드당 고유한 스택 영역을 갖는다. (멀티스레드 개념)

다른 메모리 영역과는 다르게 주소가 높은 곳에서 낮은 곳으로 쓰여지는 것이 특징이다.
이는 스택이 메모리 영역을 벗어나 다른 프로그램 영역에 영향을 끼치는 것을 예방하기 위한 구조이다.
(실제로 옛날에 이러한 방식으로 커널 영역에 침범하는 해킹 수법이 존재했다고 한다.)

스택 오버플로우(Stack Overflow)

스택에 데이터가 쌓이다가 컴파일 타임에 정해진 크기를 벗어나면 발생하는 예외이다.
대부분 재귀 함수의 연속 호출로 인해 발생한다.

class Program
{
    static void Main(string[] args)
    {
        RecursiveCall(1);
    }

    private static void RecursiveCall(long v)
    {
        if (v % 100 == 0)
        {
            Console.WriteLine(v);
        }

        return v + RecursiveCall(v + 1);
    }
}

위 코드를 실행하면 RecursiveCall 메서드가 계속 호출되면서 스택 메모리에 쌓이게 되고 결국 스택 오버플로우가 발생한다.

스택 오버플로우의 경우 특이하게 try/catch 예외 처리를 해도 프로그램이 비정상적으로 종료된다.
이는 스택 메모리가 모두 할당되어 오류를 알릴 메서드를 호출할 수 없기 때문이다.

꼬리 재귀(Tail Call)

재귀 함수로 인한 스택 오버플로우를 방지하기 위한 최적화 기법이다.

class Program
{
    static void Main(string[] args)
    {
        RecursiveCall(1);
    }

    private static void RecursiveCall(int v)
    {
        if (v % 100 == 0)
        {
            Console.WriteLine(v);
        }

        RecursiveCall(v + 1);
    }
}

스택 오버플로우 예시와의 차이점은 스택을 재활용할 수 있다는 것이다.
자세히 보면 스택 오버플로우의 예시는 재귀해야 할 명분을 남겼다.
(return v + RecursiveCall(v + 1)는 어쨌든 다음 메서드로부터 반환값을 받을 때까지 대기한다는 의미이기도 하다.)
반면에 tail call은 다음 메서드의 반환을 기다릴 필요가 없기 때문에 컴파일러의 재량에 따라 스택을 재활용할 수 있다.

하지만 tail call 최적화를 했음에도 실제로 위 코드를 실행해보면 스택 오버플로우가 발생할 것이다.
tail call 최적화는 개발자가 아닌 컴파일러, 정확히는 JIT 컴파일러가 수행하며 IL코드에 "tail." 접두사가 있어야 한다.
C#의 컴파일러는 IL코드에 "tail." 접두사를 생성하지 않는다.
단 64bit/Release 빌드에 한해서 쉬운(easy) 경우만 JIT 컴파일러가 tail call을 수행한다.
그래서 위와 같이 재사용이 가능한 쉬운 구조를 tail call이라고 부른다.


힙(Heap) 영역

사용자에 의해 동적으로 할당되는 메모리 영역이다.
동적으로 할당되기 때문에 런타임에 크기가 결정된다.

C/C++과 같은 네이티브 언어로 작성된 프로그램은 힙에 메모리를 할당할 경우 사용이 끝나면 반드시 할당을 해제해줘야 한다.
그렇지 않을 경우 할당될 메모리가 부족해 메모리 누수 현상(memory leak)이 발생할 수 있다.

하지만 C#은 관리 언어로, 가비지 수집기(Garbage Collector, GC)가 힙 메모리를 관리하며 사용되지 않는 메모리의 할당을 해제한다.
그래서 C#에서의 힙은 관리 힙(managed heap)이라고 한다.

힙 오버플로우(Heap Overflow)

스택 오버플로우와 반대로 힙 영역에서 스택 영역을 침범하는 현상을 말한다.
대부분 사용자가 메모리를 직접 관리해야 되는 경우에 발생하는 문제이므로 C#에서 힙 오버플로우가 발생할 걱정은 안해도 된다.
(하지만 unsafe한 코드를 작성한다면? 보장은 못할 듯하다.)

아래 코드로 억지로 힙 오버플로우를 유도할 수는 있다.

class Program
{
    static void Main(string[] args)
    {
        decimal[][] arr = new decimal[100][];

        try
        {
            for (int i = 0; i < 100; i++)
            {
                arr[i] = new decimal[int.MaxValue];
            }
        }
        catch(Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }
}
System.OutOfMemoryException: Array dimensions exceeded supported range.
   at Program.Main(String[] args) in C:\Users\wjdgh9577\Desktop\TestProject\TestProject\Program.cs:line 14

참고 자료
시작하세요! C# 10 프로그래밍 - 정성태
TCP School - 메모리의 구조
C#의 tail call 구현은?

profile
Runner's high를 목표로

0개의 댓글