학습 주제
정리한 내용
심볼 해석은 링커가 입력으로 들어오는 재배치 가능 오브젝트 파일들의 심볼 테이블 정보를 바탕으로 각각의 심볼 참조를 정확하게 하나의 심볼 정의에 연결시키는 작업을 뜻한다. 심볼 정의란 곧 해당 심볼을 정의하는 심볼 테이블 엔트리를 의미하며, 심볼 참조란 코드 상에서 해당 심볼을 참조하는 부분을 의미한다.
링커는 자신의 입력 재배치 가능 목적파일들의 심볼 테이블로부터 정확히 한 개의 심볼 정의에 각 참조를 연결시켜 심볼 참조를 해석한다.
- 지역 심볼의 경우 컴파일러는 모듈당 단 하나의 지역 심볼 정의만을 허용한다.
- 지역 링커 심볼들을 갖게 되는 정적 지역 변수들이 유일한 이름을 갖도록 보장
- 전역 심볼의 경우, 컴파일러가 현재 모듈에 정의되지 않은 심볼을 만나면, 이 심볼이 다른 모듈에서 정의되었다 가정 후 링커 심볼 테이블 엔트리를 생성하며, 링커가 이를 처리하도록 남긴다.
- 링커가 이 심볼의 출처를 찾을 수 없다면, 에러 메시지를 출력하고 종료한다.
- 전역 심볼의 해석은 글로벌 심볼로도 해석될 수 있으므로 해석의 난이도가 높으며, 이 경우 링커는 에러를 출력하거나 하나의 정의만을 선택해야한다.
7.6.1 링커가 중복으로 정의된 전역 심볼을 해결하는 법
링커의 입력은 여러 개의 재배치 가능 오브젝트 모듈이며, 각각은 일련의 심볼을 정의한다.
- 일부는 정의된 모듈 내에서만 볼 수 있는 지역적인 특성을 가진다.
- 일부는 다른 모듈에서도 볼 수 있는 전역적인 특성을 가진다.
- 여러 개의 모듈이 같은 이름으로 전역 심볼을 정의하는 경우(Linux)
- 컴파일러가 컴파일을 할 때, 각 전역 심볼을 어셈블러로 강하게 또는 약하게 보낸다.
- 어셈블러는 이 정보를 재배치 가능 목적파일의 심볼 테이블에 묵시적으로 인코딩한다.
- Strong 심볼 : 함수들과 초기화된 전역변수
- Weak 심볼 : 비초기화된 전역변수
- 동일한 이름을 갖는 복수의 Strong 심볼은 허용되지 않는다.
- 동일한 이름의 Strong 심볼과 다수의 Weak심볼이 있으면 강한 심볼을 선택한다.
- 동일한 이름의 다수의 Weak 심볼이 있으면 어떤 Weak 심볼을 선택해도 상관없다. <
- 결과와 이유
linux> gcc foo1.c bar1.c /tmp/ccq2Uxnd.o: In function ‘main’: bar1.c:(.text+0x0): multiple definition of ‘main’
- 이 경우 링커는 에러 메시지를 내는데, 강한 심볼 main이 두 번 정의되었기 때문이다.(규칙 1)
- 어셈블러는 이 정보를 재배치 가능 목적파일의 심볼 테이블에 묵시적으로 인코딩한다.
- 컴파일러가 컴파일을 할 때, 각 전역 심볼을 어셈블러로 강하게 또는 약하게 보낸다.
- 그러나 만일 x가 한 개의 모듈에서만 초기화 된다면 링커는 에러 없이 다른 곳에서 정의된 강한 심볼을 선택할 것이다.(규칙 2)
linux> gcc -o foobar3 foo3.c bar3.c
linux> ./foobar3
x = 15212
- x가 두 개의 약한 정의로 되어있을 경우 같은 일이 발생할 수 있다.(규칙 3)
/* foo4.c */
#include <stdio.h>
void f(void);
int x;
int main()
{
x = 15213;
f();
printf("x = %d\n", x);
return 0;
}
/* bar4.c */
int x;
void f()
{
x = 15212;
}
- 규칙 2와 3을 적용하면, 중복 정의된 심볼이 서로 다른 타입을 갖는 경우 다양한 런타임 버그가 생길 수 있음을 유의하라.
/* foo5.c */
#include <stdio.h>
void f(void);
int y = 15212;
int x = 15213;
int main()
{
f();
printf("x = 0x%x y = 0x%x \n",x, y);
return 0;
}
/* bar5.c */
double x;
void f()
{
x = -0.0;
}
- x86-64/Linux 머신에서 double은 8바이트, int는 4바이트다. x의 주소는 0x601020이고 y의 주소는 0x601024이다. 그래서 bar5.c의 라인 6에 있는 할당문 x = -0.0은 x와 y가 위치한 메모리 영역에 음수 0의 이중정밀도 부동소수점 표시로 덮어쓰게 된다.
- 이런 버그는 링커로부터 경고만 받고, 실제 프로그램의 실행에서 해당 현상이 발생해 에러가 실제 일어나는 지점과 표시되는 지점간 차이가 발생할 수 있다.
7.6.2 Static 라이브러리와 링크하기
대부분의 컴파일 시스템은 서로 연관된 여러 오브젝트 모듈들을 하나의 정적 라이브러리 파일로 패키지화하는 기능을 제공한다. 정적 라이브러리 파일은 링킹을 수행할 때 링커에 입력으로 들어가게 되며, 실제로 프로그램이 참조하는 심볼을 정의하는 오브젝트 모듈만 실행 파일 안에 포함된다. 리눅스 시스템을 기준으로 정적 라이브러리는 Archive라는 이름의 파일 형식으로 저장되며, 파일 확장자는 .a이다. 그리고 Archive 파일은 각 멤버 오브젝트 모듈의 크기와 위치 정보를 명시하는 헤더를 가진다.
ISO C99에서는 두가지 정적 라이브러리를 소개할 수 있는데, libc.a와 libm.a이다.
libc.a는 표준 I/O, 문자열 조작, 정수 수학 함수(예: atoi, printf, scanf, strcpy, rand)의 광범위한 컬렉션을 정의한다.
libm.a 라이브러리에서는 sin, cos, sqrt와 같은 광범위한 부동 소수점 수학 함수 모음을 정의한다.
이런 정적 라이브러리가 존재하지 않을 경우 라이브러리가 포함하는 함수를 어떻게 쓸 수 있을지 생각해보자.
- 컴파일러가 특정 함수에 대한 호출문을 발견할 시 해당 함수의 코드를 자신이 직접 만드는 방법.
- 표준 함수 개수가 적은 Pascal등의 언어에서는 이 방식을 채택함.
- C언어의 경우 표준함수의 개수가 많고, 컴파일러가 지나치게 느려지며, 함수가 추가될 때마다 새로 컴파일러가 작성되어야 한다.
- C 언어의 모든 표준 함수들을 하나의 재배치 가능 오브젝트 파일 안에 담는다.
- 단 하나의 파일만 입력하면 동일한 기능을 이용할 수 있음.
- 표준함수의 구현과 컴파일러의 구현을 분리할 수 있다.
- 디스크 및 메모리 낭비가 너무 심함.
- 많은 함수의 코드가 모든 실행 파일에 포함되며, 프로그램이 실행될 때도 메모리에 적재된다.
- 함수를 추가, 수정, 삭제 시 전체를 다시 컴파일해야하는 번거로움이 있다.
- 각 표준 함수별로 재배치 가능 오브젝트 파일을 만들고 잘 알려진 디렉토리에 저장한다.
- 실행파일을 만들때마다 필요한 함수에 해당하는 모듈을 직접 찾아서 입력해야한다.
이런 문제점을 모두 해결해주는 것이 정적 라이브러리다.
- 필요한 함수가 포함된 라이브러리 파일의 이름만 커맨드라인에 적으면 된다.
- 정적 라이브러리 파일을 입력해도 실행 파일에 복사되는 부분은 프로그램이 실제로 참조한 심볼이 존재하는 오브젝트 모듈뿐이므로 디스크와 메모리의 낭비를 막을 수 있다.
- 중요 정적 라이브러리 파일들은 대부분 컴파일러 드라이버가 알아서 입력해주기 때문에 프로그래머의 수고도 던다.
정적 라이브러리 파일 예제
addvec 함수를 포함하는 addvec.c 파일과 multvec 함수를 포함하는 multvec.c 파일을 가지고 libvector.a라는 이름의 정적 라이브러리 파일을 만드는 방법은 다음과 같다.
linux> gcc -c addvec.c multvec.c
linux> ar rcs libvector.a addvec.o multvec.o
linux> gcc -c main2.c
linux> gcc -static -o prog2c main2.o (-L. -lvector) or (./libvector.a)
-static은 메모리에 로드된 뒤 더 이상의 동적 링킹이 필요 없이 완전하게 실행될 수 있는 실행 파일을 만들어야 한다는 것을 나타내는 옵션이고, -lvector는 libvector.a의 약칭이며, -L은 libvector.a가 현재 디렉토리에 위치함을 나타내는 옵션이다.
7.6.3 심볼 해석 전체 과정
이제 본격적으로 심볼 해석(Symbol Resolution) 의 진행 과정을 자세히 알아보도록 하자. 링커는 커맨드 라인에 입력되는 순서대로(왼쪽 → 오른쪽) 재배치 가능 오브젝트 파일들과 아카이브 파일들을 하나씩 스캔 한다.
- 커맨드 라인에 확장자가 .c인 파일이 입력되면 그것은 자동으로 .o가 확장자인 파일로 먼저 번역된다.
링커는 스캔 과정에서 다음과 같은 세 개의 집합을 내부적으로 관리한다. 초기에는 셋 다 빈 집합이다.
- E : 실행 파일에 포함될 재배치 가능 오브젝트 파일의 집합
- U : 심볼 해석이 이뤄지지 않은 심볼 참조의 집합
- D : 지금까지의 스캔을 통해 확보한 심볼 정의들의 집합
- 명령줄 상의 매 입력파일 f에 대해서 링커는 f가 오브젝트 파일 또는 아카이브인지 결정.
- 오브젝트일 시, f를 E에 추가하고 심볼 정의와 f에서의 참조를 반영하도록 U와 D를 갱신
- f가 아카이브일 시, 링커는 U안의 미해석 심볼을 아카이브의 멤버에 정의된 심볼과 매칭을 시도.
- 아카이브의 멤버 m이 E에 추가될 시 m이 정의하는 심볼을 D에 추가한다.
- D를 바탕으로 m의 심볼 참조들과 U의 심볼 참조들을 해석한다.
- 해석에 실패한 심볼 참조들은 U에 반영한다.
- U와 D가 변하지 않을때까지 반복
만약 커맨드 라인 입력 순서대로 모든 파일을 스캔했음에도 해석되지 않은 심볼 참조가 있다면 링커는 에러 메시지와 함께 즉시 종료한다. 그렇지 않고 모든 심볼 참조를 성공적으로 하나의 심볼 정의에 연결했다면 이제 E의 모듈들을 대상으로 재배치(Relocation)를 진행한다.
주의점
- 커맨드라인의 입력 순서가 중요하다.
- 심볼 정의가 포함된 스태틱 라이브러리 파일을 심볼 참조가 포함된 재배치 가능 오브젝트 파일보다 앞에 위치시키면 해당 심볼 참조를 해석하는데 실패한다.
- 정적 라이브러리 파일은 U에 존재하는 심볼 참조에 대응되는 심볼 정의만 D에 넣기 때문이다.
- 따라서 스태틱 라이브러리 파일은 맨 뒤에 두는 것을 원칙으로 한다.
EX) p.o가 libx.a의 심볼을 참조, libx.a가 liby.a의 심볼을 참조, liby.a가 libx.a의 심볼을 참조, libx.a가 p.o의 심볼을 참조linux > gcc p.o libx.a liby.a libx.a
'Computer Science > 컴퓨터 구조' 카테고리의 다른 글
[CSAPP] 7.8 Executable Object Files(실행 가능한 객체 파일) (0) | 2023.02.21 |
---|---|
[CSAPP] 7.7 Relocation(재배치) (0) | 2023.02.21 |
[CSAPP] 7.12 Position-Independent Code (PIC)(위치 독립성 코드) (0) | 2023.02.17 |
[CSAPP] 7.11 Loading and Linking Shared Libraries from Applications (응용 프로그램으로부터 공유 라이브러리를 로드하고 링크하기) (0) | 2023.02.17 |
[CSAPP] 7.10 Dynamic Linking with Shared Libraries(공유 라이브러리 동적 링크) (0) | 2023.02.17 |