La carga de binarios en Linux

Se puede decir que los ejecutables son una representación estática de un programa y que en el momento en el que se ejecutan, el kernel utiliza la información incluida en esos ficheros para crear una representación dinámica, más conocida como la imagen del proceso. Antes de poder ejecutar un binario es necesario cargarlo en memoria y el encargado de hacerlo es el loader, que generalmente es parte del sistema operativo.

En los sistemas basados en Linux, que utilizan el estándar ELF  como formato binario, la imagen del proceso se crea cargando e interpretando los segmentos especificados en las cabeceras. Pero antes de hablar sobre el proceso de carga es necesario saber que al crear un ejecutable que utiliza dynamic linking (que son la mayoría de los programas), las bibliotecas compartidas necesarias se ubican y se enlazan en tiempo de ejecución, por lo que el linker añade un campo a las cabeceras del programa del ejecutable de tipo PT_INTERP, indicando al sistema la identidad del linker dinámico (a veces también llamado intérprete) que típicamente es /lib64/ld-linux-x86-64.so.2.  Por otra parte, en el periodo conocido como link-time, el programa (o biblioteca) se construye combinando secciones con atributos similares en segmentos. Normalmente, todas las secciones ejecutables y de datos de sólo-lectura se juntan en un mismo segmento, mientras que el resto de secciones de datos y BSS (variables no inicializadas) se combinan en otro segmento. Estos segmentos típicamente se conocen como segmentos de carga porque es necesario cargarlos en memoria en el momento de la creación del proceso, al contrario que otras secciones como la tabla de símbolos (.symtab) o la información de debugging (.debug_info).

A grandes rasgos, el proceso de carga de un binario ELF se descompone en las siguientes tareas:

  • 1. En primer lugar, se examinan las cabeceras para verificar que el fichero en cuestión tiene un formato ELF compatible.
  • 2. Se recorren las entradas de las cabeceras del programa comprobando si se ha especificado un intérprete (PT_INTERP).
  • 3. Para establecer la memoria virtual, se recorren todos los segmentos de tipo PT_LOAD en el fichero y se mapean dentro del espacio de direcciones del proceso. Después se configuran las páginas inicializadas con ceros correspondientes al segmento BSS. También se configuran otras páginas especiales como las páginas de objetos compartidos dinámicos virtuales (vDSO).
  • 4. Una vez que el código del programa se ha cargado en memoria y como en este caso suponemos que utiliza dynamic linking, el ELF handler también carga el intérprete en memoria. El proceso es prácticamente el mismo que el seguido para cargar el programa original y que hemos explicado en el punto anterior.
  • 5. Por último, la dirección de inicio del programa se configura como el punto de entrada del intérprete en lugar del programa en sí mismo. Esto implica que la ejecución empieza con el intérprete, que se encarga de satisfacer los requisitos de enlace del programa desde el espacio de usuario, es decir, busca y carga las bibliotecas compartidas de las que depende el programa y trata de resolver los símbolos no definidos a las definiciones correctas en dichas bibliotecas. Una vez se ha finalizado este proceso el intérprete puede iniciar la ejecución.

Para una explicación más detallada de este proceso con referencias al código del kernel por favor visitar el arituclo de LWN.net.

Oscar Llorente
Acerca de
Investigador de DT
Expertise: Scam, program analysis