GNU/Linux >> Linux Esercitazione >  >> Linux

Esplora il processo di collegamento GCC utilizzando LDD, Readelf e Objdump

Il collegamento è la fase finale del processo di compilazione di gcc.

Nel processo di collegamento, i file oggetto vengono collegati tra loro e tutti i riferimenti ai simboli esterni vengono risolti, gli indirizzi finali vengono assegnati alle chiamate di funzione, ecc.

In questo articolo ci concentreremo principalmente sui seguenti aspetti del processo di collegamento di gcc:

  1. File oggetto e come sono collegati tra loro
  2. Trasferimenti di codice


Prima di leggere questo articolo, assicurati di aver compreso tutte le 4 fasi che un programma C deve attraversare prima di diventare un eseguibile (pre-elaborazione, compilazione, assemblaggio e collegamento).

COLLEGAMENTO FILE OGGETTO

Cerchiamo di capire questo primo passaggio attraverso un esempio. Per prima cosa crea il seguente programma main.c.

$ vi main.c
#include <stdio.h> 

extern void func(void); 

int main(void) 
{ 
    printf("\n Inside main()\n"); 
    func(); 

    return 0; 
}

Quindi crea il seguente programma func.c. Nel file main.c abbiamo dichiarato una funzione func() tramite la parola chiave 'extern' e abbiamo definito questa funzione in un file separato func.c

$ vi func.c
void func(void) 
{ 
    printf("\n Inside func()\n"); 
}

Creare il file oggetto per func.c come mostrato di seguito. Questo creerà il file func.o nella directory corrente.

$ gcc -c func.c

Allo stesso modo crea il file oggetto per main.c come mostrato di seguito. Questo creerà il file main.o nella directory corrente.

$ gcc -c main.c

Ora esegui il comando seguente per collegare questi due file oggetto per produrre un eseguibile finale. Questo creerà il file "main" nella directory corrente.

$ gcc func.o main.o -o main

Quando esegui questo programma "principale", vedrai il seguente output.

$ ./main 
Inside main() 
Inside func()

Dall'output di cui sopra, è chiaro che siamo stati in grado di collegare correttamente i due file oggetto in un eseguibile finale.

Cosa abbiamo ottenuto quando abbiamo separato la funzione func() da main.ce l'abbiamo scritta in func.c?

La risposta è che qui potrebbe non aver avuto molta importanza se avessimo scritto anche la funzione func() nello stesso file ma pensiamo a programmi molto grandi in cui potremmo avere migliaia di righe di codice. Una modifica a una riga di codice potrebbe comportare la ricompilazione dell'intero codice sorgente che non è accettabile nella maggior parte dei casi. Quindi, programmi molto grandi a volte vengono divisi in piccoli pezzi che vengono infine collegati tra loro per produrre l'eseguibile.

L'utilità make che funziona sui makefile entra in gioco nella maggior parte di queste situazioni perché questa utility sa quali file sorgente sono stati modificati e quali file oggetto devono essere ricompilati. I file oggetto i cui file di origine corrispondenti non sono stati modificati sono collegati così come sono. Questo rende il processo di compilazione molto semplice e gestibile.

Quindi, ora capiamo che quando colleghiamo i due file oggetto func.o e main.o, il linker gcc è in grado di risolvere la chiamata di funzione a func() e quando viene eseguito l'eseguibile main finale, vediamo printf() all'interno della funzione func() in esecuzione.

Dove ha trovato il linker la definizione della funzione printf()? Poiché Linker non ha dato alcun errore, significa sicuramente che il linker ha trovato la definizione di printf(). printf() è una funzione dichiarata in stdio.h e definita come parte della libreria condivisa 'C' standard (libc.so)

Non abbiamo collegato questo file oggetto condiviso al nostro programma. Allora, come ha funzionato? Usa lo strumento ldd per scoprire, che stampa le librerie condivise richieste da ciascun programma o libreria condivisa specificata sulla riga di comando.

Esegui ldd sull'eseguibile "principale", che visualizzerà il seguente output.

$ ldd main 
linux-vdso.so.1 =>  (0x00007fff1c1ff000) 
libc.so.6 => /lib/libc.so.6 (0x00007f32fa6ad000) 
/lib64/ld-linux-x86-64.so.2 (0x00007f32faa4f000)

L'output sopra indica che l'eseguibile principale dipende da tre librerie. La seconda riga nell'output sopra è "libc.so.6" (libreria "C" standard). Questo è il modo in cui gcc linker è in grado di risolvere la chiamata di funzione a printf().

La prima libreria è necessaria per effettuare chiamate di sistema mentre la terza libreria condivisa è quella che carica tutte le altre librerie condivise richieste dall'eseguibile. Questa libreria sarà presente per ogni eseguibile che dipende da qualsiasi altra libreria condivisa per la sua esecuzione.

Durante il collegamento, il comando utilizzato internamente da gcc è molto lungo ma dal punto di vista degli utenti, dobbiamo solo scrivere.

$ gcc <object files> -o <output file name>

