우분투에서 C++ 개발하기 (1) - 컴파일 과정 및 gcc

이 글은 윈도우에서 Visual Studio만 쓰다가 Ubuntu를 생소하게 생각하는, 나와 같은 사람을 위해 쓴다.

글쓰기에 들어가기 전에 Visual Studio와 같은 통합 환경에 비해 리눅스에서 C++과 같은 언어를 개발한다는 것이 얼마나 복잡한 일인가부터 상기시켜야겠다. C++ 자체가 원래 언어차원에서부터 빌드가 영 까다로운 부분이 있다. 리눅스 사용자는 그 문제를 그대로 정면으로 맞이해야 한다.  그나마 편리하게끔 만든 것이 3편에서 나오는 CMake 정도인데, 이것마저도 복잡한 스크립트를 직접 입력해야 한다.

자유롭고 오픈될 수록 불편하고 복잡하다. 자유도가 높다는 것은 그 자유를 누릴 수 있을 수준으로 공부와 노력을 들여야 겨우 좀 사용할만하다는 뜻이 된다. 


하여튼 글 쓰기의 계획은 이렇다.

1. 맨 땅에 삽질하는 심정으로 g++컴파일러를 직접 이용해본다.
2. makefile을 이용해본다.
3. CMake를 이용해본다.
4. vscode를 활용하여 개발환경을 구축해본다.

그 첫번째, 맨 땅에 삽질하기.


1. C++의 컴파일 과정


VS만 쓰다보면 컴파일러 개념이 희박해지는데, 이는 당연한 것이다. VS라는 훌륭한 툴이 있기 때문에 우리는 아무 것도 신경 안 써도 된다. 원래 정치 선진국일수록 국민들이 정치에 무지하다. 그러나 리눅스를 제대로 이용하려면 컴파일에 대해서 좀 자세히 알아야 한다.

컴파일 과정을 모르는 사람을 위해 최대한 자세히 설명해본다.
컴파일 과정은 다음과 같다.

[소스코드] -> [바이너리] -> [실행파일]

소스코드는 사람이 읽을 수 있는 텍스트 파일이다. 이것을 컴퓨터 명령코드(기계어)로 번역하게 되는데, 이렇게 번역된 결과물을 보통 '오브젝트 파일', 혹은 '바이너리 파일'로 부른다. 두 명칭 모두 약간 애매모호한 점이 있어서 설명해본다.

우선 '오브젝트 파일'이란 말은 컴파일의 목적이 된다는 뜻에서 나왔다. 또 다른 말로 '타겟 파일' 혹은 '오브젝트 코드', '타겟 코드' 이런 말들로 불리는데 다 같은 말이다. 소스의 반댓말이 타겟 아니겠는가.

'바이너리 파일'란 말은 본래 '텍스트 파일'의 반댓말로서, 아스키 코드로 작성되어 텍스트로 읽을 수 있는 파일이 아닌 것들의 집합이다. 즉, 사람이 읽을 수 없는 파일이란 뜻이다. 소스코드는 사람이 읽을 수 있는 텍스트 파일의 일종이다. 반대로 기계어로 번역된-컴파일된 파일은 사람이 읽을 수 있는 텍스트 파일이 아니다. 이런 의미에서 컴파일된 결과물을 바이너리라고 부르는 것이다.

파일은 두 가지로 나뉜다. 프로그램과 프로그램이 아닌 것. 프로그램은 실행이 가능한 명령어로 구성된 것이고, 그렇지 않은 문서파일, MP3등은 프로그램이 아니다. 여기서 바이너리는 큰 의미에서 프로그램으로 봐야 한다. 얘기하다 보니 점점 산으로 간다.

하여튼 우리가 작성한 C++코드는 바이너리(프로그램)으로 번역된다. 바이너리는 여러 개일 수 있다. 그런데 이들은 바로 실행할 수 없다. 이들을 묶어서 OS가 실행할 수 있는 실행파일로 만들어주는 작업이 링크이다. 바이너리는 다시 말해서 프로시져(함수)들의 묶음이다. 이 함수들 중에 main이란 놈이 있다면, 이것을 시작점(엔트리 포인트)으로 해서 프로그램을 실행한다. 그래서 main 함수는 무조건 하나 있어야 하며, 한 개만 있어야 한다.

