Krafton Jungle/4. CSAPP

[Computer System] ⑦ 링커

munsik22 2025. 4. 19. 19:49

링킹linking은 여러 개의 코드와 데이터를 모아서 연결하여 메모리에 로드될 수 있고 실행될 수 있는 한 개의 파일로 만드는 작업이다. 링킹은 컴파일 시에 수행할 수 있고, 이 때 소스코드는 머신코드로 번역된다. 링킹은 대게 링커에 의해 처리된다. 링커는 SW 개발 시에 중요한 역할을 수행하는데, 그것은 이들이 독립적인 컴파일을 가능하게 하기 때문이다.

7.1 컴파일러 드라이버 ✅

대부분의 컴파일 시스템은 언어 전처리기, 컴파일러, 어셈블러, 링커를 필요에 따라 호출하는 컴파일러 드라이버를 제공한다. 예를 들어, 사용자는 GNU 컴파일 시스템을 사용해 프로그램을 작성하기 위해 다음 명령을 쉘에 입력해 GCC 드라이버를 호출할 수 있다.

gcc -Og -o prog main.c hello.c

드라이버는 먼저 C 전처리기(cpp)를 돌리고, 이것은 main.c를 ASCII 중간 파일인 main.i로 번역한다.

cpp [...] main.c /tmp/main.i

다음으로, 드라이버는 C 컴파일러(cc1)을 돌려서 main.i를 ASCII 어셈블리 언어 파일인 main.s로 번역한다.

cc1 /tmp/main.i -Og [...] -o /tmp/main.s

그 다음에 드라이버는 어셈블러(as)를 돌려서 main.s를 재배치 가능한 바이너리 목적파일인 main.o로 번역한다.

as [...] -o /tmp/main.o /tmp/main.s

마지막으로 링커 프로그램(ld)를 돌려서 필요한 시스템 목적파일들과 함께 실행 가능한 목적파일 prog를 생성한다.

ld -o prog [...] /tmp/main.o /tmp/hello.o

실행 파일 prog를 실행시키려면 Linux shell에서 그 이름을 명령줄에 입력한다.

./prog

쉘은 로더loader라고 부르는 OS 내의 함수를 호출하며, 로더는 실행파일 prog의 코드와 데이터를 메모리로 복사하고, 제어를 프로그램의 시작 부분으로 전환한다.

7.2 정적연결

정적연결: 링커는 재배치 가능 목적파일들을 연결해서 실행 가능 목적파일 prog를 형성한다.

7.3 목적파일

  • 재배치 가능 목적파일
  • 실행 가능 목적파일
  • 공유 목적파일

7.4 재배치 가능 목적 파일 ✅

전형적인 ELF 재배치 가능 목적파일

  • 위 그림은 전형적인 ELFExecutable-and-Linkable-Format 재배치 가능 목적파일의 형식을 보여준다. ELF 헤더는 목적 파일을 생성한 시스템의 워드 크기와 바이트 순서를 나타내는 16 바이트 배열로 시작한다.
  • ELF 헤더의 나머지에는 ELF 헤더의 크기, 목적파일 타입, 머신 타입, 섹션 헤더 테이블의 파일 오프셋, 섹션 헤더 테이블의 크기와 엔트리 수가 들어 있다.
  • 여러 섹션들의 위치와 크기는 섹션 헤더 테이블로 나타내며, 이 테이블은 목적파일의 각 섹션에 대해 고정된 크기의 엔트리를 갖는다.
  • ELF 헤더와 섹션 헤더 테이블 사이에는 다음과 같은 섹션 내용들이 들어있다.
    • .text: 컴파일된 프로그램의 기계어 코드
    • .rodata : printf 문장의 format string, switch문의 점프 테이블과 같은 읽기-허용 데이터
    • .data: 초기화된 C 전역변수 및 정적변수
    • .bss: 초기화되지 않은 C 전역변수와 정적변수, 0으로 초기화된 전역변수 및 정적변수
    • .symtab: 프로그램에서 정의되고 참조되는 전역변수들과 함수에 대한 정보를 가지고 있는 심볼 테이블
    • .rel.text: 링커가 이 목적파일을 다른 파일들과 연결할 때 수정되어야 하는 .text 섹션 내 위치들의 리스트
    • .rel.data: 이 모듈에 의해 정의되거나 참조되는 전역변수들에 대한 재배치 정보
    • .debug: 프로그램 내에서 정의된 지역변수들과 typedef, 프로그램과 최초 C 소스 파일에서 정의되고 참조되는 전역변수들을 위한 엔트리를 갖는 디버깅 심볼 테이블
    • .line: 최초 C 프로그램과 .text 섹션들 내 기계어 인스트럭션 내 라인 번호들 간의 매핑
    • .strtab: .symtab과 .debug 섹션들 내에 있는 심볼 테이블과 섹션 헤더들에 있는 섹션 이름들을 위한 스트링 테이블

