Bağlayıcılar ne yapar?


127

Hep merak etmişimdir. Derleyicilerin yazdığınız kodu ikililere dönüştürdüğünü biliyorum ama bağlayıcılar ne yapar? Benim için hep bir sır oldular.

'Bağlantının' ne olduğunu kabaca anlıyorum. Kütüphanelere ve çerçevelere yapılan referansların ikiliye eklendiği zamandır. Bunun ötesinde hiçbir şey anlamıyorum. Benim için "sadece işe yarıyor". Dinamik bağlantının temellerini de anlıyorum ama çok derin değil.

Birisi şartları açıklayabilir mi?

Yanıtlar:


160

Bağlayıcıları anlamak için, bir kaynak dosyayı (bir C veya C ++ dosyası gibi) çalıştırılabilir bir dosyaya (yürütülebilir dosya, makinenizde veya makinenizde çalıştırılabilen bir dosyadır) aynı makine mimarisini çalıştıran başka birinin makinesi).

Başlık altında, bir program derlendiğinde, derleyici kaynak dosyayı nesne bayt koduna dönüştürür. Bu bayt kodu (bazen nesne kodu da denir), yalnızca bilgisayar mimarinizin anladığı anımsatıcı talimatlardır. Geleneksel olarak, bu dosyalar .OBJ uzantısına sahiptir.

Nesne dosyası oluşturulduktan sonra, bağlayıcı devreye girer. Çoğu zaman, yararlı herhangi bir şey yapan gerçek bir programın diğer dosyalara başvurması gerekecektir. Örneğin C'de, adınızı ekrana yazdırmak için basit bir program şunlardan oluşur:

printf("Hello Kristina!\n");

Derleyici programınızı bir obj dosyasına derlediğinde, sadece printfişleve bir referans koyar . Bağlayıcı, bu referansı çözer. Çoğu programlama dilinin, o dilden beklenen temel şeyleri kapsayacak standart bir yordam kitaplığı vardır. Bağlayıcı, OBJ dosyanızı bu standart kitaplığa bağlar. Bağlayıcı, OBJ dosyanızı diğer OBJ dosyalarıyla da bağlayabilir. Başka bir OBJ dosyası tarafından çağrılabilen işlevlere sahip başka OBJ dosyaları oluşturabilirsiniz. Bağlayıcı, neredeyse bir kelime işlemcinin kopyalayıp yapıştırması gibi çalışır. Programınızın referans verdiği tüm gerekli işlevleri "kopyalar" ve tek bir yürütülebilir dosya oluşturur. Bazen, kopyalanan diğer kitaplıklar yine de diğer OBJ veya kitaplık dosyalarına bağımlıdır. Bazen bir bağlayıcının işini yapmak için oldukça özyinelemeli olması gerekir.

Tüm işletim sistemlerinin tek bir yürütülebilir dosya oluşturmadığını unutmayın. Örneğin Windows, tüm bu işlevleri tek bir dosyada bir arada tutan DLL'leri kullanır. Bu, yürütülebilir dosyanın boyutunu azaltır, ancak yürütülebilir dosyanızı bu belirli DLL'lere bağımlı hale getirir. DOS, Overlays (.OVL dosyaları) adı verilen şeyleri kullanırdı. Bunun birçok amacı vardı, ancak bir tanesi yaygın olarak kullanılan işlevleri bir dosyada bir arada tutmaktı (merak ediyorsanız hizmet ettiği başka bir amaç, büyük programları belleğe sığdırabilmekti. DOS'un bellekte bir sınırlaması vardır ve kaplamalar olabilir bellekten "kaldırılmış" olabilir ve diğer kaplamalar bu belleğin üzerine "yüklenebilir", dolayısıyla adı "kaplamalar"). Linux, temelde DLL'lerle aynı fikir olan paylaşımlı kitaplıklara sahiptir (tanıdığım sabit çekirdekli Linux adamları bana BİRÇOK BÜYÜK farklılık olduğunu söylerlerdi).

Umarım bu anlamanıza yardımcı olur!


9
Mükemmel cevap. Ek olarak, modern bağlayıcıların çoğu, şablon örnekleri gibi gereksiz kodları kaldıracaktır.
Edward Strange

1
Bu farklılıklardan bazılarının üzerinden geçmek için uygun bir yer burası mı?
John P

2
Merhaba, Dosyamın başka herhangi bir dosyaya başvurmadığını varsayalım. Farz edelim ki basitçe iki değişkeni açıklayıp başlatıyorum. Bu kaynak dosya da bağlayıcıya gidecek mi?
Mangesh Kherdekar

3
@MangeshKherdekar - Evet, her zaman bir bağlayıcıdan geçer. Bağlayıcı herhangi bir harici kitaplığı bağlamayabilir, ancak bağlanma aşamasının bir yürütülebilir dosya üretmek için hala gerçekleşmesi gerekir.
Icemanind

78

Adres değiştirme minimal örneği

Adresin yeniden yerleştirilmesi, bağlamanın en önemli işlevlerinden biridir.

Öyleyse minimal bir örnekle nasıl çalıştığına bir bakalım.

0) Giriş

