Programmcode von dlopen() und Ladevorgang verstehen.



  • Hallo an alle C-Hacker,

    ich habe ein Problem bei der Interpretation des C-Codes der Funktion dlopen().
    Ich kann zwar Programmieren, bin aber kein C-Programmierer, daher brauche ich bitte etwas Hilfe!

    Ich möchte folgendes Wissen:
    Eine Bibliothek, zb.: libfoo.so wurde, durch das Starten das Programms A, in den Arbeitsspeicher geladen. Dann aktualisiere ich das Betriebssystem und die Bibliothek wird durch ein neues Releas ersetzt. Was passiert, wenn die Bibliothek nun durch das
    Programm B geladen werden soll?

    Option 1: dlopen() gibt den Handler der alten Version zurück, weil diese noch im Arbeitsspeicher ist.

    Option 2: dlopen() erkennt das es sich um eine andere Datei handelt und lädt diese Bibliothek auch in den Arbeitsspeicher.

    Um das herauszufinden habe ich mir den C-Code der Funktion angesehen.
    Leider finde ich die entsprechende Stelle nicht, da ich nicht so viel Erfahrung habe. Kann mir bitte jemand beim Interpretieren der Funktion helfen?

    Schon einmal Vielen Dank!

    Gruß Meho

    PS: Der C-Code aus der Datei dlopen.c:

    #include <dlfcn.h>
    #include <libintl.h>
    #include <stddef.h>
    #include <unistd.h>
    #include <ldsodefs.h>
    
    #if !defined SHARED && defined IS_IN_libdl
    
    void *
    dlopen (const char *file, int mode)
    {
      return __dlopen (file, mode, RETURN_ADDRESS (0));
    }
    static_link_warning (dlopen)
    
    #else
    
    struct dlopen_args
    {
      /* The arguments for dlopen_doit.  */
      const char *file;
      int mode;
      /* The return value of dlopen_doit.  */
      void *new;
      /* Address of the caller.  */
      const void *caller;
    };
    
    /* Non-shared code has no support for multiple namespaces.  */
    # ifdef SHARED
    #  define NS __LM_ID_CALLER
    # else
    #  define NS LM_ID_BASE
    # endif
    
    static void
    dlopen_doit (void *a)
    {
      struct dlopen_args *args = (struct dlopen_args *) a;
    
      if (args->mode & ~(RTLD_BINDING_MASK | RTLD_NOLOAD | RTLD_DEEPBIND
    		     | RTLD_GLOBAL | RTLD_LOCAL | RTLD_NODELETE
    		     | __RTLD_SPROF))
        GLRO(dl_signal_error) (0, NULL, NULL, _("invalid mode parameter"));
    
      args->new = GLRO(dl_open) (args->file ?: "", args->mode | __RTLD_DLOPEN,
    			     args->caller,
    			     args->file == NULL ? LM_ID_BASE : NS,
    			     __dlfcn_argc, __dlfcn_argv, __environ);
    }
    
    void *
    __dlopen (const char *file, int mode DL_CALLER_DECL)
    {
    # ifdef SHARED
      if (__builtin_expect (_dlfcn_hook != NULL, 0))
        return _dlfcn_hook->dlopen (file, mode, DL_CALLER);
    # endif
    
      struct dlopen_args args;
      args.file = file;
      args.mode = mode;
      args.caller = DL_CALLER;
    
    # ifdef SHARED
      return _dlerror_run (dlopen_doit, &args) ? NULL : args.new;
    # else
      if (_dlerror_run (dlopen_doit, &args))
        return NULL;
    
      __libc_register_dl_open_hook ((struct link_map *) args.new);
      __libc_register_dlfcn_hook ((struct link_map *) args.new);
    
      return args.new;
    # endif
    }
    # ifdef SHARED
    #  include <shlib-compat.h>
    strong_alias (__dlopen, __dlopen_check)
    versioned_symbol (libdl, __dlopen_check, dlopen, GLIBC_2_1);
    # endif
    #endif
    


  • Ohne mir den von dir geposteten Code angesehen zu haben:
    Laut dlopen(3) passiert (zumindest unter OpenBSD) folgendes:

    dlopen() returns a handle to be used in calls to dlclose(), dlsym(), and dlctl(). If the named shared object has already been loaded by a previous call to dlopen() and not yet unloaded by dlclose(), a handle referring to the resident copy is returned.



  • Hallo nman,

    danke für deine Antwort!

    Ich habe mich an das offizielle Manual gehalten, wo das so ähnlich geschrieben steht. http://linux.die.net/man/3/dlopen

    If the same library is loaded again with dlopen(), the same file handle is returned. The dl library maintains reference counts for library handles, so a dynamic library is not deallocated until dlclose() has been called on it as many times as dlopen() has succeeded on it. The _init() routine, if present, is only called once. But a subsequent call with RTLD_NOW may force symbol resolution for a library earlier loaded with RTLD_LAZY

    .

    Da ich es für Red-Hat(Oracle, CentOs) wissen möchte habe ich mir den Quellcode aus dem Paket glibc-2.12-1.166.el6.src.rpm gezogen. Deswegen habe ich den mit gepostet. Dieser unterscheidet sich übrigens von der Implementierung zu Apple:
    http://www.opensource.apple.com/source/cctools/cctools-667.8.0/libdyld/dlopen.c Wie das mit OpenBSD ist weiss ich nicht.

    Leider klärt das nicht die Frage, ob die Funktion eine aktualisierte Version der Bibliothek als neue Datei ansieht und in den Speicher lädt oder ob es einfach eine Referenz auf die veraltete Version in den Arbeitsspeicher zurück gibt. ⚠

    Den Quellcode interpretiere ich so:
    Die Funktion dlopen() gibt das zurück was __dlopen() zurück gibt.
    __dlopen() gibt immer dann args.new zurück wenn es keinen Ladefehler
    mit der Bibliothek gibt. args.new wird in der Funktion dlopen_doit (void *a)
    gefüllt. Dort wird erst geprüft ob der übergeben Modus richtig ist und
    dann wird mit folgenden Code args.new verändert/gefüllt:

    args->new = GLRO(dl_open) (args->file ?: "", args->mode | __RTLD_DLOPEN, 
                     args->caller, 
                     args->file == NULL ? LM_ID_BASE : NS, 
                     __dlfcn_argc, __dlfcn_argv, __environ);
    

    Nur die Zeilen verstehe ich nicht! Da sind doch mit den ? If-Anweisungen. Welche
    gleichzeitig aber auch schon Parameter für eine Funktion sind die ich nicht zu interpretieren weis 😕

    Wäre dankbar, wenn mir jemand mit meiner Frage weiterhelfen kann!
    Gruß Meho



  • tl; dr: Auf meinem System, mit meinem Linker und Compiler, mit den Namen, die ich verwendet habe, ist es dlopen schnutzpiepegal, ob es da noch eine andere Anwendung gibt, die libmydl.so.0.0.0 im ld-Cache hat. Es wurde die aktuelle Version von der Platte geladen.

    Sagen wir mal, wir haben folgende Struktur: eine mydl.c:

    #include <stdio.h>
    
    void dl(void)
    {
            printf("dl hier: %s\n",MY_VERSION);
    }
    

    Eine main.c, in der die Lib geladen wird:

    #include <stdio.h>
    #include <dlfcn.h>
    
    int main(void)
    {
            void*handle; 
            void(*func)(void);
    
            handle = dlopen(WHOS_YOUR_DADDY,RTLD_LAZY);
    
            if(!handle)
            {
                    fprintf(stderr,"Konnte %s nicht einladen!\n",WHOS_YOUR_DADDY);
                    return -1;
            }
    
            func = dlsym(handle,WHOS_YOUR_SYMBOL);
    
            if(!func)
            {
                    fprintf(stderr,"Konnte Symbol %s in Datei %s nicht finden!\n",WHOS_YOUR_SYMBOL,WHOS_YOUR_DADDY);
                    dlclose(handle);
                    return -1;
            }
    
            func();
    
            /*Hält das Programm an.*/
            getc(stdin);
    
            dlclose(handle);
            return 0;
    }
    

    Und dann das Makefile:

    NAME_LIB                =libmydl
    NAME_LIB_VERSION_SO     =0
    NAME_LIB_VERSION_REAL   =0.0
    
    NAME_LIB_LINKER =$(NAME_LIB).so
    NAME_LIB_SO     =$(NAME_LIB_LINKER).$(NAME_LIB_VERSION_SO)
    NAME_LIB_REAL   =$(NAME_LIB_SO).$(NAME_LIB_VERSION_REAL)
    
    mydl.o: mydl.c
            gcc mydl.c -o mydl.o -fPIC -c -D MY_VERSION=\"$(NAME_LIB_VERSION_SO).$(NAME_LIB_VERSION_REAL)\"
    
    $(NAME_LIB_LINKER): mydl.o
            gcc -shared -Wl,-soname,$(NAME_LIB_SO) -o $(NAME_LIB_REAL) mydl.o
    
    prog: $(NAME_LIB_LINKER)
            gcc main.c -o prog -ldl -D WHOS_YOUR_DADDY=\"$(NAME_LIB_REAL)\" -D WHOS_YOUR_SYMBOL=\"dl\"
    

    Packen wir alles in einen Ordner, und machen dann make prog . Dann haben wir eine libmydl.so und prog . Ausgeführt bekommen wir das über:

    $ LD_LIBRARY_PATH=. ./prog
    dl hier: 0.0.0
    

    Funktioniert soweit - die Spielerei kann beginnen!
    Die Fragestellung war: welche Version einer Lib wird geladen, wenn Programm A läuft, die Libs aktualisiert werden, und dann Programm B ausgeführt wird. Dazu müssen wir spezifizieren, nach welchem Namen genau gesucht wird.

    Ist es "libmydl.so.0"? Das ist der so-Name. Wenn man sich /usr/lib anschaut, dann sieht man, dass es sich hierbei für gewöhnlich um einen Symlink auf das richtige Binary handelt. Der Grund hierfür ist folgender: man möchte vielleicht Libs mit unterschiedlicher Major-Nummer installiert haben. Anstatt Klebstoff zu essen, wie das bei Windows-DLLs der Fall wäre.

    Major-Versionsänderungen können in der Theorie das Interface (API/ABI) brechen, deswegen sucht eine Anwendung immer nach der Lib mit ihrer Major-Nummer. Aber beliebiger Minor-Nummer. Minor-Versionsänderungen sollen nur Bugfixes erhalten, nicht das Interface brechen. Aus diesem Grund verweist der so-Name immer auf den "echten" Name der Lib - weil das in der Regel eine Lib ist, die die gleiche Major-Nummer (kein Brechen des Interfaces) und die höchste Minor-Nummer (Bugfixes) hat. Vorherige Minor-Versionen bleiben normalerweise nicht erhalten, Major-Versionen schon (weil ein paar alte Anwendungen das 0-Interface brauchen, neuere das 1-Interface - da sich das Interface bei Minor aber nicht ändert, kann die alte Version dann weg).

    Wenn Bibliotheken ausgetauscht werden, dann kann sich Major oder Minor ändern. Sprich, wir können eine "libmydl.so.1.0.0" (Major) oder eine "libmydl.so.0.1.0" (Minor) bekommen.

    In unserem Beispiel suchen wir immer nach dem "echten" Namen, "libmydl.so.0.0.0" (siehe Makefile WHOS_YOUR_DADDY ). Ändert sich Minor, bricht das unsere Anwendung, weil dann nicht mehr "libmydl.so.0.0.0" vorhanden ist, sondern "0.1.0". Ist ungut. Aber vielleicht suchst du in Programm A ja genau nach dieser Lib?

    Wenn du nicht das nicht weißt, stehen die Chancen gut, dass der Linker das Auflösen der Symbole übernommen habt. In dem Fall wird dann nach "libmydl.so.0" gesucht. Das ist ein Symlink auf die aktuelle Minor.

    Es kann auch sein, dass du einfach nur nach "libmydl.so" suchst.

    Also noch mal kurze Referenz:

    libmydl.so = Linkername
    libmydl.so.0 = so-Name
    libmydl.so.0.0.0 = echter Name

    Die Frage ist daher, nach welchem Namen gesucht wird?

    Ein kleiner Test meinerseits geht vom Standard aus - wir suchen nach so-Name, dann kommt Minor-Versionsänderung. Machen wir den Symlink:

    ln -s libmydl.so.0.0.0 libmydl.so.0
    

    , ändern das Makefile:

    prog: $(NAME_LIB_LINKER)
            gcc main.c -o prog -ldl -D WHOS_YOUR_DADDY=\"libmydl.so.0\" -D WHOS_YOUR_SYMBOL=\"dl\"
    

    , und bauen und starten die Anwendung:

    $ make prog
    gcc -shared -Wl,-soname,libmydl.so.0 -o libmydl.so.0.0.0 mydl.o
    gcc main.c -o prog -ldl -D WHOS_YOUR_DADDY=\"libmydl.so.0\" -D WHOS_YOUR_SYMBOL=\"dl\"
    
    #Sichergehen, dass nach dem richtigen Namen gesucht wird:
    $ ./prog
    Konnte libmydl.so.0 nicht einladen!
    #Soll so sein, wir haben LD_LIBRARY_PATH nicht gesetzt. :)
    $ LD_LIBRARY_PATH=. ./prog 
    dl hier: 0.0.0
    

    Und hier bleibt die Anwendung erst mal stehen.
    Jetzt ändern wir die Minor-Version im Makefile:

    NAME_LIB_VERSION_REAL   =1.0
    

    , kompilieren neu und führen aus - alles, während die erste Anwendung noch läuft:

    $ rm mydl.o
    $ make mydl.o
    gcc mydl.c -o mydl.o -fPIC -c -D MY_VERSION=\"0.1.0\"
    $ make prog
    gcc -shared -Wl,-soname,libmydl.so.0 -o libmydl.so.0.1.0 mydl.o
    gcc main.c -o prog -ldl -D WHOS_YOUR_DADDY=\"libmydl.so.0\" -D WHOS_YOUR_SYMBOL=\"dl\"
    #Wir brauchen jetzt einen neuen Symlink ... Minor-Versionsänderungen und so ...
    $ rm libmydl.so.0
    $ ln -s libmydl.so.0.1.0 libmydl.so.0
    #Stunde der Wahrheit ...
    $ ./prog 
    Konnte libmydl.so.0 nicht einladen!
    #Nur ein Scherz, nur ein Scherz ...
    $ LD_LIBRARY_PATH=. ./prog 
    dl hier: 0.1.0
    


  • dachschaden schrieb:

    tl; dr: Auf meinem System, mit meinem Linker und Compiler, mit den Namen, die ich verwendet habe, ist es dlopen schnutzpiepegal, ob es da noch eine andere Anwendung gibt, die libmydl.so.0.0.0 im ld-Cache hat.

    Sorry, das mit den beiden Anwendungen hatte ich ueberlesen. Das von dir beschriebene Verhalten ueberrascht mich nicht.



  • Hallo dachschaden,

    erst einmal vielen Dank für deine Umfangreiche Antwort!
    Das mit den symbolischen Links bei den Bibliotheken ist mir bekannt
    und ich kann deiner Erklärung zustimmen.

    Meisten nutzt man den Soname oder den Linkername um die Bibliotheken mittels
    dlopen() an ein Programm zu binden. Dadurch ist der Programmierer nicht auf einen
    genauen Dateinamen festgelegt, sondern linkt nur auf den Major-Releas.

    Das Problem ist nur dass sich nicht jede Distribution an die Theorie hält.
    Ich habe bei Oracle-Linux 6 die Bibliotheken librt.so und libc.so beispielhaft untersucht.
    In den Aktualisierungspaketen sind die Bibliotheken gleich beschriftet, d.h.
    statt aus librt.so.2.5 eine librt.so.2.8 zu aktualisieren bleibt die Bezeichnung
    "librt-2.12.so" einfach erhalten. 🙄

    Mit deiner Erklärung bin ich einverstanden, bis zu den Punkt der Programmierung,
    da hapert es wieder bei mir! Ich bin Administrator und kann nur wenig programmieren, sorry!
    In die Variable func deiner main Methode sind die Funktionsnamen der
    geladenen Bibliothek enthalten, in deinem Beispiel die von dl().
    Mit fun() führst du die Methode dl() der geladenen Bibliothek aus. Richtig?

    Die Bibliothek bekommt ihre Konstante MY_VERSION beim Compilieren über das Makefile gesetzt.
    Du kompilierst 2 mal die Bibliothek mit einer anderen Versionsnummer und bei jedem mal
    Ausführen kommt die Version der Bibliothek zurück mit der sie kompileirt wurde.
    Habe ich das soweit richtig verstanden?

    Dazu folgende Kritik/Frage:
    Bibliotheken werden nicht unnötig oft in den Arbeitsspeicher geladen sondern nur ein mal.
    Das spart Speicher und schont Computerressourcen. Für die Werte der Variablen bekommt jede
    Programm seinen eigenen Bereich im Arbeitsspeicher(Datenbereich). So nutzen alle Programme
    den gleichen Bibliothks-Code aber jedes hat eigene Variableninhalte. Woher weist du jetzt,
    dass MY_VERSION nicht nur in den Datenbereich jedes Programms geschrieben wurde?
    Wird das beim Kompilieren richtig ersetzt?

    Wieso geht keiner von euch beiden, trotz eurer scheinbar guten Fachkenntnisse, auf die Implementierung
    von dlopen() ein? Kann man daran die Frage nicht beantworten?

    Danke!!
    Gruß meho



  • Meho schrieb:

    Meisten nutzt man den Soname oder den Linkername um die Bibliotheken mittels
    dlopen() an ein Programm zu binden. Dadurch ist der Programmierer nicht auf einen
    genauen Dateinamen festgelegt, sondern linkt nur auf den Major-Releas.

    Nona, das habe ich doch gesagt. 🙂
    Wenn ich in den Binärcode ein paar meiner Projekte schaue, ist die einzige Referenz halt "libxxx.so.y". Alles andere macht auch (weil ja nur das Interface für die Anwendung wichtig ist) keinen Sinn.

    Meho schrieb:

    Mit deiner Erklärung bin ich einverstanden, bis zu den Punkt der Programmierung,
    da hapert es wieder bei mir! Ich bin Administrator und kann nur wenig programmieren, sorry!
    In die Variable func deiner main Methode sind die Funktionsnamen der
    geladenen Bibliothek enthalten, in deinem Beispiel die von dl().
    Mit fun() führst du die Methode dl() der geladenen Bibliothek aus. Richtig?

    Korrekt. Mit dlsym bekomme ich eine Adresse auf das Symbol "dl" (muss man in C halt so ausdrücken, die Symbolsuche), interpretiere diese als Funktionszeiger, und rufe damit die Funktion dann auf.

    Meho schrieb:

    Die Bibliothek bekommt ihre Konstante MY_VERSION beim Compilieren über das Makefile gesetzt.
    Du kompilierst 2 mal die Bibliothek mit einer anderen Versionsnummer und bei jedem mal
    Ausführen kommt die Version der Bibliothek zurück mit der sie kompileirt wurde.
    Habe ich das soweit richtig verstanden?

    Korrekt. Der Dateiname ist der gleiche, aber der Symlink ist ein anderer. Und das ist entscheidend.

    Was meinst du mit:

    Meho schrieb:

    Woher weist du jetzt, dass MY_VERSION nicht nur in den Datenbereich jedes Programms geschrieben wurde?
    Wird das beim Kompilieren richtig ersetzt?

    ? libmydl.so ist eine dynamische Bibliothek, die zur Laufzeit eingeladen wird. Wenn du dir das Binary anschaust, wirst du feststellen, dass die Strings "0.1.0" und "dl hier: %s\n" in libmydl.so stecken.

    Das Symbol MY_VERSION ist dem Compiler für prog nicht bekannt - und braucht es auch nicht. Der braucht nur die Symbole für dlopen und dergleichen, die Auflösung nach libmydl.so machen wir selbst. Der printf -Call im mydl.o-Code bekommt hingegen zwei Pointer auf den Stack gelegt - aber diese Pointer müssen Compiler und Linker bekannt sein. Bzw. nur dem Compiler, weil der Präprozessor für MY_VERSION kein Symbol anlegt - der macht nur eine einfache Textersetzung, dann kommt der Compiler, sieht für den Call zwei Strings, packt in das Segment für statische Daten für mydl.o zwei Strings, und setzt dann relative Adressen für den Call. Relativ deswegen, weil wir positionsunabhängigen Code generieren ( -fPIC ) - wir wissen ja nicht, WO der ld unsere Lib in den Adressraum der Prozesse mappt.

    Was anderes wäre es, wenn es sich um eine statische Lib handeln würde. Dann würde der Linker die Datensegmente zusammenpacken - er braucht ja nichts mehr dynamisch zu linken zur Laufzeit.

    Meho schrieb:

    Wieso geht keiner von euch beiden, trotz eurer scheinbar guten Fachkenntnisse, auf die Implementierung von dlopen() ein? Kann man daran die Frage nicht beantworten?

    Der Code der glibc und alles, was aus der Richtung kommt, enthält wohl das, was Blumenwachstum anregt, und es ist sehr mächtig. Da möchte man eigentlich nicht reingreifen, sondern erst mal in die Doku schauen, und wenn das nicht reicht, probiert man halt aus. Man schlägt eher drei Kreuze, dass es funktioniert.

    EDIT: Problem ist halt, dass wir auch nicht wissen, WAS du jetzt genau weißt.



  • Hallo Dachschaden,

    ok ich habe das soweit kapiert! Vielen, vielen Dank für deine Hilfe! 👍
    Hätte nicht gedacht, dass ich so schnell Hilfe mit so einem konkreten Problem bekomme!

    Gruß und ein schönes Wochenende 😃 ,
    Meho


Anmelden zum Antworten