developer tip

링커는 무엇을합니까?

optionbox 2020. 8. 9. 10:20
반응형

링커는 무엇을합니까?


나는 항상 궁금했다. 컴파일러가 작성한 코드를 바이너리로 변환한다는 것을 알고 있지만 링커는 무엇을합니까? 그들은 항상 나에게 미스터리였습니다.

나는 '연결'이 무엇인지 대략 이해합니다. 라이브러리 및 프레임 워크에 대한 참조가 바이너리에 추가 될 때입니다. 나는 그 이상을 이해하지 못합니다. 나를 위해 그것은 "그냥 작동한다". 나는 또한 동적 연결의 기본을 이해하지만 너무 깊이는 없습니다.

누군가 용어를 설명 할 수 있습니까?


링커를 이해하려면 먼저 소스 파일 (예 : C 또는 C ++ 파일)을 실행 파일로 변환 할 때 "내부에서"발생하는 일을 이해하는 것이 도움이됩니다 (실행 파일은 컴퓨터에서 실행될 수있는 파일 또는 동일한 시스템 아키텍처를 실행하는 다른 사람의 시스템).

내부적으로 프로그램이 컴파일 될 때 컴파일러는 소스 파일을 객체 바이트 코드로 변환합니다. 이 바이트 코드 (때때로 개체 코드라고 함)는 컴퓨터 아키텍처 만 이해하는 니모닉 명령어입니다. 일반적으로 이러한 파일의 확장자는 .OBJ입니다.

개체 파일이 생성되면 링커가 작동합니다. 종종 유용한 일을하는 실제 프로그램은 다른 파일을 참조해야합니다. 예를 들어 C에서 이름을 화면에 인쇄하는 간단한 프로그램은 다음과 같이 구성됩니다.

printf("Hello Kristina!\n");

컴파일러가 프로그램을 obj 파일로 컴파일 할 때 단순히 printf함수 에 대한 참조를 넣습니다 . 링커는이 참조를 확인합니다. 대부분의 프로그래밍 언어에는 해당 언어에서 예상되는 기본 항목을 다루는 루틴의 표준 라이브러리가 있습니다. 링커는 OBJ 파일을이 표준 라이브러리와 연결합니다. 링커는 OBJ 파일을 다른 OBJ 파일과 연결할 수도 있습니다. 다른 OBJ 파일에서 호출 할 수있는 함수가있는 다른 OBJ 파일을 만들 수 있습니다. 링커는 워드 프로세서의 복사 및 붙여 넣기와 거의 유사합니다. 프로그램이 참조하는 모든 필수 함수를 "복사"하고 단일 실행 파일을 만듭니다. 때때로 복사되는 다른 라이브러리는 다른 OBJ 또는 라이브러리 파일에 종속됩니다. 때로는 링커가 작업을 수행하기 위해 꽤 재귀 적이어야합니다.

모든 운영 체제가 단일 실행 파일을 만드는 것은 아닙니다. 예를 들어 Windows는 이러한 모든 기능을 단일 파일에 함께 보관하는 DLL을 사용합니다. 이렇게하면 실행 파일의 크기가 줄어들지 만 실행 파일이 이러한 특정 DLL에 종속됩니다. DOS는 오버레이 (.OVL 파일)라는 것을 사용했습니다. 이것은 많은 목적을 가지고 있었지만, 하나는 일반적으로 사용되는 기능을 하나의 파일에 함께 보관하는 것이 었습니다 (또 다른 목적은 큰 프로그램을 메모리에 맞출 수 있다는 것이 었습니다. DOS는 메모리에 제한이 있고 오버레이는 할 수 있습니다. 메모리에서 "언로드"되고 다른 오버레이는 해당 메모리 위에 "로드"될 수 있으므로 "오버레이"라는 이름). 리눅스는 공유 라이브러리를 가지고 있는데, 이것은 기본적으로 DLL과 같은 생각입니다 (내가 아는 하드 코어 리눅스 사람들은 많은 큰 차이점이 있다고 말할 것입니다).

이해하는 데 도움이 되었기를 바랍니다.


주소 재배치 최소 예

주소 재배치는 연결의 중요한 기능 중 하나입니다.

