프로그래밍/C++

프로그램의 개념 및 실행되는 과정

뿌단이 2023. 4. 4. 21:22

프로그램(Program)이란?

 프로그램이란 실행 가능한 파일을 의미하며 프로그램은 실행되기를 기다리는 명령어(코드)와 정적인 데이터의 묶음이다. 실행파일은 .exe.lib, .dll이 있으며, 이 프로그램은 데이터 이므로 보조기억장치(HDD, SSD)에 적재된다.

 프로그램은 비유하자면 설계도이다. 이 프로그램이 RAM에 올라갈 때 어떻게 올라가야 하는지(명령어 코드)등의 정보를 가지고 있는 것이다. 

 

 위 확장자의 뜻은 이렇다.

 

  .exe : Execution의 약어이며 오래동안 사용되온 일반적인 실행파일의 확장명이다.

 

  .lib : Library의 약자이며 프로그램에서 자주 사용되는 함수를 매번 부르는건 비효율 적이므로 자주 사용되는 함수를 미리 만들어서 모아두는 정적 라이브러리 확장명이다.

 

  .dll : Dynamic Link Library의 약자이며 동적 링크를 사용하는 동적 라이브러리의 확장명이다. 동적 링크는 컴파일 시 함수코드가 실행파일에 저장되는것이 아닌 실행중에 함수를 가져오는 것이다.

 

확장자와 확장 프로그램

 일단 파일의 개념은 확장자라는 개념이 없이 만들어졌다. 이후 다양한 파일이 만들어지며 파일들을 구분할 필요성이 생겼다. 그리하여 확장자라는 개념을 만들어 파일들을 구분하기 시작했다. 이후 발전하여 해당 유형들의 파일을 해석하는 프로그램인 확장 프로그램들이 만들어지기 시작했다. 예를들 .txt파일 확장자를 열기 적합한  확장 프로그램은 메모장이 될 수 있다.

 

 파일 확장자는 .txt, .wav, .jpeg 등등 여러 확장자가 있다. 확장자가 없다면 파일을 어떤 확장 프로그램으로 열어야 할지 알지 못한다. 예를들어 아무 텍스트 파일을 생성하고 .txt라는 파일의 확장명을 지워주면 운영체제에서 어떤 확장 프로그램으로 열지 선택하는 창이 뜰 것이다. 또 여러 확장자의 파일도 txt 확장자로 변경하여 메모장으로도 열 수 있는것을 확인할 수 있다. ( 매핑이 잘못되어 이상한 값으로 보일 것이다.)

 

 

프로세서와 프로세스

 프로그램RAM에 올라가면 저장되어있는 명령어들이 차례차례 실행되는데 이 작업 하나하나를 전부 프로세스라고 한다. 프로세서프로세스를 처리하는 기관으로 CPU와 GPU가 프로세스(프로세서의 일의 단위)들을 처리하는 것이다. 프로세스는 RAM에 올라온 명령어들을 CPU에 보내서 결과를 받아오는 식으로 프로세스를 처리한다.

 

 

프로그램의 실행과정

실행파일 즉 프로그램이 실행되는 과정은 아래와 같다.

 .exe 실행파일이 실행되는 순간 주기억장치(RAM, 메모리)에 올라가게 된다.

 프로그램의 코드 데이터들은 RAM에 적재될 때 프로그램이 가지고 있는 설계도를 통해 4가지 공간으로 나뉘며 올라간다.

 

 

[ 코드영역 ]   (Life Time : 프로그램이 시작할 때 부터 프로그램이 종료할 때 까지)

 코드영역은 명령어 코드와 상수가 올라간다. 명령어는 위에서 말했듯이 설계도이다. 개발자가 코딩한 대로 메모리에 올라가서 기능을 수행하며 다른 메모리 영역에 명령어에 따라 적재를 하게 된다.

 

 

아래와 같은 변수를 선언한 코드가 있다.

int Val = 10;

아래에서 자세히 설명하겠지만 이는 스택 영역에 있어야 하는 코드이다.

해당 코드는 "정수 자료형 Val이라는 이름의 공간을 할당하고 그곳에 10을 넣어줘" 라는 코드이다.

 

