[컴퓨터 과학 cs50] c언어의 동작원리는 어떻게 될까? 컴파일링과 어셈블링 링킹에 대해서!

2021. 1. 28. 18:22컴퓨터과학/cs50

반응형

지난 시간에 보았던 C코드입니다

 

우선 main이라는 함수가 있습니다. 프로그램의 시작점으로써 실행 버튼을 클릭하는 것과 같습니다.

printf는 출력을 담당하는 함수입니다.

printf 함수를 사용하기 위해서는 stdio.h 라이브러리가 필요합니다.

 

Q. stdio.h가  무엇을 의미할까?

헤더 파일로서, C언어로 작성되어 있으며, 파일명이 .h로 끝나는 파일이다.

이 파일에는 printf함수의 프로토타입이 있어서 Clang컴파일러가 프로그램을 컴파일러 할 때 printf가 무엇인지 알려주는 역할을 한다.

 

맨 끝에 붙은 ;은 새(new) 줄을 의미하며, 커서가 다음줄로 넘어간다.

 

int나 void같은 부분들을 이번 시간에 배워나간다.

 

코드를 clang hello.c로 컴파일하고 ./a.out 명령으로 프로그램을 실행하지만, 우리는 이보다 더 간단한 방법으로 실행한다! 이 과정은 컴퓨터가 이해하는 0과 1로 가득 찬 파일 a.out을 생성하여 실행 가능하게 한다.

사실 a.out은 이상한 프로그래명이죠? 많은 정보를 주지 않습니다!

 

만약 a.out과 다른 이름(hello)으로 컴파일을 하고 싶다면 아래와 같이 명령행 인자를 추가해야줘야 합니다.

그래서 -o hello라는 인자로 생성되는 파일명을 지정해줍니다~

clang -o hello hello.c
./hello

 

다음과 같이 코드를 바꿔보겠습니다~

#include <cs50.h>
#include <stdio.h>

int main(void)
{
	string name = get_string("What's your name? \n");
        printf("hello, %s\n", name);
}

cs50.h는 cs50라이브러리를 나타낸다. get_string과 같은 함수 프로토타입을 사전에 정의해줘서 C에서 기본으로 제공하는 것보다 더 많은 함수를 사용할 수 있게 해줍니다.

string이라는 데이터 종류도 있어 get_string은 그 파일 안에서 선언 된 것이죠!

name은 이름을 저장했던 변수이고, string은 이름을 저장한 변수의 종류입니다!

여기서 %s는 형식 지정자를 뜻하고 이 모든 건 cs50.h가 string과 get_string을 선언해서 가능한 것이다.

 

cs50에 대해서만 얘기하고 있지만 앞으로 사용할 수많은 함수들이 사용되는 방식이 같습니다.

 

우리는 또한 CS50 라이브러리를 사용해 보았습니다.

이 처럼 CS50 라이브러리를 사용한 프로그램을 컴파일 할때는 clang에 또 하나의 프로그램(-lcs50)이 필요했습니다.

그래야 clang이 실행되었습니다.

clang -o hello hello.c -lcs50

이는 clang에게 CS50 라이브러리에 있는 모든 0과 1들을 여기에 연결하라는 의미입니다.

더 간단히는, 이 전에 배웠듯이 make 프로그램을 이용하면 이 모든 컴파일 과정을 자동으로 처리할 수 있습니다.

 

파이썬을 배우면 더 이상 컴파일링을 하지 않아도 된다! 자동으로 컴파일이 되기 때문이다~

 

make나 clang을 사용해서 프로그램을 실행할 때 아래 네 개의 단계를 거칩니다.

  • 전처리 (preprocessing)
  • 컴파일링 (Compile)
  • 어셈블링 (Assemble)
  • 링킹 (Link)

전처리 (preprocessing)

#include <cs50.h>
#include <stdio.h>  //cs50.h와 stdio.h 두 라이브러리를 추가해라!!!

