Operating System

[System Software] System Software & Program Execution

Sara.H 2021. 2. 12. 14:42

고급언어, 컴파일러, 링커, 로더, 어셈블러, 라이브러리

프로그램이 실행되는 과정

C언어와 같은 고급 언어로 프로그램을 작성하면 컴파일러는 소스코드를 컴파일하여 이를 엉셈블리 프로그램으로 변환한다. 어셈블러는 이를 기계어 프로그램(오브젝트 파일)로 변환하고, 다른 오브젝트 파일들과 라이브러리 파일들을 링커가 합쳐서 Executable File 을 만든다. 오브젝트 파일은 불완전한 형태의 기계어 파일로, 불완전한 이유는 우리가 하나의 파일에만 코드를 작성하지 않고 다른 파일에 분리해 작성하여 생긴 파일 및 내가 작성하지 않았지만 다른 사람이 작성한 함수들을 합치지 않았기 때문이다. C언어를 예로 들면 Math 나 Print 관련 함수들을 떠올릴 수 있다. 링커는 이 파일들을 합쳐 실행가능한 파일을 생성한다. GCC같은 컴파일러 소프트웨어는 위와 같은 일련의 과정들을 내포하고 있다. 프로그램이 프로세스가 되어 실행이 되려면 메모리에 올라가야 한다. Loader는 내가 생성한 Executable File을 메모리에 올려 실행하는 역할을 하며 필요에 따라 공유 라이브러리를 동적/정적으로 로딩하여 프로그램 실행을 돕는다.

 

컴파일러, 어셈블러, 링커, 로더

위와 같은 시스템 소프트웨어를 우리가 전혀 몰라도 되는 이유는(적어도 일반인이면 인지할 필요가 없다) 우리가 명시적으로 지정하는 것들이 아니래 대개 툴셋에 포함되어 있기 때문이다. 윈도우 환경에서는 비주얼 C 를 이용하고, 리눅스에서는  gcc 컴파일러를 사용하는 것이 예이다. 이런 소프트웨어들은 컴파일러, 어셈블러, 링커 들을 함께 갖고 있다. 소스파일만 유저가 명시해주면 곧바로 실행 파일을 만들어준다.로더라는 소프트웨어 또한 직접 사용한 기억이 없을것이다. 그 이유는 운영체제 내부에 로더가 포함되어 있기 때문이다. GUI환경에서 실행 파일을 클릭하면 자동적으로 로더가 메모리에 이 프로그램을 올려 실행해준다.

 

고급언어, 어셈블리언어, 기계어

컴파일러 방식의 C언어 같은 경우 컴파일 후에 기계어로 된 실행 파일이 나오는데에 반면 인터프리터 방식은 고급 언어로 작성한 프로그램을 기계어로 미리 바꾸어두는 것이 아니라 실행을 시키면 그 때 고급언어 문장 하나를 기계어로 바꾸면서 실행한다. 즉, 기계어로 바꾸는 시점의 차이가 컴파일 언어 / 인터프리터 언어의 차이인 것이다.

 

고급언어, 어셈블리언어, 기계어 예시

컴파일러의 역할 알아보기 : 오브젝트 파일은 왜 불완전할까?

앞서 오브젝트 파일을 '불완전한 기계어 파일'이라고 표현했다. 이 이유를 알기 위해서는 컴파일러가 수행하는 역할 중 부분컴파일이라는 것을 이해할 필요가 있다. 부분컴파일이란, C언어 소스가 여러개 있을 때 각각의 오브젝트파일을 묶어서 실행 파일을 만드는 것이다.

부분 컴파일 예시 코드

위와 같이 서로 다른 두개의 파일에다가 코드를 짰다고 하자. one.c에는 전역 변수가 두개가 있고, 정적 변수도 있고 메인 함수도 있다.

위의 함수를 컴파일하면 에러가 나지는 않지만 실행시킬수는없다. func 함수가 파일 내부에 정의되어 있지 않기 때문이다. func함수는 two.c 파일 안에 정의되어 있다. two.c 파일을 봐도 extern 으로 정의된 변수를 사용하는 등, 해당 파일 안에서 정의되지 않은 변수를 사용하고 있다. 각 파일에서 서로 정의된 변수들을 제대로 가져다 사용하려면 위 둘의 파일들을 묶어서 실행해야 할 것이다.

 

