'Language/C'에 해당되는 글 66건

  1. 2011.09.08 02. 알아두면 정말 유용한 C소스 컴파일 과정(1)
  2. 2011.09.08 01. 컴파일의 의미
  3. 2011.09.08 링킹/로더
  4. 2011.09.08 모듈과 라이브러리
  5. 2011.09.08 라이브러리 공격
  6. 2011.09.08 라이브러리(2)
  7. 2011.09.08 라이브러리(1)
  8. 2011.09.08 library 의 사용
  9. 2011.09.07 gcc 컴파일 과정 요약
  10. 2011.09.07 gcc

02. 알아두면 정말 유용한 C소스 컴파일 과정(1)

Language/C 2011. 9. 8. 12:14

|| C 소스의 전체 컴파일 과정


gcc는 GNU에서 만든 C컴파일러라고 말하지만 사실 컴파일러 드라이버이다.

gcc는 실제 컴파일 과정을 담당하지 않고 전처리기와 C 컴파일러, 어셈블러, 링커를 각각 호출해 주는 역할만 한다.

이렇게 각각의 컴파일 과정에 필요한 명령을 순차적으로 호출해 주는 것을 컴파일러 드라이버라고 한다.


그럼 진짜 컴파일러는 어디에 있을까?

[oneone@study study]$ /usr/libexec/gcc/i686-redhat-linux/4.4.4/

cc1       cc1plus   collect2  f951


cc1: C compiler, -E 옵션을 붙이면 C전처리기로 동작.
cc1plus : C++ compiler, -E 옵션을 붙이면 C++ 전처리기로 동작
collect2 : 링커(내부에서 ld 호출)
f951 : 포트란 컴파일러


/usr/bin 디렉토리에 가보면 다음과 같은 파일들이 있는데, 이를 컴파일러 드라이버들이다.
cc : C컴파일러 드라이버, gcc와 동일
gcc : C컴파일러 드라이버
c++ : C++ 컴파이러 드라이버, g++ 와 동일
g++ : C++ 컴파일러 드라이버
gcj : 자바 컴파일러 드라이버(페도라 미 설치)

[gcc 컴파일 과정]

like.c소스 → [cpp0 or cc1 -E 전처리기] → like.i 전처리 결과 → [cc1 C컴파일러] → like.s 어셈블리 파일 →

→ [as 어셈블러] → like.o 오브젝트 파일 → [ld or collect2] → like 실행파일 


like.c 파일의 컴파일 과정을 알아보기 위해 다음 명령으로 컴파일 한다.

[oneone@study study]$ gcc -v -save-temps -o like like.c

 [oneone@study study]$ gcc -v -save-temps -o like like.c

Using built-in specs.

Target: i686-redhat-linux

Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-languages=c,c++,objc,obj-c++,java,fortran,ada --enable-java-awt=gtk --disable-dssi --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-1.5.0.0/jre --enable-libgcj-multifile --enable-java-maintainer-mode --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --disable-libjava-multilib --with-ppl --with-cloog --with-tune=generic --with-arch=i686 --build=i686-redhat-linux

Thread model: posix

gcc version 4.4.4 20100503 (Red Hat 4.4.4-2) (GCC)

COLLECT_GCC_OPTIONS='-v' '-save-temps' '-o' 'like' '-mtune=generic' '-march=i686'

 /usr/libexec/gcc/i686-redhat-linux/4.4.4/cc1 -E -quiet -v like.c -mtune=generic -march=i686 -fpch-preprocess -o like.i

ignoring nonexistent directory "/usr/lib/gcc/i686-redhat-linux/4.4.4/include-fixed"

ignoring nonexistent directory "/usr/lib/gcc/i686-redhat-linux/4.4.4/../../../../i686-redhat-linux/include"

#include "..." search starts here:

#include <...> search starts here:

 /usr/local/include

 /usr/lib/gcc/i686-redhat-linux/4.4.4/include

 /usr/include

End of search list.

COLLECT_GCC_OPTIONS='-v' '-save-temps' '-o' 'like' '-mtune=generic' '-march=i686'

 /usr/libexec/gcc/i686-redhat-linux/4.4.4/cc1 -fpreprocessed like.i -quiet -dumpbase like.c -mtune=generic -march=i686 -auxbase like -version -o like.s

GNU C (GCC) version 4.4.4 20100503 (Red Hat 4.4.4-2) (i686-redhat-linux)

        compiled by GNU C version 4.4.4 20100503 (Red Hat 4.4.4-2), GMP version 4.3.1, MPFR version 2.4.2.

GGC heuristics: --param ggc-min-expand=42 --param ggc-min-heapsize=24314

Compiler executable checksum: 72dc052d4f2936b9a3bac52ec96d8f03

COLLECT_GCC_OPTIONS='-v' '-save-temps' '-o' 'like' '-mtune=generic' '-march=i686'

 as -V -Qy -o like.o like.s

GNU assembler version 2.20.51.0.2 (i686-redhat-linux) using BFD version version 2.20.51.0.2-15.fc13 20091009

COMPILER_PATH=/usr/libexec/gcc/i686-redhat-linux/4.4.4/:/usr/libexec/gcc/i686-redhat-linux/4.4.4/:/usr/libexec/gcc/i686-redhat-linux/:/usr/lib/gcc/i686-redhat-linux/4.4.4/:/usr/lib/gcc/i686-redhat-linux/:/usr/libexec/gcc/i686-redhat-linux/4.4.4/:/usr/libexec/gcc/i686-redhat-linux/:/usr/lib/gcc/i686-redhat-linux/4.4.4/:/usr/lib/gcc/i686-redhat-linux/

LIBRARY_PATH=/usr/lib/gcc/i686-redhat-linux/4.4.4/:/usr/lib/gcc/i686-redhat-linux/4.4.4/:/usr/lib/gcc/i686-redhat-linux/4.4.4/../../../:/lib/:/usr/lib/

COLLECT_GCC_OPTIONS='-v' '-save-temps' '-o' 'like' '-mtune=generic' '-march=i686'

 /usr/libexec/gcc/i686-redhat-linux/4.4.4/collect2 --no-add-needed --eh-frame-hdr --build-id -m elf_i386 --hash-style=gnu -dynamic-linker /lib/ld-linux.so.2 -o like /usr/lib/gcc/i686-redhat-linux/4.4.4/../../../crt1.o /usr/lib/gcc/i686-redhat-linux/4.4.4/../../../crti.o /usr/lib/gcc/i686-redhat-linux/4.4.4/crtbegin.o -L/usr/lib/gcc/i686-redhat-linux/4.4.4 -L/usr/lib/gcc/i686-redhat-linux/4.4.4 -L/usr/lib/gcc/i686-redhat-linux/4.4.4/../../.. like.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i686-redhat-linux/4.4.4/crtend.o /usr/lib/gcc/i686-redhat-linux/4.4.4/../../../crtn.o



위의 출력에서 확인 할 수 있듯이 gcc는 내부적으로 cc1 -E, cc1, as, collect2를 각각 호출함을 알 수 있다.
컴파일시 옵션 -save-temps 옵션에 의해 컴파일 중간 과정에서 생긴 like.i파일과 like.s파일이 보존되었음을 확인 할 수 있다.
[oneone@study study]$ ls
like  like.c  like.i  like.o  like.s

like.i와 like.s를 각각 열어보면

like.i 

.... 생략 

extern char *ctermid (char *__s) __attribute__ ((__nothrow__));

# 908 "/usr/include/stdio.h" 3 4

extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__));




extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__)) ;



extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__));

# 938 "/usr/include/stdio.h" 3 4


# 2 "like.c" 2


int main()

{


 printf("I like you!\n");

 return 0;

}




 like.s

         .file   "like.c"

        .section        .rodata

.LC0:

        .string "I like you!"

        .text

.globl main

        .type   main, @function

main:

        pushl   %ebp

        movl    %esp, %ebp

        andl    $-16, %esp

        subl    $16, %esp

        movl    $.LC0, (%esp)

        call    puts

        movl    $0, %eax

        leave

        ret

        .size   main, .-main

        .ident  "GCC: (GNU) 4.4.4 20100503 (Red Hat 4.4.4-2)"

        .section        .note.GNU-stack,"",@progbits



이런 like.i파일과 like.s파일은 컴파일 중간 과정의 산물로서 컴파일러 오류로 인한 문제나 전처리 과정에서의

오류로 인한 문제가 발생 했을 때 유용하게 사용할 수 있다.


|| cc1 -E에 의한 전처리 과정


첫번째 과정은 헤더파일 삽입, 두 번째 과정은 매크로 치환 및 적용이다.

like.c 

 #include <stdio.h>


void myFunc(void);


int main()

{


        myFunc();

        return 0;

}


void myFunc(void)

{

        printf("I like you\n");

}


main() 함수 내에서 myFunc() 함수를 사용할려면 myFunc() 함수 사용 이전에 함수의 원형을 void myFunc();과 같은 형식으로 선언해야한다.

printf()함수는 include한 stdio.h파일에 선형이 되어있다. 헤더파일을 C 소스내에 include하면 전처리기가 include한 자리에 헤더파일의 내용을 붙인다. 헤더파일은 설정된 디렉토리 순으로 파일을 찾는다. include할 디렉토리를 추가하기 위해선 gcc 옵션으로 -I[디렉토리] 라고 추가하면 된다.


 osCode.c

 #include <stdio.h>

#include <stdlib.h>


void main(){


#ifdef __oneone__

        printf("oneone\n");

#elif __babo__

        printf("babo\n");

#endif


}



[oneone@study study]$ gcc -D__babo__ -g -o osCode osCode.c

[oneone@study study]$ ./osCode

babo


-D옵션은 C 소스 파일의 제일 첫부분에 #define한 것과 의미가 같다.
-D 옵션 인자들은 주로 운영체제별로 코드를 달리해야 될 때 이용한다.
현재 gcc 버전에서는 자동으로 운영체제따라 -D 인자값을 설정한다(리눅스 __linux__ , 윈도 __WINDOWS__)
소스상에서 define되지 않았지만 전처리기가 임의로 define 하는 것을 predefine 매크로 한다.

출처 : 

http://www.oneone.kr/?mid=UnixPUtil&page=1&document_srl=5653 

'Language > C' 카테고리의 다른 글

02. 알아두면 정말 유용한 C소스 컴파일 과정(3)  (0) 2011.09.08
02. 알아두면 정말 유용한 C소스 컴파일 과정(2)  (0) 2011.09.08
01. 컴파일의 의미  (0) 2011.09.08
링킹/로더  (0) 2011.09.08
모듈과 라이브러리  (0) 2011.09.08
:

01. 컴파일의 의미

Language/C 2011. 9. 8. 12:13

<컴퓨터 구조> 시간 떄 배운 개념이 나옴. 따로 설명안함

어셈블리어(instrution set) 와 기계어 개념. 모르면 공부하거나 패스하길.


[like.c 소스]

 #include <stdio.h>


int main()

{


        printf("I like you!\n");

        return 0;

}




다음 명령으로 컴파일 하면 like실행 파일을 얻을 수 있다.

[oneone@study study]$ gcc -g -o like like.c


다음으로 -g 옵션으로 컴파일한 like 실행 파일에 대해 objdump 명령을 다음과 같이 실행한다.
[oneone@study study]$ objdump -S like

생략 .....

080483b4 <main>:

#include <stdio.h>


int main()

{

 80483b4:       55                      push   %ebp

 80483b5:       89 e5                   mov    %esp,%ebp

 80483b7:       83 e4 f0                and    $0xfffffff0,%esp

 80483ba:       83 ec 10                sub    $0x10,%esp


        printf("I like you!\n");

 80483bd:       c7 04 24 94 84 04 08    movl   $0x8048494,(%esp)

 80483c4:       e8 27 ff ff ff          call   80482f0 <puts@plt>

        return 0;

 80483c9:       b8 00 00 00 00          mov    $0x0,%eax

}

 80483ce:       c9                      leave

 80483cf:       c3                      ret


080483d0 <__libc_csu_fini>:

 생략 ...

 80483b4:       55                      push   %ebp

 메모리주소    기계어                어셈블리어


xxd 명령으로 16진수로 출력하면 실제로 기계어가 들어있음을 알 수 있다.

[oneone@study study]$ xxd like

0000a20: 0000 0000 0000 0000 0000 0000 0000 0000  ................

0000a30: 0000 0000 0000 0000 0000 0000 0000 0000  ................

0000a40: 0000 0000 0000 0000 0000 0000 1b00 0000  ................

0000a50: 0100 0000 0200 0000 3481 0408 3401 0000  ........4...4...

0000a60: 1300 0000 0000 0000 0000 0000 0100 0000  ................

0000a70: 0000 0000 2300 0000 0700 0000 0200 0000  ....#...........

0000a80: 4881 0408 4801 0000 2000 0000 0000 0000  H...H... .......

0000a90: 0000 0000 0400 0000 0000 0000 3100 0000  ............1...

0000aa0: 0700 0000 0200 0000 6881 0408 6801 0000  ........h...h...

0000ab0: 2400 0000 0000 0000 0000 0000 0400 0000  $...............

0000ac0: 0000 0000 4400 0000 f6ff ff6f 0200 0000  ....D......o....

0000ad0: 8c81 0408 8c01 0000 2000 0000 0500 0000  ........ .......

0000ae0: 0000 0000 0400 0000 0400 0000 4e00 0000  ............N...

0000af0: 0b00 0000 0200 0000 ac81 0408 ac01 0000  ................

0000b00: 5000 0000 0600 0000 0100 0000 0400 0000  P...............

0000b10: 1000 0000 5600 0000 0300 0000 0200 0000  ....V...........

0000b20: fc81 0408 fc01 0000 4a00 0000 0000 0000  ........J.......

0000b30: 0000 0000 0100 0000 0000 0000 5e00 0000  ............^...

0000b40: ffff ff6f 0200 0000 4682 0408 4602 0000  ...o....F...F...

0000b50: 0a00 0000 0500 0000 0000 0000 0200 0000  ................

0000b60: 0200 0000 6b00 0000 feff ff6f 0200 0000  ....k......o....

0000b70: 5082 0408 5002 0000 2000 0000 0600 0000  P...P... .......

0000b80: 0100 0000 0400 0000 0000 0000 7a00 0000  ............z...

0000b90: 0900 0000 0200 0000 7082 0408 7002 0000  ........p...p...

0000ba0: 0800 0000 0500 0000 0000 0000 0400 0000  ................

0000bb0: 0800 0000 8300 0000 0900 0000 0200 0000  ................

0000bc0: 7882 0408 7802 0000 1800 0000 0500 0000  x...x...........

0000bd0: 0c00 0000 0400 0000 0800 0000 8c00 0000  ................

0000be0: 0100 0000 0600 0000 9082 0408 9002 0000  ................

0000bf0: 3000 0000 0000 0000 0000 0000 0400 0000  0...............

0000c00: 0000 0000 8700 0000 0100 0000 0600 0000  ................

0000c10: c082 0408 c002 0000 4000 0000 0000 0000  ........@.......

0000c20: 0000 0000 0400 0000 0400 0000 9200 0000  ................

0000c30: 0100 0000 0600 0000 0083 0408 0003 0000  ................

