← Programación en C para Sistemas Operativos Bloque 1 · Del código fuente al programa · Tema 1 de 42

Tema 1

Por qué C para sistemas operativos

Cada comando que escribiste en los cursos anteriores, la shell que los interpretó y el núcleo que los ejecutó tienen algo en común: están escritos, casi por completo, en C. Este tema explica por qué un lenguaje de 1972 sigue siendo el idioma de los sistemas operativos, qué ofrece que otros lenguajes no ofrecen, y qué pide a cambio. Al final habrás compilado y ejecutado tu primer programa, habrás visto dónde viven sus variables en la memoria, y sabrás por qué un error en C puede no producir ningún mensaje.

1. Programas en C que ya usaste

En el curso de Bash escribiste cientos de líneas para la shell. Pero la shell misma es un programa, y se puede preguntar qué clase de archivo es:

terminal
$ file /usr/bin/bash
/usr/bin/bash: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=a292b52b…, for GNU/Linux 3.2.0, stripped
$ ldd /usr/bin/bash
	linux-vdso.so.1 (0x00007ffd6f1dd000)
	libtinfo.so.6 => /usr/lib/x86_64-linux-gnu/libtinfo.so.6 (0x0000759f3438c000)
	libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6 (0x0000759f34000000)
	/lib64/ld-linux-x86-64.so.2 (0x0000759f3454d000)

No es un texto como un script, sino un ejecutable ELF: instrucciones de máquina para un procesador x86-64, en el formato que entiende Linux. Fue escrito en C, compilado, y depende de unas pocas bibliotecas compartidas, entre ellas libc.so.6, la biblioteca de C del sistema. Todo eso, el formato ELF, las bibliotecas compartidas, el cargador ld-linux que las une al arrancar, es tema de este curso.

Lo mismo ocurre con casi todo lo que hay debajo de la terminal: el núcleo Linux, la biblioteca de C, systemd, el servidor SSH, gzip, tar, rsync, el propio compilador de C. Hay excepciones, y van en aumento: algunas distribuciones recientes reemplazaron los comandos básicos como ls por versiones escritas en Rust, y el núcleo admite desde 2022 partes en ese lenguaje. Pero incluso esos programas, al final, le hablan al sistema a través de la biblioteca de C y de interfaces definidas en C. Quien quiera entender cómo funciona un sistema operativo tiene que poder leer C.

2. Qué es C

C es un lenguaje de programación compilado, imperativo y de propósito general, diseñado para escribir sistemas operativos. Sus programas se traducen a instrucciones de máquina antes de ejecutarse, y le dan al programador acceso directo a la memoria a través de direcciones, con muy poca maquinaria entre el código y el procesador.

Cada palabra de esa definición tiene consecuencias que veremos a lo largo del curso:

Se suele decir que C es un lenguaje «de bajo nivel», y es cierto comparado con Python o con Bash. Pero es de más alto nivel que el lenguaje ensamblador: tiene variables con tipo, funciones, estructuras y bucles, y el mismo programa puede compilarse para procesadores muy distintos. Esa combinación, cerca de la máquina y a la vez independiente de una máquina en particular, es la razón de su éxito.

3. Una historia compartida con Unix

C y Unix nacieron juntos, en los Laboratorios Bell de AT&T, y cada uno se hizo a la medida del otro:

Una línea de tiempo: 1969, Unix escrito en ensamblador para la PDP-7; 1970, el lenguaje B; 1972, el lenguaje C; 1973, el núcleo de Unix reescrito en C; 1978, el libro de Kernighan y Ritchie; 1989, el primer estándar, ANSI C; 1991, Linux, escrito en C; 1999, C99; 2011, C11; 2024, C23 1969 Unix, en ensamblador 1970 el lenguaje B 1972 nace C 1973 el núcleo de Unix, reescrito en C 1978 el libro de K&R 1989 ANSI C, el estándar 1991 Linux, escrito en C 1999 C99 2011 C11 2024 C23
Medio siglo de C. Los puntos verdes son los momentos en que C y los sistemas operativos se cruzaron; los ámbar, las versiones del estándar.

