이 시리즈의 원 저자인 Tom Ryder의 허락을 받고 올리는 번역글입니다. IDE가 할 수 있는 기능을 UNIX 계열의 shell 안에서도 원활하게 할 수 있는 비결을 초보자도 알기 쉽게 잘 설명한 글일 뿐만 아니라 UNIX 자체의 철학이나 기본 사용법을 따라잡기에도 굉장히 좋은 글이라 생각되어 우리말로 옮기고자 합니다. 프로그래밍 용어는 웬만하면 원래 영단어로 쓰겠습니다. 언제든지 더 좋은 표현에 대한 의견은 감사합니다.



UNIX as IDE: Compiling

2012년 2월 12일 Tom Ryder가 작성


UNIX 플랫폼에서 사용할 수 있는 compiling 도구와 interpreting 도구는 여러 가지가 있고 각 도구마다 사용하는 방법이 다르지만, 개념적으로는 많은 부분에서 같다고도 할 수 있다. 여기서는 GCC(GNU Compiler Collection)을 이용해서 C 코드를 compile하는 방법을 소개하고, 간단하게 Perl 코드를 interpret하는 예제를 다뤄본다.



GCC

Compiler 모음인 GCC는 GPL 라이센스의 매우 성숙된 도구로, 아마도 C 또는 C++ 프로그램을 작업하는데 가장 널리 알려진 도구일 것이다. GNU/Linux나 BSD와 같은 UNIX-like 시스템에서는 언제나 무료로 접할 수 있기 때문에 Clang과 같이 LLVM 인프라에서 사용할 수 있는 최신 컴파일러가 있음에도 불구하고 GCC는 항상 대중적인 도구로 사용되고 있다.


GCC의 Frontend binary는 온전한 compiler 모음 자체로 빛나기 보다는 parsing, compiling, linking 등의 단계를 수행하는 일종의 드라이버처럼 사용하는 데 최고라고 생각할 수 있다. 이는 GCC가 C 코드를 실행 가능한 binary 파일로 compile하는 간단한 용도로 사용될 수도 있지만, compiling, linking 등의 여러 단계에 걸쳐서 세부적인 검사를 할 수도 있다는 뜻이다.


아마도 C 프로젝트를 만드는 데 있어서 make 파일은 필수적이겠지만, 여기서는 make 파일을 사용하는 법을 다루지는 않겠다. 추후 build 자동화 도구를 다루는 단계에서 정리하도록 하겠다.



Compiling and assembling object code

C 코드는 다음과 같이 object 코드로 compile할 수 있다.

$ gcc -c example.c -o example.o


C 코드에 문제가 없다면 위 명령은 example.o라는 이름으로 unlinked binary object 파일을 현재 위치에 생성해줄 것이다. C 코드에 문제가 있다면 compile이 안 되는 이유를 말해줄 것이다. objdump 도구를 이용해서 assembler의 내용을 조사할 수 있다.

$ objdump -D example.o


objdump 대신 gcc-S 인자를 통해서 example.o에 대응하는 assembly 코드를 직접 출력할 수도 있다.

$ gcc -c -S example.c -o example.s


이렇게 생성되는 assembly 출력물은 아래 예시처럼 소스 코드와 함께 할 때 더욱 도움이 된다.

$ gcc -c -g -Wa,-a,-ad example.c > example.lst



Preprocessor

cpp(C Preprecessor)는 주로 header 파일과 macro 정의를 포함시키는데 널리 쓰인다. 이 부분은 GCC compilation의 일부로 들어가 있지만, cpp를 직접 실행시켜서 cpp 수행 결과를 생성할 수 있다.

$ cpp example.c


위 명령을 통해서 cpp 작업이 끝나서 compile할 수 있는 완성된 코드를 출력되는 것을 확인할 수 있다.



Linking objects

여러 object 파일은 다음 명령을 통해서 binary 파일로 알맞게 연결될 수 있다.

$ gcc example.o -o example


위 GCC 명령은 GNU linker인 ld가 받을 수 있는 기본적인 명령을 만들어서 전달해주는 일을 할 뿐이다. 그 결과로 example이라는 binary 실행 파일이 생성됨을 볼 수 있다.



Compiling, assembling, and linking

위에서 여러 단계를 거쳐서 만들어진 example 실행 파일은 아래 한 명령으로 끝날 수도 있다.

$ gcc example.c -o example


위 명령은 훨씬 간단해 보이기도 하지만, object 파일을 개별적으로 compile하는 것은 불필요한 코드를 다시 compile하지 않아도 되기 때문에 실제로 더 좋은 성능을 보이기도 한다. 이에 대해서는 추후에 다시 설명하겠다.



Including and linking

C 코드와 헤더 파일은 -I 인자를 통해서 compile 단계에서 직접 포함시킬 수 있다.

$ gcc -I/usr/include/somelib.h example.c -o example


이와 비슷한 방법으로 -l 인자를 통해 /lib이나 /usr/lib에서 이미 compile 된 시스템 라이브러리를 동적으로 link시킬 수 있다. 아래 예제는 ncurses 라이브러리를 연결해서 compile한다.

$ gcc -lncurses example.c -o example


Compile 단계에서 여러 인자를 연결하고 싶다면 환경 변수에 저장해서 사용하는 것도 괜찮은 방법이다.

$ export CFLAGS=-I/usr/include/somelib.h
$ export CLIBS=-lncurses
$ gcc $CFLAGS $CLIBS example.c -o example