0000c40: 6c01 0000 0000 0000 0000 0000 1000 0000  l...............

0000c50: 0000 0000 9800 0000 0100 0000 0600 0000  ................

0000c60: 6c84 0408 6c04 0000 1c00 0000 0000 0000  l...l...........

0000c70: 0000 0000 0400 0000 0000 0000 9e00 0000  ................

0000c80: 0100 0000 0200 0000 8884 0408 8804 0000  ................

0000c90: 1800 0000 0000 0000 0000 0000 0400 0000  ................

0000ca0: 0000 0000 a600 0000 0100 0000 0200 0000  ................

0000cb0: a084 0408 a004 0000 2400 0000 0000 0000  ........$.......

0000cc0: 0000 0000 0400 0000 0000 0000 b400 0000  ................

0000cd0: 0100 0000 0200 0000 c484 0408 c404 0000  ................

0000ce0: 7c00 0000 0000 0000 0000 0000 0400 0000  |...............

0000cf0: 0000 0000 be00 0000 0100 0000 0300 0000  ................

0000d00: 4095 0408 4005 0000 0800 0000 0000 0000  @...@...........

0000d10: 0000 0000 0400 0000 0000 0000 c500 0000  ................

0000d20: 0100 0000 0300 0000 4895 0408 4805 0000  ........H...H...

0000d30: 0800 0000 0000 0000 0000 0000 0400 0000  ................

0000d40: 0000 0000 cc00 0000 0100 0000 0300 0000  ................

0000d50: 5095 0408 5005 0000 0400 0000 0000 0000  P...P...........

0000d60: 0000 0000 0400 0000 0000 0000 d100 0000  ................

0000d70: 0600 0000 0300 0000 5495 0408 5405 0000  ........T...T...

0000d80: c800 0000 0600 0000 0000 0000 0400 0000  ................

0000d90: 0800 0000 da00 0000 0100 0000 0300 0000  ................

0000da0: 1c96 0408 1c06 0000 0400 0000 0000 0000  ................

0000db0: 0000 0000 0400 0000 0400 0000 df00 0000  ................

0000dc0: 0100 0000 0300 0000 2096 0408 2006 0000  ........ ... ...

0000dd0: 1800 0000 0000 0000 0000 0000 0400 0000  ................

0000de0: 0400 0000 e800 0000 0100 0000 0300 0000  ................

0000df0: 3896 0408 3806 0000 0400 0000 0000 0000  8...8...........

0000e00: 0000 0000 0400 0000 0000 0000 ee00 0000  ................

0000e10: 0800 0000 0300 0000 3c96 0408 3c06 0000  ........<...<...

0000e20: 0800 0000 0000 0000 0000 0000 0400 0000  ................

0000e30: 0000 0000 f300 0000 0100 0000 3000 0000  ............0...

0000e40: 0000 0000 3c06 0000 5900 0000 0000 0000  ....<...Y.......

0000e50: 0000 0000 0100 0000 0100 0000 fc00 0000  ................

0000e60: 0100 0000 0000 0000 0000 0000 9506 0000  ................

0000e70: 2000 0000 0000 0000 0000 0000 0100 0000   ...............

0000e80: 0000 0000 0b01 0000 0100 0000 0000 0000  ................

0000e90: 0000 0000 b506 0000 1b00 0000 0000 0000  ................

0000ea0: 0000 0000 0100 0000 0000 0000 1b01 0000  ................

0000eb0: 0100 0000 0000 0000 0000 0000 d006 0000  ................

0000ec0: 8c00 0000 0000 0000 0000 0000 0100 0000  ................

0000ed0: 0000 0000 2701 0000 0100 0000 0000 0000  ....'...........

0000ee0: 0000 0000 5c07 0000 4800 0000 0000 0000  ....\...H.......

0000ef0: 0000 0000 0100 0000 0000 0000 3501 0000  ............5...

0000f00: 0100 0000 0000 0000 0000 0000 a407 0000  ................

0000f10: 3700 0000 0000 0000 0000 0000 0100 0000  7...............

0000f20: 0000 0000 4101 0000 0100 0000 0000 0000  ....A...........

0000f30: 0000 0000 dc07 0000 3400 0000 0000 0000  ........4.......

0000f40: 0000 0000 0400 0000 0000 0000 4e01 0000  ............N...

0000f50: 0100 0000 3000 0000 0000 0000 1008 0000  ....0...........

0000f60: 9600 0000 0000 0000 0000 0000 0100 0000  ................

0000f70: 0100 0000 5901 0000 0100 0000 0000 0000  ....Y...........

0000f80: 0000 0000 a608 0000 1200 0000 0000 0000  ................

0000f90: 0000 0000 0100 0000 0000 0000 1100 0000  ................

0000fa0: 0300 0000 0000 0000 0000 0000 b808 0000  ................

0000fb0: 6901 0000 0000 0000 0000 0000 0100 0000  i...............

0000fc0: 0000 0000 0100 0000 0200 0000 0000 0000  ................

0000fd0: 0000 0000 1410 0000 9004 0000 2500 0000  ............%...

0000fe0: 3500 0000 0400 0000 1000 0000 0900 0000  5...............

0000ff0: 0300 0000 0000 0000 0000 0000 a414 0000  ................

0001000: fa01 0000 0000 0000 0000 0000 0100 0000  ................

0001010: 0000 0000 0000 0000 0000 0000 0000 0000  ................

0001020: 0000 0000 0000 0000 3481 0408 0000 0000  ........4.......

0001030: 0300 0100 0000 0000 4881 0408 0000 0000  ........H.......

0001040: 0300 0200 0000 0000 6881 0408 0000 0000  ........h.......

0001050: 0300 0300 0000 0000 8c81 0408 0000 0000  ................

0001060: 0300 0400 0000 0000 ac81 0408 0000 0000  ................

0001070: 0300 0500 0000 0000 fc81 0408 0000 0000  ................

0001080: 0300 0600 0000 0000 4682 0408 0000 0000  ........F.......

0001090: 0300 0700 0000 0000 5082 0408 0000 0000  ........P.......

00010a0: 0300 0800 0000 0000 7082 0408 0000 0000  ........p.......

00010b0: 0300 0900 0000 0000 7882 0408 0000 0000  ........x.......

00010c0: 0300 0a00 0000 0000 9082 0408 0000 0000  ................

00010d0: 0300 0b00 0000 0000 c082 0408 0000 0000  ................

00010e0: 0300 0c00 0000 0000 0083 0408 0000 0000  ................

00010f0: 0300 0d00 0000 0000 6c84 0408 0000 0000  ........l.......

0001100: 0300 0e00 0000 0000 8884 0408 0000 0000  ................

0001110: 0300 0f00 0000 0000 a084 0408 0000 0000  ................

0001120: 0300 1000 0000 0000 c484 0408 0000 0000  ................

0001130: 0300 1100 0000 0000 4095 0408 0000 0000  ........@.......

0001140: 0300 1200 0000 0000 4895 0408 0000 0000  ........H.......

0001150: 0300 1300 0000 0000 5095 0408 0000 0000  ........P.......

0001160: 0300 1400 0000 0000 5495 0408 0000 0000  ........T.......

0001170: 0300 1500 0000 0000 1c96 0408 0000 0000  ................

0001180: 0300 1600 0000 0000 2096 0408 0000 0000  ........ .......

0001190: 0300 1700 0000 0000 3896 0408 0000 0000  ........8.......

00011a0: 0300 1800 0000 0000 3c96 0408 0000 0000  ........<.......

00011b0: 0300 1900 0000 0000 0000 0000 0000 0000  ................

00011c0: 0300 1a00 0000 0000 0000 0000 0000 0000  ................

00011d0: 0300 1b00 0000 0000 0000 0000 0000 0000  ................

00011e0: 0300 1c00 0000 0000 0000 0000 0000 0000  ................

00011f0: 0300 1d00 0000 0000 0000 0000 0000 0000  ................

0001200: 0300 1e00 0000 0000 0000 0000 0000 0000  ................

0001210: 0300 1f00 0000 0000 0000 0000 0000 0000  ................

0001220: 0300 2000 0000 0000 0000 0000 0000 0000  .. .............

0001230: 0300 2100 0000 0000 0000 0000 0000 0000  ..!.............

0001240: 0300 2200 0100 0000 0000 0000 0000 0000  ..".............

0001250: 0400 f1ff 0c00 0000 4095 0408 0000 0000  ........@.......

0001260: 0100 1200 1a00 0000 4895 0408 0000 0000  ........H.......

