GNU/Linux >> Znalost Linux >  >> Linux

Prozkoumejte proces propojení GCC pomocí LDD, Readelf a Objdump

Propojení je poslední fází procesu kompilace gcc.

V procesu propojení jsou soubory objektů propojeny a všechny odkazy na externí symboly jsou vyřešeny, konečné adresy jsou přiřazeny k volání funkcí atd.

V tomto článku se zaměříme především na následující aspekty procesu propojení gcc:

  1. Soubory objektů a jejich vzájemné propojení
  2. Přemístění kódu


Než přečtete tento článek, ujistěte se, že rozumíte všem 4 fázím, kterými musí program C projít, než se stane spustitelným souborem (předzpracování, kompilace, sestavení a propojení).

PROPOJOVÁNÍ SOUBORŮ OBJEKTŮ

Pojďme pochopit tento první krok pomocí příkladu. Nejprve vytvořte následující program main.c.

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

extern void func(void); 

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

    return 0; 
}

Dále vytvořte následující program func.c. V souboru main.c jsme deklarovali funkci func() prostřednictvím klíčového slova ‚extern‘ a tuto funkci jsme definovali v samostatném souboru func.c

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

Vytvořte soubor objektu pro func.c, jak je znázorněno níže. Tím se v aktuálním adresáři vytvoří soubor func.o.

$ gcc -c func.c

Podobně vytvořte soubor objektu pro main.c, jak je znázorněno níže. Tím se vytvoří soubor main.o v aktuálním adresáři.

$ gcc -c main.c

Nyní spusťte následující příkaz k propojení těchto dvou objektových souborů a vytvoření konečného spustitelného souboru. Tím se v aktuálním adresáři vytvoří soubor ‚main‘.

$ gcc func.o main.o -o main

Když spustíte tento „hlavní“ program, uvidíte následující výstup.

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

Z výše uvedeného výstupu je jasné, že jsme byli schopni úspěšně propojit dva objektové soubory do konečného spustitelného souboru.

Čeho jsme dosáhli, když jsme oddělili funkci func() od main.c a napsali ji do func.c?

Odpověď zní, že zde možná příliš nezáleželo na tom, kdybychom do stejného souboru napsali i funkci func(), ale uvažovali jsme o velmi velkých programech, kde bychom mohli mít tisíce řádků kódu. Změna jednoho řádku kódu může vést k rekompilaci celého zdrojového kódu, což ve většině případů není přijatelné. Takže velmi velké programy jsou někdy rozděleny do malých částí, které jsou nakonec spojeny dohromady, aby vytvořily spustitelný soubor.

Nástroj make, který pracuje s makefiles, přichází do hry ve většině těchto situací, protože tento nástroj ví, které zdrojové soubory byly změněny a které objektové soubory je třeba překompilovat. Soubory objektů, jejichž odpovídající zdrojové soubory nebyly změněny, jsou propojeny tak, jak jsou. Díky tomu je proces kompilace velmi snadný a zvládnutelný.

Nyní tedy chápeme, že když propojíme dva objektové soubory func.o a main.o, je linker gcc schopen vyřešit volání funkce func() a když se provede finální spustitelný main, uvidíme printf() uvnitř vykonávané funkce func().

Kde linker našel definici funkce printf()? Protože Linker neuvedl žádnou chybu, určitě to znamená, že linker našel definici printf(). printf() je funkce, která je deklarována v stdio.h a definována jako součást standardní sdílené knihovny ‚C‘ (libc.so)

Tento soubor sdíleného objektu jsme nepropojili s naším programem. Jak to tedy fungovalo? Pomocí nástroje ldd zjistěte, který vytiskne sdílené knihovny požadované každým programem nebo sdílenou knihovnou zadanou na příkazovém řádku.

Spusťte ldd na „hlavním“ spustitelném souboru, který zobrazí následující výstup.

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

Výše uvedený výstup naznačuje, že hlavní spustitelný soubor závisí na třech knihovnách. Druhý řádek ve výše uvedeném výstupu je „libc.so.6“ (standardní knihovna „C“). Takto je linker gcc schopen vyřešit volání funkce printf().

