레이블이 리눅스인 게시물을 표시합니다. 모든 게시물 표시
레이블이 리눅스인 게시물을 표시합니다. 모든 게시물 표시

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

앞선 시리즈 :
우분투에서 C++ 개발하기(1) - https://ladofa.blogspot.com/2018/07/c-1.html
우분투에서 C++ 개발하기(2) - https://ladofa.blogspot.com/2020/08/c-2.html
여기서 사용하는 예제는 두 번째 시리즈에서 사용한 예제를 그대로 사용한다. 예제를 몰라도 읽는데 문제는 없다.

-------------------------

1편과 2편을 잘 정독했던 분들이라도 여기서부터는 예제가 복잡해서 이해하기 쉽지 않다. 쉬운 예제로 설명하고 싶은데.. 불가능하다. cmake가 원래 복잡한 컴파일 과정을 간단하게 하는 거라서... 여기서부터는 구경만 해도 좋고, 어쩌다 CMakeLists.txt 를 분석할 일이 있으면 그 때 한 번 더 봐주시길...

-------------------------


리눅스에서 C++ 코드를 컴파일하려면 최종적으로는 gcc나 다른 컴파일러를 이용해서 컴파일 명령을 내려줘야 한다. 그러나 파일 개수가 많고 의존성이 방대한 큰 프로젝트에서 일일이 gcc 명령을 내릴 수는 없다. 이를 보완하기 위해 Makefile을 작성해서 컴파일 명령어를 미리 입력해두면 나중에는 별 다른 수고 없이 간단한 명령으로 컴파일이 가능하다. 그런데 Makefile을 작성할 때도 결국은 gcc를 잘 알고 있어야 하고, 각종 옵션과 명령어를 직접 입력하는 것도 힘든 일이다.

CMake 기본


CMake는 이런 작업들조차 단순화하여 간단한 명령어를 통해 입력 가능하도록 돕는다. CMake는 사용자가 작성한 스크립트를 해석해서 Makefile을 만든다. 결국 cmake 를 실행한 뒤 make를 다시 실행해야 한다.

CMake도 프로그램이기 때문에 안 깔려 있으면 직접 깔아야 하는데 우분투에서는 다음과 같이 패키지를 통한 설치를 지원한다.

$sudo apt install cmake


여기서 CMake의  apt 패키지 이름이 cmake이고, 실행 프로그램의 이름도 cmake이다. 터미널에 그냥 cmake라고 입력해보자.

Usage

  cmake [options] <path-to-source>
  cmake [options] <path-to-existing-build>
  cmake [options] -S <path-to-source> -B <path-to-build>

Specify a source directory to (re-)generate a build system for it in the
current working directory.  Specify an existing build directory to
re-generate its build system.

Run 'cmake --help' for more information.

위와 같이 뜨면 성공이다. 컴퓨터에 깔려 있는 cmake 프로그램을 실행한 것이다. 버전을 확인하고 싶으면 다음과 같이 입력한다.

$cmake --version

cmake가 인식하는 스크립트 파일의 이름은 CMakeLists.txt 로 정해져있다. make가 기존에 존재하고 있는 Makefile 을 자동으로 인식하는 것처럼 cmake 역시 CMakeLists.txt 라는 이름의 파일을 찾는다.

이전 시리즈에서 작업했던 것과 같이 my.cpp, my.h, main.cpp 파일이 있다고 가정할 때, 이를 빌드하기 위한 CMakeLists.txt 스크립트는 다음과 같다.


cmake_minimum_required (VERSION 3.10)
project (mytest)
add_executable (mytest my.cpp main.cpp)

맨 첫째 줄은 cmake의 최소 버전을 밝히는 것이다. 해당 버전보다 아래인 cmake로 실행할 수  없게끔 막는다.
project()는 해당 스크립트의 프로젝트 이름을 밝힌다. mytest는 이 프로젝트의 이름이며 실행파일을 생성할 경우 실행파일의 이름이 될 것이다.
add_executable은 이 스크립트를 통해 최종적으로 실행파일을 만들고자 한다는 뜻이다. 실행파일 말고 정적 라이브러리를 만들고 싶을 때는 add_library라는 명령을 이용한다.