0001270: 0100 1300 2800 0000 5095 0408 0000 0000  ....(...P.......

0001280: 0100 1400 3500 0000 3083 0408 0000 0000  ....5...0.......

0001290: 0200 0d00 4b00 0000 3c96 0408 0100 0000  ....K...<.......

00012a0: 0100 1900 5a00 0000 4096 0408 0400 0000  ....Z...@.......

00012b0: 0100 1900 6800 0000 9083 0408 0000 0000  ....h...........

00012c0: 0200 0d00 0100 0000 0000 0000 0000 0000  ................

00012d0: 0400 f1ff 7400 0000 4495 0408 0000 0000  ....t...D.......

00012e0: 0100 1200 8100 0000 3c85 0408 0000 0000  ........<.......

00012f0: 0100 1100 8f00 0000 5095 0408 0000 0000  ........P.......

0001300: 0100 1400 9b00 0000 4084 0408 0000 0000  ........@.......

0001310: 0200 0d00 b100 0000 0000 0000 0000 0000  ................

0001320: 0400 f1ff b800 0000 2096 0408 0000 0000  ........ .......

0001330: 0100 1700 ce00 0000 4095 0408 0000 0000  ........@.......

0001340: 0000 1200 df00 0000 4095 0408 0000 0000  ........@.......

0001350: 0000 1200 f200 0000 5495 0408 0000 0000  ........T.......

0001360: 0100 1500 fb00 0000 3896 0408 0000 0000  ........8.......

0001370: 2000 1800 0601 0000 d083 0408 0500 0000   ...............

0001380: 1200 0d00 1601 0000 0083 0408 0000 0000  ................

0001390: 1200 0d00 1d01 0000 0000 0000 0000 0000  ................

00013a0: 2000 0000 2c01 0000 0000 0000 0000 0000   ...,...........

00013b0: 2000 0000 4001 0000 8884 0408 0400 0000   ...@...........

00013c0: 1100 0f00 4701 0000 6c84 0408 0000 0000  ....G...l.......

00013d0: 1200 0e00 4d01 0000 0000 0000 0000 0000  ....M...........

00013e0: 1200 0000 6a01 0000 8c84 0408 0400 0000  ....j...........

00013f0: 1100 0f00 7901 0000 3896 0408 0000 0000  ....y...8.......

0001400: 1000 1800 8601 0000 9084 0408 0000 0000  ................

0001410: 1102 0f00 9301 0000 4c95 0408 0000 0000  ........L.......

0001420: 1102 1300 a001 0000 e083 0408 5a00 0000  ............Z...

0001430: 1200 0d00 b001 0000 3c96 0408 0000 0000  ........<.......

0001440: 1000 f1ff bc01 0000 4496 0408 0000 0000  ........D.......

0001450: 1000 f1ff c101 0000 0000 0000 0000 0000  ................

0001460: 1200 0000 d101 0000 3c96 0408 0000 0000  ........<.......

0001470: 1000 f1ff d801 0000 3a84 0408 0000 0000  ........:.......

0001480: 1202 0d00 ef01 0000 b483 0408 1c00 0000  ................

0001490: 1200 0d00 f401 0000 9082 0408 0000 0000  ................

 



출처: 
http://www.oneone.kr/?mid=UnixPUtil&page=1&document_srl=5649 

'Language > C' 카테고리의 다른 글

02. 알아두면 정말 유용한 C소스 컴파일 과정(2)  (0) 2011.09.08
02. 알아두면 정말 유용한 C소스 컴파일 과정(1)  (0) 2011.09.08
링킹/로더  (0) 2011.09.08
모듈과 라이브러리  (0) 2011.09.08
라이브러리 공격  (0) 2011.09.08
:

링킹/로더

Language/C 2011. 9. 8. 11:15

문서 정보

원문: http://www.linuxjournal.com/article/6463 
출처 : http://blog.naver.com/simz/20024938289 
저자: Sandeep Grover, 2002.11.26 
정리: 이동우(leedw at ssrnet.snu.ac.kr), 2005.11.26


Linkers and Loaders

링킹(Linking)이란 코드와 데이타들을 한데 묶어 메모리에 로드될 수 있는 하나의 실행파일 형태로 만드는 작업을 말한다. 링킹은 compile time, load time(로더에 의해) 혹은 run time(어플리케이션에 의해)에도 이루어질 수 있다. 링킹의 유래는 1940년까지 거슬러 올라가는데 당시에는 수작업으로 이루어졌다고 한다. 현재는 링커(Linker)가 있어서 동적으로 링크되는 공유 라이브러리(shared library)와 같은 복잡한 작업을 수행해 주고 있다. 이 문서에서는 재배치(relocation)부터 심볼 해석(symbol resolution)까지 이르는 링킹의 각 측면들을 간단히 살펴보도록 하겠다. 편의상 x86 아키텍쳐에서 구동되는 Linux 기반의 ELF 포맷과 GNU 컴파일러(gcc)/링커(ld)에 촛점을 맞추어서 논의하도록 하겠다. 그러나 링킹의 기본 컨셉은 프로세서나 운영체제, 오브젝트 포맷과 상관없이 모두 동일하다고 보면 된다.


 

Compiler, Linker and Loader in Action: the Basics

 

다음과 같이 a.c와 b.c 두 개의 프로그램이 있다고 생각해 보자. 쉘에서 두 코드들을 gcc에 입력할 때 다음과 같은 작업들이 수행된다.

gcc a.c b.c


· a.c에 대해 전처리기(preprocessor)가 실행되어 전처리기 중간 파일(a.i)이 생성된다.

   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라는 object 파일이 생성된다.

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


cpp, cc1과 as는 각각 GUN 전처리기, 컴파일러, 어셈블러로서 표준 GCC 배포본에 들어있는 툴들이다.
위의 3단계 과정이 다시 b.c에 적용되어 b.o가 생성이 된다. 이제 링커는 2개의 오브젝트 파일(a.o, b.o)을 입력받아 최종 실행파일을 만들게 된다.

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




최종으로 생성되는 실행파일(a.out)은 이제 메모리에 로드될 수 있다. 실행파일을 실행하기 위해선 shell prompt 상에서 파일 이름만 타이핑해 주면 된다.

./a.out



쉘은 로더 함수를 호출하게 되고 로더는 실행파일 a.out속에 있는 코드와 데이타를 메모리에 복사한다. 그리고 프로그램의 시작 주소로 제어권을 넘겨주게 된다. 로더는 execve라 불리는 프로그램으로서 실행파일속에 들어 있는 코드와 데이타를 메모리에 로드하고 코드의 첫번째 주소에 위치한 instruction으로 점프하여 프로그램을 실행시키는 작업을 수행한다. 
a.out는 a.out 오브젝트 파일들의 "Assembler OUTput"이라는 의미에서 명명되었다.. 이후로 오브젝트 포맷은 변해갔지만 그 이름은 계속 사용되었다.



 

Linkers vs. Loaders


링커와 로더는 여러 유사한, 그러나 개념적으로는 다른 작업들을 수행한다.

· 프 로그램 로딩(Program loading). 이것은 하드 디스크에 저장되어 있는 프로그램 이미지를 메인 메모리에 복사하여 실행상태로 만드는 것을 말한다. 어떤 경우에는 프로그램 로딩은 디스크 페이지에 가상 주소를 매핑하거나 저장 영역을 할당하는 것도 포함된다.


· 재 배치(relocation). 컴파일러와 어셈블러는 입력받은 각 모듈에 대해 0번지로 시작하는 오브젝트 코드를 생성한다. 재배치는 같은 타입의 모든 section을 하나의 단일 section으로 통합함으로써 프로그램의 각 파트에 로딩 주소를 할당하는 작업이다.


· 심 볼해석(Symbol resolution). 하나의 프로그램은 여러 서브 프로그램으로 구성되어 있다. 하나의 서브 프로그램이 다른 프로그램을 참조할 때 심볼이 사용된다. 링커가 하는 일은 심볼의 위치를 기억해 두었다가 심볼을 사용하는 호출모듈의 오브젝트 코드를 패치하여 심볼 참조를 해석(resolve)한다.


링커와 로더의 기능 사이에는 겹치는 부분이 상당히 많다. 한가지 생각해 볼 수 있는 방법은, 로더는 프로그램 로딩을 수행하고, 링커는 심볼 해석(symbol resolution)을 담당며 둘 다 재배치(relocation)을 할 수 있다는 것이다.



 

Object Files


오브젝트 파일은 다음과 같이 3개의 형태로 나뉜다.

· 재배치 가능한 오브젝트 파일(relocatable object file). compile-time에 다른 재배치 가능한 오브젝트 파일과 통합할 수 있어 하나의 실행파일이 생성될 수 있는 형태의 바이너리 코드와 데이타를 포함한다.


· 실행 오브젝트 파일(executable object file). 메모리에 직접 로딩되어 실행될 수 있는 형태의 바이너리 코드와 데이타를 포함한다.


· 공유 오브젝트 파일(shared object file). load-time이나 run-time에 동적으로 메모리에 로드되고 링크될 수 있는 특수한 타입의 재배치 가능한 오브젝트 파일.


컴파일러와 어셈블러는 재배치 가능한 오브젝트 파일(또한 공유 오브젝트 파일도)을 생성한다. 링커는 그러한 오브젝트 파일을 통합하여 실행파일을 생성한다. 
오 브젝트 파일은 시스템에 따라 각기 다른 형태를 가지는데 최초 unix 시스템에서는 a.out 포맷을 사용했었다. system V의 초기 버전에서는 COFF(common object file format)을 사용했고 windows NT는 PE(portable executable)라 불리는 COFF의 변종을 사용한다. IBM은 자체적으로 설계한 IBM 360 포맷을, Linux나 Solaris와 같은 현대 unix 시스템은 UNIX ELF(execuatble and linking format)를 사용한다. 
이 문서에서는 주로 ELF에 대해 촛점을 두겠다.


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

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

위 그림은 전형적인 ELF relocatable object file의 포맷을 보여주고 있다. ELF 헤더는 4바이트 magic string('0x7f', 'E', 'L', 'F')으로 시작된다. ELF relocatbale object file 내에 있는 여러 section들을 살펴보면 다음과 같다.



· .text : 컴파일된 프로그램의 기계어 코드


· .rodata : 읽기 전용 데이타(read-only data). printf 구문에 들어가는 format string등이 이 영역에 저장된다.


· .data : 초기화된 전역 변수가 저장되는 영역


· .bss : 초기화되지 않은 전역 변수가 저장되는 영역. BSS는 "Block Storage Start"의 약자이다. 이 section은 실제로는 오브젝트 파일내에서 아무 공간도 차지하지 않고 있다.


· .symtab : 프로그램 내에서 정의되거나 참조되고 있는 전역 변수와 함수에 대한 정보를 담고 있는 심볼 테이블. 이 테이블에는 로칼 변수에 대한 정보는 담고 있지 않은데 이는 스택이 그 정보를 담고 있기 때문이다.


· .rel.text : 링커가 이 오브젝트 파일과 다른 오브젝트 파일을 combine할 때 .text 내에서 변경되어야 할 위치들의 리스트를 담고 있는 영역.


· .rel.data : 현재 모듈에서 참조만 되고 정의되어 있지 않은 전역 변수의 재배치 정보


· .debug : 지역 및 전역 변수를 엔트리로 가지는 디버깅 심볼 테이블. 이 섹션은 컴파일러 옵션 -g가 주어졌을 때 생성된다.


· .line : 원본 c소스 프로그램의 라인번호와 .text 섹션에 있는 머신 코드간의 매핑 정보. 디버거 프로그램에서 이 정보를 사용한다.


· .strtab : .symtab과 .debug 섹션에 있는 심볼 테이블들의 스트링 테이블




Symbols and Symbol Resolution


재배치 가능한 모든 오브젝트 파일에는 심볼 테이블과 그와 관련된 심볼들을 가지고 있다. 링커의 맥락으로 살펴보면 심볼은 다음과 같은 종류가 있다. 
· 입력 모듈에서 정의되어 있는 global symbol. 다른 모듈에서 참조할 수 있다. 일반 함수와 전역 변수가 여기에 속한다.


· 입력 모듈에서 참조하는 하고 있지만 다른 곳에 정의된 global symbol. extern으로 선언된 함수와 변수가 여기에 해당


· 입력 모듈에서 정의되어 해당 모듈에서만 배타적으로 참조할 수 있는 local symbol. 정적 함수와 정적 변수가 이 범주에 속한다.


링커는 입력으로 들어온 재배치 가능한 오브젝트 파일의 심볼 테이블에 있는 엔트리를 검색해서 심볼 참조(symbol reference)를 해석한다. local symbol은 한 모듈에서 다중으로 정의할 수 없기 때문에 local symbol의 해석은 간단한 편이다. 반면에 global symbol은 약간의 기교를 필요로 한다. 컴파일할 때 캄파일러는 각각의 global symbol을 '약'하게, 혹은 '강'하게 export할 수 있다. 함수와 초기화된 전역 변수들은 강한 무게로 export되는 반면 초기화 되지 않은 전역 변수들은 약한 무게로 export된다. 링커는 다음과 같은 규칙을 사용해 심볼을 해석하게 된다.


   1. 여러 개의 strong symbol은 허락되지 않는다.


   2. 하나의 strong symbol과 여러 개의 weak symbol이 주어질 때, strong symbol을 선택한다.


   3. 여러 개의 weak symbol이 있을 경우 그 중 아무거나 선택한다.




예를 들어, 다음과 같은 두 개의 프로그램을 링크시킬 경우 링커는 에러를 발생시키게 된다. 

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


foo(전역 함수로서 strong symbol에 해당한다.)가 두 번 정의되어 있으므로 링커는 에러 메시지를 출력한다.

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


collect2는 링커 ld의 wrapper로서 gcc에 의해 호출된다.




 

Linking with Static Libraries


정적 라이브러리(static library)는 비슷한 유형의 오브젝트 파일들을 모아 놓은 것이다. 이 라이브러리는 아카이브(archive) 형태로 디스크에 저장되어 있다. 아카이브에는 검색을 빠르게 하기 위해 디렉토리 정보도 포함되어 있다. ELF archive는 매직 스트링 "!<arch>\n"로 시작한다. 
정적 라이브러리는 컴파일러 툴(링커)에 인자로 넘겨진다. 유닉스 시스템에서는 libc.a는 대부분의 프로그램이 사용하는 printf, fopen를 포함한 모든 c라이브러리 함수를 포함하고 있다.


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


libm.a는 표준 수학 라이브러리(math library)로 sqrt, sin, cos등등의 수학 함수가 정의된 오브젝트 모듈들을 포함하고 있다.



static library를 사용해 심볼 해석을 진행하는 과정에서 링커는 커맨드 라인에서 입력으로 들어온 인자의 왼쪽부터 오른쪽 방향으로 재배치 가능한 오브젝트 파일과 아카이브를 검색한다. 이 검색중에 링커는 실행 파일로 만들어질 재배치 가능한 오브젝트 파일의 묶음인 O집합, 해석되지 않은 심볼 리스트를 담고있는 U집합, 입력 모듈에서 정의된 심볼들을 나타내는 D집합을 관리하게 된다. 초기에는 이 세개의 집합은 비어있게 된다. 
· 링커는 커맨드 라인에서 입력으로 들어온 인자가 오브젝트 파일인지 아카이브인지를 결정한다. 만약 입력 모듈이 재배치 가능한 오브젝트 파일이라면 링커는 그것을 O집합에 추가하고 U와 D를 업데이트한 후 다음 입력 파일로 진행한다.


· 만 약 입력 모듈이 아카이브라면 아카이브를 구성하는 모듈 멤버들을 쭉 검색해서 U집합에 들어있는 해석되지 않은 심볼과 매치되는지 살펴본다. 만약 아카이브 멤버중에서 해석되지 않았던 심볼을 정의해 놓은 모듈이 있다면 그 모듈은 O리스트에 추가되고 U와 D는 업데이트된다. 이 과정은 모든 오브젝트 파일 멤버들에게 행해진다.


· 입력 모듈에 대해 위와 같은 두 단계가 모두 행해진 후 만약 U집합이 비어있지 않다면 링커는 에러를 리포팅하고 종료한다. U집합이 비어있다면 O집합에 있는 오브젝트 파일들은 merge되고 재배치되어 실행파일이 만들어진다.


이 과정을 통해 왜 정적 라이브러리가 링커 커맨드의 마지막에 위치하는지 알 수 있게 된다. 두 개의 라이브러리가 서로 순환적 의존 관계에 있는 경우에는 특별히 주의를 기울여야 한다. 커맨드 라인에 라이브러리를 입력할 때 심볼들이 아카이브의 멤버 라이브러리에 의해 참조될 수 있도록 순서를 가지고 입력되어야 한다. 심볼이 정의된 라이브러리가 그것을 참조하는 라이브러리보다 앞선 순으로 입력되어야 하고 만약 해석안된 심볼이 한 개 이상의 정적 라이브러리 모듈에서 정의되어 있을 때는 커맨드 라인에서 첫번째로 입력된 라이브러리의 정의를 따르게 된다.



 

Relocation

 

링커가 심볼의 해석을 모두 끝내게 되면 각각의 심볼 참조는 오직 하나의 심볼 정의를 갖게 된다. 이 시점에서 링커는 다음과 같은 두 단계의 재배치(relocation) 작업을 시작하게 된다.

· section 과 심볼 정의(symbol definition)들을 재배치한다. 링커는 같은 타입의 섹션들을 묶어 하나의 새로운 섹션으로 통합한다. 예를 들어 링커에 입력된 재배치 가능한 파일들에 있는 각각의 .data 세션을 하나의 단일한 .data 섹션으로 머지하여 최종 실행파일을 생성한다. .code 섹션에도 같은 작업이 수행된다. 그리고 링커는 이렇게 통합하여 생성된 섹션들과 입력 모듈에서 정의한 섹션들, 그리고 각각의 심볼들에게 run-time 메모리 주소를 할당한다. 이 단계가 끝나면 프로그램 내에 있는 모든 인스트럭션과 전역 변수들은 고유한 load-time 주소를 갖게 된다.


· section내에 있는 심볼 참조를 재배치한다. 링커는 이 단계에서 코드 및 데이타 섹션에 있는 모든 심볼 참조를 변경하여 심볼이 정의된 정확한 load-time 주소를 가리키게 만든다.


어셈블러가 unresolved 심볼을 만나게 되면 해당 오브젝트에 relocation entry를 생성하고 이를 .relo.text/.relo.data 섹션에 저장한다. relocation entry에는 해당 심볼의 참조를 어떻게 해석할 것인지에 대한 정보를 담고 있다. 전형적인 ELF relocation entry에는 다음과 같은 정보들로 구성되어 있다.


· Offset : 재배치 해야할 심볼 참조의 section offset. 재배치 가능한 파일에서 보면 이 값은 섹션의 시작부터 심볼 참조 위치까지의 바이트 옵셋 값이다.


· Symbol : 변경된 심볼 참조가 가리켜야할 심볼. 각각 재배치가 될 심볼 테이블의 인덱스라고 보면 된다.


· Type : 재배치 타입, 정상적인 경우 프로그램 카운터(PC) 상대 주소 방식(PC-relative addressing)을 의미하는 "R_386_PC32"이다. "R_386_32"는 절대 주소 방식(absolute addressing)을 의미한다.




링커는 재배치 가능한 오브젝트 모듈에 있는 모든 relocation entry를 조사해서 unresolved symbol을 type에 따라 재배치하게 된다. 가령 심볼이 "R_386_PC32" 타입인 경우, 재배치 주소는 S+A-P로 계산이 이루어지고 "R_386_32" 타입인 경우 S+A 식으로 계산이 된다. 여기서 'S'는 relocation entry의 심볼 값, 'P'는 section offset이나 재배치될 주소값(relocation entry에 저장된 offset값을 사용해 계산한다.), 'A'는 relocatable field의 값을 계산할 때 필요한 주소를 의미한다.


 

Dynamic Linking: Shared Libraries

 

정적 라이브러리(static library)는 몇몇 심각한 단점을 가지고 있다. 예를 들어 printf나 scanf와 같은 표준 라이브러리 함수를 생각해 보자. 이런 함수들은 거의 모든 어플리케이션에서 사용하고 있다. 만약 시스템이 50-100개의 프로세스를 실행하게 되면 각각의 프로세스는 printf, scanf 함수가 구현된 실행 코드를 가지게 될 것이다. 이것은 중요한 메모리 공간을 차지하게 되는 원인이 된다. 반면 공유 라이브러리(shared library)는 이런 정적 라이브러리의 결함을 해결하고 있다. 공유 라이브러리는 run-time시 어느때나 임의의 메모리 주소에 로딩될 수 있는 object module로서 이미 메모리에서 실행중인 프로그램에 의해서 링크될 수 있다. 공유 라이브러리는 때때로 공유 오브젝트(shared object)라고도 불린다. 대부분의 unix 시스템에서는 이런 공유 오브젝트를 ".so" 접미사로 표기하지만 HP-UX에서는 ".sl"로, MS 시스템에서는 DLL(dynamic link library)로 표기하기도 한다. 
공유 오브젝트를 빌드하기 위해선 컴파일러에 특별한 옵션을 주어 실행시킨다.


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




위 커맨드는 컴파일러 드라이버에게 2개의 오브젝트 모듈 "a.o", "b.o"를 묶어 "libfoo.so"라는 이름의 공유 라이브러리를 생성하게 만드는 커맨드이다.1 "-fPIC" 옵션은 position independent code(PIC)를 생성하도록 지시하는 옵션이다. 

이제 "a.o", "b.o"에 의존적인 메인 오브젝트 모듈 "bar.o"를 가정해 보자. 이 경우 링커는 다음과 같은 커맨드로 실행된다.


gcc bar.o ./libfoo.so


이 커맨드를 통해 load-time에 "libfoo.so"를 링크할 수 있는 형태의 "a.out" 실행파일이 생성된다. 이 "a.out"에는 공유 라이브러리를 링크했기 때문에 "a.o", "b.o"가 포함되어 있지 않다. 만약 공유 라이브러리 대신 정적 라이브러리를 포함했다면 그 2개의 오브젝트 모듈 코드가 모두 들어 있을 것이다. 
여기서 실행파일은 단지 "libfoo.so"의 데이타나 코드를 참조할 수 있도록 심볼 테이블 및 재배치 정보만 담고 있다가 run-time에 해석이 이루어진다. 따라서 "a.out"은 "libfoo.so"에 의존성을 가진 불완전한 실행파일이라고 할 수 있다. 또한 실행파일은 ".interp"라는 섹션을 가지고 있는데 여기에는 동적 링커(dynamic linker)의 이름이 포함되어 있다. 이것 또한 linux 시스템에 포함된 공유 오브젝트(ld-linux.so)이다. 그래서 실행파일이 메모리에 로딩될 때 로더는 이 동적 링커에 실행 제어를 넘긴다. 동적 링커는 공유 라이브러리를 프로그램의 주소 영역에 매핑시키는 start-up 코드를 포함하고 있다. 동적 링커는 다음과 같은 작업을 수행한다. 
· "libfoo.so"의 텍스트와 데이타를 실행파일의 memory segment로 재배치한다.


· "libfoo.so"에 정의된 심볼을 참조하는 "a.out"내의 모든 심볼 참조를 재배치한다.


마지막으로 이 작업이 끝나면 동적 링커는 어플리케이션에 실행 제어를 넘긴다. 이때부터 공유 오브젝트의 위치가 메모리에 고정된다.



 

Loading Shared Libraries from Applications

 

공유 라이브러리는 어플리케이션이 실행중이더라도 언제든 로딩이 가능하다. 공유 라이브러리를 실행 파일로 링크하지 않아도 어플리케이션은 동적 링커에게 공유 라이브러리를 메모리에 로딩하고 링크하도록 명령을 내릴 수가 있다.2 리눅스, 솔라리스와 그외 다른 시스템에서는 공유 오브젝트를 동적으로 로딩할 수 있는 일련의 함수 콜을 제공하고 있다. 
리눅스에서는 공유 오브젝트를 로딩할 때 사용할 수 있는 "dlopen", "dlsym, "dlclose"등의 system call을 지원하고 있는데 각각 공유 오브젝트를 로딩하고 공유 오브젝트내에 있는 심볼을 검색하고 공유 오브젝트를 close하는 기능을 제공하고 있다. 윈도우즈 시스템에서는 "LoadLibrary", "GetProcAddress" 함수가 각각 "dlopen", "dlsym"에 해당하는 함수들이다.




Tools for Manipulating Object Files (Object File 관리하는 Tool)

 

다음은 리눅스에서 오브젝트나 실행파일을 관리하는데 사용할 수 있는 툴들을 소개한 것이다.



ar : 정적 라이브러리를 생성한다.


objdump : 가장 중요한 바이너리 툴. 오브젝트 바이너리 파일내의 모든 정보를 출력할 수 있다.


strings : 바이너리 파일내에 printable string을 나열한다.


nm : 해당 오브젝트 파일의 심볼 테이블에 정의된 심볼들을 나열한다.


ldd : 오브젝트 바이너리가 의존하고 있는 공유 라이브러리들을 나열한다.


strip : 심볼 테이블 정보를 삭제한다.

[출처] 로더와 링커(ld)|작성자 rainofsoul81

'Language > C' 카테고리의 다른 글

02. 알아두면 정말 유용한 C소스 컴파일 과정(1)  (0) 2011.09.08
01. 컴파일의 의미  (0) 2011.09.08
모듈과 라이브러리  (0) 2011.09.08
라이브러리 공격  (0) 2011.09.08
라이브러리(2)  (0) 2011.09.08
:

모듈과 라이브러리

Language/C 2011. 9. 8. 10:39

Hello World 다시 보기

hello world 프로그램을 다시 한번 보기로 하자.

#include <stdio.h> 
 
int main(int argc, char **argv) 
{ 
    printf("Hello World!!!\n"); 
    return 1; 
} 
 
위 프로그램을 자세히 뜯어보면, #include문이 보일 것이다. 이것이 어디에 쓰는 물건인지 자세히 알아보도록 할 것이다. 우리는 7장 함수편에서 함수에 대해서 다루었다. 이를 통해서 우리는 함수를 사용하기 위해서는 함수선언과 함수원형이 필요하다는 것을 알게 되었다. 함수를 사용하는 이유는 중복되는 코드를 따로 묶어 둠으로써, 코드관리를 쉽게 하기 위함이라는 것도 역시 알고 있다. 하지만 편하겠지라고만 알고 있을 뿐, 실제 어떻게 편하게 사용되는지는 경험을 해보진 못했다.

자.. 우리는 함수라는 것을 알고 있다. 그렇다면 어떻게 해야 함수를 더 편하게 사용할 수 있을까. 답은 함수를 위한 코드를 따로 분리시킨다 이다. 위의 Hello World 프로그램은 이러한 전형적인 모습을 보여주고 있다. printf 함수는 거의 모든 프로그램에서 필수적으로 사용되는 함수다. 이런 코드를 사용자가 필요할때 마다 일일이 사용하는건 여간 귀찮은일이 아닐 것이다. 그렇다면 printf 함수를 별도의 코드로 만들어서, 모듈형태로 만들어 두면 될것이다. 그래서 printf 함수가 필요할 때, 가져다 쓰기만 하면 된다.

그런데, 컴파일러는 printf 함수가 어떤 모습을 가지는지 알 수가 없다. 그러니까 리턴값이 무엇이고, 인자로 무엇이 사용되는 지를 알 수가 없다는 얘기가 된다. 그러므로 컴파일러에게 printf 함수의 정보를 알려줄 수 있어야 한다. 그게 #include 문이 하는 일이다. stdio.h는 표준입출력과 관련된 함수의 정보가 들어있는 파일로 헤더파일이라고 부른다. 이 헤더파일에는 printf 함수가 선언되어 있다. stdio.h 헤더파일은 /usr/include 디렉토리 밑에 존재한다.

이제 우리는 stdio.h 만을 include 시킴으로써, 어느 코드에서든지 간단하게 printf 함수를 사용할 수 있게 된다.

컴파일 과정

이제 위의 hello world 프로그램이 어떻게 컴파일이 되는지 알아보도록 하자. 프로그램의 이름은 hello.c 이다.
  1. 컴파일러는 hello.c 프로그램을 읽어 들인다.
  2. hello.c 코드를 해석해서, 기계어 형태의 object 파일로 만든다. object 파일은 컴퓨터가 해석할 수 있는 단위 모듈이다.
  3. 여기에 printf 함수가 정의되어 있는 이미 만들어져 있는 object 파일과 hello object 파일을 서로 링크(연결)한다.
  4. 완전한 실행 파일이 만들어 진다.

덧셈 함수를 가진 프로그램

그럼 덧셈 함수를 가진 계산기 프로그램을 만들어서, 모듈별로 작성하고 이것들을 object 파일로 만들어서 link 시켜서 실행파일을 만드는 방법에 대해서 알아보도록 하겠다.

모듈별로 작성하지 않고, 하나의 파일로 이루어진 프로그램은 아래와 같이 작성할 수 있을 것이다.
#include <stdio.h> 
int sum(int a, int b); 
int main(int argc, char **argv) 
{ 
    int value; 
    value = sum(120, 199); 
    printf("%d\n", value); 
} 
int sum(int a, int b) 
{ 
    return a+b; 
} 
 
아주 간단하며 문제없이 작성될 것이다. 또한 덧셈을 위한 sum 이라는 함수를 만들었으니, 이후 덧셈계산이 필요할 때 마다, 그냥 sum 함수를 그대로 쓰기만 하면 될것이다. 그런데 이 프로그램은 통짜로 작성되어 있기 때문에, 다른 프로그램에서 sum 함수를 사용할려고 하면 copy & paste 할 수 밖에 없다. 이것은 비효율 적이다. 이제 sum 함수를 모듈형식으로 완전히 분리 시켜 보도록 하자. 이를 위해서는 다음과 같이 3개의 파일이 만들어져야 한다.
  1. sum 함수의 선언이 들어 있는 include 파일
  2. sum 함수를 사용할 main 함수가 있는 C 파일
  3. sum 함수가 정의되어 있는 C 파일

include 파일은 아주 간단하게 만들 수 있다. include 파일의 이름은 sum.h 로 하겠다.
int sum(int a, int b); 
 

이제 sum 함수가 정의되어 있는 C 코드를 만들어보자. 역시 간단하다. 파일이름은 sum.c로 하겠다.
int sum(int a, int b) 
{ 
    return a + b; 
} 
 

마지막으로 main 함수를 만들어 보자. 파일이름은 calc.c 로 하겠다.
#include "sum.h" 
#include <stdio.h> 
 
int main() 
{ 
    int value; 
    value = sum(130, 199); 
    printf("%d\n", value); 
} 
 

include 는 

헤더파일의 경로

우리는 #include 키워드를 이용해서, 포함시킬 헤더파일을 지정할 수 있다. 이때 헤더파일의 완전한 경로를 포함시켜 줘야 한다. 만약 따움표를 사용했다면, 이는 현재 디렉토리에서 헤더파일을 찾겠다는 것을 의미한다. 그렇지 않고 <> 를 사용했다면, 표준 Include 디렉토리와 컴파일러 옵션을 통해서 지정된 디렉토리에서 찾게된다. 유닉스 시스템의 경우 /usr/include 가 표준 Include 파일이 된다. 

헤더파일을 찾을 경로의 지정은 -I옵션을 이용하면 된다. 예를 들어 /home/yundream/include 에서 찾도록 하길 원한다면
# gcc -I/home/yundream/include -o sum sum.c 
 
와 같이 하면 된다.

만약 /home/yundream/include 를 헤더파일 찾기 경로로 지정하고 싶다면, 다음과 같이 하면 된다.
#include "/home/yundream/include" 
 


모듈별 분할 컴파일

자 이제 sum.h, sum,c, calc,c 3개의 파일이 만들어졌다. 이 3개의 파일을 컴파일해서 실행가능한 프로그램을 만들어보자.

위에서 언급되었듯이 가장 먼저 해야할일은 sum.c 와 calc.c 를 기계어가 해석가능한 object 코드로 만드는 일이다. 오브젝트 코드는 gcc에 -c 옵션을 이용해서 만들어낼 수 있다.
# gcc -c sum.c calc.c 
 
이제 sum.o 와 calc.o 라는 파일이 만들어진걸 확인할 수 있을 것이다. 확장자 .o는 이 파일이 오브젝트 파일이라는 것을 알려준다. 이제 두개의 object 파일을 링크시켜서 실행파일을 만들면 된다. -o 옵션을 이용하면, 만들어진 오브젝트 파일들을 합쳐줄 수 있다.
# gcc -o calc sum.o calc.o 
 
이제 실행파일인 calc가 만들어졌다.

이렇게 만들어진 object 파일은 기계어로 만들어져 있기 때문에, 이후에 사용할때는 sum.c를 다시 object 파일로 컴파일할 필요가 없다. 그냥 sum.o 프로그램에 링크시켜주기만 하면 된다. 다음과 같은 프로그램을 만들어보자. 프로그램의 이름은 mycal.c로 하자.
#include "sum.h" 
#include <stdio.h> 
 
int main(int argc, char **argv) 
{ 
  int value; 
  int a; 
  int b; 
  if (argc != 3) 
  { 
    printf("Usage : %s num1 num2\n", argv[0]); 
    return 1; 
  } 
 
  a = atoi(argv[1]); 
  b = atoi(argv[2]); 
  value = sum(a, b); 
  printf("%d + %d = %d\n", a, b, value); 
  return 0; 
} 
 
이 프로그램은 첫번째 프로그램보다 더 진보된 프로그램으로, 프로그램의 명령행 인자로 받아들인 숫자를 더할 수 있도록 되어 있다. atoi(3)는 문자열을 int형 숫자로 변환해주는 함수다. sum 함수는 이미 컴파일 되어서 object 파일로 만들어져 있으므로, 별도로 컴파일할 필요가 없다. 다음과 같은방법으로 실행파일을 만들 수 있다.
# gcc -c mycal.c 
# gcc -o mycal sum.o mycal.o 
 

4칙연산 프로그램

위의 프로그램은 덧셈만을 지원하고 있다. 여기에 덧붙여 뺄셈, 나눗셈, 곱샘까지 지원하는 프로그램을 만들어 보도록 하자. 각각의 연산은 모두 함수로 작성되며, 각각의 함수가 헤더파일과 함수가 정의된 C 코드 파일을 가지게 될 것이다. 그렇다면, 이 프로그램은 4칙연산을 위한 4개의 함수와 4개의 헤더파일 1개의 main 함수를 포함하는 C 파일로 구성될 것이다.
  • 헤더파일 : sum.h, sub.h, mul.h, div.h
  • 함수정의된 C 파일 : sum.c, sub.c, mul.c, div.c
  • main 함수파일 : simplecal.c

먼제 헤더파일을 작성해보자.
sum.h
int sum(int a, int b); 
 

sub.h
int sub(int a, int b); 
 

mul.h
int mul(int a, int b); 
 


div.h
int div(int a, int b); 
 

이제 함수의 정의를 담고 있는 4개의 C 소스코드 파일을 만들어야 한다.

sum.c
int sum(int a, int b) 
{ 
    return a + b; 
} 
 

sub.c
int sub(int a, int b) 
{ 
    return a - b; 
} 
 

mul.c
int mul(int a, int b) 
{ 
    return a * b; 
} 
 

div.c
int div(int a, int b) 
{ 
    return a / b; 
} 
 

이제 main 함수를 가진 코드를 만들면 된다. 
#include "sum.h" 
#include "sub.h" 
#include "mul.h" 
#include "div.h" 
#include <stdio.h> 
 
int main(int argc, char **argv) 
{ 
   int a = 1200, b=25; 
 
  printf("sum : %d\n", sum(a, b)); 
  printf("sub : %d\n", sub(a, b)); 
  printf("mul : %d\n", mul(a, b)); 
  printf("div : %d\n", div(a, b)); 
} 
 

코드를 만들었다면, gcc를 이용해서 object를 만들고 이것들을 링크시켜서 실행파일로 만들면 된다.
# gcc -c sum.c sub.c mul.c div.c simplecalc.c 
# gcc -o simplecalc sum.o sub.o mul.o div.o simplecalc.o 
 

라이브러리

이렇게 단위 함수를 별개의 소스코드와 헤더파일로 나누어서 관리하게 되면, object 혹은 단위 소스코드 파일을 재활용할 수 있다는 장점을 가진다. 그러나 여전히 불편한 점이 있다. 함수가 많아지면, 자칫 수십개의 오브젝트 파일이 생성될 수 있을건데, 이들을 관리하는건 매우 귀찮은 일이기 때문이다.

그렇다면 4개의 object 파일을 하나로 묶을 수만 있다면, 함수들을 더 편리하게 관리할 수 있을 것이다. 이렇게 오브젝트들을 하나의 파일로 다시 묶은 것을 라이브러리(library)라고 한다.

라이브러리는 다시 정적 라이브러리와 공유 라이브러리로 나뉜다. 정적라이브러리는 실행파일에 완전히 포함되어버리는 형식의 라이브러리를 말한다. 반면 공유 라이브러리는 실행파일에 포함되지 않고, 실행될때 해당 라이브러리를 불러오는 형식의 라이브러리를 말한다.

정적라이브러리

static library라고 부르기도 한다. 이 라이브러리는 단순한 오브젝트의 모음일 뿐이다. 정적라이브러리는 ar이라는 프로그램을 통해서 만들 수 있다. 그럼 ar을 이용해서 위의 사칙연산을 위한 4개의 오브젝트를 모아서 libmycalc.a라는 이름의 정적라이브러리를 생성해보도록 하자. rc 옵션을 이용하면, 정적라이브러리를 만들 수 있다.

r은 정적라이브러리를 만들겠다는 옵션이고, c는 새로 생성을 하겠다는 옵션이다.
# ar rc libmycalc.a sum.o sub.o mul.o div.o 
 
libmycalc.a 라는 파일이 생성된걸 확인할 수 있을 것이다. t 옵션을 이용하면, 해당 라이브러리가 어떤 오브젝트를 포함하고 있는지도 확인할 수 있다. t 옵션을 사용하면 된다. 참고로 정적 라이브러리의 이름은libNAME.a의 형식을 따라야 한다.
# ar t libmycalc.a 
div.o 
mul.o 
sum.o 
sub.o 
 

그럼 정적라이브러리를 이용해서 실행파일을 만들어 보도록 하자. 이전에는 4개의 오브젝트 파일을 모두 링크시켜줘야 했지만, 이제는 libmycalc.a 만 링크시켜주면 된다.

라이브러리의 링크방식은 오브젝트를 링크하는 것과는 약간 차이가 있다. library의 위치를 명확히 명시해 주어야 한다. -L 옵션을 이용해서 라이브러리가 있는 디렉토리의 위치를 명시해주고, -l옵션을 이용해서, 라이브러리 파일의 이름을 정해줘야 한다. 다음은 simplecalc.c 를 정적라이브러리를 이용해서 컴파일하는 방법을 보여준다.
# gcc -o simplecalc simplecalc.c -L./ -lmycalc 
 
-L./은 현재 디렉토리를 라이브러리 찾기 디렉토리로 하겠다는 의미가 된다. -l 옵션뒤에 붙이는 라이브러리 파일의 이름에 주목할 필요가 있다. 라이브러리 이름은 lib와 .a를 제외한 이름을 사용한다.

공유 라이브러리

공유 라이브러리는 함께 사용하는 라이브러리라는 의미다. 즉 정적 라이브러리 처럼 실행파일에 붙는 것이 아니고, 시스템의 특정디렉토리에 위치하면서, 다른 모든 프로그램들이 공유해서 사용할 수 있게끔 제작된 라이브러리다. 그러므로 공유 라이브러리를 사용하도록 제작된 프로그램은 실행시에 사용할 라이브러리를 호출하는 과정을 거치게 된다.

공유 라이브러리역시 오브젝트를 이용해서 만든다는 점에서는 정적라이브러리와 비슷하지만, 호출시에 링크하기 위한 부가적인 정보를 필요로 하므로, 정적라이브러리와는 전혀 다른 형태로 만들어 진다. 정적라이브러리와 이름이 헛갈릴 수 있으니, 라이브러리 이름은 mycalcso 로 하겠다.
# gcc -fPIC -c sum.c sub.c mul.c div.c 
# gcc -shared -W1,-soname,libmycalcso.so.1 -o libmycalcso.so.1.0.1 sum.o sub.o mul.o div.o 
 

  1. 오브젝트 파일을 만들때 부터 차이가 있는데, -fPIC 옵션을 줘서 컴파일 한다.
  2. 그다음 -shared 옵션을 이용해서 공유라이브러리 파일을 생성한다.
위의 과정을 끝내고 나면, libmycalcso.so.1.0.1 이라는 파일이 생성이 된다. 이 라이브러리는 프로그램을 컴파일할때와 실행시킬때 호출이 되는데, 호출될때는 libmycalcso.so 를 찾는다. 그러므로 ln 명령을 이용해서 libmycalcso.so 링크파일을 생성하도록 하자.
# ln -s libmycalcso.so.1.0.1 libmycalcso.so 
 
이렇게 링크를 만들게 되면, 여러가지 버전의 라이브러리 파일을 이용할 수 있으므로 관리상 잇점을 가질 수 있다. 새로운 버전의 라이브러리가 나올 경우, 오래된 버전의 라이브러리를 쓰는 프로그램은 실행시 문제가 발생할 수 있는데, 이런 문제를 해결할 수 있기 때문이다. 

이제 링크하는 과정이 남았다. 링크과정은 정적 라이브러리를 사용할때와 동일하다.
# gcc -o simplecalcso simplecalc.c -L./ -lmycalcso 
 

이제 프로그램을 실행시켜 보도록 하자. 아마 다음과 같은 에러메시지를 만나게 될 것이다.
# ./simplecalcso 
./simplecalcso: error while loading shared libraries: libmycalc.so:  
cannot open shared object file: No such file or directory 
 

이러한 에러가 발생하는 원인에 대해서 알아보도록 하자. 정적라이브러리는 실행파일에 라이브러리가 붙여지므로, 일단 실행파일이 만들어지면, 독자적으로 실행이 가능하다. 그러나 공유라이브러리는 라이브러리가 붙여지는 방식이 아니고, 라이브러리를 호출해서 해당 함수코드를 실행하는 방식이다. 그러므로 공유라이브러리 형식으로 작성된 프로그램의 경우 호출할 라이브러리의 위치를 알고 있어야만 한다.

위의 simplecalcso 프로그램을 실행시키면, 이 프로그램은 libmycal.so 파일을 찾을 것이다. 이때 파일을 찾는 디렉토리는 /etc/ld.so.conf에 정의 되어 있다.
# cat /etc/ld.so.conf 
/usr/lib 
/usr/local/lib 
 
만약 위에서 처럼되어 있다면, 프로그램은 /usr/lib 와 /usr/local/lib 밑에서 libmycal.so 를 찾게 될 것이다. 그런데 libmycal.so 가 없으니, 위에서와 같은 에러가 발생하는 것이다. 

가장 간단한 방법은 라이브러리 파일을 ld.so.conf에 등록된 디렉토리중 하나로 복사하는 방법이 될 것이다. 혹은 환경변수를 이용해서, 새로운 라이브러리 찾기 경로를 추가할 수도 있다. 이때 사용되는 환경변수는 LD_LIBRARY_PATH 다.
# export LD_LIBRARY_PATH=./:/home/myhome/lib 
 
이제 프로그램을 실행시키면 LD_LIBRARY_PATH 에 등록된 디렉토리에서 먼저 검색하게 되고, 프로그램은 무사히 실행 될 것이다.

공유라이브러리와 정적라이브러리의 장단점

이들 2가지 라이브러리의 장단점에 대해서 알아보도록 하자. 장단점을 알게되면 어떤 상황에서 이들 라이브러리를 선택할 수 있을지 알 수 있을 것이다.

정적라이브러리의 장점은 간단한 배포방식에 있다. 라이브러리의 코드가 실행코드에 직접 붙어버리는 형식이기 때문에, 일단 실행파일이 만들어지면 간단하게 복사하는 정도로 다른 컴퓨터 시스템에서 실행시킬 수 있기 때문이다. 반면 동적라이브러리는 프로그램이 실행될때 호출하는 방식이므로, 라이브러리까지 함께 배포해야 한다. 라이브러리의 호출 경로등의 환경변수까지 덤으로 신경써줘야 하는 귀찮음이 따른다. 

일반적으로 정적라이브러리는 동적라이브러리에 비해서 실행속도가 빠르다. 동적라이브러리 방식의 프로그램은 라이브러리를 호출하는 부가적인 과정이 필요하기 때문이다. 

정적라이브러리는 실행파일 크기가 커진다는 단점이 있다. 해봐야 얼마나 되겠느냐 싶겠지만, 해당 라이브러리를 사용하는 프로그램이 많으면 많을 수록 X 프로그램수만큼 디스크 용량을 차지하게 된다. 반면 공유라이브러리를 사용할 경우, 라이브러리를 사용하는 프로그램이 10개건 100개건 간에, 하나의 라이브러리 복사본만 있으면 되기 때문에, 그만큼 시스템자원을 아끼게 된다. 

마지막으로 버전 관리와 관련된 장단점이 있다. 소프트웨어 개발 세계의 불문율이라면 버그 없는 프로그램은 없다이다. 어떠한 프로그램이라도 크고작은 버그가 있을 수 있으며, 라이브러리도 예외가 아니다. 

여기 산술계산을 위한 라이브러리가 있다. 그리고 정적 라이브러리 형태로 프로그램에 링크되었어서 사용되고 있다고 가정해보자. 그런데 산술계산 라이브러리에 심각한 버그가 발견되었다. 이 경우 산술계산 라이브러리를 포함한 A 프로그램을 완전히 새로 컴파일 해서 배포해야만한다. 문제는 이 라이브러리가 A 뿐만 아니라 B, C, D 등의 프로그램에 사용될 수 있다는 점이다. 결국 B, C, D 프로그램 모두를 새로 컴파일 해서 배포해야 하게 된다. 더 큰 문제는 어떤 프로그램이 버그가 있는 산술계산 라이브러리를 포함하고 있는지 알아내기가 힘들다는 점이다. 

공유라이브러리 형태로 작성하게 될경우에는 라이브러리만 새로 컴파일 한다음 바꿔주면된다. 그러면 해당 라이브러리를 사용하는 프로그램이 몇개이던간에 깔끔하게 문제가 해결된다. 

실제 이런 문제가 발생한 적이 있었다. zlib 라이브러리는 압축을 위한 라이브러리로 브라우저웹서버, 압축관리 프로그램등에 널리 사용된다. 많은 프로그램들이 이 zlib를 정적라이브러리 형태로 포함해서 배포가 되었는데, 심각한 보안문제가 발견되었다. 결국 zlib를 포함한 모든 프로그램을 새로 컴파일해서 재 설치해야 하는 번거로운 과정을 거치게 되었다. 공유라이브러리였다면 문제가 없을 것이다. 

이상 정적라이브러리와 공유라이브러리를 비교 설명했다. 그렇다면 선택의 문제가 발생할 것인데, 자신의 컴퓨터나 한정된 영역에서 사용할 프로그램을 제작하지 않는한은 공유라이브러리 형태로 프로그램을 작성하길 바란다. 특히 인터넷을 통해서 배포할 목적으로 작성할 프로그램이라면, 공유라이브러리 형태로 작성하는게 정신건강학적으로나 프로그래밍 유지차원에서나 좋을 것이다. 

출처 :  
http://www.joinc.co.kr/modules/moniwiki/wiki.php/Site/C/Documents/CprogramingForLinuxEnv/Ch12_module 

'Language > C' 카테고리의 다른 글

01. 컴파일의 의미  (0) 2011.09.08
링킹/로더  (0) 2011.09.08
라이브러리 공격  (0) 2011.09.08
라이브러리(2)  (0) 2011.09.08
라이브러리(1)  (0) 2011.09.08
:

라이브러리 공격

Language/C 2011. 9. 8. 10:15
1.Shared library(공유라이브러리)??

공유라이브러리(Shared library)는 돌아가는 리눅스 상에서 프로그램의 크기를 작고 가볍게 하기 위하여 필요할때만 가져다 쓸수 있도록 만들어진 라이브러리로 일반적으로 gcc -o project project.c 이와같이 컴파일 하게 되는데 이렇게 컴파일을 하게 되면 자동적으로 공유라이브러리를 참조하게 된다.

우리가 일상적으로 쓰는 printf, scanf와 같은 함수도 모두 공유라이브러리를 참조해서 사용하는것이다. 





만약 project.c를 gcc -o project project.를 컴파일 하게 되면 이와같은 과정을 거쳐서 만들어 지고 기본 공유라이브러리인 lib.so.6를 참조 하게 만들어 놓는다.

project.obj같은 경우에는 -o로 컴파일 할경우에는 쓰고 지우기 때문에 눈으로는 확인할수 없고 결과물인 project.c와 project만 보일것이다. obj를 보기 위해서는 -c옵션으로 컴파일 해주시면 된다.




 

libc.so.6가 연동되어 있는것은 objdump를 이용해서 확인할수 있다. -p옵션으로 헤더만 확인해 보자. gcc로 일반적으로 컴파일한 파일 아무거나를 가져와서 보게되면


동적할당 된것중에 libc.so.6 라이브러리가 링크되어 있는것을 볼수 있다. 그리고 이렇게 보지 않더라도 오류같은것이 나게 되어서 gdb로 보게되면 어디 함수에서 걸려서 그렇다고 하면서 옆에 libc.so.6가 보이는데 그것으로도 이 함수는 저 라이브러리에 의해서 실행되고 있구나 하는것을 알수 있다.
//ldd와 readelf를 이용해서도 라이브러리 링크를 확인할수 있다. 
//ldd - print shared library dependencies
            공유라이브러리 의존성을 확인하는데 특화된 프로그램
//readelf - ELF 파일 정보 보기 -d 옵션 == dynamic

2. 공유라이브러리의 취약점을 이용한 공격라이브러리 만들기

원래 어떤 함수를 실행하기 위해서는 공유 라이브러리를 들려야만한다.

아래와 같은 코드가 공격코드가 있다고 하자. 

//chal2.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>

int main(void){

    uid_t uid = getuid();      // 공유 라이브러리 참조 함수들
    gid_t gid = getuid();

    uid_t euid = geteuid();;
    gid_t egid = geteuid();

    if(uid != 1209 | euid != 1209){ 
        printf("You are not 1209!!. GET AWAY! \n");
        exit(EXIT_FAILURE);
    }

    if(gid != 1209 | egid != 1209){
        printf("You are not 1209!!. GET AWAY!! \n");
        exit(EXIT_FAILURE);
    }

    int i,j;
    char password[] = "abcdefghij";

    for(i=0;i<10;i++)
        for(j=0;j<i;j++)
            password[i]++;

    printf("Password is :: %s\n",password);
    return 0;

}

위의 프로그램을 돌려보면 

./chal2
You are not 1209!!. GET AWAY!

이와 같이 나온다. 당연하다. 난  id를 확인해보면

id
uid=500(whdudgn) gid=500(whdudgn) groups=500(whdudgn)
context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

500이라는것을 알수 있다. 그러므로 프로그램 password를 따낼수 없다. 그렇다고 직접 id가 1209인 아이디를 찾아내서 그 아이디로 바꿀수 있는 setuid걸린 프로그램도 없고 확인해 본 바 id가 1209인 유저 자체가 없다. 매우 난감하기 짝이 없다. 이럴때에는 위에서 사용된 getuid() geteuid()같은 함수를 공략할수 밖에 없다. 

이때 공유라이브러리의 취약점을 사용할수 있게된다. 

항상 프로그램은 어떤 함수를 쓸떄 공유라이브러리 함수를 들르게 되어있다.(정적라이브러리로 프로그램에 라이브러리를 합쳐놓지 않는 이상..... 프로그램에 라이브러리가 합쳐져 있다면 대부분 용량으로 파악이 가능하다. 라이브러리가 프로그램에 같이 섞여 있다면 엄청나게 용량은 늘어날수밖에 없기 때문이다. 그러나 합쳐져서 공유라이브러리를 참조 하지 않기때문에 공유라이브러리 취약점은 사용할수 없게 된다. 정적 라이브러리는 ar rc libaaa.a aaa.o 를 써서 ar의 r은 라이브러리 아카이브에 새로운 오브젝트를 추가하고 c는 그것이 없을때 생성하라는명령어로 생성후 사용할때는  gcc로 컴파일 하는 파일 뒤에 -L./laaa와 같이 lib는 l로 그리고 아카이브인 .a는 생략하고 써주시면 컴파일 할때 정적라이브러리로 컴파일 하고 사용할수 있게된다.)
//정적라이브러리는 주제에서 약간 벗어났지만 정적라이브러리에 대해서 궁금하신 분을 위해서 약간 써 놓았다. ()안의 것은 보지 않으셔도 무방하다. 깊게 알고 싶으신 분은

http://www.joinc.co.kr/modules/moniwiki/wiki.php/Site/C/Documents/Make_Library 로 가서셔 보시기 바란다. 





그때 이 오른쪽과 같이 공유라이브러리로 가려고 하는것을 가로채서 내가 만든 라이브러리를 참조하게 되면 프로그램은 내 의도대로 움직이게 된다. 그럼 이제 우리는 공격을 위한 attack.so라는 동적라이브러리를 생성해보자.










※ 라이브러리를 만들기 위한 gcc 컴파일러의 옵션

-shared 옵션
공유 라이브러리를 우선하여 링크하도록 하는 옵션
정적라이브러리와 같이 있을 경우에 우선적으로 공유 라이브러리를 링크하도록 하는데,
사실 아무 옵션을 주지 않아도 공유 라이브러리를 우선적으로 링크한다.

-fpic -fPIC 옵션
gcc 컴파일러가 object file을 만들때, 그 안의 symbol (function, variable)들이
어떤 위치에 있더라도 동작을 하는 구조로 compile 하라는 것입니다
그렇게 된 것만이 리눅스에서 사용 가능한 모듈파일(*.so)이 될 수 있습니다.
모듈파일(*.so)은 안에는 공유 라이브러리가 포함되어있습니다.

-fpic와 -fPIC - 둘의 차이점.

      "코드를 생성하기 위해 -fPIC이나 -fpic을 사용하라. 코드를 생성하기 위해
      -fPIC이나 -fpic을 사용하는 것은 타겟에 따라서 다르다.
 
      -fPIC을 사용하는것은 언제나 동작한다.
      하지만, -fpic을 사용하는 것보다 큰 코드를 생성할 것이다
      (PIC은 더 큰코드를 위한것이라서 더 많은 양의코드를 만든다는 것을 기억하라)

      -fpic옵션은 작고 빠른 코드를 만든다. 하지만, 전역심볼이나 코드의 크기 같은 것에서
      플랫폼에 독립적이다. 링커는 공유 라이브러리를 만들때 이 옵션이 맞는지 말해줄 것이다.
      어느것을 써야 할지를 모를때, 나는 언제나 동작하는 -fPIC을 선택한다"
 
-fpic, -fPIC의 차이점은 KLDP 문서중에 인용하였습니다.
http://kldp.org/HOWTO/html/Program-Library-HOWTO/

이제 공격 라이브러리를 생성해보자. 우리는 저 함수들의 리턴값이 1209이길 바라기 때문에

#include <dlfcn.h>      //동적 라이브러리 관련 헤더파일
#include <unistd.h>
#include <sys/types.h>

uid_t getuid( void ){
        return 1209;
}
uid_t getgid( void ){
        return 1209;
}
uid_t geteuid( void ){
        return 1209;
}
uid_t getegid( void ){
        return 1209;
}

로 작성한 후에 
[whdudgn@/home/whdudgn/programming/chal]$ gcc -o attack.so attack.c -fPIC -shared
이와같이 컴파일 해주자.  그럼 우리는 동적 라이브러리를 생성하게 된것이다.

그리고 우리는 이제 프로그램이 함수를 참조하기 위해서 libc.so.6를 들르기 전에 우리가 생성한 attack.so로 오게 만들면 우리가 작성한 return값을 넘겨줄수있기 때문에 공격은 성공하게 될것이다.

그러기 위해서는 우리는 이 공격에서 가장 중요한 환경변수인 LD_PRELOAD 라는 환경변수를 알고 있어야 한다. 이 환경변수는 프로그램이 라이브러리를 가져오기전에 원하는 라이브러리를 먼저 등록시켜주는 환경변수로, 프로그램은 LD_PRELOAD로 지정된 공유 라이브러리를 먼저 들르게 된다. 그리고 그곳에 자기가 원하는 함수가 없을 경우에 표준 라이브러리인 libc.so.6로 향하게 된다.

그점을 이용해서 환경변수에 attack.so를 등록한후 우리가 공격할 프로그램을 작동시키면 아래와 같이 password를 뱉게 된다.

[whdudgn@/home/whdudgn/programming/chal]$ export LD_PRELOAD="/home/whdudgn/programming/chal/attack.so"
[whdudgn@/home/whdudgn/programming/chal]$ ./chal2
Password is :: acegikmoqs


참고 문헌 : 

http://sosal.tistory.com/125 ==> shared library hijacking.. 

'Language > C' 카테고리의 다른 글

링킹/로더  (0) 2011.09.08
모듈과 라이브러리  (0) 2011.09.08
라이브러리(2)  (0) 2011.09.08
라이브러리(1)  (0) 2011.09.08
library 의 사용  (0) 2011.09.08
:

라이브러리(2)

Language/C 2011. 9. 8. 10:06

참조: http://blog.naver.com/cdcsman?Redirect=Log&logNo=70016825391

 다음과 같은 코딩을 위해 C 라이브러리를 만들려고 한다.

<Goal>

interface.h, interface.c, application.c

interface.h: interface만을 정의해 놓은 헤더

interface.c: interface가 가지고 있는 function의 declaration

application.c: interface.h를 인클루드하여 function을 사용하는 어플리케이션 작성

<Method>

테스트 삼아 정적 라이브러리와 공유 라이브러리를 이용해 본다.

1. 정적 라이브러리

확장자가 .a로 끝나는 라이브러리로 프로그램이 컴파일 될 때 라이브러리를 프로그램에 직접 링크한다.  많이 사용하면 할수록 프로그램에 라이브러리가 더해지기 때문에 프로그램 사이즈가 커진다. 라이브러리는 ‘lib이름.a’ 형식으로 지어지며 사용은 ‘gcc -l이름’으로 사용한다.

* 사용하기

a. object 파일 생성

$ gcc -c interface.c

$ ls

interface.h interface.c interface.o application.c

b. ar 명령 사용하여 라이브러리 파일 만듬

$ar r libtest.a interface.o [ar r archivename filenames]

c. 라이브러리 파일의 사용

$ gcc application.c -o application -ltest -L/home/somebody/lib

-L을 이용하여 라이브러리 파일의 경로를 설정하거나 (-L/directory…)

xeport를 이용하여 환경 변수 설정 (export LD_LIBRARY_PATH=$HOME/lib)

2. 공유 라이브러리

확장자가 .so로 끝나며 프로그램이 컴파일 될 때 직접 링크되지 않고 실행할 때 로드되어 사용된다. 따라서 실행 파일의 크기가 증가하지 않으며 메모리에 로드된 라이브러리는 여러 프로그램에 의해 공유된다.

* 사용

a. object 파일 만들기. 이 때 -fpic 옵션을 주어 위치 독립적인 코드를 생성한다.

$ gcc -fpic -c inteface.c

$ ls

interface.h interface.c interface.o application.c

b. object 파일을 포함하는 공유 라이브러리 파일을 만든다.

$ gcc -shared -o libinterface.so.0.1.1 interface.o

c. 불려질 라이브러리 이름(lib이름.so)를 실제 라이브러리 이름과 심볼릭 링크한다.

$  ln -s libinterface.so.0.1.1 libinterface.so

d. 환경 변수를 설정한다.

$ export LD_LIBRARY_PATH=$HOME/lib

e. 실행 파일 생성

$ gcc application.c -o application -linterface

'Language > C' 카테고리의 다른 글

모듈과 라이브러리  (0) 2011.09.08
라이브러리 공격  (0) 2011.09.08
라이브러리(1)  (0) 2011.09.08
library 의 사용  (0) 2011.09.08
gcc 컴파일 과정 요약  (0) 2011.09.07
:

라이브러리(1)

Language/C 2011. 9. 8. 10:05

정적 라이브러리

정적 라이브러리란 여러 프로그램에서 사용 되는 함수를 포함하는 오브젝트 파일을 하나의 파일로 다룰 수 있도록 정리해 놓은 것이다. 프로그램을 작성할 때 각각의 소소파일을 컴파일하고 링크해서 하나의 실행 가능한 파일을 생성한다. 이때 다른 프로그램에서도 사용될 만한 모듈이 여러 개의 오브젝트 파일로 나뉘어 있으면 이것들을 한 덩어리로 다루기가 번거로워 진다. 그래서 생각해낸 것이 아카이브 파일이다. 여러개의 오브젝트 파일을 하나의 파일로 모아놓은 것이다. ar 명령어를 사용해 여러 개의 오브젝트 파일을 하나의 아카이브 파일로 합칠 수 있다. OS 따라 ranlib 를 사용하면 이 아카이브 파일 내의 오브젝트가 제공하는 심볼 정보를 해시화해서, 심볼을 제공하는 오브젝트 파일을 효율적으로 검색할 수 있게 된다. 이와 같은 아카이브 파일을 정적 라이브러리라 한다.

# gcc -c -o foo.o foo.c 
# gcc -c -o bar.o bar.c 
# ar ruv libfoo.a foo.o bar.o

라이브러리 내용은 ar 명령어로 확인 가능

 libfoo.a 가 생성 되면 아카이브 파일이 생성된 것이다. 정적 라이브러리를 링크할 경우, 링커는 다른 오브젝트 파일에서 정의되지 않은 심볼을 찾아 지정된 정적 라이브러리에서 해당 심볼을 정의하고 있는 오브젝트 파일의 사본을 추출해서 실행 파일 내에 포함 시킨다.

# gcc -L./ -o main main.o -lfoo

 정적 라이브러리를 링크해서 생성된 실행 바이너리를 실행할 경우에는 정적 라이브러리가 없어도 관계 없다. 필요한 코드는 실행 바이너리에 복사되어 포함되어 있기 때문이다.

사용자 삽입 이미지

클릭해서 보세요

위 화면은 main.c 에서 아카이브 파일 내에 있는 SUM 함수를 호출하고 컴파일한 결과이다. 라이브러리를 지정해 주지 않으면 SUM 은 정의되지 않은 심볼이라고 해서 에러가 난다. 라이브러리 경로를 지정해주고 사용할 라이브러리를 적어주면 컴파일 완료

공유 라이브러리

 공유 라이브러리는 공유 된다는 점에서 정적 라이브러리와 다르다. OS 의 가상 메모리 관리 시스템이 진보함에 따라, 하나의 파일을 mmap, semget 등을 이용해 여러 프로세스에서 메모리를 공유, 참조 할 수 있게 되었다. 이를 활용할 수 있도록 한 것이 공유 라이브러리이다. 과거 OS 는 이 공유 메모리가 거대하게 클 경우에도 프로그램이 실행하기전 로드해야 했기 때문에 비효율적이었지만 현재 OS 는 일단 메모리맵을 설정해 두고 그 메모리 내용이 참조 되었을 때 로드하는 형태를 띄고 있다.

gcc -fPIC -c -o foo.o foo.c 
gcc -fPIC -c -o bar.o bar.c 
gcc -shared -Wl, -soname, libfoo.so.0 -o libfoo.so foo.o bar.o

일반적으로 -shared -Wl, -soname 옵션을 사용해 SONAME을 지정한다.

공유 라이브러리도 정적 라이브러리와 똑같이 링크 시킨다.

gcc -L./ -o main main.o -lfoo

실질적인 내부 처리는 main.o 내에 정의 되지 않은 심볼이 공유 오브젝트에 정의 되어 있다면 정적 라이브러리와 같이 코드를 복사 하는 것이 아니라. 공유 오브젝트의 SONAME 을 실행 파일의 NEEDED 의 설정할 뿐이다. 공유 라이브러리를 사용하는 실행파일을 실행한 경우에는 동적 링커 로더(ld.so) 가 NEEDED 의 정보를 이용해 공유 라이브러리를 찾아내어, 실행 시에 해당 프로세스의 메모리맵을 조작해서 공유 라이브러리와 실행 바이너리가 같은 프로세스 공간을 사용하도록 한다.

이러한 공유 메모리는 실행파일의 크기, 메모리 사용공간에 대해서도 매우 유리하다. PIC 코드로 생성해 두면 코드 부분이 어느 주소에 위치하더라도 변경할 필요가 없기 때문에, 공유 라이브러리를 하나의 물리적인 메모리 페이지에 읽어들이는 것만으로 각각의 메모리 공간에 있는 프로세스에서 공유 라이브러리의 메모리 페이지를 공유할 수 있게 된다. 이러한 공유 메모리의 사용은 라이브러리 패치 시에도 굉장히 유용하다. 데몬과 같이 장시간 실행 하고 있는 프로그램의 경우에는 이전의 공유 라이브러리가 이미 메모리에 로드 되어있으므로 다시 실행 시키는 것이다.

Creative Commons License

'Language > C' 카테고리의 다른 글

라이브러리 공격  (0) 2011.09.08
라이브러리(2)  (0) 2011.09.08
library 의 사용  (0) 2011.09.08
gcc 컴파일 과정 요약  (0) 2011.09.07
gcc  (0) 2011.09.07
:

library 의 사용

Language/C 2011. 9. 8. 09:04

library 의 사용

윤 상배

yundream@coconut.co.kr



1절. 소개

이 문서는 library 의 사용방법에 대한 내용을 담고 있다. 왜 라이브러리가 필요한지, 라이브러리는 어떤 종류가 있으며, 어떻게 작성할수 있는지, 그리고 어떻게 사용하는지에 대해서 얘기하도록 할것이다. 그리고 중간중간에 이해를 돕기 위한 실제 코딩역시 들어갈 것이다.

라이브러리에 대한 이러저러한 세부적인 내용까지 다루진 않을것이다. 좀더 이론적인 내용을 필요로 한다면 Program Library HOWTO 를 참고하기 바란다. 이 문서에서는 라이브러리를 만들고 활용하는 면에 중점을 둘것이다. 그러므로 위의 문서는 이문서를 읽기전에 대충이라도 한번 읽어보도록 한다.

정적 라이브러리와 공유라이브러리는 일반적인 내용임으로 간단한 설명과 일반적인 예제를 드는 정도로 넘어갈 것이다. 그러나 동적라이브러리에 대해서는 몇가지 다루어야할 이슈들이 있음으로 다른 것들에 비해서 좀더 비중있게 다루게 될것이다.


2절. Library 이야기

2.1절. 라이브러리란 무엇인가

라이브러리란 특정한 코드(함수 혹은 클래스)를 포함하고 있는 컴파일된 파일이다. 이러한 라이브러리를 만드는 이유는 자주 사용되는 특정한 기능을 main 함수에서 분리시켜 놓음으로써, 프로그램을 유지, 디버깅을 쉽게하고 컴파일 시간을 좀더 빠르게 할수 있기 때문이다.

만약 라이브러리를 만들지 않고 모든 함수를 main 에 집어 넣는다면, 수정할때 마다 main 코드를 수정해야 하고 다시 컴파일 해야 할것이다. 당연히 수정하기도 어렵고 컴파일에도 많은 시간이 걸린다.

반면 라이브러리화 해두면 우리는 해당 라이브러리만 다시 컴파일 시켜서 main 함수와 링크 시켜주면 된다. 시간도 아낄뿐더러 수정하기도 매우 쉽다.


2.2절. 라이브러리의 종류

라이브러리에도 그 쓰임새에 따라서 여러가지 종류가 있다(크게 3가지). 가장 흔하게 쓰일수 있는 "정적라이브러리"와 "공유라이브러리", "동적라이브러리" 가 있다.

이들 라이브러리가 서로 구분되어지는 특징은 적재 시간이 될것이다.

정적라이브러리

정적라이브러리는 object file(.o로 끝나는) 의 단순한 모음이다. 정적라이브러린느 보통 .a 의 확장자를 가진다. 간단히 사용할수 있다. 컴파일시 적재되므로 유연성이 떨어진다. 최근에는 정적라이브러리는 지양되고 있는 추세이다. 컴파일시 적재되므로 아무래도 바이너리크기가 약간 커지는 문제가 있을것이다.

공유라이브러리

공유라이브러리는 프로그램이 시작될때 적재된다. 만약 하나의 프로그램이 실행되어서 공유라이브러리를 사용했다면, 그뒤에 공유라이브러리를 사용하는 모든 프로그램은 자동적으로 만들어져 있는 공유라이브러리를 사용하게 된다. 그럼으로써 우리는 좀더 유연한 프로그램을 만들수 잇게 된다.

정적라이브러리와 달리 라이브러리가 컴파일시 적재되지 않으므로 프로그램의 사이즈 자체는 작아지지만 이론상으로 봤을때, 라이브러리를 적재하는 시간이 필요할것이므로 정적라이브러리를 사용한 프로그램보다는 1-5% 정도 느려질수 있다. 하지만 보통은 이러한 느림을 느낄수는 없을것이다.

동적라이브러리

공유라이브러리가 프로그램이 시작될때 적재되는 반면 이것은 프로그램시작중 특정한때에 적재되는 라이브러리이다. 플러그인 모듈등을 구현할때 적합하다. 설정파일등에 읽어들인 라이브러리를 등록시키고 원하는 라이브러리를 실행시키게 하는등의 매우 유연하게 작동하는 프로그램을 만들고자 할때 유용하다.


2.2.1절. 왜 정적라이브러리의 사용을 지양하는가

예전에 libz 라는 라이브러리에 보안 문제가 생겨서 한창 시끄러웠던적이 있다. libz 라이브러리는 각종 서버프로그램에 매우 널리 사용되는 라이브러리였는데, 실제 문제가 되었던 이유는 많은 libz 를 사용하는 프로그램들이 "정적라이브러리" 형식으로 라이브러리를 사용했기 때문에, 버그픽스(bug fix)를 위해서는 문제가 되는 libz 를 사용하는 프로그램들을 다시 컴파일 시켜야 했기 때문이다. 한마디로 버그픽스 자체가 어려웠던게 큰 문제였었다. 도대체 이 프로그램들이 libz 를 사용하고 있는지 그렇지 않은지를 완전하게 알기도 힘들뿐더러, 언제 그많은 프로그램을 다시 컴파일 한단 말인가.

만약 libz 를 정적으로 사용하지 않고 "공유라이브러리" 형태로 사용한다면 bug fix 가 훨씬 쉬웠을것이다. 왜냐면 libz 공유라이브러리는 하나만 있을 것이므로 이것만 업그레이드 시켜주면 되기 때문이다.

아뭏든 이렇게 유연성이 지나치게 떨어진다는 측면이 정적라이브러리를 사용하지 않는 가장 큰 이유가 될것이다. 프로그램들의 덩치가 커지는 문제는 유연성 문제에 비하면 그리큰문제가 되지는 않을것이다.


3절. 라이브러리 만들고 사용하기

이번장에서는 실제로 라이브러리를 만들고 사용하는 방법에 대해서 각 라이브러리 종류별로 알아볼 것이다.


3.1절. 라이브러리화 할 코드

라이브러리의 이름은 libmysum 이 될것이며, 여기에는 2개의 함수가 들어갈 것이다. 하나는 덧셈을 할 함수로 "ysum" 또 하나는 뺄셈을 위한 함수로 "ydiff" 으로 할것이다. 이 라이브러리를 만들기 위해서 mysum.h 와 mysum.c 2개의 파일이 만들어질것이다.

mysum.h

int ysum(int a, int b); 
int ydiff(int a, int b);
			

mysun.c

#include "mysum.h"
int ysum(int a, int b)
{
    return a + b; 
}
int ydiff(int a, int b)
{
    return a - b;
}
			


3.2절. 정적라이브러리 제작

정적라이브러리는 위에서 말했듯이 단순히 오브젝트(.o)들의 모임이다. 오브젝트를 만든다음에 ar 이라는 명령을 이용해서 라이브러리 아카이브를 만들면 된다.

[root@localhost test]# gcc -c mysum.c
[root@localhost test]# ar rc libmysum.a mysum.o
			
아주아주 간단하다. 단지 ar 에 몇가지 옵션만을 이용해서 libmysum 이란 라이 브러리를 만들었다. 'r' 은 libmysum.a 라는 라이브러리 아카이브에 새로운 오브젝트를 추가할것이라는 옵션이다. 'c' 는 아카이브가 존재하지 않을경우 생성하라는 옵션이다.

이제 라이브러리가 실제로 사용가능한지 테스트해보도록 하자.

예제 : print_sum.c

#include "mysum.h"
#include <stdio.h>
#include <string.h>

int main()
{
    char oper[5];
    char left[11];
    char right[11];
    int  result;

    memset(left, 0x00, 11);
    memset(right, 0x00, 11);

    // 표준입력(키보드)으로 부터  문자를 입력받는다.
    // 100+20, 100-20 과 같이 연산자와 피연산자 사이에 공백을 두지 않아야 한다.  
    fscanf(stdin, "%[0-9]%[^0-9]%[0-9]", left, oper, right);
    if (oper[0] == '-')
    {
        printf("%s %s %s = %d\n", left, oper, right, 
                        ydiff(atoi(left), atoi(right)));
    }
    if (oper[0] == '+')
    {
        printf("%s %s %s = %d\n", left, oper, right, 
                        ysum(atoi(left), atoi(right)));
    }
}
			

위의 프로그램을 컴파일 하기 위해서는 라이브러리의 위치와 어떤 라이브러리를 사용할것인지를 알려줘야 한다. 라이브러리의 위치는 '-L' 옵션을 이용해서 알려줄수 있으며, '-l' 옵션을 이용해서 어떤 라이브러리를 사용할것인지를 알려줄수 있다. -l 뒤에 사용될 라이브러리 이름은 라이브러리의 이름에서 "lib"와 확장자 "a"를 제외한 나머지 이름이다. 즉 libmysum.a 를 사용할 것이라면 "-lmysum" 이 될것이다.

[root@localhost test]# gcc -o print_sum print_num.c -L./ -lmysum
			
만약 우리가 사용할 라이브러리가 표준 라이브러리 디렉토리경로에 있다면 -L 을 사용하지 않아도 된다. 표준라이브러리 디렉토리 경로는 /etc/ld.so.conf 에 명시되어 있다.

정적라이브러리 상태로 컴파일한 프로그램의 경우 컴파일시에 라이브러리가 포함되므로 라이브러리를 함께 배포할 필요는 없다.


3.3절. 공유라이브러리 제작 / 사용

print_sum.c 가 컴파일되기 위해서 사용할 라이브러리 형태가 정적라이브러리에서 공유라이브러리로 바뀌였다고 해서 print_sum.c 의 코드가 변경되는건 아니다. 컴파일 방법역시 동일하며 단지 라이브러리 제작방법에 있어서만 차이가 날뿐이다.

이제 위의 mysum.c 를 공유라이브러리 형태로 만들어보자. 공유라이브러리는 보통 .so 의 확장자를 가진다.

[root@localhost test]# gcc -fPIC -c mysum.c
[root@localhost test]# gcc -shared -W1,-soname,libmysutff.so.1 -o libmysum.so.1.0.1 mysum.o
[root@localhost test]# cp libmysum.so.1.0.1 /usr/local/lib
[root@localhost test]# ln -s /usr/local/lib/libmysum.so.1.0.1 /usr/local/lib/libmysum.so
			
우선 mysum.c 를 -fPIC 옵션을 주어서 오브젝트 파일을 만들고, 다시 gcc 를 이용해서 공유라이브러리를 제작한다. 만들어진 라이브러리를 적당한 위치로 옮기고 나서 ln 을 이용해서 컴파일러에서 인식할수 있는 이름으로 심볼릭 링크를 걸어준다.

컴파일 방법은 정적라이브러리를 이용한 코드의 컴파일 방법과 동일하다.

[root@coco test]# gcc -o print_sum print_sum.c -L/usr/local/lib -lmysum
			

공유라이브러리는 실행시에 라이브러리를 적재함으로 프로그램을 배포할때는 공유라이브러리도 함께 배포되어야 한다. 그렇지 않을경우 다음과 같이 공유라이브러리를 찾을수 없다는 메시지를 출력하면서 프로그램 실행이 중단될 것이다.

[root@coco library]# ./print_sum
./print_sum: error while loading shared libraries: libmysub.so: cannot open shared object file: No such file or directory
			
위와 같은 오류메시지를 발견했다면 libmysub.so 가 시스템에 존재하는지 확인해 보자. 만약 존재하는데도 위와 같은 오류가 발생한다면 이는 LD_LIBRARY_PATH 나 /etc/ld.so.conf 에 라이브러리 패스가 지정되어 있지 않을 경우이다. 이럴때는 LD_LIBRARY_PATH 환경변수에 libmysub.so 가 있는 디렉토리를 명시해주거나, /etc/ld.so.conf 에 디렉토리를 추가시켜주면 된다.

만약 libmysub.so 가 /usr/my/lib 에 복사되어 있고 환경변수를 통해서 라이브러리의 위치를 알려주고자 할때는 아래와 같이 하면된다.

[root@localhost test]# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/my/lib 
			
그렇지 않고 ld.so.conf 파일을 변경하길 원한다면(이럴경우 관리자 권한을 가지고 있어야 할것이다) ld.so.conf 에 라이브러리 디렉토리를 추가하고 ldconfig 를 한번 실행시켜주면 된다.
[root@localhost test]# cat /usr/my/lib >> /etc/ld.so.conf 
[root@localhost test]# ldconfig
			
ldconfig 를 실행시키게 되면 /etc/ld.so.conf 의 파일을 참조하여서 /etc/ld.so.cache 파일이 만들어지고, 프로그램은 ld.so.cache 의 디렉토리 경로에서 해당 라이브러리가 있는지 찾게 된다.


3.4절. 동적라이브러리의 사용

동적라이브러리라고 해서 동적라이브러리를 만들기 위한 어떤 특별한 방법이 있는것은 아니다. 일반 공유라이브러리를 그대로 쓰며, 단지 실행시간에 동적라이브러리를 호출하기 위한 방법상의 차이만 존재할 뿐이다.

정적/공유 라이브러리가 라이브러리의 생성방법과 컴파일방법에 약간의 차이만 있고 코드는 동일하게 사용되었던것과는 달리 동적라이브러리는 코드자체에 차이가 있다. 그럴수밖에 없는게, 동적라이브러리는 프로그램이 샐행되는 중에 특정한 시점에서 부르고 싶을때 라이브러리를 적재해야 하므로, 라이브러리를 적재하고, 사용하고 해제(free) 하기 위한 코드를 생성해야 하기 때문이다.

linux 에서는 이러한 라이브러리를 호출하기 위한 아래와 같은 함수들을 제공한다. 아래의 함수들은 solaris 에서 동일하게 사용될수 있다.

#include <dlfcn.h>

void *dlopen (const char *filename, int flag);
const char *dlerror(void);
void *dlsym(void *handle, char *symbol);
int dlclose(void *handle); 
			
dlopen 은 동적라이브러리를 적재하기 위해서 사용된다. 첫번째 아규먼트인 filename 은 /usr/my/lib/libmysum.so 와 같이 적재하기 원하는 라이브러리의 이름이다. 만약 적재시킬 라이브러리의 이름이 절대경로로 지정되어 있지 않을경우에는 LD_LIBRARY_PATH 에 등록된 디렉토리에서 찾고, 여기에서도 찾지 못할경우 /etc/ld.so.cache 에 등록된 디렉토리 리스트에서 찾게 된다. dlopen(3) 이 성공적으로 호출되면 해당 라이브러리에 대한 handle 값을 넘겨 준다. flag 는 RTLD_LAZY와 RTLD_NOW 중 하나를 정의할수 있다. RTLD_LAZY는 라이브러리의 코드가 실행시간에 정의되지 않은 심볼을 해결하며, RTLD_NOW 는 dlopen 의 실행이 끝나기전에(return 전에) 라이브러리에 정의되지 않은 심볼을 해결한다.

dlerror 는 dl 관련함수들이 제대로 작동을 수행하지 않았을경우 에러메시지를 되돌려준다. dleooro(), dlsym(), dlclose(), dlopen(3)중 마지막 호출된 함수의 에러메시지를 되돌려준다.

dlsym 은 dlopen(3) 을 통해서 열린라이브러리를 사용할수 있도록 심볼값을 찾아준다. 심볼이라고 하면 좀 애매한데, 심볼값은 즉 열린라이브러리에서 여러분이 실제로 호출할 함수의이름이라고 생각하면 된다. handle 는 dlopen(3) 에 의해서 반환된 값이다. symbol 은 열린라이브러리에서 여러분이 실제로 부르게될 함수의 이름이다. dlsym 의 리턴값은 dlopen 으로 열린 라이브러리의 호출함수를 가르키게 된다. 리턴값을 보면 void * 형으로 되어 있는데, void 형을 사용하지 말고 호출함수가 리턴하는 형을 직접명시하도록 하자. 이렇게 함으로써 나중에 프로그램을 유지보수가 좀더 수월해진다.


3.5절. 동적라이브러리를 사용하여 프로그램의 확장성과 유연성을 높이기

동적라이브러리는 실행시간에 필요한 라이브러리를 호출할수 있음으로 조금만(사실은 아주 많이겠지만 T.T) 신경쓴다면 매우 확장성높고 유연한 프로그램을 만들수 있다.

동적라이브러리의 가장 대표적인 예가 아마도 Plug-in 이 아닐까 싶다. 만약에 모질라 브라우저가 plug-in 을 지원하지 않는 다면 우리는 새로운 기능들 이 추가될때 마다 브라우저를 다시 코딩하고 컴파일하는 수고를 해야할것이다. 그러나 동적라이브러리를 사용하면 브라우저를 다시 코딩하고 컴파일 할필요 없이, 해당 기능을 지원하는 라이브러리 파일만 받아서 특정 디렉토리에 설치하기만 하면 될것이다. 물론 동적라이브러리를 사용하기만 한다고 해서 이러한 기능이 바로 구현되는 건 아니다. Plug-in 의 효율적인 구성을 위한 표준화된 API를 제공하고 여기에 맞게 Plug-in 용 라이브러리를 제작해야만 할것이다.

우리가 지금까지 얘로든 프로그램을 보면 현재 '+', '-' 연산을 지원하고 있는데, 만약 'x', '/' 연산을 지원하는 라이브러리가 만들어졌다면, 우리는 프로그램의 코딩을 다시해야만 할것이다. 이번에는 동적라이브러리를 이용해서 plug-in 방식의 확장이 가능하도록 프로그램을 다시 만들어 보도록 할것이다.


3.5.1절. 동적라이브러리를 이용한 예제

동적라이브러리를 이용해서 main 프로그램의 재코딩 없이 추가되는 새로운 기능을 추가시키기 위해서는 통일된 인터페이스를 지니는 특정한 형식을 가지도록 라이브러리가 작성되어야 하며, 설정파일을 통하여서 어떤 라이브러리가 불리어져야 하는지에 대한 정보를 읽어들일수 있어야 한다. 그래서 어떤 기능을 추가시키고자 한다면 특정 형식에 맞도록 라이브러리를 제작하고, 설정파일을 변경하는 정도로 만들어진 새로운 라이브러리의 기능을 이용할수 있어야 한다.

설정파일은 다음과 같은 형식으로 만들어진다. 설정파일의 이름은 plugin.cfg 라고 정했다.

+,ysum,libmysum.so
-,ydiff,libmysum.so
				
'-' 연산에대해서는 libmysum.so 라이브러리를 호출하며, ydiff 함수를 사용한다. '=' 연산에 대해서는 libmysum.so 라이브러리를 호출하고 ysum 함수를 사용한다는 뜻이다. 설정파일의 이름은 plugin.cfg 로 하기로 하겠다.

다음은 동적라이브러리로 만들어진 print_sum 의 새로운 버젼이다.

예제 : print_sum_dl.c

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

struct input_data
{
    char    oper[2];
    char    func[10]; 
    char    lib[30];
};

int main(int argc, char **argv)
{
    char oper[2];
    char left[11];
    char right[11];
    char buf[50];
    char null[1];
    int data_num;

    struct input_data plug_num[10]; 

    void *handle;

    int (*result)(int, int);
    int i;
    char *error;

    FILE *fp;

    // 설정파일을 읽어들이고 
    // 내용을 구조체에 저장한다. 
    fp = fopen("plugin.cfg", "r");
    data_num = 0;
    while(fgets(buf, 50, fp) != NULL)
    {
        buf[strlen(buf) -1] = '\0';
        sscanf(buf, "%[^,]%[,]%[^,]%[,]%[^,]", plug_num[data_num].oper, 
                                               null,    
                                               plug_num[data_num].func, 
                                               null,
                                               plug_num[data_num].lib);
        data_num ++;
    }
    fclose(fp);

    printf("> ");
    memset(left, 0x00, 11);
    memset(right, 0x00, 11);
    fscanf(stdin, "%[0-9]%[^0-9]%[0-9]", left, oper, right);

    // 연산자를 비교해서 
    // 적당한 라이브러리를 불러온다. 
    for (i  = 0; i < data_num ; i++)
    {
        int state; 
        if ((state = strcmp(plug_num[i].oper, oper)) == 0) 
        {
            printf("my operator is      : %s\n", plug_num[i].oper);
            printf("my call function is : %s\n", plug_num[i].func);
            break;
        }
    }    

    if (i == data_num)
    {
        printf("--> unknown operator\n");
        exit(0);
    }

    handle = dlopen(plug_num[i].lib, RTLD_NOW);
    if (!handle)
    {
        printf("open error\n");
        fputs(dlerror(), stderr);
        exit(1);
    }

    // 연산자에 적당한 함수를 불러온다. 
    result = dlsym(handle, plug_num[i].func);
    if ((error = dlerror()) != NULL)
    {
        fputs(error, stderr);
        exit(1);
    }

    printf("%s %s %s = %d\n",left, oper, right, result(atoi(left), atoi(right)) ); 

    dlclose(handle);
}
				
위의 예제 프로그램은 다음과 같이 컴파일되어야 한다. 라이브러리 파일의 위치는 /usr/my/lib 아래에 있는것으로 하며, 라이브러리 찾기 경로에 등록되어 있다고 가정하겠다.
[root@localhost test]# gcc -o print_sum_dl print_sum_dl.c -ldl 
				
이 프로그램을 실행하면 사용자의 입력을 기다리는 "> "가 뜨게 되고, 여기에 계산하기 원하는 값을 입력하면 된다. 현재는 '+'와 '-' 연산만을 지원하며, 연산자와 피연산자들 간에 간격이 없어야 한다. 다음은 실행결과 화면이다.
  
[root@localhost test]# ./print_sum_dl
> 99+99
my operator is      : +
my call function is : ysum
99 + 99 = 198
[root@localhost test]#
				
사용자가 프로그램을 실행하면 프로그램은 사용자의 입력을 받아들이고 sscanf 를 이용해서 연산자와 피연산자를 구분하게 된다. 그리고 피연산자를 값으로 하여, 설정파일에 설정된 라이브러리를 불러들이고(dlopen) 해당 함수를 가져와서(dlsym) 실행시키게 된다.

자 이렇게 해서 우리는 '+', '-' 연산이 가능한 프로그램을 하나 만들게 되었다. 그런데 A 라는 개발자가 '*','/' 연산도 있으면 좋겠다고 생각해서 아래와 같은 코드를 가지는 '*', '/' 연산을 위한 라이브러리를 제작하였다.

예제 : mymulti.h

int multi(int a, int b);
int div(int a, int b);
				
예제 : mymulti.c
int multi(int a, int b)
{
    return a * b;
}

int div(int a, int b)
{
    return a / b;
}
				

A 라는 개발자는 이것을 다음과 같이 공유라이브러리 형태로 만들어서 간단한 라이브러리의 설명과 함께 email 로 전송했다.

[root@localhost test]# gcc -c -fPIC mymulti.c
[root@localhost test]# gcc -shared -W1,-soname,libmymulti.so.1 -o libmymulti.so.1.0.1 mymulti.o
				

라이브러리를 받았으므로 새로운 라이브러리가 제대로 작동을 하는지 확인을 해보도록 하자. 우선 libmymulti.so.1.0.1 을 /usr/my/lib 로 복사하도록 하자. 그다음 설정파일에 다음과 같은 내용을 가지도록 변경 시키도록 하자.

 
+,ysum,libmystuff.so
-,ydiff,libmystuff.so
*,ymulti,libmymulti.so.1.0.1
/,ydiv,libmymulti.so.1.0.1
				
이제 print_sum_dl 을 실행시켜보자.
[root@localhost test]# ./print_sum_dl
> 10*10
my operator is      : *
my call function is : ymulti
10 * 10 = 100

[root@localhost test]# ./print_sum_dl
> 10/10
my operator is      : /
my call function is : ydiv
10 / 10 = 1
				
print_sum_dl.c 의 원본파일의 아무런 수정없이 단지 설정파일만 변경시켜 줌으로써 기존의 print_sum_dl 에 "곱하기"와 "나누기"의 새로운 기능이 추가 되었다.

위에서도 말했듯이 이러한 Plug-in 비슷한 기능을 구현하기 위해서는 통일된 함수 API가 제공될수 있어야 한다.


4절. 결론

여기에 있는 내용중 동적라이브러리에 대한 내용은 솔라리스와 리눅스에서만 동일하게 사용할수 있다. Hp-Ux 혹은 윈도우에서는 사용가능하지 않는 방법이다. 이에 대한 몇가지 해법이 존재하는데, 이 내용은 나중에 시간이 되면 다루도록 하겠다. 어쨋든 솔라리스와 리눅스 상에서 코딩되고 윈도우 혹은 다른 유닉스로 포팅될 프로그램이 아니라면 위의 방법을 사용하는데 있어서 문제가 없을것이다.

출처 :  
http://www.joinc.co.kr/modules/moniwiki/wiki.php/Site/C/Documents/Make_Library 

'Language > C' 카테고리의 다른 글

라이브러리(2)  (0) 2011.09.08
라이브러리(1)  (0) 2011.09.08
gcc 컴파일 과정 요약  (0) 2011.09.07
gcc  (0) 2011.09.07
[Linux/Unix]make 명령 코드 구현하기  (0) 2011.08.23
:

gcc 컴파일 과정 요약

Language/C 2011. 9. 7. 16:06
gcc 의 컴파일 과정을 간략히 정리 해 봅니다.

gcc 는 사실 컴파일러 자체를 말하는 것은 아니고, 컴파일 과정에 필요한 여러가지 기능들을 호출하는 역할을 합니다.
따라서 gcc 는 c 언어 뿐만 아니라 다른 언어들도 컴파일이 가능 합니다. 일련의 과정에 다른 언어를 컴파일하는
녀석들을 호출만 해주면 되기 때문입니다.

[컴파일 과정]
전처리 과정 -> 어셈블리 소스파일로 컴파일과정 -> 인스트럭션 코드 생성 과정 -> 링크 과정 

1. 전처리 ( ccp0 ) 
    ★ 소스파일에 포함된 (#include) 파일들을 불러와 현재컴파일할 파일에 복사해서 붙여 넣는다.
     * 결과물 : test.i 

2. 어셈블리 코드로 컴파일 (cc1) 
    ★ 실제 컴파일러라 할 수 있다.
    ★ 어휘분석 -> 구문분석 -> 의미분석 -> 중간언어생성 -> 코드최적화 -> 목적코드생성 의 과정을 거친다.
☆ 어휘 분석 : 전처리 과정에서 생성된 test.i 파일을 문법적 의미가 있는 최소 단위 (토큰) 로 나눈다.
☆ 구문 분석 : 문법석 오류가 있는지 검사 한 후 파서트리를 만든다.
☆ 의미 분석 : 문법상 문제가 아닌 의미상 문제, 즉 선언되지 않은 변수의 사용이나 자료형 불일치,
                    함수 인자개수 등의 문제를 검사 한다.
☆ 중간언어 생성 : 어셈블리 코드로 만들기 전에 최적화를 위해 RTL(Register Transfer Language) 라는
                          lips 언어와 유사한 코드로 생성한다.
    -> 결과물 : test.rtl
☆ 코드 최적화 : 코드의 사이즈를 줄이고, 속도를 높이기 위해 최적화를 진행 한다. gcc 컴파일의 대부분의
                       시간이 소요되며 아래 두단계를 거친다.
    - 중간코드 최적화 
1) 지역 최적화 : 연산강도 경감, 상수계산등의 최적화
2) 전역 최적화 : 사용되지 않는 코드제거
3) 루프 최적화 : 사용하지 않는 루프제거, 루프결합
    - 목적코드 최적화