main이 없으면 바로 실행할 수는 없지만 라이브러리는 될 수 있다. 정적 라이브러리, 동적 라이브러리, 이런 친구들이 될 수 있다는 것이다. 이런 라이브러리들은 main이 없다.

결론적으로 컴파일 작업을 수행하는 컴파일러와 링크 작업을 수행하는 링커는 엄연히 다른 존재다. 그러나 보통 컴파일러라고 하면 링커의 기능도 들어있다.


2. 컴파일러 잡설, gcc


컴파일러는 소스코드를 기계어로 번역하는 작업을 한다고 했다. 그런데 우리는 한 가지 기계(컴퓨터, CPU)만 가지고 있는 것이 아니다.

재미삼아 미약한 지식을 동원하여 컴퓨터의 역사를 살펴보면 옛날에는 컴퓨터란 놈이 표준이 없고 우후죽순으로 각자 자기만의 컴퓨터를 개발해서 쓰곤 했다. 완전히 다른 컴퓨터들이었기 때문에 서로 명령어와 구조가 달랐고, 그래서 각자 컴파일러를 따로 가지고 있었고, 프로그램 호환도 전혀 안 됐다. 그러다 IBM에서 자신들의 CPU 아키텍쳐를 오픈하면서 시장을 거의 점령했고, 이 IBM 구조의 CPU 시장을 점령한 회사가 인텔, 그리고 AMD이다(약 80년대 후반부터). 이 IBM호환 기종에서도 약간 변종이 있는데, 80386 CPU 시절 개발된 x86(32bit) 구조, 그리고 AMD에서 개발한 x86-64(64bit) 구조가 있다. 현재는 이 두 가지 정도 쓰이고 있는데 점점 64bit로 점령되는 추세이다. x86시리즈는 대부분 윈도우 운영체제가 탑재되며 리눅스도 많이 쓰인다. MacOS도 x86으로 갈아탔다.

여기까지는 일반적인 데스크톱 컴퓨터의 얘기인데, 임베디드 - 소형화된 컴퓨터에서는 사정이 좀 다르다. 아직도 다양한 경쟁사들이 저마다의 CPU 아키택쳐로 경쟁하고 있는 형국이며, 그 중에서도 단연 1위는 ARM이다. 스마트폰에 들어가는 CPU가 대부분(내가 알고 있는 전부는) ARM으로 되어 있고 라즈베리파이도 ARM이다. 운영체제는 전부 리눅스(안드로이드)이다. ARM은 버전 6부터 시작해서 현재 8까지 나와있다. 

컴파일러는 CPU구조, 그리고 운영체제에 따라서 달라진다. 그 말인 즉슨, 프로그램을 하나 만들었을 때, 이 프로그램이 실행될 수 있는 CPU, 그리고 운영체제가 정해져 있다는 것이다. 

여러분들이 CPU를 하나 만들었다고 하자. 그러면 그 CPU를 동작시킬 수 있는 예제 코드가 필요할 것이다. 그리고 그 코드는 보통(100%) C언어가 된다. C언어로 Hello World를 만든 뒤, 이것을 내가 만든 CPU 명령어로 번역할 수 있는 컴파일러가 필요하다. 그래서 CPU를 만들면 반드시 컴파일러도 같이 만들어줘야 한다. 이 컴파일러 프로그램을 따로 만들어서 사람들에게 나눠줄 수도 있지만, 그냥 다들 컴파일에는 gcc를 쓰고 있으니, 내 CPU에 대한 컴파일 기능을 gcc에 추가한다.

gcc는 모든 리눅스에서 공통으로 쓰는 컴파일러 모음을 뜻한다. 더 멀리는 원래 GNU프로젝트를 시작하기 위해 만들었다는데, 알게 뭐냐. 하여튼 컴파일러 = gcc이다. gcc는 리눅스 운영체제위에서 실행된다는 가정아래 x86, x86-64, armv6, v7, v8 모두를 커버할 수 있다. 그 외에도 듣도 보도 못한 CPU까지 커버한다. 앞서 설명했듯이 모든 CPU 제조사가 gcc에 자기 CPU를 추가하기 때문이다. 아래 링크를 참고하자. 


