링커(Linker) : 실행 파일의 생성 과정

LeeYulhee·2025년 1월 14일

👉 링커(Linker)란?


  • 링커(Linker)는 소프트웨어 개발 과정에서 여러 개의 프로그램 조각(코드나 데이터)을 하나로 묶어 실행 가능한 프로그램으로 만드는 도구
  • 프로그래머가 작성한 소스 코드는 컴파일러에 의해 개별적인 기계어 파일(객체 파일, Object File)로 변환됨
    • 이 파일들에는 함수나 변수 등의 코드 조각들이 들어 있지만, 서로 연결되어 있지 않아서 단독으로 실행할 수 없음
    • 링커는 여러 객체 파일과 라이브러리를 가져와 하나의 실행 파일로 합침
      • 이 과정에서 링커는 각 코드 조각이 참조하는 다른 조각(예 : 함수 호출, 변수 접근)을 연결함
    • 링커가 완료되면 실행 가능한 파일(예 : Windows의 .exe, Linux의 실행 파일)이 생성됨
      • 이 파일은 독립적으로 실행할 준비가 된 상태가 됨
  • 블록 놀이로 비유할 수 있음
    • 각각의 블록은 프로그램의 작은 조각(예 : 함수나 변수)
    • 블록들을 조립해서 하나의 완성된 구조물을 만드는 것이 바로 링커의 역할



👉 링커의 역할


  • 심벌 해석
    • 심벌(Symbol ) : 프로그램에서 사용되는 함수나 변수의 이름을 의미
      • printf, main, myFunction 같은 이름이 심벌
      • 컴파일러가 코드를 컴파일할 때, 심벌을 사용하지만 해당 심벌의 정확한 위치를 모름
    • 여기서 링커는 참조된 심벌 찾고, 심벌을 연결
      • 참조된 심벌 찾기
        • 링커는 각 객체 파일에서 정의된 심벌과 참조된 심벌을 분석
          • 정의된 심벌 : 코드에서 실제로 구현된 함수나 변수
          • 참조된 심벌 : 다른 객체 파일이나 라이브러리에 있는 함수나 변수를 사용하는 경우
      • 심벌 연결
        • 참조된 심벌이 어디에 정의되어 있는지 찾아서 서로 연결
        • 예를 들어, main.o에서 printf를 호출하면 링커는 이를 printf가 정의된 라이브러리 파일로 연결

  • 프로그램을 빌드할 때 사용하는 라이브러리를 처리
    • 정적 라이브러리(Static Library)
      • 정적 라이브러리는 .a(Linux)나 .lib(Windows) 파일 형식으로 제공
      • 링커가 정적 라이브러리를 사용하면, 라이브러리의 필요한 코드가 프로그램에 복사되어 포함됨
        • 예를 들어, libmath.a에서 sqrt 함수를 사용하면, sqrt 함수의 기계어 코드가 실행 파일에 포함
      • 특징
        • 실행 파일이 커질 수 있음(라이브러리 코드가 복사되기 때문)
        • 실행 파일이 독립적으로 동작(외부 라이브러리 파일이 필요 없음)
        • 실행 시 추가적인 라이브러리 로드 작업이 없음
    • 동적 라이브러리(Dynamic Library)
      • 동적 라이브러리는 .so(Linux)나 .dll(Windows) 파일 형식으로 제공
      • 링커는 실행 파일에 라이브러리를 직접 포함하지 않고, 라이브러리의 주소를 참조만 함
        • 실제 라이브러리 코드는 실행 시에 프로그램이 메모리에 로드될 때 연결됨
      • 특징
        • 실행 파일 크기가 작아짐(코드가 복사되지 않음)
        • 여러 프로그램이 동일한 동적 라이브러리를 공유할 수 있어 메모리를 효율적으로 사용
        • 하지만, 실행 파일이 라이브러리에 의존하기 때문에, 실행 환경에서 해당 라이브러리가 없으면 실행되지 않을 수 있음

  • 프로그램의 심벌(함수와 변수)의 실제 메모리 주소를 결정
    • 정적 링킹(Static Linking)
      • 정적 링킹은 컴파일 시간에 심벌의 실제 메모리 주소를 결정
        • 링커는 각 심벌의 고정된 주소를 할당하고, 이를 실행 파일에 포함
        • 실행 파일을 다른 시스템에서 실행해도 심벌 주소는 변하지 않음
      • 예 : 함수 호출 시, printf 함수의 위치(주소)가 실행 파일에 고정
    • 동적 링킹(Dynamic Linking)
      • 동적 링킹은 프로그램이 실행될 때, 심벌의 주소를 동적으로 결정
        • 프로그램이 실행되면 운영 체제가 동적 라이브러리를 메모리에 로드하고, 링커는 이를 기반으로 심벌의 실제 주소를 연결
        • 예 : 실행 시 printf가 정의된 라이브러리가 로드되고, 링커가 해당 라이브러리의 주소를 연결

  • 링커가 심벌 주소를 결정하는 과정
    • Relocation (재배치)
      • 컴파일 단계에서는 심벌이 정확히 어느 메모리 주소에 위치할지 알 수 없음
      • 링커는 다음과 같은 과정을 거침
        1. 절대 주소 확인 : 각 심벌이 메모리의 어느 위치에 위치할지 계산
        2. 재배치 정보 처리 : 컴파일된 코드에 포함된 상대 주소를 절대 주소로 변환
        3. 테이블 작성 : 함수와 변수의 심벌 테이블을 만들어 실행 중에 참조할 수 있도록 준비