1) 최대한 메모리보다 레지스트를 사용하게 하고, 효율적인 인스트럭션을 선택하여 메모리 접근을 최적화 한다.
 ☆ 목적코드 생성
    -  최적화된 rtl 코드를 어셈블리 코드로 변환 한다. 
    - 어셈블리코드는 cpu 마다 정해진 인스트럭션(기계어)과 1:1 로 매칭된 코드 이다.
    -> 결과물 : test.s

3. 기계어 코드 생성(as)
    ★ .s 인 어셈블리 코드를 .o 인 오브젝트 파일로 바꾼다.
    ★ 생성된 오브젝트 파일은 ELF(Executable and Linking) 바이너리 파일 구조 규약을 따르며, 
        이유는 여러 오브젝트 파일을 링크 할 때 파일의 구조가 다르다면 불가능 하기 때문임. 바이너리 포멧은 
         a.out  / ELF / COFF 등이 유닉스 시스템 에서 쓰였으며, 현재는 ELF 가 대부분 사용된다. 
         윈도우 시스템 에서는 COFF , PE 가 쓰인다.
        ☆ ELF 파일 구조는 맨위에 ELF 파일헤더, 프로그램 헤더 Table, 섹선1 ~ n , 섹션 헤더 테이블로 구된다. 
        ☆ ELF 파일 헤더를 제외하고 나머지는 컴파일된 파일 마다 다를 수 있으며, 인스트럭션과 데이터, 
            GCC컴파일러 버전 등이 기록 된다.