První knihovna je vyžadována pro provádění systémových volání, zatímco třetí sdílená knihovna je ta, která načítá všechny ostatní sdílené knihovny požadované spustitelným souborem. Tato knihovna bude přítomna pro každý spustitelný soubor, jehož spuštění závisí na jiných sdílených knihovnách.

Během propojování je příkaz, který interně používá gcc, velmi dlouhý, ale z pohledu uživatelů prostě musíme napsat.

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

PŘEMÍSTĚNÍ KÓDU

Přemístění jsou položky v binárním souboru, které jsou ponechány k vyplnění v době propojení nebo běhu. Typický záznam o přemístění říká:Najděte hodnotu ‚z‘ a vložte tuto hodnotu do konečného spustitelného souboru na offset ‚x‘

Pro tento příklad vytvořte následující reloc.c.

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

void func1(void) 
{ 
    func(); 
}

Ve výše uvedeném souboru reloc.c jsme deklarovali funkci func(), jejíž definice stále není poskytnuta, ale tuto funkci voláme ve funkci func1().

Vytvořte objektový soubor reloc.o z reloc.c, jak je znázorněno níže.

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

Použijte nástroj readelf k zobrazení přemístění v tomto objektovém souboru, jak je uvedeno níže.

$ 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 
...

Adresa func() není známa v době, kdy děláme reloc.o, takže kompilátor ponechává relokaci typu R_X86_64_PC32. Toto přemístění nepřímo říká, že „vyplňte adresu funkce func() ve finálním spustitelném souboru na offsetu 000000000005“.

Výše uvedené přemístění odpovídalo části .text v objektovém souboru reloc.o (opět je potřeba porozumět struktuře souborů ELF, aby bylo možné porozumět různým sekcím), takže část .text rozebereme pomocí utility 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

Ve výše uvedeném výstupu má offset ‚5‘ (záznam s hodnotou ‚4‘ vzhledem k počáteční adrese 0000000000000000) 4 bajty čekající na zápis s adresou funkce func().

Čeká se tedy na přemístění funkce func(), které bude vyřešeno, když propojíme reloc.o s objektovým souborem nebo knihovnou, která obsahuje definici funkce func().

Zkusme zjistit, zda se toto přemístění vyřeší nebo ne. Zde je další soubor main.c, který poskytuje definici funkce 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; 
}

Vytvořte soubor objektu main.o z main.c, jak je ukázáno níže.

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

Propojte reloc.o s main.o a zkuste vytvořit spustitelný soubor, jak je ukázáno níže.

$ gcc reloc.o main.o -o reloc

Znovu proveďte objdump a zjistěte, zda bylo přemístění vyřešeno nebo ne:

$ objdump --disassemble reloc > output.txt

Přesměrovali jsme výstup, protože spustitelný soubor obsahuje mnoho a mnoho informací a my se nechceme na stdout ztratit.
Zobrazit obsah souboru 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 
...

Na 4. řádku jasně vidíme, že prázdné bajty adresy, které jsme viděli dříve, jsou nyní vyplněny adresou funkce func().

Abych to uzavřel, propojení kompilátoru gcc je tak obrovské moře, do kterého se lze ponořit, že jej nelze pokrýt v jednom článku. Přesto se tento článek pokusil odloupnout první vrstvu procesu propojení, abyste získali představu o tom, co se děje pod příkazem gcc, který slibuje propojení různých objektových souborů za účelem vytvoření spustitelného souboru.


Linux
  1. propojení <iostream.h> v linuxu pomocí gcc

  2. Jak provést atomový přírůstek a načtení v C?

  3. Pomocí a ve smyčce Bash while

  1. Chyba kompilace pomocí arm-none-eabi-gcc a propojení knihovny liba.a

  2. Ztráta času execv() a fork()

  3. tcpdump – rotace zachycených souborů pomocí -G, -W a -C

  1. Meziprocesová komunikace v Linuxu:Použití kanálů a front zpráv

  2. Problémy s používáním sort a comm

  3. Instalace a používání XeTeXu