Що роблять лінкери?


127

Я завжди цікавився. Я знаю, що компілятори перетворюють код, який ви пишете, у двійкові файли, але що роблять лінкери? Вони завжди були для мене загадкою.

Я приблизно розумію, що таке "зв'язок". Це коли до бінарних файлів додаються посилання на бібліотеки та рамки. Я нічого не розумію поза цим. Для мене це "просто працює". Я також розумію основи динамічного зв’язку, але нічого надто глибокого.

Може хтось пояснить умови?

Відповіді:


160

Щоб зрозуміти посилання, це допомагає спершу зрозуміти, що відбувається "під кришкою", коли ви перетворюєте вихідний файл (наприклад, файл C або C ++) у виконуваний файл (виконуваний файл - це файл, який можна виконати на вашій машині або чужа машина з тією ж архітектурою машини).

Під капотом, коли компілюється програма, компілятор перетворює вихідний файл у код об'єкта байта. Цей байт-код (іноді його називають об'єктним кодом) - мнемонічні інструкції, які розуміє лише ваша архітектура комп'ютера. Традиційно ці файли мають розширення .OBJ.

Після створення об’єктного файлу посилання вступає в гру. Частіше за все потрібна справжня програма, яка робить щось корисне, для посилання на інші файли. Наприклад, у програмі C проста програма для друку вашого імені на екрані складається з:

printf("Hello Kristina!\n");

Коли компілятор компілював вашу програму в obj-файл, він просто ставить посилання наprintf функцію. Лінкер вирішує цю посилання. Більшість мов програмування мають стандартну бібліотеку процедур, яка охоплює основні речі, які очікуються від цієї мови. Лінкер пов'язує ваш файл OBJ з цією стандартною бібліотекою. Лінкер також може зв’язати ваш файл OBJ з іншими файлами OBJ. Ви можете створити інші файли OBJ, які мають функції, які можуть бути викликані іншим файлом OBJ. Посилання працює майже як копія та вставка текстового процесора. Він "копіює" всі необхідні функції, на які посилається ваша програма, і створює єдиний виконуваний файл. Іноді інші скопійовані бібліотеки залежать від інших файлів OBJ або бібліотеки. Іноді для того, щоб виконати свою роботу, лінкер повинен бути досить рекурсивним.

Зауважте, що не всі операційні системи створюють єдиний виконуваний файл. Наприклад, Windows використовує DLL, які зберігають всі ці функції разом в одному файлі. Це зменшує розмір виконуваного файлу, але робить ваш виконуваний файл залежним від цих конкретних DLL-файлів. DOS використовував речі, звані Overlays (файли .OVL). Це мало багато цілей, але одна - зберігати загальновживані функції разом у 1 файлі (іншою метою він служив, якщо вам цікаво, - вміти вміщувати великі програми в пам’ять. DOS має обмеження в пам’яті і накладки могли бути "вивантаженим" з пам'яті, а інші накладки можуть бути "завантажені" поверх цієї пам'яті, звідси і назва "накладки"). Linux має спільні бібліотеки, що в основному є такою ж ідеєю, як і DLL (жорсткі основні хлопці Linux, яких я знаю, скажуть мені, що МНОГО ВЕЛИКІ різниці).

Сподіваюся, це допоможе вам зрозуміти!


9
Чудова відповідь. Крім того, більшість сучасних посилань видалить зайві коди, такі як екземпляри шаблонів.
Едвард Странд

1
Це підходяще місце для подолання деяких цих відмінностей?
Джон П

2
Привіт, припустимо, що мій файл не посилається на жоден інший файл. Припустимо, я просто оголошу і ініціалізую дві змінні. Чи буде цей вихідний файл також переходити до посилання?
Мангеш Хердекар

3
@MangeshKherdekar - Так, це завжди проходить через лінкер. Лінкер може не зв’язувати жодної зовнішньої бібліотеки, але фаза зв’язування все ж має відбутися для отримання виконуваного файлу.
Icemanind

78

Мінімальний приклад переміщення адреси

Переїзд адреси - одна з найважливіших функцій зв'язку.

Тож давайте подивимось, як це працює з мінімальним прикладом.

0) Вступ

Короткий зміст: переїзд редагує .textрозділ файлів об'єктів для перекладу:

  • адреса файлу об'єкта
  • на остаточну адресу виконуваного файлу

Це повинен зробити лінкер, оскільки компілятор одночасно бачить лише один вхідний файл, але ми повинні знати про всі об’єктні файли відразу, щоб вирішити, як:

  • вирішувати невизначені символи, такі як оголошені невизначені функції
  • не зіткнення декількох .textі .dataрозділів декількох об'єктних файлів

Передумови: мінімальне розуміння:

Зв'язок не має нічого спільного з C або C ++, зокрема: компілятори просто генерують об'єктивні файли. Потім лінкер приймає їх як вхід, не знаючи ніколи, яка мова їх склала. Це може бути і Фортран.