4. 링크 (collect2)
    ★ 여러개의 오브젝트 파일을 하나로 링크하는 과정이며, 주로 라이브러리의 링크가 이루어 진다.
        ☆ 정적 라이브러리 : 링크시에 라이브러리를 포함해서 실행 파일을 만든다. 따라서 다른 프로그램에서도
                                     같은 라이브러리를 사용 한다면, 중복되기 때문에 용량을 많이 차지 하게 되지만,
                                     링크가 완료된 상태이므로 속도는 빠르다.
        ☆ 동적 라이브러리(공유 라이브러리) : 링크시 라이브러리의 포함 여부만 기록하고, 실제 동작할 때 메모리에
                                     라이브러리를 로드하여 사용한다. 
                                     다른 프로그램에서 같은 라이브러리를 사용 할 경우 메모리에 이미 로드된 것을 사용 하므로,
                                     공유 한다는 의미에서 공유 라이브러리 라고도 한다. 
                                     따라서 단 한개의 프로그램이라도 이 라이브러리를 사용 하고 있으면, 메모리에 상주 하며,
                                     아무 프로그램도 사용 하지 않는다면, 메모리에서 제거 된다. 
                                     실행시 로드되므로 속도는 다소 느릴 수 있으나 중복로드 되지 않아 메모리 사용이 효율 적이다.  
-> 결과물 : 실행 파일

'Language > C' 카테고리의 다른 글

라이브러리(1)  (0) 2011.09.08
library 의 사용  (0) 2011.09.08
gcc  (0) 2011.09.07
[Linux/Unix]make 명령 코드 구현하기  (0) 2011.08.23
make (Makefile)  (1) 2011.08.23
:

gcc

Language/C 2011. 9. 7. 16:04

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 옵션에 대하여 배우셨습니다. 그리고 또한 라이브러리 만들기에 대하여 맛보기를 하였습니다.

'Language > C' 카테고리의 다른 글

library 의 사용  (0) 2011.09.08
gcc 컴파일 과정 요약  (0) 2011.09.07
[Linux/Unix]make 명령 코드 구현하기  (0) 2011.08.23
make (Makefile)  (1) 2011.08.23
gcc 컴파일 과정  (0) 2011.08.23
: