Come funziona il collegamento C ++ nella pratica?

Come funziona il collegamento C ++ nella pratica? Quello che sto cercando è una spiegazione dettagliata su come avviene il collegamento, e non su quali comandi eseguono il collegamento.

C’è già una domanda simile sulla compilazione che non va troppo nel dettaglio: come funziona il processo di compilazione / collegamento?

EDIT : ho spostato questa risposta al duplicato: https://stackoverflow.com/a/33690144/895245

Questa risposta si concentra sul trasferimento degli indirizzi , che è una delle funzioni cruciali del collegamento.

Un esempio minimale sarà usato per chiarire il concetto.

0) Introduzione

Riepilogo: il riposizionamento modifica la sezione .text dei file object per tradurre:

  • indirizzo del file object
  • nell’indirizzo finale dell’eseguibile

Questo deve essere fatto dal linker perché il compilatore vede solo un file di input alla volta, ma è necessario conoscere tutti i file object contemporaneamente per decidere come:

  • risolvere i simboli non definiti come le funzioni non definite dichiarate
  • non .data sezioni .text e .data di più file object

Prerequisiti: comprensione minima di:

  • assembly x86-64 o IA-32
  • struttura globale di un file ELF. Ho fatto un tutorial per questo

Il collegamento non ha nulla a che fare con C o C ++ in modo specifico: i compilatori generano solo i file object. Il linker quindi li prende come input senza mai sapere quale linguaggio li ha compilati. Potrebbe anche essere Fortran.

Quindi, per ridurre la crosta, studiamo un mondo hello NAS Linux x86-64 ELF:

 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 

compilato e assemblato con:

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

con NASM 2.10.09.

1) .text of .o

Per prima cosa decompiliamo la sezione .text del file object:

 objdump -d hello_world.o 

che dà:

 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 

le linee cruciali sono:

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

che dovrebbe spostare l’indirizzo della stringa hello world nel registro rsi , che viene passato alla chiamata di sistema write.

Ma aspetta! Come può il compilatore sapere dove "Hello world!" finirà in memoria quando il programma è caricato?

Beh, non può, specialmente dopo aver collegato un gruppo di file .o insieme a più sezioni .data .

Solo il linker può farlo poiché solo lui avrà tutti quei file object.

Quindi il compilatore solo:

  • inserisce un valore di segnaposto 0x0 sull’output compilato
  • fornisce alcune informazioni aggiuntive al linker su come modificare il codice compilato con gli indirizzi validi

Questa “informazione extra” è contenuta nella sezione .rela.text del file object

2) .rela.text

.rela.text sta per “trasferimento della sezione .text”.

Il trasferimento parola viene utilizzato perché il linker dovrà spostare l’indirizzo dall’object nell’eseguibile.

Possiamo smontare la sezione .rela.text con:

 readelf -r hello_world.o 

che contiene;

 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 

Il formato di questa sezione è fissato su: http://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html

Ogni voce dice al linker su un indirizzo che deve essere trasferito, qui ne abbiamo uno solo per la stringa.

Semplificando un po ‘, per questa particolare linea abbiamo le seguenti informazioni:

  • Offset = C : qual è il primo byte del testo che questa voce cambia.

    Se guardiamo indietro al testo decompilato, è esattamente all’interno del movimento critico movabs $0x0,%rsi , e quelli che conoscono la codifica dell’istruzione x86-64 noteranno che questo codifica la parte di indirizzo a 64 bit dell’istruzione.

  • Name = .data : l’indirizzo punta alla sezione .data

  • Type = R_X86_64_64 , che specifica cosa esattamente quale calcolo deve essere eseguito per tradurre l’indirizzo.

    Questo campo è in realtà dipendente dal processore e quindi documentato sull’estensione ABI del sistema AMD64 V sezione 4.4 “Rilocazione”.

    Quel documento dice che R_X86_64_64 fa:

    • Field = word64 : 8 byte, quindi 00 00 00 00 00 00 00 00 all’indirizzo 0xC

    • Calculation = S + A

      • S è il valore all’indirizzo ricollocato, quindi 00 00 00 00 00 00 00 00
      • A è l’addend che è 0 qui. Questo è un campo della voce di trasferimento.

      Quindi S + A == 0 e ci trasferiremo nel primo indirizzo della sezione .data .

3) .text of .out

Ora vediamo l’area di testo dell’eseguibile ld generata per noi:

 objdump -d hello_world.out 

dà:

 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 

Quindi l’unica cosa che è cambiata dal file object sono le linee critiche:

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

che ora puntano all’indirizzo 0x6000d8 ( d8 00 60 00 00 00 00 00 in little-endian) invece di 0x0 .

E ‘questo il posto giusto per la stringa hello_world ?

Per decidere dobbiamo controllare le intestazioni del programma, che indicano a Linux dove caricare ciascuna sezione.

Li smontiamo con:

 readelf -l hello_world.out 

che dà:

 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000000d7 0x00000000000000d7 RE 200000 LOAD 0x00000000000000d8 0x00000000006000d8 0x00000000006000d8 0x000000000000000d 0x000000000000000d RW 200000 Section to Segment mapping: Segment Sections... 00 .text 01 .data 

Questo ci dice che la sezione .data , che è la seconda, inizia su VirtAddr = 0x06000d8 .

E l’unica cosa nella sezione dei dati è la nostra stringa Hello World.

In realtà, si potrebbe dire che il collegamento è relativamente semplice.

Nel senso più semplice, si tratta semplicemente di raggruppare i file object 1 come quelli che già contengono l’assembly emesso per ognuna delle funzioni / globali / dati … contenuti nella rispettiva sorgente. Il linker può essere estremamente stupido qui e trattare tutto come un simbolo (nome) e la sua definizione (o contenuto).

Ovviamente, il linker deve produrre un file che rispetti un certo formato (il formato ELF generalmente su Unix) e separerà le varie categorie di codice / dati in diverse sezioni del file, ma questo è solo un dispacciamento.

Le due complicazioni che conosco sono:

  • la necessità di de-duplicare i simboli: alcuni simboli sono presenti in diversi file object e solo uno dovrebbe farlo nella libreria / eseguibile risultante creata; è il job linker a includere solo una delle definizioni

  • ottimizzazione del link-time: in questo caso i file object non contengono l’assembly emesso ma una rappresentazione intermedia e il linker unisce tutti i file object insieme, applica i passaggi di ottimizzazione (inlining, ad esempio), compila fino all’assemblaggio e infine emette il risultato .

1 : il risultato della compilazione delle diverse unità di traduzione (approssimativamente, file sorgente preelaborati)

Oltre ai già citati ” Linkers and Loaders “, se volevi sapere come funziona un linker reale e moderno, potresti iniziare da qui .