GCC

GCC 는 GNU Compiler Collection 의 약어로써, GNU 단체에서 지원하는 오픈소스 컴파일러 컬렉션이다.

GCC 로 C++ 을 컴파일 하려면 다음의 패키지를 설치해주자.

$ sudo yum install gcc gcc-c++ -y

이후에는 g++ 명령어를 이용하면 GCC 컴파일러에 접근하여 C++ 을 컴파일 할 수 있다.

CentOS 7.4 에선 기본적으로 gcc/g++ 버전 4.8.5 가 설치된다.

빌드

아무 옵션 없이 g++ 을 이용하여 C++ 코드를 빌드하면 자동으로 실행파일까지 만들어준다.

$ g++ main.cpp # a.out 이라는 실행 파일이 두둥-

근데 이런 식으로 실제 프로젝트에서는 절-대 사용할 수 없다. 분명히 무언가 옵션이 들어가기 마련! 왜 이렇게 간단하게 아무 옵션 없이 사용하면 안되는가? 왜냐하면 실제 프로젝트에서는 우리가 목적하는 프로그램이 동작하기 위한 “모든” 코드를 작성하지 않기 때문이다. 대개의 경우 우리가 작성하는 코드는 전체적인 프로그램 중에서 굉장히 일부분에 불과하다. 따라서 그들을 사용해서 잘(깔끔하고 문제 없이 동작하도록) 빌드하는 것은 꽤나 쉽지 않은 일이다. 컴파일은 정말 어려운 일이다…

Go 의 경우 순수하게 Go 언어로만 코드를 작성하면 아무런 문제 없이 손쉽게 빌드가 가능하다. 이건… 정말로, 정말로 아름다운 일이다… ㅠㅠ

빌드 과정은 크게 ‘컴파일’ 과 ‘링크’ 두 단계로 나뉘며, 더욱 자세하게는 ‘전처리’ –> ‘컴파일’ –> ‘어셈블’ –> ‘링크’ 네 단계로 나뉜다.

스테이지 별 GCC 컴파일

GNOME 개발자 문서에서 참조한 위의 그림에서 볼 수 있듯, main.c 라는 파일을 컴파일을 한 결과물로 main.s 파일이 나온다. 이 파일은 어셈블리어 파일로써, 아직까지는 사람이 알아볼 수 있는 형태의 코드가 나온다.

가령 다음의 친숙한 Hello, World! C 프로그램을 컴파일 한 어셈블리어는 다음과 같다.

/* hello.c */
#include <stdio.h>

int main()
{
  printf("Hello, World!");
  return 0;
}
$ gcc -S hello.c
$ vi hello.s
        .file   "hello.c"
        .section        .rodata
.LC0:
        .string "Hello, World!"
        .text
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        movl    $.LC0, %edi
        movl    $0, %eax
        call    printf
        movl    $0, %eax
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-16)"
        .section        .note.GNU-stack,"",@progbits

이 역시 깊이 들어가면 재미있는 내용이지만 토픽을 벗어나므로 다음 번에 다루도록 하고, 대략 이렇게 눈으로 알아볼 수 있는 형태의 어셈블리어가 나온다는 사실을 눈으로 보고 지나가도록 하자.

이렇게 컴파일 된 어셈블리어가 실질적인 0과 1로 변환되도록 해주는 일을 하는 프로그램이 어셈블러 이며, 어셈블러의 결과물은 목적 파일, 즉 확장자가 .o 인 파일이다.

이 목적 파일은 세 가지 중 하나로 사용될 수 있다.

  • 정적 라이브러리
  • 공유 라이브러리
  • 실행 파일

공유 라이브러리

라이브러리와 관련한 내용은 꽤나 방대하기 때문에 다음 글에서 다시 다루도록 하고, 이번 챕터에서는 간단하게 공유 라이브러리에 대해서만 살펴보자.

내 경우는 이 라이브러리를 이해하고 사용할 수 있게 되기까지 꽤나 많은 시간이 걸렸다.

라이브러리의 정의 자체는 어렵지 않다. 뭐 좀만 검색해도 나오는 내용이니까, Linux 에서 정적 라이브러리는 확장자로 .a 를 사용하고 실행 파일에 포함되고, 공유 라이브러리는 확장자로 .so 를 사용하고 실행 파일에 포함되지 않고 외부와 참조해서 사용한다.

근데… 대체 그래서 어쩌라는건지?

대충 정의만 이해하고 사용법만 알아도 프로젝트를 하는 데에는 무리가 없지만, 난 좀 더 알고 싶었다. 그래서 조금 더 공부를 했다.

ldd

Linux 프로그램 중에 ldd 라는 프로그램이 있다. 공유 라이브러리들의 의존성을 보여주는 프로그램이다.

간단하게 C 프로그램을 만들고 ldd 로 확인해보자. 아까의 hello.c 를 이용하자.

$ gcc hello.c -o hello
$ ldd hello
	linux-vdso.so.1 =>  (0x00007fff2377c000)
	libc.so.6 => /lib64/libc.so.6 (0x00007feebcc70000)
	/lib64/ld-linux-x86-64.so.2 (0x00007feebd033000)

