- 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ón main definida 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
- 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::Command de Rust llama internamente a execve
- Realiza un proceso similar a la búsqueda de PATH en el shell, convirtiendo el nombre del comando en una ruta completa
- 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
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 address es la dirección de la primera instrucción que se ejecutará del programa
- En el ejemplo del encabezado ELF, se trata de un ejecutable ELF32 para arquitectura RISC-V, con
0x10358 como 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, malloc de libc
- La sección
PT_INTERP de ELF especifica el enlazador dinámico (interpreter)
- 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
- Esto proviene en gran parte de la biblioteca estándar y el código de inicialización del runtime
- Porque se enlazan implementaciones de
libc como musl o glibc
- 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.
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, envp pasados en la llamada a execve se 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_PAGESZ especifica 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
_start es el primer código en espacio de usuario al que el kernel transfiere el control
- La mayoría de los lenguajes realizan en
_start la inicialización del runtime y luego llaman a main
- Ejemplo:
std::rt::lang_start en Rust, __libc_start_main en C
- En el ejemplo de Rust, se pueden usar los atributos
#![no_std] y #![no_main] para definir _start directamente sin runtime
- Dentro de
_start, se leen argc, argv y envp desde la pila y se llama al puntero a main
- 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()
- 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
Aún no hay comentarios.