👉 링커의 작동 흐름


  • 프로그래머가 코드 작성 : 파일 확장자: .c, .cpp, .py
    #include <stdio.h>
    void greet();
    
    int main() {
        greet();
        return 0;
    }
  • 컴파일(Compile)
    • 컴파일러가 소스 코드를 객체 파일(Object File)로 변환
      • 객체 파일은 기계어로 변환된 코드이지만, 아직 독립적으로 실행할 수는 없는 상태
      • 파일 확장자 : .o(Linux/Unix), .obj(Windows)
      • 링커는 이 단계 이후에 작업을 시작
  • 링킹(Linking)
    • 여기서 링커가 일을 시작
      • 객체 파일 결합 : 여러 개의 객체 파일을 하나로 묶음
      • 심벌 연결 : 함수와 변수가 정의된 위치를 찾아 연결
      • 라이브러리 포함 : 필요한 라이브러리(정적/동적)를 프로그램에 추가하거나 참조를 설정
    • 이 결과물로 실행 가능한 파일(Executable File), 확장자 .exe(Windows), 실행 파일(Unix/Linux) 생성
  • 프로그램 실행
    • 프로그램이 실행되면 운영 체제가 동적 라이브러리를 메모리에 로드(동적 링킹)
    • 실행 중에도 심벌의 주소가 다시 확인되는 경우가 있음(동적 심벌 해석)



👉 예시


// main.c
#include <stdio.h>
void greet();

int main() {
    greet();
    return 0;
}

// greet.c
#include <stdio.h>
void greet() {
    printf("Hello, World!\n");
}
  • 컴파일 단계
    1. main.cgreet.c를 각각 컴파일 → main.ogreet.o 생성
    2. main.ogreet()의 심벌만 참조하고, 실제 구현 위치를 모름.
    3. greet.oprintf()를 참조하지만, 실제 구현은 표준 라이브러리(libc.so)에 있음
  • 링킹 과정
    • 링커가 main.o, greet.o, 표준 라이브러리를 결합
      • greet()greet.o에 있다는 것을 확인하고 연결
      • printf()가 표준 라이브러리(libc.so)에 있다는 것을 확인하고 동적 라이브러리를 참조
profile
끝없이 성장하고자 하는 백엔드 개발자입니다.

0개의 댓글