이 시리즈의 원 저자인 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 |