Apa yang dilakukan linker?

127

Saya selalu bertanya-tanya. Saya tahu bahwa kompiler mengubah kode yang Anda tulis menjadi biner, tetapi apa yang dilakukan linker? Mereka selalu menjadi misteri bagiku.

Secara kasar saya mengerti apa itu 'menghubungkan'. Ini adalah saat referensi ke pustaka dan kerangka kerja ditambahkan ke biner. Saya tidak mengerti apa pun di luar itu. Bagi saya itu "langsung bekerja". Saya juga memahami dasar-dasar penautan dinamis tetapi tidak terlalu dalam.

Bisakah seseorang menjelaskan istilah tersebut?

Kristina Brooks
sumber

Jawaban:

160

Untuk memahami linker, sebaiknya pahami dulu apa yang terjadi "di balik terpal" saat Anda mengonversi file sumber (seperti file C atau C ++) menjadi file yang dapat dijalankan (file yang dapat dieksekusi adalah file yang dapat dijalankan di komputer Anda atau mesin orang lain yang menjalankan arsitektur mesin yang sama).

Di bawah tenda, ketika sebuah program dikompilasi, kompilator mengubah file sumber menjadi kode byte objek. Kode byte ini (terkadang disebut kode objek) adalah instruksi mnemonik yang hanya dimengerti oleh arsitektur komputer Anda. Biasanya, file ini memiliki ekstensi .OBJ.

Setelah file objek dibuat, linker mulai bekerja. Lebih sering daripada tidak, program nyata yang melakukan sesuatu yang berguna perlu mereferensikan file lain. Di C, misalnya, program sederhana untuk mencetak nama Anda ke layar akan terdiri dari:

printf("Hello Kristina!\n");

Ketika kompilator mengkompilasi program Anda ke dalam file obj, itu hanya menempatkan referensi ke printffungsi tersebut. Linker menyelesaikan referensi ini. Sebagian besar bahasa pemrograman memiliki pustaka rutinitas standar untuk mencakup hal-hal dasar yang diharapkan dari bahasa itu. Linker menautkan file OBJ Anda dengan pustaka standar ini. Linker juga dapat menghubungkan file OBJ Anda dengan file OBJ lainnya. Anda dapat membuat file OBJ lain yang memiliki fungsi yang dapat dipanggil oleh file OBJ lain. Linker bekerja hampir seperti copy dan paste pengolah kata. Ini "menyalin" semua fungsi yang diperlukan yang direferensikan oleh program Anda dan membuat satu executable. Terkadang pustaka lain yang disalin bergantung pada OBJ atau file pustaka lainnya. Terkadang penaut harus cukup rekursif untuk melakukan tugasnya.

Perhatikan bahwa tidak semua sistem operasi membuat satu file yang dapat dieksekusi. Windows, misalnya, menggunakan DLL yang menyimpan semua fungsi ini bersama-sama dalam satu file. Ini mengurangi ukuran eksekusi Anda, tetapi membuat eksekusi Anda bergantung pada DLL spesifik ini. DOS dulu menggunakan sesuatu yang disebut Overlay (file .OVL). Ini memiliki banyak tujuan, tetapi salah satunya adalah untuk menjaga fungsi yang umum digunakan bersama dalam 1 file (tujuan lain yang dilayaninya, jika Anda bertanya-tanya, adalah untuk dapat memasukkan program besar ke dalam memori. DOS memiliki batasan dalam memori dan overlay dapat akan "dibongkar" dari memori dan overlay lainnya dapat "dimuat" di atas memori tersebut, oleh karena itu disebut "overlay"). Linux telah berbagi pustaka, yang pada dasarnya ide yang sama dengan DLL (orang-orang Linux hard core yang saya tahu akan memberi tahu saya bahwa ada BANYAK perbedaan BESAR).

Semoga ini bisa membantu Anda memahami!

Icemanind
sumber
9
Jawaban yang bagus. Selain itu, sebagian besar penaut modern akan menghapus kode yang berlebihan seperti contoh templat.
Edward Strange
1
Apakah ini tempat yang tepat untuk membahas beberapa perbedaan itu?
John P
2
Hai, Misalkan file saya tidak mereferensikan file lain. Misalkan saya hanya mendeklarasikan dan menginisialisasi dua variabel. Apakah file sumber ini juga akan digunakan untuk linker?
Mangesh Kherdekar
3
@MangeshKherdekar - Ya, itu selalu melalui linker. Linker mungkin tidak menautkan pustaka eksternal apa pun, tetapi fase penautan masih harus terjadi untuk menghasilkan file yang dapat dieksekusi.
Icemanind
78

Contoh alamat relokasi minimal

