KUR Creative


CSAPP독학

[CS:APP 13-2] Linking 2 - 정적 링킹lib과 동적 링킹dll

링커가 1. symbol resolution을 무사히 끝마쳐서
여러개의 relocatable object file들에 있는 symbol reference들을
단 하나의 symbol definition들에 각각 이름에 맞게 적절히 연결시켰다면

링커가 다음으로 수행할 2번째 일은 2. Relocation이다.
Relocation은 말 그대로 바이너리들의 위치를 바꾸는 일이다.

ELF 파일 형식

이를 이해하려면 먼저 실행파일들의 일반적인 format인 ELF에 대해 알아야 한다.

resource/marcil.png
ELFExecutable and Linkable Format의 두문자어로,
실행 파일, 혹은 링킹 가능한 파일들: executable, relocatable object file(.o), shared object file(.so)
이 모두 따르는 파일 포맷이다.

resource/link8.png
보면 .data 섹션에 초기화된 전역변수들이 저장된다 - 즉 strong symbol이 여기 저장됨.
그리고 .bss에는 초기화되지 않은 전역변수들이 저장된다.
악이다. 악. EVIL! 최대한 줄여라..
resource/BSS.png
여튼 그래서 .bss는 공간을 차지하지 않는다.

resource/link9.png
.rel.text 섹션과 .rel.data 섹션은 relocation에 대한 정보를 저장한다.
즉 실행파일을 만들 때 바꿔줘야하는 포인터들과 인스트럭션의 주소를 저장한다.

그리고 .symtab 섹션은 아까 몇번 언급한 symbol table이 저장되는 영역이다.
여기에 symbol들의 이름과 정의, 위치, 크기등이 struct의 배열로 저장되어 있다.


relocation은 컴파일러들이 생성한 relocatable object file(.o 파일)들에 있는
.data.text 섹션을 각각 빼낸다음 executable file의 .text 섹션과 .data 섹션으로 만드는 것이다.
resource/link10.png
여기서 이전에 제기했던 의문을 해결할 수 있다.

왜 .o 파일만으로는 실행이 불가능할까?

그것은 우리가 만든 main.o파일만으로 프로그램이 실행되는 것이 아니기 때문이다.

우리가 만든 응용프로그램은 운영체제 위에서 돌아가고
운영체제를 위한 코드와 데이터들이 바로 libc에서 제공하는 System code와 data들이다.
실제로는 system code에서 여러가지 일들을 응용프로그래머 모르게 처리해준 다음
(loader라는 게 해준다... 궁금하면 책 사서 읽으쇼 ^_^)

가장 마지막으로 main.o에 있는 main함수를 호출한다.
그때부터 우리가 만든 프로그램이 실행되는 것이다.

그리고 main함수가 반환되는 순간 다시 실행이 system code로 돌아가서
응용프로그래머 모르게 적절한 처리를 해준다.

그래서 링킹없이 relocatable object file만으로는 실행이 불가능하다.

Relocation 과정

링커가 수행하는 relocation 때문에, 컴파일러는 .o파일을 만들 때
어떤 symbol이 어느 주소에 있을지 알 수가 없다(컴파일 -> 링킹 이니까).

resource/link10.png
그래서 컴파일러는 링커에게 "여기는 니(링커)가 나중에 symbol이 있을 주소를 알아서 적어 넣어줘"라는 요청을 한다.
그러한 명령을 컴파일러에게 내리는 인스트럭션이 있다.

resource/link11.png
위의 괴상한 코드는 main.c를 컴파일하여 나온 main.o의 바이너리를 분석하여
어떤 어셈블리 코드가 사용되었는지 살펴본 것이다.

위의 암호같은 숫자들은 이진 파일의 주소: byte값 byte값... 이런 식으로 나와 있는 것이다.
위에서 9: bf 00 00 00 00 부분을 주목해보면
bf는 값을 넣으라는 뜻이고 00 00 00 00은 그 값이다.

이 부분은 사실 전역변수인 array에 대한 참조가 머신코드로 번역된 부분이다.
컴파일러는 참조인 array가 어떤 주소를 가리킬지 알 수 없다.
컴파일을 할 당시에 .o파일의 코드와 데이터들이 있던 주소들은 실행파일로 합체되면서 완전히 달라지기 때문이다.

그래서 컴파일러는 모르는 참조(빨간 사각형)를 0으로 채워두고
a:R_X86_64_32 array라는 인스트럭션으로 링커에게 차후에 주소 a부터(빨간 사각형) 값을 채워넣으라는 것이다.
f:R_X86_64_PC32 sum-0x4도 대충 비슷한 의미로 주소 f부터 4바이트는 링커가 알아서 채워넣으라는 명령이다.


흠..? 그러면 링커가 진짜 일을 잘 했는지 알아보자.
다음은 main.o와 sum.o를 붙여 실행파일을 만든 다음
실행파일의 바이너리를 분석한 것이다.
resource/link12.png
보면 main함수의 시작 위치가
주소 0에서 주소 0x4004d0로 바뀌었음을 알 수 있다. relocation이 일어났다.

그리고 원래의 main.o에서
000009: bf 00 00 00 00 였던 부분이
4004e3: bf 18 10 60 00 이 되었다! 링커가 일을 잘 해 주었다.

