« Previous : 1 : 2 : 3 : 4 : 5 : 6 : Next »

Contents

1 메시지 큐란
2 메시지큐의 생성, 사용, 제어
3 메시지큐 생성
4 메시지큐에 데이타 쓰기
5 메시지큐의 데이타 가져오기
6 예제를 통해 알아본 메시지큐
7 메시지큐의 제어
8 정리
9 참고문서

1 메시지 큐란

메시지큐는 메시지를 queue 데이타 구조 형태로 관리한다. 는 선입선출(먼저 들어간게 먼저 나오는) 데이타 구조를 말하며, 보통의 은행창구 혹은 일반적인 줄서기를 생각하면 된다. 이것은 흔히 FIFO(First in First Out)라고 불리운다(IPC 의 FIFO 설비와 혼동하지 말자). 이것과 반대되는 데이타 구조를 stack 이라고 하며, 큐와 반대로 가장 나중에 들어온게(가장 최근데이타) 먼저 나오는 형태를 가진다.

일반적인 배열을 접근방법에 따라 특수하게 분류한것이라고 생각하기 바란다.

메시지큐는 커널에서 전역적으로 관리되며(이를테면 커널 전역변수형태로), 모든 프로세스에서 접근가능하도록 되어있으므로, 하나의 메시지큐 서버가 커널에 요청해서 메시지큐를 작성하게 되면, 메시지큐의 접근자(식별자)를 아는 모든 프로세서는 동일한 메시지큐에 접근함으로써, 데이타를 공유할수 있게 된다.

메시지큐의 IPC로써의 특징은 다른 공유방식에 비해서 사용방법이 매우 직관적이고 간단하다라는데 있다. 다른 코드의 수정없이 단지 몇줄만의 코드를 추가시킴으로써 간단하게 메시지큐에 접근할수 있다.

또한 뒤에서 자세히 다루겠지만 메시지의 type 에 의해서 여러종류의 메시지를 효과적으로 다룰수 있는 장점을 가지고 있다. 여러개의 프로세스가 하나의 메시지큐를 엑세스 할때, 각 메시지에 type 를 줌으로써 각 프로세스에게 필요로하는 메시지만을 가져가게 할수 있는 상당히 편리한 기능을 제공한다.

또한 구조체를 몽땅 넘길수 있고, 이는 데이타의 사용을 매우 편하게 만들어준다.

2 메시지큐의 생성, 사용, 제어

메시지큐를 생성하고, 이를 이용 및 조작하기 위해서 Unix 시스템은 다음과 같은 함수들을 제공한다. 앞으로의 설명은 아래의 함수들을 기준으로 이루어지게 될것이다.

#include <sys/types.h> 
#include <sys/ipc.h> 
#include <sys/msg.h> 
 
int msgget (key_t key, int msgflg) 
int msgsnd (int msqid, struct msgbuf *msgp, size_t msgsz, int msgflg) 
ssize_t msgrcv (int msqid, struct  msgbuf  *msgp,  size_t msgsz,  
                long msgtyp, int msgflg)  
 
Unix 커널은 메시지큐 정보를 유지하기 위해서 msqid_ds 라는 별도의 구조체를 유지한다. msqid_ds 구조체는 /usr/include/bits/msq.h 에 선언되어 있으며 대충 다음과 같은 구조를 가진다

이건 linux os 기준이며, Unix 에 따라 약간씩 다를수 있다
struct msqid_ds 
{ 
struct ipc_perm msg_perm; /* structure describing operation permission */ __time_t msg_stime; /* time of last msgsnd command */ unsigned long int __unused1; __time_t msg_rtime; /* time of last msgrcv command */ unsigned long int __unused2; __time_t msg_ctime; /* time of last change */ unsigned long int __unused3; unsigned long int __msg_cbytes; /* current number of bytes on queue */ msgqnum_t msg_qnum; /* number of messages currently on queue */ msglen_t msg_qbytes; /* max number of bytes allowed on queue */ __pid_t msg_lspid; /* pid of last msgsnd() */ __pid_t msg_lrpid; /* pid of last msgrcv() */ unsigned long int __unused4; unsigned long int __unused5; };
최초 msgget 를 이용해서 커널에 메시지큐를 요청하면, 커널은 해당 메시지큐를 위해 메모리를 할당하고, 메모리 관리와 그밖의 정보 관리를 위해 위의 구조체를 세팅하게 된다.

3 메시지큐 생성

메시지큐의 생성과 기존에 있던 메시지큐의 참조는 msgget(2) 를 이용해서 이루어진다. 첫번째 아규먼트인 key 는 kernel 에서 유일한 메시지큐를 만들고 참조하기 위해서 사용하는 식별번호이며, msgflg 는 메시지큐를 어떻게 생성하고 참조할지 행동양 식을 정해주기 위한 아규먼트이다. key 는 적당하게 유일한 int 형의 숫자를 정해주면 된다. msgflg 에는 IPC_CREAT와 IPC_EXCL등의 시작동작을 정해줄수 있으며, 퍼미션을 지정해 줄수도 있다.
IPC_CREAT
메시지큐를 새로 생성하기 위해서 사용한다. 만약 기존에 key 로 생선된 메시지큐가 있다면 해당 메시지큐의 식별자를 되돌려준다.
IPC_EXCL
IPC_CREAT 와 함께 쓰이며, IPC_EXCL이 지정되어 있을경우 이미 key 로 존재하는 메시지큐가 있다면, -1 을 리턴하고 errno 를 세팅한다.

msgget 는 성공할경우 메시지큐에 접근할수 있는 int 형의 메시지큐 식별자를 되돌려주며, 이후로는 이 메시지큐 식별자를 통해서 필요한 작업을 하게 된다.

4 메시지큐에 데이타 쓰기

메시지를 보내기 위해서는 msgsnd(2) 를 사용한다. 첫번째 아규먼트는 msgget 를 통해서 얻어온 메시지큐 식별자이며, 2번째 아규먼트는 메시지큐에 넘기고자하는 구조체, 3번째 아규먼트는 2번째 아규먼트인 구조체의 크기, 마지막 아규먼트는 메시지전달 옵션으로 봉쇄할것인지 아니면 비봉쇄로 메시지를 결정하기 위해서 사용된다.

2번째 아규먼트가 메시지큐로 전달할 메시지라고 했는데, 이것은 구조체로 전달 되며, 다음과 같은 모습을 가지게 된다.
struct msgbuf 
{ 
    long mtype; 
    char mtext[255]; 
} 
 
위의 모습은 메시지 구조체의 매우 전형적인 모습으로 사실멤버변수는 필요에 따라서 얼마든지 변경될수 있다. 다만 long mtype 만이 필수요소이다.

mtype 는 메시지의 타입으로 반드시 0보다 더큰 정수이여야 한다. 우리는 이 mtype 를 각각 다르게 줌으로써, 특정 프로세스에서 특정 메시지를 참조할수 있도록 만들수 있다. 예를 들어 A 라는 프로세스가 A 라는 메시 타입을 참조해야 하고 B 는 B 로 참조하도록 만들어야 한다면, msgbuf 를 만들때, mtype 에 A 은 1 B 은 2 이런식으로 메시지 타입을 정의 하고 A 는 mtype 가 1인것을 B는 mtype 이 2인것을 가지고 가도록 만들면 된다.

Upload new Attachment "queue.png"

위의 그림에서 처럼 mtype 을 이용해서 자신이 원하는 메시지에만 선택 적으로 접근이 가능하다. 이특성을 잘 이용하면 매우 유용하게 사용할수 있을것이다.

msgsz 은 구조체의 크기이니 그냥 넘어가고, msgflg 에 대해서 설명하겠다. msgflg 에는 IPC_NOWAIT를 설정할수 있으며 이 값을 이용해서 봉쇄형으로 할것인지 비봉쇄형으로 할것인지 결정할수 있다. IPC_NOWAIT를 설정하면, 메시지가 성공적으로 보내지게 될때까지 해당영역에서 봉쇄(block)되며, 설정하지 않을경우 에는 바로 return 하게 된다.

5 메시지큐의 데이타 가져오기

데이타는 msgrcv(2) 함수를 이용해서 가져올수 있다. 1번째 아규먼트는 메시지큐 식별자이며, 2번째가 가져올 데이타가 저장될 구조체, 3번째는 구조체의 크기, 4번째는 가져올 메시지 타입, 5번째는 세부 조종 옵션이다.

다른것들은 굳이 설명할 필요가 없는 간단한 것들이고, 다만 4번째 메시지 타입인 msgtyp에 대해서 상세히 설명하고, msgflg 를 간단히 설명하는 정도로 넘어가도록 하겠다. 우리는 메시지를 보낼 구조체를 만들때 mtype 라는것을 정의 해서, 메시지를 분류해서 접근할수 있도록 한다는것을 알고 있다. 메시지를 가져올때는 바로 msgtyp 를 통해서 자기가 원하는 msgtyp 의 메시지 구조체에 접근할수 있게 된다.
msgtyp == 0
메시지 큐의 첫번째 데이타를 돌려준다.
msgtyp > 0
메시지의 mtype 가 msgtyp 와 같은 첫번째 데이타를 돌려준다.
msgtyp < 0
메시지의 mtype 이 msgtyp 의 절대값보다 작거나 같은 첫번째 데이타를 돌려준다.

msgflg 는 msgrcv 의 메시지 가져오는 형태를 봉쇄로 할것인지 비 봉쇄로 할것인지 지정하기 위해서 사용한다. IPC_NOWAIT 를 설정할경우 가져올 메시지가 없더라도 해당 영역에서 봉쇄되지 않고 바로 error 코드를 넘겨주고 리턴한다.


6 예제를 통해 알아본 메시지큐

여기에는 총 2개의 예제프로그램이 만들어진다. 하나는 메시지큐 생산자로써, 메시지큐를 생성하고 메시지를 만들어서 메시지큐에 보내는(msgsnd) 일을 하고 다른 하나는 소비자로써 메시지큐에 있는 데이타를 받아오는 일을 한다. 다음은 메시지큐 생산자 이다.
#include <sys/types.h>  
#include <sys/ipc.h>  
#include <sys/msg.h>  
#include <sys/stat.h>  
 
struct msgbuf 
{ 
    long msgtype; 
    char mtext[256]; 
    char myname[16]; 
    int  seq; 
}; 
 
int main() 
{ 
    key_t key_id; 
    int i; 
    struct msgbuf mybuf, rcvbuf; 
 
    key_id = msgget((key_t)1234, IPC_CREAT|0666); 
    if (key_id == -1) 
    { 
        perror("msgget error : "); 
        exit(0); 
    } 
 
    printf("Key is %d\n", key_id); 
 
    memset(mybuf.mtext, 0x00, 256);  
    memset(mybuf.myname, 0x00, 16);  
    memcpy(mybuf.mtext, "hello world 4", 13); 
    memcpy(mybuf.myname, "yundream", 8); 
    mybuf.seq = 0; 
    i = 0; 
 
    while(1) 
    { 
        // 짝수일경우 메시지 타입이 4 
        // 홀수일경우에는 메시지 타입이 3 
        if (i % 2 == 0) 
            mybuf.msgtype = 4; 
        else  
            mybuf.msgtype = 3; 
        mybuf.seq = i; 
 
        // 메시지를 전송한다.  
        if (msgsnd( key_id, (void *)&mybuf, sizeof(struct msgbuf), IPC_NOWAIT) == -1) 
        { 
            perror("msgsnd error : "); 
            exit(0); 
        }  
        printf("send %d\n", i); 
        i++; 
        sleep(1); 
    } 
 
    printf("%d \n", rcvbuf.msgtype); 
    printf("%s \n", rcvbuf.mtext); 
    printf("%s \n", rcvbuf.myname); 
    exit(0); 
} 
 
프로그램은 간단하다. mybuf 란 구조체를 만들어서 메시지를 전송하는데, 이때 메시지 타입을 i % 2 가 0일경우 4로 그렇지 않을경우 3으로 해서 전송을 하도록 만들었다.

#include <sys/types.h>  
#include <sys/ipc.h>  
#include <sys/msg.h>  
#include <sys/stat.h>  
struct msgbuf 
{ 
    long msgtype; 
    char mtext[256]; 
    char myname[16]; 
    int  seq; 
}; 
 
int main(int argc, char **argv) 
{ 
    key_t key_id; 
    struct msgbuf mybuf; 
    int msgtype; 
 
    // 아규먼트가 있을경우 msgtype 가 3인 메시지를 받아오고(홀수)  
    // 아규먼트가 없을경우 msgtype 가 4인 메시지를 받아온다(짝수)   
    if (argc == 2) 
        msgtype = 3; 
    else  
        msgtype = 4; 
 
    key_id = msgget(1234, IPC_CREAT|0666); 
    if (key_id < 0) 
    { 
        perror("msgget error : "); 
        exit(0); 
    } 
    while(1) 
    { 
        if (msgrcv( key_id, (void *)&mybuf, sizeof(struct msgbuf), msgtype, 0) == -1) 
        { 
            perror("msgrcv error : "); 
            exit(0);     
        } 
        printf("%d\n", mybuf.seq); 
    } 
    exit(0); 
} 
 
이 예제는 더 간단하다. 아규먼트가 있으면 메시지타입이 3인 메시지를 아규먼트가 없으면 메시지 타입이 4인 메시지를 가져오도록 한다.

프로그램을 컴파일후 테스트를 해보면 ./msgrcv 1을 (아규먼트를 주고 실행) 실행시키면 msgtype 가 4인 메시지를 받아오고 그렇지 않을경우 msgtype 가 3인 메시지를 받아옴을 알수 있을것이다. ./msgsnd, ./msgrcv, ./msgrcv 1 을 동시에 띄워서 테스트하면 된다.

7 메시지큐의 제어

msgctl(2)함수를 이용한다. 첫번째 아규먼트인 msqid 는 메시지 식별자이며, 2번째 아규먼트인 cmd 는 해당 작동명령, 그리고 마지막 아규먼트는 msqid_ds 구조체 이다. 우리는 cmd 를 통해서 해당 메시지식별자가 가르키는 메시지큐를 제어할수 있다. cmd 에는 아래와 같은 종류의 명령을 사용할수 있다.

IPC_STAT
메시지큐의 정보를 원할때 사용한다. 해당 메시지큐의 정보는 3번째 아규먼트인 msqid_ds 구조체를 통해 넘어오게 된다.

IPC_SET
msqid_ds 구조체 정보를 변경하고자 할때 사용한다. 주로 퍼미션 정보를 바꾸기 위해서 사용한다.
IPCRMID
현재 메시지큐를 제거한다.

8 정리


이상 메시지큐에 대해서 간단히 알아보았다. 지금까지 설명에서 처럼 메시지큐는 내부 프로세스간 통신을 위한 상당히 유연한 방법을 제공하고 있음을 알수 있다. 반면 단점이 있는데, 제어하기가 상당히 까다롭다는 점이다.

우선 메시지큐에 들어갈수 있는 데이타의 수가 고정되어 있는데, 메시지큐가 어떤 이유로 꽉찼을 경우 이를 알수 있는 방법이 애매하다. 위의 예제에서 ./msgrcv 와 ./msgrcv 1 이 메시지를 계속적으로 소비하도록 되어 있는데, 만약 둘중 하나가 이상작동을 해서 메시지를 받아오지 못할경우 결국 메시지큐가 꽉 차버리게 되고, 더이상 정상적인 작동을 못하게 될것이다. 또한 커널의 영향을 많이 받으며, 잘못된 메시지큐의 사용은 전체 시스템에 영향을 미칠수도 있게 만든다. 이는 전체 시스템에서 사용할수 있는 메시지큐의 수와 크기에 제한이 있기 때문으로, 메시지큐를 사용하기 위해서는 조심해서 사용해야될 필요성이 있다.

또한 커널은 몇개의 프로세스가 현재 메시지큐를 참조하는지를 알려주는 참조계수를 제공하지 않는다. 그러므로 프로세스에 어떤 문제가 생겼을때, 해당 프로세스에 정확하게 어떤 문제가 발생했는지 알아내는게 상당히 까다롭다.

그러므로 메시지큐를 짧은 시간에 다량의 정보를 전달하기 위한 목적으로 사용하는 데에는 적당치 않다. 그리 많지 않은 정보를 프로세스간 교환하기 위한 용도로 사용하기에 적당한 IPC 설비이다.

9 참고문서



출처 : 조인시 위키

크리에이티브 커먼즈 라이센스
Creative Commons License
이올린에 북마크하기
Posted by 소리나는연탄.

Leave your greetings here.

  
  
  
  
  
  
  
  
 

The Linux GCC HOWTO

2008/01/07 11:24 / Resource

GNU C 컴파일러와 라이브러리를 리눅스 상에서 어떻게 셋업하는지에 대해 다루고 있다. 그리고 리눅스 상에서 컴파일, 링킹, 실행, 디버깅을 어떻게 하는지에 대하여 개략적인 지식을 제공한다. 대부분의 내용은 Mitch D'Souza씨의 GCC-FAQ로부터 차용해온 것이며 (많은 부분 교체했다.) 또한 ELF-HOWTO로부터도 차용을 해온 것이다. (이것도 또한 대부분 바뀌게 될 것이다.) 이 문서는 첫번째 공개 버전이다. (버전 번호는 RCS 의 장난일 뿐이다.) 여러분의 의견을 환영한다.

1. 시작하는 말

1.1 ELF vs. a.out

리눅스 개발은 지금 현재에도 끊임없는 변화 과정에 놓여 있다. 간단히 말해서, 리눅스의 측면에서 어떻게 실행해야 하는지 알고 있는 바이너리는 바로 이 2 가지 종류가 있다. 여러분의 시스템이 어떻게 구성되어 있는지에 따라 둘 다 가지고 있을수도 있다.

2 가지를 어떻게 구별하는가? file이라고 하는 유틸리티를 사용하면 된다. ELF프로그램에 대해서는 ELF 라고 어쩌구 저쩌구 말할 것이며, a.out 프로그램에 대해서는 Linux/i386이라는 단어가 들어가는 말로 얘기해줄 것이다.

둘 간의 차이는 문서 후반부에서 설명될 것이다. ELF 는 새로운 실행화일 형식이며, 일반적으로 더 뛰어나다고 여겨지고 있다.

1.2 책임(Admistrata)

저작권에 관련된 정보는 이 문서의 마지막을 참고하라. 또한, 후반부에서 이 글을 읽고, Usenet에 바보같은 질문(존재하지 않는 GCC의 버그를 발표하는 등)을 올리지 말라는 경고를 볼 수 있을 것이다.

2. 필요한 것을 어디에서 얻을 수 있는가?

2.1 지금 이 문서

이 문서는 리눅스 하우투 문서 시리즈의 하나이다. 따라서 모든 리눅스 하우투 문서가 저장되어 있는 곳이라면 어디든 있다. 예를 들어서 http://sunsite.unc.edu/pub/linux/docs/HOWTO/와 같은 곳이 바로 그곳이다. HTML 버전은 http://ftp.linux.org.uk/~barlow/howto/gcc-howto.html에서 찾을 수 있으며 약간 버전이 높을 지도 모른다.

2.2 다른 문서들

gcc 에 대한 공식적인 문서는 소스 배포 화일에 들어있다. texinfo 화일, .info화일의 형식으로 들어있다. 네트워크 속도가 빠르다거나, 시디롬에 가지고 있거나, 또는 인내심이 많다고 생각될 때에는 그것을 untar 한 후에 해당 화일을 /usr/info디렉토리에 카피하도록 하자. 만약 없다면 tsx-11에 가서 자료를 찾아보자. 항상 최신 버전이 있는 것은 아닐 것이다.

libc 에 대한 문서는 2 가지가 있다. GNU libc 의 경우에는 info 화일들을 가지고 있는데 stdio 부분을 빼고는 아주 자세히 리눅스 libc 에 대해서 알려주고 있다. 맨페이지도 구할 수 있는데 시스템 호출(system call 섹션 2), 많은 libc 함수(섹션 3)에 대해 아주 상세히 설명하고 있다.

2.3 GCC

두 가지 답이 있다.

(a) 리눅스 GCC 의 공식적인 배포판은 ftp://tsx-11.mit.edu:/pub/linux/packages/GCC/에서 바이너리 형태로 구할 수 있다. 즉 이미 컴파일되어 있는 것을 말한다. 지금 글을 쓰고 있는 이 순간에 최신 버전은 2.7.2 로서 화일명은 gcc-2.7.2.bin.tar.gz이다. (편집자주: 이 문서의 작성시기가 96년도임을 상기하라!)

(b) FSF로부터의 최신 소스 버전은 GNU 프로그램 저장소인 GNU archives에서 구할 수 있다. 소스 버전이 항상 공식배포판 바이너리 버전과 같은 것은 아니다. configure 스크립트를 이용해서 모든 설정을 할 수 있다. tsx-11도 마저 살펴보도록 하자. 패치화일이 필요할 지도 모르기 때문이다.

어떤 것이든 컴파일이라는 것을 하기 위해서는 다음이 필요하다.

2.4 C 라이브러리와 헤더 화일들

여기서 여러분에게 필요한 것은 일단 (1)여러분의 시스템이 a.out 인가? (2) 아니면 둘 다 있는 경우에 둘 중에 무엇을 택하고 싶은가? 에 따라 달라진다. 만약 여러분이 libc 4 에서 libc 5 로 업그레이드하려고 한다면 우선은 ELF-HOWTO문서를 봐야할 것이다.

tsx-11에서 구할 수 있다.

libc-5.2.18.bin.tar.gz

--- ELF 공유 라이브러리 이미지, 정적 라이브러리 그리고 C 라이브러리와 수학 라이브러리를 위한 헤더화일들

libc-5.2.18.tar.gz

--- 위 라이브러리에 대한 소스. 여러분은 헤더 화일을 구해야 하기 때문에 위에 있는 바이너리배포판도 필요하다. 손수 컴파일을 할 것인지 아니면 그냥 바이너리를 사용할 것인지에 대한 답은 간단하다. 바이너리를 사용하라! 하지만 NYS나 셰도우 패스워드 기능을 원할 때는 손수 컴파일하는 수 밖에 없다.

libc-4.7.5.bin.tar.gz

--- a.out 공유 라이브러리 이미지, 정적 라이브러리(C 함수, 수학 함수), 위에 있는 libc 5 와 공존할 수 있게끔 디자인되어 있다. 하지만 여러분이 a.out 프로그램을 아직도 갖고 있거나 개발하려고 할 때만 필요하다.

2.5 관련된 도구들 (as, ld, ar, strings 등등)

tsx-11에서 구할 수 있으며, 현재 버전은 binutils-2.6.x.x.bin.tar.gz이다.