이제 최소한의 예제로 어떻게 작동하는지 살펴 보겠습니다.

0) 소개

요약 : 재배치 .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 포함.

1) .o의 .text

먼저 .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섹션 과 함께 여러 파일을 연결 한 후에는 할 수 없습니다 .

Only the linker can do that since only he will have all those object files.

So the compiler just:

  • puts a placeholder value 0x0 on the compiled output
  • gives some extra information to the linker of how to modify the compiled code with the good addresses

This "extra information" is contained in the .rela.text section of the object file

2) .rela.text

.rela.text stands for "relocation of the .text section".

The word relocation is used because the linker will have to relocate the address from the object into the executable.

We can disassemble the .rela.text section with:

readelf -r hello_world.o

which contains;

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

The format of this section is fixed documented at: http://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html

Each entry tells the linker about one address which needs to be relocated, here we have only one for the string.

Simplifying a bit, for this particular line we have the following information:

  • Offset = C: what is the first byte of the .text that this entry changes.

    If we look back at the decompiled text, it is exactly inside the critical movabs $0x0,%rsi, and those that know x86-64 instruction encoding will notice that this encodes the 64-bit address part of the instruction.

  • Name = .data: the address points to the .data section

  • Type = R_X86_64_64, which specifies what exactly what calculation has to be done to translate the address.

    This field is actually processor dependent, and thus documented on the AMD64 System V ABI extension section 4.4 "Relocation".

    That document says that R_X86_64_64 does:

    • Field = word64: 8 bytes, thus the 00 00 00 00 00 00 00 00 at address 0xC

    • Calculation = S + A

      • S is value at the address being relocated, thus 00 00 00 00 00 00 00 00
      • A is the addend which is 0 here. This is a field of the relocation entry.

      So S + A == 0 and we will get relocated to the very first address of the .data section.

3) .text of .out

Now lets look at the text area of the executable ld generated for us:

objdump -d hello_world.out

gives:

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

So the only thing that changed from the object file are the critical lines:

  4000ba:   48 be d8 00 60 00 00    movabs $0x6000d8,%rsi
  4000c1:   00 00 00

which now point to the address 0x6000d8 (d8 00 60 00 00 00 00 00 in little-endian) instead of 0x0.

Is this the right location for the hello_world string?

To decide we have to check the program headers, which tell Linux where to load each section.

We disassemble them with:

readelf -l hello_world.out

which gives:

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

This tells us that the .data section, which is the second one, starts at VirtAddr = 0x06000d8.

And the only thing on the data section is our hello world string.

Bonus level


In languages like 'C', individual modules of code are traditionally compiled separately into blobs of object code, which is ready to execute in every respect other than that all the references that module makes outside itself (i.e. to libraries or to other modules) have not yet been resolved (i.e. they're blank, pending someone coming along and making all the connections).

What the linker does is to look at all the modules together, look at what each module needs to connect to outside itself, and look at all the things it is exporting. It then fixes that all up, and produces a final executable, which can then be run.

Where dynamic linking is also going on, the output of the linker is still not capable of being run - there are still some references to external libraries not yet resolved, and they get resolved by the OS at the time it loads the app (or possibly even later during the run).


When the compiler produces an object file, it includes entries for symbols that are defined in that object file, and references to symbols that aren't defined in that object file. The linker takes those and puts them together so (when everything works right) all the external references from each file are satisfied by symbols that are defined in other object files.

It then combines all those object files together and assigns addresses to each of the symbols, and where one object file has an external reference to another object file, it fills in the address of each symbol wherever it's used by another object. In a typical case, it'll also build a table of any absolute addresses used, so the loader can/will "fix up" the addresses when the file is loaded (i.e., it'll add the base load address to each of those addresses so they all refer to the correct memory address).

Quite a few modern linkers can also carry out some (in a few cases a lot) of other "stuff", such as optimizing the code in ways that are only possible once all of the modules are visible (e.g., removing functions that were included because it was possible that some other module might call them, but once all the modules are put together it's apparent that nothing ever calls them).

참고URL : https://stackoverflow.com/questions/3322911/what-do-linkers-do

반응형