1. El recorrido completo
Desde el tema 4 compilamos todos los programas con una sola línea, gcc archivo.c -o programa, y los ejecutamos con ./programa. Detrás de esos dos comandos hay cinco transformaciones. Las cuatro primeras las hace gcc, que en realidad es un coordinador que invoca a otras herramientas; la quinta la hace el sistema operativo.
| Etapa | Entrada → salida | Qué hace | Opción de gcc |
|---|---|---|---|
| 1. Preprocesar | .c → .i | Resuelve las líneas que empiezan con #. | -E |
| 2. Compilar | .i → .s | Traduce C a ensamblador. | -S |
| 3. Ensamblar | .s → .o | Traduce ensamblador a código máquina. | -c |
| 4. Enlazar | .o → ejecutable | Une las piezas y fija las direcciones. | (ninguna) |
| 5. Cargar | ejecutable → proceso | Lleva el programa a memoria y lo pone en marcha. | La hace el kernel |
Las opciones de la última columna le piden a gcc que se detenga al terminar esa etapa. Las usaremos para mirar cada resultado intermedio.
2. El programa de ejemplo
Usaremos un programa pequeño pero con los ingredientes necesarios: una macro, una variable global con valor inicial, otra sin él, una función propia y una llamada a una función de biblioteca.
#include <stdio.h>
#define VECES 3
int contador = 10; /* global con valor inicial */
int acumulado; /* global sin valor inicial */
int doble(int n)
{
return n * 2;
}
int main(void)
{
for (int i = 0; i < VECES; i++)
acumulado += doble(contador);
printf("Acumulado: %d\n", acumulado);
return 0;
}
$ gcc -Wall -Wextra -g etapas.c -o etapas
$ ./etapas
Acumulado: 60
Ahora repetiremos esa compilación paso a paso. Para que los resultados intermedios sean más fáciles de leer, en las secciones siguientes omitimos la opción -g.
3. Etapa 1: preprocesar
El preprocesador trabaja sobre el texto, sin entender C. Se ocupa de las directivas, las líneas que empiezan con #: sustituye cada #include por el contenido completo del archivo nombrado, reemplaza cada macro definida con #define por su valor y elimina los comentarios.
$ gcc -E etapas.c -o etapas.i
$ wc -l etapas.c etapas.i
20 etapas.c
752 etapas.i
$ tail -n 6 etapas.i
for (int i = 0; i < 3; i++)
acumulado += doble(contador);
printf("Acumulado: %d\n", acumulado);
return 0;
}
El archivo pasó de 20 líneas a varios cientos —la cantidad exacta depende del sistema—, porque ahora contiene todo stdio.h y los archivos que este incluye a su vez. Al final sigue estando nuestro código, con una diferencia: VECES ya no existe; en su lugar está el 3.
Conviene saber qué hay en esas líneas añadidas: declaraciones, no código. stdio.h le dice al compilador que existe una función printf y qué parámetros recibe, para que pueda verificar la llamada. Las instrucciones de printf no están ahí; llegarán en la etapa 4.
4. Etapa 2: compilar
El compilador propiamente dicho hace el trabajo difícil: analiza el texto C, comprueba que sea correcto y lo traduce a lenguaje ensamblador de la arquitectura de destino. Aquí es donde aparecen los errores y las advertencias que aprendimos a leer en el tema 4.
$ gcc -S etapas.i -o etapas.s
$ cat etapas.s
…
doble:
pushq %rbp
movq %rsp, %rbp
movl %edi, -4(%rbp)
movl -4(%rbp), %eax
addl %eax, %eax
popq %rbp
ret
…
Con lo visto en el tema 6 se puede leer la función entera: las dos primeras instrucciones preparan el marco de pila; la tercera guarda en él el parámetro n, que llegó en el registro edi; la cuarta lo trae a eax; la quinta lo suma consigo mismo, que es la forma más barata de multiplicar por dos; y las dos últimas deshacen el marco y vuelven a quien llamó, dejando el resultado en eax.
Este archivo sigue siendo texto, y es específico de una arquitectura. En un equipo ARM, el mismo etapas.i produce un etapas.s completamente distinto.
5. Etapa 3: ensamblar
El ensamblador convierte cada línea de ensamblador en los bytes de la instrucción de máquina correspondiente. El resultado es un archivo objeto, con extensión .o, que ya no es texto.
$ gcc -c etapas.s -o etapas.o
$ file etapas.o
etapas.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
$ nm etapas.o # la tabla de símbolos
0000000000000000 B acumulado
0000000000000000 D contador
0000000000000000 T doble
0000000000000012 T main
U printf
El archivo objeto contiene código máquina, pero todavía no se puede ejecutar. El comando nm muestra por qué. Lista los símbolos, que son los nombres de funciones y variables globales, con una letra que indica dónde está cada uno:
| Letra | Significado | En nuestro ejemplo |
|---|---|---|
T | Código (text) | doble y main |
D | Datos con valor inicial | contador |
B | Datos sin valor inicial | acumulado |
U | No definido aquí (undefined) | printf |
Hay dos asuntos pendientes. El primero es que las direcciones no son definitivas: tres símbolos distintos figuran en la dirección cero, porque cada zona del archivo se numera desde el principio por separado. Por eso file lo describe como relocatable, reubicable. El segundo es que falta código: el archivo llama a printf, pero no la contiene. Resolver ambos es el trabajo de la última etapa.
6. Etapa 4: enlazar
El enlazador (linker) toma uno o más archivos objeto y construye con ellos un ejecutable. Hace tres cosas:
- Combina. Junta el código de todos los archivos objeto en una sola zona de código, y lo mismo con los datos. Añade además un pequeño archivo objeto que la biblioteca de C aporta a todo programa, con el código de arranque.
- Resuelve símbolos. Para cada símbolo no definido busca quién lo define, en los otros archivos objeto o en las bibliotecas. Si no lo encuentra, se produce el conocido error
undefined reference. - Reubica. Asigna a cada función y a cada variable su dirección definitiva y corrige todas las instrucciones que las mencionan.
$ gcc etapas.o -o etapas
$ file etapas
etapas: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, not stripped
$ nm etapas | grep -E ' (doble|main|_start)$'
0000000000001149 T doble
000000000000115b T main
0000000000001060 T _start
Ahora cada símbolo tiene su propia dirección: main ya no está en 0x12, sino 18 bytes después de doble, exactamente como en el archivo objeto, pero desplazadas ambas a su lugar final. Aparece también un símbolo que no escribimos, _start, del que hablaremos en la sección 9.
Esta etapa es la que permite dividir un programa grande en muchos archivos fuente, compilar cada uno por separado y volver a compilar solo los que cambian. El curso Programación en C para Sistemas Operativos de esta ruta lo desarrolla en detalle.
7. El formato ELF
Un ejecutable no es una simple secuencia de instrucciones. Es un archivo con una estructura precisa, que le indica al sistema operativo cómo cargarlo. En Linux, y en casi todos los sistemas tipo Unix, ese formato se llama ELF (Executable and Linkable Format). Windows usa otro, llamado PE, y macOS otro más, Mach-O; es una de las razones por las que un ejecutable de un sistema no funciona en otro aunque el procesador sea el mismo.
Todo archivo ELF empieza con una cabecera que describe el resto:
$ xxd -l 16 etapas # los primeros 16 bytes del archivo
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
$ readelf -h etapas
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Entry point address: 0x1060
Cada campo responde una pregunta que el kernel se hace antes de cargar el archivo:
- ¿Es un ejecutable? Los cuatro primeros bytes son siempre
7f 45 4c 46, es decir, el byte0x7fseguido de las letras «ELF». Se lo llama número mágico, y el kernel lo usa para reconocer el formato; la extensión del archivo no interviene. - ¿Para qué máquina? Class, Data y Machine indican 64 bits, orden little endian y arquitectura x86-64. Si no coinciden con el procesador, el kernel rechaza el archivo con el error «Exec format error».
- ¿Por dónde empieza? Entry point address es la dirección de la primera instrucción que debe ejecutarse: el valor inicial del contador de programa.
8. Secciones y segmentos
Después de la cabecera, el contenido del archivo está organizado en secciones, cada una con un tipo de información. Cuatro de ellas forman el programa en sí:
| Sección | Contiene | En nuestro ejemplo | Permisos en memoria |
|---|---|---|---|
.text | Las instrucciones de máquina. | doble, main, _start | Lectura y ejecución |
.rodata | Datos constantes. | La cadena "Acumulado: %d\n" | Solo lectura |
.data | Variables globales con valor inicial. | contador, con el valor 10 | Lectura y escritura |
.bss | Variables globales sin valor inicial. | acumulado | Lectura y escritura |
La sección .bss tiene una particularidad: no ocupa espacio en el archivo. Como sus variables no tienen valor inicial, no hay nada que guardar; el archivo solo anota cuántos bytes harán falta, y es el kernel quien, al cargar el programa, entrega esa memoria llena de ceros. Por eso en C una variable global sin inicializar vale siempre cero. El comando size resume los tamaños:
$ size etapas
text data bss dec hex filename
1574 604 12 2190 88e etapas
Para cargar el programa, el kernel no mira las secciones una por una, sino una segunda tabla del archivo que las agrupa en segmentos: bloques que deben llevarse a memoria juntos y con los mismos permisos.
$ readelf -lW etapas | grep LOAD
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x000628 0x000628 R 0x1000
LOAD 0x001000 0x0000000000001000 0x0000000000001000 0x0001a5 0x0001a5 R E 0x1000
LOAD 0x002000 0x0000000000002000 0x0000000000002000 0x0000f4 0x0000f4 R 0x1000
LOAD 0x002db8 0x0000000000003db8 0x0000000000003db8 0x00025c 0x000268 RW 0x1000
La primera línea de encabezados la hemos añadido para facilitar la lectura. Cada línea LOAD es una instrucción para el kernel: «toma tantos bytes del archivo a partir de esta posición y colócalos en esta dirección, con estos permisos». Dos columnas merecen atención:
Flg, los permisos. El segundo segmento, que contiene el código, esR E: se puede leer y ejecutar, pero no escribir. El último, con los datos, esRW: se puede leer y escribir, pero no ejecutar. Aquí está el origen de algo que vimos en el tema 5: intentar modificar los bytes demaintermina en «Segmentation fault». Es la defensa contra el riesgo del programa almacenado: ni se puede alterar el código ni se pueden ejecutar los datos.FileSizyMemSiz. En el último segmento no coinciden: ocupa0x25cbytes en el archivo y0x268en memoria. La diferencia, 12 bytes, es exactamente el tamaño de.bssque informósize.
9. La ejecución no empieza en main
La cabecera indicaba como punto de entrada la dirección 0x1060, y nm mostró que ahí está _start, no main. La función main es el comienzo de tu código, pero no del programa.
_start pertenece al código de arranque que el enlazador añadió en la etapa 4. Hace el trabajo previo que C da por supuesto y que nadie más haría:
- Recoge los argumentos de la línea de comandos y las variables de entorno, que el kernel dejó en la pila.
- Inicializa la biblioteca de C; por ejemplo, la memoria intermedia que usa
printf. - Llama a
main. - Cuando
maindevuelve un valor, lo pasa a la llamada al sistema que finaliza el proceso.
El cuarto punto explica algo que usamos desde el tema 4: el return 0 de main se convierte en el código de salida que la shell guarda en $?. Y es necesario, porque main no tiene a quién volver. Si la CPU ejecutara un ret sin nadie detrás, seguiría interpretando como instrucciones lo que hubiera a continuación en la memoria. Un programa nunca «se acaba» por sí solo: termina porque le pide al kernel que lo finalice.
10. Enlace dinámico y estático
El comando file describió el ejecutable como dynamically linked, y eso resuelve un misterio pendiente: el código de printf tampoco está en el ejecutable final. El enlazador solo dejó anotado que el programa necesita esa función y en qué biblioteca se encuentra.
$ ldd etapas # bibliotecas que necesita el ejecutable
linux-vdso.so.1 (0x00007ffd0b5f2000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x000079f1c2800000)
/lib64/ld-linux-x86-64.so.2 (0x000079f1c2b3e000)
libc.so.6 es la biblioteca de C, guardada en un archivo aparte como biblioteca compartida. Se incorpora al programa en el momento de ejecutarlo, no al compilarlo. Esa es la modalidad de enlace dinámico, la habitual. La alternativa es el enlace estático, que copia dentro del ejecutable todo el código de biblioteca que usa:
$ gcc -static etapas.c -o etapas_estatico
$ ls -lh etapas etapas_estatico | awk '{print $5, $9}'
16K etapas
767K etapas_estatico
$ ldd etapas_estatico
not a dynamic executable
| Enlace dinámico | Enlace estático | |
|---|---|---|
| Tamaño del ejecutable | Pequeño | Grande: lleva las bibliotecas dentro |
| Uso de memoria | Todos los procesos comparten una sola copia de cada biblioteca | Cada proceso tiene su copia |
| Actualizar una biblioteca | Basta con reemplazar su archivo | Hay que volver a enlazar cada programa |
| Dependencias | Necesita que la biblioteca esté instalada, en una versión compatible | Ninguna: funciona en cualquier sistema con ese kernel |
| Arranque | Algo más lento | Inmediato |
La fila del uso de memoria es la que más interesa a un sistema operativo. Casi todos los programas en ejecución usan la biblioteca de C. Con enlace dinámico, su código se carga en memoria física una sola vez y el kernel lo hace aparecer en el espacio de direcciones de cada proceso; es posible porque ese código es de solo lectura.
En Ubuntu y Debian la versión estática de la biblioteca de C viene con build-essential. En Fedora hay que instalar el paquete glibc-static.
11. La carga: qué hace el kernel
Llegamos a la quinta transformación, la que convierte un archivo en un programa en marcha. Cuando escribes ./etapas, la shell le pide al kernel, mediante la llamada al sistema execve, que ejecute ese archivo. La parte del kernel que lo hace se llama cargador (loader):
- Abre el archivo y comprueba los permisos. El usuario debe tener permiso de ejecución sobre él. Si no, el resultado es «Permission denied».
- Lee la cabecera y reconoce el formato. Encuentra el número mágico de ELF y verifica que la arquitectura sea la del procesador.
- Prepara un espacio de direcciones nuevo. Descarta la memoria del programa anterior y crea una vacía.
- Mapea los segmentos. Recorre las líneas
LOADy asocia cada rango de direcciones con la parte correspondiente del archivo, aplicando sus permisos. Reserva además la memoria en ceros de.bss. - Crea la pila. Reserva la zona de pila y deposita en ella los argumentos de la línea de comandos y las variables de entorno.
- Prepara el enlace dinámico. Si el ejecutable lo pide, mapea también el programa indicado en su cabecera como intérprete: el enlazador dinámico,
ld-linux. - Entrega la CPU. Coloca en el contador de programa el punto de entrada y en el puntero de pila el tope de la pila nueva, y pasa a modo usuario.
A partir de ahí, el kernel queda detenido y se ejecuta código de usuario: primero el enlazador dinámico, que localiza libc.so.6, la mapea en memoria y conecta la llamada a printf con su código; luego _start; y finalmente main.
En el paso 4 el kernel no lee el ejecutable completo. Solo anota qué parte del archivo corresponde a cada rango de direcciones. El contenido se trae del disco más tarde, por partes, la primera vez que el programa toca cada zona. Un programa enorme arranca así casi al instante, y las funciones que nunca se usan nunca llegan a ocupar memoria. El mecanismo se llama carga por demanda y se apoya en la caché de disco del tema 7.
Toda la secuencia puede observarse con strace:
$ strace ./etapas
execve("./etapas", ["./etapas"], 0x7ffe5d3a8c10 /* 36 vars */) = 0
brk(NULL) = 0x5d0e2a4c9000
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
…
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0"..., 832) = 832
mmap(NULL, 2170256, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x79f1c2800000
…
write(1, "Acumulado: 60\n", 14) = 14
exit_group(0) = ?
+++ exited with 0 +++
La primera línea es la carga misma. Todo lo que sigue hasta el write es el trabajo del enlazador dinámico: abre libc.so.6, lee su cabecera —se distingue el número mágico \177ELF, porque una biblioteca compartida es también un archivo ELF— y la mapea con mmap. Queda así explicado por qué, en el tema 4, un programa de veinte líneas hacía decenas de llamadas al sistema antes de llegar a main. Las dos últimas son las nuestras: el printf y la finalización que hace _start con el valor devuelto por main.
12. El programa en memoria
El resultado de la carga es una disposición de la memoria que se repite, con pocas variantes, en todos los procesos:
El kernel publica el mapa de memoria de cada proceso en un archivo. El siguiente comando muestra el del propio cat que lo lee:
$ cat /proc/self/maps
5c1e3a2f4000-5c1e3a2f6000 r--p 00000000 103:02 1311499 /usr/bin/cat
5c1e3a2f6000-5c1e3a2fb000 r-xp 00002000 103:02 1311499 /usr/bin/cat
5c1e3a2fb000-5c1e3a2fe000 r--p 00007000 103:02 1311499 /usr/bin/cat
5c1e3a2ff000-5c1e3a300000 rw-p 0000a000 103:02 1311499 /usr/bin/cat
5c1e5b0a1000-5c1e5b0c2000 rw-p 00000000 00:00 0 [heap]
79f1c2800000-79f1c2828000 r--p 00000000 103:02 1315020 /usr/lib/x86_64-linux-gnu/libc.so.6
…
7ffd0b4e1000-7ffd0b502000 rw-p 00000000 00:00 0 [stack]
Cada línea es un rango de direcciones con sus permisos y, a la derecha, el archivo del que procede. Se reconocen los segmentos LOAD del ejecutable con los mismos permisos que mostró readelf, la biblioteca de C mapeada por el enlazador dinámico, y dos zonas sin archivo detrás: el heap, de donde sale la memoria que se pide con malloc, y la pila. Estudiamos este mapa con detenimiento en el tema 27.
13. Programa y proceso
Este tema permite precisar una distinción que hasta ahora usamos de manera informal:
| Programa | Proceso | |
|---|---|---|
| Qué es | Un archivo: instrucciones y datos iniciales. | Un programa en ejecución. |
| Dónde está | En el disco. | En la memoria. |
| Naturaleza | Pasivo: no hace nada por sí mismo. | Activo: tiene un contador de programa que avanza. |
| Qué más tiene | Nada. | Registros, pila, heap, archivos abiertos, un PID, un usuario. |
| Duración | Hasta que se borra el archivo. | Desde que se carga hasta que termina. |
Un programa es una receta; un proceso es alguien cocinándola. De un mismo programa pueden existir varios procesos a la vez, cada uno con su propio estado: es lo que hicimos en el tema 2 al lanzar dos copias de ilusion, que partían del mismo archivo y llevaban contadores distintos.
Con esto cerramos el bloque de hardware. Sabemos qué ejecuta la CPU, dónde están los datos, cómo se habla con los dispositivos y cómo llega un programa a la memoria. En todos los casos apareció alguien que organiza, protege y decide: el kernel. El bloque 3 se ocupa de él.
14. ¿Y los lenguajes interpretados?
Un script de Bash o de Python es texto, no un archivo ELF. Sin embargo, en el tema 4 ejecutamos ./verificar.sh como si fuera un programa. La explicación está en el paso 2 de la carga: el kernel reconoce un segundo número mágico, los caracteres #! al comienzo del archivo.
Cuando los encuentra, lee el resto de esa primera línea, que contiene la ruta de un programa —por ejemplo, /bin/bash—, y carga ese programa, pasándole el nombre del script como argumento. Lo que se ejecuta es el intérprete, un ejecutable ELF como cualquier otro, que lee el texto y hace lo que este indica.
Ocurre lo mismo con Python, JavaScript o Java: el proceso que ve el kernel es el intérprete o la máquina virtual del lenguaje. Tu programa es, para el sistema operativo, un archivo de datos que ese proceso lee. Por debajo, todo lo que se ejecuta en Linux termina siendo un ELF con instrucciones de máquina.
15. Resumen
gccencadena cuatro etapas: preprocesar, compilar, ensamblar y enlazar.- Un archivo objeto contiene código máquina con direcciones provisionales y símbolos sin resolver. El enlazador une las piezas y fija las direcciones.
- El ejecutable sigue el formato ELF: una cabecera con el número mágico, la arquitectura y el punto de entrada, y el contenido repartido en secciones.
.textguarda el código,.rodatalas constantes,.datalas variables globales con valor inicial y.bsslas que no lo tienen, que no ocupan espacio en el archivo.- Los segmentos indican al kernel qué cargar y con qué permisos: el código no se puede escribir y los datos no se pueden ejecutar.
- La ejecución empieza en
_start, que prepara el entorno, llama amainy pide al kernel finalizar el proceso con el valor devuelto. - Con enlace dinámico, las bibliotecas se incorporan al ejecutar y se comparten entre procesos; con enlace estático, se copian dentro del ejecutable.
- Al ejecutar un archivo, el kernel verifica el formato, crea un espacio de direcciones, mapea los segmentos, prepara la pila y entrega la CPU en el punto de entrada.
- Un programa es un archivo pasivo; un proceso es ese programa en ejecución, con su propio estado.
16. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué hace cada una de las cuatro etapas de la compilación?
El preprocesador resuelve las directivas #include y #define, y produce texto C expandido. El compilador traduce ese texto a ensamblador. El ensamblador lo convierte en código máquina dentro de un archivo objeto. El enlazador combina los archivos objeto, resuelve los símbolos y produce el ejecutable.
2. stdio.h se incluye en la etapa 1. ¿Significa eso que el código de printf ya está en el programa?
No. stdio.h solo contiene la declaración de printf, para que el compilador pueda verificar la llamada. El código está en la biblioteca de C, y se incorpora al enlazar si el enlace es estático, o al ejecutar si es dinámico.
3. ¿Por qué un archivo objeto no se puede ejecutar?
Porque sus direcciones no son definitivas, puede tener símbolos sin definir, como printf, y le falta el código de arranque con el punto de entrada. Todo eso lo completa el enlazador.
4. Un programa declara int tabla[1000000]; como variable global sin valor inicial. ¿Aumenta en 4 MB el tamaño del ejecutable?
No. La variable va a la sección .bss, que no ocupa espacio en el archivo: solo se anota su tamaño. El kernel reserva esa memoria, en ceros, al cargar el programa. Si se le diera un valor inicial distinto de cero, iría a .data y sí ocuparía ese espacio.
5. ¿Cómo sabe el kernel que un archivo es un ejecutable y no, por ejemplo, una imagen?
Por los primeros bytes del archivo. Un ELF empieza con el número mágico 7f 45 4c 46, y un script con #!. La extensión del nombre no influye. Además, el archivo debe tener permiso de ejecución.
6. ¿Por qué el segmento de código es de lectura y ejecución, pero no de escritura?
Para impedir que el programa modifique sus propias instrucciones, por error o por un ataque. Además, al no cambiar, ese código puede compartirse entre todos los procesos que ejecutan el mismo programa o usan la misma biblioteca.
7. ¿Qué pasaría si main terminara con un ret y no existiera _start?
No habría una dirección de retorno válida ni nadie que finalizara el proceso. La CPU saltaría a una dirección arbitraria e interpretaría como instrucciones lo que hubiera allí, lo que casi seguro acabaría en un fallo. _start evita eso llamando a main y pidiendo luego al kernel que termine el proceso.
8. Da una ventaja del enlace dinámico y una del estático.
Dinámico: los ejecutables son pequeños y todos los procesos comparten en memoria una sola copia de cada biblioteca, que además puede actualizarse sin recompilar los programas. Estático: el ejecutable no depende de las bibliotecas instaladas, de modo que funciona en cualquier sistema con un kernel compatible.
9. Al ejecutar ./verificar.sh, ¿qué programa carga realmente el kernel?
Carga /bin/bash, el intérprete indicado en la primera línea del script tras #!, y le pasa el nombre del script como argumento. El proceso resultante es Bash, que lee el archivo y ejecuta sus comandos.
Para practicar
- Reproduce las cuatro etapas con
etapas.cy examina cada archivo intermedio. ¿Cuántas líneas tiene tuetapas.i? - Comenta la línea
#include <stdio.h>y compila. ¿En qué etapa aparece el problema y de qué tipo es? Restáurala y cambiaprintfporimprimir: ¿qué etapa falla ahora y con qué mensaje? - Ejecuta
objdump -s -j .rodata etapasy localiza la cadena de formato. Luegoobjdump -s -j .data etapasy busca el valor 10 decontador. ¿En qué orden están sus bytes? - Agrega a
etapas.cla variable globalint grande[100000];y compara el tamaño del ejecutable conls -ly la columnabssdesize, antes y después. Repite dándole un valor inicial:int grande[100000] = {1};. - Usa
strace -cpara comparar la cantidad de llamadas al sistema deetapasy deetapas_estatico. Explica la diferencia. - Ejecuta
file /bin/ls /bin/bash $(which python3). ¿Qué formato tienen los intérpretes?