이 스크립트를 실행하려면 다음과 같이 입력하면 되는데...

$cmake .

make 는 무조건 실행한 경로에서 Makefile의 존재유무를 찾지만 cmake는 반드시 CMakeLists.txt의 경로를 지정해줘야 한다. cmake 다음에 나오는 점(.)이 바로 경로를 나타내는 것이다. 점 하나 찍으면 현재 경로를 말한다.

cmake는 Makefile을 생성하면서 기타 다양한 파일들도 생성하므로 그냥 현재 폴더에서 실행하면 이런저런 파일이 생겨 지저분해진다. 그래서 보통은 build 디렉토리를 만들어서 거기서 실행한다.

$mkdir build
$cd build
build$cmake ..

위와 같이 build 디렉토리를 만든 뒤, 해당 디렉토리 내에서 cmake를 실행한다. 이 때 cmake 뒤에는 점 두개 (..) 가 붙어 있다. 이는 상위 디렉토리를 나타낸다. CMakeLists.txt 파일이 build디렉토리에 있지 않고 그 상위 디렉토리에 있기 때문이다.

빌드에 성공하면 이런 저런 메타파일이 생성되고, 가장 중요한 Makefile이 만들어진다. 이 Makefile의 타겟은 기본적으로 all 과 clean을 가지고 있다. 타깃을 입력하지 않으면 all을 기본으로 한다. 아래와 같이 입력해보자.

build$make

그냥 make라고 하면 자동 생성된 Makefile을 실행시킨다. 여기까지는 거의 공식처럼 사용한다.



빌드에 성공하면 mytest 파일이 만들어져 있다. mytest를 실행해보자.

build$./mytest
3^2 == 9

이제 main.cpp파일을 수정할 것이다. 다음 프린트 구문을 추가한다.

printf("This code is modified.\n" );

다시 mytest를 빌드해서 실행해보자.

build$make
build$./mytest
3^2 == 9
This code is modified.

위와 같이 수정되서 나타난다. 뭔가 신기한 일이 이뤄진 것 같지만 사실 별 것도 아니다. 여기서 일단 CMake는 하는 일이 없다. make를 실행하면 수정된 파일을 감지해서 다시 빌드를 해준다. 그리고 그 결과가 나타난 것 뿐이다.

이제 your.h, your.cpp 파일을 추가할 것이다.

<your.h>
int your_func(int x);

<your.cpp>
#include "your.h"

int your_func(int x)
{
    return x + 10;
}


main.cpp에서도 your_func 을 사용하도록 수정한다.

<main.cpp>
#include <iostream>
#include "my.h"
#include "your.h"

int main(void)
{
    printf("%d^2 == %d\n", 3, my_func(3));
    printf("This code is modified.\n" );
    printf("%d + 10 == %d\n", 3, your_func(3));
}


마지막으로 CMakeLists.txt파일을 다음과 같이 수정한다.

cmake_minimum_required (VERSION 3.10)
project (mytest)
add_executable (mytest my.cpp main.cpp your.cpp)


add_executable에 your.cpp 를 추가한 것이다. 이제 아까와 마찬가지로 새로 빌드를 해본다.

build$make
build$./mytest
3^2 == 9
This code is modified.
3 + 10 == 13

수정된 결과가 반영되어 있다. cmake는 수정된 CMakeLists.txt 파일을 자동으로 반영하여 Makefile을 미리 바꿔놓았다. 그리고 나는 make명령어를 통해  바뀐 Makefile을 실행해서 결과를 확인했다. 

만약 CMakeLists.txt에 오류가 있으면 어떻게 될까?

cmake_minimum_required (VERSION 3.10)
project (mytest)
add_executable (mytest my.cpp main.cpp your.cpp nobody.cpp)

build$make

