logo linux

Anatomía de un proceso: Elfos de Linuxoria

En Linux (y cualquier otro sistema operativo) crear un proceso es algo tan rutinario que rara vez nos paramos a pensar cómo sucede realmente. En este didáctico artículo de Linux Voice (número 18) explican todo lo que siempre has querido saber sobre los procesos en Linux y nunca te has atrevido a preguntar.

La entrada se ha dividido en tres entregas: ‘Elfos de Linuxoria, ‘Rememorando sus recuerdos’ y ‘Observando la naturaleza’.


Anatomía de un proceso

Siendo usuarios de Linux, a menudo engendramos procesos a docenas sin ni siquiera darnos cuenta. Un inocente comando como ‘cat /var/log/file | grep lo que sea’ crea dos y el portátil con Ubuntu en el que estoy escribiendo estas palabras ejecuta 150 procesos a la vez. Los procesos en Linux son un producto en el que rara vez pensamos. A pesar de todo, son entidades fundamentales del sistema operativo y cuan bien los gestione el kernel, afectará directamente en cómo trabajamos.

Es hora de conocer mejor a los procesos y en esta sección vislumbraremos cómo son por dentro. Esto no va solo de hacer méritos: con nuevas herramientas y trucos en tu arsenal, podrías resolver problemas en muchos sistemas mucho más rápido.

htop 2.0
htop(1) es un visor de procesos muy potente.

Elfos de Linuxoria

Si pregunto «¿como creas un proceso?», la mayoría de vosotros probablemente responderá: «no hay más que ejecutar un programa». Eso es cierto, sin embargo no todos los procesos empiezan en el disco. Estrictamente hablando, los procesos Unix nacen en lo que se llama una «bifurcación»: un proceso padre hace una llamada del sistema fork(2)  para crear una copia de sí mismo exacta pero independiente. Los identificadores de procesos (o PIDs) para el padre y el hijo recién nacido son diferentes, y Linux es lo suficientemente inteligente como para no copiar la memoria del proceso (lo cual tendría un coste) a no ser que sea absolutamente necesario. Después, el proceso hijo puede hacer un exec(2) [ejecución] para lanzar un nuevo código ejecutable dentro de sí mismo.

Linux (y la mayoría de sistemas Unix-like) almacenan los programa binarios compilados en ELF que viene de «Executable and Linking format». Deriva del viejo Common Object File Format (COFF) y por lo tanto es primo del formato Portable Executive (PE), el cual usa Windows para sus archivos .exe/.dll. ELF está omnipresente: los archivos objeto que crean los compiladores, bibliotecas compartidas e incluso el kernel de Linux en sí mismo y sus módulos son binarios ELF. Como resultado, hay muchas herramientas (y bibliotecas) que funcionan con ELF. Aquí nos ceñiremos a una: readelf(1).

Piensa un comando simple, digamos, pwd(1). Normalmente es implementado por /bin/pwd (en un sistema embebido podría ser un enlace simbólico BusyBox). ¿Qué podemos aprender de él?

$ readelf -h /bin/pwd
Encabezado ELF:
 Mágico: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
 Clase: ELF64
 Datos: complemento a 2, little endian
 Versión: 1 (current)
 OS/ABI: UNIX - System V
 Versión ABI: 0
 Tipo: EXEC (Fichero ejecutable)
 Máquina: Advanced Micro Devices X86-64
 Versión: 0x1
 Dirección del punto de entrada: 0x4019f7

Lo que estás viendo es la cabecera del archivo ELF. Ya puedes decir que es un binario ejecutable (EXEC) x86 64 bits. (Las bibliotecas compartidas binarias tienen el tipo DYN y los módulos del kernel o los archivos objeto se manifiestan como REL (relocatable)). El punto de entrada es donde el sistema empieza a ejecutar el programa. Por cierto, esto no es la función main() de C como podrías pensar, sino código común residiendo en una biblioteca C de tiempo de ejecución.

Internamente, un archivo ELF está hecho de «secciones». Las secciones pueden contener instrucciones del programa, datos e incluso cosas más enrevesadas como símbolos (ver más abajo). Puedes volcar las secciones con readelf –sections /bin/pwd. Esto producirá una gran cantidad de salidas, así hacer piping a ‘less’ resulta factible. Los nombres de las secciones normalmente empiezan con un punto. Digamos que .text contiene el código de un programa y .data son los datos. También hay una sección .rodata que almacena valores de constantes. .bss es un marcador de posición para los datos no inicializados de tu programa, como variables globales. Este no ocupa espacio en disco y se inicia con ceros en memoria.

