Dlaczego i jak można uruchamiać niektóre biblioteki współdzielone, tak jakby były plikami wykonywalnymi?

55

Wywoływanie tego w 32-bitowych systemach Linux

$ /lib/libc.so.6

a na systemach 64-bitowych to

$ /lib/x86_64-linux-gnu/libc.so.6

w powłoce zapewnia wyjście takie jak to:

GNU C Library stable release version 2.10.1, by Roland McGrath et al.
Copyright (C) 2009 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 4.4.0 20090506 (Red Hat 4.4.0-4).
Compiled on a Linux >>2.6.18-128.4.1.el5<< system on 2009-08-19.
Available extensions:
    The C stubs add-on version 2.1.2.
    crypt add-on version 2.1 by Michael Glad and others
    GNU Libidn by Simon Josefsson
    Native POSIX Threads Library by Ulrich Drepper et al
    BIND-8.2.3-T5B
    RT using linux kernel aio
For bug reporting instructions, please see:
<http://www.gnu.org/software/libc/bugs.html>.

Dlaczego i jak to się dzieje i jak to zrobić w innych bibliotekach współdzielonych?

Spojrzałem na /usr/libpliki wykonywalne i znalazłem /usr/lib/libvlc.so.5.5.0. Uruchomienie spowodowało błąd segmentacji . : - /

Ho1
źródło
Oprócz wszystkich poniższych odpowiedzi, pamiętam, że jeśli ustawiłeś bit x w bibliotece współdzielonej, to (być może nadal jest) możliwe było załadowanie go z pliku wykonywalnego, nawet z bitem r wyczyszczonym. Kiedyś uważano za dobrą praktykę bezpieczeństwa wyrzucanie bitów ze świata do plików wykonywalnych systemu i bibliotek. Ze względu na szeroko rozpowszechnione oprogramowanie typu open source tak naprawdę dotyczy to tylko anonimowego katalogu ftp / bin / ls. Dla mnie pozostawienie zestawu bitów x wygląda jak widok tej starej praktyki.
Joshua

Odpowiedzi:

52

Ta biblioteka ma main()funkcję lub równoważny punkt wejścia i została skompilowana w taki sposób, że jest użyteczna zarówno jako plik wykonywalny, jak i obiekt współdzielony.

Oto jedna sugestia, jak to zrobić, chociaż to nie działa dla mnie.

Oto kolejna odpowiedź na podobne pytanie dotyczące SO , którą bezwstydnie plagiatuję, poprawiam i dodam trochę wyjaśnień.

Najpierw źródło naszej przykładowej biblioteki test.c:

#include <stdio.h>                  

void sayHello (char *tag) {         
    printf("%s: Hello!\n", tag);    
}                                   

int main (int argc, char *argv[]) { 
    sayHello(argv[0]);              
    return 0;                       
}                   

Skompiluj, że:

gcc -fPIC -pie -o libtest.so test.c -Wl,-E

Tutaj kompilujemy bibliotekę współdzieloną ( -fPIC), ale mówimy linkerowi, że jest to zwykły plik wykonywalny ( -pie), i że jego tablica symboli -Wl,-Emoże zostać wyeksportowana ( ), dzięki czemu można go użytecznie połączyć.

I chociaż filepowie, że jest to obiekt współdzielony, działa jako plik wykonywalny:

> ./libtest.so 
./libtest.so: Hello!

Teraz musimy sprawdzić, czy naprawdę można go dynamicznie połączyć. Przykładowy program program.c:

#include <stdio.h>

extern void sayHello (char*);

int main (int argc, char *argv[]) {
    puts("Test program.");
    sayHello(argv[0]);
    return 0;
}

Używanie externoszczędza nam konieczności tworzenia nagłówka. Teraz skompiluj, że:

gcc program.c -L. -ltest

Zanim będziemy mogli go wykonać, musimy dodać ścieżkę libtest.sodo dynamicznego modułu ładującego:

export LD_LIBRARY_PATH=./

Teraz:

> ./a.out
Test program.
./a.out: Hello!

I ldd a.outpokaże powiązanie z libtest.so.

Zauważ, że wątpię w to, jak faktycznie kompiluje się glibc, ponieważ prawdopodobnie nie jest tak przenośny jak sam glibc (patrz man gccw odniesieniu do przełączników -fPICi -pie), ale pokazuje podstawowy mechanizm. Aby zobaczyć prawdziwe szczegóły, musisz spojrzeć na źródłowy plik makefile.

