7.6 심볼 결합 (Symbol Resolutuion)
심볼 결합은 링커가 각 심볼 참조(reference)를 정확히 하나의 정의(definition)와 연결시키는 과정이다. 이 작업은 링커가 수행하는 주요 작업 중 하나이며, 프로그램이 여러 모듈로 나뉘어 작성될 때 매우 중요하다.
7.6 링커가 중복된 심볼 이름을 해결하는 방식
입력 : 여러개의 목적 파일
링커는 여러 개의 재배치 가능한 객체 모듈(relocatable object modules)을 입력으로 받는다.
이들 각 모듈은 여러 개의 심볼을 정의하며, 일부는 로컬(local)이고 일부는 전역(global)이다.
문제 : 전역 심볼 중복
두 개 이상의 모듈이 같은 이름의 전역 심볼을 정의할 경우, 링커는 다음 중 하나를 해야 한다.
- 에러로 보고하고 종료
- 하나를 선택하고 나머지를 무시
Linux 시스템에서는 강한 심볼(Storong Symbol)과 약함 심볼(Week Symbol)의 구분을 도입해서 이 문제를 해결한다.
강함 심볼 VS 약한 심볼
구분 | 예시 | 설명 |
---|---|---|
강한 심볼 | 함수, 초기화된 전역 변수 | 링커는 이들을 반드시 하나만 존재하도록 요구 |
약한 심볼 | 초기화되지 않은 전역 변수 | 여러 개 존재해도 되며, 링커가 규칙에 따라 선택 |
컴파일러는 각 심볼을 강한 혹은 약한 심볼로 구분하고, 어셈블러는 이 정보를 심볼 테이블에 암시적으로 저장한다.
중복 심볼 처리 규칙
- 규칙 1 : 동일한 이름의 강한 심볼이 여러 개 존재 -> 에러
- 규칙 2 : 하나의 강한 심볼과 여러 개의 약한 심볼 존재 → 강한 심볼 선택
- 규칙 3 : 여러 개의 약한 심볼만 존재 → 아무거나 선택 (비결정적)
예시로 이해하기
[규칙 1 위반: 강한 심볼 중복]
// foo1.c
int main() { return 0; }
// bar1.c
int main() { return 0; }
링커 오류 발생 : mail
이 두 번 정의됨
[규칙 2 적용: 강한 + 약한]
// foo3.c
int x = 15213; // 강한 심볼
// bar3.c
int x; // 약한 심볼
링커는 x = 15213
을 선택함. 단 이 사실은 출력되지 않는다.
실행 결과: x = 15212
(함수 f가 x를 15212로 덮어씀)
[규칙 3 적용: 약한 심볼 둘 다]
// foo4.c
int x;
// bar4.c
int x;
링커는 둘 중 하나를 선택 (무작위)
버그 유발 가능성 ↑
위험한 버그 예시
// foo5.c
int x = 15213; // int 타입
// bar5.c
double x; // double 타입
int
는 4바이트,double
은 8바이트double x
가int x
의 메모리뿐 아니라 그 옆의 변수까지 침범해서 덮어씀- 실행 결과:
x = 0x0
,y = 0x80000000
→ 이상한 동작 발생
컴파일러는 경고만 하고, 링커는 실행 가능 바이너리를 생성한다.
해결법:
gcc -fno-common
플래그로 약한 심볼 중복을 에러로 처리- 또는
-Werror
로 경고를 에러로 전환
7.6.2 정적 라이브러리와의 링킹
지금까지 링커는 여러 개의 재배치 가능 목적 파일(relocatable object files) 입력으로 받아 실행 파일을 만든다. 그런데 현실에서는 관련된 목적 파일들을 묶어 정적 라이브러리(static library)로 만들어 관리하는 경우가 많다.
정적 라이브러리란?
- 정적 라이브러리는 관련된 객체 모듈들을 하나의 파일로 패키징 한 아카이브 파일(archive file)이다.
- 리눅스에서는
.a
확장자를 가지며, 예:libm.a
,libvector.a
- 링커는 이 라이브러리 안에서 프로그램에서 참조하는 심볼만 선택해서 실행 파일에 포함시킨다.
왜 정적 라이브러리를 사용할까?
예를 들어, C 표준 함수들 (printf
, scanf
, strcpy
등)은 모두 libc.a
에 정의되어 있다. 컴파일러가 이런 함수들을 다 직접 포함하기에는 현실성이 없고, 컴파일할 때마다 변경 사항 반영도 어렵기 때문에 모듈화 된 라이브러리 구조가 훨씬 유리하다.
예제 : libvector.a
라이브러리 만들기
두 개의 벡터 연산 함수 addvec
과 multvec
을 별도의 소스 파일에 정의했다고 하자
gcc -c addvec.c multvec.c # 객체 파일 생성
ar rcs libvector.a addvec.o multvec.o # 정적 라이브러리 생성
이제 main2.c
에서 addvec()
을 호출하는 프로그램을 작성한 뒤 다음과 같이 컴파일한다:
gcc -static -o prog2c main2.o ./libvector.a
- -static: 정적 링킹을 하라는 옵션
- -L.: 현재 디렉터리에서 라이브러리를 검색
- -lvector: libvector.a를 의미
링커의 동작
링커는 main2.o
에서 addvec()
이 필요하다는 것을 인식하고, libvector.a
에서 해당 심볼을 가진 addvec.o
만을 실행 파일에 복사한다. multvec.o
는 참조되지 않으므로 포함되지 않는다.
명령어 순서 중요성
라이브러리가 명령줄에서 참조 파일보다 먼저 오면, 링커는 심볼을 찾지 못해 오류가 난다.
gcc -static ./libvector.a main2.c # ❌ 실패!
링커는 순차적으로 심볼을 확인하므로 항상 라이브러리는 마지막에 배치해야 한다.
gcc -static main2.c ./libvector.a # ✅ 성공!
7.6.3 링커가 정적 라이브러리를 사용해 참조를 해결하는 방식
정적 라이브러리는 매우 유용하지만, 링커가 이들을 처리하는 방식은 초보자에게 혼란을 줄 수 있다. 특히 명령어 라인에서의 순서가 중요한 역할을 하기 때문이다.
링커의 심볼 해결 알고리즘
링커는 심볼 결합 단계에서 명령어 라인에 나타나는 순서대로 파일을 왼쪽에서 오른쪽으로 스캔한다. 이 과정에서 다음과 같은 집합을 유지한다.
E
: 실행 파일에 포함된 객체 파일 집합U
: 아직 해결되지 않은 참조(symbol reference)D
: 이미 정의된 심볼들
처음에는 모두 빈 상태에서 시작한다.
처리 로직
각 입력 파일 f
에 대해:
f
가 객체 파일일 경우:f
를E
에 추가U
,D
를 업데이트
f
가 정적 라이브러리(archive) 일 경우:U
에 있는 미해결 심볼을 기준으로 라이브러리 안의 멤버 객체 파일들 중 해당 심볼을 정의한 것만 선택- 선택된 멤버만
E
에 추가,U
,D
갱신 - 이 작업을 더 이상 변화가 없을 때까지 반복
💡 사용되지 않는 멤버 객체 파일은 무시.
실패 조건
링커가 모든 파일을 처리한 후에도 U
가 비어 있지 않으면, 링커는 에러를 발생시키고 종료한다.
예시:
linux> gcc -static ./libvector.a main2.c
이 경우 libvector.a
는 먼저 처리되었고 main2.c
는 아직 처리되지 않았기 때문에 addvec에 대한 참조가 없었다. 따라서 libvector.a
는 addvec.o
를 추가하지 않았고, 결국 main2.c
에서 참조하는 addvec
가 해결되지 않아 링커 오류가 발생한다
정리 : 정적 라이브러리 순서 규칙
- 항상 라이브러리는 명령어 줄의 끝에 배치한다.
- 라이브러리들 간 의존성이 있는 경우, 의존 순서에 맞춰 정렬해야 한다.
- 예:
libx.a → liby.a → libz.a
순서로 링크
- 예:
- 필요하면 같은 라이브러리를 여러 번 포함해도 된다.
- 예:
libx.a → liby.a → libx.a
처럼
- 예:
'크래프톤 정글 (컴퓨터 시스템: CSAPP) > 7장 링커' 카테고리의 다른 글
컴퓨터 시스템 : CSAPP 7장 정리 - 7.13 ~ 7.15 (0) | 2025.04.18 |
---|---|
컴퓨터 시스템 : CSAPP 7장 정리 - 7.11 ~ 7.12 (0) | 2025.04.18 |
컴퓨터 시스템 : CSAPP 7장 정리 - 7.8 ~ 7.10 (0) | 2025.04.18 |
컴퓨터 시스템 : CSAPP 7장 정리 - 7.7 재배치 (0) | 2025.04.18 |
컴퓨터 시스템 : CSAPP 7장 정리 - 7.1 ~ 7.5 (0) | 2025.04.17 |