Explicación detallada del principio de inicio de la aplicación de carga dyld

Todos sabemos que la función de entrada de APP es main (), y antes de que se llame a la función main (), ¿cuál es el proceso de carga de APP? A continuación, analicemos juntos el proceso de carga de la aplicación.

1. Utilice puntos de interrupción para realizar un seguimiento

  • Primero, creamos un proyecto, no escribimos ningún código, punto de interrupción en la función main (), verá la situación como se muestra a continuación:

     

    01

  1. En la figura anterior, podemos ver que en la pila de llamadas, solo vemos star y main, y el hilo principal está encendido, pero nada más. ¿Cómo puedo ver la información detallada de la pila de llamadas? Todos sabemos que hay un método que se llama antes que la función main (), es decir, la función load (). En este momento, escriba una función de carga en el controlador y ejecútela en un punto de interrupción, como se muestra en la siguiente figura:

02

  1. A través de la figura anterior, podemos ver una secuencia de llamada de función más detallada, desde _dyld_start en la línea 13 hasta dyld: notifySingle en la línea 3. Este tipo dyld aparece con mayor frecuencia, entonces, ¿qué es dyld? ¿Qué está haciendo? En pocas palabras, dyld es un enlazador dinámico que carga todas las bibliotecas y archivos ejecutables. A continuación, usaremos la relación de llamada que se muestra en la Figura 2 para rastrear dónde está dyld.

Dos, análisis del proceso de carga dyld

1. Primero descargue el código fuente de dyld .

2. Abra el proyecto de código fuente dyld y busque el método de inicio llamado en dyldbootstrap de acuerdo con la palabra clave dyldbootstrap: start en la línea 12 de la Figura 2, como se muestra en la siguiente figura:

 

3. El código fuente del método es el siguiente. A continuación, analizaremos las partes clave del método:

uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], intptr_t slide)
{
    // 读取macho文件的头部信息
    const struct macho_header* dyldsMachHeader =  (const struct macho_header*)(((char*)&_mh_dylinker_header)+slide);
    
    // 滑块,设置偏移量,用于重定位
    if ( slide != 0 ) {
        rebaseDyld(dyldsMachHeader, slide);
    }
    
    uintptr_t appsSlide = 0;
        
    // 针对偏移异常的监测
    dyld_exceptions_init(dyldsMachHeader, slide);
    
    // 初始化machO文件
    mach_init();

    // 设置分段保护,这里的分段下面会介绍,属于machO文件格式
    segmentProtectDyld(dyldsMachHeader, slide);
    
    //环境变量指针
    const char** envp = &argv[argc+1];
    
    // 环境变量指针结束的设置
    const char** apple = envp;
    while(*apple != NULL) { ++apple; }
    ++apple;

    // 在dyld中运行所有c++初始化器
    runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);
    
    // 如果主可执行文件被链接-pie,那么随机分配它的加载地址
    if ( appsMachHeader->flags & MH_PIE )
        appsMachHeader = randomizeExecutableLoadAddress(appsMachHeader, envp, &appsSlide);
    
    // 传入头文件信息,偏移量等。调用dyld的自己的main函数(这里并不是APP的main函数)。
    return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple);
}

  • 3.1 En los parámetros de la función, vemos un parámetro de macho_header ¿Qué es esto? Mach-O es en realidad la abreviatura del formato de archivo Mach Object. Es un formato de archivo ejecutable en mac e iOS, y tiene su propio directorio de formato de archivo. El archivo mach proporcionado por Apple es el siguiente:

     

    04

  • 3.2 Primero, hacemos clic en la estructura macho_header para ver su definición de la siguiente manera:

