Semana 9 — Dentro de la computadora

Objetivos

  • Describir las piezas de una computadora y el ciclo de instrucción.
  • Explicar la jerarquía de memoria y qué hace el sistema operativo.
  • Usar arreglos unidimensionales y explicar cómo se guardan en memoria.

Teoría — Dentro de la computadora · Direcciones y arreglos

Lectura: Apuntes, semana 9.

Temas:

  • CPU, RAM y almacenamiento. El ciclo de instrucción: buscar, decodificar y ejecutar.
  • Jerarquía de memoria: órdenes de magnitud de velocidad.
  • Qué hace el sistema operativo: procesos, memoria y archivos.
  • La memoria como una fila de bytes numerados. Direcciones con & y %p.
  • Arreglos: declarar, inicializar y recorrer. Por qué los índices empiezan en 0.
  • Error común: salirse del arreglo. C no avisa, y -fsanitize=address suele detectarlo, aunque no siempre.

Laboratorio — Arreglos

  1. Imprimir las direcciones de los elementos de un arreglo de int y uno de char. ¿Cuánto cambian de uno a otro? Las direcciones dependen de cada corrida.
  2. Leer N notas en un arreglo. Escribir el promedio, la mayor y cuántas superan el promedio.
  3. Contar cuántas veces sale cada cara en 10 000 lanzamientos de un dado, usando un arreglo.
  4. Salirse del arreglo a propósito. Compilar con y sin -fsanitize=address y comparar.

Preguntas de repaso

  1. Sigan el ciclo de instrucción para una suma. ¿Qué cambia en cada paso?
  2. ¿Por qué un programa corre más rápido cuando sus datos caben en cache?
  3. Un arreglo de int empieza en la dirección 1000 y cada int ocupa 4 bytes. ¿Dónde está a[5]?
  4. ¿Qué puede pasar al escribir en a[10] si a tiene 10 elementos?
  5. ¿Qué tendría que hacer cada programa por su cuenta si no existiera el sistema operativo?

Código de la semana

Compilar y probar dentro del contenedor del curso con make y make test.

Ejemplos (9)Errores (5)Pruebas (3)Laboratorio (4)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

add_two.cDescargar add_two.c
#include <stdio.h>

// Una suma simple, para mirar su ensamblador con gcc -S.
int main(void) {
    int a = 2;
    int b = 3;
    int c = a + b;

    printf("%d\n", c);
    return 0;
}
array_addresses.cDescargar array_addresses.c
#include <stdio.h>

#define SIZE 4

// Escribe dónde está cada elemento de un arreglo de int.
int main(void) {
    int prices[SIZE] = {350, 1200, 80, 999};

    printf("sizeof(int) = %zu\n", sizeof(int));
    for (int i = 0; i < SIZE; i++) {
        printf("prices[%d] = %4d está en %p\n", i, prices[i], (void *) &prices[i]);
    }
    return 0;
}
array_init.cDescargar array_init.c
#include <stdio.h>

#define SIZE 5

// Formas de inicializar un arreglo.
int main(void) {
    int full[SIZE] = {3, 1, 4, 1, 5};
    int partial[SIZE] = {7, 8};
    int zeros[SIZE] = {0};
    int counted[] = {10, 20, 30};

    for (int i = 0; i < SIZE; i++) {
        printf("i = %d: full %d, partial %d, zeros %d\n",
               i, full[i], partial[i], zeros[i]);
    }

    printf("sizeof(full)    = %zu\n", sizeof(full));
    printf("sizeof(full[0]) = %zu\n", sizeof(full[0]));
    printf("elementos de full    = %zu\n", sizeof(full) / sizeof(full[0]));
    printf("elementos de counted = %zu\n", sizeof(counted) / sizeof(counted[0]));
    return 0;
}
copy_array.cDescargar copy_array.c
#include <stdio.h>

#define SIZE 4

// Copia un arreglo elemento por elemento y cambia solo la copia.
int main(void) {
    int original[SIZE] = {5, 6, 7, 8};
    int copy[SIZE];

    for (int i = 0; i < SIZE; i++) {
        copy[i] = original[i];
    }
    copy[0] = 99;

    printf("original[0] = %d, copy[0] = %d\n", original[0], copy[0]);
    return 0;
}
first_array.cDescargar first_array.c
#include <stdio.h>

