책너두 (컴퓨터 밑바닥의 비밀) 3일차 링커
- Book/컴퓨터 밑바닥의 비밀
- 2024. 4. 15.
요약
링커는 컴파일러가 생성한 대상 파일 여러 개를 하나로 묶어 하나의 최종 실행 파일을 생성함.
링크의 전체 과정
- 대상 파일이 참조하고 있는
**외부 심벌(external symbol)**
에 대한 실제 구현이 어느 모듈이든지 단 하나만 있어야 하며, 링커는 이를 찾아내서 연결하는 작업을 하는데, 이를**심벌 해석(symbol resolution)**
이라고 함. - 대상 파일을 모아 하나로 합침.
- 대상 파일에서 다른 파일의 내용을 ‘참조’ 하고 싶지만, 아직 다른 파일의 내용이 작성되지 않은 경우, 임시로 그 값을 비워두었다가 그 파일의 작업이 마쳐지면 그때 쭉 다 바꿔준다.
- 이 과정을
**재배치(relocation)**
이라고 함.
- 이 과정을
- 대상 파일이 참조하고 있는
링크 과정 (심벌 해석)
- 심벌은 전역 변수와 함수의 이름을 포함하는 모든 변수 이름을 의미함.
- 지역 변수는 모듈 내에서만 사용되기에 외부 모듈이 참조할 수 없어서 링커의 관심 대상이 아님.
- 링커는 실제로 전역 변수에 관심을 가짐.
- 소스 파일에 다른 모듈에서 참조할 수 있는 심벌이 있다는 것과, 소스 파일이 다른 모듈에서 정의한 심벌을 참조한다는 정보를 알고 있어야 함.
- 이러한 정보는 컴파일러가 알려줌.
- 컴파일러는 기계 명령어 생성 뿐만 아니라, 명령어를 실행시키는 데이터도 생성함.
- 이 데이터는 대상 파일에 반드시 포함되어야 함.
- 대상 파일에는 두 영역이 포함되어 있음.
- 명령어 부분
- 소스 파일에 정의된 함수에서 변환된 기계 명령어가 저장되는 부분임.
코드 영역(code section)
이라고 함.
- 데이터 부분
- 소스 파일의 전역 변수가 저장되는 부분임.
데이터 영역(data section)
이라고 함.- 로컬 변수는 스택 영역에서 생성 및 제거 되므로 대상 파일에 별도로 저장되지 않음.
- 명령어 부분
- 소스 파일에 다른 모듈에서 참조할 수 있는 심벌이 있다는 것과, 소스 파일이 다른 모듈에서 정의한 심벌을 참조한다는 정보를 알고 있어야 함.
- 컴파일러는 외부에서 정의된 전역 변수나 함수를 발견하면 해당 변수의 ‘선언’을 보고 실제로 ‘정의’ 되었는지 여부는 신경 쓰지 않음.
- 이는 오로지 링커의 몫임.
- 대신 컴파일러는 소스 파일마다 외부에서 참조 가능한 심벌이 어떤 것인지 기록하고, 어떤 외부 심볼을 참조하고 있는지 기록함.
- 컴파일러는 이러한 정보를 기록하는
**심벌 테이블(symbol table)**
을 가지고 있음.- 공급 : 내가 정의 한 심벌
- 수요 : 내가 사용하는 외부 심벌
- 컴파일러는 심벌 테이블을 대상 파일에 저장함.
- 컴파일러는 이러한 정보를 기록하는
- 심벌은 전역 변수와 함수의 이름을 포함하는 모든 변수 이름을 의미함.
링크 과정 (실행 파일)
- 강력한 유틸리티 함수들이 구현되어있는 코드를 별도로 컴파일 하여 패키지로 묶고, 구현된 모든 함수의 선언을 포함하는 헤더 파일을 제공할 수 있음.
- 이를
정적 라이브러리(static library)
라고 함. - 정적 라이브러리를 활용하면 미리 컴파일이 되어 있기 때문에 다시 컴파일할 필요가 없어서 링크 과정에서 그대로 실행 파일에 복제만하면 되므로 컴파일 속도가 빨라짐.
- 이 과정을
정적 링크(static linking)
이라고 함.
- 이를
- 실행 파일도 ‘코드 영역’ 과 ‘데이터 영역’ 이 있어 대상 파일과 매우 유사해 보임.
- 하지만 실행 파일은 특수한 심벌인 _start 가 있음.
- CPU는 이 심벌 주소에서 프로그램을 실행하는 데 필요한 기계 명령어를 찾음. → main 함수를 실행하기 시작함.
- 하지만 실행 파일은 특수한 심벌인 _start 가 있음.
- 정적 링크는 실행 파일에 직접 복사하기 때문에 정적 링크로 생성된 실행 파일은 모두 동일한 코드와 데이터의 복사본을 가지게 됨.
- ex) 정적 라이브러리 크기가 2MB 인데, 이 라이브러리를 사용하는 실행 파일이 500개라면 1GB 크기의 데이터가 중복된 데이터로 구성됨.
- 또, 정적 라이브러리의 모든 내용에 종속성이 있다면 정적 라이브러리 코드가 변경될 때마다 정적 라이브러리에 종속된 프로그램 역시 매번 다시 컴파일 해야 함.
동적 라이브러리는(dynamic library)
이 문제를 해결함.공유 라이브러리(shared library)
, 또는**동적 링크 라이브러리(dynamic linked library)**
라고도 함.- 동적 라이브러리도 코드 영역, 데이터 영역이 포함되어 있음.
- 단지, 동적 라이브러리의 사용 방식, 사용 시간이 정적 라이브러리와 다름.
- 정적 라이브러리는 코드 영역와 데이터 영역을 모두 한데 묶어서 실행 파일에 복사 하지만, 동적 라이브러리는 참조된 동적 라이브러리 이름, 심벌 테이블, 재배치 정보 등 필수 정보만 실행 파일에 포함함.
- 동적 링크는 의존 관계를 실제 프로그램 실행 지점까지 미룸.
- 동적 라이브러리를 이용하면 실행 파일의 크기를 확실히 줄일 수 있음.
- 정적 라이브러리는 코드 영역와 데이터 영역을 모두 한데 묶어서 실행 파일에 복사 하지만, 동적 라이브러리는 참조된 동적 라이브러리 이름, 심벌 테이블, 재배치 정보 등 필수 정보만 실행 파일에 포함함.
- 참조된 동적 라이브러리 필수 정보는 실행 파일 내 저장됨.
- ‘동적 링크 관련 정보’ 로서 저장됨.
- 이 필수 정보는 ‘동적 링크(dynamic linking)’ 가 일어날 때 사용됨.
- 동적 링크 방식 1. (프로그램이 메모리에 적재 될 때 동적 링크 진행)
- 적재 과정에서 적재 도구(loader) 전용 프로세스가 실행됨.
- 적재 도구는 실행 파일이 동적 라이브러리 의존 여부를 확인할 수 있음.
- 동적 라이브러리가 필요하다면 동적 링커라는 별도의 프로세스가 실행되고, 참조하는 동적 라이브러리 존재 여부와 위치, 심벌의 메모리 위치 등을 확인하여 링크 과정을 마무리 함.
- 동적 링크 방식 2.
- 적재 중 고정적으로 일어나는 동적 링크 외에도 프로그램이 먼저 실행된 후, 프로그램의 실행 시간(runtime) 동안 코드가 직접 동적 링크를 실행할 수 있음.
- 실행 시간 동적 링크(runtime dynamic linking)는 실행 파일이 실행될 때까지 어떤 동적 라이브러리에 의존하는지 알 필요가 없기에 좀 더 동적인 링크 방식임.
- 실행 파일 생성 과정에서 실행 파일 내부에 동적 라이브러리 정보가 없음.
- 대신, 프로그래머가 코드에 특정 API를 사용하여 필요할 때마다 동적 라이브러리를 직접 동적으로 적재할 수 있음.
- 동적 링크 방식 1. (프로그램이 메모리에 적재 될 때 동적 링크 진행)
- 동적 라이브러리의 장단점
- 장점
- 동적 라이브러리를 사용하면 의존하는 프로그램 개수가 얼마이든 상관없이 디스크에 동적 라이브러리 복사본 하나만 저장됨.
- 메모리 적재되는 동적 라이브러리 코드도 역시 모든 프로세스가 하나의 코드를 공유함. → 하나의 코드로 공유 가능 (공유 라이브러리 라고도 함.)
- 동적 라이브러리 코드가 수정된다고 하더라도 전체를 다 컴파일 할 필요 없고 동적 라이브러리만 다시 컴파일하면 됨.
- 동적 링크는 ‘프로그램 실행 시간’에 일어 나므로 프로그램 기능을 쉽게 확장할 수 있음.
- 플러그 인을 사용함.
- 여러 언어를 혼합하여 개발할 때 매우 유용함.
- ex) 파이썬을 메인으로 개발하되, 더 높은 성능이 요구되는 부분은 C/C++ 로 작성된 동적 라이브러리를 사용한다.
- 동적 라이브러리를 사용하면 의존하는 프로그램 개수가 얼마이든 상관없이 디스크에 동적 라이브러리 복사본 하나만 저장됨.
- 단점
- 프로그램이 적재되는 시간, 실행 시간에 링크되기 때문에 정적 링크 사용할 때보다 성능이 약간 떨어짐.
- 동적 라이브러리의 코드는 특정 메모리 주소와 독립적으로 동작하기 때문에 위치 독립 코드(position-independent code) 라고 불림.
- 동적 라이브러리는 메모리에 단 하나의 복사본만 존재함.
- 해당 코드는 여러 프로세스가 공유할 수 있으므로 동적 라이브러리의 코드는 임의의 메모리 절대 주소를 참조할 수 없음.
- 적재할 때 동적 링크를 수행하는 프로그램은 실행 파일만으로 실행이 불가능 함.
- 종속된 동적 라이브러리를 제공하지 않거나 그 버전이 호환되지 않을 경우 프로그램이 실행되지 않음.
- 장점
- 강력한 유틸리티 함수들이 구현되어있는 코드를 별도로 컴파일 하여 패키지로 묶고, 구현된 모든 함수의 선언을 포함하는 헤더 파일을 제공할 수 있음.
링크 과정 (재배치)
- ex) foo 함수 호출하려고 할 때 그에 대응하는 기계 명령어를
call 0x4004d6
이라고 하자. - 컴파일러가 대상 파일을 생성할 때, foo 함수가 어느 메모리 주소에 적재될지 알 수 없음.
- 따라서 처음에는 0x00 으로 지정하여 단순히 호출한다는 사실만 기록함.
- 링커가 이 값을 채워넣어야 함.
- 링커가 최종적으로 실행되는 시점의 메모리 주소로 변경해야 한다는 것을 어떻게 아는가?
- 컴파일러는 메모리 주소를 확정할 수 없는 변수를 발견할 때마다 .relo.text 에 해당 명령어를 저장함.
- .relo.data 에는 해당 명령어와 관련된 데이터를 저장함.
- 명령어를 통해 프로그램 ‘실행’ 시점에 링커가 메모리 주소를 명확히 찾을 수 있음. → 재배치 과정
- 그런데 링커는 프로그램이 실행된 후의 변수, 기계 명령어 메모리 주소를 어떻게 알 수 있는 건가?
- 프로그램이 실행될 때마다 메모리 주소는 바뀌는데?
- 가상 메모리(virtual memory) 가 이 질문의 답을 해줌.
- ex) foo 함수 호출하려고 할 때 그에 대응하는 기계 명령어를
가상 메모리와 프로그램 메모리 구조
- 프로그램이 실행된 후 메모리 상태는
- 코드 영역, 데이터 영역, 힙 영역, 스택 영역, 커널 로 구성됨.
- 근데, 코드 영역이 어떤 프로그램이든 0x400000 부터 시작됨.
- 프로그램 A를 실행할 때 메모리 주소 0x400000 에서 가져온 값과 프로그램 B를 실행할 때 메모리 주소 0x400000 에서 가져온 값은 다름.
- 이것이 운영체제의 가상 메모리 기술임.
- 모든 프로세스의 가상 메모리는 표준화되어 있고 크기가 동일함.
- 프로세스마다 각 영역의 크기가 다를 수는 있지만 영역이 배치되는 순서는 동일함.
- 실제 물리 메모리의 크기는 가상 메모리의 크기와 무관하며, 물리 메모리에는 힙, 스택 영역 등, 영역 구분 조차 존재하지 않음.
- 모든 프로세스는 자신만의 페이지 테이블을 가지고 있음.
- 같은 가상 메모리 주소라도 페이지 테이블을 확인하여 서로 다른 물리 메모리 주소를 획들 할 수 있음.
- 프로그램이 실행된 후 메모리 상태는
컴퓨터 과학에서 추상화가 중요한 이유
- 소프트웨어는 복잡하지만 프로그래머는 추상화를 통해 복잡도를 제어할 수 있음.
- 컴퓨터 시스템은 기본적으로 추상화 기반 위에 구축됨.
- ex) 입출력 장치는 파일로 추상화 되어 있음.
- ex) 실행중인 프로그램은 프로세스로 추상화 됨.
- 추상화는 프로그래머를 저수준 계층에서 더 멀어지게 만들고, 더 저수준 계층의 세부 사항도 신경 쓸 필요가 없도록 만듦.
- 하지만, 추상화 계층을 내가 만들고 싶다면 가장 아래 위치한 저수준 계층을 이해할 필요가 있음.
'Book > 컴퓨터 밑바닥의 비밀' 카테고리의 다른 글
책너두 (컴퓨터 밑바닥의 비밀) 6일차 스레드 안전 코드 (0) | 2024.04.18 |
---|---|
책너두 (컴퓨터 밑바닥의 비밀) 5일차 스레드 간 공유되는 프로세스 리소스 (0) | 2024.04.17 |
책너두 (컴퓨터 밑바닥의 비밀) 4일차 운영 체제, 프로세스, 스레드 (0) | 2024.04.16 |
책너두 (컴퓨터 밑바닥의 비밀) 2일차 컴파일러 (0) | 2024.04.11 |
책너두 (컴퓨터 밑바닥의 비밀) 1일차 : 프로그래밍 언어 (0) | 2024.04.08 |