Отже, щоб зменшити кірку, давайте вивчимо привіт привіт світу NASM x86-64 ELF Linux:

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розділ файлу об’єкта:

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 розділі файлу об'єктів

2) .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що цей запис змінюється.

    Якщо ми оглянемося на декомпільований текст, він знаходиться саме всередині критичного movabs $0x0,%rsi, і ті, хто знає кодування інструкцій x86-64, помітять, що це кодує 64-бітну частину інструкції.

  • Name = .data: адреса вказує на .dataрозділ

  • Type = R_X86_64_64, в якому вказано, що саме потрібно зробити, щоб обчислити адресу.

    Це поле фактично залежить від процесора, і таким чином задокументоване в розділі 4.4 "Переміщення" AMD64 System V ABI .

    У цьому документі сказано, що R_X86_64_64це:

    • Field = word64: 8 байт, таким чином, 00 00 00 00 00 00 00 00адреса0xC

    • Calculation = S + A

      • S- це значення за адресою, що переселяється, таким чином00 00 00 00 00 00 00 00
      • A- додаток, яке 0тут. Це поле запису про переміщення.

      Тож S + A == 0ми переїдемо до першої адреси .dataрозділу.

3) .текст .out

Тепер давайте подивимось на текстову область 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.

І єдине, що в розділі даних - це наш привіт світовий рядок.

Рівень бонусу


1
Чувак, ти чудовий. Посилання на підручник із "глобальної структури файлу ELF" порушено.
Адам Захран

1
@AdamZahran дякую! Дурні URL-адреси сторінок GitHub, які не можуть мати справу з косою рисою!
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

15

На таких мовах, як "C", окремі модулі коду традиційно складаються окремо в крапки об'єктного коду, які готові виконати в будь-якому відношенні, крім того, що всі посилання, які модуль робить поза собою (тобто до бібліотек або інших модулів) ще не вирішено (тобто вони порожні, очікуючи на те, що хтось підійде та зробить усі з'єднання).

Що робить линкер, це переглянути всі модулі разом, подивитися, що потрібно кожному модулю підключити до себе, а також переглянути всі речі, які він експортує. Потім він виправляє все, що складається, і виробляє остаточний виконуваний файл, який потім може бути запущений.

Там, де також триває динамічне посилання, вихід посилання все ще не може бути запущений - все ще є деякі посилання на зовнішні бібліотеки, які ще не вирішені, і вони вирішуються ОС під час завантаження програми (або можливо навіть пізніше під час бігу).


Варто зазначити, що деякі асемблери або компілятори можуть виводити виконуваний файл безпосередньо, якщо компілятор "бачить" все необхідне (як правило, в одному вихідному файлі плюс нічого, що він # включає). Кілька компіляторів, як правило, для невеликих мікросхем, мають єдиний режим роботи.
supercat

Так, я намагався дати відповідь посеред дороги. Звичайно, як і у вашому випадку, навпаки, правда і те, що в деяких видах файлів об'єктів навіть не зроблено повного генерування коду; це робиться лінкером (саме так працює оптимізація програми MSVC).
Буде Дін

@WillDean та Оптимізація зв’язку часу GCC, наскільки я можу сказати, - він передає весь "код" як проміжну мову GIMPLE з необхідними метаданими, робить це доступним для посилання та оптимізує в один кінець в кінці. (Незважаючи на те, що випливає із застарілої документації, тепер за замовчуванням передається лише GIMPLE, а не старий режим "жиру" з обома представленнями об'єктного коду.)
підкреслюється

10

Коли компілятор створює об'єктний файл, він включає записи символів, визначених у цьому об'єктному файлі, та посилання на символи, які не визначені у цьому об'єктному файлі. Лінкер бере їх і з'єднує їх так (коли все працює правильно) всі зовнішні посилання з кожного файлу задовольняються символами, визначеними в інших об’єктних файлах.

Потім він об'єднує всі ці об’єктні файли разом і призначає адреси кожному з символів, і там, де один файл об'єкта має зовнішнє посилання на інший файл об'єкта, він заповнює адресу кожного символу, де б він не використовувався іншим об'єктом. У типовому випадку він також створить таблицю будь-яких використаних абсолютних адрес, тому завантажувач може / "поправить" адреси, коли файл завантажений (тобто додасть базову адресу для кожного з цих адреси, тому всі вони посилаються на правильну адресу пам'яті).

Досить багато сучасних посилань також можуть виконувати деякі (в кількох випадках чимало ) інші "речі", такі як оптимізація коду способами, які можливі лише після того, як всі модулі будуть видимі (наприклад, видалення функцій, які були включені тому що можливо , якийсь інший модуль може викликати їх, але після того, як всі модулі зібрані разом, очевидно, що ніколи їх ніхто не викликає).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.