나는 make를 실행했는데, 실행해보면 CMakeLists.txt파일이 잘못되었다는 에러 메시지가 나온다. cmake는 건들지도 않았는데!

make는 어떻게 cmake를 인식하고 일반적인 빌드 오류가 아닌 cmake 오류를 출력하는 것일까? make가 cmake와 뭔가 연동되어 있는 것일까? Makefile 내부를 보면 cmake 에러가 없는지 확인하고 메시지를 출력하도록 되어 있다. cmake가 Makefile을 생성하면서 그 속에 cmake관련 내용을 스파이처럼 넣어놨다. 그래서 make만 실행해도 CMakeLists.txt의 오류를 검사하게 된다.


라이브러리 구조화

이제 my 라이브러리를 분리해서 컴파일해보자. 보통 프로그램 개발에서 핵심적인 기능을 담당하는 모듈은 기본 프로그램과 분리해서 따로 라이브러리 형식으로 개발하고, 본 프로그램은 해당 라이브러리를 사용하도록 만든다. 지금 예제에서는 mylib 라는 이름의 라이브러리를 개발하는 것으로 가정할 것이다.

일반적으로 라이브러리를 포함한 구조는 다음과 같이 되어 있다. 이 구조를 무조건 지킬 필요는 없지만 보통 이렇다..

  - 라이브러리 이름
    - src
      - files.cpp
    - include
      - files.h
    CMakeLists.txt
  - 또 다른 라이브러리
    ......
main.cpp
CMakeLists.txt

이렇게 글씨로 쓰면 구조가 안 보일까봐 VSCode 캡쳐 화면도 준비했다.



라이브러리마다 CMakeLists.txt 파일이 따로 있고 여기에는 src 디렉토리와 include 디렉토리가 있다. 빌드를 마치고 나서 src 폴더에 있는 cpp파일은 당연히 소스코드니까 공개하지 않지만 include 폴더에 있는 h, hpp 파일은 라이브러리를 사용하는 측에게 공개해야 한다. C의 라이브러리들은 헤더파일이 있어야 쓸 수 있다. 그런 이유를 포함해서 관리상의 편리함 등 여러 가지 이유로 src와 include는 분리해놓는 것이다.

혹시나 다른 사람이 만든 C언어 소스를 보게 되면 항상 include 디렉토리와 src 디렉토리가 따로 나뉜 걸 보게 된다. 또한 src를 컴파일한 결과는 bin 혹은 build 디렉토리에 모셔놓게 된다. 만약 소스코드가 아닌 빌드된 라이브러리를 다운받게 되면 include 속에 있는 헤더파일과 bin디렉토리 내부의 컴파일된 바이너라 파일만 보게 된다. 왜 그런지를 이해하려면 1편을 참고한다...


하여튼 mylilb 속에 있는 CMakeLists.txt의 내용은 다음고 같다.


cmake_minimum_required (VERSION 2.8)
project (mylib)
add_library (mylib src/my.cpp)
target_include_directories(mylib PUBLIC include)


아까의 CMakeLists.txt와 비교해서 다른 점은 add_executable 대신 add_library로 바뀌어 있다는 것이다. 실행파일을 만들지 않고 라이브러리를 생성한다는 뜻이다. 여기에는 사용되는 모든 cpp파일을 적어주어야 한다. 그리고 target_include_directories가 추가되었는데, 여기에 include 디렉토리를 알려줘야 한다.

다음과 같이 빌드해볼 수 있다.

mylib$mkdir build
mylib$cd build
mylib/build$cmake ..
mylib/build$make

빌드하고 나면 libmylib.a 파일이 생성되었다. 이것을 메인에서 이용하면 된다. 바깥쪽에 있는 CMakeLists.txt의 내용은 다음과 같다.

cmake_minimum_required (VERSION 2.8)
project (mytest)
add_executable (mytest main.cpp)
target_link_libraries(mytest PUBLIC mylib)
target_link_directories(mytest PUBLIC mylib/build)
target_include_directories(mytest PUBLIC mylib/include)