En 1969, Ken Thompson escribió la primera versión de Unix en el lenguaje ensamblador de una pequeña computadora, la PDP-7. El ensamblador es la forma legible de las instrucciones de un procesador concreto, y tiene un problema: un sistema escrito en él solo funciona en ese procesador, y para llevarlo a otro hay que reescribirlo entero. Thompson creó entonces un lenguaje más cómodo, B, y Dennis Ritchie lo extendió con tipos de datos y estructuras para aprovechar las máquinas más nuevas. El resultado, hacia 1972, fue C.

El paso decisivo llegó en 1973: el núcleo de Unix se reescribió en C. Era una idea audaz, porque se creía que un sistema operativo solo podía escribirse en ensamblador para ser lo bastante rápido. Funcionó, y cambió la historia: por primera vez, llevar un sistema operativo a una computadora nueva no exigía reescribirlo, sino escribir un compilador de C para esa máquina y adaptar las pocas partes que dependían del hardware. Unix se extendió a universidades y empresas, y C con él.

En 1978, Brian Kernighan y Dennis Ritchie publicaron The C Programming Language, conocido como «K&R», que durante años fue la definición del lenguaje. En 1989 llegó el primer estándar oficial, ANSI C, y desde entonces el lenguaje se revisa cada tantos años. En 1991, Linus Torvalds empezó a escribir Linux, un núcleo nuevo con la forma de Unix, en C. Medio siglo después, el lenguaje sigue siendo el mismo en lo esencial: un programa de los años ochenta, con pocos cambios, compila hoy.

4. Por qué los sistemas operativos se escriben en C

Un sistema operativo tiene necesidades que la mayoría de los programas no tiene. C las cubre todas, y por eso, cincuenta años después, sigue siendo la elección por defecto:

Un sistema operativo necesita…Y C ofrece…
Controlar exactamente cómo se organizan los datos en la memoria, porque el hardware los espera en un formato preciso.Estructuras cuya disposición en memoria se conoce y se puede controlar, y punteros a cualquier dirección.
Funcionar antes de que exista cualquier otra cosa: no hay un sistema debajo que le preste servicios.Un lenguaje que no necesita un entorno de ejecución: ni intérprete, ni máquina virtual, ni recolector de basura.
Ser rápido y predecible, porque cada demora del núcleo la sufren todos los programas.Código que se traduce casi directamente a instrucciones de máquina, sin costos ocultos.
Hablar con el hardware: registros de dispositivos, interrupciones, tablas de memoria.Acceso a direcciones concretas y operaciones de bits, y la posibilidad de mezclar unas pocas líneas de ensamblador.
Funcionar en muchos procesadores distintos.Compiladores para prácticamente cualquier procesador que exista.
Ofrecer servicios a programas escritos en cualquier lenguaje.Una forma de llamar funciones, la «interfaz binaria de C», que casi todos los lenguajes saben usar.

El último punto merece atención. Python, Rust, Go y Java pueden llamar a funciones escritas en C, porque la forma en que C pasa los argumentos y devuelve los resultados es un acuerdo que respetan todos. Por eso la frontera entre los programas y el sistema operativo, y entre lenguajes distintos, se define casi siempre en C. Aunque escribas tus programas en otro lenguaje, cuando hablen con el sistema estarán hablando en C.

5. El primer programa

Por tradición, desde el libro de Kernighan y Ritchie, el primer programa en C muestra un saludo. Este es el nuestro:

hola.c
#include <stdio.h>

int main(void)
{
    printf("Hola, sistema operativo\n");
    return 0;
}
terminal · compilar y ejecutar
$ gcc -Wall -Wextra -o hola hola.c
$ ./hola
Hola, sistema operativo
$ echo $?
0
$ ls -l hola.c hola
-rwxr-xr-x 1 ana ana 15952 oct 15 10:22 hola
-rw-r--r-- 1 ana ana    94 oct 15 10:22 hola.c

En el tema 3 lo leeremos línea por línea; por ahora alcanza con lo esencial. #include <stdio.h> trae las declaraciones de la biblioteca de entrada y salida, donde está printf. main es la función por la que empieza todo programa en C. Y el return 0 final es el código de salida que el programa le devuelve a quien lo ejecutó: el mismo $? que usaste en todos los scripts del curso de Bash. Un script que llama a un programa en C se entera así de si funcionó.