바이너리 유틸리티들은 오로지 ELF 만 있다는 사실에 유의하자. 현재 라이브러리는 ELF 로만 개발되고 있으며 a.out 라이브러리는 ELF 와 같이 쓸 때만 의미있다고 생각한다. C 라이브러리 개발은 ELF 쪽으로만 진행되고 있으며, a.out으로 해야할 커다란 이유 같은게 없다면 그에 따르는 것이 좋다.

3. GCC설치와 설정

3.1 GCC 버전

현재 사용 중인 gcc 의 버전을 알고 싶은 경우에는 gcc -v라고 셸 프롬프트에서 실행시키면 된다. 또한 이렇게 명령을 내리면 여러분의 시스템이 ELF로 세팅되어 있는지 아니면 a.out 으로 되어 있는지 확실하게 알아낼 수 있다. 필자의 시스템에서는 다음과 같이 나온다.

$ gcc -v
Reading specs from /usr/lib/gcc-lib/i486-box-linux/2.7.2/specs
gcc version 2.7.2

여기서 알아두어야 할 핵심적인 내용은 다음과 같다.

  • i486. 이는 여러분이 486 프로세서 용으로 컴파일된 gcc를 사용하고 있다는 말이다. 이 부분은 다를 수 있는데 어떤 사람은 386, 586 에 따라 다를 수도 있다. 하지만 이 3 가지 칩에서 컴파일된 것들은 상관없이 서로 잘 실행된다. 차이점이라고 한다면 486 코드가 어디엔가 더해짐으로써 486 에서는 더욱 더 빨리 실행된다는 정도이다. 386 에서 실행하는데 해가 된다거나 하지는 않는다. 하지만 약간 바이너리가 커질 것이다.
  • box. 이건 전혀 중요한 부분이 아니다. 예를 들어서 box라는 말 대신에 slackwaredebian 등의 단어로 교체될 수도 있고 아예 이 부분이 없을 수도 있다. 보통은 i486-linux 이런 식일 것이다. 만약 gcc 를 컴파일해서 사용한다면 본인이 따로 i486-box-linux 라고 지정했듯이 gcc를 만들 때 정해줄 수 있다.
  • linux. 이 단어 대신에 linuxelf 라든가 linuxaout이라는 단어가 들어갈 수도 있다. 또는 리눅스 커널 버전이 들어가도록 할 수도 있다. 암튼 리눅스용임을 잘 나타내고 있다. 간단히 결론을 말하자면, 이 단어의 뜻은 사용중인 GCC 버전에 따라 다르게 해석된다.
    • 2.7.0 이상의 버전에서는 그냥 linux이면 ELF 를 의미하고 a.out은 linuxaout 과 같은 이름을 갖는다.
    • 리눅스가 ELF 쪽으로 나아가면서 이름이 linux에서 밀려났다고도 할 수 있다. 따라서 2.7.0 그 이하에서는 linuxaout 이라는 말을 찾아볼 수 없을 것이다.
    • linuxelf라는 이름은 사라진 말이다. gcc 버전 2.6.3 시절에 ELF 실행화일을 만들기 위해서 지어졌던 이름이다. gcc 2.6.3 은 ELF 실행화일을 만드는데 버그가 있다고 알려져 있다. 업그레이드하기 바란다.
  • 2.7.2 이것은 버전 번호이다.

따라서 종합해보면 필자는 지금 ELF 실행코드를 생성시키는 gcc 2.7.2 를 가지고 있다는 것이다.

3.2 도대체 내 gcc 가 어디에 있는건가?

그냥 아무 생각없이 gcc 를 설치했거나 배포판을 설치할 때 자동으로 설치하게 했다면, 도대체 리눅스 화일 시스템 상에서 어디에 위치하는지 알고 싶을 것이다. 대답은 이렇다.

  • /usr/lib/gcc-lib/target/version/ (그리고 모든 하위 디렉토리들)이 컴파일러의 대부분이 위치하는 장소이다. 컴파일을 수행하는 실행화일 그 자체와 gcc 버전에 따른 라이브러리와 헤더화일들이 들어있다.
  • /usr/bin/gcc는 컴파일러 운전사(Compiler Driver)역할을 한다. 커맨드 상에서는 gcc 라고만 명령한다. 만약 여러 버전의 컴파일러를 가지고 있다면 여러 버전과 함께 사용할 수 있다. gcc 가 사용하게 될 디폴트 버전의 컴파일러를 알아내기 위해서는 gcc -v라고 해보면 된다. 다른 버전으로 강제로 컴파일하게 하려면 gcc -V version 이런 식으로 사용하면 된다. 예를 들어서...
    # gcc -v
    Reading specs from /usr/lib/gcc-lib/i486-box-linux/2.7.2/specs
    gcc version 2.7.2
    # gcc -V 2.6.3 -v
    Reading specs from /usr/lib/gcc-lib/i486-box-linux/2.6.3/specs
    gcc driver version 2.7.2 executing gcc version 2.6.3
    
  • /usr/target/(bin|lib|include)/. 여러분이 여러 개의 목표 형식을 가지고 있다면 (일단 ELF인가 a.out 인가 또는 여러 형태의 크로스 컴파일러 등) 디폴트 목표 형식용이 아닌 라이브러리, 바이너리 유틸리티 (as, ld 등...), 헤더 화일들도 찾아볼 수 있을 것이다. 오로지 한 종류의 gcc 를 가지고 있다 하더라도 매우 많은 것들이 그 디렉토리에 깔려있음을 확인할 수 있다. 그렇지 않다면 아마도 /usr/(bin|lib|include)에 있을 것이다.
  • /lib/,/usr/lib 그리고 여타 라이브러리 디렉토리들이 기본 시스템을 위한 라이브러리 디렉토리이다. 여러분은 또한 상당히 많은 프로그램에 대하여 /lib/cpp를 가지고 있어야 한다. (X 가 실제로 많이 사용하고 있다.) /usr/lib/gcc-lib/target/version/에 있는 cpp 를 카피해놓던가? 아니면 심볼릭 링크를 해준다.

3.3 헤더 화일들은 어디에 있는가?

여러분이 손수 /usr/local/include에 설치한 것들 빼고 리눅스에는 3 가지 중요 헤더 디렉토리가 있다.

  • 대부분의/usr/include/와 그 하부 디렉토리들은 H J Lu 의 libc 바이너리 배포판에 의해서 제공된다. 여기서 본인은 "대부분"이라는 표현을 썼는데, 그 이유는 다른 소스 (예를 들어 curses, dbm 라이브러리)에서 온 헤더화일들도 있기 때문이다. 특히나 최근 libc 배포판을 가져오면 그러한 헤더화일들은 없다. (예전에는 같이 달려서 왔지만)
  • /usr/include/linux/usr/include/asm(<linux/*.h>화일과 <asm/*.h>에 의해 참조되는 헤더화일들이 있는 장소)는 각각 커널 소스에서 linux/include/linuxlinux/include/asm을 가리키는 심볼릭 링크여야 한다. 뭔가 조금이라도 큰 작업을 하려고 한다면 분명히 설치해야 한다. 커널 컴파일을 하기 위해서만 있는 것은 아니다.

    또한 커널 소스를 풀고 나서 make config라는 작업을 해주어야 할 것이다. 많은 화일들이 그 과정을 통해서 생겨나는 <linux/autoconf.h>라는 화일에 의존하기 때문이다. 그리고 어떤 버전의 커널에서는 asm 이라고 하는 것이 심볼릭 링크일 뿐, make config 할 때만 생기는 경우가 있다.

    asm 은 보통 asm-i386으로 링크되어 있다. 그전에는 오로지 인텔 머신용 헤더화일만이 있었기 때문에 asm 만이 있었지만 이제는 리눅스가 명실상부하게 멀티플랫폼 운영체제로 나아가고 있기 때문이다. asm-i386말고도 asm-alpha, asm-generic, asm-m68k, asm-mips, asm-ppc, asm-sparc등의 헤더 화일 디렉토리가 있는 것을 발견할 수 있다.

    따라서 /usr/src/linux라고 하는 디렉토리에 이미 소스를 풀어놓았다면...

    $ cd /usr/src/linux
    $ su
    # make config
    [answer the questions.  Unless you're going to go on and build the kernel
    it doesn't matter _too_ much what you say]
    # cd /usr/include
    # ln -s ../src/linux/include/linux .
    # ln -s ../src/linux/include/asm .
    

  • <float.h>, <limits.h>, <varargs.h>, <stdarg.h> 그리고 <stddef.h> 등의 화일들은 컴파일러 버전마다 다를 것이다. 그리고 그들은 /usr/lib/gcc-lib/i486-box-linux/2.7.2/include/에 위치하고 있다.

3.4 크로스 컴파일러(Cross Compiler) 만들기

목표 플랫폼으로서의 리눅스

여러분이 지금 gcc 소스 코드를 가지고 있다고 생각하겠다. 보통은 GCC 에 대한 INSTALL 화일에서 지시하는 대로 따르면 된다. configure --target=i486-linux --host=XXX 이런 식으로 해주는데, XXX는 플랫폼을 말한다. 다음에는 make 과정을 거치면 된다. 리눅스 헤더화일, 커널 헤더화일이 필요하며, 크로스 컴파일러와 크로스 링커를 만들기 위해서도 필요하다. ftp://tsx-11.mit.edu/pub/linux/packages/GCC/에서 구할 수 있다.

소스 플랫폼으로서의 리눅스, 목표 플랫폼으로서의 MSDOS

흠. 소스를 리눅스에서 작성한 뒤에 도스에서 돌아가는 프로그램으로 컴파일하기 위해서는 emx 패키지나 go extender라는 것을 필요로 한다. ftp://sunsite.unc.edu/pub/Linux/devel/msdos에 가서 관련 화일을 찾아보기 바란다.

본인으로서는 테스트해본 적이 없으며, 쓸만하다고 단언하기는 힘들다.

4. 포팅과 컴파일링

4.1 자동적으로 정의되는 심볼들

여러분은 여러분이 갖고 있는 버전의 gcc가 -v 옵션을 붙임으로써 어떠한 심볼을 자동적으로 정의하는지 알아낼 수 있다. 예를 들어 본인의 것은 다음과 같다.

$ echo 'main(){printf("hello world\n");}' | gcc -E -v -
Reading specs from /usr/lib/gcc-lib/i486-box-linux/2.7.2/specs
gcc version 2.7.2
 /usr/lib/gcc-lib/i486-box-linux/2.7.2/cpp -lang-c -v -undef
-D__GNUC__=2 -D__GNUC_MINOR__=7 -D__ELF__ -Dunix -Di386 -Dlinux
-D__ELF__ -D__unix__ -D__i386__ -D__linux__ -D__unix -D__i386
-D__linux -Asystem(unix) -Asystem(posix) -Acpu(i386)
-Amachine(i386) -D__i486__ -

만약 여러분의 코드가 리눅스에만 관계되는 코드라면, 다음과 같이 해주는 것이 좋다.

#ifdef __linux__
/* ... funky stuff ... */
#endif /* linux */

__linux__라는 이름을 사용하라. linux아니다. 후자가 정의되어 있기는 하지만 POSIX 규격에는 맞지 않기 때문이다.

4.2 컴파일러 부르기

컴파일러 스위치들에 대한 문서는 gcc info 페이지를 보면 된다. (여러분이 Emacs를 사용하고 있다면 C-h i그리고 나서 gcc 옵션을 선택하라) 여러분이 갖고 있는 배포판을 만든 사람이 gcc info 페이지를 넣어지 않았을 수도 있고, 또는 옛 버전의 것이 들어가 있을 수도 있다. 가장 좋은 방법은 ftp://prep.ai.mit.edu/pub/gnu나 또는 미러 사이트로 가서 gcc 소스 코드를 받아오는 것이다. 그리고 그 소스 안에서 카피해온다.

gcc 에 대한 맨페이지(gcc.1)는 일반적으로 시대에 뒤떨어져 있다고 말할 수 있다. 맨페이지를 보려고 하면 그러한 경고 문구를 볼 수 있다.

컴파일러 플래그(flag)

gcc를 사용할 때, -On(여기서 n은 작은 양의 정수들, 생략해도 된다)을 커맨드 라인 옵션으로 넣어주면 출력 코드가 최적화된다. 여기서 사용되는 n 값 중에서 실제 의미를 갖는 값들은 gcc의 버전에 따라 다른데, 일반적으로 0 (최적화하지 않음)부터 시작해서 2(상당히 많이 최적화), 3(아주아주 많이 최적화)까지 쓰인다.

내부적으로 gcc는 이 옵션을 -f-m 이라는 옵션들로 바꾸어서 처리하게 된다. -O의 특정 레벨이 어떤 의미를 갖는지에 대해서는 gcc 실행시에 -v-Q(문서화되지 않았음)플래그를 붙여줌으로써 확인할 수 있다. 예를 들어 -O2는 다음과 같이 나타난다. (사람들마다 서로 다를 수 있다)

enabled: -fdefer-pop -fcse-follow-jumps -fcse-skip-blocks
-fexpensive-optimizations
         -fthread-jumps -fpeephole -fforce-mem -ffunction-cse -finline
         -fcaller-saves -fpcc-struct-return -frerun-cse-after-loop
         -fcommon -fgnu-linker -m80387 -mhard-float -mno-soft-float
         -mno-386 -m486 -mieee-fp -mfp-ret-in-387

여러분의 컴파일러가 지원하고 있는 최적화 레벨보다 큰 숫자를 사용한다면 (예를 들어 -O6), 그 컴파일러가 지원하는 최적의 레벨로 최적화시켜준다. 이런 식으로 컴파일되도록 세팅되어 있는 코드를 배포하는 것은 별로 좋은 생각은 아닌 것 같다. 더 많은 최적화 레벨들이 차후 gcc 버전에 생긴다면, 잘못하면 여러분의 소스 코드가 엉뚱하게 컴파일되는 수도 있다.

만약 여러분이 지금 -O3이 최고 레벨이라는 가정하에서 -O6를 사용했다고 치자. 하지만 다음 버전(예를 들어서 2.7.3.?)에서 -O8까지 지원하게 된다면 -O6는 전혀 엉뚱한 의미를 가질 수도 있다.

gcc 버전 2.7.0 부터 2.7.2 까지의 사용자들은 -O2 최적화 플래그에 버그가 있다는 사실을 잘 알아두기 바란다. Strength Reduction이라고 하는 것이 제대로 작동하지 않는다. 이 문제를 해결할 수 있는 패치가 있고 다시 gcc 를 컴파일해야 할것이다. 또는 언제나 -fno-strength-reduce 라는 옵션을 주고 컴파일하기 바란다.

프로세서별 옵션

-O 옵션을 주어도 자동적으로 작동하지 않는 -m 플래그들이 있다. 하지만 이들은 상당히 유용하다. 중요한 것으로는 -m386-m486이 있다. 이 플래그들은 gcc더러 각각 386, 486중 어떤 것에 더 맞춰서 컴파일할 것인지를 알려주는 것이다. -m486으로 컴파일하였다고 하더라도 386 에서 실행되는데는 지장없다. 그러니 걱정할 필요없다. 486 코드가 조금 더 크지만 386 에서 느려지거나 하지는 않는다.

아직까지는 -mpentium이나 -m586과 같은 것은 없다. 리누스(Linus)는 486 코드옵티마이즈된 코드를 얻으면서도 펜티엄이 사용하지 않는 정렬방식과의 커다란 차이점이 없는 코드를 얻기 위해서는, -m486 -malign-loops=2 -malign-jumps=2 -malign-functions=2를 사용할 것을 제안하고 있다. Michael Meissner(Cygnus에 있는)는 다음과 같이 말하고 있다.

내 육감으로는 -mno-strength-reduce를 같이 쓰면 또한 x86 에서 더 빠른 코드를 얻어낼 수 있다는 것이다. (주의! 나는 지금 strength reduction 버그에 대해서 말하고 있는 것이 아니다. 그것은 전혀 다른 문제이다) 왜냐하면 x86은 다소 레지스터 숫자가 적기 때문이다. (그리고 다른 레지스터에 대하여 레지스터들을 그룹으로 묶어서 spill 레지스터 속으로 처리하는 GCC 의 처리방식은 전혀 도움이 되질 않는다) StrengthReduction은 전형적으로 곱셈을 덧셈으로 교체하기 위하여 다른 레지스터들을 사용하게 된다. -fcaller-saves 또한 이런 문제점이 있지 않나 생각하고 있다.

또 다른 예감은 이렇다. -fomit-frame-pointer는 도움이 될 수도 있고, 그렇지 않을 수도 있다는 것이다. 한 편으로는 또 다른 레지스터가 할당가능하다는 것을 의미할 수도 있고, 다른 한 편으로는 x86 이 연산지시(instruction)에 대하여 인코딩하는 방식으로서, 스택 상대적 주소가 프레임 상대적 주소보다도 더 많은 공간을 차지한다는 것을 의미하기도 한다. 이렇게 되면 프로그램에 사용될 수 있는 Icache이 약간 줄어든다. 또한 -fomit-frame-pointer는 컴파일러가 계속적으로 호출 후에도 스택 포인터를 조정해야 한다는 것을 뜻한다. 따라서 프레임을 갖는 경우, 몇 번의 호출만으로도 스택이 가득 차게 된다.

마지막 말은 리누스 또한 언급하고 있다.

만약 여러분이 최적화된 효율을 원한다면, 나를 믿지 말라. 실제로 테스트를 해봐야 한다. gcc 컴파일러의 옵션은 정말로 많다. 그리고 몇 개의 특정 조합이 가장 좋은 최적화를 이뤄줄 것이다.

Internal compiler error: cc1 got fatal signal 11

시그널 11번은 SIGSEGV, 즉 세그먼테이션 위반에 대한 시그널이다. 일반적으로 프로그램이 포인터를 잘못 썼다는 말이거나 자기가 소유하고 있지 않은 메모리에다 쓰기 작업을 하려고 할 때 발생한다. 그래서 이는 gcc의 버그일 수도 있다.

하지만 gcc는 대부분의 작업에서 매우 안정적이고 테스팅을 많이 거친 소프트웨어라는 사실을 기억하라. gcc는 또한 복잡한 자료 구조와 포인터를 엄청나게 많이 사용하고 있다. 간단히 말하자면 현재까지 소프트웨어 중에서 가장 뛰어난 램 테스팅 프로그램(RAM Tester)이라고 말할 수도 있다. 만약 매번 컴파일할 때마다 멈추는 위치가 다르다면 이는 거의 대부분 여러분 하드웨어의 문제라고 봐도 된다. (CPU, 메모리, 마더보드나 캐쉬) 여러분의 컴퓨터가 파워 온 체킹을 거쳐서 잘 부팅되었고 그리고 윈도우즈 같은 것도 잘 돌아간다고 해서 그것을 gcc의 버그로 돌리지는 말라. 이러한 사실은 무의미하다. 그리고 커널 컴파일하면서 make zImage에서 꼭 멈춘다고 해서 gcc의 버그라고 말할 수는 없다. make zImage는 무려 200개 이상의 화일을 컴파일하고 있다. 그것보다는 좀 작은 경우를 찾아보도록 하자.

만약 계속적으로 버그가 똑같이 나타나고 자그마한 프로그램 컴파일에서도 그러하다면, FSF에다가 버그 리포트를 해도 되고, 또는 linux-gcc 메일링 리스트에 글을 올려도 된다. 그러기 위해서는 우선 gcc 문서를 읽어보고 어떤 절차가 필요한지 숙지한 다음 하기 바란다.

4.3 포팅(Portability)

요즘은 만약 그 소프트웨어가 리눅스로 포팅될 수 없다면 그 소프트웨어는 가치가 없는 프로그램이라고 말한다. :-)

진지하게 말하자면, 일반적으로 리눅스의 100% POSIX 호환성을 이루기 위해서는 아주 약간의 수정작업만이 필요하다. 또한 단지 make 라고만 하면 실행화일이 만들어질 수 있도록 하기 위하여 코드의 원저자에게 수정 코드를 보내는 것이야말로 가치있는 일이다.

BSDisms (bsd_ioctl, daemon 그리고 <sgtty.h>)

여러분은 여러분의 프로그램을 -I/usr/include/bsd를 넣어서 컴파일한 후, -lbsd 옵션을 넣고 링크할 수도 있다. (즉 Makefile 안에서 -I/usr/include/bsdCFLAGS 변수에 넣고, -lbsdLDFLAGS에 넣음으로써) 이젠 BSD 타입의 시그널 행동을 얻어내기 위해서 -D__USE_BSD_SIGNAL덧붙일 필요가 없다. 왜냐하면 -I/usr/include/bsd라고 해주고 <signal.h>를 소스 안에서 포함하면 모든 일이 제대로 이루어진다.

없어진 시그널들(SIGBUS, SIGEMT, SIGIOT, SIGTRAP, SIGSYS 등)

리눅스는 POSIX를 준수하고 있다. 이러한 시그널들은 POSIX 정의 시그널들이 아니다. 이는 ISO/IEC 9945-1:1990 (IEEE Std 1003.1-1990), paragraph B.3.3.1.1 에서 다음과 같이 말하고 있는 바이다.

SIGBUS, SIGEMT, SIGIOT, SIGTRAP, 그리고 SIGSYS와 같은 시그널들은 POSIX.1으로부터 제외되었다. 왜냐하면 그들의 행동은 함축적이고 어떻게 부르느냐에 따라 다르기 때문에 적절하게 범주화시킬 수가 없다. 이러한 시그널들을 없애버리는 것이 규약 준수일 수도 있지만, 왜 그 시그널들을 제외해버렸는지에 대해서 문서화해야 한다. 그리고 그 시그널들을 어떻게 처리할 것인가에 대해서는 아무런 강제 규정도 없다.

이 문제를 해결할 수 있는 가장 간단한 방법은 이러한 시그널들을 모두 SIGUNUSED로 재정의하는 것이다. 바른방법은 물론 이러한 시그널을 처리하는 부분을 #ifdef 문장을 써서 처리하도록 하는 것이다.

#ifdef SIGSYS
/* ... POSIX 규정이 아닌 SIGSYS 코드가 여기에 온다 .... */
#endif

K & R 코드

GCC는 ANSI 컴파일러이다. 하지만 아주 많은 코드들이 ANSI가 아니다. 이럴 때는 컴파일러 플래그에 -traditional 이라고만 붙여주면 된다고 할 수 있다. 물론 괴롭게 수작업을 해줘야 하는 부분도 많이 있다. gcc info 페이지를 살펴보기 바란다.