target_link_libraries에 라이브러리 이름을 추가한다. 라이브러리 이름이란 *.a에서 맨 앞에 lib를 제외한 것이다. gcc빌드할 때와 마찬기자로 라이브러리 파일은 맨 앞에 lib가 prefix로 붙는다. 파일 확장자가 앞쪽에 붙었다고 생각하면 된다.

target_link_directories 에는 라이브러리 파일의 경로를 추가한다. target_include_directorie 에는 헤더 파일의 경로를 추가하면 된다.

사실 이런 식으로 하는 것은 아예 동떨어진 다른 라이브러리를 가져오는 것과 동일하다. 그런데 현재 디렉토리/프로젝트 구조는 mylib 를 포함하고 있는데, 이 특징을 전혀 살리지 않은 것이다. 하위 디렉토리에 라이브러리가 들어있는 상황에서는 조금 더 간결하게 cmake명령을 만들 수 있다.

이제 add_subdirectory 를 이용해서 디렉토리를 구조화한 보람을 찾아보자.  mylib 디렉토리 내에 있는 CMakeLists.txt는 그대로 두고 메인 프로그램의 CMakeLists.txt를 다음과 같이 수정한다.

cmake_minimum_required (VERSION 2.8)
project (mytest)
add_subdirectory(mylib)
add_executable (mytest main.cpp)
target_link_libraries(mytest PUBLIC mylib)

add_subdirectory에 mylib를 추가했다. 이렇게 하면 mylib 디렉토리 속에 있는 CMakeLists.txt를 인식하고 가져온다. 라이브파일의 경로나 헤더 파일의 경로는 따로 추가할 필요가 없다. 다만 target_link libraries에 mylib를 추가하면 끝이다.

이렇게 하고 build 디렉토리를 만들어서 cmake를 해보면 mylib와 mytest가 같이 만들어지게 된다. 즉 여러 개의 CMakeLists.txt에 각각 명령어를 실행할 필요 없이 한 번에 연쇄적으로 실행된다. 실제로 build 내부에 mylib 디렉토리가 따로 생성되며 여기에 빌드된 라이브러리가 들어 있다.



외부 패키지 추가


마지막으로 외부 패키지를 찾아보도록 하자. 다른 사람이 빌드한 라이브러리를 추가할 때 위에서 소개한 바와 같이 include 디렉토리와 라이브러리를 일일이 추가하는 것은 상당히 번거롭다. 때문에 누군가 고수님께서 우리를 위해 라이브러리를 만드실 때는 CMake를 활용해서 자기 라이브러리를 잘 추가할 수 있도록 메타 정보를 같이 만들어둔다. 우리는 해당 메타 정보만 읽어들이면 라이브러리 추가에 필요한 각종 파라미터(include디렉토리, lib 디렉토리 등)를 자동으로 얻을 수 있다.

대표적인 라이브러리로 OpenCV가 있다. OpenCV를 본 프로젝트에 추가하려면 CMakeLists.txt에 다음과 같이 입력한다.


cmake_minimum_required (VERSION 2.8)
project (mytest)
find_package(OpenCV REQUIRED)
add_subdirectory(mylib)
add_executable (mytest main.cpp)
target_link_libraries(mytest PUBLIC mylib ${OpenCV_LIBS})
target_include_directories(mylib PUBLIC ${OpenCV_INCLUDE_DIRS})


find_package는 주어진 이름의 패키지(메타정보)를 찾아서 라이브러리의 목록과 헤더 파일의 디렉토리 목록을 변수에 저장한다. OpenCV 패키지는 어떻게 찾는가? 그 방법에 대한 링크가 있다.


대략 9가지 정도의 방법으로 패키지를 찾는다(...) 보통은 라이브러리 설치할 때 sudo make install 과 같은 명령어를 입력하면 그 속에 CMake 패키지 등록 과정이 포함되어 있다.

라이브러리를 찾는데 성공하면 OpenCV_LIBS와 OpenCV_INCLUDE_DIRS 가 변수로 제공된다. 이것을 타겟 빌드하는데 추가해주면 된다. 다른 라이브러리인 경우 OpenCV 자리에 다른 이름을 넣으면 된다.