struct mach_header_64 {
    uint32_t    magic;      /* 区分系统架构版本 */
    cpu_type_t  cputype;    /*CPU类型 */
    cpu_subtype_t   cpusubtype; /* CPU具体类型 */
    uint32_t    filetype;   /* 文件类型 */
    uint32_t    ncmds;      /* loadcommands 条数,即依赖库数量*/
    uint32_t    sizeofcmds; /* 依赖库大小 */
    uint32_t    flags;      /* 标志位 */
    uint32_t    reserved;   /* 保留字段,暂没有用到*/
};

 

  • 3.3 Aquí macho_header es para leer la información del encabezado del archivo macho. El encabezado contendrá información sobre el archivo binario: como orden de bytes, tipo de arquitectura, número de instrucciones de carga, etc. Se puede usar para confirmar rápidamente cierta información, como si el archivo actual se usa para 32 bits o 64 bits, tipo de archivo, etc. Entonces, ¿dónde puedo encontrar el archivo de macho? Como se muestra a continuación, encontramos macho y usamos MachOView para ver:

     

    05

  • 3.4 El oscuro de arriba es el archivo macho, que es un archivo ejecutable ¿Echemos un vistazo a qué información de encabezado carga? Esta información se pasará a la siguiente función. Aquí hay una breve introducción al Número de comandos de carga número 22, que representa 22 archivos de biblioteca. En LoadCommands, hay una relación correspondiente para cargar bibliotecas. La sección es nuestra información DATA, que contiene códigos, constantes y otros datos.

     

    06

  • 3.5 Resumen: La función de estrella es principalmente para leer la información del encabezado del archivo macho y establecer el desplazamiento de la dirección virtual. El desplazamiento aquí se utiliza principalmente para la redirección. El siguiente paso es inicializar el archivo macho para la carga posterior de archivos de biblioteca y datos de datos, luego ejecutar el inicializador de C ++ y finalmente ingresar a la función principal de dyly.

  • 4. A continuación, continuamos rastreando. De acuerdo con la pila de llamadas de la Figura 2, sabemos que el método dyld :: _ main se llama en el método dyldbootstrap: star, que es el programa principal que ingresa al dyld que mencionamos anteriormente, como se muestra en la siguiente figura:

    07

  • 4.1 Ingresamos el método para seguir rastreando, interceptamos algunas de las fuentes como se muestra en la figura a continuación, encontramos que hay varios juicios si, aquí están configurando variables de entorno, es decir, si estas variables de entorno están configuradas, Xcode imprimirá la información detallada relevante en la consola :
    if ( sProcessIsRestricted )
            pruneEnvironmentVariables(envp, &apple);
        else
            checkEnvironmentVariables(envp, ignoreEnvironmentVariables);
        if ( sEnv.DYLD_PRINT_OPTS ) 
            printOptions(argv);
        if ( sEnv.DYLD_PRINT_ENV ) 
            printEnvironmentVariables(envp);
        getHostInfo();  
    
  • 4.2 Cuando configuramos las variables de entorno relevantes, Xcode imprimirá el directorio relacionado con el programa, el nivel de usuario, la biblioteca dinámica insertada, la ruta de la biblioteca dinámica, etc., como se muestra a continuación:
  • 08

  • 4.3 Después de configurar las variables de entorno, getHostInfo () se llamará a continuación para obtener el encabezado machO para obtener la información de la arquitectura actual en ejecución. El código de la función es el siguiente:
  • static void getHostInfo()
    {
    #if 1
        struct host_basic_info info;
        mach_msg_type_number_t count = HOST_BASIC_INFO_COUNT;
        mach_port_t hostPort = mach_host_self();
        kern_return_t result = host_info(hostPort, HOST_BASIC_INFO, (host_info_t)&info, &count);
        if ( result != KERN_SUCCESS )
            throw "host_info() failed";
        
        sHostCPU        = info.cpu_type;
        sHostCPUsubtype = info.cpu_subtype;
    #else
        size_t valSize = sizeof(sHostCPU);
        if (sysctlbyname ("hw.cputype", &sHostCPU, &valSize, NULL, 0) != 0) 
            throw "sysctlbyname(hw.cputype) failed";
        valSize = sizeof(sHostCPUsubtype);
        if (sysctlbyname ("hw.cpusubtype", &sHostCPUsubtype, &valSize, NULL, 0) != 0) 
            throw "sysctlbyname(hw.cpusubtype) failed";
    #endif
    }
    
  • 4.4 Luego mira hacia abajo, aquí se creará una instancia del archivo macho:
  •     try {
            // 实例化主程序,也就是machO这个可执行文件
            sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
            sMainExecutable->setNeverUnload();
            gLinkContext.mainExecutable = sMainExecutable;
            gLinkContext.processIsRestricted = sProcessIsRestricted;
            // 加载共享缓存库
            checkSharedRegionDisable();
        #if DYLD_SHARED_CACHE_SUPPORT
            if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion )
                mapSharedCache();
        #endif
    
  • 4.5 Ingrese el código del programa principal instanciado de la siguiente manera. Después de la carga, se devolverá una clase de carga de imágenes ImageLoader. Esta es una clase abstracta utilizada para cargar clases en un formato de archivo ejecutable específico. Se crearán las bibliotecas dependientes y las bibliotecas de complementos necesarios en el programa Un objeto de imagen correspondiente, vincular estas imágenes, llamar al método de inicialización de cada imagen, etc., incluida la inicialización del tiempo de ejecución.
  • {
        // isCompatibleMachO 是检查mach-o的subtype是否是当前cpu可以支持
        if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
            ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
    //将image添加到imagelist。所以我们在Xcode使用image list命令查看的第一个便是我们的machO
            addImage(image);
            return image;
        }
        
        throw "main executable not a known format";
    }
    
  • 4.6 Utilice el comando image list para demostrar la siguiente figura: La primera dirección 0x000000010401c000 que ve es la dirección del archivo ejecutable macho.

     

     

  • 4.7 Después de crear una instancia del archivo macho, verá un método checkSharedRegionDisable (), donde se carga la biblioteca de caché compartida. ¿Qué es esta biblioteca de caché compartida? De hecho, podemos entenderlo como una biblioteca dinámica compartida por el sistema (Apple prohíbe a terceros el uso de bibliotecas dinámicas). Por ejemplo, el marco UIKit más utilizado se encuentra en la biblioteca de caché compartida. Por ejemplo, WeChat, QQ, Alipay, Tmall y otras aplicaciones usarán el marco UIKit. Si todas las aplicaciones cargan UIKit, inevitablemente se producirá escasez de memoria. Entonces, de hecho, estas aplicaciones compartirán un conjunto de marco UIKit, y los métodos correspondientes en el marco UIKit se utilizan en las aplicaciones, y dyld usará los recursos correspondientes para estas aplicaciones. La siguiente figura muestra la biblioteca del marco en la biblioteca del sistema del teléfono con jailbreak, que también lo demuestra:

     

    Biblioteca de caché compartida

  • 5. Insertar biblioteca: continuemos mirando el código fuente restante en este método. Todas las bibliotecas insertadas se cargarán aquí. La inyección de código en la dirección inversa se completa en este paso. Para el proceso de inyección de código detallado del marco, consulte mi artículo . Existe una operación de sAllImages.size () - 1, que en realidad excluye el programa principal.

  •     // load any inserted libraries
            if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
                for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
                    loadInsertedDylib(*lib);
            }
            // record count of inserted libraries so that a flat search will look at 
            // inserted libraries, then main, then others.
            sInsertedDylibCount = sAllImages.size()-1;
    
    
    

    6. Vincular el programa principal: llame internamente al método de vínculo a través del objeto de instancia de imageLoader para cargar de forma recursiva las bibliotecas del sistema dependientes y las bibliotecas de terceros.

            // link main executable
            gLinkContext.linkingMainExecutable = true;
            link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, ImageLoader::RPathChain(NULL, NULL));
            gLinkContext.linkingMainExecutable = false;
            if ( sMainExecutable->forceFlat() ) {
                gLinkContext.bindFlat = true;
                gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
            }
            result = (uintptr_t)sMainExecutable->getMain();
    

    7. Función de inicialización

    10

    8. Ejecute el programa de inicialización:

     

  • 8.1 Recursividad: cargue las bibliotecas del sistema dependientes y las bibliotecas de terceros que necesitemos.

     

    12

  • 9. La función notifySingle, que es una función clave para establecer contacto con el tiempo de ejecución:

    13

  • 9.1 Encontramos que el método load_images fue llamado en la función notifySingle, hicimos clic y encontramos que este es un puntero de función, y la llamada a load_images no se encontró en él. A través de la búsqueda global del archivo dyld, no se encontró. Entonces, en este momento inferimos que se llama en tiempo de ejecución, solo porque el código de tiempo de ejecución de objc también es de código abierto, luego descargamos el código fuente de objc para su análisis.
    void     (*notifySingle)(dyld_image_states, const ImageLoader* image);
    
  • 9.2 En objc_init encontraremos la llamada, aquí load_images.
  •   _dyld_objc_notify_register(&map_images, load_images, unmap_image);
    

    14

  • 9.3 Complete la llamada call_load_methods en load_images, aquí está el método de carga para cargar todos los archivos de clase y archivos de clasificación:
  • load_images(const char *path __unused, const struct mach_header *mh)
    {
        // 如果这里没有+load方法,则返回时不带锁
        if (!hasLoadMethods((const headerType *)mh)) return;
    
        recursive_mutex_locker_t lock(loadMethodLock);
    
        // 发现load方法
        {
            mutex_locker_t lock2(runtimeLock);
            prepare_load_methods((const headerType *)mh);
        }
    
        // 加载所有load方法
        call_load_methods();
    }
    
  • 9.4 llamar al método call_load_methods, en call_load_methods, llamar a call_class_loads para cargar el método de carga de cada clase a través del bucle doWhile, y luego cargar el método de cargas clasificadas.
  • void call_load_methods(void)
    {
        static bool loading = NO;
        bool more_categories;
    
        loadMethodLock.assertLocked();
    
        // Re-entrant calls do nothing; the outermost call will finish the job.
        if (loading) return;
        loading = YES;
    
        void *pool = objc_autoreleasePoolPush();
    
        do {
            // 1. 循环调用所有类文件的laod方法
            while (loadable_classes_used > 0) {
                call_class_loads();
            }
    
            // 2.调用所有分类方法
            more_categories = call_category_loads();
    
            // 3. Run more +loads if there are classes OR more untried categories
        } while (loadable_classes_used > 0  ||  more_categories);
    
        objc_autoreleasePoolPop(pool);
    
        loading = NO;
    }
    
  • 9.5 De ​​acuerdo con la secuencia de llamada anterior, sabemos que el método de carga en el archivo de clase se carga primero y luego se carga el método de carga en el archivo de clase. La demostración se muestra en la figura:

  • 15

  • 10. Después de llamar a notifySigin, encontramos que seguimos llamando a doInitialization, doModInitFunctions llamará a la función en la sección _mod_init_func del archivo machO, que es el constructor global de C ++ que definimos en el archivo.

    // let objc know we are about to initalize this image
    fState = dyld_image_state_dependents_initialized;
    oldState = fState;
    context.notifySingle(dyld_image_state_dependents_initialized, this);
    
    // initialize this image
    this->doInitialization(context);
    
  • 10.1 Entonces, a través de la secuencia de llamada del código anterior, sabemos que la primera carga del archivo de clase, luego la carga del archivo de clase, luego el constructor de C ++ y finalmente ingresamos a nuestro programa principal. La demostración es la siguiente:

     

  • A través del análisis anterior, comenzamos desde el punto de interrupción, verificamos la secuencia de llamada de la pila del método y rastreamos el proceso de carga de dyld paso a paso, lo que indudablemente expondrá el misterio antes de la llamada a la función principal.También puede rastrear la aplicación usted mismo de acuerdo con los pasos anteriores. ¡El proceso de carga será aún más impresionante!

    Resumen: Antes de llamar a la función main (), se realiza una gran cantidad de trabajo preparatorio, principalmente el enlazador dinámico dyld está a cargo. El proceso principal es el siguiente:

    1. La ejecución del programa comienza desde _dyld_star

  • 1.1 Lea la información del archivo macho y configure el desplazamiento de la dirección virtual para la redirección.
  • 1.2 Llame al método dyld :: _ main para ingresar al programa principal del archivo macho.

