프로그램 실행에 대하여 !(1)

 

안녕하세요.

엠볼릭스 입니다.

포스팅에 앞서 제 이야기를 하겠습니다.
프로그래밍에 입문하고자 프로그래밍 학원을 다녔었습니다.

강의 첫날 주제는 프로그램 실행에 대한 것이었습니다.

"프로그램 실행이 뭐에요?"

평소에 너무나 익숙하게 하는 동작이지만 자료구조를 수강하는 학생들중 대부분은 그 물음에 명쾌히 대답하지 못했었습니다.

물론 저도 포함입니다.

지금 생각해 보면 전산학에 대한 저의 첫 관심사은 바로 이 '실행' 이었습니다.

 

1. 프로그램 실행 과정


1.jpg

그림 1. 하드웨어 스팩

 

PC의 하드웨어 스팩입니다.

최상위를 차지하는 F4 있습니다.

* 프로세서 (CPU)

* 운영체제 (OS)

* 메모리 (주기억 장치)

* 하드디스크 (보조 기억장치)

가장 상위에 위치한 것을 보니 상당히 중요한 역할을 하는 것이 분명한데요.

실행에 있어서도 이 녀석을 빼놓고서는 이야기를 할 수가 없습니다.
그럼 이 녀석들로 실행에 대해서 이야기해 보겠습니다.

2.jpg

그림 2. 프로그램 실행과정

 

1. 로딩

윈도우 시스템에서 실행파일은 PE(Portable Excutable)파일 형식을 가지고 있습니다.

Visual studio로 컴파일하면 .exe 의 확장자를 가진 파일이 생성되게 됩니다.

바로 이 파일이 PE파일형식을 가지고 있습니다.

프로그램 실행 명령이 내려지면 OS는 PE파일의 header부분을 분석해서 프로그램의 진입지점을 적절하게 메인 메모리에 로딩하게 한 됩니다.

그 후 프로그램의 명령에 따라 필요한 순차적으로 명령어들을 로딩하게 됩니다.

 

2. 패치(Instruction Fetch)

메모리에 로딩된 프로그램은 CPU를 동작시키기 위한 명령어인 인스트럭션(Instruction)의 집합이라고 할 수 있습니다.

컴파일이라는 것은 결과적으로 우리의 소스 코드를 CPU가 이해할 수 있는 인스트럭션으로 변화시켜 주는 것입니다.

프로그램이 실행되기 위해서는 메모리에 로딩된 인스트럭션들을 순차적으로 CPU로 읽어들여야 합니다.

이러한 과정을 인스트럭션 패치라고 합니다.

 

3. 디코딩(Instruction Decoding)

그림 2번의 8d 44 24 0c 는 16진수로 표현 되었을 뿐 0,1 로 구성된 바이너리 코드입니다.

실행을 위해서는 0,1로 구성된 바이너리 코드를 각 필드별로 분해하여 어떤 종류의 연산을 수행 할 것인지를 결정해야 하고

연산에 필요한 연산자를 레지스터에 인가시키는 작업이 필요합니다.

이러한 작업을 디코딩이라고 합니다.

3.gif

그림 3.MIPS Instruction Format

출처 :http://www.cise.ufl.edu/~mssz/CompOrg/CDA-lang.html

 

MIPS CPU의 Instruction Format 입니다.

디코딩 과정에서 이러한 각각의 필드를 분석하게 되는 것입니다.

 

세가지의 Instruction Format 을 간단히 설명하면

R 타입은 수행될 명령의 종류를 나타내는 op 코드와 함께 rs , rt 필드에 연산에 사용될 레지스터의 번호가 저장되어 있으며 rd필드에 연산결과가 저장될 레지스터 번호가 저장되어 있는 Instruction format입니다.

I 타입은 rt 필드 대신에 address/immediate 필드를 두어서 상수값을 저장하는 필드로 활용하게 됩니다.

J 타입은 op 필드 6bit 를 제외한 26bit를 상수값으로 사용해서 더욱 넓은 범위의 메모리 address 접근을 할 수 있도록 해줄 때 사용되는 Instruction Format입니다.

 

정말 간단하게 이야기 했을 뿐 책에 더 많은 이야기가 있습니다.

단순히 그림만 보시면 외계어를 보는듯 하실 것 같아서 간단히 필드를 설명했을 뿐 왜 필드구분을 이렇게 했는지는 긴 이야기의 내용이기 때문에 저의 이야기 주제와는 약간 벗어나는 경향이 있습니다.

또한 MIPS CPU는 3가지 종류의 Instruction Format을 구분지어서 사용합니다.

Instruction의 길이를 동일하게 맞춰주는 것인데요.

이것과도 관련해서 파이프라인에 대한 이야기도 있습니다.

더 자세히 알고 싶으시다면 컴구조 관련 책을 참고 하시기 바랍니다.

 

우리 PC에서 주로 사용되는 Intel사 CPU의 Instruction Format 입니다.

4.jpg

그림 4.Intel 64 and IA-32 Architectures Instruction Format

출처 : http://www.intel.com

 

뭔가 복잡합니다. 이래서 초보 공학도들에게는 MIPS가 친구가 되는 것인지도 모르겠습니다.

아무튼 CPU에 따라서 Instruction Format도 달라진다는 것이겠죠.

 

참고로 MIPS Instruction 의 각 필드의 bit를 더하면 32bit 입니다.

흔히 말하는 32bit 머신을 이야기 하는 것 또한 CPU처리 단위를 이야기 하는 것으로 cpu가 한번에 처리가능한 연산 단위를 말하는 것입니다.