int의 자료형이 어떤 자료형인지의 정보는 코드영역에 있다.

'=' 이라는 연산의 정보도, 10이라는 상수도 코드영역 있다.

 

위 코드들을 CPU에게 전달하면 CPU는 전달받은 명령어를 실행하여 스택영역에 정수Byte 크기의 메모리를 할당하고 그곳의 이름을 Val이라고 지으며 10이라는 값을 스택 메모리에 저장해준다.

 

 

상수는 변하지 않는 값이므로 따로 다른 영역에 메모리를 할당할 필요가 없다.

컴파일 시 코드에 있는 상수인 변수를 전부 상수로 대체하게 된다.

코드영역에 있는 이유는 전처리(#define)로 해당 이름을 전부 상수로 바꾸는 작업을 하기 때문이다.

 

아래와 같이 PI라는 상수를 선언한다

#define PI 3.141592

아래와 같은 원의 둘레를 구하는 연산을 한다면

double Radius = 3.0f;

double Area = 2.0f * PI * Radius;

 

코드영역의 전처리로 아래와 같이 변경된다.

double Area = 2.0f * 3.141592 * Radius;

 

[ const 오해 금지! ]

const심볼릭 상수, 즉 상수이기전에 변수이므로 이는 변수로 취급해야한다.

#define과 같은 메크로 상수는 전처리문이기 때문에 코드영역에서 전처리 작업이 되는 것이고,

const는 컴파일러가 해당 주소에 있는 값을 변경할 수 없게 만드는

작업을 해준다는 의미로 사용하는 변수의 자료형일 뿐이다.

 

결론적으로 const코드영역에 저장되지 않고,

const는 다른 변수와 마찬가지로 전역으로 선언하면 데이터 영역, 지역으로 선언하면 스택 영역에 저장된다.

 

 

[ 데이터 영역 ]   (Life Time : 프로그램이 시작할 때 부터 프로그램이 종료할 때 까지)

 데이터 영역은 프로그램의 전역 변수정적(static) 변수가 저장되는 공간이다.  이들은 말 그대로 코드영역 전역에 존재하는 변수들이며 따로 관리가 된다. 

 

{ } 이 기호는 실행흐름이라고 하며 함수를 정의할 때 사용된다.

해당 실행 흐름 안에서 정의된 변수들은 실행흐름이 끝난 뒤에는 전부 사라지게 된다. (스택 영역)

전역변수는 실행흐름 안에 있지 않는 변수들을 말하며 모든 영역에서 공유하여 사용할 수 있는 변수들을 의미한다.

 

 데이터 영역은 프로그램의 실행 전에 메모리에 할당이 되며

데이터 영역에 저장된 변수는 프로그램이 종료될 때까지 없어지지 않는다.

 

 

[ 힙 영역 ]   (Life Time : 사용자가 원할 때 생성, 삭제)

동적 메모리 즉 개발자가 할당하고 해제하는 메모리 공간이다.

C/C++에서는 malloc이나 new라는 키워드를 사용하여 메모리 공간을 할당하게 된다.

 

C++에선 아래와 같이 동적할당을 한다.

int* DynamicVal = new int();

 

아래와 같이 동적 할당 공간을 해제한다.

delete DynamicVal;

 

 해당 영역은 개발자가 직접 관리하는 공간이므로 C++ 개발자라면 사용 시 꼭 주의해야 한다.

 

 

[ 스택 영역 ]   (Life Time : 함수가 시작할 때부터 함수가 종료할 때까지 "  }) 

코드영역에서 함수 즉 실행흐름 { } 안에 존재하는 명령어를 호출하게 된다.

 스택영역은 이 실행흐름 안에서 필요한 데이터가 저장되는 공간이다.

 

만약 코드영역에 아래와 같은 코드가 있다면 프로세스의 연산 과정은 이러하다.

int main()
{
    int Val = 10 + 2;
}

main 함수가 실행이 되므로 스택영역에서 main 함수영역을 만들고

스택 영역에 10 + 2 연산의 결과를 넣어줄 공간을 새로 할당한다.

 

코드영역에서 상수 102와  '+' 연산정보 넘겨준다.

CPU12라는 상수 결과를 스택 영역에 결과를 넣어줄 메모리에 전달한다.

 

정수형 변수인 Val가 저장될 공간을 만든다.

결과가 저장된 메모리에 있는 12라는 상수 '=' 연산정보, 그리고 변수 Val의 메모리 주소CPU에게 넘겨준다.

Val 메모리 공간12라는 상수저장한다.

 

이후 함수의 흐름이 종료됨과 동시에 할당된 main 함수의 메모리전부 삭제된다.

(여기서 메모리는 Val의 자료형 4Byte, 결과값 저장 자료형 4Byte로 총8Byte를 사용하게 된다. )

 

 

 이 실행흐름 안에 명령어를 통하여 함수의 매개변수, 지역변수 리턴주소 등을 저장한다. 

 이를 통틀어 함수의 실행 메모리라고 하는데 이는 보통 1~2Mbyte 정도 된다. (개발환경 마다 다름)

{ } 이 실행흐름(함수) 안에 1~2Mbyte 이상의 메모리사용할 수 없다는 이야기이다.

 

 

이는 배열을 선언해보면 알 수 있다. 

아래와 같은 코드를 실행하면 오류가 발생한다.

위와 같이 컴파일러가 경고하는 것을 볼 수 있다.

 

그럼 전역변수로 선언한다면?

위와같이 전역변수로 선언하면 전혀 문제없이 사용 가능한 것도 확인할 수 있다.

 

이 실행 메모리의 크기는 컴파일러에서 조정이 가능하다. 

저만큼 변수를 선언하지도 않기 때문에 의미도 없다.

8Byte 자료형 double을 2Mb만큼 선언한다고 하자.

2MB Byte로 환산하면 2,097,152Byte 이다.

8로 나누면 "262,144"

double 자료형을 26만개를 만들어야 한다는 것이다.

 

만약 실행 메모리 즉 스택 메모리 크기를 늘리고 싶다면 Visual Studio 에서는 아래와 같이 설정하면 된다.

프로젝트 -> 속성 -> 링커 -> 시스템

 

스택 예약 크기가 빈칸이면 1MB이다.

이를 늘리고 싶다면 Byte 단위로 입력해주면 된다.

예를들어 2MB를 입력하고싶다면  2,097,152 를 입력하면 된다.

( 근데 사실 그만큼의 배열이 필요한 경우가 있을까..? )

 

이는 둘째치고 만약 실행메모리의 크기를 넘어선다면 아래와 같은 일이 발생한다.

 

힙 오버플로우 & 스택 오버플로우

 힙 영역은 위에서 아래로, 스택 영역은 아래에서 위로 데이터가 저장된다. 이렇게 만든 이유를 유추해보면 힙 영역이 꽉 차서 스택 영역을 침범했을 때, 스택 영역에 첫부분에 데이터가 있으면 문제가 발생하기 때문에 사전 방지로 스택은 아래에서부터 데이터를 적재하는 방식으로 만들지 않았을까 생각했다.

 

'

 방금 말한것 처럼 힙 영역에서 스택영역을 침범한 것을 힙 오버플로우라고 하고, 스택영역에서 힙 영역을 침범한 것을 스택 오버플로우라고 한다. RAM의 스택영역보다 큰 데이터를 쓰는건 작은 프로젝트에서는 절대 일어날 수 없는 일이지만 위와같이 배열을 100억, 100조, 무한개로 생성하게 된다면 스택영역이 꽉 차서 힙 영역을 침범하게 될 것이다. 그럼 스택 오버플로우로 문제가 발생하게 되는 것이다.

 

혹은 배열을 참조할때 아래와 같이 호출한다고 쳐보자

Arr[-1] = 0;

 

-1은 첫 배열 주소의 뒤 쪽을 가리키게 되며 그곳이 만약 힙 영역이라면 스택 오버플로우가 발생하게 되는것이다.

오버플로우들은 보통 게임하다 보게되는 런타임 에러(Runtime error)의 원인들 중 하나이다.