답변:
링커를 이해하려면 먼저 소스 파일 (예 : C 또는 C ++ 파일)을 실행 파일 (실행 파일은 컴퓨터에서 실행할 수있는 파일 또는 동일한 시스템 아키텍처를 실행하는 다른 사람의 시스템).
내부적으로 프로그램이 컴파일 될 때 컴파일러는 소스 파일을 객체 바이트 코드로 변환합니다. 이 바이트 코드 (때때로 개체 코드라고 함)는 컴퓨터 아키텍처 만 이해하는 니모닉 명령어입니다. 일반적으로 이러한 파일의 확장자는 .OBJ입니다.
개체 파일이 생성되면 링커가 작동합니다. 종종 유용한 일을하는 실제 프로그램은 다른 파일을 참조해야합니다. 예를 들어 C에서 이름을 화면에 인쇄하는 간단한 프로그램은 다음과 같이 구성됩니다.
printf("Hello Kristina!\n");
컴파일러가 프로그램을 obj 파일로 컴파일 할 때 단순히 printf
함수 에 대한 참조를 넣습니다 . 링커는이 참조를 확인합니다. 대부분의 프로그래밍 언어에는 해당 언어에서 예상되는 기본 항목을 다루는 루틴의 표준 라이브러리가 있습니다. 링커는 OBJ 파일을이 표준 라이브러리와 연결합니다. 링커는 OBJ 파일을 다른 OBJ 파일과 연결할 수도 있습니다. 다른 OBJ 파일에서 호출 할 수있는 함수가있는 다른 OBJ 파일을 만들 수 있습니다. 링커는 워드 프로세서의 복사 및 붙여 넣기와 거의 유사합니다. 프로그램이 참조하는 모든 필수 함수를 "복사"하고 단일 실행 파일을 만듭니다. 때때로 복사되는 다른 라이브러리는 다른 OBJ 또는 라이브러리 파일에 종속됩니다. 때로는 링커가 작업을 수행하기 위해 꽤 재귀 적이어야합니다.
모든 운영 체제가 단일 실행 파일을 만드는 것은 아닙니다. 예를 들어 Windows는 이러한 모든 기능을 단일 파일에 함께 보관하는 DLL을 사용합니다. 이렇게하면 실행 파일의 크기가 줄어들지 만 실행 파일이 이러한 특정 DLL에 종속됩니다. DOS는 오버레이 (.OVL 파일)라는 것을 사용했습니다. 이것은 많은 목적을 가지고 있었지만, 하나는 일반적으로 사용되는 기능을 하나의 파일에 함께 보관하는 것이 었습니다. 메모리에서 "언로드"되고 다른 오버레이는 해당 메모리 위에 "로드"될 수 있으므로 "오버레이"라는 이름). 리눅스는 공유 라이브러리를 가지고 있는데, 이것은 기본적으로 DLL과 같은 생각입니다 (내가 아는 하드 코어 리눅스 사람들은 많은 큰 차이점이 있다고 말할 것입니다).
이해하는 데 도움이 되었기를 바랍니다.
주소 재배치는 연결의 중요한 기능 중 하나입니다.
이제 최소한의 예제로 어떻게 작동하는지 살펴 보겠습니다.
요약 : 재배치 .text
는 번역 할 개체 파일 의 섹션을 편집합니다 .
컴파일러는 한 번에 하나의 입력 파일 만 볼 수 있기 때문에 링커에서 수행해야하지만 다음 방법을 결정하려면 모든 개체 파일을 한 번에 알아야합니다.
.text
과 충돌하지 않음.data
전제 조건 : 최소한의 이해 :
링크는 C 또는 C ++와 특별히 관련이 없습니다. 컴파일러는 객체 파일 만 생성합니다. 그런 다음 링커는 컴파일 된 언어를 알지 못한 채 입력으로 가져옵니다. Fortran 일 수도 있습니다.
따라서 크러스트를 줄이기 위해 NASM x86-64 ELF Linux hello world를 연구 해 보겠습니다.
section .data
hello_world db "Hello world!", 10
section .text
global _start
_start:
; sys_write
mov rax, 1
mov rdi, 1
mov rsi, hello_world
mov rdx, 13
syscall
; sys_exit
mov rax, 60
mov rdi, 0
syscall
컴파일 및 조립 :
nasm -o hello_world.o hello_world.asm
ld -o hello_world.out hello_world.o
NASM 2.10.09 포함.
먼저 .text
개체 파일 의 섹션을 디 컴파일 합니다.
objdump -d hello_world.o
다음을 제공합니다.
0000000000000000 <_start>:
0: b8 01 00 00 00 mov $0x1,%eax
5: bf 01 00 00 00 mov $0x1,%edi
a: 48 be 00 00 00 00 00 movabs $0x0,%rsi
11: 00 00 00
14: ba 0d 00 00 00 mov $0xd,%edx
19: 0f 05 syscall
1b: b8 3c 00 00 00 mov $0x3c,%eax
20: bf 00 00 00 00 mov $0x0,%edi
25: 0f 05 syscall
중요한 라인은 다음과 같습니다.
a: 48 be 00 00 00 00 00 movabs $0x0,%rsi
11: 00 00 00
hello world 문자열의 주소를 rsi
레지스터 로 이동해야하며 , 이는 쓰기 시스템 호출로 전달됩니다.
하지만 기다려! "Hello world!"
프로그램이로드 될 때 컴파일러 가 메모리에서 끝날 위치를 어떻게 알 수 있습니까?
글쎄요, 특히 우리 .o
가 여러 .data
섹션 과 함께 여러 파일을 연결 한 후에는 할 수 없습니다 .
링커 만이 모든 객체 파일을 가지므로이를 수행 할 수 있습니다.
따라서 컴파일러는 다음과 같습니다.
0x0
컴파일 된 출력에 자리 표시 자 값 을 넣습니다.이 "추가 정보"는 .rela.text
개체 파일 의 섹션에 포함되어 있습니다.
.rela.text
".text 섹션 재배치"를 의미합니다.
링커가 개체의 주소를 실행 파일로 재배치해야하기 때문에 재배치라는 단어가 사용됩니다.
다음을 사용하여 .rela.text
섹션을 분해 할 수 있습니다 .
readelf -r hello_world.o
포함하는;
Relocation section '.rela.text' at offset 0x340 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000000c 000200000001 R_X86_64_64 0000000000000000 .data + 0
이 섹션의 형식은 http://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html에 문서화되어 있습니다.
각 항목은 재배치해야하는 하나의 주소에 대해 링커에게 알려줍니다. 여기서는 문자열에 대해 하나만 있습니다.
이 특정 라인에 대해 약간 단순화하면 다음 정보가 있습니다.
Offset = C
: .text
이 항목이 변경 되는 첫 번째 바이트는 무엇입니까 ?
디 컴파일 된 텍스트를 되돌아 보면 정확히 critical 내부에 있으며 movabs $0x0,%rsi
x86-64 명령어 인코딩을 알고있는 사용자는 이것이 명령어의 64 비트 주소 부분을 인코딩한다는 것을 알 수 있습니다.
Name = .data
: 주소가 .data
섹션을 가리킴
Type = R_X86_64_64
, 주소를 변환하기 위해 수행해야하는 계산을 정확히 지정합니다.
이 필드는 실제로 프로세서에 따라 다르 므로 AMD64 System V ABI 확장 섹션 4.4 "재배치"에 문서화되어 있습니다.
그 문서는 다음과 같이 말합니다 R_X86_64_64
.
Field = word64
: 8 바이트, 따라서 00 00 00 00 00 00 00 00
at 주소0xC
Calculation = S + A
S
되는 값 의 어드레스에 따라서, 재배치되고00 00 00 00 00 00 00 00
A
0
여기에 있는 부록입니다 . 재배치 항목의 필드입니다.그래서 S + A == 0
우리는 .data
섹션의 첫 번째 주소로 재배치됩니다 .
이제 ld
생성 된 실행 파일의 텍스트 영역을 살펴 보겠습니다 .
objdump -d hello_world.out
제공합니다 :
00000000004000b0 <_start>:
4000b0: b8 01 00 00 00 mov $0x1,%eax
4000b5: bf 01 00 00 00 mov $0x1,%edi
4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi
4000c1: 00 00 00
4000c4: ba 0d 00 00 00 mov $0xd,%edx
4000c9: 0f 05 syscall
4000cb: b8 3c 00 00 00 mov $0x3c,%eax
4000d0: bf 00 00 00 00 mov $0x0,%edi
4000d5: 0f 05 syscall
따라서 개체 파일에서 변경된 유일한 것은 중요한 줄입니다.
4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi
4000c1: 00 00 00
이제 주소 0x6000d8
( d8 00 60 00 00 00 00 00
리틀 엔디안)를 가리 킵니다 0x0
.
이것이 hello_world
문자열 의 올바른 위치 입니까?
결정하려면 프로그램 헤더를 확인해야합니다. 이는 Linux에 각 섹션을로드 할 위치를 알려줍니다.
다음과 같이 분해합니다.
readelf -l hello_world.out
다음을 제공합니다.
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000000d7 0x00000000000000d7 R E 200000
LOAD 0x00000000000000d8 0x00000000006000d8 0x00000000006000d8
0x000000000000000d 0x000000000000000d RW 200000
Section to Segment mapping:
Segment Sections...
00 .text
01 .data
이것은 .data
두 번째 섹션 인 섹션이 VirtAddr
= 에서 시작 한다는 것을 알려줍니다 0x06000d8
.
데이터 섹션의 유일한 것은 hello world 문자열입니다.
'C'와 같은 언어에서 개별 코드 모듈은 전통적으로 개체 코드의 덩어리로 개별적으로 컴파일되며, 모듈이 자체 외부에서 만드는 모든 참조 (즉, 라이브러리 또는 다른 모듈)가 갖는 것 이외의 모든 측면에서 실행할 준비가되어 있습니다. 아직 해결되지 않았습니다 (즉, 누군가가 와서 모든 연결을 만들기를 기다리고있는 공백입니다).
링커가하는 일은 모든 모듈을 함께 살펴보고, 각 모듈이 외부에 연결하는 데 필요한 사항을 살펴보고, 내보내는 모든 항목을 살펴 보는 것입니다. 그런 다음 모든 문제를 수정하고 실행 가능한 최종 실행 파일을 생성합니다.
동적 연결도 진행되는 경우 링커의 출력은 여전히 실행될 수 없습니다. 아직 확인되지 않은 외부 라이브러리에 대한 참조가 여전히 있고 앱을로드 할 때 OS에서 확인합니다 (또는 가능하면 나중에 실행 중).
컴파일러가 개체 파일을 생성하면 해당 개체 파일에 정의 된 기호에 대한 항목과 해당 개체 파일에 정의되지 않은 기호에 대한 참조가 포함됩니다. 링커는 그것들을 취해 합쳐서 (모든 것이 제대로 작동 할 때) 각 파일의 모든 외부 참조가 다른 개체 파일에 정의 된 기호로 충족됩니다.
그런 다음 모든 개체 파일을 함께 결합하고 각 심볼에 주소를 할당하고, 한 개체 파일에 다른 개체 파일에 대한 외부 참조가있는 경우 다른 개체가 사용하는 모든 위치에 각 심볼의 주소를 채 웁니다. 일반적인 경우에는 사용 된 절대 주소 테이블도 작성하므로 로더는 파일이로드 될 때 주소를 "수정"할 수 있습니다 (즉, 각 주소에 기본로드 주소를 추가합니다). 주소가 모두 올바른 메모리 주소를 참조하도록합니다).
꽤 많은 현대 링커도 수행 할 수 있습니다 (몇 가지 경우 A의 일부 많은 등 (모든 모듈을 볼 수 있습니다 번만 가능 방법으로 코드를 최적화하는 등의 다른 "물건"의) 예를 들어, 포함되었다 제거 기능 이 때문에 가능한 한 다른 모듈을 호출 할 수도 있지만, 모든 모듈이 조립되면 그것은 아무것도 이제까지)를 호출 없다는 것을 알 수 있습니다.