엔지니어 블로그

[컴퓨터 밑바닥의 비밀] 링커 본문

카테고리 없음

[컴퓨터 밑바닥의 비밀] 링커

안기용 2025. 3. 10. 11:14

링커의 말할 수 없는 비밀

본 장은 링커의 존재 이유와 작동 방법에 대해 설명하고 있다. 예를 들어 외부에 정의 된 함수는 정적,동적 라이브러리로 제공 되는데 이 것을 어떻게 가져다 사용할지에 대한 내용이다.

  1. 링커는 이렇게 일 한다.
    링커는 컴파일러와 마찬가지로 일반적인 프로그램이다. 컴파일러가 생성한 다수의 대상파일을 묶어 하나의 실행 파일을 생성한다. 링커가 실행파일을 만드는 과정을 다음과 같다.
    심벌해석 -> 대상파일 묶기 -> 재배치  
  • 심벌해석
    심벌 해석은 참조하고 있는 모듈 간 종속성이 올바르게 설정 되어 있는지 확인한다. 또 외부 심벌에 대한 구현이 어느 모듈이던 하나만 있는지 확인하고 이를 연결하는 역할을 한다.
  • 묶기
    심벌해석이 끝난 대상 파일을 하나로 묶는다.
  • 재배치
    compile 시점에 컴파일러는 참조하고 있는 외부 심벌의 메모리 주소를 알 수 없다. 그래서 N 과 같은 미지수로 남겨둔 후 작업을 마친다. 링커는 외부 심벌의 메모리 주소를 알고 있기 때문에 컴파일러가 남긴 미지수 N을 실제 값으로 채워넣는다. 이 과정을 재배치라고한다.
  1. 심벌 해석
    심벌(Symbol)은 전역변수와 함수의 이름을 포함한 모든 변수의 이름을 뜻한다. 지역변수는 모듈 내에서만 사용하는 탓에 링커의 관심 대상 밖의 정보다.
    심벌 해석 단계에서 링커가 할 일은 대상파일에서 참조하고있는 각각의 모든 외부심벌마다 정의가 되어있는지, 하나만 정의되어 있는지 확인하는 일이다. 확인하기 위한 정보는 컴파일러가 제공해준다. 컴파일하면서 대상파일에 명령어 실행에 필요한 데이터를 포함하게 된다.


컴파일러는 컴파일 과정에서 외부에서 정의 된 변수나 함수를 발견하면 그냥 넘어가고 링커에게 이 일을 떠넘긴다. 참조된 변수를 찾는 일은 링커의 일이다. 그렇지만 컴파일러는 링커에게 참조된 변수를 찾기 위한 단서를 제공한다. 바로 '심벌 테이블'이다.
심벌 테이블이란 외부에서 참조 가능한 심벌이 어떤 것이 있는지, 외부에서 참조 하고 있는 심벌은 어떤 것이 있는지 파악 후 기록한 내용이 저장되어 있다. 컴파일러는 심벌테이블을 생성한 후 대상파일에 저장하게 된다.

  1. 정적/동적 라이브러리 와 링크
    정적 라이브러리 / 링크
    정적 링크는 외부 코드를 매번 컴파일 하지 않아도 되기 때문에 컴파일 속도가 빠르다는 장점이 있다. 소스파일 여러개를 미리 개별적으로 컴파일 하고 링크하여 정적 라이브러리를 생성할 수 있다. 이후 실행파일 생성할때는 자신의 코드만 컴파일 하며, 미리 컴파일이 완료 된 정적 라이브러리는 링크과정에서 그대로 실행 파일에 저장된다.
    정적 링크를 사용하게 되면 다음과 같은 단점이 존재한다.
    1. 용량 낭비
      모든 실행파일에 매번 정적 라이브러리 내용이 포함되게 된다. 예를 들어 500개의 실행파일이 2Mb의 정적 라이브러리와 종속성이 있다고 한다면 500개 파일 모두에 2Mb 사이즈의 라이브러리가 무조건 포함되어야한다. 즉 1Gb의 용량이 더 들어가게 된다.
    2. 라이브러리 수정 얼움
      정적 링크는 정적 라이브러리를 사용한다. 즉 미리 컴파일 된 라이브러리를 사용하게 되는데, 라이브러리의 내용이 변경된다면 종속 된 모든 프로그램을 다시 컴파일 해야한다.

