2023. 12. 7. 14:49ㆍ컴퓨터공학 전공공부/시스템 프로그래밍
1. Linking
1-1. 링킹 기본 개념과 링커
linking에 들어가기 전에
- linking의 발생 : 라이브러리의 특정 함수를 호출하게 되면 linking이 발생한다.
- compilation system의 분류 : compilation system은 크게 assembly phase와 linking phase로 구분된다. assembly phase에서는 C 코드를 object code로 만들고, linking phase에서는 앞에서 만든 object code와 필요한 다른 object file, 라이브러리를 결합해 단일 실행 가능한 프로그램으로 만든다.
- linker를 이해함으로써 얻는 것 :
- 큰 프로그램의 설계 가능
- 다른 중요한 시스템 컨셉 이해
- 공유 라이브러리 이용 가능
- 언어 범위 지정 규칙이 구현되는 방법 이해
static linking
코드의 첫 번째 줄에서 main.c, sum.c 두 소스파일을 합쳐 하나의 소스파일 prog에 저장함을 알 수 있다. 이후 내부적으로 다음과 같은 과정을 거치게 된다.
- main.c, sum.c가 각각의 translators를 거쳐 main.o, sum.o로 만들어진다.
- translators에는 cpp(전처리), cc1(컴파일), as(기계어로 번역)이 있다.
- 이 단계에서는 각각 컴파일되며, 재배치 가능한 object 파일이 만들어진다.
- main.o, sum.o가 링커를 통해 prog로 만들어진다.
- 이 단계에서는 실행가능한 object 파일이 만들어진다.
linker가 필요한 이유
- modularity (모듈성)
- 라이브러리를 만들 수 있는 것도 링커 덕분이다!
- 효율성
- 컴파일 과정이 각각 따로 진행되기에 시간이 절약된다.
- 특정 소스 파일이 문제가 있을 경우, 그 소스 파일만 고치면 된다. 다른 소스 파일은 건드릴 필요 없다.
- 컴파일 과정이 각각 따로 진행되기에 시간이 절약된다.
object 파일의 종류 세 가지 (모듈)
각각의 object 파일은 모듈이라 볼 수 있다. 아래 세 종류는 리눅스 기준 확장자의 종류라고도 볼 수 있다.
- 재배치 가능한 object 파일 (.o file)
- 실행가능한 object 파일 (a.out file)
- 공유된 object 파일 (.so file)
- 이 파일은 linking 과정 없이, 실행할 때 합쳐진다. 즉, 동적으로 링킹된다.
Executable and Linkable Format (ELF)
object 파일에 대한 표준 이진 포맷이며, .o file, a.out file, .so file에 대한 통일된 포맷이다. ELF binaries라고도 부른다.
ELF object 파일 포맷은 다음 내용으로 구성된다.
- ELF header
- word 크기, byte ordering, 파일 종류(.o, exec, .so), 기기 종류 등에 대한 정보를 담는다.
- Segment header table
- 섹션을 나누어, 각 섹션이 몇 번째 바이트부터 시작되는지 명시한다.
- 페이지 크기, 가상 메모리 섹션과 그 크기 등에 대한 정보를 담는다.
- .text section
- 기계어 코드를 담는다.
- .rodata section
- read only data에 대한 정보를 담는다. (printf의 strings, jump table 등)
- .data section
- 초기화된 전역변수를 담는다.
- .bss section
- 초기화되지 않은 전역변수를 담는다. 공간을 할당하지 않고 '존재한다'고 메모만 해 두는 느낌이다.
- .symtab section
- 함수, 전역변수를 테이블화하여 담는다. 각 섹션의 이름과 위치를 담는다.
- .rel.text section
- 적절한 값으로 바뀌어야 하는 text 데이터들을 담는다.
- 예를 들면, 상수자리에 변수가 온 경우 이 변수에 대한 데이터를 담는다. 나중에 바뀌어야 하는 데이터를 저장해 놓는 느낌!
- 적절한 값으로 바뀌어야 하는 text 데이터들을 담는다.
- .rel.data section
- 적절한 값으로 바뀌어야 하는 data 데이터들을 담는다. 위와 비슷한 느낌.
- .debug section
- 디버그 관련 정보를 담는다. (gcc -g)
- section header table
- 각 섹션에 대한 정보 테이블이다.
링커의 역할
- symbol resolution
- 각각의 obj 파일은 소위 '구멍'이 있다. (내 파일에 구현되지 않은 함수 등) 이 구멍을 적절한 파일에 연결하는 것이 symbol resolution이다. 다시 말해, 참조자 파일의 입장에서 어딜 참조해야 할지, 즉 정의된 부분이 어디인지를 알려준다.
- relocation (재배치)
- 분리되어있는 코드, 데이터들을 하나의 섹션으로 통합한다.
- symbol을 재배치해 메모리 배치 최종본을 만든다.
- 모든 참조를 업데이트한다.
링커 symbol
- global symbols
- 특정 한 모듈에서 정의되었지만, 다른 모듈에서도 참조 가능한 symbol들이다.
- non-static C 함수, non-static 전역변수 등이 있다.
- external symbols
- 특정 한 모듈에서 참조되고 있지만, 다른 모듈에서 정의된 symbol들이다. 이 또한 global symbols와 마찬가지로 non-static이어야 한다.
- 쉽게 말해, 내가 정의하지 않은 global symbol이다.
- extern 키워드를 사용한다.
- ex. extern int a;
- local symbols
- 특정 한 모듈에서 배타적으로 정의되고, 참조되는 symbol들이다.
- local linker symbol과 local program variable은 같은 뜻이 아니다!
- 전자는 local symbol이고, 후자는 local symbol이 아니다.
- 전자는 .bss나 .data에 저장되고, 후자는 stack이나 register에 저장된다.
- local linker symbol의 경우 obj 파일 안에선 전역변수지만, local program variable은 obj 파일 안에서도 특정 함수 안에서만 쓰인다.
- local program variable (지역변수)는 symbol이 아니다.
- object 파일 단위로 들어있는 static 변수이다. (쉽게 얘기하면, 모든 static 변수이다.) 외부 파일 입장에서 보이지 않는다. (반면 global, external은 외부 파일에서 볼 수 있다.)
1-2. step 1, symbol resolution
symbol resolution 예시
- sum 함수는 같은 파일에 구현이 안 되어있고, 선언만 되어 있으므로 external symbol이다. 함수가 external symbol일 경우에는 extern 키워드를 붙이지 않아도 된다.
- array와 main함수 모두 global symbol이다.
- main함수 내의 val, sum함수 내의 i,s는 지역변수로, symbol이 아니다.
symbol resolution 예시 문제
static으로 선언되어 있기에 local symbol임에 유의하자.
링커의 symbol 규칙
링커는 symbol에 대하여 strong, weak으로 분류한다. 모든 구현된 함수와 초기화된 전역변수가 strong, 초기화되지 않은 전역변수가 weak에 들어간다. 이렇게 분류함으로써 충돌을 방지한다.
충돌에 대해서, 예를 들어 p1.c에 int foo=5;가 있고, p2.c에 int foo가 있을 경우 foo가 중복되어 충돌이 일어난다. 이 때 전자의 foo가 strong이기에 우선으로 처리한다.
위의 내용을 기반으로 링커는 세 개의 규칙을 가지고 symbol을 처리한다.
- 여러 개의 strong symbol은 허용되지 않는다.
- 즉, 선언은 여러 번 가능하나 정의는 딱 한 번만 가능하다.
- 여러 개의 strong symbol 발생 시 linker error가 발생한다.
- strong symbol 하나와 여러 개의 weak symbol이 있을 때, strong symbol을 선택한다.
- 위의 예시에 적용해 보면, p2.c에서 foo를 접근하려 하면 p1.c의 foo로 접근하게 된다.
- strong symbol 없이 여러 개의 weak symbol만이 있을 때, 임의로 선택한다.
링커의 symbol 규칙 예시
- 두 개의 strong symbol (p1)이 충돌해 error 발생한다.
- 두 개의 weak symbol (x) 중에서 임의로 하나를 뽑을 것이다. 임의로 뽑는 건 예상치 못한 결과를 발생시킬 수 있으므로, static이나 extern 키워드를 적절히 사용해 weak symbol이 없게끔 하는 것이 좋다.
- 두 개의 weak symbol (x) 중에서 임의로 하나를 뽑을 것이다. 왼쪽의 x를 뽑을 경우, 오른쪽의 x에 접근하여 x값 수정 시 왼쪽의 y값까지 영향받는 문제가 발생할 수도 있다.
- 왼쪽의 x값이 strong symbol이므로 왼쪽 x를 선택한다. 이 경우 위에서 언급한 문제가 무조건 발생한다.
- 왼쪽의 x값이 strong symbol이므로 왼쪽 x를 선택한다. 이 경우 오른쪽 x 앞에 extern 키워드를 적어 명백히 해주는 것이 좋다.
1-3. step 2, relocation
relocation(재배치) 한 눈에 보기
relocation entries
relocation에 필요한 정보를 사전에 저장해 둔 것이다. 아래의 C code에 대응해 Elf64_Rela라는 relocation entry가 존재한다.
//C code
int array[2] = {1,2};
int main () {
int val = sum(array,2);
return val;
}
//relocation entry
typedef struct {
long offset; //수정돼야 하는 참조변수의 section offset
long type:32, symbol:32; //type은 전반부 32 bit, symbol은 후반부 32 bit에 해당한다.
//type은 어떻게 참조할지 나타내고, symbol은 어떤 symbol인지 나타낸다.
long addend; //bias
} Elf64_Rela;
아래 사진은 main.c 에 relocation entry가 포함된, main.o이다.
relocated .text 섹션
재배치가 필요한 부분에 대한 정보를 담고 있다.
1-4. step 3, 실행 가능한 obj 파일 로드하기
실행 가능한 obj 파일 로드하기
재배치가 끝난, 실행 가능한 obj 파일은 하드디스크에 존재한다. 이를 '실행한다'라는 것은, 실행에 필요한 부분을 메모리에 하나하나 꺼내오며 실행한다는 뜻이다.
1-5. 라이브러리
자주 쓰이는 함수를 묶기
자주 쓰이는 함수를 묶는다는 말은 결국, 라이브러리를 만들겠다는 뜻이다. 그렇다면 이 라이브러리를 어떻게 묶을 것인가? 이에 대해 두 가지 방법이 있다.
- 모든 함수를 하나의 소스 파일에 넣는다.
- 큰 obj 파일이 만들어진다.
- 공간적으로도, 시간적으로도 비효율적이다. (작은 함수 하나를 수정하고 싶어도 전체를 수정해야 하기 때문이다.)
- 각각의 함수를 다 다른 소스 파일에 넣는다.
- 개발자들이 필요한 것을 명시적으로 포함해 사용한다.
- 위의 방법에 비해 효율적이지만, 하나하나 포함해 사용하는 것이 개발자에게는 부담이다.
따라서, 위의 두 가지 방법을 적절히 섞어 사용해야 한다.
1-6. 구식 방법 라이브러리 구축 : static libraries
static libraries(.a archive files)
연관된 obj 파일들을 하나로 묶어 관리한다. 아카이브라 불리며, 일종의 압축파일이다. 아카이브에는 아카이브에 속한 파일의 정보를 링커에게 알려주기 위한 인덱스가 포함되어 있다. 링커는 직접 아카이브를 순회하며 symbol을 찾아야 한다. 아카이브 멤버 파일이 어떤 참조를 확인하면, 이 참조를 실행 파일에 연결한다.
static library 만들기
- 각각의 c 파일을 각각의 relocated obj 파일로 만든다.
- 아카이버를 통해 하나의 아카이브로 묶는다. 아카이버는 묶어주는 작업을 하는 프로그램이다.
아카이버로 묶을 때엔 다음과 같은 코드를 사용한다.
unix> ar rs libc.a \ //rs는 옵션, libc.a는 생성할 아카이브 파일명, \는 줄바뀜을 의미한다.
atoi.o printf.o ... random.o //libc.a의 재료이다.
//rs 추가설명
//r: insert and replace
//s: index 생성
자주 쓰이는 라이브러리들 (아카이브들)
- libc.a (C 표준 라이브러리)
- C 코드를 짤 때마다 거의 매번 include된다.
- 기본적인 C 동작을 지원한다.
- I/O, 메모리 할당, string 제어, 난수 생성 등
- libm.a (C 수학 라이브러리)
- floating point math를 지원한다.
static library를 이용한 링킹
링커는 링킹 시 몇 개의 아카이브를 자동으로 찾아보고 필요한 것을 링킹한다. 한편 libvector.a는 링커가 자동으로 찾아보는 디렉토리에 들어있지 않으므로, 컴파일할 때 따로 지칭해야 한다.
static library의 활용
gcc main.o sum.o
위의 코드에 따라 컴파일한다면, 이는 다음의 과정을 거친다.
- main.o를 살펴본다.
- main.o에서 구멍을 확인한다.
- sum.o에서 2의 구멍을 메울 정보를 얻고, 메꾼다.
따라서 obj 파일들을 나열하는 순서가 중요하다! '피참조' 파일을 뒤에 배치해야 한다.
1-7. 현대식 방법 라이브러리 구축 : shared libraries
static library의 단점과 shared library의 등장
static library(정적 라이브러리)는 다음과 같은 단점을 가진다.
- 실행파일마다 라이브러리를 필요로 하므로, 라이브러리 중복이 일어난다.
- 실행파일 또한 중복이 일어난다. (실행프로그램 데이터의 중복)
- 라이브러리를 수정하면 모든 실행파일에 대해 relink 해야한다.
이러한 단점을 개선한 것이 shared library (동적 라이브러리)이다. shared library는 라이브러리가 필요하면 그때 그때 필요한 정보를 빼오는 방식으로 동작한다. 이를 통해 단점 1을 해결할 수 있다.
한편, 단점 2를 해결하고 싶으면 '필요한 정보 빼오기'를 런타임에 수행하면 된다. (예를 들어, 런타임 중에 printf를 발견하면 그 때 관련 정보를 빼온다. 그런데 이미 과거에 메모리에 올려놓은 것이 있다면 그 정보를 이용한다.)
shared library의 동적 링킹
동적 링킹은 두 시기에서 발생할 수 있다. 한편 shared library 링킹 과정은 여러 프로세스에 의해 공유될 수 있다.
- 시기 1: load-time linking
- dynamic linker(ld-linux.so)에 의해 자동적으로 제어된다. 이 dynamic linker는 시스템에 상주하며 돌아가는 프로그램이다.
- libc.so(C 표준 라이브러리) 또한 보통 동적으로 링킹된다.
- 시기 2: run-time linking
- dlopen()이라는 함수를 활용한다. 이 함수 안에서 run-time linking을 지시한다.
- run-time linking은 library interpositioning(바꿔치기), 웹 서버의 성능 향상 등을 용이하게 한다.
load-time linking 시각화
libvector.so는 내가 만든 shared library이다. 따라서 symbol table을 이용하여 libvector.so를 올려야 한다고 메모해 놓아야 한다. 이 때 주의할 점은, symbol table에 코드를 올리는 것은 아니며, 코드는 실행될 때 올라간다.
'컴퓨터공학 전공공부 > 시스템 프로그래밍' 카테고리의 다른 글
시스템 프로그래밍 chap 8. Process and Virtual Memory (2) | 2023.12.07 |
---|---|
시스템 프로그래밍 chap 6. Machine-Level 프로그래밍 : 데이터 (2) | 2023.12.07 |
시스템 프로그래밍 chap 5. Machine-Level 프로그래밍 과정 (2) | 2023.12.04 |