RILOCAZIONE DEL CODICE

Le rilocazioni sono voci all'interno di un binario che vengono lasciate da riempire al momento del collegamento o in fase di esecuzione. Una tipica voce di trasferimento dice:trova il valore di 'z' e inserisci quel valore nell'eseguibile finale all'offset 'x'

Crea il seguente reloc.c per questo esempio.

$ vi reloc.c
extern void func(void); 

void func1(void) 
{ 
    func(); 
}

In reloc.c sopra abbiamo dichiarato una funzione func() la cui definizione non è ancora fornita, ma chiamiamo quella funzione in func1().

Crea un file oggetto reloc.o da reloc.c come mostrato di seguito.

$ gcc -c reloc.c -o reloc.o

Usa l'utilità readelf per vedere le rilocazioni in questo file oggetto come mostrato di seguito.

$ readelf --relocs reloc.o 
Relocation section '.rela.text' at offset 0x510 contains 1 entries: 
Offset          Info           Type           Sym. Value    Sym. Name + Addend 
000000000005  000900000002 R_X86_64_PC32     0000000000000000 func - 4 
...

L'indirizzo di func() non è noto nel momento in cui eseguiamo reloc.o, quindi il compilatore lascia un riposizionamento di tipo R_X86_64_PC32. Questo trasferimento dice indirettamente che "riempi l'indirizzo della funzione func() nell'eseguibile finale all'offset 000000000005".

Il trasferimento di cui sopra corrispondeva alla sezione .text nel file oggetto reloc.o (di nuovo è necessario comprendere la struttura dei file ELF per comprendere le varie sezioni), quindi disassembla la sezione .text usando l'utilità objdump:

$ objdump --disassemble reloc.o 
reloc.o:     file format elf64-x86-64 

Disassembly of section .text: 

0000000000000000 <func1>: 
   0:	55                   	push   %rbp 
   1:	48 89 e5             	mov    %rsp,%rbp 
   4:	e8 00 00 00 00       	callq  9 <func1+0x9> 
   9:	c9                   	leaveq 
   a:	c3                   	retq

Nell'output sopra, l'offset '5' (immissione con valore '4' relativo all'indirizzo iniziale 00000000000000000) ha 4 byte in attesa di essere scritti con l'indirizzo della funzione func().

Quindi, c'è un trasferimento in sospeso per la funzione func() che verrà risolto quando colleghiamo reloc.o al file oggetto o alla libreria che contiene la definizione della funzione func().

Proviamo a vedere se questo trasferimento viene risolto o meno. Ecco un altro file main.c che fornisce la definizione di func() :

$ vi main.c
#include<stdio.h> 

void func(void) // Provides the defination 
{ 
    printf("\n Inside func()\n"); 
} 

int main(void) 
{ 
    printf("\n Inside main()\n"); 
    func1(); 
    return 0; 
}

Crea il file oggetto main.o da main.c come mostrato di seguito.

$ gcc -c main.c -o main.o

Collega reloc.o a main.o e prova a produrre un eseguibile come mostrato di seguito.

$ gcc reloc.o main.o -o reloc

Esegui nuovamente objdump e verifica se il trasferimento è stato risolto o meno:

$ objdump --disassemble reloc > output.txt

Abbiamo reindirizzato l'output perché un eseguibile contiene molte, molte informazioni e non vogliamo perderci su stdout.
Visualizza il contenuto del file output.txt.

$ vi output.txt
... 
0000000000400524 <func1>: 
400524:       55                      push   %rbp 
400525:       48 89 e5                mov    %rsp,%rbp 
400528:       e8 03 00 00 00          callq  400530 <func> 
40052d:       c9                      leaveq 
40052e:       c3                      retq 
40052f:       90                      nop 
...

Nella quarta riga, possiamo vedere chiaramente che i byte di indirizzo vuoti che abbiamo visto in precedenza ora sono riempiti con l'indirizzo della funzione func().

Per concludere, il collegamento del compilatore gcc è un mare così vasto in cui immergersi che non può essere trattato in un articolo. Tuttavia, questo articolo ha tentato di rimuovere il primo livello del processo di collegamento per darti un'idea di ciò che accade sotto il comando gcc che promette di collegare diversi file oggetto per produrre un eseguibile.


Linux
  1. Sostituzione del processo e tubo?

  2. Systemd e spawn dei processi:i processi figlio vengono uccisi quando il processo principale esce?

  3. Come impostare la priorità del processo Linux usando i comandi nice e renice

  4. Quale processo sta utilizzando tutto il mio disco IO

  5. collegamento <iostream.h> in Linux usando gcc

Come scoprire quale programma utilizza Internet e quanto?

Padroneggia il processo di uccisione di Linux utilizzando ps, pgrep, pkill e altro

Come modificare la priorità del processo utilizzando Linux Nice e Renice Esempi

Come uccidere i processi in Linux usando kill, killall e pkill

Problemi di utilizzo di sort e comm

Installazione e utilizzo di XeTeX