int main(void)
{
	string name = get_string("What's your name? \n");
        printf("hello, %s\n", name);
}

clang이나 make로 이 프로그램을 실행하게 되면 # 기호로 시작하는 이 두 줄은 해당 파일의 실제 코드로 대체된다.

 

cs50을 추가하는 이 첫 줄 대신에 해당되는 코드를 가져와서 hello.c라는 우리의 파일에 붙여 넣게 된다.

다음 줄인 stdio.h역시 파일 내의 해당하는 코드로 대체가 된다.

실제로 어떤 코드인지는 중요하지 않지만 int printf(string format, ..);~~~ 이런식으로 생겼다~

물론 위 아래로도 수많은 코드들이 추가 되었을거다!

 

컴파일링 (compiling)

소스 코드를 머신 코드로 바꾸는 단계

앞에서 거친 전처리 단계를 먼저 거친 후에 위에 코드는 clang과 같은 컴파일러에 의해 아래의 그림처럼 바뀐다.

이것을 어셈블리 코드라고 한다. 

수십년 전까지만 해도 사람들은 이런 코드로 프로그래밍을 했다. 이거에 비하면 C는 훨씬 보기가 좋다!

 

이 코드가 굉장히 어려워보이지만! 노란색 강조된 표시를 보면 몇 가지는 또 우리에게 익숙하다!

맨 위에 main이 있고 저 아래 printf도 있다! 

즉 코드가 clang에 의해 컴파일이 되면 C로 작성된 소스 코드는 어셈블리 코드라는 중간 단계로 바뀌고

컴퓨터의 뇌인 CPU가 실제로 이해할 수 있는 언어에 조금 더 가까워진다.

 

이제 노란색으로 강조된 부분은 명령어라고 부른다.

인텔이나 AMD와 같이 CPU를 만드는 여러 회사들이 만든 CPU가 실제로 이해하는 건 노란색 명령어처럼 아주아주 기초적인 수준의 명령어들이다!!

이들 역시 메모리에서 뭔가를 옮기거나 복사하거나 읽을 수 있다

혹은 화면에 표시하는 등의 일을 하지만 C보다 훨씬 이해하기 어려운 방식으로 수행한다.

 

하지만! 우리는 이런 것들을 자세히 알 필요가 없다! clang이 모두 다 해주기 때문이다!

 

어셈블리 (assembling)

 

하지만 어셈블리 코드가 생기면 그걸 실제 0과 1로 이루어진 머신 코드로 바꾸어야한다.

그게 바로 clang이 수행하는 어셈블링이라는 거다!

clang의 또 다른 부분이 바로 이 어셈블리 코드를 입력으로 받아 0과 1로 이루어진 머신 코드로 만든다!

아래의 그림을 보면 이해가 쉽다!

 

하지만 이 hello.c와 같은 프로그램은 여러 다른 파일들과 연관이 되어있다

 

위의 그림에서 노란색글씨인 나의 코드와 cs50라이브러리와 stdio.h파일 즉 3개의 다른 파일들을 clang이 컴파일해야한다. 만약 이를 위해 우리가 clang을 3번 실행해야 한다면 아주 귀찮겠죠?

다행히 그럴 필요는 없습니다! 모두 자동적으로 해주니까요!

 

이제 전처리와 컴파일링 어셈블링이 끝난 뒤 마지막 단계는 바로 모든 0과 1들을 하나의 큰 파일로 합치는 거다!

 

hello나 a.out처럼!!!

hello.c나 cs50.c와 같은 소스코드는 컴퓨터의 하드 드라이브 어딘가에 있다!

stdio.c 역시 하드 어딘가에 있는데 사실 stdio.c안에 있는 printf.c라는 파일을 사용한다.

이 각각이 어셈블링이되어 hello나 a.out이라고 하는 하나의 큰 파일이 된다.

 

이제 이 모든 것을 컴파일링이라고 한다.