// Guarda cinco notas en un arreglo, cambia dos y las escribe.
int main(void) {
    int grades[5] = {14, 9, 17, 11, 20};

    grades[1] = 12;
    grades[4] = grades[4] - 1;

    for (int i = 0; i < 5; i++) {
        printf("grades[%d] = %d\n", i, grades[i]);
    }
    return 0;
}
reverse.cDescargar reverse.c
#include <stdio.h>

#define MAX_VALUES 100

// Lee n enteros y los escribe en orden inverso.
int main(void) {
    int values[MAX_VALUES];
    int n;

    if (scanf("%d", &n) != 1) {
        printf("Entrada inválida\n");
        return 1;
    }
    if (n < 1 || n > MAX_VALUES) {
        printf("n debe estar entre 1 y %d\n", MAX_VALUES);
        return 1;
    }

    for (int i = 0; i < n; i++) {
        if (scanf("%d", &values[i]) != 1) {
            printf("Entrada inválida\n");
            return 1;
        }
    }

    for (int i = n - 1; i >= 0; i--) {
        printf("%d\n", values[i]);
    }
    return 0;
}
temperatures.cDescargar temperatures.c
#include <stdio.h>

#define DAYS 7

// Suma, promedio y mayor de las temperaturas de una semana.
int main(void) {
    int temperatures[DAYS] = {-2, 1, 4, -5, 0, 3, 2};
    int sum = 0;

    for (int i = 0; i < DAYS; i++) {
        sum = sum + temperatures[i];
    }

    int max_day = 0;
    for (int i = 1; i < DAYS; i++) {
        if (temperatures[i] > temperatures[max_day]) {
            max_day = i;
        }
    }

    printf("Suma: %d\n", sum);
    printf("Promedio: %.2f\n", (double) sum / DAYS);
    printf("Mayor: %d, en el día %d\n", temperatures[max_day], max_day);
    return 0;
}
variable_addresses.cDescargar variable_addresses.c
#include <stdio.h>

// Escribe el valor, el tamaño y la dirección de tres variables.
int main(void) {
    int age = 19;
    double average = 14.5;
    char section = 'A';

    printf("age     vale %d, ocupa %zu bytes, está en %p\n",
           age, sizeof(age), (void *) &age);
    printf("average vale %.1f, ocupa %zu bytes, está en %p\n",
           average, sizeof(average), (void *) &average);
    printf("section vale %c, ocupa %zu byte, está en %p\n",
           section, sizeof(section), (void *) &section);
    return 0;
}
votes.cDescargar votes.c
#include <stdio.h>

#define CANDIDATES 3

// Lee votos (1, 2 o 3) hasta un 0 y cuenta los de cada candidato.
int main(void) {
    int counts[CANDIDATES] = {0};
    int invalid = 0;
    int vote;

    if (scanf("%d", &vote) != 1) {
        printf("Entrada inválida\n");
        return 1;
    }
    while (vote != 0) {
        if (vote >= 1 && vote <= CANDIDATES) {
            counts[vote - 1]++;
        } else {
            invalid++;
        }
        if (scanf("%d", &vote) != 1) {
            printf("Entrada inválida\n");
            return 1;
        }
    }

    for (int i = 0; i < CANDIDATES; i++) {
        printf("Candidato %d: %d\n", i + 1, counts[i]);
    }
    printf("Inválidos: %d\n", invalid);
    return 0;
}

Errores

Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.

errors/assign_array.cDescargar errors/assign_array.c
#include <stdio.h>

// No compila: un arreglo no se asigna con =.
int main(void) {
    int original[3] = {1, 2, 3};
    int copy[3];

    copy = original;
    printf("%d\n", copy[0]);
    return 0;
}
errors/compare_arrays.cDescargar errors/compare_arrays.c
#include <stdio.h>

// Compila, con una advertencia, pero no compara los elementos.
int main(void) {
    int first[3] = {1, 2, 3};
    int second[3] = {1, 2, 3};

    if (first == second) {
        printf("iguales\n");
    } else {
        printf("distintos\n");
    }
    return 0;
}
errors/fixed_constant.cDescargar errors/fixed_constant.c
#include <stdio.h>

// Bug: el índice 5 es constante y está fuera del arreglo.
int main(void) {
    int values[5] = {1, 2, 3, 4, 5};

    values[5] = 99;
    printf("%d\n", values[0]);
    return 0;
}
errors/off_by_one.cDescargar errors/off_by_one.c
#include <stdio.h>