위와 같은 환경 변수를 이용한 인자 설정은 매우 흔한 방법으로, 실제로 Makefile의 작업을 추상화하는 설계에서 사용된다.



Compilation 계획

gcc가 여타 호출과 함께 어떤 일을 하는지 조금 더 상세하게 알기 위해서, -v를 통해서 standard error stream에다가 compilation 계획을 어떻게 세웠는지 출력할 수 있다.

$ gcc -v -c example.c -o example.o


실제로 object 파일이나 linked binary를 생성하고 싶지 않다면 -v 대신 -###를 사용하는 것이 더 깔끔할 때도 있다.

$ gcc -### -c example.c -o example.o


Compilation 계획을 보는 것은 gcc가 사용자를 위해 (사용자가 몰라도 되도록) 어떤 단계를 추상화시키는지 알아보는데 좋은 방법이다. 반대로 당신이 추상화하지 않길 원하는 단계를 gcc가 처리해버리는지 확인하는 데에도 쓰일 수 있다.



더 상세한 에러 검사

-Wall, -pedantic 옵션은 에러를 일으킬만 한 사항들을 경고해준다.

$ gcc -Wall -pedantic -c example.c -o example.o


이 옵션들은 Makefile에 환경 변수에 저장해도 좋고, Vim의 makeprg 정의에 포함시켜서 quickfix 창에서 그 결과를 볼 수 있도록 하는 것도 좋다. 그렇게 하면 에러 가능성에 대해 격렬하게 경고를 받으면서 읽기 쉽고, 호환성 좋고, 에러를 덜 일으킬 수 있는 코드를 작성하도록 도움 받을 수 있다.



Profiling compilation time

-time 옵션을 통해서 gcc가 밟는 단계마다 걸리는 시간을 출력할 수 있다.

$ gcc -time -c example.c -o example.o



최적화

포괄적인 최적화 옵션을 gcc에게 던져주면 compilation 시간에 조금 더 공을 들이게 해서 더 효율적인 object 파일과 linked library를 생성하게 할 수 있다. 필자는 -O2 옵션이 프로덕션 레벨의 코드에서 전반적으로 만족할 수 있는 타협점이 된다고 본다.

  • gcc -O1
  • gcc -O2
  • gcc -O3


Vim에서도 여타 Bash 명령처럼 수행할 수 있다.

:!gcc % -o example



Interpreters

UNIX 계열 시스템에서 해석형 코드를 접근하는 방식은 컴파일의 경우와 매우 다르다. 아래 예제부터는 Perl 언어를 사용하겠지만, Python이나 Ruby 등의 해석형 코드에도 대부분 동일하게 적용될 수 있는 원칙이다.



간단한 한 줄 Perl 코드

Perl 코드의 문자열을 Perl 해석기에 넣고 돌리는 방법은 아래 예시처럼 여러가지가 있다. "Hello, world."와 줄바꿈을 포함하는 하나의 문장을 출력하는 코드를 예제로 삼겠다. 첫 번째 명령은 Perl 코드를 돌리는 아마도 가장 간단하고 정석적인 방법이 될 것이고, 두 번째 방법은 heredoc 문자열을 이용하는 방법, 세 번째는 클래식한 UNIX shell pipe를 사용하는 방법이다.

$ perl -e 'print "Hello world.\n";'
$ perl <<<'print "Hello world.\n";'
$ echo 'print "Hello world.\n";' | perl


물론 코드를 파일에 저장하는 당연한 방식을 따른다면 직접 실행시킬 수 있겠다.

$ perl hello.pl


위에 소개한 모든 방법에서 -c를 통해 실제로 코드를 실행시키지 않고 syntax 검사를 할 수 있다.

$ perl -c hello.pl


스크립트를 논리적인 binary 파일로 사용하고 싶다면 -- 스크립트가 어떤 해석기를 필요로 하는지 사용자가 알 필요 없이 그 파일을 호출하는 것 만으로 실행시키고 싶다면 -- 스크립트의 첫 줄에 마법과 같은 "shebang"을 추가해서 이 파일이 어떤 해석기를 필요로 하는지 표시해 놓을 수 있다.

#!/usr/bin/env perl
print "Hello, world.\n";


이 스크립트는 chmod를 통해서 실행 가능한 모드로 변환시켜줘야 한다. 또한 확장자를 없애서 논리적 binary 파일임을 알려주는 것도 좋은 습관이다.

$ mv hello{.pl,}
$ chmod +x hello


이제는 hello를 마치 compile된 binary인 것처럼 실행할 수 있다.

$ ./hello


이런 논리적 binary 스크립트는 굉장히 직관적으로 명쾌하게 동작하기 때문에, 현대 GNU/Linux 시스템의 기본 도구의 많은 부분이 실제로 Perl 또는 Python 스크립트로 만들어져 있기도 하다.


다음 글에서는 make를 이용해서 프로젝트 빌딩을 어떻게 정의하고 자동화하는지 정리하겠다. 더욱이 IDE가 제공하는 기능에 맞춰서 make가 어떻게 같은 일을 할 수 있는지 알아보고, make의 철학을 승계한 Ruby의 rake도 들여다 보겠다.



UNIX as IDE




'GNU-Linux' 카테고리의 다른 글

UNIX as IDE: 6. Debugging  (0) 2017.11.08
UNIX as IDE: 5. Building  (0) 2017.11.07
UNIX as IDE: 3. Editing  (0) 2017.11.06
UNIX as IDE: 2. Files  (0) 2017.11.06
UNIX as IDE: 1. Introduction  (0) 2017.11.06