-traditional라는 옵션은 gcc 가 이용하려고 하는 C 언어 방식을 바꾸는 것 말고도 다른 효과를 지니고 있다. 예를 들어 그 옵션은 -fwritable-strings을 작동시키는데, 문자열 상수를 데이타 영역으로 보내는 역할을 한다. (텍스트 영역, 즉 그들이 쓸 수 없는 영역을 말한다) 이런 경우 프로그램의 메모리 사용흔적(footprint)이 증가하게 된다.

전처리기 심볼이 코드의 프로토타입과 충돌할 때

많이 발생하는 문제들 중에 하나가 바로 몇몇 함수들이 이미 리눅스 헤더화일들에 매크로로 정의되어 있고 전처리기가 코드 내에서 유사한 프로토타입에 대하여 처리 거부를 하는 경우이다. 보통 atoi()atol()인 경우가 많다.

sprintf()

sprintf(string, fmt, ...)이 많은 유닉스 시스템에서는 문자열에 대한 포인터를 반환하는 반면에 ANSI를 따르는 리눅스는 문자열에 삽입된 문자의 갯수를 반환한다. 이는 특히나 SunOS와 같은 것으로부터 포팅하는 경우에 더욱 주의해야 한다.

FD_* 같은 것들? fcntl과 그 비슷한 녀석들. 도대체 정의부분이 어디에 있는가?

<sys/time.h>에 있다. 만약 fcntl을 이용하고자 한다면 실제 프로토타입을 위하여 <unistd.h> 또한 포함시키고 싶을 것이다.

일반적으로 말하자면 어떤 함수에 대한 맨페이지를 보면 SYNOPSYS 부분에서 어떤 헤더화일을 #include 해야하는지 자세히 나타내주고 있으니 그것을 참고하기 바란다.

select()에서 타임아웃이 걸리고 프로그램이 계속 기다리기만 한다.

예전에는 select()에 대한 타임아웃 파라미터가 읽기전용으로만 사용되었다. 그리고 그 때에도 맨페이지에는 다음과 같은 경고가 있었다.

select()는 아마도 적절한 곳에 있는 시간값을 변경함으로써 만약에 그러한 일이 발생한다면 원래의 타임아웃부터 남은 시간을 반환해야 할 것이다. 하지만 이 기능은 차기 버전에서나 구현될 것이다. 따라서 타임 아웃 포인터가 select() 호출에 의하여 수정되지 않을 것이라고 생각하는 것은 바람직하지 못하다.

바로 그 날이 왔다! 최소한 그것이 이루어지고 있다. select()호출로부터 돌아올 때, 타임아웃 인수는 데이터가 도착하지 않는다면 기다리려고 했던 잔류 시간으로 세팅된다. 만약 아무 데이터도 도착하지 않았었다면 이 값은 0(zero)이 되었을 것이다. 그리고 같은 타임아웃 구조체를 가지고 호출을 하게 되면 호출 즉시 되돌아올 것이다.

이 문제를 해결하기 위해서는 타임아웃 값을 매번 select()를 호출할 때마다 관련 구조체에 적어주어야 한다. 다음과 같은 코드가 있다면,

      struct timeval timeout;
      timeout.tv_sec = 1; timeout.tv_usec = 0;
      while (some_condition)
            select(n,readfds,writefds,exceptfds,&timeout); 
아래와 같이 바꾸도록 하라.
      struct timeval timeout;
      while (some_condition) {
            timeout.tv_sec = 1; timeout.tv_usec = 0;
            select(n,readfds,writefds,exceptfds,&timeout);
      }

모자익(Mosaic)의 몇몇 버전이 한 때 이러한 문제로 떠들썩했었다. 회전하는 지구 애니매이션의 속도가 네트워크를 통해 들어오는 자료의 속도에 반비레하는 일이 벌어진 것이다!

시스템 호출이 인터럽트될 때

증상:

프로그램이 Ctrl+Z로 서스펜드되고 다시 시작되어 버린다. 또는 다른 때에는 Ctrl+C와 같은 시그널을 발생시키고 자식 프로세스들을 죽인다 등등... "interrupted system calls" 또는 "write: unknown error" 또는 그런 것 비슷한 에러를 낸다.

문제점:

POSIX 시스템은 다른 구식 유닉스 체제에서보다 약간 더 많이 시그널에 대해서 체킹을 행한다. 리눅스는 시그널 핸들러들(signal handler)을 실행시킬 것이다.

  • 타이머가 째깍댈 때마다 비동기적으로.
  • 모든 시스템 호출 반환시에.
  • 그리고 다음과 같은 시스템 호출 동안에도 그러하다: select(), pause(), connect(), accept(), 터미널 상에서의 read(), 소켓, 파이프나 라인 프린터, FIFO에 대한 open(), PTY나 시리얼 라인, 터미널에 대한 ioctl(), F_SETLKW 명령을 내리는 fcntl(), wait4(), syslog(), 모든 TCP 또는 NFS 작업

다른 운영체제의 경우에는 다음과 같은 시스템 호출에 대해서도 체크할 것이다. 위에서 말한 것 이외에도 다음과 같은 시스템 호출들: creat(), close(), getmsg(), putmsg(), msgrcv(), msgsnd(), recv(), send(), wait(), waitpid(), wait3(), tcdrain(), sigpause(), semop()

만약 시그널(프로그램에서 핸들러를 인스톨한 경우)이 시스템 호출 중에 발생한다면, 그에 대한 핸들러가 호출된다. 그리고 핸들러가 반환되면 (시스템 호출로), 시스템 호출은 중간에 가로채기를 당했는지 살펴보고 즉시 -1 값을 가지고 반환된다. 그리고errno 를 EINTR 로 세팅한다. 프로그램은 그러한 일이 있을 것이라고 예상하지 못하고 죽는 것이다.

여러분은 다음 2 가지 해결책 중에 하나를 고르면 된다.

(1) 여러분이 설치한 모든 시그널 핸들러에 대하여 SA_RESTART를 sigaction 플래그에 첨가한다. 다음과 같은 것이 있다면,

  signal (sig_nr, my_signal_handler);
를 다음과 같이 바꾼다.
  signal (sig_nr, my_signal_handler);
  { struct sigaction sa;
    sigaction (sig_nr, (struct sigaction *)0, &sa);
#ifdef SA_RESTART
    sa.sa_flags |= SA_RESTART;
#endif
#ifdef SA_INTERRUPT
    sa.sa_flags &= ~ SA_INTERRUPT;
#endif
    sigaction (sig_nr, &sa, (struct sigaction *)0);
  }

이 방법이 대부분의 시스템 호출에 적용되기는 하지만, read(), write(), ioctl(), select(), pause(), connect()에 대해서는 여러분 스스로 EINTR를 체크해주어야 한다. 다음을 살펴보자.

(2) 여러분이 직접 명시적으로 EINTR을 체크해준다.

read()를 사용하는 코드가 원래 이렇게 되어 있다고 치자.

int result;
while (len > 0) { 
  result = read(fd,buffer,len);
  if (result < 0) break;
  buffer += result; len -= result;
}
이 코드를 다음과 같이 바꾸어주면 된다.

int result;
while (len > 0) { 
  result = read(fd,buffer,len);
  if (result < 0) { if (errno != EINTR) break; }
  else { buffer += result; len -= result; }
}
이번에 이런 코드가 있다면,

int result;
result = ioctl(fd,cmd,addr);
그것은 또한 다음과 같이 바뀌어야 한다.
int result;
do { result = ioctl(fd,cmd,addr); }
while ((result == -1) && (errno == EINTR));

BSD 유닉스의 몇몇 버전에서는 시스템 호출을 재개하는 것이 기본 행동으로 되어 있는 경우도 있으므로 주의하자. 시스템 호출이 가로채기를 허용하기 위해서는 SV_INTERRUPT 또는 SA_INTERRUPT 플래그를 사용하도록 하자.

쓰기 가능 문자열 (프로그램이 랜덤하게 세그폴트를 낸다)

GCC는 gcc를 사용하는 사람들이 문자열 상수에 대하여 정확히 상수로서 계속 사용할 것이라고 낙관하고 있는 듯 하다. 따라서 그 문자열 상수를 프로그램의 텍스트 영역에 집어넣는다. 이렇게 함으로써 스왑 영역을 사용하는 것이 아니라 프로그램의 디스크 이미지로부터 페이지 인 & 아웃을 행할 수 있도록 해준다. 그러므로 문자열 상수에 대하여 다시 쓰기 작업을 하게 되면 세그멘테이션 폴트를 일으키게 되는 것이다.

예를 들어서 문자열 상수를 인수로 하여 mktemp()를 호출하는 옛날 프로그램들에서는 문제가 발생할 것이다. mktemp()는 주어진 인수에 다시 쓰려고 하기 때문이다.

이 문제를 고치기 위해서는 (a) -fwritable-strings 이라는 옵션을 주어서 컴파일한다. 이렇게 해주면 gcc는 문자열 상수를 데이타 영역에 넣게 된다. 또는 (b) 문제가 되는 부분을 수정해서 상수가 아니라 변수로 주어지게 만들고 호출 전에 strcpy 를 사용하여 데이터를 그곳으로 카피해준다.

execl()호출이 실패하는가?

원인은 간단하다. 제대로 호출을 하지 않았기 때문이다. execl에 대한 첫번째 인수는 실행하고자 하는 프로그램이다. 그리고 두번째부터는 호출하는 프로그램에 전달할 argv배열이다. 기억하라! argv[0]는 전통적으로 아무런 인수 없이 실행되더라도 세팅이 된다는 사실을! 따라서 다음과 같이 코드를 써야한다.

execl("/bin/ls","ls",NULL);
절대로 다음과 같이 쓰면 안된다.
execl("/bin/ls", NULL);

아무런 전달인수 없이 실행시키는 경우에도 실행형식은 자신의 동적 라이브러리 의존성을 나타낼 수 있는 방식으로 구문을 맞춰준 형태라야 한다. 최소한도 a.out의 경우는 그러하다. ELF는 좀 다른 방식으로 작동한다.

(만약 이러한 라이브러리 정보를 원한다면 아주 간단한 인터페이스가 있다. 동적 로딩Dynamic Loading에 대한 섹션을 보거나 ldd에 대한 맨페이지를 참고하라)

5. 디버깅과 Profiling

5.1 예방적인 관리(lint)

문제가 발생하고 나서 해결하는 것보다는 문제를 미연에 방지하는 것이 중요하지 않을까? 리눅스에 널리 쓰이는 lint는 없다. 아마도 대부분의 사람들이 gcc가 내놓는 자세한 경고 메세지에 만족하고 있기 때문인 것 같다. 아마도 가장 유용하쓰이는 것은 -Wall 스위치일 것이다. 이것이 의미하는 바는 "Warnings, all"로서 모든 경고 메세지를 발생시키라는 말이다. 또한 아주 자세하게 나온다.

Public Domain lint는 ftp://larch.lcs.mit.edu/pub/Larch/lclint에서 얻을 수 있다. 하지만 얼마나 괜찮은지 본인은 모른다.

5.2 디버깅

어떻게 하면 프로그램의 디버깅 정보를 알아낼 수 있는가?

그러기 위해서는 -g 옵션을 주고 컴파일/링크해야 한다. 그리고 -fomit-frame-pointer 스위치는 빼주어야 한다. 사실 모든 부분을 다시 컴파일할 필요는 없고, 여러분이 관심 갖고 있는 부분만을 그렇게 해주면 된다.

a.out에 있어서 공유라이브러리가 만약 -fomit-frame-pointer 스위치를 가지고 컴파일되었다면 gdb를 사용할 수 없을 것이다. -g 옵션을 주는 이유는 바로 정적 링크를 행하라는 말을 함축하게 된다.

만약 링커가 libg.a를 찾을 수 없다고 하면서 실패하게 된다면, 여러분이 /usr/lib/libg.a을 갖고 있지 않기 때문일 것이다. 그 화일은 특별한 라이브러리로서 디버깅 가능 C 라이브러리이다. libc 패키지에 포함되어 있거나 또는 libc 소스 코드를 받아서 컴파일하면 생긴다. 실제로 그렇게 필요한 것은 아니고 대충 /usr/lib/libc.a/usr/lib/libg.a로 링크시켜버려도 대부분 상관없을 것이다.

디버깅 정보를 어떻게 하면 다시 꺼낼 수 있는가?

아주 많은 GNU 소프트웨어들은 -g 옵션을 가지고 컴파일되어 있으므로 화일 크기가 매우 크다. (종종 정적 링크되어 있음) 그렇게 괜찮은 생각인 것 같지는 않다.

만약 프로그램이 autoconf에 의해 만들어진 configure를 가지고 있다면, 보통의 경우 Makefile을 건드림으로써 디버깅 정보를 넣지 않게 할 수 있다. 물론 ELF를 사용하고 있다면, 프로그램은 -g 세팅과는 상관없이 동적 링크되며, 그냥 쉽게 strip(디버깅 정보를 실행화일에서 빼버리는 행위)시킬 수 있다.

관련 소프트웨어

대부분의 사람들은 gdb를 사용하고 있다. gdb는 GNU archive sites에서 소스의 형태로, 아니면 tsx-11이나 선사이트에서 바이너리의 형태로 구할 수 있다. xxgdb는 gdb에 기초한 X 윈도우 디버거이다. 즉, 우선적으로 gdb를 이미 설치했어야 한다는 뜻이다. 그 소스는 ftp://ftp.x.org/contrib/xxgdb-1.08.tar.gz에서 찾을 수 있다.

또한 UPS 디버거가 Rick Sladkey씨에 의해 포팅되었다. X 윈도우에서도 잘 돌아간다. 하지만 xxgdb와 같이 텍스트 디버거인 gdb같은 것에 의존하는 형태는 아니다. 아주 훌륭한 기능들을 많이 가지고 있다. 따라서 여러분이 디버깅에 많은 시간을 할애하고 있다면, 우선적으로 UPS 디버거를 권한다. 리눅스용으로 컴파일된 바이너리나 소스 패치화일은 ftp://sunsite.unc.edu/pub/Linux/devel/debuggers/에서 구할 수 있고 오리지널 소스는 ftp://ftp.x.org/contrib/ups-2.45.2.tar.Z에서 찾을 수 있다.

디버깅에 쓰이는 또 다른 툴 하나를 들자면 strace를 들 수 있다. strace는 프로그램이 만들어내는 시스템 호출을 화면에 표시해준다. 이것 말고도 다방면으로 사용가능한데, 예를 들어 어떠한 패스명이 소스코드를 갖고 있지 않은 바이너리 화일 안에 컴파일되어 들어가있는지, 분명히 바이너리 안에 들어있는 조건들을 발견하고자 할 때, 일반적으로 일반적으로 어떻게 작동하고 있는지를 알아내고자 할 때 사용한다. 최신 strace 버전(현재 3.0.8)은 ftp://ftp.std.com/pub/jrs/에서 구할 수 있다.

백그라운드 (데몬) 프로그램

데몬 프로그램들은 전형적으로 fork()를 먼저 하고 나서, 부모 프로세스를 종료시켜 버린다. 이는 디버깅 세션에 대하여 공격적인 요소임이 분명하다.

이럴 때 가장 간단한 방법은 fork에 대하여 정지점(breakpoint)을 지정해주는 것이고 프로그램이 멈추면 다시금 그것을 0 으로 만들어주는 것이다.

(gdb) list 
1       #include <stdio.h>
2
3       main()
4       {
5         if(fork()==0) printf("child\n");
6         else printf("parent\n");
7       }
(gdb) break fork
Breakpoint 1 at 0x80003b8
(gdb) run
Starting program: /home/dan/src/hello/./fork 
Breakpoint 1 at 0x400177c4

Breakpoint 1, 0x400177c4 in fork ()
(gdb) return 0
Make selected stack frame return now? (y or n) y
#0  0x80004a8 in main ()
    at fork.c:5
5         if(fork()==0) printf("child\n");
(gdb) next
Single stepping until exit from function fork, 
which has no line number information.
child
7       }

코어 화일(Core file)

보통 리눅스 부팅시에 코어 화일을 만들지 않도록 세팅되어 있다. 하지만 코어화일 생성을 가능케 하려고 한다면 그것을 다시 가능케 하는 셸의 내장 명령을 사용한다.

셸 호환 셸(예. tcsh)을 쓰고 있다면 다음과 같이 명령을 내린다.

% limit core unlimited

만약 본셸류(sh, bash, zsh, pdksh)를 사용하고 있다면,

$ ulimit -c unlimited

만약 코어 화일의 이름에 대하여 융통성을 가지고 싶다면, 커널 소스를 약간만 변경해주면 된다. 자, fs/binfmt_aout.cfs/binfmt_elf.c와 같은 화일을 찾아보자.

        memcpy(corefile,"core.",5);
#if 0
        memcpy(corefile+5,current->comm,sizeof(current->comm));
#else
        corefile[4] = '\0';
#endif

grep 같은 것을 가지고 이런 부분을 모두 찾은 후에 0이라고 되어 있는 것을 1이라고 모두 고쳐준다.

5.3 Profiling

Profiling이라고 하는 것은 프로그램의 어떤 부분이 제일 자주 호출되고 있는지 또는 많은 시간을 소요하고 있는지를 조사하는 것이다. 코드를 최적화시키고 시간이 가장 많이 소비되는 곳을 고쳐주는 좋은 방법이다. 이렇게 하기 위해서는 -p 옵션을 주어서 시간 정보를 오브젝트 화일들이 가질 수 있도록 다시 컴파일해주어야 한다. 또한 binutil 패키지에 있는 gprof 라는 것을 필요로 한다. 자세한 사항은 gprof 맨페이지를 참고하기 바란다.

6. 링크

호환되지 않는 두 개의 바이너리 형식, 정적 라이브러리와 동적 라이브러리의 구분, 컴파일 과정 후에 일어나는 작업과 이미 컴파일을 마친 실행 프로그램이 실행될 때 일어나는 작업 둘 다에 대하여 "링크"라는 같은 말을 사용하여 생기는 혼란함(사실은 로드(load)한다라는 말에 대한 과부하라고 말할 수도 있다), 이런 모든 것에 대하여 다루므로 이번 섹션은 좀 복잡할 것이다. 말만 어려울 뿐이므로 크게 걱정할 필요는 없다.

이러한 혼란을 완화하기 위해서, 우리는 실행시(runtime)에 일어나는 일에 대하여 동적 로딩(Dynamic Loading)이라는 단어를 사용하겠다. 그리고 다음 섹션에 가서 다루고자 한다. 또는 동적 링킹(Dynamic Linking)이라는 단어로 표현되기도 한다. 이번 섹션에서는 오로지 컴파일 과정 바로 직후에 생기는 링크라는 작업에 대해서만 다루기로 한다.

6.1 정적 라이브러리 vs 공유 라이브러리

프로그램을 만드는 마지막 작업이 바로 링크(Link)라는 과정이다. 필요한 조각들을 모두 모으거나 어떤 부분이 빠져 있는지 알아보기 위한 과정이다. 분명히 프로그램들은 해야할 일이 많다. 이 모든 것을 일일이 다 짜주는 것은 아니다. 예를 들어 화일을 연다든지 하는 일인데 그러한 일들은 이미 여러분에게 라이브러리라는 형태로 주어져 있다. 평범한 리눅스 시스템에서는 /lib/usr/lib/에서 그러한 라이브러리들을 찾을 수 있다.

정적 라이브러리(Static Library)를 사용할 때, 링커는 프로그램이 필요로 하는 부분을 라이브러리에서 찾아서 그냥 실행화일에다 카피해버린다. 공유 라이브러리(또는 동적 라이브러리)의 경우에는 이렇게 하는 것이 아니라 실행화일에다가 단지 "실행될 때 우선 이 라이브러리를 로딩시킬 것"이라는 메세지만을 남겨놓는다. 당연히 공유 라이브러리를 사용하면 실행화일의 크기가 작아진다. 그들은 메모리도 또한 적게 차지하며, 하드 디스크의 용량도 적게 차지한다. 리눅스의 기본 행동은 일단 공유 라이브러리가 있으면 그것과 링크를 시키고, 그렇지 않으면 정적 라이브러리를 가지고 링크 작업을 한다. 공유 라이브러리를 쓴 실행화일을 얻고자 했는데, 우연찮게 정적 실행화일이 만들어졌다면 우선 공유 라이브러리가 제대로 있는지(a.out은 *.sa, ELF는 *.so)살펴보고 읽기 퍼미션이 주어져 있는지 알아본다.

리눅스에서 정적 라이브러리는 libname.a 과 같은 식의 이름을 갖는다. 그에 비해 공유 라이브러리는 libname.so.x.y.z 라는 식의 이름을 갖는데 x.y.z는 버전을 뜻한다. 또한 공유 라이브러리는 종종 링크되어 있다. (아주 중요) libname.so.x 그리고 libname.so라는 식의 링크를 갖는다. 표준 라이브러리들은 이 둘을 모두 가지고 있다.

여러분은 ldd라는 것을 사용함으로써 특정 프로그램이 어떤 공유 라이브러리를 원하는지 알 수 있다. (ldd = List Dynamic Dependencies)

$ ldd /usr/bin/lynx
        libncurses.so.1 => /usr/lib/libncurses.so.1.9.6
        libc.so.5 => /lib/libc.so.5.2.18

위 결과는 본인의 시스템에서 텍스트용 웹 브라우져로 사용하고 있는 lynx 라는 프로그램에 대하여 의존성 체크를 해본 결과이다. libc.so.5 (C 라이브러리)와 libncurses.so.1 (터미널 제어에 사용되는 라이브러리)를 필요로 하고 있다고 출력하고 있다. 아무런 공유 라이브러리도 필요없으면 그냥 `statically linked' 또는 `statically linked (ELF)' 라고만 출력한다.

6.2 라이브러리 들여다보기 (도대체 sin()은 어디에 들어있는가?)

nm libraryname 이라고 실행시키면 라이브러리 내의 모든 심볼을 출력해준다. 이는 공유 라이브러리와 정적 라이브러리 둘 다 적용된다. 만약 tcgetattr()이라는 함수를 찾고 싶다면 다음과 같이 해주면 된다.

$ nm libncurses.so.1 |grep tcget
         U tcgetattr

U가 뜻하는 바는 "undefined" 즉 ncurses 라이브러리가 사용하고는 있지만 아직 정의는 하지 않고 있다는 뜻이다.

이렇게도 할 수 있다.

$ nm libc.so.5 | grep tcget
00010fe8 T __tcgetattr
00010fe8 W tcgetattr
00068718 T tcgetpgrp

`W'는 "weak" 즉 심볼이 정의는 되어있으나 다른 라이브러리에 의해 재정의될 수 있는 형태라는 의미이다. 일반적으로 정상적인 경우에는 `T'라고 씌여진다.