#define SIZE 5

// Bug: i <= SIZE lee un elemento más de los que tiene el arreglo.
int main(void) {
    int grades[SIZE] = {14, 9, 17, 11, 20};
    int sum = 0;

    for (int i = 0; i <= SIZE; i++) {
        printf("grades[%d] = %d\n", i, grades[i]);
        sum = sum + grades[i];
    }
    printf("Suma: %d\n", sum);
    return 0;
}
errors/write_out.cDescargar errors/write_out.c
#include <stdio.h>

#define SIZE 4

// Bug: no revisa el índice que lee. Se usa para ver qué pasa al salirse.
int main(void) {
    int before = 111;
    int values[SIZE] = {1, 2, 3, 4};
    int after = 222;
    int index;

    if (scanf("%d", &index) != 1) {
        printf("Entrada inválida\n");
        return 1;
    }

    values[index] = 99;

    printf("values[%d] = %d\n", index, values[index]);
    printf("before = %d, after = %d\n", before, after);
    return 0;
}

Pruebas

Comprueban lo que afirman los apuntes; se ejecutan con make test.

test_examples.pyDescargar test_examples.py
"""Comprueba lo que los apuntes afirman sobre los programas de la semana 9."""
import pathlib
import re
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
FLAGS = ["-Wall", "-Wextra", "-std=c17"]


def run(program, stdin=""):
    """Ejecuta un programa de build/; devuelve su salida (con errores) y su estado."""
    done = subprocess.run(
        [str(BUILD / program)], input=stdin, text=True,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
    )
    return done.stdout, done.returncode


def compile_demo(name, *extra):
    """Compila un programa de errors/; devuelve los mensajes, el estado y el ejecutable."""
    out = pathlib.Path(tempfile.mkdtemp()) / name
    done = subprocess.run(
        ["gcc", *FLAGS, *extra, f"errors/{name}.c", "-o", str(out)], cwd=HERE,
        text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    )
    return done.stdout, done.returncode, out


def run_binary(binary, stdin=""):
    done = subprocess.run(
        [str(binary)], input=stdin, text=True,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
    )
    return done.stdout, done.returncode


class CleanCompile(unittest.TestCase):
    def test_examples_have_no_warnings(self):
        for source in sorted(HERE.glob("*.c")):
            with self.subTest(source=source.name):
                done = subprocess.run(
                    ["gcc", *FLAGS, "-Werror", "-fsyntax-only", str(source)],
                    text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )
                self.assertEqual(done.returncode, 0, done.stdout)


class Addresses(unittest.TestCase):
    def test_variable_addresses_show_sizes(self):
        out, status = run("variable_addresses")
        self.assertEqual(status, 0)
        self.assertRegex(out, r"age     vale 19, ocupa \d+ bytes, está en 0x[0-9a-f]+\n")
        self.assertIn("average vale 14.5", out)
        self.assertIn("section vale A, ocupa 1 byte,", out)

    def test_elements_of_an_array_are_contiguous(self):
        out, status = run("array_addresses")
        self.assertEqual(status, 0)
        size = int(re.search(r"sizeof\(int\) = (\d+)", out).group(1))
        addresses = [int(a, 16) for a in re.findall(r"está en 0x([0-9a-f]+)", out)]
        self.assertEqual(len(addresses), 4)
        for i, address in enumerate(addresses):
            self.assertEqual(address, addresses[0] + i * size)


class Arrays(unittest.TestCase):
    def test_first_array(self):
        out, _ = run("first_array")
        self.assertEqual(out.split("\n")[1], "grades[1] = 12")
        self.assertEqual(out.split("\n")[4], "grades[4] = 19")

    def test_missing_initializers_are_zero(self):
        out, _ = run("array_init")
        self.assertIn("i = 2: full 4, partial 0, zeros 0", out)
        self.assertIn("i = 4: full 5, partial 0, zeros 0", out)

    def test_element_count_does_not_depend_on_int_size(self):
        out, _ = run("array_init")
        self.assertIn("elementos de full    = 5", out)
        self.assertIn("elementos de counted = 3", out)

    def test_copy_is_independent(self):
        self.assertEqual(run("copy_array")[0], "original[0] = 5, copy[0] = 99\n")

    def test_assigning_an_array_does_not_compile(self):
        messages, status, _ = compile_demo("assign_array")
        self.assertNotEqual(status, 0)
        self.assertIn("assignment to expression with array type", messages)

    def test_comparing_arrays_warns_and_says_different(self):
        messages, status, binary = compile_demo("compare_arrays")
        self.assertEqual(status, 0)
        self.assertIn("-Warray-compare", messages)
        self.assertEqual(run_binary(binary)[0], "distintos\n")


