El viaje antes de que se ejecute la función main()
(amit.prasad.me)- Antes de que se ejecute un programa, se explora el proceso mediante el cual el kernel crea e inicializa un proceso a través de la llamada al sistema
execve - Esta llamada pasa la ruta del ejecutable, los argumentos y las variables de entorno, y con base en ello el kernel carga un archivo ejecutable en formato ELF
- Un archivo ELF incluye código, datos, símbolos e información de enlace dinámico, y el kernel lo interpreta para realizar el mapeo de memoria y la inicialización de la pila
- Después, el kernel transfiere el control al punto de entrada
_start, y solo tras inicializarse el runtime de cada lenguaje se llama a la funciónmaindefinida por el usuario - Este proceso muestra la estructura de colaboración entre el sistema operativo, el compilador y el runtime, y es importante para entender cómo se ejecuta un programa a nivel de sistema
El punto de inicio de la ejecución de un programa: llamada a execve
- En Linux, la ejecución de un programa comienza mediante la llamada al sistema
execve- Con la forma
execve(const char *filename, char *const argv[], char *const envp[]), se pasan el nombre del ejecutable, la lista de argumentos y la lista de variables de entorno - A través de esto, el kernel determina qué programa ejecutar y en qué entorno
- Con la forma
- En lenguajes de alto nivel, esta llamada está envuelta por la API de ejecución de procesos de la biblioteca estándar
- Ejemplo:
std::process::Commandde Rust llama internamente aexecve - Realiza un proceso similar a la búsqueda de PATH en el shell, convirtiendo el nombre del comando en una ruta completa
- Ejemplo:
- En el caso de scripts con Shebang(
#!), el kernel ejecuta el programa usando el intérprete especificado- Ejemplo:
#!/usr/bin/python3→ se ejecuta con el intérprete de Python
- Ejemplo:
ELF: la estructura del archivo ejecutable
- Los archivos ejecutables en Linux siguen el formato ELF (Executable and Linkable Format)
- ELF es un formato estándar de ejecutables que incluye código, datos, símbolos e información de reubicación
- Otros sistemas operativos usan formatos distintos como Mach-O (macOS) o PE (Windows)
- El encabezado ELF contiene información sobre la estructura del archivo y su disposición en memoria
- Ejemplos de campos:
ELF Magic,Class,Entry point address,Program headers,Section headers Entry point addresses la dirección de la primera instrucción que se ejecutará del programa
- Ejemplos de campos:
- En el ejemplo del encabezado ELF, se trata de un ejecutable ELF32 para arquitectura RISC-V, con
0x10358como punto de entrada
Componentes internos de ELF
- Un archivo ELF está compuesto por varias secciones (
section).text: código ejecutable.data: variables globales inicializadas.bss: variables globales no inicializadas.plt: tabla para llamadas a bibliotecas compartidas.symtab,.strtab: tablas de símbolos y cadenas
- La PLT (Procedure Linkage Table) permite las llamadas a funciones de bibliotecas compartidas
- Ejemplo:
printf,mallocdelibc - La sección
PT_INTERPde ELF especifica el enlazador dinámico (interpreter)
- Ejemplo:
- El kernel lee el ELF para ubicar en memoria las secciones cargables y, cuando corresponde, aplica funciones de seguridad como ASLR y el bit NX
Tabla de símbolos y enlace en tiempo de ejecución
- La tabla de símbolos (
symtab) de ELF contiene la información de direcciones de funciones y variables- Ejemplos: existen entradas como
_start,main,__libc_start_main - Incluso un programa simple de “Hello, World!” puede incluir más de 2300 símbolos
- Ejemplos: existen entradas como
- Esto proviene en gran parte de la biblioteca estándar y el código de inicialización del runtime
- Porque se enlazan implementaciones de
libccomomusloglibc
- Porque se enlazan implementaciones de
- Tras cargar cada sección del ELF, el kernel transfiere el control al intérprete (enlazador dinámico)
- El intérprete se encarga de la reubicación (
relocation), la aleatorización de direcciones (ASLR), la configuración de permisos de ejecución (bit NX), etc.
- El intérprete se encarga de la reubicación (
Proceso de inicialización de la pila
- Antes de ejecutar el programa, el kernel debe construir directamente la pila (
stack)- La pila se usa para variables locales, marcos de llamada de funciones y paso de argumentos
- Los
argv,envppasados en la llamada aexecvese almacenan en la pila- El programa accede a los argumentos de línea de comandos y a las variables de entorno a través de ellos
- El kernel también incluye en la pila el vector auxiliar ELF (
auxv)- Contiene unas 30 entradas, como el tamaño de página, metadatos ELF e información del sistema
- Ejemplo:
AT_PAGESZespecifica el tamaño de página de memoria (por ejemplo, 4 KiB)
- En el ejemplo del emulador RISC-V, el puntero de pila (
sp) comienza en una dirección alta y apila en orden inverso los argumentos, las variables de entorno y el vector auxiliar
El punto de entrada y la función _start
- El punto de entrada del ELF se define como la dirección de la función
_start_startes el primer código en espacio de usuario al que el kernel transfiere el control
- La mayoría de los lenguajes realizan en
_startla inicialización del runtime y luego llaman amain- Ejemplo:
std::rt::lang_starten Rust,__libc_start_mainen C
- Ejemplo:
- En el ejemplo de Rust, se pueden usar los atributos
#![no_std]y#![no_main]para definir_startdirectamente sin runtime- Dentro de
_start, se leenargc,argvyenvpdesde la pila y se llama al puntero amain
- Dentro de
- El runtime de cada lenguaje realiza tareas de inicialización específicas del lenguaje, como constructores globales, almacenamiento local de hilo y manejo de excepciones
Flujo completo hasta la llamada a main()
- Todo el proceso puede resumirse así
- Llamada a
execve→ el kernel carga el archivo ELF - Interpretación del ELF → mapeo de las secciones de código/datos, especificación del intérprete
- Construcción de la pila → almacenamiento de argumentos, variables de entorno y vector auxiliar
- Ejecución del punto de entrada
_start - Inicialización del runtime y luego llamada a
main()
- Llamada a
- Esta secuencia muestra la estructura de cooperación entre el kernel del sistema operativo, el formato ELF y el runtime del lenguaje
- El kernel real de Linux incluye lógica interna adicional para el espacio de direcciones, la tabla de procesos, la gestión de grupos, etc., pero este texto explica el flujo esencial de la etapa previa
Conclusión y corrección
- El proceso de ejecución antes de
main()es una combinación de inicialización a nivel kernel y configuración del runtime - Incluso un programa simple de “Hello, World!” se ejecuta después de pasar por una estructura ELF compleja y la inicialización del runtime
- En la versión inicial del texto, parte de la lógica de carga de secciones se atribuía al kernel, pero se corrigió que en realidad corresponde al intérprete ELF
- Este análisis sirve como material base útil para comprender programación de sistemas, compiladores y arquitectura de sistemas operativos
1 comentarios
Comentarios de Hacker News
Explica el proceso de enlazado dinámico de un archivo ELF
El kernel mapea los segmentos PT_LOAD de ELF, carga el enlazador dinámico (ld.so) indicado por PT_INTERP y luego le cede el control
Después, el enlazador dinámico se reubica a sí mismo y carga los objetos compartidos necesarios con mmap/mprotect
Compara esta estructura con el mecanismo de shebang(#!) de los scripts
Comparte que antes se confundió cuando intentó insertar un archivo arbitrario en un ELF con objcopy y el kernel no lo cargó
Al final creó directamente una herramienta para parchear la tabla de encabezados de programa, y dice que esta función también fue añadida al enlazador mold
Artículo relacionado: Self-contained Lone Lisp Applications
Dice que hizo experimentos para empaquetar todo el código antes de main() o incluso sin main()
Artículo relacionado: Packing a codebase into a single function
Bromea con que basta convertir todas las funciones a la forma main(100+n, ...)
Si este tema te interesa, recomienda revisar cpu.land, que él mismo creó
Trata multitarea y el proceso de carga de código más que el layout de memoria
Se pregunta cuántos proyectos en C evitan la biblioteca estándar y llaman directamente solo a syscalls de Linux
Siente que escribir código así es mucho más divertido
Para funciones como ALSA o DRM, hay muchas ventajas en acceder mediante bibliotecas del sistema en lugar de syscalls del kernel
Explica que este enfoque es mejor que el estilo de Windows en términos de portabilidad y mantenibilidad
Ahora lo dejó porque los headers nolibc de Linux están bastante bien,
pero actualmente está desarrollando un lenguaje intérprete Lisp basado en syscalls
Dice que fue un viaje muy interesante experimentar con construir directamente el espacio de usuario de Linux mediante llamadas al sistema
Explica que el intérprete ELF (ld.so) se encarga de toda la carga después de mapear los segmentos ELF iniciales
execve mapea los segmentos PT_LOAD y llena el aux vector en la pila,
luego salta al punto de entrada del intérprete ELF
El kernel no sabe nada sobre PLT/GOT
Como alguien que enseña este tema en la universidad, dice que los estudiantes se confunden por los diagramas de memoria
Los libros los dibujan con las direcciones más altas arriba, pero en un proceso Linux real
las direcciones bajas aparecen arriba y las altas abajo
En
/proc/<pid>/maps, mientras más te desplazas hacia abajo, mayores son las direccionesEs decir, expresiones como “el heap crece hacia arriba (y la stack crece hacia abajo)” solo indican una dirección numérica,
pero visualmente es más bien al revés
Propone que sería mucho más intuitivo dibujarlo como en un IDE, donde al bajar aumentan las direcciones
Aun así, propone que visualizarla en horizontal sería más natural
Dice que le gusta hacer este tipo de experimentos con microcontroladores PIC16 antiguos
Le parece divertido manipular directamente el puntero de pila, temporizadores, configuración de variables, etc.
Comparte una experiencia relacionada con shebang(#!)
Una aplicación Java mostraba un error diciendo que no podía encontrar el script de ejecución,
pero el problema real era que la ruta del shebang del script estaba mal
En local funcionaba bien, pero el problema surgía porque la ruta del intérprete era distinta en el servidor remoto
Aconseja ejecutarlo con strace para verificar de inmediato en qué syscall ocurrió el error
Dice que al depurar siempre se confunde sobre en qué momento se aplica el orden de reubicación del binario principal
Expresa que parece magia negra saber si ocurre antes o después de que el enlazador resuelva sus propios símbolos
Señala que el enlace de la parte “lang_start function (defined here)” en el Markdown está roto