오! 무언가 참조하는 다른 라이브러리들이 있다.

  • linux-vdso.so.1: Linux Kernel 에 의해 모든 프로세스 내에 삽입되는 라이브러리. 참조. 이 라이브러리는 다른 라이브러리들과는 다르게 화살표가 지칭하는 대상 없이 스스로 존재함을 눈여겨보자. 다른 라이브러리들은 지칭하는 대상이 존재하지 않으면 Not Found 라고 나온다!
  • libc.so.6 => /lib64/libc.so.6: GNU C Library (glibc), C API 를 사용하는 경우 반드시 필요하다. CentOS 7.4 기준으로, glibc 버전 2.17 이 내장되어있다. 저 화살표는, libc.so.6 파일을 /lib64/libc.so.6 에서 찾겠다는 의미이다. ls 명령어로 /lib64/libc.so.6 을 조회해보면 /lib64/libc-2.17.so 파일의 심볼릭 링크임을 알 수 있다.
  • /lib64/ld-linux-x86-64.so.2: ld (Dynamic Linker) 라이브러리. 공유 라이브러리를 로드하기 위해 필요하다.

C++ 프로그램을 만들고 ldd 로 확인해보자. iostream 을 이용해서 hello.c 와 동일하게 Hello, World! 를 찍는 간단한 프로그램이다.

// hello.cpp
#include <iostream>

int main()
{
  std::cout << "Hello, World!";
  return 0;
}
$ g++ hello.cpp -o hellocpp
$ ldd hellocpp
	linux-vdso.so.1 =>  (0x00007fff9b9b2000)
	libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007f3a3b4ba000)
	libm.so.6 => /lib64/libm.so.6 (0x00007f3a3b1b8000)
	libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f3a3afa2000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f3a3abdf000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f3a3b7c2000)

C++ 프로그램은 C 프로그램에 비해 더 많은 공유 라이브러리를 참조하고 있음을 알 수 있다. C 프로그램에서는 보지 못한 라이브러리가 3개가 있는데, 다음과 같다.

각자를 다시 ldd 해보면 다시금 glibc 를 참조하고 있음을 알 수 있다. 즉 서로 돌고 도는 관계인 것이다!

공유 라이브러리는 이렇게 서로 서로를 참조하며, 각 부분(예를 들면 glibc) 은 우리보다 똑똑한 여러 사람들에 의해 최적화 된 부분이기 때문에 믿고 쓸 수 있다.

프로그램 빌드하기

어떠한 소스 코드를 작성하면 결국 목적은 두 가지 중 하나일 것이다. 라이브러리로 사용될 코드이거나, 실행 프로그램으로 사용될 코드이거나. 어쨌든 위에서도 언급하였지만, 결국 ‘목적 파일’이 되어야 한다. 그리고 목적 파일을 만드려면 소스코드를 컴파일 하고, 어셈블까지 진행해야한다.

GCC 에서는 어셈블까지 진행해서 목적 파일을 생성해 줄 수 있도록 파라미터로 -c 플래그를 받는다. 이번에는 다른 예시를 보자.

// rand.cpp
#include <cstdlib>
#include <ctime>

int getRand()
{
  srand(time(NULL));
  return rand()%25+5;
}
// main.cpp
#include <iostream>

int getRand();
int main()
{
  std::cout << getRand();
}
$ g++ -c rand.cpp
$ g++ rand.o main.cpp -o random_no
$ ldd random_no
	linux-vdso.so.1 =>  (0x00007ffce17e8000)
	libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007fa2541ca000)
	libm.so.6 => /lib64/libm.so.6 (0x00007fa253ec8000)
	libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fa253cb2000)
	libc.so.6 => /lib64/libc.so.6 (0x00007fa2538ef000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fa2544d2000)
$ ./random_no
16

하나 이상의 소스 코드 파일이 존재하는 경우, 일부는 컴파일 한 후 목적 파일로 만든 후, 나중에 같이 컴파일 하여 빌드할 수도 있다. GCC 가 링크 과정까지 같이 신경써준다.

한편 위의 코드를 간단한 공유 라이브러리로 만들어보자.

$ g++ -c rand.cpp -fPIC
$ g++ -shared rand.o -o librand.so
$ ldd librand.so
	linux-vdso.so.1 =>  (0x00007ffed299e000)
	libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007f3d6a19f000)
	libm.so.6 => /lib64/libm.so.6 (0x00007f3d69e9d000)
	libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f3d69c87000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f3d698c4000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f3d6a6a9000)
$ g++ main.cpp -L. -lrand -o random_no
$ ls
main.cpp librand.so rand.cpp rand.o random_no
$ ldd random_no
	linux-vdso.so.1 =>  (0x00007ffd8cb83000)
	librand.so => not found
	libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007f2c02049000)
	libm.so.6 => /lib64/libm.so.6 (0x00007f2c01d47000)
	libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f2c01b31000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f2c0176e000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f2c02351000)
$ ./random_no
./random_no: error while loading shared libraries: librand.so: cannot open shared object file: No such file or directory

오 이런… librand.so 파일은 모든 라이브러리가 제대로 링크 되어 있는데, random_no 파일은 librand.so 를 찾을 수 없다고 한다. 분명 같은 폴더에 있는데 말이다! 왜 이럴까?


다음 글에서 계속.