Relokasi alamat adalah salah satu fungsi penting dari menghubungkan.

Jadi mari kita lihat cara kerjanya dengan contoh minimal.

0) Pendahuluan

Ringkasan: relokasi mengedit .textbagian file objek yang akan diterjemahkan:

  • alamat file objek
  • ke alamat akhir file yang dapat dieksekusi

Ini harus dilakukan oleh linker karena kompilator hanya melihat satu file input pada satu waktu, tetapi kita harus mengetahui semua file objek sekaligus untuk memutuskan cara:

  • menyelesaikan simbol yang tidak terdefinisi seperti fungsi tak terdefinisi yang dideklarasikan
  • tidak bentrok beberapa .textdan .databagian dari beberapa file objek

Prasyarat: pemahaman minimal tentang:

  • x86-64 atau rakitan IA-32
  • struktur global dari file ELF. Saya telah membuat tutorial untuk itu

Menautkan tidak ada hubungannya dengan C atau C ++ secara khusus: kompiler hanya membuat file objek. Linker kemudian mengambilnya sebagai masukan tanpa pernah mengetahui bahasa apa yang menyusunnya. Mungkin juga Fortran.

Jadi untuk mengurangi kerak, mari pelajari 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

disusun dan dirakit dengan:

nasm -o hello_world.o hello_world.asm
ld -o hello_world.out hello_world.o

dengan NASM 2.10.09.

1) .teks dari .o

Pertama kita mendekompilasi .textbagian dari file objek:

objdump -d hello_world.o

pemberian yang mana:

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

garis krusialnya adalah:

   a:   48 be 00 00 00 00 00    movabs $0x0,%rsi
  11:   00 00 00

yang harus memindahkan alamat string hello world ke dalam rsiregister, yang diteruskan ke panggilan sistem tulis.

Tapi tunggu! Bagaimana mungkin kompilator mengetahui di mana "Hello world!"akan berakhir di memori ketika program dimuat?

Ya, itu tidak bisa, terutama setelah kami menautkan banyak .ofile bersama dengan beberapa .databagian.

Hanya penaut yang dapat melakukannya karena hanya dia yang akan memiliki semua file objek tersebut.

Jadi kompilernya hanya:

  • menempatkan nilai placeholder 0x0pada output yang dikompilasi
  • memberikan beberapa informasi tambahan kepada linker tentang cara mengubah kode yang dikompilasi dengan alamat yang baik

"Informasi tambahan" ini terdapat di .rela.textbagian file objek

2) .rela.text

.rela.text singkatan dari "relokasi bagian teks.".

Kata relokasi digunakan karena linker harus merelokasi alamat dari objek ke file yang dapat dieksekusi.

Kita dapat membongkar .rela.textbagian tersebut dengan:

readelf -r hello_world.o

yang mengandung;

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

Format bagian ini diperbaiki didokumentasikan di: http://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html

Setiap entri memberi tahu linker tentang satu alamat yang perlu direlokasi, di sini kita hanya memiliki satu untuk string.

Sedikit menyederhanakan, untuk baris khusus ini kami memiliki informasi berikut:

  • Offset = C: apa byte pertama .textyang diubah entri ini.

    Jika kita melihat kembali teks yang didekompilasi, itu persis di dalam kritis movabs $0x0,%rsi, dan mereka yang tahu pengkodean instruksi x86-64 akan melihat bahwa ini mengkodekan bagian alamat 64-bit dari instruksi.

  • Name = .data: alamat menunjuk ke .databagian tersebut

  • Type = R_X86_64_64, yang menentukan apa sebenarnya kalkulasi yang harus dilakukan untuk menerjemahkan alamat.

    Bidang ini sebenarnya bergantung pada prosesor, dan karenanya didokumentasikan pada ekstensi AMD64 System V ABI bagian 4.4 "Relokasi".

    Dokumen itu mengatakan bahwa R_X86_64_64:

    • Field = word64: 8 byte, jadi 00 00 00 00 00 00 00 00alamat at0xC

    • Calculation = S + A

      • Sadalah nilai di alamat yang akan direlokasi00 00 00 00 00 00 00 00
      • Aadalah tambahan yang ada di 0sini. Ini adalah bidang entri relokasi.

      Jadi S + A == 0dan kami akan dipindahkan ke alamat pertama dari .databagian tersebut.

3) .teks dari .out

Sekarang mari kita lihat area teks yang dapat dieksekusi yang lddibuat untuk kita:

objdump -d hello_world.out

memberikan:

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

Jadi satu-satunya hal yang berubah dari file objek adalah baris kritis:

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