gcc es el compilador: lee hola.c y produce hola, un ejecutable de casi dieciséis mil bytes a partir de un fuente de noventa y cuatro. La diferencia no es el saludo: es todo lo que hace falta para que el sistema pueda cargar y arrancar el programa, y para que encuentre printf en la biblioteca de C. Las opciones -Wall -Wextra piden al compilador que advierta todo lo que parezca un error, y las usaremos siempre. El tema 4 sigue paso a paso el camino del fuente al ejecutable.

6. Ver la memoria desde un programa

Lo que distingue a C se ve en cuanto un programa pregunta dónde están sus propios datos. Este muestra el valor de dos variables, cuánto ocupan y en qué dirección de memoria viven, y lo mismo para una variable global y para el propio código de main:

memoria.c
#include <stdio.h>

int contador = 7;

int main(void)
{
    int edad = 30;
    char inicial = 'A';

    printf("edad    vale %d, ocupa %zu bytes y vive en %p\n", edad, sizeof edad, (void *) &edad);
    printf("inicial vale %c, ocupa %zu byte  y vive en %p\n", inicial, sizeof inicial, (void *) &inicial);
    printf("contador vale %d y vive en %p\n", contador, (void *) &contador);
    printf("el código de main empieza en %p\n", (void *) main);
    return 0;
}
terminal · dos ejecuciones seguidas
$ gcc -Wall -Wextra -o memoria memoria.c
$ ./memoria
edad    vale 30, ocupa 4 bytes y vive en 0x7ffed608d3a4
inicial vale A, ocupa 1 byte  y vive en 0x7ffed608d3a3
contador vale 7 y vive en 0x558354925010
el código de main empieza en 0x558354922169
$ ./memoria
edad    vale 30, ocupa 4 bytes y vive en 0x7fff04021ed4
inicial vale A, ocupa 1 byte  y vive en 0x7fff04021ed3
contador vale 7 y vive en 0x56860c8a8010
el código de main empieza en 0x56860c8a5169

Hay mucho que leer en esas ocho líneas, y cada cosa tiene su tema en el curso:

Ningún programa en Bash puede preguntar estas cosas. En C son lo primero que se aprende a mirar, porque la memoria es la materia con la que se trabaja.

7. Lo que C ofrece

VirtudQué significa en la práctica
VelocidadUn bucle en C puede ser cientos de veces más rápido que el mismo bucle en Bash o en Python, porque se ejecuta como instrucciones de máquina, sin un intérprete en el medio.
ControlEl programa decide cuánta memoria usa, cuándo la pide y cuándo la devuelve, y cómo se ordenan los datos en ella.
TransparenciaCasi cada línea se corresponde con unas pocas instrucciones de máquina previsibles. No hay operaciones costosas escondidas detrás de una sintaxis inocente.
PortabilidadHay compiladores de C para prácticamente todos los procesadores, desde los de un microondas hasta los de una supercomputadora.
UbicuidadCada sistema tiene un compilador de C y una biblioteca de C, y cada lenguaje sabe llamar a funciones de C.
PequeñezEl lenguaje entero cabe en un libro delgado, el de Kernighan y Ritchie. Se puede conocer completo, algo difícil con la mayoría de los lenguajes modernos.

8. Lo que C exige a cambio

La otra cara de esa libertad es que C confía en el programador por completo, y no lo protege de sus errores. Este programa tiene uno muy común: el bucle recorre un arreglo de tres elementos, pero la condición i <= 3 deja que i llegue a 3, una posición más de las que existen:

fuera.c
#include <stdio.h>

int main(void)
{
    int notas[3] = {7, 9, 10};
    int otra = 1234;

    for (int i = 0; i <= 3; i++)          /* error: i llega a 3 */
        printf("notas[%d] = %d\n", i, notas[i]);
    printf("otra = %d\n", otra);
    return 0;
}
terminal
$ gcc -Wall -Wextra -o fuera fuera.c
$ ./fuera
notas[0] = 7
notas[1] = 9
notas[2] = 10
notas[3] = -448843008
otra = 1234
$ echo $?
0

El compilador no advirtió nada, el programa no se detuvo, no hubo ningún mensaje de error, y terminó con código 0, como si todo estuviera bien. notas[3] leyó los cuatro bytes que había en la memoria justo después del arreglo, fueran lo que fueran, y los mostró como un número. En Python, el mismo error produce un IndexError inmediato. En C, el programa sigue adelante con un dato inventado. Y si en lugar de leer hubiera escrito, habría modificado otra variable, o algo peor, sin que nadie se enterara.