MIPS CPU는 인스트럭션 길이의 기준이 CPU의 처리 단위로 정하기 때문에 모든 인스트럭션이 32bit길이로 구성되게 됩니다.

 

4. 실행(Execution)

디코딩 과정에서 수행 될 연산의 종류는 연산코드의 형식으로 CPU에서 실질적인 연산을 수행하는 산술논리 연산장치(arithmetic-logic unit) : ALU 에 입력됩니다.

ALU는 연산코드에 맞춰서 레지스터에 저장된 연산자들을 이용하여 연산을 수행하게 됩니다.

하나의 인스트럭션이 실행을 마치면 다시 새로운 인스트럭션을 패치해 오게 되는 과정을 반복하게 됩니다.

이 과정을 좀더 자세하게 보면 이런 그림인 것입니다.

 

5.gif

그림 5. Fetch - Decode - Execute Cycle

출처 :http://www.cise.ufl.edu/~mssz/CompOrg/CDA-lang.html

 

앞서서 대략적으로 프로그램의 실행과정에 대해서 간략히 적어봤는데요.

사실 어려운 부분을 모두 빼고 이야기한 내용일 뿐 그 내부에는 어마어마한 이야기가 숨어 있습니다.

정말 자세하게 실행과정에 대해 알기 위해서는 PE실행파일의 원리 와 OS의 전반적인 내용까지 이해해야 합니다.

 

2. PE실행파일

우리는 컴파일을 C소스를 CPU가 인식할 수 있는 기계어(인스트럭션 집합)로 변환하는 것으로 알고 있습니다.

하지만 실질적으로 생성되는 .exe 파일은 단순한 인스트럭션의 집합만으로 이루어져있지는 않습니다.

 

6.jpg

그림 6. PE파일의 기본 구조

 

왼쪽 그림은 PE실행파일의 구조입니다.

그리고 오른쪽은 PE파일이 가상 메모리에 로딩(또는 맵핑) 되었을 때의 그림입니다.

네 드디어 가상메모리라는 이야기가 나왔습니다.

가상메모리는 일단 제쳐두고 중요한 내용은 PE파일의 구조를 간단히 보는 것 입니다.

구조를 크게 나눠보자면 Header 부분 Section 부분으로 나눌 수 있습니다.

 

Header 부분에는 실행에 필요한 정보들이 구조체 형식으로 저장되어 있습니다.

프로그램이 실행되면 OS의 Loader는 Header의 정보를 살펴보면서 가상메모리에 PE실행파일의 내용을 오른쪽의 그림처럼 로딩(맵핑)시키는 것입니다.

NT header에 IMAGE_OPTIONAL_HEADER32를 보면

ImageBase에 가상 메모리내에서 PE파일이 로딩되는 시작주소가 적혀 있습니다.

위 그림에서는 ImageBase 의 값이 0x1000000h 인 것입니다.

또한 각 Section Header에는 각 섹션들이 가상 메모리에서 차지하는 크기와 주소에 맵핑되는 상대적인

주소값(RelativeVirtualAddress)등이 적혀 있습니다.

RVA를 사용하는 이유는 가상메모리에 로딩되는 순간 그 위치에 다른 PE파일이 로딩되어 있을 수도 있기 때문입니다.

RVA가 아닌 절대적인 경로로 저장 되어 있을 경우 이러한 순간에 다른 주소로 재배치 하기가 곤란해지게 되는 것입니다.

또한 상대적인 주소로 되어 있기 때문에 어느 위치에 맵핑 되어도 메모리 참조가 가능하게 됩니다.

 

Section 부분을 보면

 

Section(".text") = 실행 코드

Section(".data") = 전역 변수

Section(".rsrc") = 리소스

 

3가지 섹션 영역들이 있습니다.

Section의 이름에서 유추해 볼 수 있듯이 각각의 섹션에 실행 코드와 전역으로 사용했던 변수, 그리고 리소스 들이 각각 나눠져서

저장되어 있습니다.

한마디로 우리가 C코드를 컴파일을 하면 우리가 한 소스코드 내에서 같이 사용했던 실행코드들과 전역변수들이 각각 분리되어서

따로 저장되는 것이었습니다.

Section 부분에는 우리가 코딩한 실질적인 프로그램의 내용이 저장되어 있는 것입니다.

 

3. 가상 메모리


드디어 여기까지 이야기가 진행 되었습니다.

자 그렇다면 앞서 간략히 말씀드렸던 실행으로 돌아가도록 하겠습니다.

프로그램이 실행되기 위해서는 메인 메모리(물리적인 메모리)에 프로그램의 인스트럭션이 올라와야 합니다.

그런데 우리는 가상메모리에 PE실행파일의 내용을 로딩(맵핑)했습니다.

실행파일의 내용을 메인 메모리에 바로 올리지 않고 번거롭게 가상메모리 어쩌고 하는건 왜일까요?

그리고 가상메모리에 실행파일을 로딩(맵핑)한다고 썼는데요.

실체도 없는 가상메모리에 어떻게 로딩(맵핑) 한다는 것이며 그 실체는 무엇일까요?

대학 OS과목을 들어보면 가상메모리와 메인메모리의 내용을 페이징기법으로 처리한다고 합니다.

실체도 없는 가상메모리의 내용이 어떻게 page out , page in 될까요?

 

그럼 자세한 내용은 다음 포스팅에 계속 이어가도록 하겠습니다.