Özet: yer değiştirme, çevrilecek .textnesne dosyalarının bölümünü düzenler :

  • nesne dosyası adresi
  • yürütülebilir dosyanın son adresine

Bu, bağlayıcı tarafından yapılmalıdır çünkü derleyici bir seferde yalnızca bir girdi dosyası görür, ancak nasıl yapılacağına karar vermek için tüm nesne dosyalarını aynı anda bilmemiz gerekir:

  • tanımlı tanımsız işlevler gibi tanımsız sembolleri çözer
  • birden çok nesne dosyasının birden çok bölümünü .textve .databölümünü çakışmaz

Ön koşullar: asgari düzeyde anlayış:

Bağlamanın özellikle C veya C ++ ile ilgisi yoktur: derleyiciler sadece nesne dosyalarını oluşturur. Bağlayıcı daha sonra onları hangi dilin derlediğini bilmeden girdi olarak alır. Fortran da olabilir.

Kabuğu azaltmak için, bir NASM x86-64 ELF Linux merhaba dünyasını inceleyelim:

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

derlenmiş ve derlenmiş:

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

NASM 2.10.09 ile.

1) .o metni

İlk .textönce nesne dosyasının bölümünü derledik :

objdump -d hello_world.o

hangi verir:

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

önemli satırlar:

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

hello world dizgisinin adresini rsiyazma sistem çağrısına iletilen yazmacıya taşımalıdır .

Fakat bekle! Derleyici "Hello world!", program yüklendiğinde bellekte nereye gideceğini nasıl bilebilir ?

Pekala, özellikle bir grup .odosyayı birden çok .databölümle birbirine bağladıktan sonra olamaz .

Tüm bu nesne dosyalarına yalnızca o sahip olacağından, bunu yalnızca bağlayıcı yapabilir.

Yani derleyici sadece:

  • 0x0derlenen çıktıya bir yer tutucu değeri koyar
  • derlenmiş kodun iyi adreslerle nasıl değiştirileceğine dair bağlayıcıya bazı ekstra bilgiler verir

Bu "ek bilgi", .rela.textnesne dosyasının bölümünde bulunur

2) .rela.text

.rela.text ".text bölümünün yeniden konumlandırılması" anlamına gelir.

Yeniden konumlandırma kelimesi, bağlayıcının adresi nesneden yürütülebilir dosyaya yeniden konumlandırması gerekeceğinden kullanılır.

.rela.textBölümü şu şekilde demonte edebiliriz :

readelf -r hello_world.o

içerir;

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

Bu bölümün biçimi şu adreste sabitlenmiştir: http://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html

Her giriş, bağlayıcıya yeniden konumlandırılması gereken bir adres hakkında bilgi verir, burada dize için yalnızca bir adresimiz vardır.

Biraz sadeleştirmek gerekirse, bu belirli satır için aşağıdaki bilgilere sahibiz:

  • Offset = C: .textbu girişin değiştirdiği ilk bayt nedir .

    Derlenmiş metne geri dönüp bakarsak, tam olarak kritik movabs $0x0,%rsiolanın içindedir ve x86-64 komut kodlamasını bilenler, bunun komutun 64 bit adres bölümünü kodladığını fark edeceklerdir.

  • Name = .data: adres .databölümü işaret eder

  • Type = R_X86_64_64, adresi çevirmek için tam olarak hangi hesaplamanın yapılması gerektiğini belirtir.

    Bu alan aslında işlemciye bağlıdır ve bu nedenle AMD64 System V ABI uzantısı bölüm 4.4 "Yer Değiştirme" belgesinde belgelenmiştir .

    Bu belge şunu söylüyor R_X86_64_64:

    • Field = word64: 8 bayt, dolayısıyla 00 00 00 00 00 00 00 00adres0xC

    • Calculation = S + A

      • Solduğu değer adresinde böylece Yeniden yerleştirilen00 00 00 00 00 00 00 00
      • Aburada bulunan eklentidir 0. Bu, yer değiştirme girişinin bir alanıdır.

      Böylece S + A == 0, .databölümün ilk adresine taşınacağız .

3) .out metni

Şimdi ldbizim için oluşturulan çalıştırılabilir dosyanın metin alanına bakalım:

objdump -d hello_world.out

verir:

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

Dolayısıyla, nesne dosyasından değişen tek şey kritik satırlardır:

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

şimdi yerine adresi 0x6000d8( d8 00 60 00 00 00 00 00little-endian olarak) gösterir 0x0.

