[OS]/Embedded

[펌] gcc 이야기(5)

하늘을닮은호수M 2005. 9. 6. 22:20
반응형

gcc 이야기(5)

글쓴이 : holelee (2002년 05월 13일 오후 07:29)

[ 임베디드강좌/이규명 ] @ KELP

=== 시작하기에 앞서
gcc라는 컴파일러를 이용하여 C 언어 프로그램을 컴파일 하기 위해서 알아야 할 기본적인 옵션 및 발생할 수 있는 에러에 대해 초보자를 대상으로 작성된 글입니다. 고급 사용자라면 읽으실 필요가 없을 것으로 생각됩니다. Architecture dependent한 부분은 가능한 배제하였습니다. 단 gcc의 사용은 Linux를 비롯한 Unix 계열의 OS에서 사용된다는 가정을 하였습니다. 또한 이 글에 대한 모든 내용은 본인이 사용하고 있는 alzza linux 6.1에서 gcc-2.91.66을 바탕으로 하고 있습니다. gcc에 대하여 좀더 많은 것을 알고 싶으신 분은 gcc manpage와 gcc manual을 참조하시기 바랍니다.
이 글에 나오는 모든 내용이 정확하다고 할 수는 없으며, 그 글에 나오는 내용을 따라 gcc를 사용하는데 있어서 문제가 발생할 경우, 본인은 책임을 지지 않습니다. 이 글에 대한 저작권은 본인(holelee)에게 있습니다. 글에 대해 잘못된 점이나 지적할 사항이 있으신 분은 저 위의 “holelee”를 클릭하여 메일을 보내 주시기 바랍니다.


=== 시작 및 복습
개인적 사정으로 linking과정의 글을 적는데 조금 오래 걸렸습니다. 그리고 alzza linux 6.1 리눅스 머신을 더 이상 사용할 수 없게 되었습니다. 이 글에 있는 내용은 redhat 7.2 리눅스 머신을 기준으로 작성되었습니다.(어짜피 별 차이도 없습니다.)
마지막 linking과정입니다. Linking 과정의 기본에 대해서는 gcc 이야기(4)에서 맛보기로 살펴보았습니다. 이제 좀 더 구체적인 이야기를 살펴보도록 하겠습니다. 조금은 긴 이야기가 될 것 같아 지루할 것도 같지만, 많은 생략을 통해 핵심만 간단히 적도록 노력하겠습니다.

=== 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
gcc 이야기(4)의 마지막 부분에 나오는 예제인 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가 결정되지 않았다는 의미로 사용된다고 gcc 이야기(4)에서 말씀드렸죠.(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별로 쌓게 됩니다. gcc 이야기(4)에서 잠시 나왔던 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 파일을 찾을 수 있게 됩니다.

가야 할 길은 한참 남았는데 벌써 너무 길어진 것 같습니다. 다음 글에서 남겨진 부분에 대해서 말씀 드리도록 하겠습니다. 남겨진 부분도 꽤나 길 것으로 생각됩니다…쩝..

반응형

'[OS] > Embedded' 카테고리의 다른 글

kernel compile  (0) 2005.11.16
[펌] gprof를 사용한 프로파일링  (0) 2005.09.09
[펌] gcc 이야기(6)  (0) 2005.09.06
[펌] gcc 이야기(4)  (0) 2005.09.06
[펌] gcc 이야기(3)  (0) 2005.09.06