실행 파일을 생성하는 링커
CS

실행 파일을 생성하는 링커

반응형

2024.07.27 - [CS] - 소스코드파일부터 실행파일까지 컴파일러 과정 알아보기

 

소스코드파일부터 실행파일까지 컴파일러 과정 알아보기

여러분들은 개발을 진행하면서 단순히 소스코드를 작성하게 되고, 개발한 소스코드를 토대로 실행파일이 생성이 됩니다.어떻게 우리가 작성한 소스코드가 실행파일이 되는지 알아가보겠습니

jhost.tistory.com

대상 파일(object file) 생성 과정에 대해서 궁금하신 분은 이전에 작성한 글을 참고해 주시면 됩니다.

 

링커

여러분들이 특정 프로그램을 실행할때 해당 프로그램을 실행하기 위한 실행 파일은 하나죠?

(물론 그 하나의 실행 파일을 실행 시키기 위해 부가적인 파일이 필요하기도 합니다.)

 

이전 글에서 말씀드린 컴파일러를 통해서 대상 파일(object file)이 생성된다는 걸 알았습니다.

컴파일러가 생성한 이 대상파일 파일을 여러개를 하나로 묶어서 최종 실행 파일을 생성해야 됩니다.

실행 파일을 생성하는 과정인 링커(linker)에 대해서 알아보겠습니다.

 

소스 코드 (func.c) -> 컴파일 (func.o)

func.o라는 이름의 기계명령어에 해당하는 코드를 저장하는 파일이 생성되고, 이 파일을 대상 파일(object file)이라고 합니다.

 

간단한 응용 프로그램부터 웹 브라우저나 웹 서버 같은 복잡한 응용프로그램들은 윈도에서 흔히 보이는 exe 형식의 실행 파일이나 리눅스의 elf 파일 같은 실행 파일은 링커가 필요한 대상 파일을 한데 모아 구성됩니다.

 

링커 작업 과정

링크의 전체 과정은 저자 여러명이 각각 특정 부분을 맡아 챕터별로 따로 집필하고, 개별 장을 묶어 책 한 권으로 출판하는 것과 비슷합니다.

 

1. 심벌 해석(symbol resolution)

심벌은 전역 변수와 함수의 이름을 포함하는 모든 변수 이름을 의미 합니다.

 

책의 특정 장이 다른 장의 내용을 참고할 때가 있는데, 이것은 우리가 작성하는 프로그램이 다른 모듈의 프로그래밍 인터페이스(programming interface) 또는 변수(variable)를 참조하는 것과 같습니다.

 

예를 들어서 list.c에서 일종의 연결 리스트(linked list)를 구현하고 다른 모듈에서 그 연결 리스트를 사용해야 한다면 이때 이 두 모듈 사이에 의존성이 있다고 말하죠.

 

즉, 링커가 하는 일 중 하나는 의존성(종속성)이 올바르게 설정되어 있는지, 다시 말해 인터페이스 구현이 의존성이 있는 모듈에서 사용 가능한지 확인하는 것입니다.

책 한 권이 오류 없이 완성되려면 서로 참고한 내용이 실제로 그 책 안에 정리가 되어 있어야겠죠?

 

실제 참조하고 있는 외부 심벌(external symbol)에 대한 실제 구현이 어느 모듈이든지 단 하나만 있어야 하는데, 링커는 이를 찾아내 연결하는 작업을 합니다.

 

2. 재배치(relocation)

책에서 어떤 하나의 장에서 다른 장의 내용을 인용할 때 몇 페이지의 내용이라고 언급해야 하는데, 각 저자가 집필할 때는 어느 페이지에 인용한 내용이 들어가는지 미리 알 수 없기 때문에 (책이 아직 완성된 상태가 아니기 때문에) 임시로 N 쪽이라고 표시하고 이후 책이 최종적으로 편집 작업에 들어가야 N 쪽이 몇 쪽인지 알 수 있습니다.

 

CPU에 대한 자세한 설명은 N 쪽을 참고하세요. -> 재배치 -> CPU에 대한 자세한 설명은 150쪽을 참고하세요. 

 

이 과정에서 N 쪽이라고 표시된 부분을 전부 150쪽이라고 치환하는 과정을 재배치라고 합니다.

 

실제 코드에서도 위처럼 재배치가 일어납니다.

특정한 소스 파일에서 다른 모듈에 정의되어 있는 함수를 참조할 때, 컴파일러가 이 소스 파일을 컴파일하는 시점에서는 이 함수가 어느 메모리 주소(memory address)에 위치할지 정확히 알 수 없기 때문에 컴파일러는 이 함수를 N으로 표시해 두고 일단 넘어가게 됩니다.