No estamos indefensos, sin embargo. El compilador detecta este caso concreto si se le pide optimizar, porque entonces analiza el bucle con más cuidado; y una herramienta de diagnóstico, AddressSanitizer, incorporada en gcc, agrega al programa comprobaciones que lo detienen en el acceso exacto:

terminal · dos formas de detectarlo
$ gcc -O2 -Wall -Wextra -o fuera fuera.c
fuera.c: In function ‘main’:
fuera.c:9:9: warning: iteration 3 invokes undefined behavior [-Waggressive-loop-optimizations]
    9 |         printf("notas[%d] = %d\n", i, notas[i]);
      |         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
fuera.c:8:23: note: within this loop
    8 |     for (int i = 0; i <= 3; i++)          /* error: i llega a 3 */
      |                     ~~^~~~
$ gcc -g -fsanitize=address -o fuera fuera.c
$ ./fuera
notas[0] = 7
notas[1] = 9
notas[2] = 10
=================================================================
==491==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x733399dd002c …
READ of size 4 at 0x733399dd002c thread T0
    #0 0x6474b59943e6 in main /home/ana/c/fuera.c:9
…
  This frame has 1 object(s):
    [32, 44) 'notas' (line 5) <== Memory access at offset 44 overflows this variable

La advertencia de la optimización usa una expresión que aparecerá mucho, undefined behavior, comportamiento indefinido, y AddressSanitizer dice exactamente qué pasó: una lectura de 4 bytes justo después del final de notas, en la línea 9. Aprender C es, en buena medida, aprender a usar estas herramientas, que veremos en los temas 29, 39 y 40. Estas son las responsabilidades que C deja en tus manos:

En C, nadie comprueba…Por eso hay que…Temas
Que un índice esté dentro del arregloLlevar la cuenta del tamaño de cada arreglo y respetarlo.17, 18
Que una cadena quepa en su destinoUsar las funciones que reciben el tamaño del destino.21, 22
Que la memoria pedida se devuelvaLiberar cada bloque exactamente una vez, y no usarlo después.28, 29
Que un puntero apunte a algo válidoInicializarlos siempre, y comprobar el puntero nulo.15
Que una operación haya funcionadoMirar el valor que devuelve cada llamada, y errno cuando falla.37
Que una variable tenga un valor antes de usarlaInicializar las variables al declararlas.6

9. El comportamiento indefinido

El estándar de C llama comportamiento indefinido a lo que ocurre cuando un programa hace algo que el lenguaje prohíbe pero no está obligado a detectar, como leer fuera de un arreglo. El estándar no promete nada sobre el resultado: el programa puede dar un valor absurdo, detenerse, parecer funcionar perfectamente, o comportarse distinto con otro compilador o con otras opciones.

Es el concepto más importante, y más traicionero, de C. Un programa con comportamiento indefinido no está «casi bien»: está mal, aunque las pruebas pasen. Un ejemplo clásico es el desbordamiento de un entero con signo:

desborde.c
#include <stdio.h>
#include <limits.h>

int main(void)
{
    int x = INT_MAX;
    printf("INT_MAX     = %d\n", x);
    printf("INT_MAX + 1 = %d\n", x + 1);
    unsigned int u = UINT_MAX;
    printf("UINT_MAX + 1 = %u\n", u + 1);
    return 0;
}
terminal · sumar 1 al mayor int
$ gcc -Wall -Wextra -o desborde desborde.c
$ ./desborde
INT_MAX     = 2147483647
INT_MAX + 1 = -2147483648
UINT_MAX + 1 = 0
$ gcc -fsanitize=undefined -o desborde desborde.c && ./desborde
desborde.c:8:5: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
…

La salida parece razonable: el número «da la vuelta» y se vuelve negativo. Pero para un int, que tiene signo, ese resultado es casualidad: el estándar dice que el desbordamiento es indefinido, y el compilador tiene derecho a suponer que nunca ocurre, lo que puede hacer que una comprobación como if (x + 1 < x) desaparezca al optimizar. Para un unsigned int, en cambio, la vuelta a cero está definida y garantizada. El sanitizador de comportamiento indefinido, -fsanitize=undefined, lo detecta en tiempo de ejecución. Estos son los casos más frecuentes, cada uno con su tema:

Comportamiento indefinidoEjemploTema
Leer o escribir fuera de un arreglonotas[3] en un arreglo de 317, 18
Usar una variable sin inicializarint x; printf("%d", x);6
Desbordar un entero con signoINT_MAX + 17
Desreferenciar un puntero nulo o inválido*p con p == NULL15
Usar memoria ya liberadaLeer un bloque después de free29
Liberar dos vecesfree(p); free(p);29
Modificar una cadena literalchar *s = "hola"; s[0] = 'H';21

10. C y el sistema operativo

Un programa en C no puede escribir en la pantalla por sí mismo, ni abrir un archivo, ni pedir memoria: todo eso lo hace el núcleo, y el programa se lo pide con una llamada al sistema. La herramienta strace, que muestra cada llamada al sistema de un programa, deja ver qué hizo realmente hola:

terminal · las últimas llamadas al sistema de hola
$ strace ./hola 2>&1 | tail -n 6
brk(NULL)                               = 0x5622a618b000
brk(0x5622a61ad000)                     = 0x5622a61ad000
write(1, "Hola, sistema operativo\n", 24Hola, sistema operativo
) = 24
exit_group(0)                           = ?
+++ exited with 0 +++

El printf del programa terminó convertido en write(1, "Hola, sistema operativo\n", 24): escribir 24 bytes en el descriptor 1, la salida estándar de la que tanto hablamos en los cursos anteriores. Y el return 0, en exit_group(0), que le entrega al núcleo el código de salida. Las brk anteriores piden memoria para el buffer de printf. En total, este programa de dos líneas hizo 34 llamadas al sistema, casi todas para que el cargador preparara el proceso antes de llegar a main.

Entre el programa y el núcleo hay una capa intermedia, la biblioteca de C, que en Linux es casi siempre la de GNU, glibc: el libc.so.6 que mostraba ldd. Ofrece dos tipos de funciones, que el manual separa en secciones distintas:

Sección del manualContieneEjemplos
man 2Las llamadas al sistema: funciones finas que piden algo directamente al núcleo.write, open, read, fork, brk
man 3Las funciones de biblioteca: más cómodas, a menudo construidas sobre las anteriores.printf, fopen, malloc, strlen

En este curso usaremos sobre todo las de la sección 3, y en el tema 38 veremos las dos formas de trabajar con archivos, una sobre cada capa. Los cursos siguientes, empezando por Procesos e Hilos, viven en la sección 2.

11. C frente a otros lenguajes

C no es la mejor elección para todo, y conviene saber cuándo no lo es. Esta comparación, simplificada, ubica a C entre los lenguajes que probablemente conoces o conocerás:

LenguajeSe ejecutaLa memoriaBrilla en
BashInterpretadoNo se manejaUnir programas y automatizar tareas del sistema.
PythonInterpretadoAutomáticaDatos, herramientas, prototipos: escribir rápido.
CCompiladoManualNúcleos, controladores, bibliotecas del sistema, dispositivos pequeños.
C++CompiladoManual, con ayudasProgramas grandes y rápidos: navegadores, juegos, bases de datos.
RustCompiladoManual, verificada por el compiladorLo mismo que C, con garantías contra los errores de memoria.
GoCompiladoAutomáticaServidores y herramientas de red e infraestructura.

Rust merece una mención especial, porque fue creado precisamente para resolver los problemas de la sección 8: su compilador rechaza los programas que podrían acceder a memoria inválida. Por eso está entrando, de a poco, en terrenos que eran exclusivos de C, como el núcleo Linux y los comandos básicos de algunas distribuciones. Pero C sigue siendo el idioma común: las interfaces del sistema se definen en C, la enorme mayoría del código existente está en C, y entender C es la base para entender por qué existe Rust. Lo que aprendas aquí sobre la memoria vale para cualquier lenguaje de sistemas.

12. Las versiones del lenguaje

C está definido por un estándar internacional, ISO/IEC 9899, que se revisa cada tantos años. Cada versión agrega cosas sin romper los programas anteriores:

VersiónAñoLo más importante que trajo
K&R C1978El lenguaje original, definido por el libro.
C89 / C901989El primer estándar: prototipos de funciones y la biblioteca estándar.
C991999Comentarios con //, declarar variables en cualquier lugar, stdint.h, bool.
C112011Hilos y operaciones atómicas en el estándar, _Static_assert.
C172018Correcciones de C11, sin cambios nuevos: la versión más estable y difundida.
C232024bool, true y false como palabras clave, nullptr, constexpr, atributos.

Los compiladores eligen una versión por omisión, que cambia con el tiempo; gcc 15, por ejemplo, usa C23 con extensiones propias de GNU. Para que los ejemplos del curso funcionen igual con compiladores de distintas épocas, compilaremos con -std=c17, y señalaremos lo que cambia en C23 cuando aparezca.

Hay además un segundo estándar que nos acompañará: POSIX, que no define el lenguaje sino la interfaz de los sistemas de la familia Unix. printf y malloc son de C, y existen en cualquier sistema; write, open o getopt son de POSIX, y existen en Linux, macOS y los demás Unix, pero no necesariamente en otros sistemas.

13. Cómo trabajaremos

Todos los ejemplos del curso están desarrollados y probados en Linux con gcc. Para seguirlos hace falta un sistema Linux, real, en una máquina virtual o en WSL, con las herramientas de desarrollo instaladas:

terminal · las herramientas del curso, en Debian y Ubuntu
$ sudo apt install build-essential gdb valgrind strace
# build-essential incluye gcc, make y la biblioteca de C con sus encabezados

En el tema 2 preparamos el entorno con detalle. Desde ya, cuatro costumbres que valen para todo el curso:

  1. Compilar siempre con las advertencias encendidas: -Wall -Wextra, y tratar cada advertencia como un error hasta entenderla. El compilador es el primer revisor de tu código. advertencias
  2. Compilar a menudo, cada pocas líneas, en lugar de escribir cien y descubrir veinte errores juntos. pasos cortos
  3. Probar con los sanitizadores mientras se desarrolla: -fsanitize=address,undefined convierte el comportamiento indefinido silencioso en un mensaje claro. sanitizadores
  4. Consultar el manual: man 3 printf y man 2 write dicen exactamente qué hace cada función, qué devuelve y cómo informa los errores. manual

Y la automatización del curso anterior sigue siendo útil: el build.sh del tema 36 de Bash, o un Makefile, que aprenderás a escribir en el bloque 8, compilan y prueban un programa con una sola orden.

14. El mapa del curso

Los 42 temas están ordenados para que cada bloque se apoye en los anteriores. Cada uno responde una pregunta:

BloqueTemasLa pregunta que responde
1. Del código fuente al programa1–4¿Qué es C, y cómo se convierte un archivo de texto en un programa?
2. Tipos, operadores y control5–10¿Cómo se representan los datos, y cómo decide y repite un programa?
3. Funciones y organización11–14¿Cómo se divide un programa en partes y en archivos?
4. Punteros15–20¿Qué es una dirección, y qué se puede hacer con ella?
5. Cadenas y arreglos21–23¿Cómo se trabaja con texto y con listas sin desbordarlos?
6. Estructuras y tipos propios24–26¿Cómo se agrupan datos, y cómo quedan en la memoria?
7. Memoria dinámica27–30¿Cómo se pide y se devuelve memoria mientras el programa corre?
8. Compilación, enlazado y bibliotecas31–36¿Cómo se construyen programas grandes y se comparte código?
9. Errores, archivos y depuración37–40¿Cómo se detectan, se informan y se corrigen los errores?
10. Integración y cierre41–42¿Cómo se usa todo junto en una herramienta completa?

Los bloques 2 y 3 enseñan lo que C comparte con casi todos los lenguajes, y van rápido si ya programaste. Los bloques 4 a 7 son el corazón del curso: lo que hace a C distinto, y lo que más se usa en los cursos siguientes. El 8 enseña a construir programas reales, y el 9, a encontrar lo que falla en ellos.

15. Resumen

16. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué diferencia hay entre un script de Bash y un programa en C a la hora de ejecutarlos?

El script es texto que Bash lee e interpreta línea por línea cada vez que se ejecuta. El programa en C se traduce una sola vez, con el compilador, a un ejecutable con instrucciones de máquina, que luego se ejecuta directamente, sin necesitar al compilador ni al código fuente.