yang sekarang mengarah ke alamat 0x6000d8( d8 00 60 00 00 00 00 00dalam little-endian), bukan 0x0.

Apakah ini lokasi yang tepat untuk hello_worldstring?

Untuk memutuskan, kita harus memeriksa header program, yang memberi tahu Linux tempat memuat setiap bagian.

Kami membongkar mereka dengan:

readelf -l hello_world.out

pemberian yang mana:

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

Ini memberitahu kita bahwa .databagian, yang kedua, dimulai pada VirtAddr= 0x06000d8.

Dan satu-satunya hal di bagian data adalah string hello world kami.

Tingkat bonus

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
sumber
1
Bung, kamu luar biasa. Tautan ke tutorial 'struktur global file ELF' rusak.
Adam Zahran
1
@AdamZahran terima kasih! URL halaman GitHub bodoh yang tidak dapat menangani garis miring!
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
15

Dalam bahasa seperti 'C', setiap modul kode secara tradisional dikompilasi secara terpisah menjadi gumpalan kode objek, yang siap dieksekusi dalam segala hal selain dari semua referensi yang dibuat modul di luar dirinya (yaitu ke perpustakaan atau ke modul lain). belum terselesaikan (yaitu kosong, menunggu seseorang datang dan membuat semua koneksi).

Apa yang dilakukan linker adalah melihat semua modul bersama-sama, melihat apa yang dibutuhkan setiap modul untuk terhubung ke luar itu sendiri, dan melihat semua hal yang diekspornya. Itu kemudian memperbaiki semuanya, dan menghasilkan eksekusi akhir, yang kemudian dapat dijalankan.

Di mana penautan dinamis juga terjadi, keluaran dari penaut masih belum dapat dijalankan - masih ada beberapa referensi ke pustaka eksternal yang belum diselesaikan, dan mereka diselesaikan oleh OS pada saat memuat aplikasi (atau mungkin bahkan nanti saat berlari).

Will Dean
sumber
Perlu dicatat bahwa beberapa assembler atau compiler dapat mengeluarkan file yang dapat dieksekusi secara langsung jika compiler "melihat" semua yang diperlukan (biasanya dalam satu file sumber plus apa pun yang #termasukinya). Beberapa kompiler, biasanya untuk mikro kecil, memiliki itu sebagai satu-satunya mode operasi mereka.
supercat
Ya, saya mencoba memberikan jawaban di tengah jalan. Tentu saja, seperti halnya kasus Anda, kebalikannya juga benar, bahwa beberapa jenis file objek bahkan tidak memiliki pembuatan kode lengkap; yang dilakukan oleh penaut (begitulah cara kerja pengoptimalan seluruh program MSVC).
Akankah Dean
@WillDean dan GCC's Link-Time Optimization, sejauh yang saya tahu - ini mengalirkan semua 'kode' sebagai bahasa perantara GIMPLE dengan metadata yang diperlukan, membuatnya tersedia untuk linker, dan mengoptimalkan sekaligus di akhir. (Terlepas dari apa yang tersirat dalam dokumentasi usang, hanya GIMPLE yang sekarang dialirkan secara default, daripada mode 'gemuk' lama dengan kedua representasi kode objek.)
underscore_d
10

Ketika kompilator menghasilkan file objek, itu menyertakan entri untuk simbol yang didefinisikan dalam file objek itu, dan referensi ke simbol yang tidak didefinisikan dalam file objek itu. Linker mengambilnya dan menyatukannya sehingga (ketika semuanya bekerja dengan baik) semua referensi eksternal dari setiap file dipenuhi oleh simbol yang ditentukan dalam file objek lain.

Kemudian menggabungkan semua file objek tersebut bersama-sama dan memberikan alamat ke masing-masing simbol, dan di mana satu file objek memiliki referensi eksternal ke file objek lain, itu mengisi alamat setiap simbol di mana pun itu digunakan oleh objek lain. Dalam kasus umum, itu juga akan membangun tabel dari alamat absolut yang digunakan, sehingga loader dapat / akan "memperbaiki" alamat ketika file dimuat (yaitu, itu akan menambahkan alamat pemuatan dasar ke masing-masing alamat sehingga semuanya mengacu pada alamat memori yang benar).

Beberapa linker modern juga dapat melakukan beberapa (dalam beberapa kasus banyak ) "hal" lain, seperti mengoptimalkan kode dengan cara yang hanya mungkin dilakukan setelah semua modul terlihat (misalnya, menghapus fungsi yang disertakan karena ada kemungkinan bahwa beberapa modul lain mungkin memanggilnya, tetapi setelah semua modul disatukan, jelas tidak ada yang memanggilnya).

Jerry Coffin
sumber