sin()이 어디에 있는가라는 질문에 대한 가장 짧은 답은 libm.(so|a)이다. <math.h>에 정의되어 있는 모든 함수들은 바로 이 수학 라이브러리에 들어있다. 그것을 사용하기 위해서는 링크시에 -lm 옵션을 주어야 한다. using any of them.

6.3 화일 찾기

ld: Output file requires shared library `libfoo.so.1`

컴파일을 하다보면 위와 같은 메세지가 종종 나오는 것을 볼 수 있을 것이다. ld 그리고 유사한 프로그램들이 화일을 찾는 방식은 버전에 따라 다르지만 기본적으로 /usr/lib를 찾게 된다. 이 곳 말고도 다른 곳에 라이브러리를 가지고 있고 그것을 ld 에게 알려주기 위해서는 gcc 나 ld 에게 라이브러리가 잇는 디렉토리를 -L 옵션을 줘서 알린다.

-L 옵션을 주어도 안된다면, ld 가 원하는 화일이 적절한 장소에 가 있는지 확인해보라. a.out 에 대해서는 -lfoo 라고 하면 ld는 libfoo.sa (공유 라이브러리)를 찾게 된다. 만약 그것을 찾는데 실패하면 libfoo.a (정적 라이브러리)라는 화일을 찾는다. ELF에 한해서는 libfoo.so를 찾고 나서 libfoo.a를 찾는다. libfoo.solibfoo.so.x에 대한 링크이다.

6.4 여러분만의 라이브러리 만들기

버전 관리

다른 모든 프로그램과 마찬가지로 라이브러리 또한 계속적으로 버그를 잡아가야 한다. 또는 새로운 기능을 도입하거나 현재 있는 것을 더 효율적인 것으로 교체한다든지 그리고 필요없는 것은 없애버린다든지 하는 일이 필요하다. 이런 경우 변화하는 라이브러리를 가지고 프로그래밍하는 것은 문제가 아닐 수 없다. 만약 사라져버린 옛 기능에 의존하는 프로그램이라면?

그래서 우리는 라이브러리 버전이라고 하는 것을 도입한다. 그리고 라이브러리의 변화를 마이너 또는 메이저 변화 이렇게 분류하고 마이너 업그레이드는 기존의 프로그램들과 충돌이 없는 변화를 지칭하게 한다. 라이브러리의 버전은 화일명을 보면 알 수 있다. (사실 엄밀히 말하자면, ELF에 대해서는 거짓말이다. 왜 그러한지는 계속 읽어보면 나올 것이다) libfoo.so.1.2는 메이저 버전 1 이고 마이너 버전2 이다. 마이너 버전도 다소 중요한 것이 될 수도 있다. libc의 경우에는 마이너버전에다 패치레벨을 집어넣는다. 따라서 libc.so.5.2.18과 같은 이름이 생긴다. 숫자 말고도 문자, 언더스코어문자(_), 또는 프린트 가능한 문자를 넣어도 좋다.

ELF와 a.out 형식의 커다란 차이점 중에 하나가 바로 공유 라이브러리를 만드는 방식에 있다. 우선은 ELF를 알아보기로 하자. 왜냐하면 더 쉽기 때문이다.

ELF? 도대체 그게 무엇인가?

ELF (Executable and Linking Format)이라고 하는 것은 원래 USL(UNIX System Laboratories)라고 하는 곳에서 개발한 바이너리 형식이다. 그리고 현재는 솔라리스와 SVR4에서 사용 중이다. 리눅스가 사용해왔던 오래된 a.out보다 더욱 더 좋은 유연성 때문에 GCC와 C 라이브러리 개발자들은 지난 해 리눅스 표준 바이너리 형식과 마찬가지로 ELF로 이동하기로 결정하였다.

다시 한 번 더?

이번 섹션은 '/news-archives/comp.sys.sun.misc' 문서로부터 나오는 내용이다.

ELF ("Executable Linking Format)라고 하는 것은 "새롭고 향상된" 오브젝트 화일 형식으로서 SVR4 에 도입되었다. ELF는 그냥 COFF 방식보다 더욱 강력하다. 왜냐하면 사용자 확장성이 있기 때문이다. ELF는 오브젝트 화일을 임의의 길이를 갖는 섹션들의 리스트라고만 생각한다. 그것은 고정된 크기의 객체을 갖는 배열과는 다르다. 이러한 섹션은 COFF와는 달리 특정 위치에 있을 필요도 없고, 또한 특수한 순서대로 놓여있을 필요도 없다. 사용자들은 원한다면 새로운 섹션을 첨가할 수 있다. ELF는 또한 DWARF(Debugging With Attribute Record Format)라고 하는 아주 아주 강력한 디버깅 포맷을 가지고 있다. - 리눅스에서는 아직 완벽히 구현되고 있지는 않다. 하지만 작업이 진행 중이다 DWARF DIE들(또는 Debugging Information Entries) ELF 에서 .debug 섹션을 형성한다. 고정된 크기의 작은 정보들 대신에 DWARF DIE들은 각각 임의의 길이를 갖는 복잡한 속성들을 포함하고 있으며 영역별로 프로그램 데이타의 트리구조로씌여져 있다. DIE는 COFF .debug 섹션보다 많은 양의 정보를 잡아낼 수 있다.(COFF의 경우에는 C++ 계승 그래프와 같은 것들을 잡아낼 수 없다.)
ELF 화일들은 SVR4(솔라리스 2.0 ?)의 ELF 접근 라이브러리를 통해서 접근할 수 있다. 그 라이브러리는 ELF에 대하여 쉽고 빠른 인터페이스를 제공하고 있다. ELF 접근 라이브러리를 쓰면서 생기는 중요한 잇점중의 하나는 ELF 화일을 유닉스 화일로서 볼 필요가 전혀 없다는 것이다. 그것은 단지 Elf * 로서 접근가능하다. elf_open() 호출을 하면 그 다음부터 가능하다. 그 후에 elf_foobar()와 같은 작업을 한다. 이는 예전의 COFF 방식에서 실제 디스크 상의 이미지를 가지고 작업했던 것과는 전혀 다른 것이다.

ELF에 대한 찬성/반대, 그리고 현재의 a.out 시스템을 ELF 지원 시스템으로 업그레이드해야 할 필요성들은 ELF하우투 문서에서 다루고 있으며 본인은 그것을 여기에 적고자 하지는 않는다.

ELF 공유 라이브러리

libfoo.so라는 공유 라이비르러를 만들기 위한 기본적인 절차는 다음과 같다.

$ gcc -fPIC -c *.c
$ gcc -shared -Wl,-soname,libfoo.so.1 -o libfoo.so.1.0 *.o
$ ln -s libfoo.so.1.0 libfoo.so.1
$ ln -s libfoo.so.1 libfoo.so
$ LD_LIBRARY_PATH=`pwd`:$LD_LIBRARY_PATH ; export LD_LIBRARY_PATH

이렇게 하면 libfoo.so.1.0이라는 공유 라이브러리가 만들어질 것이다. 그리고 ld (libfoo.so 필요)와 동적 링커(libfoo.so.1 필요)에 필요한 적절한 링크가 만들어진다. 그것을 테스트해보기 위해서 우리는 LD_LIBRARY_PATH에다 현재 디렉토리를 첨가한다.

만약 라이브러리가 제대로 작동한다는 것을 확인하면, 그 라이브러리를 /usr/local/lib로 이동시킨다. 그리고 다시 링크를 만들어준다. libfoo.so.1로부터 libfoo.so.1.0에 이르는 링크는 ldconfig라고 하는 프로그램에 의해 항상 최신 정보로 관리된다. 보통은 부팅과정에서 알아서 해준다. 하지만 libfoo.so는 수동으로 해주어야 한다. 여러분이 한번에 한 라이브러리의 모든 부분들(예를 들어 헤더화일도 해당) 꼼꼼히 업그레이드해주려고 한다면 libfoo.so -> libfoo.so.1이라는 링크를 만들어 주면 된다. 그렇게 되면 ldconfig가 알아서 링크를 관리해준다. 만약에 이런 것까지 모두 여러분 스스로 모두 행하려고 한다면 나중에 문제가 생길 수도 있다. 분명히 말해두었다.

$ su
# cp libfoo.so.1.0 /usr/local/lib
# /sbin/ldconfig
# ( cd /usr/local/lib ; ln -s libfoo.so.1 libfoo.so )

버전 번호 붙이기, soname 그리고 심볼릭 링크

각 라이브러리는 soname이라는 것을 가지고 있다. 링커가 찾고 있는 라이브러리 안에서 이러한 이름을 발견하게 되면, 실제 화일명(libfoo.so와 같은 이름)이 아니라 soname이라고 하는 것을 실행 바이너리에 표시해둔다. 실행시에는 동적 로더가 soname을 갖는 화일을 찾게 된다. 이 역시 화일명이 아니다. 이는 무엇을 의미하는가? 하면 libfoo.so 화일명을 가진 라이브러리는 libbar.so라는 soname을 가질 수도 있고 그곳에 링크된 모든 프로그램은 결국 libbar.so를 찾는다는 것이다.

이것은 상당히 무의미한 기능처럼 보이는데 사실은 이것이야말로 같은 라이브러리의 서로 다른 버전이 어떻게 한 시스템에서 공존할 수 있는가를 이해하는데 있어 핵심적인 부분이다. 리눅스에서 라이브러리 이름짓는 사실상의 표준은 라이브러리를 libfoo.so.1.2 이런 식으로 부르고 libfoo.so.1이라는 soname을 부여하는 것이다. 만약 표준 라이브러리 디렉토리(예를 들어/usr/lib)에 추가되면 ldconfig는 libfoo.so.1 -> libfoo.so.1.2라는 링크를 만들어 줄 것이다. 그렇게 함으로써 실행시에 적절한 이미지가 선택되도록 해준다. 여러분은 또한 libfoo.so -> libfoo.so.1이라는 심볼릭 링크도 필요하다. 왜냐하면 ld 가 링크할 때 정확한 soname 을 찾게 하기 위해서이다.

따라서 라이브러리의 버그를 고칠 때 또는 새로운 기능을 첨가할 때(기존의 프로그램에 악영향을 주지 않는 변화들), 다시 라이브러리를 만들고 같은 soname을 주고 화일명은 바꾸도록 한다. 만약 여러분의 라이브러리와 링크되어 있는 기존의 프로그램들과 충돌하게 되는 라이브러리로 변화할 때는 soname의 숫자를 하나 늘리면 된다. 이러한 경우 새로운 버전의 라이브러리는 libfoo.so.2.0이 될테고, soname은 libfoo.so.2가 될 것이다. 그리고 이번에는 libfoo.so를 새로운 버전의 라이브러이에 심볼릭 링크시키도록 하자.

여러분이 꼭 이런 식으로 라이브러리 이름을 지어줄 필요는 없다. 하지만 그것은 괜찮은 관습이다. ELF는 여러분에게 라이브러리 이름짓기에 있어 유연성을 주고 있지만 그렇다고 해서 꼭 그렇게만 하라는 것은 아니다.

요약하자면, 여러분이 호환성을 깨는 것이 메이저 업그레이드이고 그렇지 않은 것이 마이너 업그레이드라는 전통을 준수한다면 다음과 같이 하라.

gcc -shared -Wl,-soname,libfoo.so.major -o libfoo.so.major.minor

모든 것이 제대로 될 것이다.

a.out 전통적인 형식

공유 라이브러리 만들기의 용이함은 ELF로의 업그레이드에 대한 중요한 이유이다. a.out으로 가능하기는 하다. ftp://tsx-11.mit.edu/pub/linux/packages/GCC/src/tools-2.17.tar.gz를 받아오자. 그리고 그 화일을 풀어서 나오는 20 페이지짜리 문서를 읽어본다. 남들에게 뻔히 보이는 열성지지자가 되고 싶지는 않다. 하지만 나는 나 자신을 귀찮게 하고 싶지는 않다. :-)

ZMAGIC vs QMAGIC

QMAGIC 이라고 하는 것은 예전의 a.out(ZMAGIC 이라고 알려져 있다)과 마찬가지로 실행 화일의 형식이다. 하지만 첫번째 페이지는 매핑하지 않는 바이너리이다. 0-4096 까지 어떠한 매핑도 존재하지 않기 때문에 이렇게 함으로써 NULL 디레퍼런시 트래핑(deference trapping)을 아주 쉽게 할 수 있다. 부차적인 효과로서 여러분의 실행화일은 약 1K 정도 작아지게 된다.

구식 링커들은 오로지 ZMAGIC 만을 지원한다. 약간 덜 구식의 링커들은 둘 다 지원하면, 최신 버전들은 오로지 QMAGIC 만을 지원하고 있다. 이것은 별로 중요하지 않다. 왜냐하면 커널 자체가 두 가지를 모두 실행시킬 수 있기 때문이다.

file 명령을 주면 그것이 QMAGIC인지 판별할 수 있을 것이다.

화일 위치(File Placement)

a.out(DLL) 공유 라이브러리는 2 개의 실제적인 화일 그리고 하나의 링크로 구성 되어 있다. 이 문서 전체를 통해서 계속 사용해온 이름인 foo 라는 라이브러리에 대하여 예를 들어 알아보자. foo 에 대하여 libfoo.sa, libfoo.so.1.2 그리고 libfoo.so.1 이라는 링크로 구성되어 있다. 링크는 libfoo.so.1.2를 가리킨다. 이것들 모두 무엇인가?

컴파일할 때 ldlibfoo.sa를 찾는다. 이것이야말로 라이브러리에 대한 그루터기 화일이 된다. 그리고 링크과정에 대한 모든 외부 데이타와 함수에 대한 포인터를 지니고 있다.

하지만 실행시에는 동적 로더가 libfoo.so.1을 찾는다. 이는 실제 화일이 아니라 심볼릭 링크이다. 그 이유는 앞서와 마찬가지로 라이브러리가 기존의 어플리케이션과의 충돌없이, 더 새로운, 버그가 잡힌 새로운 버전으로 교체될 수 있도록 하기 위해서이다. 새로운 버전이 나오면(예를 들어 libfoo.so.1.3)이라고 하자. ldconfig를 실행시키면 자동으로 libfoo.so.1 --> libfoo.so.1.3 링크 작업을 해 줄 것이다. 구버전을 쓰는 프로그램도 아무 이상이 없을 것이다.

DLL 라이브러리(동어반복이라는 사실은 알고 있다. 역자 주 :DLL 에 이미 라이브러리라는 말이 들어있다)는 종종 정적 라이브러리보다 크다. DLL은 미래의 확장성을 위해서 뻥 뚤린 구멍의 형태로 자리를 유보해둔다. 하지만 그 자리는 디스크 영역을 차지하지는 않도록 할 수 있다. 간단한 cpmakehole이라는 프로그램으로 이렇게 하는 것이 가능하다. 이미 고정된 위치에 주소들이 있으므로 라이브러리 생성 후에 strip 할 수 있다. 하지만 ELF 라이브러리에 대해서는 strip하지 말라.

libc-lite 란 무엇인가?

libc-lite 라고 하는 것은 libc 에 대한 소규모 버전이라고 할 수 있다. 하나의 플로피 안에 들어가고 유닉스의 자잘한 많은 업무들에 충분한 정도만으로 구성된 라이브러리이다. 그것은 curses 나 dbm, termcap 등의 코드를 포함하고 있지 않다. 만약 여러분의 /lib/libc.so.4가 lite 버전의 라이브러리에 링크되어 있다면 즉시 완전한 libc 버전으로 교체하기 바란다.

보통 슬랙웨어의 루트 디스켓을 마운트해보면 이 lite 버전의 C 라이브러리가 들어있음을 알 수 있을 것이다. 설치 준비와 설치에 필요한 만큼의 작은 C 라이브러리이다.

링크하기 : 일반적인 문제들

여러분의 링크 문제를 내게 보내달라! 그러면 그것에 대해서 나는 아무 일도 하지 않을 것이다. 하지만 많이 쌓이는 문제에 대해서는 글을 쓰겠다.

공유 라이브러리와 링크되길 바라는데 정적 라이브러리와 링크되고 있다.

우선은 ld가 공유라이브러리를 제대로 찾을 수 있도록 링크가 알맞게 되어 있는지 점검한다. ELF에 대해서라면 이것은 libfoo.so 심볼릭 링크를 말하며 a.out의 경우에는 libfoo.sa화일을 말하는 것이다. ELF binutil 2.5 버전에서 2.6 버전으로 업그레이드한 많은 사람들이 겪고 있는 문제이다. 전 버전이 공유 라이브러리에 대하여 오히려 더 똑똑하게 찾아냈는데, 그 사람들은 모든 링크를 제대로 만들지 않았던 것이다. 지적인 행동양식을 다른 모든 설계방식과의 호환성을 위해서 신버전에서 제거되었다. 지적 행동양식은 잘못된 가정을 갖게 되고 오히려 더 많은 문제를 낳기 때문에 그렇게 한 것이다.

DLL 툴인 mkimage 가 libgcc를 찾는데 실패한다.

libc.so.4.5.x와 그 이상의 버전에 관하여 libgcc는 더 이상 공유 라이브러리가 아니다. 따라서 여러분은 `-lgcc'와 같은 라인을 모두 `gcc -print-libgcc-file-name`로 바꿔주어야 한다. (주의할 것은 바로 백쿼우트문자(`)의 사용이다. 꼭 이 문자만을 사용하라.)

또한 모든 /usr/lib/libgcc* 화일들을 삭제하라. 이것이 중요하다.

__NEEDS_SHRLIB_libc_4도 마찬가지 문제이다.
DLL 생성시에 ``Assertion failure'' 메시지

