ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 라이브러리
    Linux System 2019. 3. 11. 01:21

    라이브러리 정리를 도와주실 코드 3분을 모셔봤다.

    우선 헤더파일(*.h)은 그 자체로는 *.c 파일을 가리킬 수 없다.

    1. 헤더 파일에 선언 뿐 아니라 구현까지 다 해놓던가

    2. 각 소스코드를 기계어 코드로 바꾼 뒤 링킹을 하던가

    3. 아니면 라이브러리를 쓰던가


    1. 처음에 많이 사용하는 2번 방식.

    -> 위 예를 컴파일 하게 되면 "gcc main.c my_lib.c -o main" 이런 식으로 하게 됨.

    이를 뜯어보면

    -> 위 예를 보면 main.c에는 전역변수 my_data 선언과 함수 foo 구현이 없음.

    그래서 main.c를 main.o로 바꿔도 my_data, foo의 심볼을 알 수가 없음.

    (즉, 완벽한 기계어로 바뀌지 않고 비어있음)


    -> 이 불완전한 main.o(ELF파일)의 포맷 중 .rel.text라는 섹션이 존재.

    여기에 알 수 없는 심볼들을(my_data, foo) 정의해야 한다는 내용이 써져있음.

    "readelf -r main.o" : ELF 파일의 내부 내용을 파싱해서 보여주는 것

    .rel.text 부분의 Sym.Name에 my_data, foo가 써져 있음을 알 수 있음.

    그리고 Offset에 해당하는 위치에 해당 심볼이 위치하고 그 위치를 대체(relocation)해야함을 알 수 있음.


    (Sym.Name : relocatable Symbol이 Symbol reference resolvng이 필요한. 즉 링킹 시 다른 소스? 라이브러리? 여튼 다른데서 가져와야할 심볼들)


    링커는 이 부분을 보고 정의 되지 않은 심볼들을 정의해 주어 컴파일이 완료되게 함.


    따라서 파일에는 위의 심볼들을 정의해 줄 수 있게 링커에게 정보를 주는 실제  데이터 코드가 존재

    (즉, 라이브러리의 사용으로 그냥 껍데기만 있던 심볼들을 실제 주소로 대체)


    -> 2번 방식의 문제점 ( 라이브러리를 쓰지 않고 소스파일, 헤더파일을 추가하는 방식)

    1. main이 바뀌어도 my_lib.c까지 역시 재컴파일이 되어야 함.

    2. 코드가 공개됨 -> 소스코드를 주기 때문에




    2. 라이브러리 방식


    1) 라이브러리의 종류




    2) 정적 라이브러리 (*.a)(archive)

    컴파일 시 라이브러리를 사용해 relocatable symbol들을 reference resolving 하는것(링킹)

    즉, 컴파일 시 필요한 코드(라이브러리)를 프로그램에 적재 -> 완벽한 하나의 프로그램을 만듬.


    장점 : 결과 실행파일은 외부 어떤 라이브러리에도 의존성이 없어져 이식성이 좋고,

     런타임시 외부를 참조할 필요가 없어 속도에도 장점이 생김

    단점 : 필요한 코드를 프로그램에 다 적재해 두므로 (.text) 프로그램 크기가 약간 커짐.

     라이브러리 변경이 필요할 시(패치), 라이브러리만 재배포하면 되는 것이 아니라   변경된 라이브러리로 재컴파일된 통짜 프로그램을 재배포 해야함.


    만드는 법

    1. 라이브러리의 *.c 파일들을 컴파일 해 *.o 파일(ELF 파일)로 변경

    2. ar util로 1에서 나온 *.o 파일들을 *.a, 정적 라이브러리 파일로 만듬.


    gcc -c my_lib.c        // object 파일 생성


    ar rc libmy_lib.a my_lib.o     //  *.a 파일의 이름 포맷은 항상 lib(libaray_name).a 일것.


    gcc main.c -o main -L./ -lmy_lib

    파일 이름이 lib(library_name).a 이므로, 자동적으로 라이브러리의 이름은(라이브러리 파일의 이름이 아님) "my_lib"이 된다.

    -> -L(library's path) -l(library's name)



    3) 공유 라이브러리(*.so) => gcc에 아무 옵션도 주지 않는 경우

    응용 프로그램이 시작되는 순간, 즉, 커널이 사용자 주소공간(VM)을 만들고, 응용프로그램 ELF를 적재하는 순간 같이 메모리에 적재됨.


    - 이 공유 라이브러리를 사용하는 다른 프로그램이 이미 실행되어, 해당 라이브러리가 메모리에 올라가 있으면 그것을 참조해서 링킹

    - 없으면 라이브러리를 메모리 상에 올리고 링킹

    (공유 라이브러리에서는 링킹도 코드를 완전히 붙이는 것이 아님)


    즉, 메모리에 라이브러리를 한번 만 올려두고, 프로세스는 그 라이브러리의 원하는 symbol이 있는 주소를 참조만 함.

    (메모리 구조에서 libc 부분에 적재)


    장점  

    프로그램 변경 시 변경된 부분의 공유 라이브러리만 재배포하면 되므로 유지 보수가 쉽고 유연

    라이브러리를 프로그램에 완전히 붙이는것이 아닌 여러 프로그램이 한 번 메모리에 올려진 것을 공유하므로 메모리 사용 공간이 정적 라이브러리보다 적음

    단점

    외부 의존도가 생겨 이식성이 떨어짐

    공유 라이브러리를 메모리에 올리려면 찾고 올리는데 시간이 걸리므로 성능저하가 있음(이론상...)


    - 공유 라이브러리는 이식성이 낮기 때문에, 업데이트 시 라이브러리의 원할한 재배포와 더불어 라이브러리들의 버전 관리가 필수

    그래서 관습적으로


    "[접두사 lib] + [라이브러리 이름] + [.so.] + [Major 번호] + [Minor 번호] + [배포번호]" 


    ex) libmy_lib.so.1.2.3


    형식을 따름


    - 만드는 법

    1. -fPIC 옵션을 주어 라이브러리를 *.o 파일로 컴파일

    2. *.o파일을 공유 라이브러리 *.so 파일로 컴파일

    "gcc -shared -o lib(library_name).so.(major_num).(mino_num).(release_num) *.o"

    3. 위 과정을 거쳐 나온 라이브러리 파일을 /usr/lib으로 이동.

    여기에서 /usr/lib으로 옮겨주지 않을 경우 gcc로 컴파일 시 -L[path]로 라이브러리를 검색할 경로를 추가해 주어야 함.

    -> gcc에서는 기본적으로 /lib이나 /usr/lib을 뒤짐

    4. /etc/ld.so.cache 업데이트를 위해 ldconfig 명령 수행

    5. 이식성을 위해 버전명을 빼고 링크

    6. 사용


    - Ex)

    1. gcc -fPIC -c my_lib.c         // object 파일 생성

    2. gcc -shared -fPIC -o libmy_lib.so.1.0.1 my_lib.o    // *.so 생성

    3. mv libmy_lib.so.1.0.1 /usr/lib/libmy_lib.so.1.0.1

    4. ldconfig

    5. ln -s /usr/lib/libmy_lib.so.1.0.1 /usr/lib/libmy_lib.so

    6. gcc -o main main.c -lmylib




    4) 동적 라이브러리 (*.so)

    프로그램 실행 도중 응용 프로그램에서 특정 라이브러리를 사용할지 말지를 결정함.


    즉, 정적 : 컴파일 타임에

    공유 : 프로그램 실행 시 reference resolving

    동적 : relocatable symbol을 사용할 때 reference resolving


    장점 : 공유 라이브러리 같이 유연하고 확장성 높은 프로그램을 만들 수 있음.

    단점 : 이식성이 떨어짐.

    ( 공유 라이브러리와 거의 동일)


    - 만드는 법은 공유 라이브러리와 동일


    공유 라이브러리와의 차이는 reference resolving을 언제 하느냐.

    공유 : 프로그램 실행시 runtime link editor가.

    동적 : 프로그램이 특정 라이브러리를 사용할 때.


    1. 공유라이브러리와 같은 방식으로 *.so를 만들어 냄

    2. 소스코드 내에서 dl* 관련 함수를 이용해서 라이브러리를 사용 가능


    (1) void* dlopen(const char* library_name, int flag)

    library_name : 라이브러리를 flag에 맞추어 열고 그 핸들을 리턴

    flag : * RTLD_NOW - dlopen 실행이 끝나기 전에 (즉 리턴전에) reference                             resolving을 함

    * RTLD_LAZY - 실행시간에, 즉 특정 심볼을 사용할 때, 그 심볼에 대한                     reference resolving을 함

    (2) int dlclose(void* dl_handle) : 라이브러리 핸들을 닫음

    (3) void* dlsym(void* dl_handle, char* symbol_name)

    : 라이브러리 핸들에서 특정 심볼을 가져옴

    (4) const char* dlerror(void) : 에러를 반환함.


    3. 라이브러리를 사용할 응용프로그램을 컴파일 (이 때, 동적 라이브러리를 사용하기     위한 과정을 거침)

    "gcc -o main main.c -ldl -rdynamic"

    -ldl : dl* 관련 함수를 사용하기 위한 라이브러리 추가

    -rdynamic : (dlopen에서 역추적을 허용하도록) 모든 심볼을 동적 심볼 테이블에                 추가하라고 링커에 알려주는 목적

    - 정리

    1. dlopen으로 라이브러리를 열고, 그것을 void*인 dl_handle에 담음.

    2. dlsynㅇ로 필요한 심볼을 읽어옴.

    2-1. 함수이면 함수 포인터로 받음.

    2-2. 전역변수이면 포인터로 받음.

    3. dlclose로 사용한 라이브러리 핸들을 닫음.


    Ex)





    ※ 메모리에 공유라이브러리가 없는 경우 찾아서 올리게 되는데, /lib/ld.so 또는 

    /lib/ld-linux.so라는 공유라이브러리 로더에 의해 로드를 하게 됨.

    공유라이브러리 로더 : 공유라이브러리를 로드하고 참조함수를 확인하는 프로그램

    LD_LIBRARY_PATH나 /etc/ld.so.conf에 설정된 경로를 통해 공유 라이브러리를 검색함.

    해당 conf 파일에 경로를 추가한 후 'ldconfig' 명령을 이용하면 캐시를 업데이트 함.


    캐시 : 로드되는 시간을 최소화 하기 위해서 공유라이브러리 로더는 /etc/ld.so.cache에 공유 라이브러리 로드 때마다 캐시를 저장 및 유지 관리

    (즉, 캐시 파일은 ldconfig 응용 프로그램에 의해 생성, 관리되고 이 ldconfig 프로그램은 /etc/ld.so.conf 파일을 기반으로 수행을 함)


    'Linux System' 카테고리의 다른 글

    ELF 파일 포맷(3)  (0) 2019.03.14
    ELF 파일 포맷(2)  (0) 2019.03.11
    ELF 파일 포맷(1)  (0) 2019.03.11
    파일 디스크립터  (0) 2019.03.10

    댓글

Designed by Tistory.