만약 cmake가 없었다면? 우리는 OpenCV를 활용하기 위해 OpenCV 헤더 정보가 있는  include디렉토리를 직접 찾아서 입력해야 하고, 라이브러리 파일도 직접 찾아서 (아마 Makefile에) 추가해줘야 했을 것이다. 라이브러리 버전이 바뀌거나 업데이트되면? 또 달라진 경로와 파일을 일일이 수정해줘야 한다. 이 과정을 cmake가 알아서 해준다.

여기까지 와서 왜 라이브러리와 인클루드 디렉토리를 추가해야 하는가? 이런 의문이 들면 C컴파일 방법을 처음부터 다시 배워야 한다. 이글 맨 위의 링크에서 1탄으로 가시기 바란다.

참고로 CMake 스크립트에서는 순서가 중요하다. target_xxx 인 명령어는 add_executable이나 add_library 뒤에 와야 한다. 그 밖의 명령어는 add_executable/add_library 앞에 오면 된다. 제일 중요한 부분인데.... 마지막에 밝힌다.



  이상으로 우분투에서 C+ 개발하기 시리즈를 마친다.

--------

VSCode로 빌드하기... 는 그냥 연재 안 하는 것으로...  검색해보면 다른 글이 많으니 굳이 내가 수고할 필요가 없다. Remote SSH로 연결해서 컴파일하고, 디버그하고.. 그런 내용일 것이다.
여러분들이 라즈베이파이, 어디 서버 등 리눅스에서 돌릴 프로그램을 개발한다면 파이썬이든 C++ 이든 무조건 VSCode의 Remote SSH 기능을 활용하는 것을 추천한다.

--------

제 블로그에서 가장 인기있는 글이 바로 여기 시리즈라서 따로 인사를 남깁니다. 방문하신 모든 분들, 댓글 달아주신 분들 감사합니다.


Read More

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

우분투에서 C++ 개발하기 (1) : https://ladofa.blogspot.com/2018/07/c-1.html


 1탄을 만들어 놓고 몇 년이 흘렀는가 모르겠다. 2탄을 만들게 될 줄도 몰랐다.

하여튼 이어서 makefile에 대해서 알아본다.

C나 C++을 사용하다 보면 수도 없이 컴파일 하고 빌드하고 이런 일이 반복되는데 매번 라이브러리 경로와 관련된 라이브러리 파일과 기타 등등등을 입력하는 것도 번거롭고 새로 수정한 파일이 무엇인지 따라다니면서 컴파일하는 것도 힘들다. 그래서 리눅스에서는 Makefile이란 툴을 쓴다. 윈도우에서 Visual Studio로 개발하면 이런 거 필요없는데.

Makefile은 빌드에 필요한 스크립트를 텍스트 형식으로 저장한 파일이다. 이 파일은 반드시 이름이 Makefile 이어야 한다. 확장자 없이 이름만 Makefile 이면 된다. 무슨 파일 이름이 Makefile이냐. ...

Makefile 속에 있는 스크립트를 해석해서 실행하는 프로그램은 make이다. 프로그램 이름이 make 다. make를 실행하면 실행한 현재 경로에서 Makefile을 찾아내고 요걸 해석해서 빌드를 수행한다.


우선 다음과 같은 예제를 생각해보자.

<my.h>

int my_func(int x);


<my.cpp>

#include "my.h"

int my_func(int x)

{

    return x * x;

}


<main.cpp>

#include <iostream>

#include "my.h"

int main(void)

{

    printf("%d^2 == %d\n", 3, my_func(3));

}


뭐 이렇게 간단히 구성되어 있다고 하자. 요걸 컴파일 하려면 1탄에서 배운 대로 다음과 같이 입력해야 할 것이다.

$ g++ -c my.cpp

$ g++ -c main.cpp

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