이 메시지는 여러분이 가지고 있는 jump table 슬롯이 원래의 jump.vars화일에 너무 적은 공간 밖에 예약되지 않았기 때문에 오버플로우로 인해 생기는 문제이다. 여러분은 tools-2.17.tar.gz 패키지에 들어 있는 `getsize' 명령을 사용하여 그 범인을 찾아낼 수 있다. 아마도 유일한 해결책은 메이저 번호의 증가 밖에 없는 것 같다. 단지 이전 버전과 호환되도록 고려하면서 말이다.

ld: output file needs shared library libc.so.4

이러한 문구는 보통 libc가 아닌 라이브러리들 (즉, X 윈도우 라이브러리들...)하고 링크하려고 할 때 발생한다. -static을 함께 사용하지 않고 링크 시에 -g 옵션을 주었을 때이다.

공유 라이브러리에 대한 .sa 화일은 보통 정의되지 _NEEDS_SHRLIB_libc_4 라는 심볼을 가지고 있는데 나중에 libc.sa에서 해결된다. 하지만 -g 옵션을 주게 되면 libg.a 또는 libc.a와 링크되게 되므로 그 심볼은 해결이 되지 않게 되고 위와 같은 에러 메세지가 뜨게 되는 것이다.

결론적으로 -g 플래그로 컴파일할 때는 -static 이라는 옵션을 함께 주기 바란다. 또는 -g로 컴파일하지 않으면 된다. 링크할 것 없이 원하는 부분만 -g 옵션을 주고 컴파일해도 충분한 디버깅 정보를 얻을 수 있다.

7. 동적 로딩(Dynamic Loading)

이번 섹션은 지금 현재로선 아주 적은 내용만을 가지고 있다. ELF 하우투 문서를 발췌함으로써 그 내용이 계속적으로 늘어나게 될 것이다.

7.1 개념 잡기

리눅스는 공유 라이브러리를 가지고 있다. 이 글 전체를 읽는 동안 이제는 이런 말 듣는 것도 질렸을 것이다. 전통적으로 프로그램 링크 과정에서 행한 작업은 로딩 과정에서 그 반대 과정을 거쳐야 한다.

7.2 에러 메세지

can't load library: /lib/libxxx.so, Incompatible version

a.out 에서만 일어나는데, 이 말은 여러분의 라이브러리 메이저 버전이 틀리다는 말이다. 다른 버전을 가지고 있다고 해서 눈가림식으로 심볼릭 링크하는 것으로 안된다. 된다 할지라도 결국엔 세그폴트를 일으킬 것이다. 새로운 버전을 가져오라.ELF에서도 비스한 메세지가 나온다.

ftp: can't load library 'libreadline.so.2'
warning using incompatible library version xxx

a.out의 경우이다. 프로그램 컴파일한 사람보다 낮은 마이너 버전의 라이브러리를 갖고 있기 때문에 발생하는 경고 메세지이다. 프로그램이 실행되기는 할 것이다. 업그레이드하는 것이 어떨까?

7.3 동적 로더의 작동 제어하기

많은 환경 변수들이 동적 로더에 관계한다. 대부분은 일반 사용자보다는 ldd에게 유용하다. ldd에 다양한 스위치를 줌으로써 쉽게 세팅할 수 있다.

  • LD_BIND_NOW --- 일반적으로 함수가 호출되기 전까지는 라이브러리에서 찾아보지 않는다. 이 플래그를 세팅해주면 라이브러리 적재시에 모든 체크를 하게 되고 시작은 상당히 느리게 된다. 이것은 여러분이 만든 프로그램이 모든 것들과 제대로 링크가 되었는지 시험해볼 때 유용하다.
  • LD_PRELOAD --- overriding 함수 정의를 가지고 있는 화일에 세팅될 수 있다. 예를 들어서 메모리 할당 방법을 테스팅하려고 하며, malloc를 교체하려고 할 때는 여러분이 원하는 루틴으로 만든 후에 교체할 수가 있다. malloc.o 라는 이름으로 컴파일한 후 다음과 같이 해보자.
    $ LD_PRELOAD=malloc.o; export LD_PRELOAD
    $ some_test_program
    
    LD_ELF_PRELOADLD_AOUT_PRELOAD 이 둘은 비슷하다. 하지만 각각 특정 형태에만 관계한다. 만약 LD_ELF_PRELOADLD_PRELOAD가 둘 다 사용되었다면 좀 더 자세히 지정한 전자 LD_ELF_PRELOAD가 사용된다.
  • LD_LIBRARY_PATH --- 이것은 공유 라이브러리를 찾을 때 참고할 디렉토리를 콜론(:)을 분리자로 써서 표현한 리스트이다. 그것은 ld에 영향을 주지는 못한다. 단지 실행시에만 관계한다. 또한 setuid나 setgid를 갖는 프로그램에 대해서는 무용지물이다. 마찬가지로 LD_ELF_LIBRARY_PATHLD_AOUT_LIBRARY_PATH는 각각의 바이너리 형식에만 적용되도록 하고 있다. LD_LIBRARY_PATH는 정상적인 경우 그렇게 필요하진 않다. 대신에 /etc/ld.so.conf/에 디렉토리를 추가하고 ldconfig를 다시 한 번 실행시키는게 좋다.
  • LD_NOWARN --- 이는 a.out에만 적용된다. 예를 들어 다음과 같이 세팅하면 LD_NOWARN=true; export LD_NOWARN) 마이너 버전이 다르다든지 하는, 크게 심각하지 않는 경고를 표시하지 않도록 한다.
  • LD_WARN --- 이는 ELF에만 해당된다. 세팅되면 일반적으로 ``Can't find library''와 같은 심각한 에러를 경고로 바꾸어준다. 별로 필요없는 옵션이다.
  • LD_TRACE_LOADED_OBJECTS --- ELF에만 적용된다. 프로그램으로 하여금 ldd 하에서 실행되고 있다고 생각하게끔 만든다.
    $ LD_TRACE_LOADED_OBJECTS=true /usr/bin/lynx
            libncurses.so.1 => /usr/lib/libncurses.so.1.9.6
            libc.so.5 => /lib/libc.so.5.2.18
    

7.4 동적 로딩을 사용하는 프로그램 만들기

이는 솔라리스 2.x의 동적 로딩 지원이 이뤄지는 방식과 매우 흡사하다. H J Lu의 ELF 프로그래밍 문서에 자세히 나와 있으며 dlopen(3) 맨페이지에 아주 잘 나와 있다. 맨페이지는 ld.so 패키지에 들어있다. 다음 프로그램을 -ldl 옵션을 주고 링크하라.

#include <dlfcn.h>
#include <stdio.h>

main()
{
  void *libc;
  void (*printf_call)();

  if(libc=dlopen("/lib/libc.so.5",RTLD_LAZY))
  {
    printf_call=dlsym(libc,"printf");
    (*printf_call)("hello, world\n");
  }

}

8. 개발자와 연락하기(이하는 번역되어 있지 않습니다.)

8.1 Bug reports

Start by narrowing the problem down. Is it specific to Linux, or does it happen with gcc on other systems? Is it specific to the kernel version? Library version? Does it go away if you link static? Can you trim the program down to something short that demonstrates the bug?

Having done that, you'll know what program(s) the bug is in. For GCC, the bug reporting procedure is explained in the info file. For ld.so or the C or maths libraries, send mail to linux-gcc@vger.rutgers.edu. If possible, include a short and self-contained program that exhibits the bug, and a description both of what you want it to do, and what it actually does.

8.2 Helping with development

If you want to help with the development effort for GCC or the C library, the first thing to do is join the linux-gcc@vger.rutgers.edu mailing list. If you just want to see what the discussion is about, there are list archives at http://homer.ncm.com/linux-gcc/. The second and subsequent things depend on what you want to do!

9. The Remains

9.1 The Credits

Only presidents, editors, and people with tapeworms have the right to use the editorial ``we''.
(Mark Twain)

This HOWTO is based very closely on Mitchum DSouza's GCC-FAQ; most of the information (not to mention a reasonable amount of the text) in it comes directly from that document. Instances of the first person pronoun in this HOWTO could refer to either of us; generally the ones that say ``I have not tested this; don't blame me if it toasts your hard disk/system/spouse'' apply to both of us.

Contributors to this document have included (in ASCII ordering by first name) Andrew Tefft, Axel Boldt, Bill Metzenthen, Bruce Evans, Bruno Haible, Daniel Barlow, Daniel Quinlan, David Engel, Dirk Hohndel, Eric Youngdale, Fergus Henderson, H.J. Lu, Jens Schweikhardt, Kai Petzke, Michael Meissner, Mitchum DSouza, Olaf Flebbe, Paul Gortmaker, Rik Faith, Steven S. Dick, Tuomas J Lukka, and of course Linus Torvalds, without whom the whole exercise would have been pointless, let alone impossible :-)

Please do not feel offended if your name has not appeared here and you have contributed to this document (either as HOWTO or as FAQ). Email me and I will rectify it.

9.2 Translations

At this time, there are no known translations of this work. If you wish to produce one, please go right ahead, but do tell me about it! The chances are (sadly) several hundred to one against that I speak the language you wish to translate to, but that aside I am happy to help in whatever way I can.

dan@detached.demon.co.uk. My PGP public key (ID 5F263625) is available from my web pages, if you feel the need to be secretive about things.

9.4 Legalese

All trademarks used in this document are acknowledged as being owned by their respective owners.

This document is copyright (C) 1996 Daniel Barlow <dan@detached.demon.co.uk> It may be reproduced and distributed in whole or in part, in any medium physical or electronic, as long as this copyright notice is retained on all copies. Commercial redistribution is allowed and encouraged; however, the author would like to be notified of any such distributions.

All translations, derivative works, or aggregate works incorporating any Linux HOWTO documents must be covered under this copyright notice. That is, you may not produce a derivative work from a HOWTO and impose additional restrictions on its distribution. Exceptions to these rules may be granted under certain conditions; please contact the Linux HOWTO coordinator at the address given below.

In short, we wish to promote dissemination of this information through as many channels as possible. However, we do wish to retain copyright on the HOWTO documents, and would like to be notified of any plans to redistribute the HOWTOs.

If you have questions, please contact Tim Bynum, the Linux HOWTO coordinator, at linux-howto@sunsite.unc.edu via email.

10. Index

Entries starting with a non-alphabetical character are listed in ASCII order.

  • -fwritable-strings 39 56
  • /lib/cpp 16
  • a.out 1
  • ar 10
  • as 8
  • <asm/*.h> 19
  • atoi() 40
  • atol() 41
  • binaries too big 63 65 77
  • chewing gum 3
  • cos() 68
  • debugging 59
  • dlopen() 82
  • dlsym() 83
  • documentation 4
  • EINTR 52
  • elf 0 71
  • execl() 57
  • fcntl 47
  • FD_CLR 44
  • FD_ISSET 45
  • FD_SET 43
  • FD_ZERO 46
  • file 2
  • <float.h> 20
  • gcc 6
  • gcc -fomit-frame-pointer 61
  • gcc -g 60
  • gcc -v 14
  • gcc, bugs 15 28 29 84
  • gcc, flags 13 25 26
  • gdb 64
  • header files 17
  • interrupted system calls 51
  • ld 9
  • LD_* environment variables 80
  • ldd 81
  • libc 7
  • libg.a 62
  • libgcc 79
  • <limits.h> 21
  • lint 58
  • <linux/*.h> 18
  • manual pages 5
  • <math.h> 70
  • maths 69
  • mktemp() 55
  • optimisation 27
  • QMAGIC 76
  • segmentation fault 30 54
  • segmentation fault, in GCC 33
  • select() 50
  • SIGBUS 34
  • SIGEMT 35
  • SIGIOT 36
  • SIGSEGV 31 53
  • SIGSEGV, in gcc 32
  • SIGSYS 38
  • SIGTRAP 37
  • sin() 67
  • soname 73
  • sprintf() 42
  • statically linked binaries, unexpected 66 78
  • <stdarg.h> 23
  • <stddef.h> 24
  • strings 11
  • <sys/time.h> 48
  • <unistd.h> 49
  • <varargs.h> 22
  • version numbers 12 74
  • weird things 72
  • ZMAGIC 75
크리에이티브 커먼즈 라이센스
Creative Commons License
이올린에 북마크하기
Posted by 소리나는연탄.
TAGS ,

Leave your greetings here.

  
  
  
  
  
  
  
  
 

gcc와 make

2008/01/07 11:21 / Resource
참고문헌

Running LINUX(Matt Welsh, Lar Kaufman), "오렐리 출판사"

1. 시작하면서

2. gcc 강좌

3. make 강좌


1. 시작하면서

1.1 C 와 gcc 와의 관계

세상에서 제일 뛰어난 C 컴파일러 중 하나인 gcc 는 리눅스나 기타 자유 운영체제에 있어 커다란 보배가 아닐 수 없습니다. 우리가 알고 있는 유닉스가 C 언어로 거의 다 만들어졌듯이 리눅스의 모국어는 바로 gcc 입니다.

사실 많은 분들이 리눅스 해커(hacker), 구루(guru)의 경지가 되고 싶어합니다. 그렇게 되길 원하신다면 리눅스용의 모국어인 gcc 를 익히십시요. gcc 를 알면 리눅스를 아는 것이나 다름 없습니다. 사실 C 와 유닉스가 따로 떨어진 것이 아니라 어떻게 보면 일심동체라고 할 수도 있듯이 gcc 와 리눅스는 일심동체라고 봐도 무방합니다.

C 언어! 이는 유닉스와 심지어 마이크로소프트 제품에 이르기까지(어떤 식으로 변질되었든 간에 ) 컴퓨터 세상의 ``만국 공통어''입니다. 여태까지 이러한 언어의 통일을 이뤄낸 것은 C 언어밖에 없습니다. 컴퓨터 언어의 에스페란토어를 지향하는 많은 언어들( 자바, 티클/티케이 )이 나오고 있으나 이는 두고 볼 일입니다. 그리고 그 언어를 구사한다 할 지라도 C 언어는 역시나 ``기초 교양 언어''입니다.

여러분은 리눅스에서 gcc 를 통하여 그 동안 도스/윈도 환경에서 배운 엉터리 C 언어를 잊으셔야 합니다. 감히 말하건데 그것은 C 언어가 아닙니다. C 언어는 만국 공통어야 함에도 불구하고 몇몇 회사들, 도스/윈도와 같은 환경에서 변질되어 각 환경마다 ``새로운 문법''을 배워야 하는 어처구니없는 사태가 벌어졌습니다. 터보 C 와 MS-C를 배우면서 혼란도 많았고 그 뒤에 나온 녀석들은 완전히 다른 놈들이나 다름 없습니다.

지금 리눅스에서 여러분은 C 언어의 ``정통 소림권법''을 익히실 수 있습니다. 기초가 없이 비법만 전수받아 보았자 다른 곳에 가면 수많은 비법을 지닌 무림고수들에게 여지없이 깨지기 마련입니다. 하지만 아무리 괴로와도 처음에 물 길어오는 것, 마당 쓰는 일부터 시작하면 철통같은 신체를 단련하기 때문에 온갖 꽁수 비법으로는 여러분을 헤칠 수 없습니다. 또한 정통 권법을 연마한 사람은 기본기가 갖춰져 있으므로 대련 중에도 상대의 비법을 금방 간파하고 심지어 상대의 비법만마저 자신의 것으로 하기도 합니다. ^^


1.2 gcc 에 대한 이야기 하나

gcc 는 GNU 프로젝트에 의해 만들어진 작품의 하나로서 그 명성은 하늘을 찌를 듯합니다. GNU 프로젝트응의 산물 중 가장 멋진 것을 꼽으라면 저는 주저하지 않고 C 컴파일러의 최고봉인 gcc 를 지목할 것입니다.

실제로 gcc 의 명성은 뛰어나며 수많은 상용 회사도 스폰서를 해주고 있다는 것을 아시는지요? 예를 들어 넥스트사( 지금은 사라짐 )의 새로운 C 언어인 ``오브젝티브 C''는 gcc 를 가져다 만든 것이며 FSF 측에 다시 기증 되었습니다.

gcc 는 아주 강력합니다! 이미 상용 유닉스에 달려오는 AT&T 스타일, BSD 스타일의 C 언어 문법은 물론 ANSI C 를 기본으로 하여 모든 문법을 소화해낼 수 있으며 특유의 문법도 가지고 있습니다. 아주 구식 컴파일러, 아주 구식 문법도 소화해낼 수 있습니다. 이미 많은 사람들이 상용 유닉스에도 gcc 를 설컴치하여 사용하는 경우가 많지요. ( 물론 금전적인 문제가 많이 작용 ^^ )

gcc 는 매우 단순합니다. 어떤 의미로 이런 말을 하는가 하면, 터보 C/볼랜드 C 통합환경이나 윈도 환경의 비주얼한 환경을 제공하지 않는다는 것입니다. -.- 그들이 상당히 오토매틱한 성격을 갖는 반면, gcc 는 오로지 수동 스틱방식입니다. 각각의 장단점이 있지만 여러분이 일단 gcc 를 만나시려면 각오는 하고 계셔야 합니다. 도스/윈도에서 보던 것을 원하지 마십시요. gcc 는 껍데기에 신경쓸 겨를조차 없습니다. gcc 는 오로지 명령행 방식만을 제공합니다. 그리고 그 자체로 파워풀합니다. 개발 방향은 계속 ``뛰어난 능력''이지 겉모양 화장은 아닐 것입니다. ( 만약 겉모양을 원하신다면 그것은 여러분의 몫입니다. xwpe 같은 것이 그 좋은 예라고 할 수 있습니다 )

gcc 는 어떻게 보면 C 언어에 대한 개념이 서지 않는 사람에게는 무리인 C 컴파일러인 듯 합니다. 기초 지식없이 사용한다는 것은 불가능에 가깝습니다. 하지만 C 언어를 확실하게 기초부터 배워서 어디서든 쓰러지지 않는 무림고수가 되기 위해서는 gcc 를 권합니다. 자잘한 무공을 하는 깡패가 되느냐? 아니면 정신을 지닌 무림고수가 되느냐?는 여러분의 선택에 달렸습니다.

gcc 가 어렵기만 한가? 하면 그렇지는 않습니다. gcc 는 상당한 매력을 지니고 있습니다. 그 매력으로 인해 한 번 빠지면 다른 컴파일러가 상당히 우습게 보이기까지 합니다. ( 그렇다고 다른 컴파일러를 비웃지는 마세요 ^^ 그거 쓰는 사람들이 자존심 상해서 엄청 화를 낼 테니까요. 개인적으로 생각하기에 gcc 에 대적할 수 있을 정도되는 컴파일러는 와콤 C 컴파일러 정도? )

gcc 를 배우시려면 정신 무장(?)이 중요하다고 생각해서 이렇게 장황하게 읊었습니다. 심플하게 배우면서 여러분의 리눅스, C 컴파일러에 대한 두려움을 하나씩 없애고 C 언어 위에 군림하시기 바랍니다.

자, 이제는 잡담없이 시작합니다.

2. gcc 강좌

2.1 gcc 에 대한 기본 이해

명령행 상태에서 다음과 같이 입력해봅시다. 여러분이 사용하같고 있는 gcc 버전은 알아두고 시작하셔야겠죠?

 [yong@redyong yong]$ gcc -v
 Reading specs from /usr/lib/gcc-lib/i386-linux/2.7.2.1/specs
 gcc version 2.7.2.1
 [yong@redyong yong]$ 

gcc -v 이라고 입력하니까 ``Reading specs from..'' 이라같고 말하면서 그 결과값을 ``gcc version 2.7.2.1''이라고 말해주고 있습니다. 자, 어디서 gcc 에 대한 정보를 읽어오는지 봅시다.

  /usr/lib/gcc-lib/i386-linux/2.7.2.1/specs

gcc 를 여러분이 소스를 가져다 손수 설치해보신 적은 없을 것입니다. 보통은 바이너리 패키지로 된 것을 가져다 설치하지요. 나중에 정말 휴일에 너무 심심하다 싶으면 gcc 의 소스를 가져와서 컴파일해보십시요. 참, 재미있는 경험이 될 것입니다. 이미 여러분이 갖고 있는 gcc 를 가지고 새로운 gcc 를 컴파일하여 사용합니다. C 컴파일러를 가지고 새 버전의 C 컴파일러를 컴파일하여 사용한다! 이런 재미있는 경험을 또 어디서 해보겠습니까?

gcc 패키지가 어떤 것으로 구성되어 있는지.. gcc 가 제대로 설치되어 있는지 알아보면 좋겠죠?

다음과 같습니다.

 /lib/cpp       -----------> /usr/lib/gcc-lib/i386-linux/2.7.2.1/cpp ( 링크임 )
 /usr/bin/cc    -----------> gcc ( 링크임 )
 /usr/bin/gcc                C 컴파일러 ``front-end''
 /usr/bin/protoize
 /usr/bin/unprotoize
 /usr/info/cpp.info-*.gz     GNU info 시스템을 이용하는 화일들
 /usr/info/gcc.info-*.gz                        
 /usr/lib/gcc-lib

마지막 /usr/lib/gcc-lib 디렉토리에 아래에 gcc 에 관한 모든 내용이 설치됩니다.

보통 다음과 같은 디렉토리 구조를 가집니다.

        /usr/lib/gcc-lib/<플랫폼>/< gcc 버전 >

보통 우리는 리눅스를 i386 ( 인텔 환경 )에서 사용하고 있으므로 다음과 같이 나타날 것입니다.

        /usr/lib/gcc-lib/i386-linux/2.7.2.1

( i386-linux, i486-linux, i586-linux 는 각기 다를 수 있습니다. 하지만 상관없는 내용입니다. 미친 척 하고 다른 이름을 부여할 수도 있습니다. )

그럼 계속 해서 /usr/lib/gcc-lib 밑의 내용을 살펴보죠.

 /usr/lib/gcc-lib/i386-linux/2.7.2.1/cc1
 /usr/lib/gcc-lib/i386-linux/2.7.2.1/cpp
 /usr/lib/gcc-lib/i386-linux/2.7.2.1/include/*.h
 /usr/lib/gcc-lib/i386-linux/2.7.2.1/libgcc.a
 /usr/lib/gcc-lib/i386-linux/2.7.2.1/specs

cc1이 진짜 C 컴파일러 본체입니다. gcc 는 단지 적절하게 C 인가, C++ 인가 아니면 오브젝티브 C 인가를 검사하고 컴파일 작업만이 아니라 ``링크''라는 작업까지 하여 C 언어로 프로그램 소스를 만든 다음, 프로그램 바이너리가 만들어지기까지의 모든 과정을 관장해주는 ``조정자'' 역할을 할 뿐입니다.

C 컴파일러는 cc1, C++ 컴파일러는 cc1plus, 오브젝티브 C 컴파일러는 cc1obj 입니다. 여러분이 C++/오브젝티브 C 컴파일러를 설치하셨다면 cc1plus, cc1obj 라는 실행화일도 찾아보실 수 있을 겁니다. cpp 는 "프리프로세서"입니다. #include 등의 문장을 본격적인 cc1 컴파일에 들어 가기에 앞서 먼저(pre) 처리(process)해주는 녀석입니다.

참고로 g++ 즉 C++ 컴파일러( 정확히는 C++ 컴파일러 프론트 엔드 )에 대한 패키지는 다음과 같습니다.

 /usr/bin/c++   --------------------------->    g++ 에 대한 링크에 불과함
 /usr/bin/g++
 /usr/lib/gcc-lib/i386-linux/2.7.2.1/cc1plus    ( 진짜 C++ 컴파일러 )

오브젝티브 C 컴파일러 패키지는 다음과 같습니다.

 /usr/lib/gcc-lib/i386-linux/2.7.2.1/cc1obj
 /usr/lib/gcc-lib/i386-linux/2.7.2.1/include/objc/*.h
 /usr/lib/gcc-lib/i386-linux/2.7.2.1/libobjc.a

구성요소가 어떤 것인지 아셨으니 좀 도움이 되셨을 겁니다.

2.2 gcc 사용하기

hello.c 라는 지긋지긋한 소스 하나를 기준으로 설명합니다 ^^


#include <stdio.h>

int
main ( void )
{
  (void) printf ( "Hello, Linux Girls! =)\n" );
  return 0;
}

참고로 제일 간단한 소스는 다음과 같은 것입니다. ^^


main () {}

컴파일을 해보겠습니다! $ 는 프롬프트이지 입력하는 것이 아닌 것 아시죠?

 $ gcc hello.c
 $

무소식이 희소식이라... gcc <C소스 화일명> 이렇게 실행하고 나서 아무런 메시지도 나오지 않고 다음 줄에 프롬프트만 달랑 떨어지면 그것이 바로 컴파일 성공입니다.

여러분은 무심코 다음과 같이 결과 프로그램을 실행시키려 할 것입니다.

 $ hello
 bash: hello: command not found
 $

예. 땡입니다. ^^

여러분은 다음과 같이 실행시켜야 합니다.

 $ ./a.out

맨 앞의 도트 문자(.)는 현재 디렉토리를 의미합니다. 그 다음 디렉토리 구분 문자 슬래쉬(/)를 쓰고 유닉스 C 에서 ``약속한'' C 컴파일러의 출력 결과 바이너리 화일인 a.out 을 써줍니다.

이러한 습관은 아주 중요합니다. 여러분이 현재 디렉토리에 어떤 실행 화일을 만들고 나서 테스트를 해 보려고 한다면 꼭 ./<실행 화일명> 이라고 적어줍니다.

유닉스는 기본적으로 PATH 라는 환경변수에 있는 디렉토리에서만 실행화일을 찾을 뿐입니다. 만약 PATH 라는 환경변수에 현재 디렉토리를 의미하는 도트 문자(.)가 들어있지 않으면 현재 디렉토리의 실행화일은 절대 실행되지 않습니다. 게다가 현재 디렉토리를 PATH 환경 변수에 넣어준다 할 지라도 도스처렁럼 현재 디렉토리를 먼저 찾는다든지 하는 일은 없습니다. 오로지 PATH 에 지정한 순서대로 수행합니다.

실행 바이너리명이 계속 a.out 으로 나오면 좀 곤란하죠. 뭐 물론 mv 명령으로 a.out 의 이름을 바꾸면 되지만서도...

-o 옵션

-o 옵션( 소문자 o 임! )은 출력(output) 화일명을 정하는 옵션입니다. 위에서 우리는 hello.c 라는 소스를 가지고 일반적으로 hello 라는 이름의 실행화일을 만들고 싶어할 것입니다.

 $ gcc -o hello hello.c
       ^^^^^^^^

또는 다음과 같이 순서를 바꿔도 무방합니다.

 $ gcc hello.c -o hello
               ^^^^^^^^

워낙 유닉스 쪽은 명령행 방식이 전통적으로 주된 방식이라 명령행에서 하는 일은 뛰어납니다.

당연히 실행을 하려면 ./hello 라고 하셔야 합니다. 결과는 다음처럼 나오겠지요?

 $ ./hello
 Hello, Linux Girls! =)
 $

주의

제일 안좋은 습관 중 하나가 바로 테스트용으로 만든 소스라고 다음처럼 하는 것입니다.

 $ gcc -o test test.c
 $ test
 $

문제를 알아내기 위하여 위에서 작성한 hello.c 를 컴파일/링크해봅시다.

 $ gcc -o test hello.c
 $ test
 $

원하는 문자열이 출력되지 않았습니다. -.-

 $ ./test
 Hello, Linux Girls! =)
 $

-c 옵션

어떤 이유로 오로지 컴파일(compile) 작업만 하고 싶은 경우가 있습니다. 그럴 때는 다음과 같이 합니다.

 $ gcc -c hello.c
 $

그 결과 만들어지는 화일은 전통적으로 hello.c 에서 .c 부분을 떼어내고 .o 를 붙인 화일입니다. 오브젝트 화일, 목적 화일이라고 하지요.

hello.o 라는 화일이 만들어집니다.

여러분은 C 언어로 조금이라도 복잡한 프로그램을 만들기 시작하면 여러 개의 소스로 나누어서 전체 프로그램을 짜게 됩니다. 그럴 때는 각 소스가 전체 기능에 기여하는 특정 기능의 함수들을 가지게 되고 오로지 한 녀석만 main 함수를 가집니다.

만약 어떤 프로그램이 foo.c, bar.c 이렇게 두 개의 소스로 이루어져 있다고 합시다. 이럴 때는 다음과 같이 하는 것이 가능합니다.

방법(1)

 $ gcc -o baz foo.c bar.c
 $ ./baz
방법(2)

 $ gcc -c foo.c
 $ gcc -c bar.c

          또는
 
 $ gcc -c foo.c bar.c
 $ gcc -o baz foo.o bar.o
              ^^^^^^^^^^^
 $ ./baz

위에서 보면 "아니! C 컴파일러에 .c 즉 소스 화일이 아닌 오브젝트 화일도 막 써주나?"라는 생각을 하시게 될 겁니다.

그렇습니다! 왜냐? gcc 는 정확히 말해서 C 컴파일러가 아닙니다. gcc 라는 실행 화일 자체는 "C 컴파일러를 돌리는 녀석"입니다.

더욱 더 정확히 말해보겠습니다.

C 언어는 기본적으로 두 가지 과정을 거쳐야만 실행화일을 만들어냅니다.

  1. 컴파일 ( .c -------> .o )
  2. 링크 ( .o -------> 실행화일 a.out )

1. 과정을 실제로 맡는 것은 cc1 이라는 녀석이고 2. 과정을 맡는 것은 ld 라는 링커(linker)입니다.

gcc 는 상당히 편리한 도구로서 .c, .o 등의 화일명 꼬리말을 보고 적절하게 C 컴파일러와 링커를 불러다가 원하는 실행화일을 만들어줍니다. gcc 는 "컴파일러와 링커를 불러주는 대리인"입니다.

hello.c 를 괜히 어렵게 컴파일/링크해봅시다 ^^

 $ gcc -c hello.c
          ^^^^^^^
 $ gcc -o hello hello.o
                ^^^^^^^

gcc 가 얼마나 똑똑피한 놈인지 알아보죠.

 $ gcc -c hello.o

이게 무슨 의미가 있겠습니까? ^^

 gcc: hello.o: linker input file unused since linking not done

위와 같이 불평합니다. 링크 과정을 수행하지 않으므로 링커에 대한 입력 화일인 hello.o 를 사용하지 않았다!

-I 옵션

#include 문장에서 지정한 헤더 화일이 들어있는 곳을 정하는 옵션입니다. 아주 많이 사용되는 옵션 중 하나입니다.


 #include <stdio.h>
 #include "my_header.h"

전자( <> 문자를 쓴 경우 )는 시스템 표준 헤더 디렉토리인 /usr/include를 기준으로 화일을 찾아서 포함시킵니다. 표준 디렉토리이지요.

후자( "" 문자를 쓴 경우 )는 지금 컴파일러가 실행되고 있는 현재 디렉토리를 기준으로 헤더 화일을 찾습니다.

이 두 디렉토리가 아닌 곳에 대해서는 명시적으로 -I<디렉토리> 로 정해줍니다.

 $ gcc -c myprog1.c -I..
 $ gcc -c myprog1.c -Iinclude

첫번째는 헤더 화일이 현재 소스 하위 디렉토리(..)에 있다는 뜻이며 두번째는 현재 디렉토리의 include라는 디렉토리에 들어있다는 뜻입니다.

-I 옵션은 얼마든지 여러번 쓸 수 있으며 주어진 순서대로 헤더 화일을 검색합니다.

주의

디렉토리명은 -I 라는 문자 바로 다음에 붙여서 씁니다. 즉 -I <디렉토리>라는 식이 아니라 -I<디렉토리> 입니다. 또한 유닉스에 있어 표준 헤더 화일 디렉토리는 /usr/include 라는 사실을 기억하시기 바랍니다. 또한 리눅스에 있어서는 커널 소스가 아주 중요한데 리눅스 고유의 기능을 쓰는 리눅스용 프로그램의 경우에는 /usr/include/linux, /usr/include/asm, /usr/include/scsi (최신 커널의 경우) 라는 디렉토리가 꼭 있어야 하며 각각은 커널 소스의 헤더 디렉토리에 대한 링크입니다. 따라서 커널 소스를 꼭 설치해두셔야 합니다.

 /usr/include/linux   -------------->  /usr/src/linux/include/linux
 /usr/include/asm     -------------->  /usr/src/linux/include/asm  
 /usr/include/scsi    -------------->  /usr/src/linux/include/scsi

( 위에서 /usr/src/linux/include/asm은 사실 대부분의 경우 /usr/src/linux/include/asm-i386 이라는 디렉토리에 대한 링크입니다 )

각각 linux는 일반적인 C 헤더 화일, asm은 각 아키텍쳐별 의존적인 어셈블리 헤더 화일, 맨 마지막은 SCSI 장치 프로그래밍에 쓰이는 헤더 화일이 들어있는 곳입니다.

일반적으로 커널 소스( 약 6 메가 이상되는 소스 )는 /usr/src 에서 tar, gzip으로 풀어두는 것이 관례입니다.

맨 처음 프로그래밍을 할 때는 그렇게 많이 쓰지는 않는 옵션이지만 여러분이 다른 소스를 가져다 컴파일할 때 아주 많이 보게 되는 옵션이므로 일단 이해는 할 수 있어야겠죠?

-l 옵션과 -L 옵션

옵션을 제대로 이해하기에 앞서 ``라이브러리''라는 것에 대한 이야기를 먼 저 하지 않으면 안될 듯 하군요.

  • 라이브러리


       ``라이브러리(Library)''라는 것은 개념상 영어 단어 그대로입니다.
      무엇인가 유용한 지식을 한 곳에 모아둔 곳이라는 개념이지요.

       C 프로그래밍을 하다 보면 반복적으로 사용하게 되는 함수들이 있기
      마련이고 그것은 하나의 함수로 떼내어 어디에서든 유용하게 사용할
      수 있도록 합니다.

       이 함수가 극도로 많이 사용되는 경우에는 ``라이브러리''라는 것으
      로 만들어두고 매번 컴파일해둘 필요없이 가져다 사용할 수 있도록
      하지요.

       라이브러리에 대한 얘기는 다음 번에 또 하게 되겠지만 일단 지금
      필요한 지식만 쌓기로 하겠습니다.

       일반적으로 관례상 라이브러리는 화일명 끝이 .a 로 끝납니다.
      여기서 a 는 Archive 라는 의미일 것입니다.

       라이브러리의 예를 들어보도록 하죠. 지금 /usr/lib 디렉토리를 한
      번 구경해보십시요. 정말로 많은 라이브러리들이 있지요.

      libc.a
      libm.a
      libdb.a
      libelf.a
      libfl.a
      libg++.a
      libg.a
      libncurses.a
      libreadline.a
      libvga.a
      등등...

       이러한 라이브러리는 우리가 컴파일 과정을 거쳐서 만든 .o 화일을
      한 곳에 통들어 관리하는 것에 불과합니다. 따라서 archive 를 의미
      하는 .a 라고 이름을 짓게 된 것이죠. 라이브러리는 ``도서관''으로
      서 그냥 .o 를 무작위로 집어넣은 것은 아니고 당연히 도서관에는
      소장하고 있는 책에 대한 목록(index)을 가지듯 포함되어 있는 .o
      에 대한 인덱스(index)를 가지고 있습니다.

       라이브러리는 다음과 같이 구성되어 있다고 할 수 있는 것입니다.

            라이브러리 = 목차(index) + ( a.o + b.o + c.o + ... )
        
       libc.a 를 가지고 한 번 놀아볼까요? 라이브러리 아카이브를 관리하
      는 ar 이라는 GNU 유틸리티를 써보겠습니다.

      $ cd /usr/lib
      $ ar t libc.a
      assert-perr.o
      assert.o
      setenv.o
      ftime.o
      psignal.o
      mkstemp.o
      sigint.o
      realpath.o
      cvt.o
      gcvt.o
      ctype-extn.o
      ctype.o
      <등등... 계속>

      $ ar t libc.a | grep printf
      iofprintf.o
      ioprintf.o
      iosprintf.o
      iovsprintf.o
      iovfprintf.o
      printf_fp.o
      vprintf.o
      snprintf.o
      vsnprintf.o
      asprintf.o
      vasprintf.o
      printf-prs.o
      reg-printf.o
      $

       위에서 볼 수 있다시피 .o 화일들이 그 안에 들어있습니다.

       <주목>
       유닉스에서 라이브러리 이름은 lib 로 시작합니다.

간단하게 라이브러리를 하나 만들어서 사용해보도록 합시다.

이번 예제는 3 개의 화일로 이루어졌습니다.

        myfunc.h
        myfunc.c
        hello.c

첫번째 myfunc.h 헤더 화일의 내용입니다.


extern void say_hello ( void );

두번째 myfunc.c, 실제 함수 정의부입니다.


#include <stdio.h>
#include "myfunc.h"

void 
say_hello ( void )
{
  printf ( "Hello, Linux guys!\n" );
}

마지막으로 메인 함수(main)가 들어있는 hello.c 입니다.


#include "myfunc.h"

int
main ( void )
{
  say_hello ();
  return 0;
}

main 함수에서 say_hello 라는 함수를 사용하게 됩니다. 이 정도야 그냥 이렇게 해버리고 말죠 ^^

 $ gcc -o say_linux hello.c myfunc.c

하지만 라이브러리를 만들어보고 시험해보려고 하는 것이므로 일부러 어렵게 한 번 해보기로 하겠습니다.

 $ gcc -c myfunc.c
 $ ar r libmylib.a myfunc.o
 $ ar s libmylib.a
 $ ar t libmylib.a
 myfunc.o
 $ gcc -o say_linux hello.c -lmylib
                            ^^^^^^^^
 ld: cannot open -lmylib: No such file or directory

흠... 처음부터 만만치 않죠? ^^ 실패하긴 했지만 몇 가지를 일단 알아봅시다.

-l 옵션

링크(link)할 라이브러리를 명시해주는 옵션이 바로 -l ( 소문자 L ) 옵션입니다.

-I 옵션과 마찬가지로 바짝 붙여서 씁니다. 절대 떼면 안됩니다.

우리는 libmylib.a 라는 라이브러리를 만들어두었습니다. 그것을 사용하기 위해서는 -lmylib 라고 적어줍니다. 라이브러리 화일명에서 어떤 글자들을 떼내고 쓰는지 주목하십시요.

 libmylib.a
    ^^^^^  

앞의 lib 를 떼내고 맨 뒤에 붙는 .a 를 떼냅니다.

링크(link)라는 것이 어떤 것이 모르신다면 당장 C 프로그래밍 책을 다시 읽어보시기 바랍니다. 이 글에서 설명할 범위는 아닌 듯 합니다.

-L 옵션

ld 는 유닉스에서 사용되는 링커(Linker)입니다. C 프로그램 컴파일의 맨 마지막 단계를 맡게 되지요.

위에서 우리는 다음과 같은 에러 메세지를 만났습니다.

 ld: cannot open -lmylib: No such file or directory

자, 이제 배워야 할 옵션은 ``라이브러리의 위치를 정해주는'' -L ( 대문자 L ) 옵션입니다. 사용형식은 -L<디렉토리명> 입니다.

리눅스에서 어떤 라이브러리를 찾을 때는 /lib, /usr/lib, /usr/local/lib와 같은 정해진 장소에서만 찾게 되어 있습니다. 그것은 규칙이지요.

중요한 사실은 아무리 여러분 라이브러리를 현재 작업 디렉토리에 놓아두어도 ld 는 그것을 찾지 않는다는 사실입니다. ld 더러 라이브러리가 있는 장소를 알려주려면 다음과 같이 -L 옵션을 붙이십시요.

 $ gcc -o say_linux hello.c -lmylib -L.
                                    ^^^

-L. 은 현재 디렉토리에서 라이브러리를 찾으라는 말입니다. -L 옵션은 여러번 줄 수 있습니다.

성공적으로 컴파일되었을 겁니다.

 $ ./say_linux
 Hello, Linux guys!

지금까지 여러분은 gcc 옵션 중 두번째로 중요한 -I, -l, -L 옵션에 대하여 배우셨습니다. 그리고 또한 라이브러리 만들기에 대하여 맛보기를 하였습니다.


3. make 강좌

3.1 머릿말

소스 한두 개로 이루어진 C/C++ 언어 교양과목 과제물을 제출하는 것이 아니라면 약간만 프로젝트가 커져도 소스는 감당할 수 없을 정도로 불어나게 되고 그것을 일일이 gcc 명령행 방식으로 처리한다는 것은 상당히 곤역스러운 일입니다.

그래서 하나의 프로젝트를 효율적으로 관리하고 일관성있게 관리하기 위하여 Makefile 이라는 형식을 사용하고 make 라는 유틸리티를 사용합니다.

여러분이 리눅스에서 소스 형태로 되어 있는 것을 가져와서 컴파일하게 되면 보통 마지막에는 make 라는 명령, 또는 make <어쩌구> 이런 식으로 치게 됩니다.

make 라는 유틸리티는 보통 현재 디렉토리에 Makefile 또는 makefile 이라는 일정한 규칙을 준수하여 만든 화일의 내용을 읽어서 목표 화일(target)을 만들어냅니다. Makefile의 이름을 다르게 명시하고 싶을 때는 다음과 같이 합니다.

        $ make -f Makefile.linux

보통 멀티플랫폼용 소스들은 Makefile.solaris, Makefile.freebsd, Makefile.hp 이런 식으로 Makefile 을 여러 개 만들어두는 경향이 있지요. 또는 적절하게 만들어두어 다음과 같이 make <플랫폼> 라는 식으로 하면 컴파일되도록 하기도 합니다.

        $ make linux

이런 일은 보통의 관례일 뿐이죠. 더 예를 들어보자면 이런 식입니다. 우리가 커널 컴파일 작업할 때를 보십시요.

        $ make config           /* 설정 작업을 한다 */
        $ make dep              /* 화일 의존성을 검사한다 */
        $ make clean            /* 만든 화일들을 지우고 
                                   깨긋한 상태로 만든다 */
        $ make zImage           /* zImage(압축커널)를 만든다 */
        $ make zlilo            /* 커널을 만들고 LILO를 설정한다 */
        $ make bzImage          /* bzImage(비대압축커널)를 만든다 */
        $ make modules          /* 커널 모듈을 만든다 */
        $ make modules_install  /* 커널 모듈을 인스톨한다 */

복잡한 것같아도 우리는 항상 make, make, make ... 일관성있게 make 라고만 쳐주면 됩니다. ^^ 분량이 작은 소스들의 경우에는 일반적으로 다음만 해도 되는 경우가 많죠.

        $ make  또는 make all
        $ make install

영어권에 사는 사람들에게는 더욱 친밀하게 느껴질 겁니다. 그렇겠죠? ``만들라!''라는 동사를 사용하고 있는 것이고 그 다음에는 그들의 정상적인 어순에 따라 목적어가 나오죠.

        $ make install.man

또한 관례상 ``맨페이지'' 같은 것은 별도로 인스톨하도록 배려하는 경우가 많습니다. 프로그램에 대해 잘 아는 사람이라면 맨페이지를 자질구레하게 설치하고 싶지 않을 때도 많으니까요.

다른 사람에게 공개하는 소스라면 더욱 make 를 사용해야 합니다. 그들뿐 아니라 여러분 자신도 make 라고만 치면 원하는 결과가 나올 수 있도록 하는 것이 좋습니다. 많은 소스를 작성하다 보면 여러분 스스로도 까먹기 쉽상입니다.

일단 make를 사용하는 일반적인 관례를 익히는 것이 중요하다고 봅니다. 리눅스 배포판 패키지만 설치하지 마시고 적극적으로 소스를 가져다 컴파일해보십시요. 실력이든 꽁수든 늘기 시작하면 여러분은 더욱 행복해지실 수 있습니다. =)

3.2 make 시작해 봅시다.

일관성있게 make라고만 치면 모든 일이 술술 풀려나가도록 하는 마술은 Makefile이라는 것을 어떻게 여러분이 잘 만들어두는가에 따라 결정됩니다. 바로 이 Makefile 을 어떻게 만드는지에 대하여 오늘 알아봅니다.

상황 1)

        $ gcc -o foo foo.c bar.c

여기서 foo 라는 실행화일은 foo.c, bar.c 라는 2 개의 소스로부터 만들어지고 있습니다.

여러분이 지금 계속 코딩을 하고 있는 중이라면 이 정도쯤이야 가상콘솔 또는 X 터미널을 여러 개 열어두고 편집하면서 쉘의 히스토리 기능을 사용하면 그만이지만 하루 이틀 계속 해간다고 하면 곤역스러운 일이 아닐 수 없습니다.

자, 실전으로 들어가버리겠습니다. vi Makefile 해서 만들어봅시다. ( 편집기는 여러분 마음 )


 foo:   foo.o bar.o 
        gcc -o foo foo.o bar.o

 foo.o: foo.c
        gcc -c foo.c

 bar.o: bar.c
        gcc -c bar.c

입력하는데 주의하실 것이 있습니다. 자, 위 화일을 보십시요. 형식은 다음과 같습니다.


 목표:  목표를 만드는데 필요한 구성요소들...
        목표를 달성하기 위한 명령 1
        목표를 달성하기 위한 명령 2
        ...

Makefile은 조금만 실수해도 일을 망치게 됩니다.

맨 첫번째 목표인 foo 를 살펴보죠. 맨 첫 칸에 foo: 라고 입력하고 나서 foo가 만들어지기 위해서 필요한 구성요소를 적어줍니다. foo가 만들어지기 위해서는 컴파일된 foo.o, bar.o 가 필요합니다. 각 요소를 구분하는데 있어 콤마(,) 같은 건 사용하지 않고 공백으로 합니다.

중요! 중요! 그 다음 줄로 넘어가서는 <탭>키를 누릅니다. 꼭 한 번 이상은 눌러야 합니다. 절대 스페이스키나 다른 키는 사용해선 안됩니다. 목표 화일을 만들어내기 위한 명령에 해당하는 줄들은 모두 <탭>키로 시작해야 합니다. Makefile 만들기에서 제일 중요한 내용입니다. <탭>키를 사용해야 한다는 사실, 바로 이것이 중요한 사실입니다.

foo를 만들기 위한 명령은 바로 gcc -o foo foo.o bar.o 입니다.

다시 한 번 해석하면 이렇습니다. foo 를 만들기 위해서는 foo.o와 bar.o가 우선 필요하다.( foo: foo.o bar.o )

일단 foo.o, bar.o 가 만들어져 있다면 우리는 gcc -o foo foo.o bar.o 를 실행하여 foo 를 만든다.

자, 이제부터 사슬처럼 엮어나가는 일만 남았습니다.

foo를 만들려고 하니 foo.o와 bar.o 가 필요합니다!

그렇다면 foo.o는 어떻게 만들죠?


 
 foo.o: foo.c
        gcc -c foo.c

바로 이 부분입니다. foo.o는 foo.c를 필요로 하며 만드는 방법은 gcc -c foo.c입니다.

그 다음 bar.o 는 어떻게 만들죠?


 bar.o: bar.c
        gcc -c bar.c

이것을 만들려면 이것이 필요하고 그것을 만들기 위해서는 또 이것이 필요하고...

소스를 만들어서 해봅시다.

  • foo.c 의 내용

extern void bar ( void );

int
main ( void )
{
  bar ();
  return 0;
}

  • bar.c 의 내용

#include <stdio.h>

void
bar ( void )
{
  printf ( "Good bye, my love.\n" );
}

Makefile을 위처럼 만들어두고 그냥 해보죠.

        $ make 또는 make foo
        gcc -c foo.c
        gcc -c bar.c
        gcc -o foo foo.o bar.o

명령이 실행되는 순서를 잘 보십시요. 여기서 감이 와야 합니다. ^^

        $ ./foo
        Good bye, my love.

다시 한 번 실행해볼까요?

        $ make
        make: `foo' is up to date.

똑똑한 make는 foo를 다시 만들 필요가 없다고 생각하고 더 이상 처리하지 않습니다.

이번에는 foo.c 를 약간만 고쳐봅시다. return 0; 라는 문장을 exit (0); 라는문장으로 바꾸어보죠. 그리고 다시 한 번 다음과 같이 합니다.

        $ make
        gcc -c foo.c
        gcc -o foo foo.o bar.o

자, 우리가 원하던 결과입니다. 당연히 foo.c 만 변화되었으므로 foo.o 를 만들고 foo.o가 갱신되었으므로 foo도 다시 만듭니다. 하지만 bar.c는 아무변화를 겪지 않았으므로 이미 만들어둔 bar.o 는 그대로 둡니다.

소스크기가 늘면 늘수록 이처럼 똑똑한 처리가 필요하지요.

        $ rm -f foo
        $ make
        gcc -o foo foo.o bar.o

이것도 우리가 원하던 결과입니다. foo 실행화일만 살짝 지웠더니 make는 알아서 이미 있는 foo.o, bar.o 를 가지고 foo 를 만들어냅니다. :)

상황 2) 재미를 들였다면 이번에는 청소작업을 해보기로 합시다.


 clean:
        rm -f foo foo.o bar.o

이 두 줄을 위에서 만든 Makefile 뒷부분에 추가해보도록 합시다.

        $ make clean
        rm -f foo foo.o bar.o
        $ make
        gcc -c foo.c
        gcc -c bar.c
        gcc -o foo foo.o bar.o

make clean이라는 작업 또한 중요한 작업입니다. 확실히 청소를 보장해주어야 하거든요.

make, make clean 이런 것이 되면 상당히 멋진 Makefile 이라고 볼 수 있죠? 이번 clean 에서 보여드리고자 하는 부분은 이런 것입니다.

우리의 머리 속에 clean 이라는 목표는 단지 화일들을 지우는 일입니다.

clean: 옆에 아무런 연관 화일들이 없지요?

그리고 오로지 rm -f foo foo.o bar.o 라는 명령만 있을 뿐입니다. clean이라는 목표를 수행하기 위해 필요한 것은 없습니다. 그러므로 적지 않았으며 타당한 make 문법입니다.

상황 3)


 all: foo

