제목 거창하죠? Inside the C Programming 이라니.. 이게 뭔 소리 랍니까? 그냥 단순한게.. "C 한번 까발려 보자... " 이 쯤으로 해석 하시면 될 것 같네요...

C 언어 강의야 어디서든지 볼 수 있고 들을 수 있으니, 많은 사람들이 다루지 않는 곳을 한번.. 다뤄 볼까 합니다.
대강 제가 주로 사용하는 환경인 linux 환경에서 gcc를 가지고 내용을 진행을 할 거구요. 뭐 어차피 다른 플랫폼이라도 상관 없는 내용이니까.. ^^; 그러려니 하고 읽어 보세요. 그리고 전 microsoft의 visual tool을 거의 사용해 본 적이 없어서, 그 쪽 환경으로 설명을 해 달라고 하시면.. -_-; 뭐라 할 이야기가 없네요.. 그리고, ms의 개발툴은 너무 비싸요 -_-;

처음듣는 용어나.. 이해가 안가는 부분이 있으시면.. 절 찾아와서.. 다시 설명해 달라고 하시면.. 가능한한 쉽게 이야기 해 드릴께요.. ( 참고로.. 저 뇌물 좋아라 합니다.. 흐흐..)

그럼 시작해 볼까요?

gcc를 사용해 보셨나요? 주로 리눅스 또는 unix환경에서,  make를 이용해 linux kernel을 비롯해 기존에 제공된 패키지를 컴파일해 보셨을 것으로 생각됩니다. 물론 윈도우즈에서도 mingw 또는, cygwin등을 이용하여 사용할 수도 있습니다.^^;

gcc는 한마디로 GNU에서 개발된 ANSI C 표준을 따르는 C 언어 컴파일러라고 말할 수 있습니다. gcc는 ANSI C 표준에 따르기는 하지만 ANSI C 표준에는 없는 여러 가지 확장 기능이 있습니다.
또한 gcc는 통합개발환경(IDE)을 가지고 있지 않은 command line 컴파일러입니다. Visual C++을 사용하시는 분들은 cl.exe, 옛날 turbo-C를 주로 사용해 보셨던 분들은 tcc.exe와 비슷한 녀석이라 보시면 되겠습니다.

(주1 : make를 모르신다구요? make는 Makefile이라는 일종의 스크립트(?)를 실행하는 프로그램입니다. Makefile은..  흐음.. 쉽게 비교하면 vc++ 에서 사용하는 project 파일 정도로 생각 하시면 됩니다. 프로그램의 덩치가 커지면, 여러개의 파일로 나누어 작성하게 되는데, 각 소스들의 관계를 정의한 파일이라 보시면 됩니다. )
(주2 : GNU - GNU is Not Unix 라는 문구의 첫 글자를 딴 재귀적 단어입니다. 자유 소프트웨어 운동(Free Software Movement)를 주칭하고 있는 FSF(Free Software Foundation)의 자유 운영체제 프로젝트의 이름이지요...)

이제 직접 gcc를 실행해 보면서 이야기를 계속 하도록 하겠습니다. 다음과 같이 shell(command line)상에서 입력하면 결과가 나옵니다.

blueguy@blueguy:~$ gcc -v
Using built-in specs.
Target: i486-linux-gnu
Configured with: ../src/configure -v --enable-languages=c,c++,fortran,objc,obj-c++,treelang --prefix=/usr --enable-shared --with-system-zlib --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --enable-nls --with-gxx-include-dir=/usr/include/c++/4.1.3 --program-suffix=-4.1 --enable-__cxa_atexit --enable-clocale=gnu --enable-libstdcxx-debug --enable-mpfr --enable-checking=release i486-linux-gnu
Thread model: posix
gcc version 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)


(*) -v 옵션
현재 사용되고 있는 gcc의 버전을 나타내는 옵션입니다. 간혹 특정 소프트웨어 패키지를 컴파일하기 위해 어느 버전 이상의 gcc를 쓰도록 권장하는 경우가 있는데 시스템에 깔려있는 gcc의 버전을 파악하려면 위와 같이 하면 되겠습니다. 위의 결과는 ubuntu  gutsy에 깔려있는 gcc의 버전을 나타냅니다. 당연히 여러분들의 시스템에는 다른 결과가 나올 수 있습니다.

이제 직접 프로그램 하나를 컴파일 해 보도록 하겠습니다. 정말 간단한 hello.c를 해보죠.

-- start of file “hello.c”
#include <stdio.h>

int main()
{
printf(“hello gcc\n”);
return 0;
}
-- end of file “hello.c”

$ gcc -o hello hello.c
로 컴파일 하면 실행파일 hello가 만들어 집니다. 어떻게 실행하고 그것이 어떤 결과를 주는지는 설명 안 해도 아시겠죠?

(*) -o 파일이름 옵션
gcc의 수행 결과 파일의 이름을 지정하는 옵션입니다. 위의 예제를 단순히
$ gcc hello.c
로 컴파일 하면 hello라고 하는 실행파일이 만들어 지는 것이 아니라 보통의 경우 a.out이라는 이름의 실행파일이 만들어 집니다. -o hello 옵션을 줌으로써 결과(여기서는 실행파일)를 hello라는 이름의 파일로 만들어 주었습니다.

위의 컴파일 과정을 외부적으로 보기에는 단순히 hello.c파일이 실행파일 hello로 바뀌는 것만 보이지만 내부적으로는 다음과 같은 단계를 거쳐 컴파일이 수행됩니다.
(1) C Preprocessing
(2) C 언어 컴파일
(3) Assemble
(4) Linking

C Preprocessing은 C 언어 배우실 때 배운 #include, #define, #ifdef 등 #으로 시작하는 여러 가지를 처리해 주는 과정이라는 것을 아실 겁니다. 그 다음 C 언어 컴파일은 C Preprocessing이 끝난 C 소스 코드를 assembly 소스코드를 변환하는 과정입니다. Assemble은 그것을 다시 object 코드(기계어)로 변환하고 printf()함수가 포함되어 있는 라이브러리와 linking을 하여 실행파일이 되는 것입니다.

위의 네 가지 과정을 모두 gcc라는 실행파일이 해 주는 것일까요? 겉으로 보기에는 그렇게 보일지 모르지만 실제로는 gcc는 소위 말해 front-end라고 하여 껍데기에 지나지 않고 각각을 해 주는 다른 실행파일을 gcc가 부르면서 수행됩니다.
C Preprocessing을 전담하고 있는 실행파일은 cpp라고 하여 /usr/bin 디렉토리에 존재합니다. C 언어 컴파일은 cc1이라는 실행파일이 담당하는데 /usr/lib/gcc/i486-linux-gnu/4.1.3 디렉토리(당연히 gcc버전과 시스템에 따라 디렉토리 위치가 다릅니다. gcc -v로 확인해보세요.)에 존재합니다. Assemble과 linking은 각각 as와 ld라는 실행파일이 담당하고 /usr/bin 디렉토리에 존재하는 파일입니다. (참고 : 시스템에 따라 /usr/bin이 아니라 /bin또는 /usr/local/bin 디렉토리에 존재할 수도 있습니다.)