이러한 단점을 해결하고자 동적 링크를 사용한다.

동적 라이브러리 / 링크

동적 라이브러리는 실행 파일에 라이브러리 내용 전체를 저장하지 않는다. 심벌 테이블, 라이브러리 이름 등 필수 정보만 저장하여 저장 효율성을 높힌다. 저장 된 필수 정보를 바탕으로 동적 링크를 진행하게 된다.
동적 링크는 컴파일 단계가 아닌, 실제 프로그램의 실행 시점까지 미룬다. 동적 링크에는 2가지 방법이 존재한다.
1. 메모리 적재 시점
실행 파일이 메모리에 적재되는 시점에 동적 링크를 진행하는 것이다. 실행파일이 디스크에서 메모리로 옮겨지는 과정에서 loader라는 프로세스가 실행된다. loader는 실행파일이 동적 라이브러리에 의존하는지 여부를 확인하고 필요하다면 동적 링커라는 프로세스가 실행되어 동적 링크과정을 마무리한다.
2. 프로그램 실행 시점
프로그램이 먼저 실행되고 run time에 동적 링크가 진행된다. 실행시간 동적링크는 실행파일이 실행될 때 까지 어떤 라이브러리에 의존하는지 알 필요가 없기 때문에 좀 더 동적인 링크 방식이다. 실행파일 내부에 동적 라이브러리 정보가 저장되지 않고, 프로그래머가 코드상에서 API를 활용하여 필요할때마다 동적 라이브러리를 직접 적재하게 된다.

  1. 재배치
    컴파일러는 컴파일 하면서 메모리 주소를 확정할 수 없는 변수를 발견할 때 마다 .relo.txt에 명령어를, .relo.data에 명령어 관련 데이터를 저장하여 링커에게 단서를 제공한다.
    예를들어 foo 함수의 경우 컴파일러는 call 명령어를 생성하면서 .relo.txt에 다음 메시지를 기록한다. '코드 영역의 시작 주소 기준 오프셋 60바이트인 위치에서 foo 심벌을 발견했지만 실행시 어떤 메모리 주소에 서 실행해야할지 알수 없다. 링커가 실행파일 생성시 명령어를 수정해야한다.
    심벌 해석이 끝나 대상파일에서 각 유형의 영역이 모두 결합되면 모든 기계 명령어와 전역변수가 프로그램 실행 시간에 위치할 메모리 주소를 결정할 수 있게 된다. 이때 예시에 있는 foo 함수의 실행 메모리 주소도 알 수 있게 된다.
    링커는 .relo.txt 를 읽어 나가면서 수정이 필요한 foo 명령어가 있으며 코드 영역 시작 주소 기준 오프셋이 60바이트 라는 것을 확인하다. 이 정보를 토대로 실행파일에서 해당 call 명령어를 찾고 임시로 0x00으로 기입된 메모리 주소를 이전에 확인한 메모리주소로 대체한다.
    프로그램이 실행된 후 변수나 기계명령어의 메모리 주소를 확인할 수 있는 이유는 가상메모리 덕분이다.
  2. 가상메모리
    가상메모리는 물리적으로 존재하지 않는 가짜 메모리다. 가상 메모리는 각각의 프로그램이 실행 중일 때, 자기 자신이 모든 메모리를 독점하고 있는 것 처럼 착각하게 만든다. 예시로 32비트 시스템에서는 시스템에 설치된 물리적 메모리가 얼마든지 2^32 바이트 즉 4Gb 메모리를 독점한다고 착각한다.
    하지만 결국 데이터와 명령어는 물리 메모리에 적재되어야 한다. 그렇다면 CPU가 프로그램 A를 실행하여 메모리 0x400000에 접근할 때 실제 명령어를 꺼내는 물리 메모리 주소를 어떻게 찾을까?
    페이지 테이블이 가능하게 한다. 페이지 테이블 가상 메모리와 물리메모리의 매핑 관계를 저장하고 있다.

본 내용은 컴퓨터 밑바닥의 비밀을 공부하고 요약 한 내용입니다.