class Patterns(unittest.TestCase):
    def test_temperatures(self):
        self.assertEqual(run("temperatures"),
                         ("Suma: 3\nPromedio: 0.43\nMayor: 4, en el día 2\n", 0))

    def test_reverse(self):
        self.assertEqual(run("reverse", "3 10 20 30"), ("30\n20\n10\n", 0))
        self.assertEqual(run("reverse", "1 7"), ("7\n", 0))

    def test_reverse_full_array(self):
        values = list(range(100))
        text = "100 " + " ".join(map(str, values))
        expected = "".join(f"{v}\n" for v in reversed(values))
        self.assertEqual(run("reverse", text), (expected, 0))

    def test_reverse_rejects_what_does_not_fit(self):
        for n in [0, -1, 101, 200]:
            with self.subTest(n=n):
                self.assertEqual(run("reverse", f"{n} 1 2"), ("n debe estar entre 1 y 100\n", 1))

    def test_reverse_bad_input(self):
        for text in ["", "abc", "3 10 20", "3 10 x"]:
            with self.subTest(text=text):
                self.assertEqual(run("reverse", text), ("Entrada inválida\n", 1))

    def test_votes(self):
        out, status = run("votes", "1 2 2 3 7 2 0")
        self.assertEqual(status, 0)
        self.assertEqual(out, "Candidato 1: 1\nCandidato 2: 3\nCandidato 3: 1\nInválidos: 1\n")

    def test_votes_rejects_indexes_out_of_the_array(self):
        out, status = run("votes", "4 -1 100000 -100000 3 0")
        self.assertEqual(status, 0)
        self.assertEqual(out, "Candidato 1: 0\nCandidato 2: 0\nCandidato 3: 1\nInválidos: 4\n")

    def test_votes_bad_input(self):
        for text in ["", "abc", "1 2", "1 x"]:
            with self.subTest(text=text):
                self.assertEqual(run("votes", text), ("Entrada inválida\n", 1))


class OutOfBounds(unittest.TestCase):
    """Sin sanitizer no se compara nada: el resultado es comportamiento indefinido."""

    def test_compiling_says_nothing(self):
        for name in ["off_by_one", "write_out", "fixed_constant"]:
            with self.subTest(name=name):
                messages, status, _ = compile_demo(name)
                self.assertEqual((messages, status), ("", 0))

    def test_address_sanitizer_reports_the_read(self):
        messages, status, binary = compile_demo("off_by_one", "-g", "-fsanitize=address")
        self.assertEqual((messages, status), ("", 0))
        out, status = run_binary(binary)
        self.assertNotEqual(status, 0)
        self.assertNotIn("grades[5] =", out)
        self.assertIn("stack-buffer-overflow", out)
        self.assertIn("READ of size 4", out)
        self.assertIn("errors/off_by_one.c:11", out)

    def test_address_sanitizer_reports_the_write_next_to_the_array(self):
        _, _, binary = compile_demo("write_out", "-g", "-fsanitize=address")
        out, status = run_binary(binary, "4")
        self.assertNotEqual(status, 0)
        self.assertIn("stack-buffer-overflow", out)
        self.assertIn("WRITE of size 4", out)

    def test_valid_index_gives_no_report(self):
        _, _, binary = compile_demo("write_out", "-g", "-fsanitize=address")
        self.assertEqual(run_binary(binary, "3"),
                         ("values[3] = 99\nbefore = 111, after = 222\n", 0))

    def test_undefined_sanitizer_checks_the_index(self):
        _, _, binary = compile_demo("write_out", "-g", "-fsanitize=address,undefined")
        out, status = run_binary(binary, "4")
        self.assertNotEqual(status, 0)
        self.assertIn("runtime error: index 4 out of bounds for type 'int [4]'", out)
        self.assertIn("stack-buffer-overflow", out)

    def test_debug_flag_adds_the_line_number(self):
        _, _, without_g = compile_demo("off_by_one", "-fsanitize=address")
        self.assertNotIn("errors/off_by_one.c:11", run_binary(without_g)[0])