그래서 결론 : 컴파일러에는 gcc만 있는 것은 아니지만 그런데 gcc면 왠만큼 다 해결이 된다는 것이다.

크로스 컴파일이란 빌드 머신과 타깃 머신이 다른 경우를 말한다. 빌드 머신이란 현재 컴파일이 수행되고 있는 컴퓨터이다. 타깃 머신이란 컴파일 결과물이 실행될 컴퓨터이다. 안드로이드의 경우가 가장 대표적이다. 안드로이드 폰에서 돌아가는 프로그램을 안드로이드에서 개발하는 경우는 거의 없다. 휴대폰에서 엄지손가락으로 코딩하고 그걸 빌드하는 사람이 있겠는가? 보통은 리눅스나 윈도우에서 개발을 해서 apk까지 빌드하고, 이 파일을 안드로이드에 보낸다. apk가 실행되는 타깃머신은 안드로이드인데, apk를 만들어낸 개발 머신은 윈도우이다. 이런 경우를 가리켜 크로스 컴파일이라고 한다. 라즈베리 파이나 그보다 작은 소형 임베디드 보드는 거기서 직접 컴파일 하고 디버깅하고 어쩌구 하기가 매우 불편하니까 대개 크로스컴파일을 하게 된다.

그럼 자바, C#, 파이썬은 어떻게 동작하는가? 이들 언어는 동작하는 방식이 C와 꽤 다르다. 너무 산으로 가면 안 되니 생략.

g++은 gcc의 일부분으로서 C++언어의 컴파일러이다. 요즘 나오는 우분투에는 기본으로 깔려 있다. 여기서 재미있는 사실은 gcc는 C언어의 컴파일러인데 이건 C++로 만들었다. 마인크래프트에 보면 손으로 나무 깎아서 나무 곡괭이 만들고 그걸로 돌캐서 돌곡괭이 만들고 그걸로 철 캐서 철도끼 만들고 철도끼로 나무 캐고 그렇게 하듯이 컴퓨터 언어의 세계도 비슷하다. 포트란으로 C컴파일러 만들고 C컴파일러로 C++컴파일러 만들고 그걸로 다시 C컴파일러 만들고 그걸로 파이썬 만들고 파이썬으로 파이썬 컴파일러 만들고 ...

만약 gcc가 안 깔려있으면 깔아줘야 한다. 우분투에서는 C 언어 프로그래밍 개발에 필요한 것들을 패키지로 묶어서 배포한다.


$sudo apt install build-essential

요렇게 입력하면 gcc랑 make랑 cmake 등등 필요한 것들이 설치될 것이다.

3. 간단한 빌드

드디어 잡설을 끝내고!

본격적으로 개발에 들어가보자.

대충 폴더 하나를 만들고, 그 속에 main.cpp 파일을 작성한다.

<main.cpp>
#include <iostream>
int main()
{
    std::cout << "U**** F***** UBUNTU!\n";
    return 0;
}

다음과 같이 실행해본다.


g++ -c main.cpp #main.o 파일 생성

이렇게 하면 main.o 파일이 자동으로 생성된다. main.o는 앞서 말한 오브젝트 파일 : 바이너리다. 바이너리 파일은 바로 실행할 수 없다. 메인 함수가 물론 포함되어 있지만 그래도 곧바로 실행할 수는 없고, 리눅스가 실행할 수 있는 형식의 파일로 만들어야 한다.


g++ -o test main.o #main 파일 생성
./test #실행

main.o 파일이 바이너리 파일이고, 확장자가 없는 test 파일이 실행파일이다.

하나의 c/cpp 파일은 하나의 바이너리를 생성한다. 그런데 만약 파일이 여러개라면 어떻게 될까? 아래와 같이 파일을 만들어보자.

<my.h>
int myfunc(int val);

