3 Replies
      Latest reply on Sep 10, 2019 2:20 AM by eskimo
      oskard Level 1 Level 1 (0 points)

        Hello,

         

        I am encountering an unsual problem when linking a library to macOS Frameworks: as soon as I'm linking to some specific frameworks (detailed below), dlclose will no longer work, meaning that the said library will not be unloaded from process memory.

        This is causing problems, because the update functionality of my application will not work - calling dlopen again will load the "old" library.

         

        The build details are:

        • macOS High Sierra (10.13.6)
        • Xcode 9.4.1
          • C++ Language Dialect: C++14 [-std=c++14]
          • C++ Standard Library: libc++ (LLVM C++ standard library with C++11 support)

         

        Steps to reproduce the problem:

        1. Library (dylib) is linked ("Link Binary with Libraries") to: CoreFoundation and IOKit, plus the following additional frameworks - OpenDirectory, SystemConfiguration, Security, CoreServices, ApplicationServices
        2. Compiling works fine, of course, and then the library is loaded, using dlopen by another application (Unix / terminal app)
        3. After calling dlclose, the library is still reported as being loaded in the process memory

         

        The problem will only happen when:

        • the terminal app is ran from a terminal window (it cannot be reproduced when using Xcode directly to run / debug it)
        • the library is linked to CoreFoundation, IOKit plus any one of the other frameworks (OpenDirectory, SystemConfiguration, Security, CoreServices, ApplicationServices)

         

        Does anyone know what may be causing this problem?

         

        On Linux based systems, the "-fno-gnu-unique" flag will not fix the problem, but at least offer the option to use the new / updated library. Is there, maybe, a similar flag for macOS?

         

        Any help would be greatly appreciated.

         

        Code snippet below:

         

        // Returns the full path of the application executable file
        const char* getApplicationPath()
        {
            static char s_szDir[1024] = {0};
            
            if (!*s_szDir) {
                char buffer[1024];
                char *answer = getcwd(buffer, sizeof(buffer));
                strncpy(s_szDir, answer, 1024);
                s_szDir[1024-1] = '\0';
            }
            return s_szDir;
        }
        
        // prints all loaded modules in the process memory (address space)
        void printLoadedModules( )
        {
            std::cout<<"\ndynamic libraries loaded in the process memory:";
            int count = _dyld_image_count();
            for(int  i = 0; i < count; i++ )
            {
                const char * imageName = _dyld_get_image_name(i);
                if( !imageName )
                    continue;
                
                std::string strImageName = imageName;
                
                if( strImageName.find( "libwa" ) != std::string::npos )
                    std::cout<<"\n   " << strImageName;
            }
        }
        
        int main(int argc, const char * argv[])
        {
            std::string lib_path = getApplicationPath();
            lib_path += "/libtest.dylib";
            
            // Step 1: Print loaded libraries in the process memory before dlopen()
            std::cout<<"\n### Before dlopen \n";
            printLoadedModules();
            
            // Step 2: Load libtest.dylib
            void * lib_handle = (void*) dlopen( lib_path.c_str() , RTLD_LOCAL );
            if( !lib_handle )
            {
                std::cout<<"\nFailed to load libtest.dylib with error \n"<<dlerror();
                exit(1);
            }
            
            // Step 3: Print loaded libraries in the process memory after dlopen()
            std::cout<<"\n\n### After dlopen \n";
            printLoadedModules();
            
            // Step 4: Unload libtest.dylib
            int close_code = dlclose( lib_handle );
            lib_handle = nullptr;
            
            // Step 5: Print loaded libraries in the process memory after dlclose()
            std::cout<<"\n\n### After dlclose (closed code: " << close_code << ")\n";
            
            printLoadedModules();
            
            std::cout<<"\n\n";
            return 0;
        }
        
        
        

         

        Thank you.

        • Re: dlclose will not unload library when using macOS Frameworks
          eskimo Apple Staff Apple Staff (11,815 points)

          Unloading code has always been a challenge on macOS because of the Objective-C runtime.  If a chunk of code registers with the runtime, the runtime maintains pointers into that code and there’s no way to disconnect those in order to unload the code.  In addition, any code that works with CFString literals has similar issues [1].

          I’m not 100% sure what’s causing the library to be marked as not unloadable in this case, although many of the frameworks you referenced heavily depend on Objective-C and thus it’s not hard to imagine how that’s come about.  It’s possible that one of the DYLD_PRINT_xxx environment variables will offer some insight into this (see the dyld man page ) for details.

          Share and Enjoy

          Quinn “The Eskimo!”
          Apple Developer Relations, Developer Technical Support, Core OS/Hardware
          let myEmail = "eskimo" + "1" + "@apple.com"

          [1] I vaguely recall an option that prevented CFString literals from leaving dangling pointers like this, but I wasn’t able to track that down.

            • Re: dlclose will not unload library when using macOS Frameworks
              oskard Level 1 Level 1 (0 points)

              Thank you @eskimo, for the quick answer.

               

              Are there any general accepted solutions in this case? Maybe restarting the process after the update process?

                • Re: dlclose will not unload library when using macOS Frameworks
                  eskimo Apple Staff Apple Staff (11,815 points)

                  Maybe restarting the process after the update process?

                  That should work.  Both the Objective-C and dyld state are confined to your process.


                  Oh, one thing to watch out for when updating code: Do not modify a Mach-O image on disk.  Rather, write the updated image to a new file and then use that to replace the old file (using rename, or your preferred wrapper).

                  The kernel caches code signature information about a Mach-O image in the file’s vnode.  If you rewrite the file, you confuse this horribly.  A new file gets a new vnode, and thus avoids this problem.

                  Share and Enjoy

                  Quinn “The Eskimo!”
                  Apple Developer Relations, Developer Technical Support, Core OS/Hardware
                  let myEmail = "eskimo" + "1" + "@apple.com"