class Assembly(unittest.TestCase):
    def test_the_sum_stays_in_the_assembly(self):
        # Una suma entre dos registros: add de tres operandos en ARM, addl de dos en x86-64.
        # Con -O2 el compilador calcula el resultado y la suma desaparece.
        sum_of_registers = r"\badd\s+w\d+, w\d+, w\d+|\baddl\s+%\w+, %\w+"
        folder = pathlib.Path(tempfile.mkdtemp())
        for level, check in (("-O0", self.assertRegex), ("-O2", self.assertNotRegex)):
            target = folder / f"add_two{level}.s"
            subprocess.run(["gcc", "-S", level, "add_two.c", "-o", str(target)], cwd=HERE, check=True)
            check(target.read_text(), sum_of_registers)
        self.assertEqual(run("add_two")[0], "5\n")


if __name__ == "__main__":
    unittest.main()
test_lab.pyDescargar test_lab.py
"""Pruebas del laboratorio. LAB_DIR dice dónde está el código: lab o solution."""
import os
import pathlib
import re
import subprocess
import unittest

HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
MAX_GRADES = 100


def run(program, stdin=""):
    done = subprocess.run(
        [str(HERE / "build" / LAB_DIR / program)], input=stdin, text=True,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=20,
    )
    return done.stdout.strip(), done.returncode


def stats(average, largest, above):
    return f"Promedio: {average}\nMayor: {largest}\nSuperan el promedio: {above}"


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe {LAB_DIR}/")
class LabTests(unittest.TestCase):
    def test_no_warnings(self):
        for name in ["addresses", "grades_stats", "dice", "out_of_bounds"]:
            with self.subTest(name=name):
                done = subprocess.run(
                    ["gcc", "-Wall", "-Wextra", "-std=c17", "-Werror", "-fsyntax-only",
                     str(HERE / LAB_DIR / f"{name}.c")],
                    text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )
                self.assertEqual(done.returncode, 0, done.stdout)

    def test_addresses_step_by_element_size(self):
        out, status = run("addresses")
        self.assertEqual(status, 0)
        found = re.findall(r"^(int|char) (\d): 0x([0-9a-f]+)$", out, re.M)
        self.assertEqual(len(found), 10, out)
        sizes = {"int": 4, "char": 1}
        for kind, size in sizes.items():
            rows = [(int(i), int(a, 16)) for k, i, a in found if k == kind]
            self.assertEqual([i for i, _ in rows], [0, 1, 2, 3, 4])
            for (_, low), (_, high) in zip(rows, rows[1:]):
                with self.subTest(kind=kind):
                    self.assertEqual(high - low, size)

    def test_grades_stats_normal(self):
        cases = {
            "4 12 18 10 16": stats("14.00", 18, 2),
            "3 11 12 14": stats("12.33", 14, 1),
            "1 20": stats("20.00", 20, 0),
            "1 0": stats("0.00", 0, 0),
            "2 0 1": stats("0.50", 1, 1),
        }
        for text, expected in cases.items():
            with self.subTest(text=text):
                self.assertEqual(run("grades_stats", text), (expected, 0))

    def test_grade_equal_to_average_does_not_count(self):
        self.assertEqual(run("grades_stats", "3 10 10 10"), (stats("10.00", 10, 0), 0))
        self.assertEqual(run("grades_stats", "3 9 10 11"), (stats("10.00", 11, 1), 0))

    def test_largest_can_be_first_or_last(self):
        self.assertEqual(run("grades_stats", "3 20 1 3")[0], stats("8.00", 20, 1))
        self.assertEqual(run("grades_stats", "3 1 3 20")[0], stats("8.00", 20, 1))

    def test_grades_stats_full_array(self):
        text = f"{MAX_GRADES} " + " ".join(["15"] * (MAX_GRADES - 1) + ["20"])
        self.assertEqual(run("grades_stats", text), (stats("15.05", 20, 1), 0))

    def test_grades_stats_bad_count(self):
        for n in [0, -1, MAX_GRADES + 1, 1000000]:
            with self.subTest(n=n):
                self.assertEqual(run("grades_stats", f"{n} 10 10"), ("Cantidad fuera de rango", 1))

    def test_grades_stats_bad_input(self):
        for text in ["", "abc", "3 10 abc", "3 10 12"]:
            with self.subTest(text=text):
                self.assertEqual(run("grades_stats", text), ("Entrada inválida", 1))

    def test_grades_stats_bad_grade(self):
        for text in ["2 10 21", "2 -1 10"]:
            with self.subTest(text=text):
                self.assertEqual(run("grades_stats", text), ("Nota fuera de rango", 1))

    def test_dice_counts_add_up_and_are_near_the_expected_value(self):
        for seed in [1, 2, 2026]:
            with self.subTest(seed=seed):
                out, status = run("dice", str(seed))
                self.assertEqual(status, 0)
                found = re.findall(r"^Cara (\d): (\d+)$", out, re.M)
                self.assertEqual([face for face, _ in found], list("123456"), out)
                counts = [int(count) for _, count in found]
                self.assertEqual(sum(counts), 10000)
                # La desviación típica de cada cara es cercana a 37: 300 es muy holgado.
                for count in counts:
                    self.assertLess(abs(count - 10000 / 6), 300)

    def test_dice_seed_repeats_the_results(self):
        self.assertEqual(run("dice", "7"), run("dice", "7"))
        self.assertNotEqual(run("dice", "7"), run("dice", "8"))

    def test_dice_bad_seed(self):
        for text in ["", "abc"]:
            with self.subTest(text=text):
                self.assertEqual(run("dice", text), ("Entrada inválida", 1))

    def test_out_of_bounds_inside(self):
        for index in [0, 2, 4]:
            with self.subTest(index=index):
                self.assertEqual(run("out_of_bounds", str(index)), (f"values[{index}] = 99", 0))

    def test_out_of_bounds_bad_input(self):
        self.assertEqual(run("out_of_bounds", "abc"), ("Entrada inválida", 1))

    def test_out_of_bounds_is_seen_by_the_sanitizer(self):
        out, status = run("out_of_bounds", "5")
        self.assertIn("stack-buffer-overflow", out)
        self.assertNotEqual(status, 0)


