[SW정글 36일차] 링커

rg.log·2022년 10월 24일
0

SW 사관학교 JUNGLE

목록 보기
7/31

링킹(linking)이란

여러 코드와 데이터를 모아 연결하여 메모리에 로드될 수 있고 실행될 수 있는 1개의 파일로 만드는 작업이다.

링킹은 컴파일 때 뿐 아니라, 프로그램이 실행을 위해 메모리에 로드되고 로더에 의해 실행될 때에는 로드 타임 때에도, 심지어 실행 중에도 할 수 있다.
컴파일 시에는 정적 링커에 의해, 로드타임과 런타임에는 동적 링커에 의해 수행된다.

링커의 역할을 예를 들면 카페 사장님이 맛있는 당근 케이크를 만들고자 할 때, 좋은 당근을 캐서 손질하는 레시피와 좋은 밀가루를 구해서 반죽하는 레시피를 순서에 맞게 섞어 당근 케이크를 만드는 레시피를 탄생시키는 것이 링커의 역할이다. 이 후 해당 레시피를 제빵사에게 전달하여 맛있는 당근 케이크가 탄생하게 된다. 제빵사의 역할은 눈치챘듯 CPU로 볼 수 있다.

링커라는 프로그램에 의해 자동으로 되는데 왜 배워야할까?!

링커가 소프트웨어 개발자에게 중요한 이유는, 독립적인 컴파일을 가능하게 하기 때문이다. 모듈 중 한 개만 변경시, 다른 파일들은 재컴파일의 필요없이 해당 파일만 간단히 재컴파일 후 링크한다. 평소 컴팩트하다는 말을 많이 사용하는데, 이 얼마나 컴팩트한가. 뿐만 아니다.

  1. 큰 프로그램을 작성하는데 도움이 된다.
  2. 위험한 프로그래밍 에러를 피할 수 있게 된다.
  3. 링킹의 이해로 공유 라이브러리에 대해 이해할 수 있다.

링킹을 알면 좋은 점을 명확히 알아야 공부하고 싶을 것이다. 각각에 대해 좀 더 자세히 보자.

1. 큰 프로그램을 작성하는데 도움이 된다.

큰 규모의 프로그램을 개발하는 프로그래머는 종종 모듈이 없어서, 라이브러리 없거나 맞지 않은 버전으로 링커 에러를 발생시킨다. 이제는 어떻게 링커가 참조를 해결해가고, 어떻게 링커가 라이브러리를 사용하는지 이해하여 이러한 에러를 이해하고 해결할 수 있다.

2. 위험한 프로그래밍 에러를 피할 수 있게 된다.

위험한 프로그래밍 에러 중 하나인 전역변수를 중복해서 정의한 프로그램도 기본 설정의 경우 경고 메세지 없이 링커를 통과할 수 있다. 어느 순간 해당 프로그램은 혼란스러운 런타임으로 디버깅에 어려움을 줄 수 있다. 왜 이런 일이 발생하고, 어떻게 회피하는지 공부하자.

3. 링킹의 이해로 공유 라이브러리에 대해 이해할 수 있다.

현대 운영체제에서 공유 라이브러리와 동적 링킹의 높아진 중요성으로, 링킹은 유능한 프로그래머에게 상당한 능력을 제공하는 복잡한 프로세스가 되었다.
예를 들어보자. 많은 sw 제품들은 공유 라이브러리를 사용해서 크기가 줄어든 바이너리를 런타임에 업그레이드할 수 있게 해준다.
또, 많은 웹서버는 동적 컨텐츠를 서비스하기 위해 공유 라이브러리의 동적 링킹을 사용한다.

자, 이제 내가 알아야하는 알고 싶은 링커가 어디서 등장하는가.

컴파일 과정 속 링커


이미 알듯 위의 과정으로 컴파일이 될 때
대부분 컴파일 시스템은 사용자 대신 언어 전처리기, 컴파일러, 어셈블러, 링커를 필요에 따라 호출하는 컴파일러 드라이버를 제공한다.

이해를 위해 예시를 가져왔다. main.c라는 소스 코드에서 sum.c라는 소스 코드를 불러와 사용할 때, 컴파일러 드라이버는 무엇을 할까?

드라이버는 먼저 C 전처리기(cpp)를 돌리고, 소스 파일 main.c를 main.i로 번역한다.
다음으로, c 컴파일러(cc1)을 돌려서 main.i를 어셈블리 언어 파일인 main.s로 번역한다.
다음으로, 어셈블러를 돌려서 main.s를 재배치 가능한 바이너리 목적파일인 main.o로 번역한다.

동일 과정을 걸쳐 sum.o를 생성하고,
마지막으로 링커 프로그램 ld를 실행하여, 필요한 시스템 목적파일들과 main.o와 sum.o를 연결해 실행 가능 목적파일 prog를 생성한다.

실행파일인 prog를 실행시키고자 할 때 아래와 같은 입력을 할 것이다.