매번 이렇게 하기 귀찮으니까 이제 Makefile을 이용할 차례이다. Makefile 은 타깃의 집합이다. 타깃은 다음과 같이 서술해야 한다.

[타깃이름]: [타깃에 필요한 파일들]

      [타깃 실행 코드]

여기서 타깃 이름은 그냥 아무 이름이 될 수도 있고, 파일명이 될 수도 있다. 실행 코드를 통해 결과 파일 하나가 확실히 나오는 경우에는 타깃 이름을 파일명으로 한다. 예를 들어

<Makefile>

my.o: my.h my.cpp

    g++ -c my.cpp

이와 같이 Makefile을 작성할 수 있다. 달랑 두 줄이다. 여기서 중요한 것은 두 번째 줄 앞에 있는 공간이 탭 하나이다. 지금 블로그를 작성할 때는 어쩔 수 없이 스페이스를 때려 넣었지만 실제로는 반드시 1탭이어야 한다.

이제 아래와 같이 실행한다.

$ make my.o

그 결과로 my.o 파일이 생성된다. 만약 이미 생성되었다면 up to date. 메시지가 뜰 것이다.


하나의 타깃은 다음 타깃의 재료가 될 수 있다. my.o 그리고 main.o 는 링커에서 실행 파일을 만드는 재료가 된다. 혹은 재료가 필요 없는 명령도 있다. 다음 예를 보자.

<Makefile>

my.o: my.h my.cpp

    g++ -c my.cpp


main.o: my.h main.cpp

    g++ -c main.cpp


test: my.o main.o

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


all: test


clear:

    rm -f my.o main.o test


여기서 my.o 와 main.o 는 test의 재료가 된다. 만약 make test를 했는데 my.o가 존재하지 않는다면 해당 타깃부터 만들고 난 뒤에 test를 만들게 된다. 한 번 더 make test를 실행하면 기존에 모든 것이 이미 있으므로 더 이상 작업을 진행하지 않는다. 파일을 수정하고 나면 수정된 파일을 재료로 하는 타깃만 새로 빌드될 것이다.

all 타깃은 재미있게도 실행 명령이 없고 타깃만 명시되어 있다. make all 을 실행하게 되면 그 재료가 되는 test를 만들 것이다. 만들고 나서 특별히 할 일은 없다.

clear는 반대로 타깃 파일이 없다. 보통은 타깃 파일이 존재해야 그 다음으로 명령문을 수행하게 될테지만 요구사항이 되는 타깃이 없으므로 그냥 무조건 명령을 수행하게 된다.

all, clear, install 이 세 가지 타깃은 모든 Makefile에서 관용적으로 쓰이는 것들이다. 모르는 설치파일이라도 우선 make install 부터 실행해보면 된다.


여기까지 이해했으면 이제 위키피디아에서 제공하는 샘플 Makefile을 살펴보자.

맨 윗줄에 있는 것들은 환경변수이다. 환경 변수의 이름 역시 암묵적으로 정해져 있다. 왤케 암묵적인게 많냐... $(OBJ)을 보면 타깃 여러 개를 한 번에 지정하고 있다. 그리고 이상한 기호들이 보이는데 $@는 타깃 이름, $^는 재료 이름, $<는 재료 중 맨 첫번째 항목을 의미한다. 

대충 이 정도면 원리는 이해한 셈이고, 그 밖에 엄청난 규칙들이 많다. 심지어는 요즘도 버전업이 되고 있다. 나머지는  http://doc.kldp.org/KoreanDoc/html/GNU-Make/GNU-Make-4.html 이런 곳에서 자세히 살펴보길 바란다.


요즘은 CMake나 다른 툴을 이용하고 Makefile은 거의 안 쓰기는 하지만 그래도 기본 원리 정도는 어렵지 않으므로 알고 있는 것이 좋다. 임베디드나 기타 작은 소프트웨어에서는 여전히 직접 작성해서 쓰기도 한다.


다음으로는 CMake를 사용한 컴파일 과정을 간단히 살펴볼 것이다.


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

우분투에서 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
Read More
Powered by Blogger.