들어가기 전에
우리가 작성한 C 코드를 실행하기 위해서는 컴파일링을 해줘야 합니다. C문법로 작성된 텍스트 형식의 파일은 컴파일링시 구체적으로 어떤 단계를 거쳐서 컴퓨터가 해석 가능한 파일로 변환될까요?
학습 목표
컴파일링의 네 단계를 설명할 수 있습니다.
핵심 단어
- 컴파일링
- 어셈블링
- 링킹
강의 듣기
들어가기 전에
우리가 작성한 C 코드를 실행하기 위해서는 컴파일링을 해줘야 합니다. C문법로 작성된 텍스트 형식의 파일은 컴파일링시 구체적으로 어떤 단계를 거쳐서 컴퓨터가 해석 가능한 파일로 변환될까요?
학습 목표
컴파일링의 네 단계를 설명할 수 있습니다.
핵심 단어
강의 듣기
지금까지는 아무것도 모른채 마구잡이로 쓴 코드가 잘 돌아갔다면 이제부터는 연습과 응용을 통해 동작 원리를 이해할 수 있을 것입니다.
우선 첫 수업에 봤던 예제를 다시 살펴보며 지금 사용하는 방법이 그때 우리가 사용한 방법과 어떻게 다른지 알아봅시다.
첫 수업에 봤던 C코드를 다시 봐보겠습니다.
우선 main이라는 함수가 있습니다. 프로그램의 시작점으로써 실행 버튼을 클릭하는 것과 같습니다.
printf는 출력을 담당하는 함수입니다.
printf 함수를 사용하기 위해서는 stdio.h 라이브러리가 필요합니다.
정확히 말하면 stdio.h는 헤더 파일로 C언어로 작성되어 있으며 파일명이 .h로 끝나는 파일입니다.
이 파일에는 printf 함수의 프로토타입이 있어서 Clang 컴파일러가 프로그램을 컴파일할때 printf가 무엇인지 알려주는 역할을 합니다.
코드를 clang hello.c로 컴파일하고 ./a.out 명령으로 프로그램을 실행할 때 이 과정은 컴퓨터가 이해하는 0과 1로 가득찬 파일 a.out을 생성하여 실행 가능하게 합니다.
이해하기 어려운 이 과정에 대한 이해는 잠시 미뤄두고 우선 넘어가보도록 하겠습니다.
만약 a.out과 다른 이름(hello)으로 컴파일을 하고 싶다면 아래와 같이 명령행 인자를 추가해야줘야 합니다.
clang -o hello hello.c
우리는 또한 CS50 라이브러리를 사용해 보았습니다.
이 처럼 CS50 라이브러리를 사용한 프로그램을 컴파일 할때는 clang에 또 하나의 프로그램(-lcs50)이 필요했습니다.
그래야 clang이 실행되었습니다.
clang -o hello hello.c -lcs50
이는 clang에게 CS50 라이브러리에 있는 모든 0과 1들을 여기에 연결하라는 의미입니다.
더 간단히는, 이 전에 배웠듯이 make 프로그램을 이용하면 이 모든 컴파일 과정을 자동으로 처리할 수 있습니다.
make나 clang을 사용해서 프로그램을 실행할 때 아래 네 개의 단계를 거칩니다.
우리가 명령어를 실행할 때 정확히 어떤 일이 일어나는지 알아보도록 하겠습니다.
전처리(Precompile)
컴파일의 전체 과정은 네 단계로 나누어볼 수 있습니다. 그 중 첫 번째 단계는 전처리인데, 전처리기에 의해 수행됩니다. # 으로 시작되는 C 소스 코드는 전처리기에게 실질적인 컴파일이 이루어지기 전에 무언가를 실행하라고 알려줍니다.
예를 들어, #include는 전처리기에게 다른 파일의 내용을 포함시키라고 알려줍니다. 프로그램의 소스 코드에 #include 와 같은 줄을 포함하면, 전처리기는 새로운 파일을 생성하는데 이 파일은 여전히 C 소스 코드 형태이며 stdio.h 파일의 내용이 #include 부분에 포함됩니다.
컴파일(Compile)
전처리기가 전처리한 소스 코드를 생성하고 나면 그 다음 단계는 컴파일입니다. 컴파일러라고 불리는 프로그램은 C 코드를 어셈블리어라는 저수준 프로그래밍 언어로 컴파일합니다.
어셈블리는 C보다 연산의 종류가 훨씬 적지만, 여러 연산들이 함께 사용되면 C에서 할 수 있는 모든 것들을 수행할 수 있습니다. C 코드를 어셈블리 코드로 변환시켜줌으로써 컴파일러는 컴퓨터가 이해할 수 있는 언어와 최대한 가까운 프로그램으로 만들어 줍니다. 컴파일이라는 용어는 소스 코드에서 오브젝트 코드로 변환하는 전체 과정을 통틀어 일컫기도 하지만, 구체적으로 전처리한 소스 코드를 어셈블리 코드로 변환시키는 단계를 말하기도 합니다.
어셈블(Assemble)
소스 코드가 어셈블리 코드로 변환되면, 다음 단계인 어셈블 단계로 어셈블리 코드를 오브젝트 코드로 변환시키는 것입니다. 컴퓨터의 중앙처리장치가 프로그램을 어떻게 수행해야 하는지 알 수 있는 명령어 형태인 연속된 0과 1들로 바꿔주는 작업이죠. 이 변환작업은 어셈블러라는 프로그램이 수행합니다. 소스 코드에서 오브젝트 코드로 컴파일 되어야 할 파일이 딱 한 개라면, 컴파일 작업은 여기서 끝이 납니다. 그러나 그렇지 않은 경우에는 링크라 불리는 단계가 추가됩니다.
링크(Link)
만약 프로그램이 (math.h나 cs50.h와 같은 라이브러리를 포함해) 여러 개의 파일로 이루어져 있어 하나의 오브젝트 파일로 합쳐져야 한다면 링크라는 컴파일의 마지막 단계가 필요합니다. 링커는 여러 개의 다른 오브젝트 코드 파일을 실행 가능한 하나의 오브젝트 코드 파일로 합쳐줍니다. 예를 들어, 컴파일을 하는 동안에 CS50 라이브러리를 링크하면 오브젝트 코드는 GetInt()나 GetString() 같은 함수를 어떻게 실행할 지 알 수 있게 됩니다.
이 네 단계를 거치면 최종적으로 실행 가능한 파일이 완성됩니다.
생각해보기
만약 컴파일링 과정을 거치지 않기 위해 바로 머신코드로 우리가 원하는 프로그램을 작성하려고 한다면 어떤 문제가 있을까요?
참고자료
comment
머리안좋은 나같은 사람들이 돌아버림
인간이 머신코드로 프로그램을 만드는 것은 불가능할 것 같아요.
비효율적으로 사용성이 낮아짐
활용하기도 읽기도 코드 짜기도 힘들 것 같음
매우 생산성이 떨어지게 될것입니다
머신코드는 인간친화적이지 않기 때문에 간단한 코드를 작성하는데도 매우 큰 어려움을 겪을 것입니다
간단한 출력문 작성도 매우 힘들어질것입니다.
가독성이 겁나 떨어지고 수정과 버전 업그레이드 할 때 엄청난 비용과 시간이 들것입니다.
비효율적
머신 코드로 바로 작성하는 것은 코딩이 효율성과 정확도를 떨어뜨리고 유지보수가 어렵다.
코드 가독성이 떨어지고 유지보수가 어려움
컴파일 과정을 거치지 않는다면 코드를 작성하는 데 어려움을 겪을 뿐만 아니라 유지보수 하는 것도 쉽지 않을 것 같다.
머신 코드는 이해하고 작성하기 어려운 언어라 디버깅과 유지 보수가 어렵고 깊은 이해를 필요로 하기 때문에 진입 장벽이 높아집니다
머신 코드는 사람이 이해하고 작성하기 어려운 저수준 언어야. 각 명령어는 특정 CPU 아키텍처에 대한 깊은 이해를 필요로 하며, 복잡한 로직을 표현하기 어려워.
머신 코드는 특정 하드웨어 아키텍처에 종속적이야. 따라서 작성한 코드를 다른 아키텍처에서 실행하려면 코드를 완전히 다시 작성해야 해.
머신 코드는 디버깅과 유지 보수가 매우 어려워. 코드의 의미를 파악하기 어렵고, 작은 변경 사항도 전체 코드를 이해해야만 가능해.
고수준 언어는 추상화를 통해 개발자의 생산성을 향상시켜. 반면, 머신 코드에서는 모든 작업을 수동으로 수행해야 해.
난이도가 높기때문에 진입장벽이 높아지고, 유지보수도 어려워질 것입니다.
1. 개발의 난이도가 급격하게 올라가 초보자들이 입문하기 매우 어려워집니다.
2. 코드 작성의 효율이 떨어지며 생산성이 현저하게 낮아집니다.
3. 가독성 또한 매우 안좋아지므로 버그픽스 및 유지보수가 힘들어집니다.
가독성, 편의성이 떨어지고, 오류를 찾아내기 힘들어진다
가독성, 접근성 저하
비정형적 코드로 오류의 증가
생산성이 많이 떨어질 수 있고 오류가 발생하면 오류를 잡기도 힘들 것 같습니다.
이진수의 코드를 사람이 바로 작성하기는 매우 어려우며, 오류 발생시 정확한 위치 파악 및 수정 또한 어렵다