linux> ./prog

하면 쉘은 로더(loader) 즉, os내의 함수를 호출하며, 로더는 실행파일 prog의 코드와 데이터를 메모리에 복사하고, 제어를 prog 프로그램의 시작 부분으로 전환한다. 이 것이 프로그램이 실행되기까지의 과정이다.

여기서 알 수 있듯 정적 링커들은 main.o와 sum.o와 같은 재배치 가능한 목적파일들을 받아들여 로드 및 실행될 수 있는 완전히 링크된 실행 가능 목적 파일을 생성한다.

정적 링커에 대해 보았으니 동적 링크는 무엇이 다른지, 실행 가능한 파일이 생겼는데 동적 링커는 등장하지 않았는지 궁금할 것이다.

현대 많이 사용되는 동적 링킹

우선 정적 라이브러리들은 다른 모든 소프트웨어처럼 관리해야 하고 주기적으로 갱신해야 하는 단점이 있다.또한 printf와 scanf와 같은 표준 I/O 함수들을 거의 모든 C 프로그램이 사용하기 때문에 각각 프로세스의 메모리 내에 저장되야 하므로 이는 상당한 메모리 자원의 낭비가 될 수 있다. 개발자특 이러한 단점이 있으면 보완하는 무언가를 만들어낸다. 그것이 공유 라이브러리다.

공유 라이브러리는 런타임이나 로드타임에 임의의 메모리 주소에서 로드되고, 메모리에서 프로그램으로 연결될 수 있는 목적 모듈이다. 이 과정이 동적 링킹이고 동적 링커라는 프로그램에서 수행된다. 리눅스에서는 .so 확장자로 window는 DLL로 나타낸다.

공유 라이브러리는 메모리에 있는 .text 섹션을 통해 서로 다른 실행 중 프로세스들이 공유할 수 있다. 그러므로 프로세스마다 할당되는 메모리의 많은 공간을 아낄 수 있게 되는 것!

위 그림과 같이 첫 링킹은 실행 가능 파일이 생성될 때 정적으로 수행하고, 프로그램이 로드될 때 링킹 작업을 동적으로 완료하는 것이다.
첫 링킹 작업시 libvector.so라는 동적 라이브러리로부터 코드와 데이터 섹션 중 실제로 실행파일 prog21에 복사된 것은 아무것도 없다.
다만, 일부 재배치와 심볼 테이블 정보를 복사하고 libvector.so 내부 코드와 데이터에 대한 참조가 로드타임에 해결되도록 해준다.

아래 동적 링커는
1. libc.so의 텍스트와 데이터를 일부 메모리 세그먼트에 재배치한다.
2. 다른 메모리 세그먼트로 libvector.so의 텍스트와 데이터를 재배치한다.
3. libc.so와 libvector.so에서 정의된 심볼들로 prog21에서의 참조를 재배치한다.

이러한 과정을 거친 후 제어를 응용프로그램에 넘겨준다. 이 때 공유 라이브러리의 위치는 고정되고 프로그램 실행동안에는 바뀌지 않는다.

자, 여기까지도 로드타임시 동적 링킹이었다. 마지막 런타임시 동적 링킹은 도대체 어떤 놈일까?

MS사의 응용프로그램 개발자들은 sw의 업데이트를 배포하기 위해 공유 라이브러리를 사용한다. 해당 공유 라이브러리를 다운로드하면 나의 응용프로그램을 실행할 때, 자동으로 새 공유 라이브러리와 링크하고 로드하게 된다. wow

이를 가능하게 하는 것은 코드 일부분을 컴파일해서 링커에 의해 수정되지 않고도 메모리 어디든 로드되도록 하는 위치-독립성 코드(PIC)이다. 위치-독립성코드로 컴파일된 공유 라이브러리는 어느 곳에나 로드될 수 있고, 다수의 프로세스에 의해 런타임에 공유된다.


오늘의 나는

링킹에 대한 이해를 바탕으로 c언어로 rb-tree를 구현하는 중 일어나는 일들에 대한 이해의 폭이 넓어졌다. 처음 사용하는 c언어이기에 실행 가능한 목적 파일을 만들고, 실행시키는 과정조차 낯설었던 게 정말 엊그제인데.. 함께 공부한 팀원들과 그저 블로그를 보고 따라 하고 이해하는 것이 아닌 CSAPP를 통해 링커와 컴파일에 대한 조금 더 깊어진 이해를 바탕으로 왜 우리 파일이 실행되지 않고, 어떻게 테스트해 봐야 하는지를 나누는 것이 신기한 하루였다. 실력 있고 좋은 동기들로부터 많은 것을 배울 수 있었고 조금은 뿌듯하게 두 다리 쭉- 뻗고 자도 되는 하루인 거 같다!

feat. 노란 가을, 사장님의 당근 케이크 품평회

참고. CS:APP 7장 링커

0개의 댓글