책너두 (컴퓨터 밑바닥의 비밀) 3일차 링커

요약

  • 링커는 컴파일러가 생성한 대상 파일 여러 개를 하나로 묶어 하나의 최종 실행 파일을 생성함.

  • 링크의 전체 과정

    1. 대상 파일이 참조하고 있는 **외부 심벌(external symbol)**에 대한 실제 구현이 어느 모듈이든지 단 하나만 있어야 하며, 링커는 이를 찾아내서 연결하는 작업을 하는데, 이를 **심벌 해석(symbol resolution)** 이라고 함.
    2. 대상 파일을 모아 하나로 합침.
    3. 대상 파일에서 다른 파일의 내용을 ‘참조’ 하고 싶지만, 아직 다른 파일의 내용이 작성되지 않은 경우, 임시로 그 값을 비워두었다가 그 파일의 작업이 마쳐지면 그때 쭉 다 바꿔준다.
      • 이 과정을 **재배치(relocation)** 이라고 함.
  • 링크 과정 (심벌 해석)

    • 심벌은 전역 변수와 함수의 이름을 포함하는 모든 변수 이름을 의미함.
      • 지역 변수는 모듈 내에서만 사용되기에 외부 모듈이 참조할 수 없어서 링커의 관심 대상이 아님.
    • 링커는 실제로 전역 변수에 관심을 가짐.
      • 소스 파일에 다른 모듈에서 참조할 수 있는 심벌이 있다는 것과, 소스 파일이 다른 모듈에서 정의한 심벌을 참조한다는 정보를 알고 있어야 함.
        • 이러한 정보는 컴파일러가 알려줌.
        • 컴파일러는 기계 명령어 생성 뿐만 아니라, 명령어를 실행시키는 데이터도 생성함.
          • 이 데이터는 대상 파일에 반드시 포함되어야 함.
          • 대상 파일에는 두 영역이 포함되어 있음.
            • 명령어 부분
              • 소스 파일에 정의된 함수에서 변환된 기계 명령어가 저장되는 부분임.
              • 코드 영역(code section) 이라고 함.
            • 데이터 부분
              • 소스 파일의 전역 변수가 저장되는 부분임.
              • 데이터 영역(data section) 이라고 함.
                • 로컬 변수는 스택 영역에서 생성 및 제거 되므로 대상 파일에 별도로 저장되지 않음.
    • 컴파일러는 외부에서 정의된 전역 변수나 함수를 발견하면 해당 변수의 ‘선언’을 보고 실제로 ‘정의’ 되었는지 여부는 신경 쓰지 않음.
      • 이는 오로지 링커의 몫임.
      • 대신 컴파일러는 소스 파일마다 외부에서 참조 가능한 심벌이 어떤 것인지 기록하고, 어떤 외부 심볼을 참조하고 있는지 기록함.
        • 컴파일러는 이러한 정보를 기록하는 **심벌 테이블(symbol table)** 을 가지고 있음.
          • 공급 : 내가 정의 한 심벌
          • 수요 : 내가 사용하는 외부 심벌
        • 컴파일러는 심벌 테이블을 대상 파일에 저장함.
  • 링크 과정 (실행 파일)

    • 강력한 유틸리티 함수들이 구현되어있는 코드를 별도로 컴파일 하여 패키지로 묶고, 구현된 모든 함수의 선언을 포함하는 헤더 파일을 제공할 수 있음.
      • 이를 정적 라이브러리(static library) 라고 함.
      • 정적 라이브러리를 활용하면 미리 컴파일이 되어 있기 때문에 다시 컴파일할 필요가 없어서 링크 과정에서 그대로 실행 파일에 복제만하면 되므로 컴파일 속도가 빨라짐.
      • 이 과정을 정적 링크(static linking) 이라고 함.
    • 실행 파일도 ‘코드 영역’ 과 ‘데이터 영역’ 이 있어 대상 파일과 매우 유사해 보임.
      • 하지만 실행 파일은 특수한 심벌인 _start 가 있음.
        • CPU는 이 심벌 주소에서 프로그램을 실행하는 데 필요한 기계 명령어를 찾음. → main 함수를 실행하기 시작함.
    • 정적 링크는 실행 파일에 직접 복사하기 때문에 정적 링크로 생성된 실행 파일은 모두 동일한 코드와 데이터의 복사본을 가지게 됨.
      • ex) 정적 라이브러리 크기가 2MB 인데, 이 라이브러리를 사용하는 실행 파일이 500개라면 1GB 크기의 데이터가 중복된 데이터로 구성됨.
    • 또, 정적 라이브러리의 모든 내용에 종속성이 있다면 정적 라이브러리 코드가 변경될 때마다 정적 라이브러리에 종속된 프로그램 역시 매번 다시 컴파일 해야 함.
    • 동적 라이브러리는(dynamic library) 이 문제를 해결함.
      • 공유 라이브러리(shared library), 또는 **동적 링크 라이브러리(dynamic linked library)** 라고도 함.
      • 동적 라이브러리도 코드 영역, 데이터 영역이 포함되어 있음.
      • 단지, 동적 라이브러리의 사용 방식, 사용 시간이 정적 라이브러리와 다름.
        • 정적 라이브러리는 코드 영역와 데이터 영역을 모두 한데 묶어서 실행 파일에 복사 하지만, 동적 라이브러리는 참조된 동적 라이브러리 이름, 심벌 테이블, 재배치 정보 등 필수 정보만 실행 파일에 포함함.
          • 동적 링크는 의존 관계를 실제 프로그램 실행 지점까지 미룸.
        • 동적 라이브러리를 이용하면 실행 파일의 크기를 확실히 줄일 수 있음.
      • 참조된 동적 라이브러리 필수 정보는 실행 파일 내 저장됨.
        • ‘동적 링크 관련 정보’ 로서 저장됨.
        • 이 필수 정보는 ‘동적 링크(dynamic linking)’ 가 일어날 때 사용됨.
          • 동적 링크 방식 1. (프로그램이 메모리에 적재 될 때 동적 링크 진행)
            • 적재 과정에서 적재 도구(loader) 전용 프로세스가 실행됨.
            • 적재 도구는 실행 파일이 동적 라이브러리 의존 여부를 확인할 수 있음.
              • 동적 라이브러리가 필요하다면 동적 링커라는 별도의 프로세스가 실행되고, 참조하는 동적 라이브러리 존재 여부와 위치, 심벌의 메모리 위치 등을 확인하여 링크 과정을 마무리 함.
          • 동적 링크 방식 2.
            • 적재 중 고정적으로 일어나는 동적 링크 외에도 프로그램이 먼저 실행된 후, 프로그램의 실행 시간(runtime) 동안 코드가 직접 동적 링크를 실행할 수 있음.
            • 실행 시간 동적 링크(runtime dynamic linking)는 실행 파일이 실행될 때까지 어떤 동적 라이브러리에 의존하는지 알 필요가 없기에 좀 더 동적인 링크 방식임.
              • 실행 파일 생성 과정에서 실행 파일 내부에 동적 라이브러리 정보가 없음.
              • 대신, 프로그래머가 코드에 특정 API를 사용하여 필요할 때마다 동적 라이브러리를 직접 동적으로 적재할 수 있음.
    • 동적 라이브러리의 장단점
      • 장점
        • 동적 라이브러리를 사용하면 의존하는 프로그램 개수가 얼마이든 상관없이 디스크에 동적 라이브러리 복사본 하나만 저장됨.
          • 메모리 적재되는 동적 라이브러리 코드도 역시 모든 프로세스가 하나의 코드를 공유함. → 하나의 코드로 공유 가능 (공유 라이브러리 라고도 함.)
        • 동적 라이브러리 코드가 수정된다고 하더라도 전체를 다 컴파일 할 필요 없고 동적 라이브러리만 다시 컴파일하면 됨.
        • 동적 링크는 ‘프로그램 실행 시간’에 일어 나므로 프로그램 기능을 쉽게 확장할 수 있음.
          • 플러그 인을 사용함.
        • 여러 언어를 혼합하여 개발할 때 매우 유용함.
          • ex) 파이썬을 메인으로 개발하되, 더 높은 성능이 요구되는 부분은 C/C++ 로 작성된 동적 라이브러리를 사용한다.
      • 단점
        • 프로그램이 적재되는 시간, 실행 시간에 링크되기 때문에 정적 링크 사용할 때보다 성능이 약간 떨어짐.
        • 동적 라이브러리의 코드는 특정 메모리 주소와 독립적으로 동작하기 때문에 위치 독립 코드(position-independent code) 라고 불림.
          • 동적 라이브러리는 메모리에 단 하나의 복사본만 존재함.
          • 해당 코드는 여러 프로세스가 공유할 수 있으므로 동적 라이브러리의 코드는 임의의 메모리 절대 주소를 참조할 수 없음.
        • 적재할 때 동적 링크를 수행하는 프로그램은 실행 파일만으로 실행이 불가능 함.
          • 종속된 동적 라이브러리를 제공하지 않거나 그 버전이 호환되지 않을 경우 프로그램이 실행되지 않음.
  • 링크 과정 (재배치)

    • ex) foo 함수 호출하려고 할 때 그에 대응하는 기계 명령어를 call 0x4004d6 이라고 하자.
    • 컴파일러가 대상 파일을 생성할 때, foo 함수가 어느 메모리 주소에 적재될지 알 수 없음.
      • 따라서 처음에는 0x00 으로 지정하여 단순히 호출한다는 사실만 기록함.
      • 링커가 이 값을 채워넣어야 함.
    • 링커가 최종적으로 실행되는 시점의 메모리 주소로 변경해야 한다는 것을 어떻게 아는가?
      • 컴파일러는 메모리 주소를 확정할 수 없는 변수를 발견할 때마다 .relo.text 에 해당 명령어를 저장함.
      • .relo.data 에는 해당 명령어와 관련된 데이터를 저장함.
      • 명령어를 통해 프로그램 ‘실행’ 시점에 링커가 메모리 주소를 명확히 찾을 수 있음. → 재배치 과정
    • 그런데 링커는 프로그램이 실행된 후의 변수, 기계 명령어 메모리 주소를 어떻게 알 수 있는 건가?
      • 프로그램이 실행될 때마다 메모리 주소는 바뀌는데?
      • 가상 메모리(virtual memory) 가 이 질문의 답을 해줌.
  • 가상 메모리와 프로그램 메모리 구조

    • 프로그램이 실행된 후 메모리 상태는
      • 코드 영역, 데이터 영역, 힙 영역, 스택 영역, 커널 로 구성됨.
      • 근데, 코드 영역이 어떤 프로그램이든 0x400000 부터 시작됨.
      • 프로그램 A를 실행할 때 메모리 주소 0x400000 에서 가져온 값과 프로그램 B를 실행할 때 메모리 주소 0x400000 에서 가져온 값은 다름.
        • 이것이 운영체제의 가상 메모리 기술임.
    • 모든 프로세스의 가상 메모리는 표준화되어 있고 크기가 동일함.
      • 프로세스마다 각 영역의 크기가 다를 수는 있지만 영역이 배치되는 순서는 동일함.
    • 실제 물리 메모리의 크기는 가상 메모리의 크기와 무관하며, 물리 메모리에는 힙, 스택 영역 등, 영역 구분 조차 존재하지 않음.
    • 모든 프로세스는 자신만의 페이지 테이블을 가지고 있음.
      • 같은 가상 메모리 주소라도 페이지 테이블을 확인하여 서로 다른 물리 메모리 주소를 획들 할 수 있음.
  • 컴퓨터 과학에서 추상화가 중요한 이유

    • 소프트웨어는 복잡하지만 프로그래머는 추상화를 통해 복잡도를 제어할 수 있음.
    • 컴퓨터 시스템은 기본적으로 추상화 기반 위에 구축됨.
      • ex) 입출력 장치는 파일로 추상화 되어 있음.
      • ex) 실행중인 프로그램은 프로세스로 추상화 됨.
    • 추상화는 프로그래머를 저수준 계층에서 더 멀어지게 만들고, 더 저수준 계층의 세부 사항도 신경 쓸 필요가 없도록 만듦.
    • 하지만, 추상화 계층을 내가 만들고 싶다면 가장 아래 위치한 저수준 계층을 이해할 필요가 있음.

댓글

Designed by JB FACTORY