2. Configure algunas variables de entorno

  • 2.1 Las variables de entorno establecidas nos resultan convenientes para imprimir más información.
  • 2.1. Llame a getHostInfo () para obtener el encabezado machO para obtener información sobre la arquitectura actual en ejecución.

3. Cree una instancia del programa principal, que es el archivo ejecutable macho.

4. Cargue la biblioteca de caché compartida.

5. Inserte la biblioteca de caché dinámica.

6. Vincular el programa principal.

7. Función de inicialización.

  • 7.1 Después de una serie de funciones de inicialización, finalmente se llama a la función notifSingle.
  • 7.2. Esta devolución de llamada es una función load_images asignada cuando se inicializa mediante runtime _objc_init
  • 7.3 La función call_load_methods se ejecuta en load_images, y el método de carga de la clase y clasificación utilizadas se llama cíclicamente.
  • 7.4 La función doModInitFunctions llama internamente al constructor del objeto global C ++, es decir, funciones como _ _ atributo_ _ ((constructor)).

8. Regrese a la función de entrada del programa principal y comience a ingresar a la función principal () del programa principal.



Enlace: https://www.jianshu.com/p/1b9ca38b8b9f

Supongo que te gusta

Origin blog.csdn.net/wangletiancsdn/article/details/104740563
Recomendado
Clasificación