Los símbolos son sólo nombres de localizaciones dadas. Son muy útiles en la fase de enlazado pero normalmente se descartan (o se les hace strip(1)) del binario resultante. Una excepción son los símbolos que vienen de bibliotecas dinámicas y que son resueltos en el tiempo de ejecución. Viven en .dynsym y puedes volcarlos con:

$ readelf --symbols /bin/pwd

La tabla de símbolos '.dynsym' contiene 73 entradas:
 Num: Valor Tam Tipo Unión Vis Nombre Ind
...
 3: 0000000000000000 0 FUNC GLOBAL
 DEFAULT UND free@GLIBC_2.2.5 (2)
 ...
 41: 0000000000000000 0 FUNC GLOBAL
 DEFAULT UND malloc@GLIBC_2.2.5 (2)

Puedes ver que incluso un comando simple como pwd hace referencia a varias decenas de símbolos. Vienen de la biblioteca GNU libc (glibc). La lista muestra malloc(3) y free(3) que son formas estándar de asignar y liberar memoria en programas C. Ten en cuenta que los valores de los símbolos son cero ya que se resuelven en el momento de la ejecución. Con un programa C++ la salida tendrá un aspecto ligeramente diferente:

$ readelf --symbols hellocpp 

Symbol table ‘.dynsym’ contains 35 entries:
Num:
Value Size Type Bind Vis Ndx Name 
... 18: 0000000000000000 0 FUNC GLOBAL
DEFAULT UND _ZNSsC1EPKcRKSaIcE@GLIBCXX_3.4 (2) 
19: 0000000000601780 272
OBJECT GLOBAL DEFAULT 25 _ZSt4cout@GLIBCXX_3.4 (2)

Fíjate en los nombres. No parecen ser inteligibles por humanos debido a la mutilación del nombre que usa C++ para implementar sobrecarga y otras características del lenguaje. Haz pipe a la salida a c++filt para «descifrar» los nombres.

También habrás visto secciones como .got (Global Offset Table) o .plt (Procedure LinkageTable). También se usan en el enlazado dinámico: .got almacena los offsets a localizaciones externas (como funciones definidas en bibliotecas o variables) y .plt contiene código para unirlas y llamarlas.

¿De donde viene el enlazador dinámico? (Normalmente, es /lib/ld-linux-x86-64.so.2 en Linux de 64 bits). La sección .interp lo referencia. El kernel se da cuenta de este hecho cuando hace exec(2) y mapea el enlazador antes que tu código. Los binarios estáticos no tiene una sección .interp. «Interp» es una abreviatura de «intérprete» por lo que ‘ld-linux-x86-64.so.2’ técnicamente es un intérprete de binarios dinámicos ELF. No confundir con intérpretes de lenguajes de alto nivel como Python o Perl.

En enlazador dinámico es prácticamente invisible pero puedes influenciar en sus operaciones con variables de entorno. Quizás la más popular de ellas sea LD_LIBRARY_PATH que contiene nombres separados por dos puntos o directorios en los que buscar bibliotecas compartidas. Otra cosa a tener en cuenta es LD_PRELOAD: el enlazador buscará símbolos en la biblioteca que esté primera en la lista, antes de proceder con las usuales. De esa forma puedes escribir una biblioteca personalizada para interceptar, digamos, operaciones de socket y forzar a las aplicaciones a usar un proxy. A esto se le llama «truco del LD_PRELOAD»; www.inet.no tiene un ejemplo en el mundo real. Finalmente mencionemos LD_BIND_NOW. Por defecto, el enlazador resuelve símbolos sólo cuando tu programa accede a ellos. Sin embargo, si esta variables está establecida, todos los símbolos se resuelven al inicio del programa. Esto tarda más en ejecutarse pero después de eso, es más predecible la ejecución (cada llamada a funciones tiene el mismo ‘overhead’).

¿Cómo sabes si el binario es dinámico y qué bibliotecas usa? Ejecuta ldd:

$ ldd /bin/pwd
 linux-vdso.so.1 (0x00007ffc6fd75000)
 libc.so.6 => /lib64/libc.so.6 (0x00007f0d76e82000)
 /lib64/ld-linux-x86-64.so.2 (0x00007f0d77236000)

Esto muestra las bibliotecas, los archivos reales que el enlazador ha encontrado en tu sistema y también carga las direcciones (mira la siguiente sección). Si alguna biblioteca no se encuentra, se informará y así podrás averiguar que le falta a tu programa. Para ejecutables estáticos, Idd simplemente dirá: «no es un ejecutable dinámico».


· Segunda parte: Rememorando sus recuerdos

· Tercera parte: Observando la naturaleza