<my.cpp>
#include "my.h"
int myfunc(int val)
{
    return val + 1;
}

<main.cpp>
#include <iostream>
#include "my.h"
int main()
{
    std::cout << "calling up myfunc : " << myfunc(3) << std::endl;
    return 0;
}

이제 다음과 같이 명령어를 입력한다.


g++ -c main.cpp

컴파일러는 main.cpp파일을 들여다보고 my.h 파일을 현재 디렉토리에서 찾아낸다. my.h에는 myfunc에 대한 정의가 있으므로 컴파일에는 문제가 없다. main.o 파일이 생성된다.


g++ -c my.cpp

myfunc의 구현이 컴파일되어 my.o 에 담긴다. 이제 main.o와 my.o를 묶어서 test라는 실행 프로그램을 만들면 된다.


g++ -o test main.o my.o

이 실행의 결과로 test파일이 생성된다. 오브젝트 파일이 더 많을 경우에는 줄줄이 다 갖다 붙이면 된다.

여러 개의 바이너리 파일 중 main함수는 단 한개만 있어야 한다. 두 개 있거나 없으면 실행파일이 안 만들어진다.


이 쯤 되서 헤더파일이 무엇인지, 컴파일과 링크가 무엇인지 다시 한 번 생각해보자. 헤더 파일은 함수의 스펙을 적어놓는 것이다. myfunc 함수를 이용해야겠는데, 그 함수가 입력값이 어떤지 출력은 어떤 타입인지 알 수가 없다. 그래서 이 함수는 이렇소이다~ 하고 소개해놓은 것이 헤더파일이다. main.c에서는 헤더 파일의 정보만 보고 프로그램이 잘 돌아갈 지 생각해본다. 헤더에 의하면 입력은 int 한 개이고 출력도 int이다. 문법적으로 문제가 없으면 일단 OK, main은 컴파일이 가능하다.

컴퓨터 조립을 할 때, 부품을 하나의 공장에서 생산하지 않는다. CPU는 인텔에서 만들고 메인보드는 MSI에서 만들 때, 서로 통일된 스펙과 인터페이스만 맞춰놓고 각자 만든다. 나중에 컴퓨터 조립을 할 때, 각자 만든 부품을 연결해서 완성시킨다. 컴파일 과정도 마찬가지인데, 두 개의 바이너리가 각자 빌드된다(컴파일). 서로의 인터페이스에 대한 정보는 헤더파일을 참고한다. 나중에 링커를 통해서 그 둘을 연결(링크)하면 비로서 하나의 프로그램이 된다.


4. 라이브러리 참조


우리가 앞서서 #include <iostream> 이라고 작성했을 때, 무슨 일이 일어났는지 다시 생각해보자. iostream은 헤더 파일 이름이다. (확장자가 아예 없다.) 이 파일은 내 시스템 어딘가에 위치해 있는데, 보통은 /usr/include/c++/4.xxx/ 디렉토리에 들어있다. 본래 어떤 헤더 파일이든지 include 하려면 그 헤더파일이 들어있는 경로를 컴파일러에 알려줘야 한다. 하지만 왠만한 프로그램에서 다 쓰는 이런 스탠다드 라이브러리는 미리 디폴드 경로가 등록되어있다. 이것을 확인하고 싶으면 다음과 같이 명령어를 입력해본다.


g++ -v

혹은


export CPLUS_INCLUDE_PATH

이제 iostram파일을 포함하여 컴파일 하는 데는 문제가 없다. 하지만 링크 과정에서 iostream에 있는 함수와 클래스들이 어디에 어떤 파일로 로 구현되어있는지를 알려줘야 한다. 이것도 디폴트로 등록되어 있다.
우선 라이브러리 파일(바이너리)의 이름은 libstdc++.so.6 이고 파일의 위치는 대체로 /usr/lib 이다. 이 디렉토리는 환경변수 $LD_LIBRARY_PATH에 등록되어 있다.