재미있는 것을 한 번 더 해보도록 하죠. 다음과 같이 입력을 해보죠. (당연히 “//”은 주석이니깐 입력하지 말아야겠죠?)

$ mv hello.c hello.s // hello.c파일의 이름을 hello.s로 바꿉니다.
$ gcc -o hello hello.s // 컴파일

어떤 결과가 나오나요? Assembler error라고 메시지가 뜨죠?(참고 : gcc가 혁신적인 버전업을 했을 경우 에러가 안 나고 제대로 컴파일 될 수도 있습니다.) 파일명만 바뀌었을 뿐 똑 같은 입력을 주었는데 이번에는 에러가 납니다. 이것은 gcc라는 실행파일이 주어진 입력 파일명의 확장자를 보고 이것이 C 언어 소스코드가 아니라 assembly 코드(확장자는 .S또는 .s입니다.)로 인식하고 위에서 설명한 (1)번과 (2)번을 건너 뛰고 (3)을 바로 수행했기 때문입니다. 당연히 assembler(as 실행파일)는 assembly 문법과 다르기 때문에 에러를 냅니다.

이제 gcc라는 실행파일이 하는 일을 정리해 보면 다음과 같습니다.
(1) 사용자에게 옵션과 소스 파일명들의 입력을 받는다.
(2) 소스 파일명의 확장자를 보고 어떤 단계를 처리해야 할지 결정합니다.
(3) 사용자의 옵션을 각각의 단계를 맡고 있는 실행파일의 옵션으로 변경합니다.
(4) 각각의 단계를 맡고 있는 실행파일을 호출(fork와 exec이겠죠?)하여 단계를 수행하도록 한다.

이제까지 gcc라는 이름의 실행파일이 하는 일을 알아보았고, 중요한 옵션 두 가지(-o, -v)에 대해서 살펴보았습니다. 다음에는 C Preprocessing을 맡고 있는 cpp가 하는 일과 그에 해당하는 gcc옵션, 수행 중에 일어날 수 있는 에러 등에 대해서 알아보도록 하겠습니다.

=== C Preprocessing(cpp)
C preprocessing을 우리말로 하면 "C 언어 전처리"라고 할 수 있겠죠? 모든 C 언어 문법책에서 정도의 차이는 있지만 C preprocessing에 대한 내용을 다루고 있습니다. C preprocessing에 대한 문법은 C 언어 문법의 한 부분으로 가장 간단한 예제인 hello.c에도 나오니 당연하겠죠. C preprocessing에 관한 문법은 모두 '#'으로 시작됩니다. 또한 정확하게는 '#'은 그 줄(line)의 선두 문자이어야 합니다. 즉, '#' 앞에는 어떠한 문자(공백 문자 포함)도 오면 안되죠. 하지만 대부분의 compiler가 '#'앞에 공백 문자가 오는 경우에도 처리를 해주는 것으로 알고 있습니다. 아무튼, 문법에 대해서는 가지고 있는 문법책을 참조하시길 바랍니다.

그럼 C preprocessing이 하는 일을 자세히(?) 알아보도록 하죠.

== C preprocessing이 하는 일
(1) 입력 : C 언어 소스 코드
(2) 출력 : C 언어 소스 코드(C preprocessing된)
(3) 하는 일
- 파일 포함(file inclusion : 직역이 어색하네요)
- 매크로(macro) 치환
- 선택적 컴파일(conditional compile)
- 기타(#line, #error, #pragma)

너무 간단한가요? 말로 하면 cpp는 C 언어 소스코드를 입력 받아서 C preprocessing에 관련된 문법 사항을 적절히 처리하고 결과로 C 언어 소스코드를 출력하는 프로그램입니다. 입력은 작성된 C 언어 소스 코드이므로 굳이 설명을 안 해도 될 듯하고 출력으로 나온 C 언어 소스 코드에는 C preprocessing 문법에 관련된 어떠한 것도 남아있지 않습니다. 즉, #define, #include 등을 찾을 수 없다는 이야기입니다. 단, 남아 있는 정보는 있습니다. 그것은 file 이름과 줄수(line number)에 관한 정보는 여전히 남아 있습니다. 그 이유는 추후의 컴파일 과정에서 에러가 날 때 그 정보를 이용해서 error를 리포팅할 수 있도록 하기 위해서 입니다. 그럼 C preprocessing을 직접 해보도록 하죠. shell command line에 다음과 같이 입력하세요.(당연히 $은 shell prompt이므로 입력하지 말아야 하고, hello.c는 전에 입력했던 그 소스코드 파일입니다.)

$ gcc -E -o hello.i hello.c

결과로 hello.i라는 파일이 생깁니다. 그 파일 내용이 너무 길어 여기에 싣지는 못합니다만 에디터로 한번 열어보세요. hello.c의 첫번째 줄에 있는 #include <stdio.h>를 처리한 결과가 보입니까?

(*) -E 옵션
-E 옵션은 gcc의 컴파일 과정 중에서 C preprocessing까지만 처리하고 나머지 단계는 처리하지 말라는 것을 지시하는 것입니다. 평소에는 별로 쓸모가 있는 옵션이 아니지만 다음과 같은 경우에 유용하게(?) 사용할 수 있습니다.
(1) C 언어 소스 코드가 복잡한 선택적 컴파일을 하고 있을 때, 그 선택적 컴파일이 어떻게 일어나고 있는지 알고 싶은 경우.
(2) preprocessing의 문제가 C 언어 에러로 나타날 경우. 다음과 같은 소스코드를 고려해 보죠.

-- start of cpperr.c
#define max(x, y) ((x) > (y) ? (x) : (y) /* 마지막에 ")"가 없다!!! */
int myMax(int a, int b)
{
return max(a, b);
}
-- end of cpperr.c
$ gcc -c cpperr.c
다음과 같은 에러가 납니다.(>>는 에러메시지를 나타내는 기호이며 실제로 출력되지 않습니다.)
>> cpperr.c: In function `myMax':
>> cpperr.c:4: parse error before `;'
cpperr.c파일의 4번째 줄에서 ';'가 나오기 전에 parse error(뒤를 참조)가 났다고 하는 군요. 하지만 실제 에러는 #define에 있었으므로 그것을 확인하려면 -E 옵션으로 preprocessing을 하여 살펴 보면 쉽게(?) 알 수 있습니다.

(*) 참고 : parse error before x(어떤 문자) 에러는 그야말로 parsing을 할 때 발생한 에러를 말합니다. parsing이란 syntax analysis라는 과정인데 쉽게 말하면 C 언어 소스코드를 읽어드려 문법적 구성요소 들을 분석하는 과정이라고 할 수 있습니다. 보통 gcc에서 parse error라고 하면 괄호가 맞지 않았거나 아니면 ';'를 빼먹거나 했을 때 발생합니다. 보통의 경우 before x라고하여 x라는 것이 나오기 전에 parse error가 발생하였음을 알려주기 때문에 그 x가 나오기 전에 있는 C 소스 코드를 뚫어지게 바라보면 문제를 찾을 수 있습니다.

C preprocessing의 문법과 나머지 C 언어의 문법과는 거의 관계가 없습니다. 관계가 있는 부분이 있다면 정의된 macro가 C 언어의 문법 상의 char string literal에는 치환되지 않는다는 정도입니다. (좀더 쉽게 이야기 하면 큰 따옴표 안에서는 macro 치환이 되지 않습니다.) 또한 c preprocessing은 architecture dependent하지 않습니다. 즉, i386용으로 컴파일된 cpp를 다른 architecture에서 사용해도 무방합니다. 조금 문제가 있을 수 있는 부분이 있다면 gcc의 predefined macro(i386의 경우 i386용 자동으로 define된다.)가 다를 수 있다는 점 뿐입니다. 따라서 cpp를 C 언어 소스코드가 아닌 다른 부분에서 사용하는 경우도 있습니다. 대표적으로 assembly 소스 코드에서도 사용합니다. assembler가 사용하고 있는 macro 문법이 c preprocessing의 macro문법 보다는 배우기 쉽기 때문이죠.(정확히는 쉽다고 하기 보다는 새로 assembler macro를 배우지 않아도 되므로...)

이제 preprocessing이 하는 일에 대해서 좀더 알아 보겠습니다.

== 파일 포함(file inclusion)
#include <stdio.h>
#include "config.h"
위와 같이 많은 C 언어 소스코드에서 헤더 파일을 포함하죠. <>와 ""의 차이는 아실테고...(혹 모르신다면 C 언어 문법책을 참조하세요.) 그런데 여기서 한가지 의문이 생깁니다. "include한 헤더 파일을 어느 디렉토리에서 찾는가?"입니다. 보통은 default로 특정 디렉토리를 찾게 됩니다. Linux 시스템의 경우 /usr/include가 default 디렉토리죠.(실제로도 그곳에 stdio.h라는 파일이 있습니다.) 그 다음은 현재 디렉토리를 찾게 됩니다.(<>와 ""에 따라서 다릅니다만...) 파일이 없으면 당연히 에러가 나겠죠. gcc의 경우 다음과 같은 에러가 납니다. (>>은 에러메시지를 나타내는 기호로 실제로는 출력되지 않습니다.)
>>소스코드파일명:line number: 헤더파일명: No such file or directory
또는(LANG=ko일때)
>>소스코드파일명:line number: 헤더파일명: 그런 파일이나 디렉토리가 없음

그렇다면 include하고 싶은 파일이 default 디렉토리와 현재 디렉토리에 없으면 어떻게 할까요? 그런 문제를 해결하기 위해서 다음과 같은 옵션이 존재합니다.
(*) -Idir 옵션
여기서 dir은 디렉토리 이름이고 -I와 디렉토리 이름을 붙여 써야 합니다. 그럼 include한 헤더 파일을 그 디렉토리에서도 찾아 주게 됩니다. 당연히 옵션을 여러 번 다른 디렉토리 이름으로 줄 수도 있어서 헤더 파일을 찾을 디렉토리를 여러 개로 지정할 수 있습니다. 꼭 알아 두어야 할 옵션입니다.

관련 옵션을 하나만 더 알아보죠.
(*) -nostdinc
이 옵션은 default 디렉토리(standard include 디렉토리)를 찾지말라고 지시하는 옵션입니다. 어플리케이션 프로그래머는 관심을 둘 필요가 없지만 kernel 프로그래머는 관심 있게 볼 수 있는 옵션이죠.

== macro 치환
macro 치환에 대해서는 특별히 일어날만한 에러는 없습니다. 가끔 문제가 되는 부분이 macro 정의가 한 줄을 넘어선 경우 역슬레쉬('\')로 이어져야 하는데 그 소스 파일이 windows용 에디터로 편집 되었으면 parse error가 나는 경우가 있습니다. 그것은 개행문자(new line character)가 서로 달라서 그런 것인데...음 자세히 이야기하자면 끝이 없으므로 그냥 넘어가도록 하죠. 또한 macro가 define된 상황에서 macro를 undef하지 않고 다시 define하면 다음과 같은 Warning이 납니다.
>> 'xxxx' redefined
macro 치환에서 대한 옵션 두개를 알아보도록 하죠.

(*) -Dmacro 또는 -Dmacro=defn 옵션
gcc의 command line에서 macro를 define할 수 있도록 하는 옵션입니다. 예를 들어 -D__KERNEL__이라는 옵션을 주면 컴파일 과정 중에 있는 C 언어 소스코드의 맨 처음에 #define __KERNEL__이라고 해준 것과 같이 동작합니다. 또한 -DMAXLEN=255라고하면 C 언어 소스코드의 맨 처음에 #define MAXLEN 255 라고 한 것과 동일한 결과를 줍니다. 선택적 컴파일을 하는 경우에 많이 이용하는 옵션으로 꼭 알아야 할 옵션입니다.

(*) -Umacro 옵션
이 옵션은 #undef하고 하는 일이 똑같은데 C 언어 소스코드와는 하등의 관계가 없습니다. -Dmacro옵션처럼 C 언어 소스코드의 맨처음에 #undef macro를 해주는 것은 아무런 의미가 없기 때문이죠.(어짜피 #define은 그 이후에 나올 것이므로...) 이 옵션의 목적은 위의 -Dmacro옵션으로 define된 macro를 다시 undef하고자 할 때 쓰는 옵션입니다. 평상시에는 별로 쓸 일이 없는 옵션이지만 그냥 -Dmacro와 같이 짝으로 알아 두시길 바랍니다.

== 선택적 컴파일
#if 시리즈와 #else, #elif, #endif 등으로 선택적 컴파일을 수행할 수 있는 것은 모두 아실 것으로 생각됩니다. 위에서 설명한 -Dmacro 옵션과 같이 쓰는 경우가 많죠. 암튼 특별히 설명할 옵션은 없고 #if와 #else, #endif의 짝이 잘 맞아야 합니다. 안 그러면 당연히 에러가 발생합니다. 단순히 parse error라고 나오는 경우는 드물고, #else, #if 에 어쩌고 하는 에러가 납니다. 많이 경우의 수가 있으므로 직접 에러가 발생되도록 코딩을 해보고 확인해 보셔도 좋을 듯 합니다.

== 기타(#line, #error, #pragma)
#line, #error, #pragma라는 것이 있는지도 모르는 분들이 꽤나 있을 듯 싶습니다. 자세한 것은 당연히 C 언어 문법 책을 찾아 봐야 겠죠. #line의 경우 C 언어 소스코드 직접 쓰이는 경우는 거의 없으니까 무시하고 #pragma는 compiler에 dependent하고 gcc에서 어떤 #pragma를 사용하는지도 알 수 없으므로 그냥 넘어가도록 하겠습니다. #error의 경우 C preprocessing 과정에서 강제로 에러를 만드는 지시어입니다. 선택적 컴파일 과정에서 도저히 선택되어서는 안 되는 부분에 간혹 쓰입니다. 당연히 #error를 만나면 에러가 생깁니다. linux kernel 소스코드에서 include 디렉토리를 뒤져 보시면 사용하는 예를 만나실 수 있습니다.

== predefined macro
사용자가 C 언어 소스코드에서 #define을 하지 않아도 이미 #define된 macro가 있습니다. ANSI C에서는 __LINE__, __FILE__, __TIME__, __DATE__, __STDC__ 다섯 가지는 이미 define되어 있는 macro로 강제적인 사항입니다.(당연히 모르면 문법책 참조) gcc도 당연히 다섯 가지 macro를 predefine합니다. 뿐만 아니라 GCC의 버전에 대한 macro, architecture에 관한 사항 등을 -Dmacro 옵션 없이도 predefine합니다. 전에 말씀드린 -v 옵션을 실행하면 출력되는 specs파일을 열어보시면 감을 잡으실 수 있을 겁니다.(specs파일이 어떻게 해석되는지는 저도 잘 모르니까 묻지 마시길...)

== 꼭 알아두면 좋은 옵션 한가지
다음과 같이 shell 상에 입력해 보세요.(hello.c는 계속되는 그 녀석입니다.)
$ gcc -M hello.c
어떤 것이 출력되나요? "hello.o: hello.c /usr/include/stdio.h 어쩌구저쩌구"가 출력될 것입니다. 어디서 많이 본 듯한 형식 아닌가요?

(*) -M 옵션
-M 옵션은 cpp에게 makefile로 만들 수 있는 rule을 만들어달라고 하는 요청을 보내는 명령입니다. file을 include하는 녀석은 cpp이므로 rule은 cpp가 만들 수 있겠죠. 당연히 -Dmacro, -Umacro, -Idir 옵션 등을 같이 사용할 수 있고 그에 따라 결과가 달라질 수도 있습니다. makefile을 좀 쉽고 정확하게 만들 때 쓰는 옵션이므로 알아두면 좋습니다. 단지 안 좋은 점은 default 디렉토리에 있는 보통 사용자는 고칠 수도 없는 파일까지 무식(?)하게 만들어 준다는 것입니다.


이제 총 정리 해 보도록 하겠습니다. C preprocessing은 C 언어 소스코드를 입력으로 받아 file inclusion, macro 치환, 선택적 컴파일, 기타를 처리하고 C 언어 소스코드를 출력하는 과정입니다. 그 중에 몇 가지 에러가 날 수 있는 부분이 있고(물론 사용자의 잘못으로) 몇 가지 중요한 옵션을 알아봤습니다.

=== C 언어 컴파일 과정
처음에 말씀 드린 바대로 C 언어 컴파일 과정은 gcc라고 하는 frontend가 cc1이라는 다른 실행파일을 호출(fork & exec 이겠죠?)하여 수행하게 됩니다. 사용자가 cc1이라는 실행파일을 직접 실행해야 할 하등의 이유도 없고 권장되지도 않습니다.
지난 두 번의 이야기에서 미처 말하지 못한 내용이 있는데, 여기서 잠시 하도록 하겠습니다. gcc의 입력으로 여러 개의 파일(C 소스 파일, object 파일 등)을 준다고 하더라도 컴파일 과정 중 앞 3단계, 즉 cpp, C 컴파일, assemble은 각각의 파일 단위로 수행됩니다. 서로 다른 파일의 영향을 받지 않고 진행됩니다. 당연한 거죠. 특정 C소스 코드에서 #define된 macro가 다른 파일에는 당연히 반영되면 안됩니다. header 파일의 존재 의미를 거기서 찾을 수 있습니다.

이제 C 언어 컴파일 과정이 하는 일을 자세히(?) 알아보도록 하겠습니다.

== C 언어 컴파일 과정이 하는 일
(1) 입력 : C 언어 소스 코드(C preprocessing된)
(2) 출력 : Assembly 소스 코드
(3) 하는 일 : 컴파일(너무 간단한가요?)

C preprocessing과 마찬가지로 너무 간단합니다. 하지만 위의 “컴파일” 과정은 cc1 내부에서는 여러 단계로 나누어져 다음과 같은 순서로 일어납니다. Parsing(syntax analysis)이라고 하여 C 언어 소스 코드를 파일로부터 읽어 들여 컴파일러(여기서는 cc1)가 이해하기 쉬운 구조로 바꾸게 됩니다. 그 다음에 그 구조를 컴파일러가 중간 형태 언어(Intermediate Language)라고 하는 다른 언어로 변환하고 그 중간 형태 언어에 여러가지 최적화를 수행하여 최종 assembly 소스 코드를 만들게 됩니다.

우선 직접 수행해 보도록 하겠습니다. 다음과 같이 shell의 command line에 입력해 보죠. 역시 지긋지긋한 hello.c를 이용하도록 하겠습니다.
$ gcc –S hello.c
결과로 출력된 hello.s를 에디터로 열어서 살펴보세요 (혹시 위의 command로 hello.s가 만들어 지지 않는다면 gcc –S –o hello.s hello.c로 다시 해보세요.). “.”으로 시작하는 assembler directive와 “:”로 끝나는 label명, 그리고 몇 개의 assembly mnemonic이 보이나요? Assembly 소스를 읽을 줄 몰라도 그게 assembly 소스 코드구나 하시면 됩니다.

(*) –S 옵션
-S 옵션은 gcc의 컴파일 과정 중에서 C 언어 컴파일 과정까지만 처리하고 나머지 단계는 처리하지 말라는 것을 지시하는 것입니다. 평소에는 별로 쓸모가 있는 옵션이 아니지만 다음과 같은 경우에 유용하게(?) 사용할 수 있습니다.
(1) 어셈블리 코드가 어떻게 생겼는지 볼 수 있는 보고 싶은 호기심이 발동한 경우(그런 경우 별로 없죠?)
(2) C calling convention을 알아보거나 stack frame이 어떻게 관리되고 있는 지 보고 싶은 경우(조금 어려운 가요?)
보통의 경우는 아니지만 사용자가 직접 assembly 코딩을 하는 경우가 종종 있습니다. 아무래도 사람이 기계보다는 훨씬 똑똑(?)하기 때문에 사람이 직접 assembly 코딩을 해서 최적화를 시도하여 소프트웨어의 수행 시간을 단축시키거나, 아니면 linux kernel이나 bootloader 등과 같이 꼭 assembly가 필요한 경우가 있습니다. 이때도 보통의 경우는 소프트웨어의 전 부분을 assembly 코딩하는 것이 아니라 특정 부분만 assembly 코딩을 하고 나머지는 C 언어나 다른 high-level 프로그래밍 언어를 써서 서로 연동을 하도록 하죠. 그럼 C 언어에서 assembly 코딩된 함수를 호출할 때(반대의 경우도 마찬가지), 함수의 argument는 어떻게 전달되는 지, 함수의 return 값은 어떻게 돌려지는지 등을 알아볼 필요가 있습니다. 이렇게 argument와 return 값의 전달은 CPU architecture마다 다르고 그것을 일정한 약속(convention)에 따라서 처리해 주게 됩니다. 위의 hello.s를 i386용 gcc로 만들었다면 파일 중간에 xorl %eax,%eax라는 것이 보일 겁니다. 자기 자신과 exclusive or를 수행하면 0(zero)이 되는데 이것이 바로 hello.c에서 return 0를 assembly 코드로 바꾼 것이죠. 결국 i386 gcc에서는 %eax 레지스터에 return 값을 넣는 다는 convention이 있는 겁니다.(실제로는 gcc뿐 아니라 i386의 convention으로 convention을 따르는 모든 compiler가 %eax 레지스터를 이용하여 return값을 되돌립니다.) argument의 경우도 test용 C 소스를 만들어서 살펴볼 수 있겠죠. 물론 해당 CPU architecture의 assembly 소스코드를 어느 정도 읽을 수 있는 분 들에게만 해당하는 이야기 입니다.(그럼 이 글을 읽을 만한 초보자가 아니겠지만…) stack frame도 비슷한 얘기 쯤으로 알아 두시면 됩니다.

== Parsing(Syntax Analysis)
위에서 cc1이 컴파일을 수행하는 과정 중에 맨 첫 과정으로 나온 Parsing에 대해서는 좀더 언급을 해야 겠습니다.(나머지 과정은 설명 안 합니다.) Parsing과정은 그야말로 구문(Syntax)을 분석(Analysis)하는 과정입니다. Parsing의 과정은 파일의 선두에서 뒤쪽으로 한번 읽어 가며 수행됩니다.(중요!!!) Parsing 중에 컴파일러는 구문의 에러를 찾는 일과 뒤에 수행될 과정을 위해서 C 언어 소스 코드를 내부적으로 다루기 쉬운 형태(보통은 tree형식을 이용합니다.)로 가공하는 일을 수행합니다. 이 중에 구문의 에러를 찾는 과정은 (1) 괄호 열고 닫기, 세미콜론(;) 기타 등등의 문법 사항을 체크하는 것 뿐만 아니라 (2) identifier(쉽게 말해 변수나 함수 이름 들)의 type을 체크해야 합니다.
(1) 괄호 열고 닫기, 세미콜론(;) 기타 등등의 문법 사항에 문제가 생겼을 때 발생할 수 있는 에러가 전에 이야기한 parse error입니다. 보통 다음과 같이 발생합니다.
>> 파일명과 line number: parse error before x
당연히 에러를 없애려면 ‘x’ 앞 부분에서 괄호, 세미콜론(;) 등을 눈 빠지게 보면서 에러를 찾아 없애야 합니다.
(2) type checking
구문 에러를 살필 때 type 체크를 왜 해야 할까요? 다음과 같은 예를 보도록 하겠습니다.
var3 = var1 + var2;
앞 뒤에서 parse error가 없다면 위의 C 언어 expression은 문법적인 문제가 없습니까? 하지만 var1이 파일의 앞에서 다음과 같이 정의(definition)되었다면 어떻게 될까요?
struct point { int x; int y; } var1;
당연히 ‘+’ 연산을 수행할 수 없습니다.(C 언어 문법상) 결국은 에러가 나겠죠. 이렇게 identifier(여기서는 var1, var2, var3)들의 type을 체크하지 않고서는 구문의 에러를 모두 찾아낼 수 없습니다.
만약 var1과 var3가 파일의 앞에서 int var1, var3;로 정의되어 있고 var2가 파일의 앞에 어떠한 선언(declaration)도 없이 파일의 뒤에서 int var2;로 정의되어 있다면 에러가 발생할까요? 정답은 “예”입니다. 위에서 언급했듯이 Parsing은 파일의 선두에서 뒤쪽으로 한번만(!!!) 읽으면서 진행하기 때문입니다.(모든 C 컴파일러가 그렇게 동작할지는 의심스럽지만 ANSI C 표준에서는 그렇게 되어 있는 것으로 알고 있습니다. Assembler는 다릅니다.)
그렇다면 어떤 identifier를 사용하려면 반드시 파일 중에 사용하는 곳 전에 identifier의 선언(declaration) 또는 정의(definition)가 있어야 겠군요. 하지만 identifier가 함수 이름일 경우(즉 identifier뒤에 (…)가 올 경우)는 조금 다릅니다. C 컴파일러는 함수 이름 identifier의 경우는 int를 return한다고 가정하고 Error가 아닌 Warning만 출력합니다.(Warning옵션에 따라 Warning조차 출력되지 않을 때도 있습니다.) 그럼 다음과 같은 소스 코드를 생각해 보겠습니다.
int var3, var2;
….
var3 = var1() + var2;
….
struct point var1(void) { … }
위와 같은 경우도 문제가 생깁니다. 맨 처음 var1이라는 함수 이름 identifier를 만났을 때 var1 함수는 int를 return한다고 가정했는데 실제로는 struct point를 return하므로 에러 또는 경고를 냅니다.
결국 권장하는 것은 모든 identifier는 사용하기 전(파일 위치상)에 선언이나 정의를 해 주는 것입니다.
다음과 같은 에러 메시지들을 짧막하게 설명하죠.
>>파일명 line number: ‘x’ undeclared …. 에러
‘x’라는 이름의 identifier가 선언되어 있지 않았다는 것이죠.
>>파일명 line number: warning: implicit declaration of function `x' … 경고
‘x’라는 이름의 함수가 선언되어 있지 않아 int를 return한다고 가정했다는 경고(Warning) 메시지입니다.

(*) 여기서 잠깐
변수나 함수의 선언(declaration)과 정의(definition)에 대해서 알지 못한다면 C 언어 문법책을 찾아서 숙지하시길 바랍니다. 그런 내용이 없다면 그 문법책을 휴지통에 버리시길 바랍니다.

Parsing 과정에는 위의 identifier 에러 및 경고를 비롯한 수많은 종류의 에러와 경고 등이 출력될 수 있습니다. 에러는 당연히 잡아야 하고 경고도 무시하지 않고 찾아서 없애는 것이 좋은 코딩 습관이라고 할 수 있겠습니다. 경고 메시지에 대한 gcc 옵션을 살펴보도록 하겠습니다.

(*) –W로 시작하는 거의 모든 옵션
이 옵션들은 어떤 상황 속에서 경고 메시지를 내거나 내지 말라고 하는 옵션들입니다. –W로 시작하는 가장 강력한 옵션은 –Wall 옵션으로 모든 경고 메시지를 출력하도록 합니다. 보통은 –Wall 옵션을 주고 컴파일 하는 것이 좋은 코딩 습관입니다.

== Parsing 이후 과정
특별한 경우가 아닌 이상 Parsing을 정상적으로 error나 warning없이 통과한 C 소스 코드는 문법적으로 완벽하다고 봐야 합니다. 물론 논리적인 버그는 있을 수 있지만 이후 linking이 되기 전까지의 과정에서 특별한 error나 warning이 나면 안됩니다. 그런 경우가 있다면 이제는 사용자의 잘못이 아니라 gcc의 문제로 추정해도 무방합니다. Parsing이후에 assembly 소스 코드가 생성되는데, 당연히 이 과정에는 특별히 언급할 만한 error나 warning은 없습니다. 그냥 중요한 옵션 몇 가지만 집고 넘어가도록 하겠습니다.

(*) –O, -O2, -O3 등의 옵션
이 옵션은 컴파일러 최적화를 수행하라는 옵션입니다. –O 뒤의 숫자가 올라갈수록 더욱 많은 종류의 최적화를 수행하게 됩니다. 최적화를 수행하면 당연히 코드 사이즈도 줄어 들고 속도도 빨라지게 됩니다. 대신 컴파일 수행 시간은 길어지게 되겠죠. 그리고 linux kernel을 위해 언급하고 싶은 것은 inline 함수들은 이 옵션을 주어야 제대로 inline됩니다.

(*) –g 옵션
이 옵션은 소스 레벨 debugger인 gdb를 사용하기 위해 debugging 정보(파일명, line number, 변수와 함수 이름들과 type 등)를 assembly code와 같이 생성하라는 옵션입니다. 당연히 gdb를 이용하고 싶으면 주어야 합니다. –g 옵션을 주지 않고 컴파일한 프로그램을 gdb로 디버깅하면 C 소스 레벨이 아닌 assembly 레벨 디버깅이 됩니다. 즉 C 소스 코드에 존재하는 변수 이름, line number 등이 없는 상황에서 디버깅을 해야 합니다. 또한 –g 옵션을 –O 옵션과 같이 사용할 수도 있습니다. 단 그런 경우 최적화 결과, C 소스 코드에 존재하는 심볼(symbol; 쉽게 말해 함수와 변수 이름)중에 없어지는 녀석들도 존재합니다.

(*) 또 여기서 잠깐
이런 것까지 알아야 할지 의심스럽지만… identifier와 symbol이 모두 “쉽게 말해 함수와 변수 이름”이라고 했는데 어떻게 차이가 날까요? 엄밀히 말하면 차이가 조금 있습니다. symbol이 바로 “쉽게 말해 함수와 변수 이름”이며 각 symbol은 특정 type과 연계되어 있습니다. 하지만 identifier는 그냥 “이름” 또는 “인식어”일 뿐입니다. 예를 들어 struct point { int x; int y; };라는 것이 있을 때 point는 symbol은 아니지만 identifier입니다. 보통 identifier라는 말은 parsing에서만 쓰인다는 정도만 알아두시면 좋겠습니다. 이후에 symbol이나 identifier라는 말이 나오면 “쉽게 말해 함수와 변수 이름”이라고 표기하지 않겠습니다.

(*) –p 옵션과 –pg 옵션
profiling을 아십니까? 수행시간이 매우 중요한 프로그램(real time 프로그램이라고 해도 무방할 듯)을 작성할 때는 프로그램의 수행 시간을 함수 단위로 알아야 할 필요가 있는 경우가 많습니다. 프로그램의 수행 시간을 함수 단위나 더 작은 단위로 알아보는 과정을 profiling이라고 하는데, profiling은 프로그램 최적화에 있어서 중요한 기능을 담당합니다. 대부분의 개발 툴이 지원하고 Visual C++에도 존재합니다. 옛날 turbo C에는 있었는지 모르겠군요.(제가 turbo C를 사용 할 때는 profiling을 해야 할 프로그램을 작성한 적이 없어서) 아무튼 gcc도 역시 profiling을 지원합니다. –p 옵션 또는 –pg 옵션을 주면 프로그램의 수행 결과를 특정 파일에 저장하는 코드를 생성해 주게 됩니다. 그 특정 파일을 적당한 툴(prof또는 gprof 등)로 분석하면 profiling 결과를 알 수 있게 해 줍니다. 당연히 linux kernel 등에서는 사용할 수 없습니다.(이유는 특정 파일에 저장이 안되므로…) 초보자 분들은 이런 옵션도 존재하고 profiling을 할 수 있다는 정도만 알아 두시면 좋을 듯 싶습니다. 나중에 필요하면 좀 더 공부해서 사용하시길.

(*) 기타 옵션(-m과 –f시리즈)
중요한 옵션들이기는 하지만 초보자가 알아둘 필요가 없는 옵션 중에 f또는 m으로 시작하는 옵션들이 있습니다. f로 시작되는 옵션은 여러 가지 최적화와 assembly 코드 생성에 영향을 주는 architecture independent한 옵션입니다.(assembly 코드 생성이 architecture dependent이므로 정확히 말하면 f로 시작되는 옵션이 architecture independent라고 할 수는 없습니다.) m으로 시작되는 옵션은 보통 architecture dependent하며 주로 CPU의 종류를 결정하는 옵션으로 assembly 코드 생성에 영향을 주게 됩니다. 하지만 대부분은 초보자는 그런 것이 있다는 정도만 알아두면 되고 특별히 신경 쓸 필요는 없다고 생각됩니다. m으로 시작되는 옵션 중에 KELP 사이트의 사용자가 관심을 둘만한 옵션 중에 –msoft-float옵션이 있습니다.(물론 특정 architecture에만 존재하는 옵션입니다.) –msoft-float 옵션은 CPU에 FPU(floating point unit)가 없고, kernel에서 floating-point emulation을 해 주지 않을 때 C 소스 코드 상에 있는 모든 floating-point 연산을 특정 함수 호출로 대신 처리하도록 assembly 코드를 생성하라고 지시하는 옵션입니다. 이 옵션을 주고 라이브러리를 linking시키면 FPU가 없는 CPU에서도 floating 연산을 할 수 있습니다.(대신 엄청 느리죠. 어찌보면 kernel floating-point emulation보다는 빠를 것 같은데 확실하지는 않습니다.)

이상 C 언어 컴파일 과정에 대해서 알아보았습니다. 다음에는 assemble 과정에 대해서 알아보겠습니다.

=== Assemble 과정
Assemble 과정은 앞선 과정과 동일하게 gcc라는 frontend가 as라는 실행 파일을 호출(?)하여 수행됩니다. 그런데 as는 cpp와 cc1과는 달리 gcc 패키지 안에 존재하는 것이 아니라 별도의 binutils라고 하는 패키지에 존재합니다. binutils 패키지 안에는 as를 비롯해 linking을 수행하는 ld, library 파일을 만드는 ar, object 파일을 보거나 복사할 수 있는 objdump, objcopy 등 여러 가지 툴이 들어 있습니다.

이제 Assemble 과정이 하는 일을 자세히(?) 알아보도록 하겠습니다.

== Assemble 과정이 하는 일
(1) 입력 : Assembly 소스 코드
(2) 출력 : relocatable object 코드
(3) 하는 일 : assemble(너무 간단한가요?)

역시나 간단하네요. 입력은 당연히 C 언어 컴파일 과정을 거치면 나오는 Assembly 소스 코드입니다. Assemble 과정을 거치면 소위 기계어(machine language)라는 결과가 relocatable object 형식으로 나오겠죠. “relocatable”이라는 말이 어려우면 그냥 object 코드라고 해 두죠. 어짜피 나중에 설명이 나올테니깐.
이제 직접 수행해봐야 겠죠? shell의 command line에 다음과 같이 입력하면 됩니다.
$ gcc –c hello.c
지겨운 hello.c를 썼습니다. 결과는 hello.o라고 하는 파일이 나옵니다. hello.o는 binary형식의 파일이니깐 editor로 열어봐야 정보를 얻기 힘듭니다. 당연히 위의 예는 assemble 과정만 수행한 것이 아니라 C preprocessing 과정, C 언어 컴파일 과정, Assemble 과정을 수행했겠죠. Assemble 과정만 수행하고 싶으면 다음과 같이 입력하면 됩니다.
$ gcc –c hello.s
역시 hello.o가 생기겠죠. hello.s는 C 언어 컴파일 과정에서 –S 옵션으로 만들었던 그 파일입니다. 별로 관심이 안 생기면 as를 직접 수행할 수도 있습니다. 다음과 같습니다.
$ as –o hello.o hello.s
역시 hello.o가 생기죠?

Assemble 과정 이야기가 짧기 때문에 늘려서 써 봤습니다(이해해 주시길…).

(*) –c 옵션
많이 쓰는 옵션이죠. Assemble 과정까지의 과정만 수행하고 linking 과정을 수행하지 말라는 옵션입니다. 여러 개의 C 소스 파일로 이루어진 프로그램을 컴파일 할 때 모든 소스 파일을 assemble 과정까지 수행하고 맨 마지막에 linking하죠. 보통은 Makefile을 많이 이용하는데 그 때 많이 쓰이는 옵션이죠.

Assemble 과정에서는 더 이상 기억해야 하는 옵션도 없고 이게 끝입니다. C 언어 컴파일 과정에서 말씀 드린 바대로 C 언어 컴파일 과정이 끝난 C 소스 파일은 문법적으로 완전하다고 볼 수 있으므로 assemble 과정에서 Error나 Warning 나는 경우는 없습니다. 만약 Error나 Warning이 나는 경우가 있다면 gcc의 inline assemble을 이용했을 때, 그 inline assemble 소스 코드에 있는 문제 때문에 생길 수 있습니다. 안타깝지만 error나 warning 메시지가 나온 다면 C 소스 파일과 line number 정보는 없습니다. 잘 알아서 처리하는 수 밖에 다른 방법은 없는 것 같습니다. inline assemble 같은 것을 사용하지 않았는데도 error나 warning이 난다면 gcc의 버그라고 생각하셔도 무방합니다.


여기서 끝내면 너무 짧으니까 재미있는(?) 이야기나 하도록 하죠. 이 이야기는 linking 과정을 이해하기 위해서 필요할 지 모르니 조금 어렵더라도 참고 읽어 두시길 바랍니다.

== relocatable object 코드 파일 내용
어떤 정보가 object 파일 안에 들어있을까요? 당연히 code와 data가 들어 있습니다. C 컴파일 과정에서 C 언어 함수 안에 있는 내용들이 assembly mnemonic 들로 바뀌었고 그것이 assemble되어 기계어(machine language)가 되었을 겁니다. 그 부분이 code를 이루겠죠. C 언어 소스 코드에 있는 나머지는 전역 변수(external variable)와 정적 변수(static variable)들이 data를 이룰 겁니다. 또한 문자열 상수를 비롯한 상수도 data에 들어 있겠죠. 또한 프로그램 수행에 쓰이지는 않고 단순한 정보로서 들어 있는 data들도 있습니다. 예를 들어 –g 옵션을 주고 컴파일 하면 프로그램의 디버깅 정보(변수, 함수 이름, C 소스 파일이름, line number 등)가 data에 속한다고 볼 수 있습니다. 그런데 code와 data가 무질서하게 섞여 있는 것은 아니고 section이라고 불리우는 단위로 서로 구분되어 저장되어 있습니다. Code는 text section에 들어 있고, data는 성격에 따라 data section, bss section, rodata section 등에 나누어져 저장되어 있습니다.(text, data, bss, rodata 등의 section 이름은 그냥 관습적인 것입니다.) 아무튼 section 이야기는 이 정도만 우선 알아두시면 될 듯 싶습니다. 좀 더 복잡한 이야기를 할 수도 있지만 초보자에게는 별 필요 없을 듯 하여 그만 두겠습니다.

== Symbol 이야기
relocatable object code안에 code와 data가 들어 있다고 했는데, 아주 중요한 것을 빠뜨렸습니다. 이 이야기는 linking 과정을 이해하기 위해 꼭 필요한 부분이므로 반드시 읽어 두셔야 할 듯 싶습니다.
우선 Symbol이 무엇인지 아시죠? C 언어 컴파일 과정에서 identifier와 함께 설명 드렸는데 잠시 다시 말씀 드리면 Symbol은 함수와 변수 이름입니다. 변수 중에 특히 관심두어야 할 것 들은 자동 변수(?,auto variable)들이 아닌 전역 변수(external variable)와 정적 변수(static variable) 입니다. 자동 변수는 함수의 stack frame에 존재하는 변수이기 때문에 현재 stack pointer(sp, 보통의 CPU의 register중에 하나)에 대한 offset으로 표현됩니다. 즉 현재 함수에서 자동 변수(auto variable)를 access(read/write)하고 싶으면 sp+상수의 어드레스를 access하면 되죠. 하지만 전역 변수와 정적 변수는 그냥 32bit(32bit CPU기준) 어드레스를 읽어야 합니다. stack pointer랑은 전혀 관계 없죠. 아무튼 여기서 관심을 두는 Symbol은 함수, 전역 변수와 정적 변수의 이름이라고 할 수 있습니다.
이제 생각해 볼 것은 C 언어 소스 파일을 C preprocessing, C 언어 컴파일, assemble 과정을 거치면 완전한 기계어로 바꿀 수 있느냐 하는 점입니다. 완전히 기계어로 바꿀 수 있을 까요? C 언어 소스 파일 하나로 이루어지는 프로그램이라면 완전히 기계어로 바꾸는 것이 가능하겠지만 일반적으로는 불가능 합니다. 다음과 같은 예제를 살펴보죠.
-- start of test1.c
int func3(void); /* func3 선언 */
extern int mydata; /* mydata 선언 */

int func2(void) /* func2 정의 */
{
….
}

int func1(void) /* func1 정의 */
{
int i;
…..
func2();
…..
func3();
….
i= mydata+3;
…..
}
-- end of test1.c
-- start of test2.c
int mydata = 3; /* mydata 정의 */
int func3(void) /* func3 정의 */
{
…..
}
-- end of test2.c

위의 예제를 컴파일 한다고 생각해보죠. test1.c에서 func1()의 내용을 기계어로 바꾸고 싶은데 func2()를 호출하는 시점에서는 별로 문제가 안됩니다. func2()는 같은 소스 코드 내에 존재하고 func2()를 호출하는 instruction과 func2()의 실제 위치(어드레스)의 차이를 계산해 낼 수 있으므로 상대 어드레스를 이용하는 함수 호출 instruction으로 완전히 기계어로 바꿀 수 있습니다. 그런데 문제는 func3()를 호출할 때는 func3()의 실제 위치(address)를 계산할 수 없다는 문제점이 있습니다. 당연히 동일한 파일에 존재하는 함수가 아니므로 그 함수가 존재하게 될 어드레스를 계산할 수 없겠죠. 어드레스를 모르는데 함수 호출 instruction을 완전히 만들 수 있을까요? 만들 수 없죠. 당연히 전역 변수 mydata를 access하는 부분도 마찬가지로 mydata의 어드레스를 모르므로 완전히 instruction으로 바꿀 수 없습니다. 그럼 어떻게 해야 될까요?
그때 assembler는 그냥 함수 어드레스 없는 함수 호출 instruction을 기계어로 바꾸어 놓습니다. 그런 다음에 그 instruction에 “func3()를 호출한다”라는 표지를 붙여 놓습니다. 그럼 그 후의 과정(linking 이겠죠?)에서 func3()의 address를 계산했을 때 그 빈 공간을 채워 넣게 됩니다. mydata와 같은 전역 변수도 마찬가지로 동작합니다. 그럼 test1.c을 컴파일할 때는 “func3()”, “mydata” 라는 표지를 사용해야 겠죠? 그럼 test2.c를 컴파일 할 때는 무엇이 필요할까요? 상식적으로 생각하면 “func3()”, “mydata”가 여기 있다라는 정보를 가지고 있어야 겠죠?
정리하면 object 파일 안에는 그 object 파일에 들어있는 symbol들(test1.o에서는 func1과 func2, test2.o에서는 func3와 mydata)에 대한 정보가 들어있고, 그 object 파일이 reference하고 있는 symbol들(test1.o에서 func3와 mydata 사용)에 대한 정보가 들어 있습니다. 이해 되시나요?

== Relocatable의 의미
위에서 object 코드라고 하지 않고 relocatable object 코드라고 지칭했는데 relocatable이 뜻하는 것을 잠시 집고 넘어 가겠습니다. Relocatable을 사전에서 찾아보면 “재배치가 가능한” 정도의 뜻입니다. “재배치가 가능한” 이라는 의미는 상당히 모호합니다. 좀 더 구체적으로 말씀드리면 위에서 설명된 symbol들의 절대 어드레스가 정해지지 않았다는 뜻입니다. 즉 test1.c의 func1()이 절대 어드레스 0x80000000에 존재해야 한다라고 정해지지 않고 어떤 절대 어드레스에 존재해도 관계 없다는 뜻입니다. 그런데 이 말과 헷갈리는 말이 한가지 더 있는데 그것은 position independent code입니다. C 언어 컴파일 과정에서 설명한 옵션중에 –f 시리즈가 있었습니다. 그 중에 –fpic라는 position independent code를 만들라고 강제하는 옵션이 있습니다. position independent code도 역시 절대 어드레스상에 어느 위치에 있어도 무방한 code를 지칭합니다. 하지만 두 가지는 분명 차이가 있는데… 에이, 그냥 넘어 가도록 하죠. 설명을 하려면 상당히 복잡하기 때문에… 그냥 relocatable은 절대 어드레스가 결정되지 않았다는 뜻, 그러나 position independent code와는 다른 말임을 알아 두세요.


=== Linking 과정
Linking 과정은 ld라고 하는 실행파일이 담당하고 있습니다. Assemble을 담당하는 as와 마찬가지로 binutils 패키지의 일부분이죠. 보통 어플리케이션을 컴파일하는 경우에는 gcc(실행파일)를 이용하여 ld를 호출하나, 특별한 경우에 있어서는 ld를 직접 수행하여 linking을 하는 경우가 종종 있습니다.

== Linking 과정이 하는 일
(1) 입력 : 하나 이상의 relocatable object 코드 와 library
(2) 출력 : 실행파일(executable) 또는 relocatable object 코드
(3) 하는 일 : symbol reference resolving & location

좀 복잡한가요? Linking 과정은 하나 또는 그 이상의 object 파일과 그에 따른 library를 입력으로 받습니다. 출력은 보통의 경우는 실행파일(executable file)이지만, 경우에 따라서 object 파일을 생성하게 할 수도 있습니다. 여러 개의 object 파일을 합쳐서 하나의 object 파일로 만드는 과정을 partial linking이라고 부르기도 합니다. Linking 과정이 하는 일은 symbol reference resolving하고 location이라고 했는데, 저도 정확한 단어를 적은 것인지 의심스럽습니다. 정확한 용어를 사용한다면 좋겠지만 그렇지 못하더라도 내용을 정확히 이해하는 것이 중요하니깐 내용에 대해서 살펴보도록 하겠습니다.

== symbol reference resolving
앞서 나오는 예제인 test1.c, test2.c에서 다룬 내용입니다. 우선 그것을 안 보셨다면 살펴보시길 권합니다. 어떤 C 소스 파일에서 다른 파일에 있는 함수와 전역 변수(symbol)에 대한 참조(reference)를 하고 있다면 assemble 과정에서 완전한 기계어로 바꿀 수 없습니다.(실제로는 같은 소스 파일에 있는 전역 변수를 참조하는 것도 보통의 경우, 완전한 기계어로 바꿀 수 없습니다.) 그 이유는 당연히 assemble 까지의 과정은 단일 파일에 대해서만 진행되고, 다른 파일에 있는 해당 함수와 전역 변수의 address가 상대적이든 절대적이든 결정될 수 없기 때문입니다. 따라서 완전히 기계어로 바꿀 수 없는 부분은 그대로 “공란”으로 남겨두고 표시만 해 두게 됩니다.
Linking 과정에서 그 “공란”을 채워 넣게 됩니다. 그 과정을 보통 “resolve한다”라고 말합니다. 어떻게 할까요? 당연히 실행 파일을 이루는 모든 object 파일을 입력으로 받기 때문에 object 파일들을 차곡 차곡 쌓아 나가면(아래 location 참조) object 파일 안에 있는 모든 symbol(함수나 전역 변수 이름)의 address를 상대적이든 절대적이든 계산할 수 있습니다. 이제 각 symbol의 address가 계산되었으므로 표시가 남아 있는 “공란”에 해당하는 symbol의 address를 잘 넣어주면 됩니다. 어떻습니까? 간단한가요?
linking 과정에서 나올 수 있는 에러는 대부분 여기에서 발생합니다. 표시가 남아 있는 “공란”을 채울 수 없는 경우가 있습니다. 크게 두 가지로 나누어지는데요 우선 reference하고 있는 symbol을 찾을 수 없는 경우와 reference하고 있는 symbol의 정의가 여러 군데에 있는 경우죠. 이해하기 쉽죠?

>> object파일명: In function ‘func’:
>> object파일명: undefined reference to ‘symbolname’
위의 에러 메시지는 함수 func 안에서 사용되고 있는 symbolname이란 이름의 symbol이 어디에도 정의되지 않아서 “공란”을 채울 수 없다는 뜻입니다. 당연히 symbolname을 잘못 입력하였던지 아니면 그 symbol이 속해있는 object 파일이나 library와 linking되지 않았기 때문입니다.

>> object파일명1: multiple definition of ‘symbolname’
>> object파일명2: first defined here
위의 에러 메시지는 symbolname이란 이름의 symbol이 여러 번 정의되고 있다는 뜻입니다. object파일1에서 정의가 있는데 이미 object파일2에서 정의된 symbol이므로 그 symbol을 reference하고 있는 곳에서 정확하게 “공란”을 채울 수 없다는 뜻입니다. 당연히 두 symbol중에 하나는 없애거나 static으로 바꾸거나 해야 해결될 것입니다.

== location(용어 정확하지 않을 수 있음)
이전 까지 object 코드를 모두 relocatable이라고 표현했습니다. 아직 절대 address가 결정되지 않았다는 의미로 사용된다고 말씀드렸죠.(position independent code와는 다른 의미라는 말씀과 함께) object 코드의 절대 address를 결정하는 과정이 “location”입니다. Symbol reference resolving과정에서 입력으로 받은 모든 object 파일들을 차곡 차곡 쌓아 나간다고 했습니다. 그런데 object 파일이 무슨 벽돌도 아닌데 차곡 차곡 쌓는 다는 것이 말이 되나요? 여기서 쌓는 다는 말을 이해하기 위해서 다음과 같은 그림(?)을 살펴 보도록 하죠.(처음으로 그림(?)을 그리는 것 같네요.)

많은 object code들
----------------- address(0xAAAAAAAA+0x5000)
test2.o(size 0x3000)
----------------- address(0xAAAAAAAA+0x2000)
test1.o(size 0x2000)
----------------- address(0xAAAAAAAA)
(그림)
절대 address 0xAAAAAAAA에 test1.o의 내용을 가져다 놓습니다. test1.o의 크기(파일 크기와는 의미가 조금 다르지만 그냥 무시하고 파일 크기라고 생각하기 바람)가 0x2000이므로 다음에 test2.o를 쌓을 수 있는 address는 0xAAAAAAAA+0x2000가 되죠. 그곳에 다시 test2.o를 쌓고 또 test2.o의 크기를 보고 새로운 address 계산하고 또 object 코드 쌓고, 계속 반복이죠. 이렇게 쌓을 때 초기 절대 address 0xAAAAAAAA가 무슨 값을 가지게 되면 모든 object 파일에 있는 symbol의 절대 address도 계산해 나갈 수 있겠죠. 그걸로 symbol reference를 resolve하게 되죠. 그 초기 절대 address 0xAAAAAAAA의 값을 정하는 것을 location이라고 합니다. 그럼 왜 절대 address를 결정해야 할까요? 꼭 그래야 할 필요는 없습니다만 CPU의 instruction이 대부분의 경우 절대 address를 필요로 하는 경우가 많기 때문이라고 할 수 있습니다.

(주의) object 를 쌓는 것은 위의 예처럼 단순하지는 않습니다. 실제로는 object 전체를 쌓지 않고 object안에 있는 section별로 쌓게 됩니다. 앞에서 잠시 나왔던 section 기억하시죠?

그럼 이제 직접 수행해 봐야겠죠.
$ gcc –o hello hello.o
간단하죠? object 파일이 하나라서 너무 단순하다고 생각하십니까? 물론 hello.o 하나만 command line에 나타나지만 실제로는 조금 많은 object 파일이 linking되고 있습니다.(아래에서 좀더 자세한 이야기를 하죠.) 지겹지만 hello를 실행해 보세요. 제대로 동작합니까? 제대로 동작한다면 그 사이 어떤 일이 벌어졌을까요? 그 사이에 벌어진 일을 간단히 적어보면 다음과 같습니다. shell이 fork() 시스템콜을 호출하고 자식 process는 exec() 시스템콜을 통해 hello라는 파일 이름을 kernel에 넘깁니다. kernel에서는 hello파일을 보고 linking할 때 location된 address(여기서는 absolute virtual address입니다.)상의 메모리로 hello 파일을 복사하고 PC(program counter)값을 바꾸면 수행되기 시작합니다. 간단하죠?
(주의) 실제로 위의 hello가 수행되는 과정은 많은 생략과 누락이 있었습니다. 실제로는 hello 파일을 완전히 메모리로 복사하는 것도 아니고, dynamic linking & loading 등의 개념이 완전히 빠져 있습니다만 그냥 이해하기 쉽게 하기 위해서 간단하게 적어 본 겁니다. 딴지 걸지 마시길…

= library
hello.o를 linking하여 hello라고 하는 실행파일을 만드는데 command line에서는 아무것도 없지만 library가 같이 linking되고 있습니다. 그것은 지극히 당연합니다. hello.c의 main함수에서 printf함수를 호출(linking이니깐 참조 혹은 reference라고 해야 좋겠습니다.)하고 있는데 printf함수 자체는 소스 중에 그 어디에도 없습니다.(물론 stdio.h에 printf함수의 선언은 있습니다만 정의는 어디에도 없습니다.) 잘 알다시피 printf함수는 C standard library 안에 있는 함수입니다. C standard library가 같이 linking되었기 때문에 제대로 동작하는 hello 실행파일이 생긴 것이죠.
library라는 것은 아주 간단한 것입니다. relocatable object 파일들을 모아 놓은 파일이죠. 소스로 제공할 수도 있으나 그러면 매번 cpp, c 컴파일, assemble 과정을 거쳐야 하므로 컴파일 시간이 매우 증가하게 되겠죠. 그래서 그냥 relocatable object 파일로 제공하는 것이 컴파일 시간 단축을 위해서 좋습니다. 그런데 필요한 relocatable object 파일이 너무 많으면 귀찮으니까 그것을 묶어서 저장해 놓은 녀석이 바로 library라고 할 수 있습니다.
Linux를 비롯한 unix 계열에서는 대부분의 library 파일의 이름이 lib로 시작됩니다. 확장자는 두 가지가 있는데, 하나는 .a이고 또 하나는 .so입니다.(뒤에 library 버전 번호가 붙는 경우가 많이 있습니다.) .a로 끝나는 library를 보통 archive형식의 library라고 말하며 .so로 끝나는 library를 보통 shared object라고 부릅니다. /lib 디렉토리와 /usr/lib 디렉토리에 가면 많이 볼 수 있습니다.
archive library 안에 있는 symbol를 reference하게 되면 library중에 해당 부분(object 파일 단위)을 실행 파일 안에 포함시켜 linking을 수행합니다. 즉 해당 object 파일을 가지고 linking을 수행하는 것과 동일한 결과를 가집니다. 보통 이런 linking을 static linking이라고 부릅니다.
그런데 시스템 전체에 현재 수행되고 있는 실행파일(우리는 실행파일이 수행되고 있는 하나의 단위를 process라고 부르죠.)들에서 printf함수를 사용하고 있는 녀석들이 매우 많으므로 그것이 모두 실행 파일에 포함되어 있다면 그것은 심각한 메모리 낭비를 가져온다는 문제점을 가지고 있습니다. 그래서 생각해 낸 것이 dynamic linking이라는 개념입니다. 예를 들어 실행파일이 printf함수를 사용한다면 실행파일이 메모리로 loading될 때 printf가 포함되어 있는 library가 메모리 상에 있는 지 검사를 해 보고 있으면 reference resolving만 수행하고 아니라면 새로 loading과 reference resolving을 하게 됩니다. 그렇게 되면 printf가 포함되어 있는 library는 메모리 상에 딱 하나만 loading되면 되고 메모리 낭비를 막을 수 있죠. 그런 일을 할 수 있도록 도입된 것이 shared object입니다. MS Windows쪽의 프로그래밍을 하시는 분이라면 DLL과 동일한 개념이라고 보시면 됩니다.
그런 shared object를 이용하여 dynamic linking을 하면 실행파일의 크기가 줄어 듭니다. 반면에 당연히 실행파일이 메모리에 loading될 때는 reference resolving을 위해서 CPU의 연산력을 사용하죠. 하지만 MS Windows의 DLL과는 달리 shared object 파일과 static linking을 할 수도 있습니다.(반대로 archive library를 이용하여 dynamic linking을 수행할 수는 없습니다.) 암튼 각설하고 여기서 gcc 옵션 한 가지를 살펴 보죠.

(*) –static 옵션
dynamic linking을 지원하고 있는 시스템에서 dynamic linking을 수행하지 않고 static linking을 수행하라는 옵션입니다. dynamic linking을 지원하고 있는 시스템에서는 dynamic linking이 default입니다.

직접 수행해 보도록 하겠습니다.
$ gcc –o hello_static –static hello.o
실행파일 hello, hello_static 을 수행하면 결과는 똑같습니다. 파일의 크기를 비교해 보세요.

여기서 의문점이 또 생기는군요. /lib, /usr/lib에는 엄청 많은 library 파일들이 존재합니다. 그럼 linker가 찾아야 하는 symbol을 모든 library 파일에 대해서 검사를 해 볼까요? CPU하고 HDD가 워낙 빠르면 그래도 무방하겠지만, 그렇게 하지 않습니다.(“사용자가 쉽게 할 수 있는 일을 컴퓨터에게 시키지 말라.”라는 컴퓨터 사용 원칙이죠.) 우선 gcc는 기본적인 library만 같이 linking을 하게 되어 있습니다. 나머지 library는 사용자의 요구가 있을 때만 같이 linking을 시도하도록 되어 있습니다. 그럼 기본적인 library가 무엇인지 알아야 하고 gcc에게 사용자의 요구를 전달할 옵션을 있어야 겠죠? 기본적인 library는 당연히 C standard library입니다. C standard library의 이름은 libc.a또는 libc.so입니다. 최근의 linux 머신을 가지고 계신 분은 /lib/libc.so.6이라는 파일을 찾아 보실 수 있을 겁니다(symbolic link되어 있는 파일이지만). 그리고 libgcc라고 하는 것이 있는데…생략하고. 이제 옵션을 알아보죠.

(*) –nostdlib 옵션
이름에서 의미하는 바대로 standard library를 사용하지 말고 linking을 수행하라는 뜻입니다. 실제로는 standard library뿐 아니라 startup file이란 녀석도 포함하지 않고 linking이 수행됩니다. startup file에 대해서는 좀 있다가 알아보도록 하겠습니다.

(*) –l라이브러리이름 옵션
특정 이름의 library를 포함하여 linking을 수행하라는 뜻입니다. 예를 들어 –lmyarchive라고 하면 libmyarchive.a(또는 libmyarchive.so)라는 library파일과 같이 linking을 수행하는 겁니다. library 파일 이름은 기본적으로 lib로 시작하니깐 그것을 빼고 지정하도록 되어 있습니다.

library에 대해서 또 하나의 옵션을 알아야 할 필요가 있습니다. 다름 아닌 “어느 디렉토리에서 library를 찾는가”입니다. 모든 library가 /lib와 /usr/lib에 있으라는 보장이 없잖아요. 그 디렉토리를 정하는 방법은 두 가지 인데 LD_LIBRARY_PATH라고 하는 이름의 환경 변수를 셋팅하는 방법이 있고 또 한 가지는 gcc의 옵션으로 넘겨 주는 방법이 있습니다.

(*) –Ldir 옵션
library 파일을 찾는 디렉토리에 “dir”이란 디렉토리를 추가하라는 옵션입니다.(-Idir 옵션처럼 –L과 dir을 붙여서 적습니다.) 예를 들어 –L/usr/local/mylib 라고 하면 /usr/local/mylib라는 디렉토리에서 library 파일을 찾을 수 있게 됩니다.
== entry 이야기
application을 작성하고 compile, linking 과정이 지나면 실행 파일이 만들어집니다. 그리고 그 실행 파일이 수행될 때는 메모리로 load되어 수행이 시작된다는 사실을 알고 있습니다. 여기서 한가지 의문이 생기는데, “과연 코드의 어떤 부분에서 수행이 시작되는가?”입니다. 답이 너무 뻔한가요? main함수부터 수행된다고 답하시겠죠? 다소 충격적이겠지만 “땡”입니다. main함수부터 수행되지 않고 그전에 수행되는 코드가 존재합니다. 그 먼저 수행되는 코드에서 하는 일은 여러 가지가 있는데 그냥 건너 뛰도록 하겠습니다. 아무튼 그 코드에서 main함수를 호출해 주고 main함수가 return하면 exit 시스템호출을 불러 줍니다. 그래서 main이 맨 처음 수행되는 것처럼 보이고 main이 return하면 프로그램 수행이 종료되는 겁니다. 그럼 그 코드는 어디 있을까요? 시스템에 따라서 다르겠지만 일반적으로 /lib혹은 /usr/lib 디렉토리에 crt1.o라는 이름의 object 파일이 있는데 그 object 파일 안에 있는 _start라는 이름의 함수(?)가 맨 먼저 수행되는 녀석입니다. 결국 보통 application의 entry는 _start함수가 됩니다.
그럼 crt1.o object 파일 역시 같이 linking되어야 겠죠? gcc를 이용해 linking을 수행할 때 command line에 아무 이야기를 해주지 않아도 자동으로 crt1.o 파일이 함께 linking됩니다. 실제로는 crt1.o 뿐 아니라 비슷한 crt*.o 파일들도 같이 linking되는데요. 그렇게 같이 linking되고 있는 object파일들을 startup file이라고 부르는 것 같습니다.(-nostdlib 옵션 설명할 때 잠시 나왔던 startup file이 바로 이 녀석들입니다.)
여기서 한 가지 의문사항이 떠오를만 합니다. 그럼 ld는 _start파일이 entry인지 어떻게 알고, 다른 이름의 함수를 entry로 할 수는 없는걸까요? 의문의 해결은 아래 linking script부분에서 해결될 겁니다.

== 실행 파일에 남아 있는 정보

linking의 결과 실행파일이 생겼는데, 보통 linux에서는 실행파일 형식이 ELF라는 포멧을 가집니다.(linux 시스템에 따라 다를 수 있는지 모르겠네요.) ELF는 Executable and Linkable Format의 약자입니다. 보통 linux 시스템에서의 relocatable object 파일의 형식도 ELF인데요, 실제로 실행파일과 relocatable object 파일과는 조금 다른 형식을 가집니다. 암튼 그건 상식으로 알아두고, 그럼 실행파일에 있는 정보는 무엇일까요?
이제까지의 알아낸 정보들을 모두 종합하면 알 수 있습니다. 우선 실행 파일이라는 녀석이 결국은 relocatable object를 여러 개 쌓아놓은 녀석이므로 원래 relocatable object 파일이 가지고 있던 code와 data 정보는 모두 남아있을 겁니다. 그리고 entry를 나타내는 address가 있어야 수행을 할 수 있겠죠? 또, dynamic linking을 했을 경우 관련된 shared object 정보도 남아있어야 하겠죠.
실행 파일 속에 남아있는 data는 relocatable object에 있는 data처럼 프로그램 수행에 필요한 data가 있고 그냥 실행 파일을 설명하는 정보로서의 data가 있습니다. 예를 들어 –g 옵션을 주고 컴파일한 실행파일에서 디버깅 정보들은 실행과는 전혀 관계 없죠. 따라서 그러한 정보들은 실행 파일 수행시에 메모리에 load될 필요도 없습니다.(load하면 메모리 낭비니깐) 실행 파일 속에 남아있는 code와 data는 relocatable object처럼 특별한 단위로 저장되어 있습니다. ELF 표준에서는 segment라고 부르는데 보통의 경우는 object 파일처럼 section이라는 말이 쓰입니다. reloctable object 파일과 마찬가지로 code는 text section에 저장되고 프로그램 수행 중에 필요한 data가 성격에 따라 나누어져 data, rodata, bss section이란 이름으로 저장되어 있습니다. 그 section단위로 메모리로 load될 필요가 있는지에 대한 flag정보가 있고 각 section이 load될 address(location과정에서 정했겠죠?)가 적혀 있어야 정확하게 loading을 할 수 있습니다.
기타로 symbol reference resolving이 끝났는데도 ELF형식의 실행파일은 보통의 경우 많은 symbol 정보를 그냥 가지고 있는 경우가 있습니다. symbol 정보 역시 수행에는 하등 관계가 없으므로 없애도 되는데, strip이라고 하는 binutils안에 있는 tool로 없앨 수 있습니다.

== linking script
흠 이제 좀 어려운 이야기를 할 차례입니다. Location과정에서 어떤 절대 address를 기준으로 각 section들을 쌓는지, 그리고 entry는 어떤 symbol인지에 대한 정보를 linker에게 알려줄 필요가 있습니다. 보통 application의 경우는 시스템 마다 표준(?, 예를 들어 entry는 _start다 하는 식)이 있는지라 별로 문제될 것은 없는데, bootloader나 kernel을 만들 때는 그런 정보를 사용자가 넘겨 주어야 할 필요가 있습니다. 그런 것들을 ld의 command line argument로 넘길 수도 있지만 보통의 경우는 linking script라고 하는 텍스트 형식의 파일 안에 저장하여 그 script를 참조하라고 알려 줍니다.(아무래도 command line argument로 넘겨 줄 수 있는 정보가 한계가 있기 때문이라고 생각이 듭니다. location과 entry에 관한 내용 중에 ld의 command line argument로 줄 수 있는 옵션이 몇가지 있으나 한계가 있습니다.) ld의 옵션 –T으로 linking script 파일 이름을 넘겨 주게 됩니다.(gcc의 옵션 아님) linux kernel source를 가지고 있는 분은 arch/*/*.lds 파일을 함 열어 보세요. 그게 linking script고, 초기 절대 address하고 section별로 어떻게 쌓으라는 지시어와 entry, 실행 파일의 형식 등을 적어 놓은 내용이 보일 겁니다. 물론 한 줄 한 줄 해석이 된다면 이런 글을 읽으실 필요가 없습니다. 그 script를 한 줄 한 줄 정확히 해석해 내려면 GNU ld manual 등을 읽으셔야 할 것입니다.

== linux의 insmod

device driver 등은 linux kernel module(이하 module) 형식으로 run-time에 올릴 수 있다는 것을 아실 겁니다. module을 run-time에 kernel에 넣기 위해서 사용하는 명령어가 insmod죠.(modprobe도 가능)
이 module이라는 것이 만들어 지는 과정을 잘 살펴 보시면 gcc의 옵션중에 -c옵션으로 컴파일만 한다는 것을 알 수 있습니마. 확장자는 .o를 사용하구요. 그럼 relocatable object 파일이겠네요. 당연히 ELF형식이겠구요.
그럼 이 module이 linux kernel과 어떻게 합쳐질까요? 당연히 linking 과정을 거쳐야 됩니다. 일종의 run-time linking인데요. 당연히 module은 kernel내의 많은 함수와 전역 변수를 참조합니다. 그렇지 않다면 그 module은 linux kernel의 동작과는 전혀 관계 없는 의미 없는 module이 될테니까요. 그럼 참조되고 있는 symbol을 resolving하기 위해서는 symbol의 절대 address를 알아야 겠네요. 그 내용은 linux kernel 내부에 table로 존재합니다. /proc/ksyms라고 하는 파일을 cat해보시면 절대 address와 symbol 이름을 살펴보실 수 있을 겁니다. 살펴보시면 아시겠지만 생각보다 적은 양이죠? 적은 이유는 그 table이 linux kernel source에 있는 전역 symbol의 전부를 포함한 것이 아니라 kernel source 내부나 module 내부에서 EXPORT_SYMBOL()과 같은 특별한 방법으로 선언된(?, 이 선언은 C 언어 문법의 declaration과는 다릅니다.) symbol들만 포함하기 때문입니다. 다른 전역 symbol 들은 module 프로그래밍에 별 필요가 없다고 생각되어 지는 녀석들이기 때문에 빠진 겁니다. 따라서 EXPORT_SYMBOL()등으로 선언된 symbol들만 사용하여 module을 작성해야 합니다.
당연히 linking 과정을 거치기 때문에 앞서 설명드린 linking에서 발생할 수 있는 에러들이 발생할 수 있습니다. 제일 많이 발생할 수 있는 것은 역시 undefined reference 에러일 겁니다. gcc의 에러와는 조금 다른 메시지가 나오겠지만 결국은 같은 내용입니다.

== map 파일
linking 과정을 끝내면 당연히 모든 symbol에 대한 절대 address가 정해지게 됩니다. 그 정보를 알면 프로그램 디버깅에 도움이 될 수도 있으니 알았으면 좋겠죠. ld의 옵션중에 '-Map 파일이름'이라는 옵션이 있는데 우리가 원하는 정보를 문서 파일 형식으로 만들어 줍니다. 그 파일을 보통 map 파일이라고 부르죠. symbol과 address 정보 말고 section에 대한 정보도 있고 많은 정보가 들어 있습니다.
linux kernel을 컴파일을 하고 나면 나오는 결과 중에 System.map이라는 파일이 있는데 이 녀석이 바로 ld가 만들어 준 map 파일의 내용 중에 symbol과 symbol의 절대 address가 적혀 있는 파일입니다. linux kernel panic으로 특정 address에서 kernel이 죽었다는 메시지가 console에 나오면 이 System.map 파일을 열어서 어떤 함수에서 죽었는지 알아볼 수도 있습니다.

== 옵션 넘기기
gcc의 이야기 맨 처음에 gcc는 단순히 frontend로 command line으로 받은 옵션을 각 단계를 담당하고 있는 tool로 적절한 처리를 하여 넘겨준다고 말씀드렸습니다. 위에서 나온 ld의 옵션 -T와 -Map 과 같은 옵션은 gcc에는 대응하는 옵션이 존재하지 않습니다. 이런 경우 직접 ld를 실행할 수도 있고 gcc에게 이런 옵션을 ld에게 넘겨 주라고 요청할 수 있습니다. 하지만 application을 컴파일할 때는 ld를 직접 실행하는 것은 조금 부담이 되므로, gcc에 옵션을 넘기라고 요청하는 방법이 조금 쉽다고 볼 수 있습니다. 그런 경우 사용되는 것이 -Wl 옵션인데 간단히 이용해 보도록 하겠습니다.
$ gcc -o hello -static -Wl,-Map,hello.map hello.c
그럼 hello.map이라는 매우 큰 문서 파일이 만들어 집니다. 한번 살펴 보세요.(-static 옵션을 안 넣으면 살펴볼 내용이 별로 없을까봐 추가했습니다.)
실제로는 -Wl 옵션처럼 as에게도 옵션을 넘겨 줄 수 있는 -Wa와 같은 옵션이 있는데 쓰는 사람을 본 적이 없습니다.

=== 끝
이상 gcc를 사용할 때 필요한 지식에 대해서 간략히 알아보았습니다. 더 많은 정보를 얻고 싶은 분들은 gcc, cpp, as, ld 등의 manpage와 manual을 참조하시길 바랍니다.
Posted by blueguy 트랙백 1 : 댓글 0
컴퓨터 산업을 뒤흔든 한 발표에서 켄 톰슨, 데니스 리치, 브라이언 커니건, 이상 3명은 그들이 만든 유닉스와 C언어가 20년동안이나 살아남은 단지 매우 공들인 만우절 장난이었음을 고백했다. 최근의 유닉스월드 소프트웨어 개발 포름에서 톰슨은 다음과 같은 사실을 고백했다:

"1969 년 AT&T는 GE/Honeywell/AT&T가 공동으로 진행했던 멀틱스 프로젝트를 끝냈습니다. 브라이언과 저는 니콜라우스 워스(역자주:이 이름을 어떻게 읽느냐 하는 것에 대해서는 시비를 걸지말길 바란다. 대부분 자기 맘대로 읽고 있고 난 지금 '영어'를 쳐다보고 있는 것만으로도 머리가 아프다) 교수의 스위스 ETH연구실에서 개발된 파스칼의 초기판을 가지고 일하고 있었고, 그 세련된 단순함과 언어의 막강함에 감명을 받았었죠. 데니스는 당시 막 `Bored of the Rings(반지에 질렸다???)'를 다 읽었던 참이었는데 아시다시피 그 소설은 대문호 톨킨의 `Lord of the Rings(반지의 주인)' (역자주 : 이 책은 환상(fantasy) 소설의 효시로서 톨킨의 뛰어난 고대언어학지식을 기반으로 쓰여진 불후의 명작이다. 울티마, 로그, 영화 코난 등의 환상 소설 류의 게임 및 영화는 아마 이 소설이 없었다면 존재하지 못했을 것이다. 반지전쟁 이라는 이름으로 번역된 바 있다)을 흉내낸 국가 풍자 패러디의 하나였습니다. 장난으로 우리는 멀틱스 환경과 파스칼의 패러디를 하기로 결정했죠.

데니스와 저는 운영체제를 맡았습니다. 우린 멀틱스를 보고 -가능한 아주 복잡하고 암호같이 모호해서- 일반 사용자들은 아예 사용할 엄두를 내지도 못할 새로운 시스템을 설계했습니다. 그리고, 멀틱스의 패러디로 이름을 유닉스로 정했죠. 뭐 일부는 좀 비꼬는듯한 암시를 주기 위한 이유도 있었지만요. 그 다음 데니스와 브라이언은 파스칼을 완전히 뒤섞어 놓은 듯한 언어를 만들고 이름을 'A'라고 했습니다. 그뒤 사람들이 그 언어로 진짜 중요한 프로그램을 개발하려고 시도하고 있다는 것을 알고나서 우리는 재빨리 언어를 암호화해서 더욱 사용하기 어렵게 만들었고 이 언어는 'B'언어를 거쳐 BCPL, 그리고 결국 C가 되었습니다. 우린 다음과 같은 문장을 깨끗하게 컴파일 할 수 있을 때가 되서야 비로서 개발을 중단했습니다:

for(;P("\n"),R-;P("|"))for(e=C; e-; P("_"+(*u++/8)%2))P("|"+(*u/4)%2);

현 대의 프로그래머들이 이렇게 암호같은 문장을 허용하는 개떡같은 언어를 사용할 것이라고는 전혀 생각지 못했습니다. 그건 우리의 상식밖이었죠. 우린 실제로 이걸 소련에 팔아서 소련의 컴퓨터 과학기술을 20년이상 퇴보하게 만들 생각이었거든요.

상상해 보세요. AT&T와 다른 미국회사들이 실제로 유닉스와 C를 사용하려고 발악하기 시작했을 때 우리가 얼마나 놀랐겠는지. 그 기업들이 이 1960년대의 기술적 패러디를 이용해서 근근히 써먹을 만한 응용 프로그램을 개발하기에 충분한 기술을 축적하기까 지는 20년이 걸렸습니다. 하지만, 우린 일반적인 유닉스, C 프로그래머들의 고집에 매우 감명을 받았습니다 (물론 농담입니다. 그게 어디 재정신입니까?).

브라이언, 데니스, 그리고 저는 지난 몇 년간 어떤 일을 하든 애플 매신토시(역자주:이렇게 읽는게 아니었남?!) 에서 파스칼만을사용하고 있습니다. 그리고, 사실 오래전의 우리의 어리석은 장난으로부터 야기된 혼돈과 혼란과 진짜 엉망이 된 프로그래밍에 대해 진정으로 죄의식을 느끼고 있습니다."

AT&T, MICRO$OFT(역자주: 영어로 써야만 MS에 대한 내 감정을 표현할 수가 있었다), 휴렛 패커드, GTE, NCR(역자주 : 금전등록기로 유명하죠), DEC 등을 포함한 주요한 유닉스, C 판매업자들과 고객들은 이때 그 사실에 대한 어떠한 평도 거부했다. 터보 파스칼, 터보 C/C++등의 파스칼과 C툴 개발로 유명한 볼랜드사는 다음과 같이말했다. "우린 이미 몇년동안 이 사실에 대해 의심하고 있었습니다. 그래서 지속적으로 파스칼 제품을 향상시키고 C제품을 개발하는데 더이상의 지원을 하지 않을 생각입니다."

한편, IBM의 대변인은 순간 대책없이 웃어대기 시작하더니 결국 급히 요청한 RS-6000에의 운명에 대한 기자회견을 연기해야만했고 단지 `VM이 _지금_ _정말로_ _곧_ 나올겁니다'라고 말했다. 파스칼, 모듈라2와 오베론 언어를 만든 ETH연구소의 니콜라우스워스(역자주: 따지지 말라니까?!)교수는 조금은 비비꼬은 말로 P.T.Barnum(역자주:I have no idea!)이 맞았다고만 말했다.

최근 나온 관련된 이야기에 믿을 만한 정보통에 따르면 MICRO$OFT의 빌게이츠도 MeSsy-DOS와 원도즈운영체제에 관해 이와비슷한 고백을 곧 할 것이라고 한다. 또한, IBM의 대변인은 Virtual Machine(VM) 제품이 아예 빗나간 국제적인 장난이라는 사실을 부인하기 시작했다.

.. 하지만 그 중에서도 우리의 장난을 꿰뚫어 본 사람이 있었다. 그는 Stroustroup이라는 사람으로 우리의 장난을 꽤 재미있어 했다.

그 리고 그도 우리의 놀이 동참하기 시작하였다. 그는 C를 더욱 복잡하게 만들어 도저히 사용할 수 없도록 만들기 시작했다. 그리하여나온 것이 바로 C++ 라고 하는 언어였다. 그가 C++를 발표하자 모든 사람들은 다시C++를 지원하려고 노력을 하기 시작했고 ANSI 에서도표준화 연구를 위해 대거 인원이 투입되었다. 그러자 Stroustroup은 당황하기 시작하였다. 그래서 더욱 헷갈리도록 하기 위해 탬플릿을추가하였고 예외처리도 넣었다. 그래도 사람들이 계속 따라오려고 노력을 하자 다중상속을 추가하여 컴파일러 구현을 거의 불가능하게 만들었지만 vendor들은 기를 쓰고 이를 구현하려고 노력을 하였던 것이다.

1998. 1. 1., Bjarne Stroustrup는 IEEE Computer지와 인터뷰 했다. 자연스럽게 편집자는 그가 C++을 창조한 당사자로서 7년간의 object-oriented 설계에 대한 종합적인 의견을 보여주리라 생각했다. 인터뷰가 끝날 즈음, Interviewer는 그의 기대 이상의 것을 알게 되었고, 편집자는 '산업계의 이익'을 위해 그 내용을 편집하기로 하였으나, 세상 만사가 그렇듯이 비밀은 없다.....
Posted by blueguy 트랙백 0 : 댓글 0
C를 처음 접하신다는 분들이 예상외로 많은 것 같아서 그냥 생각나는대로 한번 적어 봅니다. ^^;
횡설 수설 하더라도.. 그러려니 해 주세요..

쉽게 한번 풀어 보려는데..
혹시라도 이해가 안되시는 부분이 있으시면 댓글로...

그럼 시작해 볼까요?

컴퓨터라는 놈은.. 0 or 1 밖에 모르는 무지한 기계입니다.
이 놈이랑 소통을 하기 위해서, 우리는 컴퓨터 언어라는 것을 쓰지요.. 그런데 왜 C고 C++이냐...

먼저 간단한 배경을 설명 드리면..
C 언어의 태생은 UNIX라는 운영체제의 탄생과 함께 합니다.
(주: 운영체제란 컴퓨터가 동작하는데 있어서 필수 적인 프로그램입니다. 윈도우즈, 리눅스 같은 놈을 이야기 하죠.)

Bell 연구소에서 UNIX라는 운영체제를 만들기 위하여 기존에 있던 ALGOL-60에서 이어진  B라는 언어를 개량하여 만들어 낸 것이죠.

초기의 UNIX 시스템은 Assembly Language 로만 작성이 되었습니다. 하드웨어를 직접 제어할 수 있는 언어는 당시에는 Assembly 밖에 없었으니까요.  
그 당시 고급언어의 주종을 이루었던 언어로는 COBOL과 FORTRAN이 있었습니다. COBOL은, 회계 및 상업용 소프트웨어의 제작에 강점이 있었고, FORTRAN은 FORmular TRANslator 라는 이름에 걸맞게 수치 계산에 강점을 가지고 있었지요. 70년대의 컴퓨터 언어계의 스타였던 두 언어중 COBOL은 당시에 제작되었던 회계 및 은행 시스템을 구축하는데 사용되었으며, 그 때 제작된 시스템을 현재까지 쓰고 있기 때문에, 1999년에서 2000년으로 넘어갈 무렵 즈음에 밀레니엄 버그 라는 특수를 누리기도 합니다만, 현재 새로이 구축되는 곳에서는 거의 쓰지 않기 때문에, 이미 "고어"가 되어 버렸죠. FORTRAN과 같은 경우에는 수치계산에 특화되어 있고, 기존에 작성된 수 많은 수학 라이브러리 때문에, 현재 까지도 과학  계산 분야에서는 널리 쓰이고 있습니다.

잠깐 옆으로 샜는데..  C언어 이야기를 다시 한번 해 볼까요?
앞서 이야기 한 것 처럼 C는 Dennis Ritchie라는 사람에 의해 UNIX라는 운영체제를 만들기 위해서 만들어진 언어입니다. 운영체제를 만드는데 쓰여졌기 때문에, 기존에는 Assembly로만 가능하던 Low-Level 수준의 하드웨어 제어가 필요하게 되었고, 또한 사용자로 하여금 쉽고 편하게 접근할 수 있어야 한다는 컨셉을 가지고 만들어졌습니다.

당시에는 여러 종류의 CPU들이 막 만들어지고, 보급되기 시작한 시점이었습니다. 그리하여, 개인이 사용할 수 있는 컴퓨터, 즉 PC(Personal Computer)의 보급이 시작되었고, 이른바 "해커"라 불리우는 선지자들에 의해서 컴퓨터는 기존에 사용되던 빠른 계산만이 필요한 분야뿐만이 아니라, 다양한 용도로 사용이 확장되었습니다. 많은 해커들이 자신이 만든 소스 코드를 공개하였으며, 이로 인하여 많은 사람들이 컴퓨터라는 기계가 단순히 계산만을 위한 기계가 아니라, 무언가 새로운 것을 시도할 수 있도록 하는 보조장치로 사용할 수 있다는 것을 깨닫게 됩니다.

또 옆으로 새어 버렸군요.. ^^;

많은 종류의 CPU들이 보급되면서 사람들은 좀 더 편하게 프로그램을 짤 수 있는 방법을 생각하게 됩니다. CPU는 그 제조사 마다, 각각의 명령어 세트를 가지고 있으며, Assembly는 그 각각의 명령어에 1:1로 매칭이 되는 그나마 가독성을 가진 언어인데, 이 조차도 일반 사용자들이 보기에는 난해한 기호일 뿐, 접근하기가 쉽지 않습니다.

Assembly 같은 경우는 CPU에 따라 다른 명령어 체계를 가지고 있기 때문에, 다른 플랫폼(다른 종류의 CPU)를 사용하는 기계에서는 기존에 작성되었던 프로그램들을 모두 재작성해야 하는 번거로움이 있었죠. 그렇기에 사용자가 좀 더 보기 편하게 만들어 지는 언어들이 이른바 "고급 언어"라는 놈이며, 앞서 이야기 한 COBOL, FORTRAN, C 등이 이에 속하게 됩니다. 고급언어로 만들어진 프로그램들은, 각각 "컴파일러" 또는 "인터프리터"라는 녀석들이 CPU 구조에 맞게, Assembly로 변환시켜 줍니다. 그리 되면서 부터 사용자는 CPU에 따라, 다른 프로그램을 작성할 필요 없이 하나의 프로그램 소스를 가지고, 여러 종류의 CPU에 사용할 수 있게 된거죠. 이를 좀 어려운 말로 "이식성"이라고 합니다.

프로그램 소스를 컴파일하여 바이너리를 만드는데, 이 바이너리 라는 놈이 실제로 우리가 실행을 하는 놈이지요. 바이너리는 CPU에 맞는 machine code 즉.. 기계어로 변환되어 있는 놈이기 때문에, 예를 들어 우리가 사용하는 일반적인 PC(Windows를 사용하는 Pentium PC)에서 실행 되는 놈이 APPLE 매킨토시(Mac OS X를 사용하는 이쁜 아이맥?)에서 실행이 되지는 않습니다. 하지만, 같은 소스를 각각의 플랫폼에 맞는 컴파일러를 사용하여 컴파일을 할 경우에 간단한 수정을 통하여 사용할 수가 있게 됩니다. 이를 "소스 수준에서 호환성을 가진다" 라고 이야기 하며, "이식성이 좋다"고 이야기 하죠.

뭐 이야기가 길어졌군요..

다시 C 언어 이야기로 돌아와서 정리를 하면, 1972년 Dennis Ritchie는 B 언어의 성능을 개선시켜 Assembly 의 강력한 기능과 고급 언어의 이식성을 동시에 갖춘 C 언어를 개발하였습니다. 그래서 Assembly로 작성되었던 초기의 UNIX를 C 언어로 다시 작성하였고, 그 결과로 UNIX는 프로그램의 개발과 연구에 유용한 환경을 제공하는 훌륭한 OS로 새롭게 탄생하게 된 것이죠.

C 언어는 1970년대를 지나면서 UNIX와 함게 점차 보급되기 시작하였고, 1980년대 이후로 PC의 대중화에 힘입어 MS-DOS 등에서도 광범위하게 사용되었죠. 그러나 Dennis Ritchie가 처음 C 언어를 설계할 때에 비해 컴퓨터 환경이 많이 변해 비효율적인 면들이 점점 늘어나기 시작했고, 또한 애매한 문법들이 존재했었기 때문에, 각 컴파일러를 만드는 개발사들은 경쟁적으로 수많은 C컴파일러를 발표했습니다.(예: Turbo-C 시리즈를 만들었던 Borland사, Quick-C, MS-C를 만들었던 microsoft사 등). 각 제작사들은 시장의 요구와 필요에 따라 조금씩 언어의 기능을 확장함으로써 C언어에도 많은 변종들이 생겨나게 되었고, 이렇게 되면 서 작성한 컴파일러에 따라 소스 차원의 호환성이 없어지는 문제점이 발생하였으며, 이는 사회적으로 큰 낭비를 초래하게 되었죠. 변화된 환경에 적응하고 경쟁으로 인해 훼손된 이식성을 복구하기 위해서는 표준의 제정이 절실히 필요하게 되었고, 그래서 ANSI는 83년부터 표준 제정작업에 들어가 89년에 표준안을 완성했으며 90년에 ISO에 의해 승인(ISO9899)되었습니다.  이때 제정된 C 표준을 ANSI C(또는 C90)라고 하며 그 이전의 C를 Classic C(K&R C)라고 합니다. ANSI C는 클래식 C에 비해 안전성을 높이고 애매한 기능을 정리하고 새로운 기능을 추가하게 됩니다.

오늘은 여기까지만 한번 정리를 해 보죠..

반응이 괜찮으면.. C++이나. 기타 다른 언어에 대해서도 잠깐 썰을 풀어 볼지도 -_-;

만일 처음 C언어를 공부하시는 분이라면, 제가 여지까지 봐 왔던 C언어 책 중에서 A Book on C (줄여서 ABC 라고도 합니다.)를 추천 합니다. 예제들이 많고, 내용이 그리 어렵지 않아, 천천히 따라가다 보면, 어느 덧 C에 익숙해져 있는, 자신을 보게 될지도.^^;
참고 : http://kangcom.com/common/bookinfo/bookinfo.asp?sku=200006140005


그리고, 나는 C언어는 좀 알기 때문에 귀찮은 문법 같은거는 다시 보고 싶지 않다. 하시는 분들은.. "The C Programming Language" 라는 책이 있습니다. C언어를 처음 만들었던 Ritchie와 Kernighan 이라는 사람이 쓴 책인데.. 압축적으로 많은 내용을 담고 있어서 정리를 하는 용도로는 꽤 괜찮더군요.. ^^;;
참고 : http://kangcom.com/common/bookinfo/bookinfo.asp?sku=199608030003

(참고로 두 책 모두 원서보다 번역본이 쌉니다. ^^; 번역 상태도 좋구요.. )

그럼 여기까지...

Posted by blueguy 트랙백 0 : 댓글 1
성당과 시장’에서 본 오픈소스 개발 모델의 적용

“재미있는 문제를 풀어보고 싶다면 자신에게 재미있는 문제를 찾아 나서는 것부터 시작하라(To solve an interesting problem, start by finding a problem that is interesting to you).” - 성당과 시장, 에릭 레이몬드

우리나라에 리눅스라는 새로운 운영체제가 널리 알려지기 시작한 것은 대략 1999년을 기점으로 생각할 수 있다. 이른바 리눅스 1세대라 불리는 사람들이 노력한 결과 수차례의 공동체 세미나 등 여러 매체를 통한 홍보를 통해 일반 대중들에게 널리 알려질 수 있었으며, 또한 네트워크에 대한 급속한 사용량 증가에 따라 그를 감당할 수 있는 네트워크 서버가 필요하게 되었고, 저비용으로 그러한 시스템을 구축할 수 있는 새로운 운영체제가 필요한 시기였기 때문에 리눅스라는 운영체제가 급속도로 퍼질 수 있었다.
하지만 리눅스가 사람들에게 폭발적인 힘을 가지고서 널리 퍼질 수 있었던 가장 큰 이유는 리눅스의 개발 방법인 오픈소스 개발 모델 때문이 아닐까? 늘어나는 네트워크 사용량과 많은 이유로 발생하는 보안 문제 등의 여러 문제의 해결 방안에 대한 요구가 기존의 운영체제 소프트웨어 업체는 감당하기 힘들 만큼 늘어나게 되었고, 이를 기존의 소프트웨어 개발 방법으로는 엄청난 부하가 걸리게 된다. 이러한 문제들은 리눅스는 오픈소스 개발 모델을 통해 해결해 왔다. 우리가 가장 쉽게 접할 수 있는 오픈소스 개발 모델은 GNU 프로젝트와 리눅스이다.

GNU 프로젝트와 오픈소스 모델의 가치
GNU 프로젝트는 1983년에 리차드 스톨만에 의해 시작되었다. 그는 MIT 대학에서 직업적인 연구 활동을 시작했던 1971년에 자유 소프트웨어만을 사용하는 연구 그룹에서 일하고 있었으며, 그 시절에는 상업적인 컴퓨터 회사들조차도 자유 소프트웨어를 배포하던 때였으므로 프로그래머들은 아무런 제약 없이 서로 협력하며 개발을 진행해 왔다. 하지만 1980년대에 이르러 거의 모든 소프트웨어들은 소유와 독점에 관한 법률에 의해서 제한되었으며, 소유권자들은 소프트웨어의 자유로운 이용을 통한 사용자들의 상호 협력을 그들의 권리를 내세워서 금지시켰다. 바로 이것이 GNU 프로젝트가 시작된 이유이다. GNU 프로젝트가 진행되어 가면서 GCC(GNU C Complier)와 Emacs 등의 소프트웨어들이 개발되었고, 1991년 리누스 토발즈에 의해 리눅스 커널이 공개되면서 완전한 자유 소프트웨어로 구성된 운영체제인 GNU/리눅스와 GNU/Hurd, GNU/Dawrin 등이 현재 계속 개발되어가고 있다.
여기서 오픈소스 개발에 관심을 가진 사람이라면 한번쯤은 접하게 되는 것이 ‘성당과 시장’이라는 에릭 레이몬드의 글이다. 그는 두 가지 형태의 소프트웨어 개발 모델을 제시하였다. 그의 표현을 빌리자면 ‘찬란한 고독 속에서 일하는 몇 명의 도사 프로그래머나 작은 그룹의 뛰어난 프로그래머들에 의해 조심스럽게 만들어지고 때가 되어야 발표할 수 있는 엄숙한 성당 건축 방식’과 ‘일찍, 그리고 자주 발표하여 다른 사람에게 위임할 수 있는 것은 모두 위임하고, 뒤범벅된 부분까지 공개하는 그런 스타일은 서로 다른 의견과 접근 방식이 난무하는 매우 소란스러운 시장 같은 분위기’이다. 이런 시장 바닥에서 조리 있고 안정적인 시스템이 탄생했고(리눅스의 탄생) 성당 건축가들이 상상하기도 힘든 속도로 계속 성장하고 강해지는 것을 보고 의식적으로 자기가 가진 의구심을 시험해 볼 수 있는 기회라고 생각하여 fetchmail(이메일 클라이언트의 일종)을 ‘시장’ 개발 모델을 이용하여 개발했다.
이 개발이 진행되는 동안 에릭 레이몬드는 이른바 리누스의 법칙(“충분히 많은 베타 테스터와 공동 개발자가 있으면 거의 모든 문제들은 빨리 파악될 것이고 쉽게 고치는 사람이 있게 마련이다”)을 이용하여 fetchmail을 완성할 수 있었으며, 이 ‘시장’개발 모델이 충분히 효율적이라는 것을 검증할 수 있었고, 그 결과 fetchmail은 매우 큰 성공을 이룬 프로젝트가 되었다.
‘성당과 시장’이라는 글이 발표된 뒤 7개월이 지나 넷스케이프는 자사의 웹 브라우저인 네비게이터 소스를 공개하기로 발표한다. 상업 소프트웨어 즉, 성당 건축가들이 시장 스타일을 받아들이기로 한 것이다. 이는 소프트웨어 개발 모델에 있어 ‘성당’ 모델 보다는 ‘시장’ 모델이 좀더 효율적이라고 판단되어 내린 결정일 것이다.
한 사람이 어떤 프로젝트를 온전히 해내는 건 불가능하다. 하다못해 버그 패치라도 다른 누군가의 손을 한번 더 거치면 더 좋은 코드가 나올 수 있다는 것이 자유 소프트웨어의 전제이다. 외국에서는 이런 역할 분담이 매우 잘 정착된 것처럼 보여진다. 어딘가 빈 자리가 보이면 자신의 역량이 허용하는 만큼을 추가한다. 굳이 멋진 프로그램으로 만들어 낼 필요는 없다. 단지 자신이 생각했을 때 아쉬웠던 기능을 적당한 형식으로 추가하기만 하면 된다. 그렇게만 해 두면, 그 이상의 것은 나중에 또 누군가가 수정하고 최적화할 것이기 때문이다. 이것이 바로 자유 소프트웨어 개발 모델의 특징이자 장점이기 때문이다. 현재 우리나라 사람들에게 필요한 것이 바로 이런 것이 아닌지 모르겠다. 사람들의 나쁜 습성 중 하나가 감사에는 인색하고, 자신보다 나은 사람이 있으면 칭찬하고 격려하기 보다는 어떻게든 흠집을 내서 깎아내린다는 것이다. 자신의 의견을 밝히는 데는 누구보다 열성적인 사람들이 직접 그 문제를 해결하기 위해 얼마나 노력을 하는지 한번 지켜 볼 문제이다.

필요에 따른 비전과 계획
이러한 ‘시장’ 모델의 소프트웨어 개발방식에 있어 가장 중요한 것은 그 소프트웨어들이 나아갈 방향을 제시하는 것이라 생각이 든다. 에릭 레이몬드는 “소프트웨어에 있어서 모든 좋은 성과들은 개발자 자신의 가려운 곳을 긁는 것으로부터 시작된다”라고 주장했다 이것은 매우 타당한 말이다. 하지만 그것만으로는 부족하다. 프로젝트가 있으면 그 프로젝트를 현명하게 이끌어 나갈 수 있는 무언가가 필요하기 때문이다.
현재 개발되어 있는 대부분의 GNU 소프트웨어의 핵심적인 부분들은 대부분 완전한 자유 운영체제를 만들기 위한 목적으로 개발된 것이다. GNU 소프트웨어는 가려울 때마다 가려운 곳을 긁듯이 그때그때 만들어진 것이 아니라 비전과 계획에 의해 만들어 진 것이다. 예를 들어, GNU C 라이브러리를 개발한 이유는 유닉스 형태의 운영 환경에서 C 라이브러리가 필요했기 때문이고, BASH를 개발한 이유 또한 유닉스 환경이 셸을 요구했기 때문이다.
GNU tar나 리차드 스톨만이 직접 개발한 GNU C 컴파일러와 GNU Emacs 그리고 GDB와 GNU Make도 같은 이유에서 개발된 것들이었다. 물론, 이 모든 것이 어떤 필요에 의해 그 문제를 해결해 나가는 과정상에 있는 것들이지만, 이를 적절하게 조율하고 순서를 정하는 것 역시 매우 중요한 일이다. 프로젝트의 방향을 제시하는 사람은 독창적인 설계를 제시하는 사람이기 보다는 좋은 설계를 알아볼 수 있으며 그 프로젝트를 구성하는 사람들과 의사소통이 잘 되는 사람이라야 할 것이다.

오픈소스 모델이 사회 곳곳에 퍼지기를
개인적인 욕심이 있다면 이러한 과정을 사회적 의미로 분석하여 검증된 사안을 사회 곳곳으로 퍼져나가도록 돕는 학자들의 손길이 좀더 정교하고 깊숙하게 파고들었으면 하는 바람이다. 「컴퓨터 프로그래밍의 심리학」이라는 글에서는 ‘자아를 내세우지 않는 프로그램(egoless programming)’이 극적으로 빠른 개선을 가져왔다고 진단하고 있다. 리눅스의 발전과정이나 fetchmail의 오픈 프로젝트 과정에서 우리가 배울 수 있는 것은 숙련된 기술자들이 가지고 있는 능력을 어떻게 잘 엮어내느냐 하는 것만 있는 것은 아닌 것 같다.
여기에서 밝히는 ‘시장’ 개발 모델은 여러 사람들이 이루어 나가는 단체, 이른바 노동조합, 시민단체, 기업, 심지어는 이 나라를 운영해 나가는 정치 집단 등에게 던지는 메시지가 담겨 있다고 생각한다. 하나의 목적을 위하여 공동으로 노력을 해 나가는 데 있어 가까이서 ‘감 놔라 배 놔라’ 훈수를 두는 숙련된 사람들을 끌어들이는 능력과 공동의 이해를 위해 서로 협력하게 하는 능력을 어떻게 발휘할 것인가 하는 것이다.
Posted by blueguy 트랙백 0 : 댓글 0
 

자유 소프트웨어와 GNU 그리고, GPL(GNU Public License)

필요로 하는 자가 있거든 쓰게 하라. 이는 자비가 아니라 의무이니라.

-- 움베르토 에코, ``장비의 이름'' 중에서, 1980년


근래 들어 "Free Software"라는 말을 자주 듣게 됩니다. 그러나 많은 사람들이 “Free Sofware"라는 단어의 의미에 대해서 제대로 알고 있지는 않습니다. Free software에 대해 처음 알게 된 많은 사람들은 "free software"라는 용어의 "free"가 예상했던 의미와 굉장히 다르다는 것을 알게 되면 무척 당황스러울 것이라 생각합니다.


"Free Software"에서 사용된 ”free"라는 단어의 의미는 무엇에 구속되거나 얽매이지 않는다는 관점에서의 자유를 말합니다. 자유에 해당하는 영어 단어인 "free"는 2가지 뜻을 함께 갖고 있습니다. 첫 번째는 구속되지 않는다는 의미이고, 두 번째는 돈을 지불하지 않아도 된다는 무료(無料, gratis)라는 의미입니다. 영어에서는 이러한 중의적인 의미의 차이 때문에 많은 오해가 발생하기도 하지만 다행히 한국어에서는 자유 소프트웨어의 참된 의미만을 나타내 줄 수 있는 "자유(自由)"라는 단어가 독립적으로 존재하기 때문에 용어상의 문제로 인한 혼동은 없다고 할 수 있습니다. 따라서 GNU/FSF에서 사용하는 자유 소프트웨어를 지칭할 때는 용어상의 혼동이 존재할 수 있는 "프리 소프트웨어"가 아닌 보다 명확한 의미를 가진 "자유 소프트웨어"로 번역하는 것이 바람직합니다. 자유 소프트웨어는 무엇에 구속되거나 얽매이지 않아도 되는 소프트웨어라는 뜻입니다.


여기에서 자유 소프트웨어의 역사 및, FSF(Free Software Foundation)와 GNU에 대해서 알아보고 난 후, 이야기를 진행하도록 하겠습니다. 자유 소프트웨어의 개념은 1980년대 중반 리차드 스톨만(Richard M. Stallman) 및 그가 세운 자유 소프트웨어 재단(Free Software Foundation)과 함께 생겨났습니다. 리차드 스톨만이 MIT에서 직업적인 연구활동을 시작했던 1971년에 그는 자유 소프트웨어만을 사용하는 연구 그룹에서 일하게 되었는데, 그 시절은 상업적인 컴퓨터 회사들 조차도 자유 소프트웨어를 배포하던 때였으므로 프로그래머들은 아무런 제약 없이 서로 협력할 수 있었습니다. 그러나 1980년대에 이르러 거의 모든 소프트웨어들은 소유와 독점에 관한 법률에 의해서 제한되었으며, 소유권자들은 소프트웨어의 자유로운 이용을 통한 사용자들의 상호 협력을 그들의 권리를 내세워서 금지시켰습니다. 그래서 스톨만은 소프트웨어가 컴퓨터 하드웨어로부터 떨어져 나오면서 상업적인 제품으로 팔리기 시작한 것은 지극히 잘못된 일이라고 주장했습니다. 그는 모든 소프트웨어는 무제한 공유되고 무료로 배포돼야 한다는 주장과 함께, MIT를 떠나 FSF를 설립하고, 상업용 소프트웨어와의 전쟁을 선포합니다. 바로 이것이 GNU 프로젝트가 시작된 이유였습니다.


GNU 프로젝트는 초기의 컴퓨터 공동체 안에 충만해 있던 호의적인 상호 협력의 정신을 재건하기 위한 구체적인 실현 방법으로 1983년에 시작되었으며, 이는 독점 소프트웨어의 소유자들이 만든 장벽들을 제거함으로써 상호 협력의 풍토를 다시 부활시키는 것을 그 목적으로 합니다.


모든 컴퓨터 사용자들은 운영체제가 필요합니다. 만약 자유롭게 사용할 수 있는 운영체제가 없다면 컴퓨터를 사용하고자 하는 모든 사람들은 독점적인 상용 운영체제를 이용할 수밖에 없을 것입니다. 따라서, 자유 소프트웨어에 대한 첫번째 과제는 자유 운영체제를 만드는 것이었습니다.


운영체제는 단순히 커널만을 의미하는 것이 아닙니다. 운영체제에는 컴파일러와 문서 편집기, 스프레드 쉬트, 메일 소프트웨어와 같은 다양한 종류의 소프트웨어들이 통합되어 있어야 합니다. 따라서, 완성된 운영체제를 만든다는 것은 무척이나 방대한 작업이며 많은 세월이 필요한 일입니다.


이러한 운영체제를 유닉스와 호환되도록 결정했던 이유는 유닉스의 설계 방식에 대한 전반적인 우수성과 이식성이 이미 충분히 증명되었기 때문이고, 호환성을 통해서 많은 유닉스 사용자들이 보다 쉽게 GNU 환경으로 적응할 수 있도록 하기 위해서 입니다.


자유 운영체제에 대한 GNU 프로젝트의 첫 번째 계획은 1990년대에 와서 실현되었습니다. 커널을 제외한 주요 부분들을 새롭게 작성하고 취합하던 과정에서 GNU/FSF는 리누스 토발즈(Linus Tovalds)에 의해서 리눅스가 개발되고 있다는 사실을 알게 되었고, 리눅스는 곧 GNU와 합류하게 되었습니다. 자유 소프트웨어인 리눅스 커널과의 결합으로 GNU 시스템은 독립된 운영체제로서의 완성된 모습을 갖출 수 있었고 슬랙웨어와 데비안, 레드햇과 같은 GNU 시스템에 기반한 많은 운영체제들이 이제는 수십만에 달하는 사용자를 갖게 되었습니다.


그럼 다시 자유 소프트웨어의 이야기로 돌아가 보도록 하겠습니다. 우리가 소프트웨어를 사용하는데 있어서 돈을 내지 않아도 된다는 것은 전혀 자유로운 것이 아닙니다. 우리가 가지고 있는 소프트웨어를 다른 사람에게 배포하거나 새로운 기능을 추가하고자 할 때, 이를 금지당할 수도 있습니다. 무료로 배포되는 대부분의 소프트웨어들을 보면, 관련 제품을 판촉하거나, 사업상의 전략으로 경쟁자를 시장에서 몰아내거나, 영업을 방해하기 위한 한 수단으로 사용되는 경우가 많습니다. 앞서 스톨만의 경우와 같이, 이렇게 배포된 소프트웨어가 영구적으로 무료 소프트웨어로 남아 있을 것이라고는 그 누구도 장담하지 못합니다.


진정한 자유 소프트웨어라면, 항상 자유로워야 합니다. 일반적으로 배포된 자유 소프트웨어는 비(非) 자유 소프트웨어로 변경될 수도 있습니다. 그렇게 되면, 기존의 자유 소프트웨어상태일 때, 개선되었던 많은 부분과 많은 가능성 들이 사라질 수도 있습니다. 자유 소프트웨어가 진정한 자유 소프트웨어로 자유롭게 남아 있기 위해서는 소프트웨어는 저작권(copyright)과 라이센스가 반드시 명시되어야 합니다.


좀 더 간단하게 이야기 하면, 소프트웨어는 자유로울 수도 있고, 그렇지 않을 수도 있습니다. 하지만, 실제로는 이렇게 쉽게 구분지을 수가 없습니다. 많은 사람들이 “이 소프트웨어는 자유 소프트웨어이다”라고 이야기 할 때, 이것이 어떤 의미를 가지는 있는지는 그 소프트웨어가 가지고 있는 라이센스를 읽어보면 좀 더 자세히 알 수 있습니다.


GNU/FSF에서는 어떤 프로그램이 "자유 소프트웨어"인가 아닌가를 판단할 수 있는 기준으로 다음과 같은 4가지 조건을 규정하고 있는데, 이 조건들을 모두 충족시키는 소프트웨어를 자유 소프트웨어라고 할 수 있습니다.


      자유 0: 프로그램을 어떠한 목적을 위해서라도 실행할 수 있는 자유

      자유 1: 프로그램의 작동 원리를 연구하고, 이를 자신의 필요에 맞게 변경시킬 수있는 자유

      자유 2: 이웃을 돕기 위해서 프로그램을 복제하고 배포할 수 있는 자유

      자유 3: 프로그램을 향상시키고 이를 공동체 전체의 이익을 위해서 다시 환원시킬 수 있는 자유


위의 4가지 조건들은 자유 소프트웨어를 명확하게 규정하기 위한 것이며, 이러한 기준을 만족하는 소프트웨어를 보호하기 위한 법률적 장치가 GNU GPL이라고 불리는 사용권 허가 방식입니다. 흔히 말하는 카피레프트라는 단어는 특정한 소프트웨어를 자유 소프트웨어로 만드는 방식들을 통칭하는 일반적인 명칭이며 카피레프트가 성립하기 위해서는 "자유2"에 해당하는 프로그램에 대한 배포가 일어날 경우에 원시 코드의 개작 여부에 상관없이 배포 기준을 그대로 유지시켜야 합니다. 예를 들면, 자신이 얻은 원시 코드를 개작해서 새로운 기능을 추가하거나 보다 확장된 프로그램을 만들었다 하더라도 최초의 원시 코드에 설정된 사용권 허가 방식을 변경하거나 새로운 제한 사항을 추가하지 않고 원래의 내용을 그대로 유지시켜야 합니다. 따라서 카피레프트로 설정된 프로그램을 기반으로 만들어진 프로그램들은 모두 카피레프트가 될 수밖에 없는 순환적인 구조를 갖게 됩니다. 자유 소프트웨어의 범위와 체계가 현재와 같이 커질 수 있었던 가장 근본적인 외적 이유는 카피레프트가 이러한 순환적이고 팽창적인 구조를 갖고 있기 때문입니다.


GNU GPL은 이러한 카피레프트를 실제로 구현한 방법 중 하나이며, 만약 새로운 사용권 허가 문서를 만들 경우에 카피레프트에 맞게 설계된다면 이는 GNU GPL과 같이 카피레프트를 담보할 수 있는 실제적인 법률 장치 중의 하나가 되는 것입니다. 따라서 반드시 GNU GPL을 사용하지 않더라도 자유 소프트웨어에 대한 4가지 조건들을 만족시키면서 소프트웨어를 사용하거나 배포할 수 있도록 허용한다면 그것은 GNU/FSF가 견지하고 있는 "자유 소프트웨어" 외적인 형식면에서 호환된다고 말할 수 있으며, 이러한 GPL 호환 사용권 허가 방식들은 GPL과 충돌하지 않기 때문에 함께 혼용될 수 있습니다. 여기에 해당하는 것으로는 LGPL, X11 라이센스, Zlib 라이센스, Python 라이센스, Expat 라이센스 등이 있는데 여러가지 사용권 허가 방식들과 이들 간의 호환 여부에 대한 간략한 설명은 http://www.gnu.org/philosophy/license-list.html 페이지를 통해 참고할 수 있습니다. 이 글은 GNU/FSF가 카피레프트의 방법으로 공식적으로 사용하고 있는 GPL에 집중하고 있기 때문에 GPL 호환 및 비호환 사용권 허가 방법들에 대한 설명은 생략합니다.


자유 소프트웨어를 가름하는 4가지 기준에 따라 다양한 종류의 소프트웨어들을 다음과 같은 다이어그램으로 분류해 볼 수 있습니다.



그림 1) 자유 소프트웨어가 되기 위한 4가지 조건에 의한 소프트웨어의 분류


GPL 소프트웨어 - GPL'ed software


GPL을 사용권 허가 방법으로 사용하고 있는 소프트웨어를 지칭합니다. GNU/FSF가 배포하고 있는 거의 대부분의 소프트웨어들은 GPL 소프트웨어입니다.



카피레프트 소프트웨어 - Copylefted software


GNU/FSF가 정의하고 있는 자유 소프트웨어 대한 4가지 조건을 충족시키는 소프트웨어 중에서 카피레프트 방식의 배포가 이루어지는 소프트웨어를 지칭합니다. 즉 원시 코드의 개작 여부에 관계없이 원래의 배포 기준을 그대로 유지시켜야하는 소프트웨어를 말합니다. GPL은 카피레프트를 구현하는 방식이기 때문에 GPL 소프트웨어는 카피레프트 소프트웨어의 부분 집합이 됩니다.


공용 소프트웨어 - Public domain software


저작권자가 저작권을 명시적으로 포기했거나 저작권자를 알 수 없는 공개된 소프트웨어를 지칭합니다. 카피레프트는 저작권을 인정하는 방식이기 때문에 공용 소프트웨어가 카피레프트에 포함되지는 않지만, 저작권이 없음으로 인해서 사용상의 어떠한 제한도 존재하지 않기 때문에 자유 소프트웨어처럼 사용될 수 있습니다. 이 말은 공용 소프트웨어가 독점 소프트웨어로 사용될 수도 있다는 것을 함축합니다. [그림 1]에서는 보다 명확한 분류를 위해 공용 소프트웨어를 자유 소프트웨어의 부분 집합으로만 표시했지만, 경우에 따라서 독점 소프트웨어의 부분 집합으로 표시될 수도 있습니다. GNU/FSF에서는 사용자에게 유리한 방향으로 정의하는 기준을 적용해서 공용 소프트웨어를 `카피레프트 이외의 자유 소프트웨어'로 분류하고 있습니다. [그림 1]에서 자유 소프트웨어를 구성하는 부분 집합 중 카피레프트 소프트웨어의 여집합이 `카피레프트 이외의 자유 소프트웨어'입니다.


XFree86 형태의 소프트웨어 - XFree86 style software


공용 소프트웨어와 마찬가지로 `카피레프트 이외의 자유 소프트웨어' 중 하나입니다. 대표적인 그래픽 유저 인터페이스인 X 윈도우 시스템에 적용되는 사용권 허가 방식이 여기에 해당하는데, 이 방식에서는 개작과 재배포가 허용되지만, 카피레프트에는 허용되지 않는 추가적인 제약의 설정이 가능합니다. 이 말은 이러한 형태의 소프트웨어를 이용해서 독점 소프트웨어를 만드는 것이 가능하다는 뜻입니다.


오픈 소스 소프트웨어 - Open Source software


오픈 소스에 대한 정의를 충족시키는 소프트웨어를 지칭합니다. 오픈 소스에 대한 정의는 http://korea.gnu.org/people/chsong/copyleft/osd-korean.html 문서를 통해 참고할 수 있습니다. 예외가 존재하기는 하지만, 명칭상의 차이를 제외하면 일반적으로 오픈 소스 소프트웨어는 자유 소프트웨어와 동일하다고 볼 수 있습니다.3


셰어웨어 - Shareware


일정한 기간 동안 무료로 사용할 수 있게 하는 등의 부분적인 제한을 설정해서 배포되지만, 계속해서 사용하기 위해서는 비용을 지불해야 하는 소프트웨어를 지칭합니다. 셰어웨어는 상업적인 목적을 위한 마케팅 방법의 하나로 대부분 원시 코드가 제공되지 않거나 배포상의 제약이 설정되므로 독점 소프트웨어에 속합니다.


프리웨어 - Freeware


셰어웨어와 유사한 형태의 소프트웨어로, 일반적으로 배포는 허용하지만 개작은 허용하지 않는 경향을 갖고 있습니다. 프리웨어라는 표현에 특히 주의해야 하는 것은 자유 소프트웨어가 아님에도 불구하고 유사한 어감의 단어를 사용함으로써 사용자들을 혼동시키고 있기 때문입니다. GNU/FSF에서는 프리웨어라는 표현을 사용하지 않고 있으며, 프리웨어는 결코 자유 소프트웨어가 아닙니다.


비공개 소프트웨어 - Closed software


(오픈 소스 소프트웨어에 대한 상대적인 표현으로) 원시 코드가 공개되지 않는 소프트웨어를 지칭합니다. 원시 코드가 공개되지 않는 것은 `자유1'과 `자유 3'을 성립시킬 수 없기 때문에 비공개 소프트웨어는 독점 소프트웨어에 속합니다.


독점 소프트웨어 - Proprietary software


원시 코드가 공개되지 않거나 프로그램에 대한 복제 및 배포가 금지되는 등의 자유 소프트웨어에 대한 4가지 조건이 충족되지 않는 소프트웨어를 지칭합니다. [그림 1]에 의하면 무료로 다운 받을 수 있는 소프트웨어의 영역에 독점 소프트웨어의 일부가 포함되어 있기도 한데, 무료로 다운 받을 수 있다고 하더라도 사용과 복제 및 배포 상의 제약이 있다면 이는 자유 소프트웨어가 될 수 없기 때문에 독점 소프트웨어에 포함됩니다. Acrobat Reader나 Real Player와 같은 프로그램이 이러한 형식을 갖는 대표적인 예라고 할 수 있습니다. 이와 반대로 무료로 다운 받을 수 있는 소프트웨어의 영역에 자유 소프트웨어의 일부가 제외되어 있는 이유는 자유 소프트웨어라고 해서 무조건 무료로 제공되어야 한다는 조건이 자유 소프트웨어에 대한 4가지 조건에는 포함되어 있지 않기 때문입니다. 따라서 A라는 기업이 GPL로 사용권 허가를 설정한 프로그램을 새롭게 창작한 뒤에 이를 유료로 판매하면서 제품을 구입하는 사람에게만 원시 코드를 제공하는 방식으로 사업을 영위한다고 해도 이는 GPL 위반이 되지 않습니다. GNU GPL은 카피레프트를 실제로 구현한 것이기 때문에 카피레프트 방식을 충족시키기만 하면 구체적인 배포 형태에 대한 제한은 두지 않습니다.


상용 소프트웨어 - Commercial software


[그림 1]에는 언급되어 있지 않지만, 소프트웨어의 종류를 구분할 때 빼놓을 수 없는 중요한 형태의 하나가 바로 상용 소프트웨어입니다. 상용 소프트웨어는 판매 수익을 통해 돈을 벌기 위한 목적으로 만들어진 소프트웨어를 말합니다. 자유 소프트웨어에서 사용된 "자유"는 무료를 의미하는 금전적인 측면의 자유가 아니라 구속되지 않는다는 관점에서의 자유라고 이미 언급한 바 있습니다. 따라서 자유 소프트웨어를 유료로 판매하는 것은 전혀 문제가 되지 않기 때문에 경우에 따라서 자유 소프트웨어가 상용 소프트웨어가 될 수도 있습니다. 또한 독점 소프트웨어라고 해서 모두 상용 소프트웨어가 되는 것도 아닙니다. A라는 기업에 의해서 만들어진 인터넷 유해 정보 차단 프로그램이 인터넷을 통해서 무료로 배포되고 있다고 가정해 봅시다. A가 프로그램의 원시 코드를 공개하지 않고 제3자에 의한 개작과 배포를 허용하지 않는다고 해도 프로그램을 개발한 목적 자체가 공익을 위한 것이었고 또한 프로그램을 전액 무료로 배포하고 있다면 이 프로그램을 상용 소프트웨어로 볼 수 없습니다. 그러나 이 프로그램이 자유 소프트웨어의 조건들을 충족시키지는 못하기 때문에 자유 소프트웨어로 간주될 수도 없습니다. 이 프로그램을 굳이 [그림 1]의 기준에 따라 분류해 본다면 `비상용 독점 소프트웨어'가 된다고 할 수 있습니다. 따라서 상용 소프트웨어는 자유 소프트웨어와 독점 소프트웨어를 가늠하는 구분이 아니라 단지 돈을 받고 판매하는가 아닌가에 근거한 구분 방식입니다. 자유 소프트웨어와 독점 소프트웨어는 경우에 따라서 모두 상용 소프트웨어와 비상용 소프트웨어가 될 수 있습니다.


위의 분류 형태를 가만히 살펴보면, 그 기준이 저작권이 누구에게 있는가에 초점을 맞추고 있는 것이 아니라 프로그램의 원시 코드가 제공되는 지와 개작과 배포를 허용하는지의 여부에 있다는 것을 알 수 있습니다. 이는 소프트웨어의 분류가 다른 사람들의 사용을 염두에 둔 기준을 바탕으로 한 것이기 때문입니다.


일반적으로 라이센스(License)라는 말로 불리는 "사용권 허가" 또는 "사용 허가"는 정해진 범위 안에서의 사용을 허가해 주는 저작권자로부터의 일방적인 승인입니다. 이러한 상황에서는 사용자가 저작권자의 승인 내용에 포함된 조건에 동의할 경우에만 사용 계약이 성립됩니다. GNU GPL(GNU General Public License)은 마지막에 포함된 라이센스라는 단어가 말해주듯이 저작권 설정 문서가 아닌 사용권 허가 문서입니다. 새로운 저작물을 창작하게 되면 명시적인 저작권 설정 절차를 거치지 않는다고 하더라도 창작과 함께 저작권이 자동적으로 인정됩니다.   컴퓨터 프로그램 또한 저작권법에 의해서 이러한 권리가 자동으로 인정되는데, 미국과는 달리 한국에서는 "컴퓨터 프로그램 보호법"이라는 별개의 특별법이 적용됩니다. 그러나 실제적으로는 컴퓨터 프로그램의 특수성을 감안해서 법률의 이름만을 따로 만들 뒤에 세부적인 조항을 저작권법의 범위 안에서 컴퓨터 프로그램에 맞게 적용하도록 입법한 것이기 때문에 저작권법의 틀에서 벗어나지 않습니다.


따라서, GNU GPL은 프로그램을 창작한 저작자로부터 불특정 다수의 사람들에게 해당 프로그램을 GNU/FSF가 정의하고 있는 자유 소프트웨어의 범위 안에서 마음껏 사용할 수 있도록 합법적으로 허용해 주는 사용권 허가 문서라고 할 수 있습니다.


마지막으로, 자유 소프트웨어는 어느 한계까지 발전할 수 있을까요? 특허 제도와 같은 법률적 강제 장치가 자유 소프트웨어를 전면적으로 금지시키지 않는 한 그 가능성은 무한하다고 생각하고 있습니다. GNU/FSF의 궁극적인 목표는 컴퓨터 사용자들이 희망하는 어떠한 형태의 작업도 완벽하게 실현시킬 수 있는 자유 소프트웨어를 제공하는 것이며, 그것은 곧 소프트웨어에 있어서 독점이라는 해악을 영원히 사라지게 만들기 위한 것입니다.


Posted by blueguy 트랙백 0 : 댓글 0