이후 링크 과정에서 링커가 N으로 표시되어 있는 곳들을 확인하고 모아 실행 파일을 생성하는 과정에서 함수의 정확한 주소를 확인하고 실제 메모리 주소로 대체합니다.

 

각 저자가 자신 분량을 모두 집필하고 나면 이를 모아 하나로 합쳐야 책 한 권이 완성될 수 있는데, 이렇게 완성된 책 한 권은 링크 과정을 마친 후 최종적으로 생성된 실행 파일에 비유할 수 있습니다.


 

지역 변수는 모듈 내에서만 사용되어 외부 모듈에서 참조할 수 없기 때문에 링커의 관심 대상이 아니죠.

즉, 링커가 실제로 관심을 갖는 것은 전역 변수입니다. 

 

1. 소스 파일에 다른 모듈에서 참조할 수 있는 심벌

2. 소스 파일이 다른 모듈에서 정의한 심벌 두 개를 참조

 

그렇다면 링커는 위 두 가지 정보를 어떻게 알 수 있을까요?

-> 컴파일러를 통해서 알 수 있습니다.

 

컴파일러는 소스 코드를 기계 명령어로 번역하고 그 정보를 대상 파일에 저장하는데, 기계 명령어 생성할 뿐만 아니라 이 명령어를 실행시키는 데이터도 생성합니다. 이 데이터는 대상 파일에 반드시 포함이 되어 있습니다.

 

생성된 대상 파일에는 코드영역, 데이터 영역이 포함됩니다.

코드 영역 : 소스 파일에 정의된 함수에서 변환된 기계 명령어가 저장되는 부분

데이터 영역 : 소스 파일의 전역 변수가 저장되는 부분 

(지역 변수는 프로그램이 실행된 후 스택 영역에서 생성되고 사용하면 제거되기 때문에 대상 파일에는 별도로 저장되지 않음)

 

컴파일러가 링커 부담을 조금이나마 줄여주고자 추가적으로 하는 작어비 있습니다.

소스 파일마다 외부에서 참조 가능한 심벌이 어떤 것인지 그 정보를 기록하고, 반대로 어떤 외부 심벌을 참조하고 있는지 기록하여 링커에게 전달해 줍니다.

외부 심벌 정보를 기록하는 표를 심벌 테이블이라고 합니다.

 

이렇게 대상 파일에 코드 영역, 데이터 영역, 심벌 테이블이 저장됩니다.

링커는 컴파일러를 통해 필요한 정보를 얻고, 공급과 수요를 충족하는지 확인합니다.

실제 코드를 작성할 때는 공급이 수요를 초과할 수는 있지만 반대로 수요가 공급을 초과하는 상황이 발생해서는 안됩니다.

-> 실제로 사용하지 않는 함수를 정의할 수는 있지만 존재하지 않는 함수를 사용할 수는 없음

 

재배치 과정에 대해서 자세히 알아보면, 재배치는 실행 시 주소를 정확한 주소지를 결정해줘야 합니다.

링커는 실행 파일을 생성할 때 프로그램이 실행되는 시점에 함수가 적재될 메모리 주소를 확정해야 하는데 이 메모리 주소는 어떻게 알 수 있을까요? 

 

컴파일러가 메모리 주소를 확정할 수 없는 변수를 발견할 때마다 . relo.text, . relo.data 파일을 생성합니다.

.relo.text : 해당 명령어를 저장

.relo.data : 해당 명령어와 관련된 데이터를 저장

최종 대상 파일

 

 대상 파일에서 각 유형의 영역이 모두 결합되면 모든 기계 명령어와 전역 변수가 프로그램 실행 시간에 위치할 메모리 주소를 결정할 수 있습니다. 링커는 각 대상 파일의. relo.text 영역을 읽어 명령어를 확인하고, 심벌의 코드 영역 시작 주소 값을 확인합니다.

최초 주소는 0x00으로 표시되고,. relo.text,. relo.data를 읽어 들여 심벌의 코드 영역 시작 주소 기준 오프셋을 확인하고 실행 파일에서 해당 명령어를 찾아 이동할 소스 주소를 0x00에서 이동할 주소로 수정합니다.

 

이렇게 심벌의 메모리 주소를 수정하는 과정이 재배치입니다.

 

각 프로그램마다 변수나 명령어의 메모리 주소는 전부 다를 텐데, 링커가 프로그램이 실행된 후의 변수나 기계 명령어의 메모리 주소를 확인할 수 있는 이유는 무엇일까요? 이는 가상 메모리와 관련이 있습니다.

 

 

 

반응형