지금까지 봐왔던 .o 파일이 아니라 .so 인 이유는 동적 라이브러리이기 때문이다. 동적 라이브러리는 윈도우로 치면 dll파일이다. 정적 라이브러리는 링크할 때 직접 그 내용이 실행 파일 안에 복사된다. 그래서 바이너리 파일 크기만큼 실행 파일의 크기가 늘어난다. 반면에 동적 라이브러리는 프로그램에 포함되지 않는다. 대신 프로그램 실행할 때 위치만 알려주면 된다. 그래서 실행파일의 크기가 늘어나지는 않지만, 실행파일 단독으로 프로그램 실행이 안 되고, 꼭 동적 라이브러리를 옆에 붙여줘야 한다. 여러 프로그램에서 동시에 쓰는 바이너리 파일이 있다면 동적 라이브러리가 되는게 유리하다.

만약에 다른 라이브러리를 추가하고 싶으면 다음 세 가지를 컴파일러에 알려줘야 한다.

*관련 헤더파일의 검색 경로
*관련 라이브러리 검색 경로
*관련 라이브러리 파일 이름

헤더파일의 경로에는 -I(대문자 아이) 옵션,  라이브러리 검색 경로에는 -L옵션, 라이브러리 파일 이름에는 -l(소문자 엘) 옵션을 준다. 누구야 첨에 이거 만든 사람 왤케 헷갈리게 I랑 l이랑 섞어놨어..

myfunc을 라이브러리화 해서 추가한다면 다음과 같이 명령어를 입력한다.

#my를 컴파일하여 my.o를 생성한다.
g++ -c my.cpp


#my.o를 묶어서 static library로 만든다. (libmy.a 생성)
ar rcs libmy.a my.o

여기서 ar은 오브젝트 파일을 정적 라이브러리로 만들어주는 툴이다. 오브젝트 파일과 정적 라이브러리는 사실상 차이가 없으며 형식상 약간 다를 뿐이다. 정적 라이브러리란 오브젝트 파일들의 집합이라고 표현할 수도 있으므로 zip으로 묶어주는 것과 비슷한 개념으로 생각해도 좋다.
또한 여기서 라이브러리 이름에 lib라고 붙인 것은 gcc에서 관습적으로 쓰는 표현인데 꼭 지켜야 한다. 해당 파일이 라이브러리임을 나타낸다. 확장자가 a라는 것만 봐도 라이브러리임이 명확한데 굳이 파일 이름에 prefix까지 붙이는 건지 나로서는 이해할 수가 없다.

이제 다음과 같이 입력한다.

#실행파일 'test' 생성
g++ -o test main.cpp -lmy -L./

여기서 -l, -L옵션과 뒤에 나오는 인자 사이에 공백을 두지 않음에 유의한다. libmy.a 파일에서 앞의 lib와 확장자 .a는 빼야 한다. 기호 "./"는 현재 경로라는 뜻으로서 libmy.a 파일이 들어있는 경로를 밝혀줘야 한다. l은 숫자 1이 아니라 소문자 알파벳 k다음에 나오는 l 이다.

동적라이브러리 만드는 과정은 더 쉽다. 그건 알아서 찾아보자.. ㅎㅎ

라이브러리 한 두개 정도는 이런 식으로 추가가 가능하지만 일반적인 프로젝트는 수십개의 오브젝트파일과 수십개의 라이브러리를 묶어서 컴파일하기 마련이다. 이러한 컴파일 옵션을 간소화하기 위해 makefile이 생겨났고, makefile도 작성하기 힘들어서 CMake와 같은 빌드 툴이 개발됐다. 다음 화에서 차차 살펴볼 것이다.


우분투에서 C++ 개발하기 (2) - Make

우분투에서 C++ 개발하기 (3) - CMake

댓글 7개:

  1. 좋은 글 감사합니다! 덕분에 잘 배웠어요!

    답글삭제
  2. 감사합니다. 정말 많이 도움되었어요

    답글삭제
  3. 친절하게 알려주셔서 감사합니다 잘 배웠어요 ㅎ

    답글삭제
    답글
    1. 부족한 글에 좋은 댓글 달아주신 분들 감사합니다.

      삭제

Powered by Blogger.