이 글에서는 주소 바인딩(Address Binding)을 이해하기 위해 꼭 알아야 하는 용어들을 설명한다. 원래 주소 바인딩을 바로 다루려고 했으나 정리하다가 용어가 헷갈려서 공부할겸 정리한다. 컴파일, 런타임, 로딩을 정확하게 이해해야 주소 바인딩을 이해할 수 있다.
컴파일이란?
컴파일은 원시 코드를 컴퓨터가 이해할 수 있는 언어로 번역해주는 과정을 말한다. 아래 그림을 보자.
위 그림은 컴파일 과정을 나타낸 그림이다. 우리는 컴퓨터가 0과 1만 이해할 수 있다는 것을 잊으면 안 된다. 우리가 흔히 쓰는 파이썬이나 자바, C++과 같은 고급 프로그래밍 언어는 컴퓨터 입장에서는 외계어다. 따라서 우리가 짠 소스코드를 컴퓨터가 이해할 수 있는 언어로 번역해주는 과정이 필요한데, 이를 '컴파일'이라고 한다. 이제 각 단계의 역할을 하나씩 살펴보자.
1. Preprocessor
직역하면 전처리기다. #로 시작하는 부분(#include, #define)을 소스코드로 변경해준다. #include 같은 경우 해당 헤더파일에서 코드를 복붙해주고, #define의 경우 정의한 값 또는 식으로 치환 및 적용시켜준다. 예를 들어 C++에서 <iostream> 라이브러리를 include 시키고 , 코드에 '<< cout' 이 있으면 전처리기는 <iostream> 라이브러리의 '<< cout' 함수 코드를 복붙해준다.
2. Compiler
컴파일러는 전처리기로 확장된 소스코드를 기계어에 근접한 어셈블리어로 번역해주는 녀석이다. 엄밀히 말해서 아직까지는 이진수로 번역된 상태는 아니다.
3. Assembler
어셈블리 언어를 기계어로 번역해준다. 이 단계에서 생성된 파일을 목적 파일(object file)이라고 한다. 여기까지를 보통 컴파일이라고 칭한다.
4. Linker
여러 개의 object file을 하나로 합치는 과정이다. 작성된 소스코드가 사용하는 OS API(system call) 또는 표준 라이브러리를 연결시켜서 exe 파일로 만든다. 실행 파일(.exe)은 타겟 파일(target file)이라고도 한다.
하나의 프로젝트에 여러 개의 소스코드 파일이 있으면 각 소스코드마다 목적 파일이 생성되고, Linker가 모든 목적 파일과 라이브러리를 종합해 하나의 타겟 파일을 만드는 것이다.
여기서 원시코드~목적파일 까지를 'Compiling', 목적파일~타겟파일 까지를 'Linking'으로 부른다. Compiling 구간만을 컴파일로 보는 관점을 '좁은 의미의 컴파일'이라 하고, 'Compiling + Linking' 전 과정을 컴파일로 보는 관점을 '넓은 의미의 컴파일'이라고 한다. 아래 그림은 컴파일의 전 과정을 이해하기 쉽게 그린 그림이다.
Static Linking vs Dynamic Linking
위에서 설명한 Linking 과정은 정적 링킹(Static Linking) 방법이다. 실행파일 안에 모든 내용이 다 들어있기 때문에 별도의 라이브러리가 필요 없다. 따라서 실행시 속도가 빠르다는 장점이 있지만 잘 생각해보면 이는 심각한 메모리 낭비다.
수십명의 유저가 리눅스 서버에 접속해 동시에 정적 링킹으로 만들어진 실행 파일을 실행시키는 상황을 생각해보자. 실행 파일은 자신의 이름을 출력해주는 아주 간단한 프로그램이다. 여기서 출력하는데 사용된 cout이라는 클래스 라이브러리가 정적으로 링킹되어 있다는 것은 실행 파일에 cout 클래스 라이브러리 코드가 다 들어가있다는 뜻이다. 즉, 50명이 이를 동시에 실행시키면 똑같은 cout 코드가 메모리에 50번 적재되는 것과 같다.
그래서 대안으로 등장한게 동적 링킹(Dynamic Linking) 방법이다. 많이 쓰이는 라이브러리 함수를 메모리에 한 번만 올리고, 프로그램이 라이브러리 함수를 호출할 때 메모리에 있는 함수 주소로 점프해 실행한 후 다시 돌아오도록 하는 방법이다. 이러한 동적 링크 라이브러리를 윈도우에서는 DLL(Dynamic Link Library)라 부르고 유닉스나 리눅스에서는 Shared Library로 부른다. 그러나 동적 링킹은 점프 과정에서 약간의 오버헤드가 생기고, 점프를 위한 불필요한 코드가 추가된다는 단점이 있다.
게임을 샀는데 게임의 라이브러리 일부분이 버전up 된 경우를 살펴보자. 만약에 게임이 정적 링킹으로 만들어졌다면 버전up 된 게임을 새로 구매해야 한다. 왜냐하면 실행 파일 안에 라이브러리 코드가 다 들어있기 때문이다. 하지만 게임이 동적 링킹으로 만들어졌다면 게임을 새로 사는 것이 아니라 DLL만 업데이트 하면 된다. 왜냐하면 동적 링킹은 원래 코드와 라이브러리를 별도로 분리해놓기 때문이다. 라이브러리만 버전up 시키면 그 성능을 그대로 받을 수 있다.
런타임이란?
런타임이란, 사용자에 의해 응용프로그램이 동작되어지는 때를 말한다. 무슨 말인지 잘 모르겠다면 '프로그램 실행중'으로 이해하면 된다. 만약 어떤 소스코드가 이미 실행 가능한 프로그램으로 컴파일 되었다 할지라도 이것은 여전히 프로그램 실행중에 버그를 일으킬 수 있다. 예를 들어 프로그램은 예상치 못한 오류(0 나누기) 또는 충돌로 동작하지 않을 수 있는데, 이렇게 프로그램 실행중에 발생하는 형태의 오류를 런타임 오류라고 한다.
동적 링킹에서 라이브러리 함수로의 점프도 런타임 단계에서 실행된다. 동적 링킹이란, 다시 말하면, 실행 가능한 목적 파일을 만들 때 프로그램에서 사용하는 모든 라이브러리 모듈을 복사하지 않고 해당 모듈의 주소만을 가지고 있다가, 런타임에 함수 호출시 해당 모듈의 주소로 가서 필요한 것을 들고 오는 방식이다.
아까 위에서 다룬 게임 예시를 다시 생각해보자. 동적 링킹은 원래 코드와 라이브러리를 분리시킨다고 했었다. 따라서 게임 실행 중에 라이브러리 함수를 호출할 일이 있으면 해당 함수가 있는 메모리 주소로 점프하면 된다. 라이브러리가 없다면 어떻게 될까? 일단 게임이 실행되기는 할 것이다. 다만 라이브러리 함수를 만나면 점프할 주소가 없으므로 런타임 오류가 발생할 것이다.
로딩이란?
로딩은 데이터를 메모리로 옮기는 것을 말한다. 컴파일이 완료된 프로그램을 실행시키면 .exe에 있는 파일이 메모리에 올라가게 되는데, 이를 로딩이라고 한다. 로딩도 크게 Static Loading 과 Dynamic Loading 이 있다. 위에 링킹과 설명이 거의 중복된다.
Static Loading 은 모든 데이터를 메모리에 옮기는 것이다. 그러나 이런 방법은 메모리 낭비가 심하기 때문에 쓰이지 않는다. 실제로 프로그램이 실행될 때 쓰이는 기능은 일부분이다. 프로그램 실행 중에 사용되지 않는 기능도 정말 많다. 그래서 요즘에는 routine이 call 돼야 메모리에 올라가는 Dynamic Loading을 사용한다. 참고로 routine이란 '코드의 한 부분' 을 말한다.
'CS > 운영체제' 카테고리의 다른 글
[ 운영체제 ] 메모리 관리 1 - 페이징(Paging) (2) | 2022.02.04 |
---|---|
[ 운영체제 ] 주소 공간(Address Space), 물리적 주소(Physical Address), 논리적 주소(Logical Address), 주소 바인딩(Address Binding) (2) | 2022.02.02 |
[ 운영체제 ] 메모리 계층 구조 (0) | 2022.02.01 |
[ 운영체제 ] Banker's Algorithm (0) | 2022.01.31 |
[ 운영체제 ] 데드락(Deadlock) 해결 방법 (0) | 2022.01.31 |