7.5 심볼과 심볼 테이블

[static으로 변수와 함수 이름 감추기]
C 프로그래머는 static 속성을 사용하여 모듈 내에서 변수 및 함수 선언을 숨길 수 있다. 이는 Java와 C++에서 publicprivate 선언을 사용하는 것과 유사하다. C에서는 소스 파일이 모듈의 역할을 한다. static 속성으로 선언된 전역 변수나 함수는 모듈에 private하다. 반면, static 속성이 없는 전역 변수나 함수는 public이며, 다른 모듈에서 접근할 수 있다. 가능한 한 변수와 함수를 보호하기 위해 static 타입을 사용하는 것이 좋은 프로그래밍 습관이다.

7.9 실행가능 목적파일의 로딩 ✅

Linux shell에 ./prog를 입력하면, prog가 내장 쉘 명령어에 대응되지 않기 때문에 쉘은 prog가 실행가능 목적파일(이하 EOF)이라고 가정하며, 쉘은 로더라고 불리는 메모리 상주 OS 코드를 호출해서 이 프로그램을 실행한다. 로더는 디스크로부터 EOF 내의 코드와 데이터를 메모리로 복사하고 이 프로그램의 첫번째 인스트럭션(엔트리 포인트)으로 점프해서 프로그램을 실행한다. 이와 같은 프로그램을 메모리로 복사하고 실행하는 과정을 로딩이라고 부른다.

Linux x86-64 런타임 메모리 이미지 ✅

세그먼트 정렬 요건으로 인한 간격과 주소공간 배치 랜덤화(ASLR)는 나타내지 않았고, 크기도 일치하지 않는다.

모든 실행 중인 Linux 프로그램은 위 그림과 유사한 런타임 메모리 이미지를 가진다. x86-64 Linux 시스템에서 코드 세그먼트는 주소 0x400000에서 시작하고, 뒤이어 데이터 세그먼트가 온다. 런타임 힙heap은 데이터 세그먼트 다음으로 오고, malloc 라이브러리를 호출해서 위로 성장한다.

 

이 다음에는 공유 모듈들을 위해 따로 놔둔 영역이 존재한다. 사용자 스택은 가장 큰 합법적 사용자 주소(248 - 1)에서 시작해서 더 작은 메모리 주소 방향인 아래로 성장한다. 스택 위의 영역은 OS의 메모리 상주 부분인 커널의 코드와 데이터를 위해 예약되어 있다.

 

링커는 런타임 주소를 스택, 공유 라이브러리, 힙 세그먼트에 할당할 때, 주소 공간 배치 랜덤화(ASLR)를 사용한다. 이들의 위치가 프로그램이 실행될 때 매번 변경되기는 하지만 상대적인 위치는 항상 동일하다.

 

메모리가 로딩된 후에는 엔트리 포인트로 점프한다. 이는 모든 프로그램에서 _start라는 함수의 위치로 동일하다. _start 함수는 시스템 초기화 함수__libc_start_main을 실행하는데, 이는 실행 환경을 초기화하고 사용자가 정의한 main 함수를 호출하고 return 값을 처리하며, 필요한 경우 제어권을 넘겨주는 역할을 한다.