이 한 줄을 Makefile 맨 앞에 넣어두도록 합시다.

        $ make clean
        $ make all
        gcc -c foo.c
        gcc -c bar.c
        gcc -o foo foo.o bar.o

이번예는 all 이라는 목표에 그 밑에 나오는 다른 목표만이 들어있을 뿐, 아무런 명령도 없는 경우입니다. 보통 우리는 make all 하면 관련된 모든 것들이 만들어지길 원합니다.

 all: foo1 foo2 foo3
 foo1: <생략>
 foo2: <생략>
 foo3: <생략>

이런 식으로 해두면 어떤 장점이 있는지 알아봅시다.

보통 make all 하면 foo1, foo2, foo3가 모두 만들어집니다. 그런데 어떤 경우에는 foo1만 또는 foo2만을 만들고 싶을 때도 있을 겁니다. 괜히 필요없는 foo3 같은 것을 컴파일하느라 시간을 보내기 싫으므로 우리는 단지 다음과 같이만 할 겁니다.

        $ make foo1
        $ make foo2

물론 일반적으로 다 만들고 싶을 때는 make all 이라고만 하면 됩니다.

make all 이건 아주 일반적인 관례이지요. 그리고 외우기도 쉽잖아요?

3.3 꼬리말 규칙, 패턴 규칙

잘 관찰해보시면 어쩌구.c -----------> 어쩌구.o 라는 관계가 매번 등장함을 알 수 있습니다. 이것을 매번 반복한다는 것은 소스 화일이 한 두 개 정도일 때야 모르지만 수십 개가 넘게 되면 정말 곤역스러운 일이라고 하지 않을 수 없지요.

다음과 같은 표현을 Makefile 에서 보는 경우가 많을 겁니다.


 .c.o:
        gcc -c ${CFLAGS} $<

여기서 .c.o 의 의미를 생각해보겠습니다. ".c 를 입력화일로 받고 .o 화일을 만든다"

        gcc -c ${CFLAGS} $<

이 문자을 보면 일단 눈에 띄는 것은 ${CFLAGS}라는 표현과 $< 라는 암호와도 같은 표현입니다. 여기서는 일단 $< 라는 기호의 의미를 알아보겠습니다.

유닉스에서 쉘을 잘 구사하시는 분들은 눈치채셨을 겁니다. 작다 표시(<)는 리다이렉션에서 입력을 의미하는 것을 아십니까? 그렇다면 $< 는 바로 .c.o 라는 표현에서 .c 즉 C 소스 화일을 의미합니다.

예를 들어 foo.c 가 있다면 자동으로

        gcc -c ${CFLAGS} foo.c

가 수행되며 gcc 에 -c 옵션이 붙었으므로 foo.o 화일이 만들어질 것입니다.

3.4 GNU make 확장 기능

.c.o 라는 전통적인 표현 말고 GNU 버전( 우리가 리눅스에서 사용하는 것은 바로 이것입니다 )의 make 에서 사용하는 방법을 알아봅시다.

위에서 예로 든 것을 GNU 버전의 make 에서 지원하는 확장문법을 사용하면 다음과 같습니다.


 %.o: %.c
        gcc -c -o $@ ${CFLAGS} $<

그냥 설명 전에 잘 살펴보시기 바랍니다.

우리가 위에서 알아보았던 표준적인 .c.o 라는 꼬리말 규칙(Suffix rule)보다 훨씬 논리적이라는 것을 발견하셨습니까?

우리가 바로 전 강의에서 main.o : main.c 이런 식으로 표현한 것과 같은 맥락이지요? 이것을 우리는 패턴 규칙(Pattern rule)이라고 부릅니다. 콜론(:) 오른쪽이 입력 화일이고 왼쪽이 목표 화일입니다. 화일명 대신 퍼센트(%) 문자를 사용한 것만 유의하면 됩니다. 여기서 foo.c 라는 입력화일이 있다면 % 기호는 foo 만을 나타냅니다.

        gcc -c -o $@ ${CFLAGS} $<

라는 표현을 해석해봅시다. ( 후  마치 고대 문자판을 해석하는 기분이 안드십니까? ^^ )

$< 는 입력화일을 의미하고 $@ 은 출력화일을 의미합니다. .c.o와 같은 꼬리말 규칙과 별 다를 바 없다고 생각하실 지 모르나 -o $@ 를 통하여 .o 라는 이름 말고 전혀 다른 일도 해낼 수 있습니다.

다음 예는 그냥 이런 예가 있다는 것만 한 번 보아두시기 바랍니다.


 %_dbg.o: %.c
        gcc -c -g -o $@ ${CFLAG} $<

 DEBUG_OBJECTS = main_dbg.o edit_dbg.o

 edimh_dbg: $(DEBUG_OBJECTS)
        gcc -o $@ $(DEBUG_OBJECTS)

%_dbg.o 라는 표현을 잘 보십시요. foobar.c 라는 입력화일(%.c)이 있다면 % 기호는 foobar 를 가리키므로 %_dbg.o 는 결국 foobar_dbg.o 가 됩니다.

기호정리

 $<     입력 화일을 의미합니다. 콜론의 오른쪽에 오는 패턴을 치환합니다.
 $@     출력 화일을 의미합니다. 콜론의 왼쪽에 오는 패턴을 치환합니다.
 $*     입력 화일에서 꼬리말(.c, .s 등)을 떼넨 화일명을 나타냅니다.

역시 GNU 버전이라는 생각이 들지 않으시는지요?

3.5 매크로(Macro) 기능

앞에서도 잠깐씩 나온 ${CFLAGS} 라는 표현을 보도록 합시다.

gcc 옵션도 많이 알고 make을 능수능란하게 다룰 수 있는 사람들은 다음과 같이 해서 자신의 프로그램에 딱 맞는 gcc 옵션이 무엇인지 알아내려고 할 것입니다.

 $ make CFLAGS="-O4"
 $ make CFLAGS="-g"

이제 매크로에 대한 이야기를 나눠볼까 합니다. 이 이야기를 조금 해야만 위의 예를 이해할 수 있다고 보기 때문입니다. 그냥 시험삼아 해보십시다. 새로운 것을 배우기 위해서는 꼭 어떤 댓가가 와야만 한다는 생각을 버려야겠지요?


 myprog: main.o foo.o
        gcc -o $@ main.o foo.o

이것을 괜히 어렵게 매크로를 이용하여 표현해보기로 하겠습니다.


 OBJECTS = main.o foo.o
 myprog: $(OBJECTS)
        gcc -o $@ $(OBJECTS)

여러분은 보통 긴 Makefile을 훔쳐 볼 때 이런 매크로가 엄청나게 많다는 것을 보신 적이 있을 겁니다. ^^


 ROOT = /usr/local
 HEADERS = $(ROOT)/include
 SOURCES = $(ROOT)/src

예상하시듯 위에서 HEADERS는 당연히 /usr/local/include가 되겠지요?

다음과 같은 문장도 있습니다.


 ifdef XPM
     LINK_DEF = -DXPM
 endif

  $ make XPM=yes

이렇게 하면 ifdef   endif 부분이 처리됩니다.

자, make CFLAGS="-O" 이런 명령을 한 번 봅시다. ${CFLAGS}에서 {} 표현은 유닉스 쉘에서 변수값을 알아낼 때 쓰는 표현입니다. CFLAGS 값을 여러분이 Makefile에 고정적으로 집어넣지 않고 그냥 make 만 실행하는 사람에게 선택권을 주기 위해서 사용하거나 자기 스스로 어떤 옵션이 제일 잘 맞는지 알아보기 위해서 사용합니다. 다른 옵션으로 컴파일하는 것마다 일일이 다른 Makefile을 만들지 말고 가변적인 부분을 변수화하는 것이 좋습니다.

3.6 마지막 주의 사항


 target:
        cd obj
        HOST_DIR=/home/e 
        mv *.o $HOST_DIR

하나의 목표에 대하여 여러 명령을 쓰면 예기치 않은 일이 벌어집니다. 기술적으로 말하자면 각 명령은 각자의 서브쉘에서 실행되므로 전혀 연관이 없습니다. -.- cd obj 도 하나의 쉘에서 HOST_DIR=/home/e도 하나의 쉘에서 나머지도 마찬가지입니다. 각기 다른 쉘에서 작업한 것처럼 되므로 cd obj 했다 하더라도 다음번 명령의 위치는 obj 디렉토리가 아니라 그대로 변함이 없이 현재 디렉토리입니다. 세번째 명령에서 HOST_DIR 변수를 찾으려 하지만 두번째 명령이 종료한 후 HOST_DIR 변수는 사라집니다.


 target:
        cd obj ; \
        HOST_DIR=/hom/e ; \
        mv *.o $$HOST_DIR

이렇게 적어주셔야 합니다. 세미콜론으로 각 명령을 구분하지요. 처음 두 줄의 마지막에 쓰인 역슬래쉬(\) 문자는 한 줄에 쓸 것을 여러 줄로 나누어 쓴다는 것을 나타내고 있습니다.

주의! 세번째 줄에 $HOST_DIR이 아니라 $$HOST_DIR인 것을 명심하십시요. 예를 하나 들어보죠. ^^


 all:
         HELLO="안녕하세요?";\
         echo $HELLO

Makefile의 내용을 이렇게 간단하게 만듭니다.

 $ make
 HELLO="안녕하세요?";\
 echo ELLO
 ELLO
<verb>

 우리가 원하는 결과가 아니죠?

 $HELLO를 $$HELLO로 바꾸어보십시요.

<verb>
 $ make
 HELLO="안녕하세요?";\
 echo $HELLO
 안녕하세요?


 all:
         @HELLO="안녕하세요?"; echo $$HELLO

명령의 맨 처음에 @ 문자를 붙여봅시다.

 $ make
 안녕하세요?

3.7 잠시 마치면서

Makefile에 대한 내용은 이것보다 훨씬 내용이 많습니다. 하지만 모든 것을 다 알고 시작할 수는 없겠지요? 이 정도면 어느 정도 충분하게 창피하지 않을 정도의 Makefile을 만들 수 있습니다.

참고로 autoconf/automake라고 하는 아주 훌륭한 GNU make 유틸리티를 시간나면 배워보시는 것도 좋습니다.

시간을 내서 리눅스에서의 C 프로그래밍에 필요한 다른 여러 가지 유틸리티들( 간접적이든 직접적이든 grep, awk, rcs, cvs 등 )의 간단/실전 사용법도 올려드릴까 생각 중입니다. ^^



출처: 이 문서는 나우누리 "Linux 사용자 모임" 12-2번 강좌란 게시판에 올라온 이만용님의 강좌 "gcc와 make에 대한 강좌"를 sgml문서로 만든 것입니다.


참고문헌

Running LINUX(Matt Welsh, Lar Kaufman), "오렐리 출판사"

크리에이티브 커먼즈 라이센스
Creative Commons License
이올린에 북마크하기
Posted by 소리나는연탄.
TAGS ,

Leave your greetings here.

  
  
  
  
  
  
  
  
 

2.링크와 로더

2008/01/07 10:35 / Resource

1. 서문

링킹(linking)은 여러가지 코드와 데이터를 묶어 메모리로 로드될 수 있는 하나의 실행가능한 파일을 만드는 작업이다. 링킹은 컴파일-타임때 행해질 수도 있고, 로드-타임(로더에 의해), 혹은 런-타임(응용 프로그램에 의해)때도 행해질 수 있다. 1940년대에는 이러한 링킹작업을 사람이 손수 하였다. 현재에는 공유 라이브러리(shared library)들을 동적으로 링킹시켜주는 등의 복잡한 일을 할 수 있는 링커(linker)라는 것이 있다. 이 문서는 링킹의 모든 과정, 예로 들자면 재배치(relocation)와 심볼 해석(symbol resolution)부터 위치 독립적(position independent)인 공유 라이브러리 지원등에 대해 다룬다.문제를 간단하고 이해하기 쉽게 하기위해, 나는 이 문서를 x86 아키텍처에 기반한 리눅스와 GNU 컴파일러(GCC)와 링커(ld)에 기반한 ELF(executable and linking format) 실행파일에 초점을 맞추었다. 그러나, 기본적인 링킹의 아이디어는 운영체제, 프로세서 또는 오브젝트 파일의 형식에 무관하게 적용될 수 있다.


1.1. 저작권 정보

이 문서는 2002년 11월 26일, Linux Journal에 Sandeep Grover씨가 Linkers and Loaders라는 제목으로 기재하신 글입니다. 원 저자에게 메일로 연락하여 실렸던 잡지의 이름과 원저자가 누군지를 밝히면 번역을 해도 좋다는 동의를 얻었습니다.


1.2. 피드백

이 문서에 대한 발전적인 제안이나 수정사항, 문제점 등에 대한 피드백은 언제든지 환영합니다. 메일을 보내 주십시오.


2. 컴파일러, 링커, 로더들의 동작: 기본 사항

a.c와 b.c 두 개의 프로그램이 있다고 가정하고, 쉘 프롬프트에서 a.c와 b.c를 gcc를 이용하여 아래와 같은 명령을 수행하면 다음과 같은 일들이 순서대로 수행된다.

gcc a.c b.c

  • a.c에 대하여 전처리기(preprocessor)를 수행시키고, 그 결과를 전처리된 임시파일에 저장한다.

    cpp [other-command-line options] a.c /tmp/a.i

  • a.i에 대하여 컴파일러를 수행시키고, a.s라는 어셈블러 코드를 생성한다.

    cc1 [other-command-line options] /tmp/a.i -o /tmp/a.s

  • a.s에 대하여 어셈블러를 수행시키고, a.o라는 오브젝트 파일을 생성한다.

    as [other-command-line options] /tmp/a.s -o /tmp/a.o

cpp, cc1, as는 GNU의 전처리기, 컴파일러, 어셈블러를 각각 나타내며, GCC 배포본 안에 들어있다.

위와 같은 스텝은 b.c에도 똑같이 적용되어 b.o라는 오브젝트 파일을 하나 더 생성하게 된다. 그러면 링커의 작업은 이러한 두 개의 오브젝트 파일들(a.o, b.o)을 입력으로 받아서 최종적으로 실행가능한 파일을 만드는 것이다.

ld [other-command-line options] /tmp/a.o /tmp/b.o -o a.out

최종적으로 만들어진 실행파일(a.out)은 이제 로드될 준비가 되었다. 이것을 실행시키기 위해서 우리는 쉘 프롬프트상에서 아래와 같이 타이핑한다.

./a.out

그러면 쉘은 로더를 불러 a.out의 코드와 데이터를 메모리로 복사하고, 프로그램내의 제일 처음으로 제어권을 넘긴다. 여기서 말하는 로더는 execve라는 것으로 실행가능한 오브젝트 파일의 코드와 데이터를 메모리로 로드하고 그 프로그램의 첫번째 명령어가 저장된 주소로 점프함으로써 프로그램을 수행하게 한다.

a.out이라는 명칭은 a.out 오브젝트 파일들안에 있는 어셈블러의 출력물에서 그 유래를 찾을 수 있다. 그 이후로 오브젝트 형식은 다양하게 바뀌어 왔지만, 그 이름은 계속 사용되어지고 있다.


3. 링커와 로더

링커와 로더는 많은 부분이 연관되어 수행되지만 개념적으로는 다른 작업들을 수행한다.

  • 프로그램 로딩(Program Loading). 이것은 프로그램을 실행가능한 상태로 만들기 위해 하드 디스크로부터 프로그램 이미지를 읽어서 메인 메모리로 복사하는 것을 말한다. 어떤 경우에는 프로그램 로딩이 저장(storage)공간을 할당하거나 가상주소를 디스크 페이지로 매핑하는 일도 한다.

  • 재배치(Relocation). 컴파일러와 어셈블러는 각각의 입력 파일들로부터 시작주소가 제로인 오브젝트 코드를 생성한다. 재배치라는 것은 프로그램의 각기 다른 부분들(코드와 데이터)에 대해 로드되는 주소를 할당하는 것이다. 이러한 작업은 같은 타입(코드 혹은 데이터)으로 정의된 모든 구간들을 하나의 구간으로 합치고, 이러한 구간들이 런-타임때 올바른 주소를 가리킬 수 있도록 조정하는 것을 말한다.

  • 심볼 해석(Symbol Resolution). 프로그램은 다양한 하위 프로그램(subprogram)들로 구성된다; 하나의 상위 프로그램이 다른 하위 프로그램을 참조하는 것은 심볼이라는 것을 통해 이루어진다. 링커의 작업은 이러한 심볼의 위치를 알아내어 상위 프로그램의 오브젝트 코드에 하위 프로그램의 주소를 기입하여 참조를 해석하도록 한다.

링커와 로더사이에는 중첩되는 일들과 각각 차이나는 일들도 있는데, 이렇게 생각하도록 하자: 로더는 프로그램이 로딩되도록 하며; 링커는 심볼을 해석하며; 링커와 로더, 둘 다 재배치를 할 수 있다.


4. 오브젝트 파일들

오브젝트 파일들은 세가지로 분류될 수 있다.

  • 재배치 가능한 오브젝트 파일(Relocatable object file). 이것은 바이너리 코드와 데이터를 가지고 있으며, 실행가능한 오브젝트 파일을 만들기 위해 컴파일-타임때 재배치 가능한 다른 오브젝트 파일들과 결합될 수 있는 것을 가리킨다.

  • 실행가능한 오브젝트 파일(Executable object file). 이것은 바이너리 코드와 데이터를 가지고 있으며, 메모리로 직접 로드되어 실행될 수 있는 것을 가리킨다.

  • 공유 오브젝트 파일(Shared object file). 이것은 재배치 가능한 오브젝트 파일의 특별한 타입으로, 로드-타임이나 런-타임때 동적으로 메모리로 로드되고 링킹될 수 있는 것을 가리킨다.

컴파일러와 어셈블러는 재배치 가능한 오브젝트 파일을 생성한다(공유 오브젝트 파일도 또한 생성한다). 링커는 이러한 오브젝트 파일들을 합쳐 실행가능한 오브젝트 파일들을 생성한다.

오브젝트 파일들은 시스템에 따라 그 형식이 다르다. 최초의 유닉스 시스템은 a.out 포맷을 사용하였다. System V의 초기 버전에서는 COFF(Common object file format)라는 것을 사용하였고, 윈도우즈 NT는 COFF의 변형인 PE(portable executable)라는 형식을 사용한다; IBM은 독자적인 IBM 360 형식을 사용한다. 리눅스와 솔라리스와 같은 현대적인 유닉스 시스템들은 유닉스 ELF(executable and linking format)포맷을 사용한다. 이 문서는 주로 ELF에 대해 다룬다.

표 1. 전형적인 재배치 가능한 ELF 오브젝트 파일의 형식

ELF Header
.text
.rodata
.data
.bss
.symtab
.rel.text
.rel.data
.debug
.line
.strtab

ELF 헤더는 4-byte magic문자열(177ELF)로 시작한다. ELF 재배치 가능한 오브젝트 파일의 각 구간의 의미는 아래와 같다.

  • .text, 컴파일된 코드의 머신 코드가 들어있다.

  • .rodata, read-only 데이터가 들어있다, printf문의 문자열등이 이에 해당한다.

  • .data, 초기화된 전역 변수들이 들어있다.

  • .bss, 초기화되지 않은 전역 변수들이 들어있다. BSS는 block storage start의 이니셜이고, 이 구간은 실제적으로 오브젝트 파일에서 공간을 차지하지 않고 단지 공간을 확보하는 역할만 한다.

  • .symtab, 프로그램에서 정의된 전역 변수들과 함수들에 대한 참조 정보를 가지고 있다. 이 테이블은 지역 변수에 대한 것은 담고 있지 않다; 지역 변수들은 스택에 의해 유지된다.

  • .rel.text, .text에 들어있는 각 머신 코드의 위치를 나타낸다. 이것들은 나중에 링커가 이 오브젝트 파일을 다른 오브젝트 파일들과 연결시킬때 필요하다.

  • .rel.data, 현재의 파일에서는 정의되어 있지 않지만 참조되는 전역 변수에 대한 재배치 정보를 담고 있다.

  • .debug, 지역, 전역 변수들에 대한 디버깅 심볼들이 들어있다. 이 구간은 컴파일러가 -g 옵션과 함께 수행될 때 생성된다.

  • .line, .text에 들어있는 머신 코드와 실제 C 코드의 라인 넘버에 대한 메핑 정보가 들어있다. 디버거 프로그램이 이 정보를 필요로 한다.

  • .strtab, .symtab, .debug 구간에 있는 심볼 테이블에 들어있는 스트링들에 대한 테이블이다.


5. 심볼들과 심볼 해석

모든 재배치 가능한 오브젝트 파일들은 심볼 테이블과 그와 관련된 심볼들을 가지고 있다. 링커의 관점에서 볼 때 심볼들을 다음과 같이 분류할 수 있다.

  • 현재의 파일에서 정의되고, 다른 파일들에서 참조되는 전역 심볼. 모든 non-static 함수들과 전역 변수들이 이 분류에 해당한다.

  • 현재의 파일에서 참조는 되나, 다른 곳에서 정의된 전역 심볼. extern으로 정의된 모든 함수들과 변수들이 이 분류에 해당한다.

  • 현재의 파일에서만 정의되고 참조되는 지역 심볼. 모든 static 함수들과 변수들이 이 분류에 해당한다.

링커는 심볼의 참조를 해석할 때, 입력으로 주어지는 재배치 가능한 오브젝트 파일의 심볼 테이블로부터 꼭 하나만 존재하는 심볼의 정의를 참조하여 심볼 참조를 해석한다. 지역 심볼(local symbol)은 그에 대한 다중 정의(multiple definitions)를 심볼 테이블이 가질 수 없으므로 쉽게 해석된다. 그러나 전역 심볼의 해석은 약간의 트릭이 요구된다. 컴파일 타임때, 컴파일러는 전역 심볼들을 strong 혹은 weak한 것으로 만드는데, 함수들과 초기화된 전역 변수들은 strong하게, 초기화되지 않은 변수들은 weak하게 만든다. 그러면 링커는 아래의 룰을 적용하여 심볼들을 해석하게 된다.

  1. 다중 strong 심볼들은 허가되지 않는다.

  2. 하나의 strong 심볼과 여러개의 weak 심볼들이 있으면, strong 심볼을 선택한다.

  3. 여러개의 weak 심볼들이 있으면, 그것들중 아무거나 선택한다.

예로, 다음과 같은 두 프로그램의 링킹은 링크-타임 에러를 낸다.

/* foo.c */

int foo() {
	return 0;
}
/* bar.c */

int foo() {
	return 1;
}

int main() {
	foo();
}