코드를 컴파일한다고 하면 이 모든 일들이 일어나게 된다.

 


개요

컴파일은 소스 코드를 오브젝트 코드로 변환시키는 과정입니다. 여기서 소스 코드는 여러분이 C언어와 같은 프로그래밍 언어로 작성한 코드이고, 오브젝트 코드는 기계어라고 알려져 있는데, 0과 1로 이루어져 있으며 컴퓨터에게 프로그램이 어떻게 실행되어야 하는지 알려주는 코드입니다. make명령어 자체는 컴파일러가 아니고, clang이라는 컴파일러를 호출해서 C소스 코드를 오브젝트 코드로 컴파일 하도록 합니다.

 

전처리(Precompile)

컴파일의 전체 과정은 네 단계로 나누어볼 수 있습니다. 그 중 첫 번째 단계는 전처리인데, 전처리기에 의해 수행됩니다. # 으로 시작되는 C 소스 코드는 전처리기에게 실질적인 컴파일이 이루어지기 전에 무언가를 실행하라고 알려줍니다.

예를 들어, #include는 전처리기에게 다른 파일의 내용을 포함시키라고 알려줍니다. 프로그램의 소스 코드에 #include 와 같은 줄을 포함하면, 전처리기는 새로운 파일을 생성하는데 이 파일은 여전히 C 소스 코드 형태이며 stdio.h 파일의 내용이 #include 부분에 포함됩니다.

 

 

컴파일(Compile)

전처리기가 전처리한 소스 코드를 생성하고 나면 그 다음 단계는 컴파일입니다. 컴파일러라고 불리는 프로그램은 C 코드를 어셈블리어라는 저수준 프로그래밍 언어로 컴파일합니다.

어셈블리는 C보다 연산의 종류가 훨씬 적지만, 여러 연산들이 함께 사용되면 C에서 할 수 있는 모든 것들을 수행할 수 있습니다. C 코드를 어셈블리 코드로 변환시켜줌으로써 컴파일러는 컴퓨터가 이해할 수 있는 언어와 최대한 가까운 프로그램으로 만들어 줍니다. 컴파일이라는 용어는 소스 코드에서 오브젝트 코드로 변환하는 전체 과정을 통틀어 일컫기도 하지만, 구체적으로 전처리한 소스 코드를 어셈블리 코드로 변환시키는 단계를 말하기도 합니다.

 

 

어셈블(Assemble)

소스 코드가 어셈블리 코드로 변환되면, 다음 단계인 어셈블 단계로 어셈블리 코드를 오브젝트 코드로 변환시키는 것입니다. 컴퓨터의 중앙처리장치가 프로그램을 어떻게 수행해야 하는지 알 수 있는 명령어 형태인 연속된 0과 1들로 바꿔주는 작업이죠. 이 변환작업은 어셈블러라는 프로그램이 수행합니다. 소스 코드에서 오브젝트 코드로 컴파일 되어야 할 파일이 딱 한 개라면, 컴파일 작업은 여기서 끝이 납니다. 그러나 그렇지 않은 경우에는 링크라 불리는 단계가 추가됩니다.

 

 

링크(Link)

만약 프로그램이 (math.h나 cs50.h와 같은 라이브러리를 포함해) 여러 개의 파일로 이루어져 있어 하나의 오브젝트 파일로 합쳐져야 한다면 링크라는 컴파일의 마지막 단계가 필요합니다. 링커는 여러 개의 다른 오브젝트 코드 파일을 실행 가능한 하나의 오브젝트 코드 파일로 합쳐줍니다. 예를 들어, 컴파일을 하는 동안에 CS50 라이브러리를 링크하면 오브젝트 코드는 GetInt()나 GetString() 같은 함수를 어떻게 실행할 지 알 수 있게 됩니다.


이 네 단계를 거치면 최종적으로 실행 가능한 파일이 완성됩니다.

 

반응형