2. ¿Por qué fue tan importante reescribir el núcleo de Unix en C en 1973?

Porque un sistema escrito en ensamblador solo funciona en el procesador para el que se escribió. Escrito en C, llevarlo a una máquina nueva solo requería un compilador de C para ella y adaptar las pocas partes que dependían del hardware. Eso permitió que Unix se extendiera a muchas computadoras distintas.

3. Nombra tres necesidades de un sistema operativo que C cubre bien.

Por ejemplo: controlar exactamente cómo se organizan los datos en la memoria; funcionar sin un entorno de ejecución debajo, como un intérprete o un recolector de basura; ser rápido y predecible; acceder al hardware; funcionar en muchos procesadores; y ofrecer una interfaz que cualquier lenguaje pueda llamar.

4. ¿Qué relación hay entre el return 0 de main y el $? de la shell?

El valor que devuelve main es el código de salida del programa, que el núcleo le entrega al proceso que lo ejecutó. En la shell, ese valor queda en $?, y es lo que usan if, && o set -e para saber si el programa funcionó.

5. En memoria.c, ¿por qué las direcciones cambian de una ejecución a otra?

Por ASLR: el sistema coloca la pila, el código y las demás zonas de memoria de cada proceso en lugares al azar cada vez que se ejecuta, para dificultar los ataques que necesitan conocer esas direcciones. Las distancias dentro de una misma zona se mantienen.

6. ¿Qué pasó en fuera.c cuando el bucle leyó notas[3], y por qué el programa terminó con código 0?

Leyó los bytes que había en la memoria justo después del arreglo y los mostró como un número. C no comprueba que el índice esté dentro del arreglo, de modo que no hubo ningún error ni ningún mensaje, y el programa siguió hasta el final con normalidad. Es comportamiento indefinido: el resultado podría haber sido cualquier otro.

7. ¿Qué es el comportamiento indefinido, y por qué un programa que lo tiene está mal aunque funcione?

Es lo que ocurre cuando un programa hace algo que el lenguaje prohíbe sin obligación de detectarlo. El estándar no promete ningún resultado: que hoy funcione es casualidad, y puede dejar de hacerlo con otro compilador, otras opciones de optimización u otros datos.

8. ¿Por qué INT_MAX + 1 es comportamiento indefinido, pero UINT_MAX + 1 no?

Porque el estándar define que la aritmética sin signo da la vuelta: UINT_MAX + 1 vale siempre 0. El desbordamiento de un entero con signo, en cambio, no está definido, y el compilador puede suponer que nunca ocurre, con efectos inesperados al optimizar.

9. ¿Qué mostró strace sobre el printf de hola.c?

Que terminó convertido en una llamada al sistema write(1, "Hola, sistema operativo\n", 24): escribir 24 bytes en el descriptor 1, la salida estándar. El programa no escribe en la pantalla por sí mismo, sino que se lo pide al núcleo.

10. ¿Qué diferencia hay entre las secciones 2 y 3 del manual?

La sección 2 documenta las llamadas al sistema, funciones finas que piden algo directamente al núcleo, como write u open. La sección 3, las funciones de la biblioteca de C, más cómodas y a menudo construidas sobre las anteriores, como printf o fopen.

Para practicar

  1. Ejecuta file y ldd sobre varios programas de tu sistema: bash, gzip, ssh, python3. ¿Cuáles dependen de libc.so.6? Si alguno es un script, ¿qué intérprete indica file?
  2. Compila y ejecuta hola.c. Cambia el return 0 por return 3, vuelve a compilar y comprueba el valor de $?. Prueba también ./hola && echo funcionó con cada valor.
  3. Compila memoria.c y ejecútalo varias veces. Calcula la distancia entre las direcciones de edad e inicial, y entre contador y main. ¿Cuáles se mantienen?
  4. Compila fuera.c sin opciones, con -O2 -Wall y con -fsanitize=address, y compara lo que ocurre en cada caso. Luego cambia el bucle para que escriba en notas[3] en lugar de leer, y observa el valor de otra.
  5. Ejecuta strace -c ./hola y strace -c ls. ¿Cuántas llamadas al sistema hace cada uno? ¿Cuáles aparecen en ls que no aparecen en hola?
  6. Lee man 3 printf y man 2 write, y anota qué devuelve cada función y cómo informa un error.