foo (전역 함수로써 strong 심볼이다)가 두 번 정의 되었으므로, 링커는 아래와 같은 에러 메세지를 낸다.

gcc foo.c bar.c

/tmp/ccM1DKre.o: In function 'foo':

/tmp/ccM1DKre.o(.text+0x0): multiple definition of 'foo'

/tmp/ccIhvEMn.o(.text+0x0): first defined here

collect2: ld returned 1 exit status

collec2는 GCC에 의해 호출되는 링커 ld의 wrapper이다.


6. 정적 라이브러리의 링킹

정적 라이브러리는 비슷한 형을 지닌 오브젝트 파일들의 집합이다. 이러한 라이브러리들은 디스크에 아카이브(archive) 형식으로 저장된다. 아카이브는 라이브러리를 구성하고 있는 것들을 좀 더 빠르게 검색하기 위해 디렉토리 정보를 또한 가지고 있다. 각각의 ELF 아카이브는 !arch\n (\n은 뉴라인을 뜻한다)의 8자로 구성된 magic 문자열로 시작한다.

정적 라이브러리들은 링커에게 인자 (arguments)로써 전달된다. 그러면 링커는 프로그램에서 참조되는 오브젝트 모듈들만을 복사한다. 유닉스 시스템에서 libc.a는 모든 C 라이브러리 함수들 (printf나 fopen등과 같은)을 담고 있다.

gcc foo.o bar.o /usr/lib/libc.a /usr/lib/libm.a

libm.a는 유닉스 시스템에서 sqrt, sin, cos과 같은 수학관련 함수들을 담고 있는 라이브러리이다.

정적 라이브러리를 이용할 때, 심볼 해석과정이 어떻게 이루어지나 보면, 링커는 커맨드 라인에서 입력으로 받은 재배치 가능한 오브젝트 파일들과 아카이브들을 왼쪽에서 오른쪽으로 스캔한다. 이러한 스캔 과정중에, 링커는 세가지의 집합을 유지한다. 먼저, 재배치 가능한 오브젝트 파일들이 실행 가능한 파일의 상태로 들어간 집합 O; 아직 해석되지 않은 심볼들을 담고 있는 집합 U, 이전의 입력 파일에서 정의된 심볼을 담고 있는 집합 D가 그것이다. 이러한 집합들은 초기에 비워진 상태이다.

  • 커맨드 라인의 각각의 입력 파일에 대해, 링커는 그것이 오브젝트 파일인지 아카이브인지를 먼저 결정한다. 만약 입력이 재배치 가능한 오브젝트 파일이면, 링커는 그것을 집합 O에 추가하고, 집합 U와 D를 업데이트한 후, 다음 입력 파일로 넘어간다.

  • 만약 입력 파일이 아카이브라면, 링커는 현재의 집합 U에 들어있는 아직 해석이 안된 심볼들을 해석하기 위해 아카이브를 구성하고 있는 멤버 모듈들을 스캔하여 풀어나간다. 만약 아카이브를 구성하고 있는 멤버 모듈 자체에도 해석이 안된 심볼들이 있으면, 그 멤버는 집합 O에 추가되고 집합 U와 D도 업데이트 된다. 이러한 과정은 아카이브를 구성하고 있는 모든 오브젝트 파일 모듈들에 대해 행해진다.

  • 만약 커맨드 라인에 주어진 모든 입력 파일들에 위의 두 스텝을 행하였는데도, 집합 U가 아직 비어 있지않다면, 링커는 에러 메세지를 내고 종료한다. 집합 U가 비어 있다면, 링커는 집합 O에 들어있는 모든 오브젝트 파일들을 병합하고 재배치시켜 실행가능한 출력 파일을 만들어 낸다.

위의 일련의 순서때문에 커맨드 라인에서 정적 라이브러리가 끝에 온다. 또한 라이브러리들 사이에 발생할 수 있는 순환적인 의존성도 주의깊게 살펴야한다. 입력으로 주어지는 라이브러리들은 순서대로 주어져서 아카이브의 멤버들이 참조할 수 있도록 해야하며, 정의된 하나의 심볼은 뒤따르는 커맨드 라인의 입력에 의해 참조되어야 한다. 만약 해석이 안된 심볼이 있고, 그 심볼이 여러 정적 라이브러리들내에서 정의되어 있으면, 커맨드 라인에서 처음에 주어진 라이브러리에 정의된 것을 받아들인다.


7. 재배치(Relocation)

링커가 모든 심볼을 해석하고 나면, 심볼 참조는 오직 하나의 심볼 정의만을 가지게 된다. 그 때, 링커는 아래 두 스텝으로 구성된 재배치 작업을 하게된다.

  • 섹션과 심볼정의들을 재배치한다. 링커는 같은 타입의 모든 섹션들을 새로운 하나의 섹션으로 통합한다. 예로 들면, 링커는 입력으로 받은 모든 재배치 가능한 오브젝트 파일들의 .data섹션을 합쳐 하나의 .data섹션을 만든다. 같은 과정이 .code에 대해서도 행해진다. 그런 후에 링커는 병합된 새로운 섹션과, 병합된 새로운 섹션내의 각 섹션, 그리고 모든 심볼들에 대해 런-타임 메모리 주소를 할당한다. 이러한 작업후에는 프로그램의 모든 코드와 전역 변수들은 고유한 로드-타임 주소를 가지게 된다.

  • 섹션들안에 있는 심볼의 참조를 재배치한다. 이 과정에서, 링커는 코드와 데이터 섹션에 있는 모든 심볼 참조를 수정하여, 그것들이 올바른 로드-타임 주소를 가지게 한다.

어셈블러가 해석안된 심볼들을 만날 때마다, 어셈블러는 오브젝트 파일의 .rel.text/.rel.data 섹션에 해석안된 심볼들을 위한 재배치 항목을 생성한다. 이러한 재배치 항목은 해석안된 심볼들이 어떻게 해석되어야 하는지에 대한 정보들을 담고 있다. 전형적인 ELF 재배치 항목은 다음과 같은 멤버들로 구성된다.

  • 옵셋, 재배치되어질 필요가 있는 심볼 참조의 섹션내애서의 옵셋을 나타내며, 혹은 디스크의 저장공간이 오브젝트 파일내에서 재배치되어질 필요가 있을 시, 이 값은 재배치될 필요가 있는 디스크 섹션의 처음부터 바이트단위로 얼마만큼 떨어져 있는가를 나타낸다.

  • 심볼, 이것은 심볼 테이블에서의 인덱스로서, 아직 해석이 안된 심볼이 심볼 테이블에서 몇 번째 위치에 있는가를 나타낸다.

  • 타입, 재배치 타입, 일반적으로 R_386_PC32, 이는 PC-relative 주소지정방식을 나타내며, R_386_32는 절대주소지정방식을 나타낸다.

링커는 재배치 가능한 오브젝트 모듈들 안에 있는 모든 재배치 엔트리에 대해 이 작업을 반복하고 그것들의 타입에 따라 해석안된 심볼들을 재배치한다. R_386_PC32는 재배치 주소를 S+A-P로 계산하며, R_386_32는 S+A로 계산한다. 이 계산에서, S는 재배치 항목의 심볼항목에 들어있는 값을 가리키며, P는 섹션 옵셋 혹은 재배치되는 저장장치의 주소를 나타낸다 (재배치 항목의 옵셋값으로부터 계산된다). 그리고 A는 재배치 가능한 필드를 계산하는데 필요한 주소이다.


1. 동적 링킹: 공유 라이브러리

위의 정적 라이브러리는 몇가지 중요한 단점들을 지니고 있다; 예로, printf나 scanf와 같은 함수들을 고려해보자. 이러한 함수들은 거의 모든 응용 프로그램에서 사용된다. 만약 시스템이 50~100개의 프로세스를 동작시키고 있다면, 각 프로세스는 각각 printf와 scanf의 실행 가능한 코드의 복사본을 가지고 동작하게 된다. 이것은 메모리 공간의 중대한 낭비를 초래한다. 공유 라이브러리는 이러한 정적 라이브러리의 단점을 해결한다. 공유 라이브러리는 런-타임때 메모리의 임의의 위치로 로드될 수 있는 오브젝트 모듈이다. 그리고 그것은 메모리에서 프로그램과 링킹될 수 있다. 공유 라이브러리는 종종 공유 오브젝트라고도 불리운다. 대부분의 유닉스 시스템에서는 .so로 공유 라이브러리 파일명이 끝나며; HP-UX에서는 .sl로 끝나고 마이크로 소프트사는 DLL(dynamic link libraries)로 부른다.

공유 오브젝트를 만들기 위해, 컴파일러는 다음과 같은 옵션을 가지고 호출된다.

gcc -shared -fPIC -o libfoo.so a.o b.o

위의 명령어는 컴파일러가 a.o, b.o라는 두 개의 오브젝트 모듈들로부터 libfoo.so라는 공유 라이브러리를 만들도록 한다. -fPIC 옵션은 컴파일러에게 위치 독립적인 코드(position independent code)를 만들도록 한다.

만약 bar.o라는 오브젝트 모듈이 a.o, b.o와 의존성이 존재한다고 가정하면, 링커는 다음처럼 불리워진다.

gcc bar.o ./libfoo.so

이 명령어는 로드-타임때 링크될 수 있는 a.out이라는 실행파일을 만든다. 여기서 a.out은 정적 라이브러리를 사용할 때는 포함되었던 a.o와 b.o 오브젝트 모듈을 포함하고 있지 않다. 이 실행파일은 런-타임때 libfoo.so와 함께 해석될 수 있는 재배치와 심볼 테이블만을 포함하고 있다. 따라서, a.out은 libfoo.so와의 의존성이 존재하는 부분적으로 실행가능한 파일(partially executable file)이다. 이 실행가능한 파일은 .interp라는 섹션을 가지고 있는데, 이 섹션은 동적링커(dynamic linker)의 이름을 가리키고 있다. 동적링커 자체도 리눅스에서는 ld-linux.so라는 공유 오브젝트이다. 그래서 실행파일이 메모리로 적재될 때, 로더는 제어권을 동적링커로 넘긴다. 동적링커는 공유 라이브러리들과 해당 프로그램의 주소공간을 매핑시킬 수 있는 start-up 코드를 가지고 있고 다음과 같이 동작한다.

  • libfoo.so의 코드와 데이터를 메모리 속으로 재배치한다.

  • a.out에 있는 참조들을 libfoo.so에 정의되어 있는 것으로 재배치한다.

마지막으로, 동적링커는 제어권을 응용프로그램으로 넘긴다. 이때부터 공유 오브젝트는 메모리에 고정되게 된다.


2. 응용 프로그램으로부터 동적 라이브러리 로딩

동적 라이브러리들은 응용 프로그램이 실행되고 있는 중에도 로드될 수 있다. 응용 프로그램은 공유 라이브러리들을 자신과 링킹하지 않아도, 동적링커에게 요청하여 공유 라이브러리들을 로드하고 링킹시킬 수 있다. 리눅스와 솔라리스 그리고 다른 시스템들에서는 이를 위해 동적으로 공유 오브젝트들을 로드할 수 있는 몇가지 종류의 함수들을 제공한다. 리눅스에서는 공유 오브젝트를 열 수 있는 dlopen; 공유 오브젝트의 심볼 테이블을 볼 수 있는 dlsym, 공유 오브젝트를 닫을 수 있는 dlclose와 같은 시스템 콜을 제공하며, 윈도우즈에서는 LoadLibrary와 GetProcAddress와 같은 함수들을 제공한다.

오브젝트 파일들을 조작할 수 있는 툴들

여기에 오브젝트 파일들과 실행파일들을 조사할 수 있는 툴들의 목록이 있다.

  • ar: 정적 라이브러리들을 만든다.

  • objdump: 가장 중요한 바이너리 툴; 바이너리 형식 오브젝트 파일의 모든 정보를 보여준다.

  • strings: 바이너리 파일의 출력가능한 모든 문자열들을 보여준다.

  • nm: 오브젝트 파일의 심볼 테이블에 정의된 심볼들의 리스트를 보여준다.

  • ldd: 오브젝트 바이너리 파일이 의존하고 있는 공유 라이브러리들의 목록을 보여준다.

  • strip: 심볼 테이블 정보를 지운다.

크리에이티브 커먼즈 라이센스
Creative Commons License
이올린에 북마크하기
Posted by 소리나는연탄.
TAGS ,

Leave your greetings here.

  
  
  
  
  
  
  
  
 

 

솔라리스를 사용하다보면 간혹 홈 디렉토리에 코어(core)화일이 생기는 것을 볼 수가 있습니다. 사용에 별 지장이 없다면 그냥 지나쳐도 상관없겠지만, 그 코어가 내가 운영하는 서비스와 관련이 있는 지도 모르므로 확인해보는 습관은 중요합니다.


대개 메모리 접근 위반(Segmentation Violation)으로 생성되는 것이 대부분인데, 솔라리스의 애플리케이션에 포인터 처리가 미숙했거나, 솔라리스가 허용하는 데이타 패싱이 아니었거나 솔라리스에서 허용하는 프로토타입과 상이하게 다르거나 하는 경우에 흔히 생깁니다.

일단 core 화일이 생기게 되면, 해당 애플리케이션의 current directory에 생성됩니다. 대게 많은 애플리케이션들이 홈에서 시작이 되므로, 혹은 홈으로 현재 디렉토리를 옮기게 되므로 홈 디렉토리에 core가 왕왕 생기는 것을 볼 수가 있습니다. 또한, 솔라리스 내장되어 있는 각종 패키지들(gnome)과 같은 데스크 탑 솔루션등도 시작할때 홈에서 시작하므로 버그가 있는 경우 홈에서 core를 많이 볼 수가 있습니다.

core가 생기면 어떤 화일이 어디에서 문제가 생겨서 죽었는 지를 다음과 같이 해서 알 수 있습니다.

#file core
core: ELF 32-bit MSB core file SPARC Version 1, from 'thunderbird-bin'

#pstack core

core 'core' of 3968: /opt/sfw/lib/thunderbird/thunderbird-bin
----------------- lwp# 1 / thread# 1 --------------------
ff3416e0 _lwp_kill (b, ffbfe4b0, 0, b, fc00, 1) + 8
0051086c ???????? (b, 0, 510748, 510400, a, c4)
ff340618 __sighndlr (b, 0, ffbfe620, 510784, 0, 1) + c
ff335710 call_user_handler (b, ffbffeff, 0, 0, fe2b2000, ffbfe620) + 3b8
010b4794 ???????? (6e6e800, ff0a49bc, 24002400, 28002800, 28002800, 6e6e800)
010b5b44 ???????? (6cf5090, 63636c8, ff000000, 0, 80000000, ff0a39e4)
ff0a6fc0 __1cYnsOutputStreamReadyEventMEventHandler6FpnHPLEvent__pv_ (6c6fa74,
6c6faa8, 6c6fa70, 6cf5090, 10b5ab8, 18d79a0) + 34
ff0c03a0 PL_HandleEvent (6c6fa74, 1985454, ff0a6f8c, 1985454, 1985450, 6c6fa74) + 14
ff0c02b8 PL_ProcessPendingEvents (12, 1, d, 0, 0, 1985450) + 7c
ff0c23cc __1cQnsEventQdDueueImplUProcessPendingEvents6M_I_ (196dd08, 80004000,
ff000000, 0, 0, 1aef090) + 20
0072b840 ???????? (1af2f38, 1, 196dd08, 1, 1, ff0c23ac)
fea55ac8 g_main_dispatch (196e898, feabec00, 0, 0, fffffffd, ffffffef) + 19c
fea56ffc g_main_context_dispatch (196e898, c8, 0, 1, feabec00, 196e898) + 9c
fea574c8 g_main_context_iterate (1, 1, 1, 196e898, 196e8a0, 9) + 454
fea57c44 g_main_loop_run (1b99fe0, feabec00, ff339c58, 199a2f8, feaaa800, feaaa800) + 348
fee2a424 gtk_main (0, 0, 1b99fe0, 1aa4500, feff1eb0, 4ce0) + d0
0072bba4 ???????? (1aa6ac8, 196dd08, 18332d4, 72bbc0, c1f30000, 0)
....

위의 core는 thunderbird-bin이 생성한 것임을 알 수가 있습니다.
위의 core는 불행하게도 두가지 문제가 있는데, 하나는 debugging symbol을 가지고 있지 않는 함수가 있다는 것이고, 하나는 C++에 의한 맹글링으로 인해 함수 이름을 알아보기 어렵다는 점이 있습니다.

일단, C++ mangling 문제는 다음과 같이 함으로써 해결할 수 있습니다.

$pstack core | /opt/SUNWspro/bin/c++filt | less
core 'core' of 3968: /opt/sfw/lib/thunderbird/thunderbird-bin
----------------- lwp# 1 / thread# 1 --------------------
ff3416e0 _lwp_kill (b, ffbfe4b0, 0, b, fc00, 1) + 8
0051086c ???????? (b, 0, 510748, 510400, a, c4)
ff340618 __sighndlr (b, 0, ffbfe620, 510784, 0, 1) + c
ff335710 call_user_handler (b, ffbffeff, 0, 0, fe2b2000, ffbfe620) + 3b8
010b4794 ???????? (6e6e800, ff0a49bc, 24002400, 28002800, 28002800, 6e6e800)
010b5b44 ???????? (6cf5090, 63636c8, ff000000, 0, 80000000, ff0a39e4)
ff0a6fc0 void*nsOutputStreamReadyEvent::EventHandler(PLEvent*) (6c6fa74, 6c6faa8, 6c6fa70, 6cf5090, 10b5ab8, 18d79a0) + 34
ff0c03a0 PL_HandleEvent (6c6fa74, 1985454, ff0a6f8c, 1985454, 1985450, 6c6fa74) + 14
ff0c02b8 PL_ProcessPendingEvents (12, 1, d, 0, 0, 1985450) + 7c
ff0c23cc unsigned nsEventQueueImpl::ProcessPendingEvents() (196dd08, 80004000,
ff000000, 0, 0, 1aef090) + 20
0072b840 ???????? (1af2f38, 1, 196dd08, 1, 1, ff0c23ac)
fea55ac8 g_main_dispatch (196e898, feabec00, 0, 0, fffffffd, ffffffef) + 19c
fea56ffc g_main_context_dispatch (196e898, c8, 0, 1, feabec00, 196e898) + 9c
fea574c8 g_main_context_iterate (1, 1, 1, 196e898, 196e8a0, 9) + 454
fea57c44 g_main_loop_run (1b99fe0, feabec00, ff339c58, 199a2f8, feaaa800, feaaa800) + 348
fee2a424 gtk_main (0, 0, 1b99fe0, 1aa4500, feff1eb0, 4ce0) + d0
0072bba4 ???????? (1aa6ac8, 196dd08, 18332d4, 72bbc0, c1f30000, 0)
....

c++filt는 썬 스튜디오를 설치하면 따라오는 유틸리티입니다.
기본적으로 /usr/ccs/bin/nm도 디맹글링을 지원합니다만 사용 환경이 다릅니다.

위에서 발생한 core는 thunderbird-bin이 링크하는 동적 라이브러리에서 출력 스트림 이벤트 핸들러에서 발생한 문제임을 알수가 있습니다. 어느 라이브러리에서 발생했는지 알기 위해서는 커널 디버거인 mdb를 사용해야 합니다.

> mdb core
Loading modules: [ libc.so.1 libuutil.so.1 ld.so.1 ]
> $C
ffbfe3f0 libc.so.1`_lwp_kill+8(b, ffbfe4b0, 0, b, fc00, 1)
ffbfe450 0x51086c(b, 0, 510748, 510400, a, c4)
ffbfe4c0 libc.so.1`__sighndlr+0xc(b, 0, ffbfe620, 510784, 0, 1)
ffbfe520 libc.so.1`call_user_handler+0x3b8(b, ffbffeff, 0, 0, fe2b2000, ffbfe620)
ffbfe8d8 0x10b4794(6e6e800, ff0a49bc, 24002400, 28002800, 28002800, 6e6e800)
ffbfe940 0x10b5b44(6cf5090, 63636c8, ff000000, 0, 80000000, ff0a39e4)
ffbfe9a8
libxpcom_core.so`__1cYnsOutputStreamReadyEventMEventHandler6FpnHPLEvent__pv_+0x34(6c6fa74, 6c6faa8, 6c6fa70, 6cf5090, 10b5ab8, 18d79a0)
ffbfea08 libxpcom_core.so`PL_HandleEvent+0x14(6c6fa74, 1985454, ff0a6f8c,
1985454, 1985450, 6c6fa74)
ffbfea68 libxpcom_core.so`PL_ProcessPendingEvents+0x7c(12, 1, d, 0, 0, 1985450)
ffbfeac8 libxpcom_core.so`__1cQnsEventQdDueueImplUProcessPendingEvents6M_I_+0x20(196dd08, 80004000, ff000000, 0, 0, 1aef090)
ffbfeb30 0x72b840(1af2f38, 1, 196dd08, 1, 1, ff0c23ac)
ffbfeb90 libglib-2.0.so.0.400.1`g_main_dispatch+0x19c(196e898, feabec00, 0, 0,
fffffffd, ffffffef)
ffbfebf8 libglib-2.0.so.0.400.1`g_main_context_dispatch+0x9c(196e898, c8, 0, 1,
feabec00, 196e898)

....

위에서 $C 명령어는 core가 유도된 스레드의 스택을 보여줍니다.
장애의 소스가 되는 관심있는 함수와 모듈을 알수 있습니다.
libxpcom_core.so를 확인하게 되면 앞서 보았던, void*nsOutputStreamReadyEvent::EventHandler(PLEvent*)
임을 알 수 있기 때문입니다. 따라서, 개발자들은 이 함수의 소스를 확인해야 합니다.

지금부터는 이 함수의 어느 부분이 문제인지 확인하기 위해서는 소스 디버거나 dtrace를 사용해야만 하겠죠.

그러나, 경험적으로 core의 몇가지 정보를 더 얻을 수 있다면, 문제가 무엇인지 직감하는데 도움이
되는 경우도 있습니다.

예를 들면, 해당 core가 어떤 크리덴셜(퍼미션)을 가지고 실행했는가 ?
해당 코어는 어떤 환경에서 실행되었는가등을 알면 매우 큰 도움이 되는 경우가 있습니다.
다음과 같은 유틸리티를 통해서 알수가 있습니다.

#pargs -e core
#pldd core
#pcred core

크리에이티브 커먼즈 라이센스
Creative Commons License
이올린에 북마크하기
Posted by 소리나는연탄.

Leave your greetings here.

  
  
  
  
  
  
  
  
 
« Previous : 1 : 2 : 3 : 4 : 5 : 6 : Next »