if __name__ == "__main__":
    unittest.main()
test_notes.pyDescargar test_notes.py
"""Vuelve a correr los comandos que aparecen en los apuntes y en las diapositivas
y comprueba que el código citado existe de verdad en esta carpeta.

Casi toda la salida de este capítulo depende de la máquina, así que un comentario antes
del bloque dice cómo se compara:
  <!-- direcciones -->  igual, salvo las direcciones
  <!-- sanitizer -->    solo las frases estables del informe
  <!-- esta corrida --> comportamiento indefinido o datos del sistema: no se compara
En las diapositivas, los entornos salidadir, salidasan y salidacorrida dicen lo mismo.
"""
import pathlib
import re
import subprocess
import unittest

HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-09.md"
SLIDES = ROOT / "slides" / "week-09.tex"
STABLE = [
    r"stack-buffer-overflow",
    r"READ of size 4",
    r"WRITE of size 4",
    r"runtime error: index \d+ out of bounds for type 'int \[\d+\]'",
    r"errors/\w+\.c:\d+",
]
MARKS = {"<!-- direcciones -->": "addresses", "<!-- sanitizer -->": "sanitizer",
         "<!-- esta corrida -->": "run"}


def normalize(text):
    # Según el idioma del entorno, gcc usa comillas tipográficas o rectas.
    return text.replace("‘", "'").replace("’", "'")


def hide_addresses(text):
    return re.sub(r"0x[0-9a-f]+", "0x...", text)


def run_shell(command):
    done = subprocess.run(
        ["bash", "-c", command], cwd=HERE, text=True,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
    )
    return normalize(done.stdout)


def split_commands(lines):
    """Parte un bloque de terminal en pares (comando, líneas de salida)."""
    pairs = []
    for line in lines:
        if line.startswith("$ "):
            pairs.append((line[2:], []))
        elif pairs:
            pairs[-1][1].append(normalize(line.rstrip()))
    return pairs


def note_blocks(language):
    """Devuelve (líneas, tipo de comparación) de cada bloque de los apuntes."""
    blocks, current, mark, previous = [], None, None, ""
    for raw in NOTES.read_text(encoding="utf-8").splitlines():
        line = re.sub(r"^> ?", "", raw)
        if current is None:
            if line.strip() == "```" + language:
                current, mark = [], MARKS.get(previous.strip())
            elif line.strip():
                previous = line
        elif line.strip() == "```":
            blocks.append((current, mark))
            current = None
        else:
            current.append(line)
    return blocks


