7. Linking
이번에 배울 Linking은 시스템 프로그래밍에서 대단히 중요한 역할을 수행하는 Linker(링커)가 하는 일이다.
이 단원에서 가장 중요한 것은 "다른 파일에 있는 함수를 어떻게 가져올 것인가?"가 문제가 되는데 천천히 살펴보자.
1. C 프로그래밍에서의 Linking
C언어에서의 Linking 코드이다.
extern 키워드를 이용해 main.c 파일에 있는 buf 배열을 가져와 사용하는 것을 볼 수 있다.
우리는 이 코드를 직전 단원에서 배운 C 코드를 어셈블러로 변환하는 방법을 이용해 변환해 볼 것이다.
그럼 한 번 불러 볼까요?
짜잔! 어셈블러로 바뀌었다!
왼쪽 코드를 실행하면 오른쪽 데이터가 저장되는데, 이전에 보지 못했던 명령어가 보인다.
movw, movt 란?
기존 32비트를 움직일 수 없는 mov 명령어에 대한 해결책으로
movw, movt 두 조각으로 32비트를 마치 한번에 바꾸는 것처럼 해주는 명령어이다.
- movw : 16비트 상수를 레지스터로 옮겨준다. (이건 mov와 같다.)
- movt : 위에서부터 16비트 상수를 레지스터로 옮기되, 아래 16비트 상수는 유지한다.
코드
아래 두 명령어를 실행하면, #10000000을 r1 레지스터에 옮겨 담을 수 있다.
여기서 중요한 건, 반드시 movw -> movt 순서대로 실행되어야 한다는 점인데,
movt에는 아래 비트를 고려하는 과정이 들어가 있지만, movw는 상위 비트를 없애고 저장하기 때문이다.
Static Linking
코드
main.o 와 swap.o 두 파일을 합쳐주는 코드이다.
위에 두 줄은 합치기 전에 위치를 바꿀 수 있도록(relocatable 이라 표현한다.) 따로 컴파일하는 과정인데, 따로 자세히 살펴보자.
- -marm : arm code로 만들어주는 역할을 한다. 이 명령어가 없을 시 thumb모드가 될 수도 있다.
- -fno-section-anchors : 절대값을 사용해 포인터를 지정해준다. (이해하기 쉽도록)
- -g : 디버그 시 심볼을 보여주는 역할을 한다.
- -c : 다른 것 없이 컴파일만 하라는 옵션이다.
이제 이 두 줄의 코드를 실행하여 relocatable object file로 변환 후 3번째 코드를 실행하면 두 object file을 linker가 합쳐주게 된다.
2. 왜 링커를 사용하는가?
링커를 사용하는 이유, 즉 장점은 다음과 같다.
- Modularity (모듈화 가능)
-> 큰 프로젝트를 할 경우 많은 소스 파일들을 하나로 만드는 것은 굉장히 비효율적이기 때문에 작은 여러 개의 파일로 나누어 한꺼번에 합칠 수 있도록 한다. 이렇게 되면 각 파일들이 가벼워지기 때문에 다루기 쉬워지고, 추가로 라이브러리도 만들 수 있게 된다. - Efficiency (효율성)
-> 시간적으로 컴파일 시간을 줄일 수 있고, 공간적으로는 코드의 중복을 막을 수 있어 코드가 짧아지게 된다.
3. 링커가 하는 일
1) symbol resolution
링커는 심벌과 주소 값을 매칭시켜놓는 심볼 테이블을 구현해 주소값을 해결해 줄 수 있다.
2) Relocation
보통 object 파일을 만들 때에는, 심벌마다 상대 값(relative location)을 가지고 있도록 하는데,
링커는 이를 바탕으로 합친 후, 각자 다른 상대 주소 값으로 재 지정하는 역할을 한다. (겹치지 않기 위함)
위 그림은 시스템 코드, main, swap을 보여준다.
(시스템 코드는 기본적으로 있는 것)
메인에는 buf array / swap에는 buf pointer가. data에 들어가 있다.
data section에는. data와. bss가 있는데,. bss는 초기화되지 않은 변수가 들어가 있다.
→ 이 둘을 합치면 초기에 코드는 코드대로,. data는 data끼리 합치게 된다. 이것이 executable obj file이고, 맨 위에 이게 무엇인지 설명해주는 헤더가 포함되어있다.
4. 3종류의 오브젝트 파일
- Relocatable object file (. o file) : 코드와 데이터를 포함하고 있다.
- Executable object file (a.out file) : 이것은 바이너리 코드와 데이터를 가지고 있으며, 메모리로 직접 로드되어 실행될 수 있는 것을 가리킨다. (-o 옵션을 주지 않으면 디폴트 값으로 이게 생성된다.)
- Shared object file (.so file) : 특정 기능을 구현해 놓은 파일을 의미한다. DLL(Dynamic Link Libraries)라고도 한다.
위 3가지 파일은 ELF(Executable and Linkable Format) 형식을 따르는데, 형식 내용은 다음과 같다.
- ELF header : word size, byte ordering(litter endian, big endian), file type(.o, .so, exec)
- Segment header table : Page size, virtual addr memory segments, segment size
- .text section : 코드
- .rodata section : 읽기 전용(read only data) ex. jump table처럼 jump 할 주소 값으로 이루어진 table
- .data section : read, write가 가능. .data는 초기화가 가능한 전역 변수가 들어간다. (링커가 관리를 해주어야 할 변수)
- .bss section : 전역 변수 중 초기화가 되지 않은 것들이 들어간다.
- symtab section : 함수의 이름들이나 section 등이 들어간다.
- .rel.text section : .text section에 대한 relocation info가 들어간다. 이건 exec 파일이 만들어질 때 여기서 정보 참조
- .rel.data section : .data section (buffer pointer 등) 참조하는 정보가 들어간다.
- .debug section : gcc -g 옵션처럼 디버깅을 할 때는 symbol을 보면서 하면 좋기 때문에 여기서 symbol 정보를 갖고 있다.
- Section header table : 각 섹션의 옵션 및 크기에 대한 정보를 담고 있다.
Linker symbol의 종류
링커가 사용하는 심벌의 종류는 다음과 같다.
- Global symbols : 다른 쪽에서 참조할 수 있도록 하는 것.
- External symbol : 다른 모듈에 정의되어있고, 내가 참조하는 것
- Local symbol : 다른 모듈은 모르고 나만 아는 심벌. Local linker symbol은 링커가 관여하는 데 이 모듈 안에서만 관여한다는 뜻이지 지역변수 같은 느낌이랑 다르다.
전역 변수를 많이 사용하게 되면 어떤 문제가 발생할까?
- 메모리 오버헤드 : 어떤 처리를 하기 위해 들어가는 메모리 (불필요한)
- Linking 오버헤드 : 재정의로 인해 발생
-> 전역변수를 가급적 적게 쓰는 방법으로는, 레지스터와 스택을 많이 쓰는 방법이 있다.
전역변수를 스택으로 처리하는 방법에 대한 예시
왼쪽 방법은 buf라는 20바이트 크기의 전역 변수에 값을 저장하고 사용하는 코드이다.
방법은 간단하다.
1) 20바이트만큼 스택을 비우고 (sub sp, sp, #20)
2) buf에 넣는 값 그대로 스택에 저장한 후 (mov r10, sp)
3) 사용이 끝나면 아까 비운 그대로 스택을 다시 채워두면 된다. (add sp, sp, #20)
5. 라이브러리
이제 많은 오브젝트 파일을 다루는 법도 알았고, 합치는 법도 알았다.
그럼 이걸 쓰는 최종적인 이유! 라이브러리에 대해 알아보자.
라이브러리의 역사
많은 사람들은 고민해왔다. 여러 파일들의 많은 기능들을 어디다 저장할까?
그 고민의 결과 세 가지 방법이 나왔다.
- 하나의 파일에 다 때려 박자! -> 파일이 너무 무거워진다.
- 각각의 기능들을 분할된 소스파일에 넣자! -> 괜찮은 방법이나 각 파일을 유지 보수하기 힘들다.
- 서로 관계가 있는 파일들을 모아 두자! -> 라이브러리
-> 라이브러리는 서로 관계있는 object 파일을 모아두어, 링커로 하여금 이를 다룰 수 있게 만드는 것이다.
라이브러리를 만드는 과정
라이브러리는 다음 과정을 통해 만들어진다.
소스 파일들을 오브젝트 파일로 각각 바꾼 뒤, 링커를 통해 합친다.
명령어
- 라이브러리 생성 및 수정 시 : [ar rs "라이브러리 이름" "object file1" "object file2" "..." ]
- 라이브러리 내부 파일 보는 법 : [ ar -t "라이브러리 이름" ]
6. Shared Libraries
위에서 설명한 라이브러리는 static 라이브러리로, 몇 가지 단점이 존재한다.
바로 static 한 특성인데, 이는 프로그램을 하나로만 본다는 특징이 있다. 따라서 만약 여러 개의 프로그램이 동시에 동작한다면, 똑같은 함수가 여러개 필요하므로 복제를 해야 한다.
하지만 shared 라이브러리는 따로 공동 map을 두고, 거기에 함수를 놓는다.
만약 동시에 동작하는 여러 프로그램들이 같은 함수를 부르더라도 참조를 한 번만 하게 된다는 장점이 있다.
Shared 라이브러리의 링킹 과정
static 라이브러리의 링킹 과정과 다른 점은 못 생기게 동그라미 친 두 부분이다.
윗부분 linker에는 라이브러리의 코드와 데이터가 들어가지 않고, 심벌 테이블 정보만 넣어준다.
이후 마지막 Dynamic linker 부분에서 Loading을 할 때 코드와 데이터를 넣음으로써 최종적으로 Load 할 때만 코드가 들어간다는 차이가 있다.
문제점
이렇게 각각 다른 프로그램이 똑같은 라이브러리를 사용할 때 주소 값이 다른 문제점이 발생하지 않나?
방법 : relocation을 하지 않고, 전부 상대 주소를 갖도록 하면 해당 문제를 해결할 수 있다!