Złotowłosa
źródło
1
Świetna odpowiedź, dzięki! :-) Próbowałem użyć nmw bibliotece współdzielonej, ale nie była to wersja do debugowania. Więc dlaczego libvlci inni się zawieszają?
Ho1
1
Ponieważ większość bibliotek współdzielonych nie jest przeznaczona do wykonywania, GNU libcjest wyjątkiem.
goldilocks,
Znalazłem dwa inne: ldi libpthread.
Ho1
@ Ho1 ld.sojest wyjątkowy pod innymi względami. W pewnym stopniu jest to bardziej prawdziwy plik wykonywalny niż normalny plik wykonywalny z dynamicznym łączeniem.
Random832,
1
Powyższe opcje, mimo że tworzą bibliotekę współdzieloną z plikiem wykonywalnym, ale są niekompletne w tym sensie, że oznaczają błąd, gdy jakiś plik wykonywalny próbuje połączyć się z tym. Dodano szczegółowe odniesienie do próbki tutaj: unix.stackexchange.com/a/479334/152034
parasrish
21

Zanurzmy się w poszukiwaniu odpowiedzi w losowym repozytorium glibc w github. Ta wersja zawiera „banner” w pliku version.c.

W tym samym pliku znajduje się kilka interesujących punktów: __libc_print_versionfunkcja, która zapewnia drukowanie na tym samym tekście i symbolu, __libc_main (void)co jest udokumentowane jako punkt wejścia. Tak więc ten symbol jest wywoływany podczas uruchamiania biblioteki.

Skąd więc linker / kompilator wie, że jest to dokładnie funkcja punktu wejścia?

Zanurzmy się w makefile . W flagach linkera jest ciekawa flaga:

# Give libc.so an entry point and make it directly runnable itself.
LDFLAGS-c.so += -e __libc_main

To jest flaga linkera do ustawiania punktu wejścia w bibliotece. Podczas budowania biblioteki możesz zapewnić -e function_namelinkerowi tworzenie plików wykonywalnych. Co to naprawdę robi? Spójrzmy na instrukcję (nieco przestarzałą, ale wciąż aktualną) :

Język poleceń konsolidatora zawiera polecenie przeznaczone specjalnie do zdefiniowania pierwszej instrukcji wykonywalnej w pliku wyjściowym (jej punkcie wejścia). Jego argumentem jest nazwa symbolu:

WEJŚCIE (symbol)

Podobnie jak przypisania symboli, komenda ENTRY może być umieszczona albo jako niezależna komenda w pliku komend, albo między definicjami sekcji w komendzie SECTIONS - cokolwiek ma sens dla twojego układu.

WEJŚCIE jest tylko jednym z kilku sposobów wyboru punktu wejścia. Możesz to wskazać na jeden z poniższych sposobów (pokazane w malejącej kolejności priorytetów: metody znajdujące się wyżej na liście zastępują metody znajdujące się niżej).

the `-e' entry command-line option;
the ENTRY(symbol) command in a linker control script;
the value of the symbol start, if present;
the address of the first byte of the .text section, if present;
The address 0. 

Na przykład, możesz użyć tych reguł do wygenerowania punktu wejścia z instrukcją przypisania: jeśli w twoich plikach wejściowych nie ma zdefiniowanego początku symbolu, możesz go po prostu zdefiniować, przypisując mu odpowiednią wartość ---

start = 0x2020;

Przykład pokazuje adres bezwzględny, ale możesz użyć dowolnego wyrażenia. Na przykład, jeśli pliki obiektów wejściowych używają innej konwencji nazwy symbolu dla punktu wejścia, możesz po prostu przypisać wartość dowolnego symbolu zawierającego adres początkowy, aby rozpocząć:

start = inny symbol;

(aktualna dokumentacja znajduje się tutaj )

Naprawdę ldlinker tworzy plik wykonywalny z funkcją punktu wejścia, jeśli podasz mu opcję wiersza poleceń -e(najbardziej praktyczne rozwiązanie), podasz symbol funkcji startlub wstrzykujesz adres symbolu w asemblerze.

Należy jednak pamiętać, że wyraźnie nie gwarantuje się współpracy z innymi linkerami (nie wiem, czy llvm's lld ma tę samą flagę). Dlaczego nie powinno to być przydatne do celów innych niż podawanie informacji na temat takiego pliku, nie wiem.

IBr
źródło
1
Gdyby to był Python, zapewniłby testy jednostkowe.
Erik Aronesty,