Bu hello_worlddizi için doğru konum mu?

Karar vermek için Linux'a her bölümü nereye yükleyeceğini söyleyen program başlıklarını kontrol etmeliyiz.

Onları şu şekilde parçalara ayırıyoruz:

readelf -l hello_world.out

hangi verir:

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

Bu bize .dataikinci olan bölümün VirtAddr= ile başladığını söyler 0x06000d8.

Ve veri bölümündeki tek şey merhaba dünya dizimizdir.

Bonus seviyesi


1
Dostum harikasın. 'Bir ELF dosyasının genel yapısı' öğreticisinin bağlantısı kesildi.
Adam Zahran

1
@AdamZahran teşekkürler! Eğik çizgilerle başa çıkamayan aptal GitHub sayfaları URL'leri!
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

15

'C' gibi dillerde, ayrı ayrı kod modülleri geleneksel olarak ayrı ayrı nesne kodu blobları halinde derlenir; bu, modülün kendi dışında yaptığı tüm referansların (yani kitaplıklara veya diğer modüllere) sahip olması dışında her açıdan uygulamaya hazırdır. henüz çözülmedi (yani boşlar, birinin gelmesini ve tüm bağlantıları yapmasını bekliyorlar).

Bağlayıcının yaptığı şey, tüm modüllere birlikte bakmak, her modülün kendi dışına bağlanmak için neye ihtiyaç duyduğuna bakmak ve dışa aktardığı her şeye bakmaktır. Daha sonra tüm bunları düzeltir ve daha sonra çalıştırılabilen son bir yürütülebilir dosya üretir.

Dinamik bağlantının da devam ettiği yerlerde, bağlayıcının çıktısı hala çalıştırılamaz - hala çözülmemiş harici kitaplıklara bazı referanslar vardır ve bunlar, uygulamayı yüklerken işletim sistemi tarafından çözülür (veya muhtemelen koşu sırasında bile daha sonra).


Derleyici gerekli olan her şeyi "görürse" (tipik olarak tek bir kaynak dosyada ve içerdiği herhangi bir şeyde), bazı derleyicilerin veya derleyicilerin çalıştırılabilir bir dosyayı doğrudan çıkarabileceğini belirtmek gerekir. Tipik olarak küçük mikrolar için birkaç derleyici, tek çalışma modu olarak buna sahiptir.
supercat

Evet, orta yol cevabı vermeye çalıştım. Elbette, sizin durumunuzda olduğu gibi bunun tersi de geçerlidir, çünkü bazı türden nesne dosyalarının tam kod üretimi bile yapılmamıştır; bu bağlayıcı tarafından yapılır (MSVC tüm program optimizasyonu böyle çalışır).
Will Dean

@WillDean ve GCC'nin Bağlantı Zamanı Optimizasyonu, anlayabildiğim kadarıyla - tüm 'kodu' gerekli meta verilerle GIMPLE ara dili olarak yayınlıyor, bunu bağlayıcıya açık hale getiriyor ve sonunda tek seferde optimize ediyor. (Eski belgelerin ima ettiğine rağmen, artık nesne kodunun her iki gösterimiyle eski 'şişman' modu yerine varsayılan olarak yalnızca GIMPLE yayınlanmaktadır.)
undercore_d

10

Derleyici bir nesne dosyası oluşturduğunda, o nesne dosyasında tanımlanan semboller için girişler ve o nesne dosyasında tanımlanmamış sembollere referanslar içerir. Bağlayıcı bunları alır ve bir araya getirir, böylece (her şey doğru çalıştığında) her dosyadaki tüm harici referanslar, diğer nesne dosyalarında tanımlanan sembollerle karşılanır.

Daha sonra tüm bu nesne dosyalarını bir araya getirir ve sembollerin her birine adresler atar ve bir nesne dosyasının başka bir nesne dosyasına harici bir referansı olduğunda, başka bir nesne tarafından kullanıldığı her yerde her bir sembolün adresini doldurur. Tipik bir durumda, kullanılan tüm mutlak adreslerin bir tablosunu da oluşturacaktır, böylece yükleyici, dosya yüklendiğinde adresleri "düzeltebilir" (yani, temel yük adresini bunların her birine ekleyecektir). adresleri, böylece hepsi doğru bellek adresini gösterir).

Epeyce Modern bağlayıcılar da birkaç durumda (dışarı bazı taşıyabilir çok , kodu yalnızca tüm modüller görünür olduktan sonra mümkün olacak şekilde optimize etmek gibi ) başka "şeylerin" (örneğin, dahil edilen işlevleri kaldırma çünkü başka bir modülün onları çağırması mümkündü , ancak tüm modüller bir araya getirildiğinde hiçbir şeyin onları çağırmadığı aşikar.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.