즉 relocation은 컴파일러와 링커의 협력 하에 일어난다.
컴파일러가 링커에게 "여기는 심볼 참조니까 난 몰라 니가 알아서 주소 적어" 하고 요청하고
링커는 .o파일들을 쪼개서 실행파일을 만든다음 요청에 따라 적절한 주소를 적어 넣어 준다.

라이브러리

우리는 링커가 하는 일들을 모두 알았다.
그러면 이제는 lib과 DLL라이브러리에 대해 알아보자.

허구한날 쓰이는 함수들을 어떻게 패키지로 만들고 관리할까?

  1. 모든 함수들을 하나의 .c소스파일로 만들고 .o파일로 만들어 둔다.

    • 반론: 필요없는 함수까지 링크시켜야 한다. 링킹시간, 공간(바이너리크기)낭비다
  2. 각각의 함수들을 하나의 소스파일로 만들고, 프로그램에 필요한 함수가 들어있는 .o파일만 링크시킨다.

    • 반론: 효율적이나 드럽게 어렵다... 모든 함수가 어디에 있는지 알아야 하고
      커맨드라인이 졸라 길어진다 ex) gcc -o prog sum.o log.o malloc.o... 쓰는 걸 다 써줘야 해!

이러한 문제점을 해결한 첫번째 방법이 lib, a(archive) 파일.
static library다.

정적 라이브러리(lib)

각각의 함수func 하나씩을 각각의 소스파일func.c로 만들고
제각각 컴파일하여 relocatable object file들func.o을 만든다.

그리고 이런 foo.o bar.o func.o ... 파일들을 인덱스 테이블과 함께
**하나의 파일(libXXX.a)**로 만든다
resource/link13.png

그리고 새로운 실행파일(내꺼)을 만들 때 아카이브(.a)파일의 멤버중 참조가 있었던 .o파일만 빼내서
새로 만드는 실행파일의 .o와 링크시킨다.

여러개의 아카이브 파일이 사용될 수도 있다.

resource/link14.png
만일 아카이브(.a파일)를 업데이트하려면 업데이트 하려는 .o파일만 재컴파일해서 다시 링크하면 된다.
간단함!

하지만 static library는 몇가지 단점이 있다.

  1. 디스크 중복. printf를 쓰는 모든 실행파일들은 printf.o파일을 링크해서 가지고 있어야 한다.
    디스크 공간이 낭비된다.
  2. 메모리 중복. 현재 실행되는 프로세스(메모리에 올려져 실행중인 프로그램)들 간에도 중복이 일어난다.
    현대적인 시스템은 보통 몇천개의 프로세스가 동시에 실행되고, 거의 대부분의 C 프로그램은 printf를 사용한다.
    printf는 쓸데없이 중복되어 메모리를 차지하게 되고 이는 분명한 메모리 공간 낭비다.
  3. 시스템 라이브러리의 작은 버그 픽스가 일어난다면, 이를 사용하는 모든 프로그램들은 다시 링크되어야 한다.

동적 라이브러리(DLL, Shared Lib)

resource/link15.png
현대적인 해결책은: Shared Library이다. 혹은 Dynamic Link Library. 즉 DLL이라고 불리기도 한다.

아 너무 멋있다... 근데 dynamic linking동적 링킹은 어떻게 일어나길래 이렇게 멋있는가?

load-time DLL 링킹

dynamic linking의 핵심은

  1. 실행 파일을 만들 때 부분적으로 링킹을 해준다.
  2. 프로그램 실행시에 loader가 실행파일을 메모리에 올린 후 dynamic linker를 호출하고 컨트롤을 넘겨준다.
  3. dynamic linker는 사용하려는 shared library(.so파일)을 메모리에 올려 링킹을 완료한다.

resource/link16.png
부분 링킹은 relocation과 symbol table에 대한 정보만을 포함하여 실행파일에 링킹한다.
이를 통해 실행파일에서 나중에 어떤 라이브러리의 어떤 심볼을 참조할 지 알 수 있다.
그러나 이 때 .so 파일의 코드나 데이터는 복사되지 않는다.(즉, .text나 .data 섹션은 복사되지 않는다)

완전한 링킹은 Dynamic linker에 의해서 이루어진다.
Dynamic linker는 shared library(.so)의 코드와 데이터를 메모리의 어떤 위치에 로드한다(.text, .data섹션들)

여기까지가 load-time에 자동으로 이루어지는 동적링킹이고

run-time DLL 링킹

다음은 run-time에 명시적으로 이루어지는 동적 링킹이다.

resource/link17.png
​dlopen​이라는 함수를 통해 런타임에 .so파일을 열고

resource/link18.png
dlsym​이라는 함수에 사용하려는 ​함수 이름​을 먹여서 반환된 ​함수 포인터​를 받아
.so파일의 함수를 사용할 수 있다.



library interpositioning을 설명하려다 말았다
넘 힘드렁
궁금하면 검색하라우


이거 정리 하는 거 졸라 오래 걸리네 ㅋㅋㅋ
책은 거의 못읽음 ㅋㅋㅋㅋㅋ


과거 블로그 댓글
프로그래밍 갤러리 댓글


23년 추가
나 미1친 놈인가? 이거 다 옮기는데만 몇시간이 걸렸다
그러면 글 처음 쓸 때는 대체 얼마나 시간을 들인거야

진짜 광기다
왜 이런 짓을 한거지?
념글에 미친놈이었나

#from/old-blog
이전글CSAPP독학다음글
kur1701091416Archivekur1701222256