컴파일 할때는 함수나 변수가 "선언이 되어 있는지"를 확인한다. 정의는 밖에 되어있어도 된다.

c 의 자료형이 선언이 되어 있는가,

func 라는 함수가 선언이 되어 있는가 (즉, input, output 에 대한 정의가 되어있는가)

등을 확인하고 컴파일러가 OK해줌.

 

int c 하나를 static 으로 정의하지 않고, 그냥 int c 로 선언하면, 전역변수는 전체 프로그램에서 동일한 이름을 가진 것이 하나만 있어야 하는 것이므로 실행중에 에러가 난다.

- 에러를 해결하려면 하나의 c 를 d로 바꾸는 등 ... 이름을 바꿔주어야

- 근데 귀찮다.

 

그래서 한 쪽의 변수 이름 앞에 static 을 붙여주면 .. 에러 없이 링크가 잘 된다.
static 이라는 것은 그 안에서만 참조할 수 있다 라는 뜻 !
그래서 c라는 변수는 이 파일 안에서만 볼 수 있는 변수라는 뜻임.
다른 파일에 있는 c는 다른 파일들에서도 사용할 수 있으나, static int c 는 해당 파일 안에서만 사용 가능.

변수 x 를 a 에 선언해두고 b와 c 파일에서 공유해서 쓰는 것

extern int x; 라고 선언하면, 파일 안에서 직접 공간을 만들어서 쓰는게 아니라 외부의 것을 가져다가 쓰겠다는 것임.

 

이렇게 부분 컴파일을 해주면 심볼의 위치는 명확하지 않고 type 만 파악한다.

💡 이런 상태의 기계어 파일을 오브젝트파일이라고 한다 ! (symbol 들의 위치가 명확하지 않기 때문에 불완전한 기계어파일이라고 말한 것)

 

💡 부분 컴파일 된 오브젝트파일들을 묶어서 링킹을 해주면 symbol 의 위치가 명확해지고

그 파일들이 모여서 .out 혹은 .exe 파일이 된다.

 

file.c 파일에서 변수들을 위와 같이 만들어서 사용하는데

extern int z; 즉 외부에서 가져다 쓰는게 있다고 하자.

z 의 값에서 1을 빼라는 문장은 정확한 MIPS 인스트럭션은 아니지만, 먼저 메모리에서 z 변수의 위치에 있는 값을 레지스터로 읽어들이고, 레지스터의 값에서 1을 빼고 원래의 메모리 주소에 저장하는 인스트럭션으로 바뀔 것이다.

1을 증가시키는 것도 마찬가지일것.

 

하지만 z 는 외부에 있기 때문에 오브젝트파일에 z의 정확한 주소값이 들어갈 수 없음. - 온전하지 못함.

 

컴파일러가 오브젝트파일을 만들때는 그래서 심볼테이블이라는 것을 유지하고 있음.

각각의 심볼들 마다 (변수나 함수들마다) 의 메모리 주소를 표시해두는 테이블.

기계어가 실행될 때 어디로 점프할지 주소를 쭉 유지하고 있다가, 나중에 심볼들을 기계어에서 없애면서 주소값으로 치환하게 되는데 z의 경우는 알 수 없다.

 

그러다가 다른 어딘가에 z가 정의되어 있으면 실행 파일을 묶을 때야 비로소 주소를 넣어줌.

 

 

-- 다음 강의

 

실행파일이 잘 만들어졌다는 의미는

각 파일에서 사용한 변수나 함수들은 어딘가에 정의가 되어있다는 뜻.

그렇지 않으면 링크할 때 에러나 난다. 중복 정의된 경우도 에러가 날 것.

 

큰 프로그램을 작성할 때 일어나는 문제들을 잘 해결하는 테크닉?

 

헤더 파일을 사용하는 이유?

만약 파일의 개수가 1000개가 넘고 변수가 많다면 일일이 고치는게 시간낭비 ...

 

그래서 외부 참조를 하는 변수들은 헤더파일로 따로 빼게 됨.

 

헤더 파일에 적어두고 다른 파일들에서 extern 대신 include 를 넣어줘서 사용함.

 

인클루드로 들어간 헤더 파일의 변수들을 프로세싱 후에 extern 으로 치환함

 

 

라이브러리

원칙적으로는 라이브러리 함수들을 사용하는 경우, 그것에 대한 선언이 있어야 한다.

라이브러리 함수들에 대한 헤더 파일들이 존재함.

헤더파일에 가면 함수의 정의가 아니라 선언이 있음.

 

printf 함수는 인풋으로 어떤 자료형이 들어가고 아웃풋으로 어떤게 들어간다 등 ...

 

실제 함수의 정의는 별도의 라이브러리 파일에 존재한다.

 

헤더파일은 리눅스 환경의 경우 usr/include/sys 등과 같은 곳에 존재함.

 

그래서 라이브러리까지 포함해서 컴파일되고 실행되는 과정을 설명하면

 

내가 함수나 변수들을 정의하고 사용한 정보들이 .c 파일 어딘가에는 존재해야 함. 라이브러리 함수는 예외.

(라이브러리) libc.a 파일과 내가 정의했던 c 파일들에 대한 .o 파일들이 생성됨.

 

printf 는 내가 작성한 파일 어디에도 없지만 라이브러리에 있어서 이것들을 묶어서 실행파일을 만들면 정상 동작함.

=> a.out 파일

 

정적 링킹, 동적 링킹

라이브러리 파일이 두 종류가 있다.

 

- 실행 파일에 포함되는 것 => 스태틱 라이브러리

- 포함되지 않는 것 => 공유 라이브러리

 

라이브러리들을 연결하는 과정을 static linking, dynamic linking 이라고 함.

 

static linking 은 Object -> Executable 로 바꾸는 과정이고

 

dynamic linking 은 라이브러리가 실행 시 link 된다.

 

내 프로그램도 다른 프로그램도 printf 를 다 사용한다고 하면, 이 두 프로그램을 실행시켰을 때

printf 코드가 별개의 메모리 공간에 올라가기 때문에 메모리의 낭비가 있을 수 있다. (static linking 의 특징)

 

하지만 공유 라이브러리는 라이브러리 함수가 프로그램에 포함되는 것이 아니라, 라이브러리 파일을 찾을 수 있는 위치 정보만 넣어놓고, 실제 코드는 실행파일에서 제외됨.

정말 그 함수가 호출되면 그 때 link 가 이루어진다.

 

근데 왜 shared 라는 용어를 쓰는가?

상대의 프로그램도, 나의 프로그램도 같은 함수를 사용하는 경우, 해당 함수가 동적으로 링킹이 이루어지면

먼저 내 프로그램을 실행시킨다고 했을 때 해당 함수가 내 코드에 포함되어 있지 않고, 함수를 스토리지에서 동적으로 찾아서 실행시키다가, 다른 사람의 프로그램도 똑같은 함수를 실행시켰을때 해당 코드를 (파일 형태로 존재할것) 찾아서 쓸건데, 이미 메모리에 올라와있기 때문에 스토리지에서 찾을 필요가 없다.

 

컴파일하고 링크를 해서 실행 파일을 만든는 시점에 라이브러리 코드가 내 프로그램에 들어가는게 static lib 이고, shared 의 경우 함수의 위치 정보만 실행 파일에 적어두고, 실제 실행을 시키는 도중 함수 호출이 일어나면 그 때 라이브러리 함수가 포함된 파일을 메모리에 올려서 실행한다.

 

바인딩 : 갖다 붙이는 것. (위치를 찾는 것?) 이라고 생각하면 될 듯 ..?

라이브러리가 그 프로그램에 달라 붙는 것이라 생각하자.

 

언제 달라붙느냐?

스태틱 라이브러리의 경우 compile 하고 link 되는 시점에 달라붙는다. 보통 compile 과 link 는 한꺼번에 이루어짐.

공유 라이브러리는 그 프로그램의 실행파일이 실행될 때 라이브러리 함수의 호출 시점에 프로그램에 달라붙어 실행. 런타임에 바인딩됨.

 

shared library 의 단점 ?

- 내가 만든 프로그램이 공유 라이브러리를 사용한다면 해당 실행파일을 돌리는 다른 환경에서도 동일한 공유 라이브러리가 있어야 함.

- 만약 동일한 환경이면 좋지만 그렇지 않은 경우 실행되지 않을수도. (실행파일만으로는 프로그램이 돌아가지 않는...)

- 다소 속도가 느리다는 단점도 있음. 실행 중에 라이브러리 위치 찾고, 메모리에 올려 실행해야 하기 때문에 오버헤드.

- 또 한가지 오버헤드는 컴파일 할 때도 공유 라이브러리가 들어간 것을 컴파일 하면 좀 느려짐.