def slide_blocks(environments):
    text = SLIDES.read_text(encoding="utf-8")
    pattern = r"\\begin\{(?:%s)\}[^\n]*\n(.*?)\\end\{(?:%s)\}" % (environments, environments)
    return [match.group(1).splitlines() for match in re.finditer(pattern, text, re.S)]


def known_code_lines():
    lines = set()
    for source in list(HERE.glob("*.c")) + list((HERE / "errors").glob("*.c")):
        lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
    return lines


def stable_phrases(line):
    return [m.group(0) for pattern in STABLE for m in re.finditer(pattern, line)]


class Case(unittest.TestCase):
    def check_sanitizer_block(self, block):
        for command, expected in split_commands(block):
            actual = run_shell(command)
            for line in expected:
                for phrase in stable_phrases(line):
                    with self.subTest(command=command, phrase=phrase):
                        self.assertIn(phrase, actual)

    def check_addresses_subset(self, block):
        """Una diapositiva puede omitir líneas, pero no cambiarlas ni reordenarlas."""
        for command, expected in split_commands(block):
            actual = hide_addresses(run_shell(command)).splitlines()
            position = 0
            for line in expected:
                line = hide_addresses(line).strip()
                if not line or line == "...":
                    continue
                with self.subTest(command=command, line=line):
                    found = [i for i in range(position, len(actual)) if actual[i].strip() == line]
                    self.assertTrue(found, f"no aparece en la salida real: {line}")
                    if found:
                        position = found[0] + 1


class NotesAreTrue(Case):
    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_terminal_blocks_in_notes(self):
        checked = 0
        for lines, mark in note_blocks("text"):
            if not lines or not lines[0].startswith("$ "):
                continue
            if mark == "sanitizer":
                self.check_sanitizer_block(lines)
                continue
            for command, expected in split_commands(lines):
                actual = run_shell(command).rstrip("\n")
                checked += 1
                with self.subTest(command=command):
                    if mark == "addresses":
                        self.assertEqual(hide_addresses(actual), hide_addresses("\n".join(expected)))
                    elif mark is None:
                        self.assertEqual(actual, "\n".join(expected))
        self.assertGreater(checked, 10)

    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_code_blocks_in_notes_exist(self):
        known = known_code_lines()
        for lines, _ in note_blocks("c"):
            for line in lines:
                if line.strip() and line.strip() != "...":
                    with self.subTest(line=line):
                        self.assertIn(line.strip(), known)

    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_hierarchy_numbers_are_labeled_as_approximate(self):
        text = NOTES.read_text(encoding="utf-8")
        self.assertIn("órdenes de magnitud aproximados", text)
        self.assertIn("Tiempo de un acceso (aproximado)", text)


class SlidesAreTrue(Case):
    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_terminal_blocks_in_slides(self):
        for block in slide_blocks("terminal|salidalarga|salidadir"):
            self.check_addresses_subset(block)

    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_sanitizer_blocks_in_slides(self):
        for block in slide_blocks("salidasan"):
            self.check_sanitizer_block(block)

    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_code_blocks_in_slides_exist(self):
        known = known_code_lines()
        for block in slide_blocks("codigo|codigochico|codigomini"):
            for line in block:
                if line.strip() and line.strip() != "...":
                    with self.subTest(line=line):
                        self.assertIn(line.strip(), known)


if __name__ == "__main__":
    unittest.main()

Laboratorio

Archivos de partida del laboratorio de la semana.

lab/addresses.cDescargar lab/addresses.c
// Laboratorio, parte 1: dónde están los elementos de un arreglo.
//
// Declaren un arreglo de 5 int y uno de 5 char, con los valores que quieran.
// Con un for para cada uno, el programa escribe una línea por elemento:
//   int 0: 0x...
//   ...
//   int 4: 0x...
//   char 0: 0x...
//   ...
//   char 4: 0x...
// donde 0x... es la dirección del elemento, escrita con %p y (void *).
// Termina con return 0.
//
// Las direcciones cambian de una corrida a otra. Respondan: ¿cuánto aumenta la
// dirección de un elemento al siguiente en cada arreglo? ¿Eso también cambia?
//
// Pruébenlo con make test-lab.
#include <stdio.h>

int main(void) {
    // Escriban aquí su programa.
    return 0;
}
lab/dice.cDescargar lab/dice.c
// Laboratorio, parte 3: contar las caras de un dado.
//
// El programa lee una semilla entera y la pasa a srand. Después lanza un dado
// 10 000 veces con rand y, usando un arreglo, cuenta cuántas veces sale cada cara.
// Al final escribe seis líneas, de la cara 1 a la 6:
//   Cara 1: 1650
//   ...
//   Cara 6: 1702
// Los números del ejemplo son solo eso, un ejemplo. Termina con return 0.
//
// Si la semilla no se puede leer, escribe Entrada inválida y termina con return 1.
//
// Pruébenlo con make test-lab.
#include <stdio.h>
#include <stdlib.h>

#define FACES 6
#define THROWS 10000

int main(void) {
    // Escriban aquí su programa.
    return 0;
}
lab/grades_stats.cDescargar lab/grades_stats.c
// Laboratorio, parte 2: estadísticas de N notas.
//
// El programa lee N y después N notas enteras, y escribe exactamente:
//   Promedio: 14.00
//   Mayor: 18
//   Superan el promedio: 2
// con el promedio a dos decimales. "Superan" quiere decir estrictamente mayor: una
// nota igual al promedio no cuenta. Termina con return 0.
//
// Si algo falla, escribe solo una de estas líneas y termina con return 1:
//   Entrada inválida          no se pudo leer un número (N o una nota)
//   Cantidad fuera de rango   N menor que 1 o mayor que 100
//   Nota fuera de rango       una nota menor que 0 o mayor que 20
//
// Pruébenlo con make test-lab.
#include <stdio.h>

#define MAX_GRADES 100

int main(void) {
    // Escriban aquí su programa.
    return 0;
}
lab/out_of_bounds.cDescargar lab/out_of_bounds.c
// Laboratorio, parte 4: salirse del arreglo a propósito.
//
// Declaren int values[5] = {10, 20, 30, 40, 50}. El programa lee un índice i y, SIN
// revisar que esté en el arreglo, hace values[i] = 99 y escribe:
//   values[2] = 99
// con el índice leído y el valor que quedó en values[i]. Termina con return 0.
// Si el índice no se puede leer, escribe Entrada inválida y termina con return 1.
//
// Este programa está mal a propósito. Solo aquí se deja el índice sin revisar.
// Los pasos para compilarlo con y sin -fsanitize=address están en los apuntes.
//
// Pruébenlo con make test-lab.
#include <stdio.h>

int main(void) {
    // Escriban aquí su programa.
    return 0;
}

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 9: dentro de la computadora, direcciones y arreglos. Se usa dentro del entorno del curso.
#
#   make           compila los programas de la clase en build/
#   make test      corre las pruebas de los ejemplos y de los apuntes
#   make test-lab  prueba su laboratorio (lab/*.c); falla hasta que lo terminen
#   make clean     borra lo generado
#
# Los programas de errors/ tienen errores a propósito. No se compilan con make:
# los compilan las pruebas, y ustedes, con los comandos de los apuntes.

CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
BUILD = build
PROGRAMS = add_two variable_addresses first_array array_init array_addresses \
           temperatures reverse votes copy_array
LAB_PROGRAMS = addresses grades_stats dice out_of_bounds

.PHONY: all lab test test-lab clean

all: $(PROGRAMS:%=$(BUILD)/%)

$(BUILD)/%: %.c | $(BUILD)
	$(CC) $(CFLAGS) $< -o $@

$(BUILD)/lab/%: lab/%.c | $(BUILD)/lab
	$(CC) $(CFLAGS) $< -o $@

$(BUILD)/solution/%: solution/%.c | $(BUILD)/solution
	$(CC) $(CFLAGS) $< -o $@

$(BUILD) $(BUILD)/lab $(BUILD)/solution:
	mkdir -p $@

lab: $(LAB_PROGRAMS:%=$(BUILD)/lab/%)

# Si existe solution/, también se comprueba la solución del laboratorio.
test: all
	@if [ -d solution ]; then $(MAKE) --no-print-directory \
	  $(LAB_PROGRAMS:%=$(BUILD)/solution/%); fi
	python3 -m unittest discover -p 'test_*.py'

test-lab: all lab
	LAB_DIR=lab python3 -m unittest -v test_lab

clean:
	rm -rf $(BUILD) __pycache__