gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1346Shell 97Rust 63Markdown 571650 Definitions

⎇ 0f5f53259f607efa1d43a651f478b4ea2b16553c recipes/gdb.toml TOML · 298 lines · 19.4 KB History raw

gdb: la distro ya puede mirar un core — y el segfault salió de leer un core 3fc189b50 · Sergio ·

1# gdb 16.3 — el depurador. Entra porque su ausencia me costó tiempo HOY, no por completar una fila.
2#
3# ══ EL ARGUMENTO, Y ES MEDIDO SOBRE ESTA SESIÓN ════════════════════════════════════════════════
4# `censo-distro.py` marca «depurar y medir» **6 de 6 ausente**, con el comentario «se construye la
5# distro entera y no hay con qué mirar un core». El 2026-10-20 eso pasó de nota a factura:
6# `libime_pinyindict` segfaulteaba con cualquier entrada, y el diagnóstico hubo que sacarlo de
7#
8#     dmesg → `ip` del fallo → el binario es no-PIE → `nm --numeric-sort` → el símbolo
9#
10# para descubrir que saltaba a `typeinfo for std::__1::collate<wchar_t>`. Con un `bt` eso eran diez
11# segundos. **Una distro que se compila entera a sí misma y no puede depurarse es una distro que
12# arregla sus bugs a ciegas.**
13#
14# ══ CUESTA UNA RECETA: SUS DEPS YA ESTÁN TODAS ═════════════════════════════════════════════════
15# gdb ≥14 exige `gmp` y `mpfr` —los dos en el corpus— además de `ncurses`, `zlib`, `expat`, `xz`,
16# `zstd` y `binutils`, todos presentes. `libiconv` es la única que falta y **no hace falta**: musl
17# trae iconv en libc.
18#
19# ══ ⚠⚠ LA VERSIÓN ES 16.3 PORQUE 18.1 **NO ARRANCA** EN MUSL — Y ESTO ES MEDIDO ════════════════
20# Empecé con 18.1, la última. Compiló entera (dos veces, ~50 min cada una), instaló, y **segfaulteó
21# antes de imprimir un solo carácter**: `--version`, `--help`, `--configuration` y
22# `-nx --batch -ex quit` dan los cuatro `rc=139` con salida vacía ⇒ el crash es en el ARRANQUE
23# (inicializadores estáticos o el principio de `main`), no en el manejo de opciones.
24#
25# ⚠ **Y mi primera hipótesis era equivocada, conviene dejarlo escrito.** El primer build tenía
26# cuatro `NEEDED` colgantes a las `.so` del lab, y supuse un desajuste de ABI. Declaré las variantes
27# `-shared` del corpus, el control de superficie dinámica pasó —los 8 sonames resuelven **desde el
28# store**, verificado con `ld-musl --list`— y **siguió segfaulteando igual**. Las fugas al lab eran
29# un problema real y no eran ESTE problema.
30#
31# ⇒ La salida fue preguntar quién tiene gdb andando sobre musl: **Alpine empaqueta 16.3**, con
32# exactamente el parche de señales de abajo. 18.1 sobre musl es terreno no probado, y diagnosticar
33# un crash de arranque **sin depurador** (el que estoy construyendo) es caro de verdad.
34#
35# La lección de método, que es la misma que destrabó el chino: para un paquete con fricción conocida
36# en musl, la versión no se elige por «la última» sino por **la que una distro musl ya sostiene**.
37#
38# ══ ⚠ EL PARCHE DE MUSL: TRECE LÍNEAS Y SE SABE EXACTAMENTE POR QUÉ ════════════════════════════
39# `gdbsupport/signals.cc` usa `__SIGRTMIN`/`__SIGRTMAX`, que son nombres **internos de glibc**; musl
40# no los define y la compilación corta. Se comprobó mirando qué parchea Alpine: de sus siete
41# parches, para x86_64 sólo aplica `musl-signals.patch` (los otros son ppc/s390x o compatibilidad
42# con GCC 15, y acá el compilador es clang). El contenido es definirlos si no existen, con los
43# valores que musl usa de hecho.
44#
45# ══ LAS PERILLAS ═══════════════════════════════════════════════════════════════════════════════
46#   --disable-binutils --disable-ld --disable-gas --disable-gprof --disable-gprofng
47#                       el tarball de gdb trae el árbol de binutils entero. **Ya tenemos
48#                       `binutils`**, y construirlo otra vez son minutos y un segundo juego de
49#                       `ar`/`ld`/`as` en el artefacto que nadie quiere
50#   --disable-sim       el simulador de arquitecturas ajenas; no se depura un MIPS acá
51#   --enable-tui        la interfaz de pantalla completa sobre ncurses. Es lo que hace a gdb usable
52#                       sin un IDE, que es el caso de esta distro
53#   --with-expat        descripciones de objetivo y tablas de syscalls en XML: sin esto `catch
54#                       syscall` no sabe nombres
55#   --disable-nls       la cadena de gettext, cuyo `msgfmt` en este lab tiene huecos medidos
56#   MAKEINFO=true       no hay `texinfo` en el corpus (el censo lo marca) y gdb intentaría generar
57#                       su manual. Lo que se pierde es el `.info`, no el programa
58#
59# ⚠ **`--without-python`, Y ES UNA DECISIÓN CON COSTE DICHO, NO UN OLVIDO.** Con soporte de Python
60# gdb trae *pretty-printers*: `std::string` se ve como texto en vez de como una estructura, y en un
61# corpus con libc++ eso importa de verdad. Queda afuera en esta primera pasada por una razón
62# concreta: `libpython3.12.a` es estática, así que gdb la enlazaría dentro, pero **seguiría
63# necesitando la biblioteca estándar de Python EN LA IMAGEN** para cargar los printers — o sea que
64# encender esto cambia lo que un rootfs con gdb tiene que llevar. Primero el depurador que funciona;
65# los printers son su propia decisión, y ahora está escrita en vez de ser un hueco que nadie eligió.
66# ══ ⚠ NO SE DECLARA EN NINGÚN PERFIL, Y ES UNA DECISIÓN ════════════════════════════════════════
67# El artefacto son **232 MB** (226 MB de ELF, de los que 98 son `.debug_*` ⇒ ~128 MB desnudado).
68# gdb es grande de verdad: lleva BFD, los analizadores de expresiones de una docena de lenguajes y
69# las tablas de todas las arquitecturas que soporta.
70#
71# Hornear eso en las ocho imágenes para una herramienta que se usa el día que algo falla no es
72# proporcionado. Y el modelo del repo ya tiene la respuesta: **una receta sin perfil se publica
73# igual** —la Etapa F la empaqueta— así que gdb queda **instalable desde el repo** sin pesar en
74# ningún rootfs. Es el mismo argumento que `[solo-build]` usa para `cmake` («que se pueda instalar
75# desde el repo es correcto; que venga horneado, no»), aunque gdb no sea una herramienta de build y
76# por eso no va en esa lista.
77#
78# ⇒ En el censo aparecerá como `tiene-sin-viajar`, que es **la verdad** y es visible. El día que
79# alguien quiera un perfil de desarrollo con depurador, es una línea en su `paquetes`.
80#
81# Instala cinco cosas, no una: `gdb`, `gdbserver` (depurar por red o en otra máquina), `gcore`
82# (volcar un core de un proceso vivo), `gstack` (su traza sin pararlo) y `gdb-add-index`.
83name    = "gdb"
84version = "16.3"
85license = "GPL-3.0-or-later"
86
87[source]
88tarball = "https://ftp.gnu.org/gnu/gdb/gdb-16.3.tar.xz"
89sha256  = "bcfcd095528a987917acf9fff3f1672181694926cc18d609c99d0042c00224c5"
90
91[build]
92compiler = "zig-cc"
93target   = "x86_64-linux-musl"
94# ⚠ **`dynamic` Y NO `static`, Y ESTO ES MEDIDO: `gmp` y `mpfr` del corpus son SÓLO COMPARTIDAS.**
95# Con `link = "static"` el lab pasa `-static` y el test de configure murió así:
96#
97#     hammer-zig-cc -o conftest -g -O2 -static conftest.c -lmpfr -lgmp
98#     error: unable to find static system library 'mpfr' using strategy 'no_fallback'
99#     error: unable to find static system library 'gmp'
100#     configure: error: Building GDB requires GMP 4.2+, and MPFR 3.1.0+.
101#
102# El mensaje de configure culpa a las versiones y el problema era la FORMA: `ls <artefacto>/usr/lib`
103# da `libgmp.so{,.10,.10.5.0}` y `libmpfr.so{,.6,.6.2.2}`, sin una sola `.a`.
104#
105# ⚠ **Y es el INVERSO EXACTO del caso de `exiv2` del mismo día**, que vale recordar junto: allá el
106# corpus publicaba sólo `.a` y **el LAB** tenía la `.so`, así que el enlace dinámico se fue en
107# silencio a la imagen base y sellé un `NEEDED` colgante. Acá el corpus publica sólo `.so` y el
108# estático falla **ruidosamente**. ⇒ **el corpus NO es uniformemente estático**, y `link` no es una
109# preferencia de estilo: tiene que coincidir con lo que las deps de ESTA receta publican de hecho.
110# Con `dynamic` cada dep entra como pueda —`ncurses`, `readline` y `zlib` siguen siendo `.a` y
111# entran estáticas; `gmp` y `mpfr` quedan en NEEDED, y las dos las provee el corpus.
112link     = "dynamic"
113flags    = []
114
115[build.phases]
116configure = '''
117set -e
118# El parche de musl. Se comprueba ANTES (que el marcador exista) y DESPUÉS (que se aplicó), para que
119# el día que upstream lo arregle o lo mueva esto falle diciéndolo en vez de parchear a ciegas.
120SC=gdbsupport/signals.cc
121[ -f "$SC" ] || { echo "gdb: no existe $SC — upstream reorganizó el árbol; revisar el parche de musl" >&2; exit 1; }
122grep -q '__SIGRTMIN' "$SC" || { echo "gdb: $SC ya no usa __SIGRTMIN; el parche de musl sobra o cambió de forma — REVISAR antes de seguir" >&2; exit 1; }
123grep -q 'ifndef __SIGRTMIN' "$SC" && { echo "gdb: $SC ya trae el guard de __SIGRTMIN — upstream lo arregló, quitar este parche" >&2; exit 1; }
124python3 - "$SC" <<'PYEOF'
125import sys
126p = sys.argv[1]
127s = open(p, encoding='utf-8').read()
128ancla = '#if defined(__SIGRTMIN)'
129if ancla not in s:
130    sys.exit("gdb: no encuentro '%s' en %s" % (ancla, p))
131guard = ('#ifndef __SIGRTMIN\n# define __SIGRTMIN 32\n#endif\n'
132         '#ifndef __SIGRTMAX\n# define __SIGRTMAX _NSIG\n#endif\n\n')
133open(p, 'w', encoding='utf-8').write(s.replace(ancla, guard + ancla, 1))
134PYEOF
135grep -q 'ifndef __SIGRTMIN' "$SC" || { echo "gdb: el parche de musl no se aplicó" >&2; exit 1; }
136echo "gdb: parche de señales de musl aplicado"
137
138# ⚠⚠ **`-fPIE`/`-pie` ES LA CURA DEL SEGFAULT, Y SALIÓ DE LEER UN CORE — no de conjeturar.**
139#
140# Las dos primeras hipótesis fueron equivocadas y conviene dejarlas escritas, porque cada una costó
141# un build de ~50 min:
142#   1. «son las `.so` del lab» → declaré las variantes `-shared`, el control de superficie dinámica
143#      pasó (los 8 sonames resuelven desde el store, verificado con `ld-musl --list`) y **siguió
144#      segfaulteando**. Las fugas al lab eran reales y NO eran esto.
145#   2. «18.1 no está probada en musl, bajar a la 16.3 que empaqueta Alpine» → **16.3 segfaultea
146#      igual**. La versión tampoco era.
147#
148# El dato salió de provocar un core (`ulimit -c unlimited`) y parsearlo a mano: `NT_PRSTATUS` da
149# `rip`, y `NT_FILE` dice en qué mapeo cae.
150#
151#     señal=11  rip=0x7fc99ebe8fa5  →  ld-musl-x86_64.so.1 +0x41fa5  →  **getenv +0x38**
152#
153# O sea: **no muere en el código de gdb, muere leyendo el ENTORNO durante el arranque.** Y la causa
154# está en el ELF:
155#
156#     readelf -rW gdb | grep -c R_X86_64_COPY   ⇒  12
157#     … R_X86_64_COPY  environ + 0              ← ésta
158#     (las otras once son globales de ncurses: curscr, COLORS, COLOR_PAIRS, acs_map, BC…)
159#
160# Un ejecutable **no-PIE** que referencia un dato de una biblioteca compartida obtiene una
161# relocación **COPY**: el cargador copia el objeto dentro del `.bss` del programa, y a partir de ahí
162# hay DOS `environ` que pueden divergir. `getenv` lee la copia y revienta.
163#
164# ⚠ Y por eso los demás binarios del corpus no lo sufren aunque también sean no-PIE (`smartctl`,
165# `swayimg`, las herramientas de libime): **ninguno referencia datos globales de una `.so`**. gdb sí,
166# por `environ` y por el TUI de ncurses. La diferencia no es «no-PIE», es «no-PIE **y** usa datos
167# globales de una compartida».
168#
169# ⇒ `-fPIE`/`-pie` elimina las COPY por construcción: el ejecutable llega al `environ` de libc por
170# el GOT en vez de copiarlo. El guardián de abajo comprueba que no quede ninguna.
171#
172# ⚠ **Y `-pie` global choca con la ÚNICA biblioteca compartida que gdb construye.** El primer
173# intento con PIE murió así:
174#
175#     CXXLD  libinproctrace.so
176#     error: dynamic libraries cannot be position independent executables
177#
178# `libinproctrace.so` es el **agente en-proceso de gdbserver** (los *fast tracepoints*: inyectar
179# código en el proceso depurado para trazar sin parar). Tiene perilla propia, así que no hay que
180# pelearse con los LDFLAGS por subdirectorio: `--disable-inprocess-agent` y listo.
181#
182# Lo que se pierde, dicho: las trazas rápidas de gdbserver. Depurar en local, los breakpoints, el
183# `bt` y todo lo que motivó esta receta no las usan.
184CFLAGS="-fPIE" CXXFLAGS="-fPIE" LDFLAGS="-pie" \
185./configure --build=$CBUILD --host=$CHOST --prefix=/usr --sysconfdir=/etc \
186  --disable-werror --disable-nls --disable-sim \
187  --disable-inprocess-agent \
188  --disable-binutils --disable-ld --disable-gas --disable-gprof --disable-gprofng \
189  --enable-tui --with-expat --without-python \
190  --with-system-zlib \
191  MAKEINFO=true
192'''
193compile = 'make -j"$(nproc)" MAKEINFO=true'
194# ⚠ EL GUARDIÁN LO EJECUTA, Y SOBRE UN BINARIO CON DWARF DE VERDAD.
195#
196# Que `/usr/bin/gdb` exista no dice nada: un depurador que no puede abrir un ELF ni leer su DWARF es
197# un binario inerte, y ésa es justo la figura que este catálogo persigue. Así que el control compila
198# un programa mínimo con `-g` y le pide a gdb la línea de `main`: eso ejercita bfd (leer el ELF), el
199# lector de DWARF y la tabla de símbolos, que es la capacidad entera.
200#
201# **No se intenta `run`/`bt`**: el sandbox va con `--unshare-all` y `ptrace` sobre un proceso hijo no
202# es algo con lo que se pueda contar ahí. Pedirlo haría fallar un gdb correcto — el error que cometí
203# cinco veces hoy escribiendo guardianes contra mi modelo del entorno y no contra el entorno.
204install = '''
205set -e
206make install DESTDIR=/out MAKEINFO=true
207[ -x /out/usr/bin/gdb ] || { echo "gdb: no quedó /usr/bin/gdb — NO sello" >&2; ls /out/usr/bin 2>/dev/null >&2; exit 1; }
208
209# ⚠⚠ **PRIMERO SE FIJA LA SUPERFICIE DINÁMICA, Y ESTE CONTROL YA SE ESCRIBIÓ MAL UNA VEZ.**
210#
211# El primer build compiló, instaló y **segfaulteó en `--version`**, y la causa estaba acá: cuatro
212# `NEEDED` hacia `.so` que el corpus no publica —salieron del lab, de otra versión— ⇒ desajuste de
213# ABI. Un artefacto así sella, reproduce y no corre.
214#
215# Mi primer intento de control fue **vetar nombres** (`libexpat`, `libz.so`, `libncursesw`…), y eso
216# habría rechazado el build CORRECTO: con las variantes `-shared` declaradas esas mismas `.so` son
217# legítimas y vienen del corpus. Desde dentro del sandbox **no se puede distinguir el origen** de
218# una `.so`, porque el lab y las deps se montan en el mismo `/usr/lib`. Pretender distinguirlo era
219# el error.
220#
221# ⇒ Lo que SÍ se puede, y es lo correcto: **fijar el conjunto exacto de sonames**. Si aparece uno
222# que no está en la lista, alguien tiene que mirarlo — puede ser una dep nueva legítima o una fuga
223# al lab, y la receta no debe adivinar cuál. La provenencia la verifica `vigia-sonames.py` sobre la
224# IMAGEN, que es su sitio.
225# ⚠ CERO relocaciones COPY: es la condición que hace que este binario arranque. Si vuelven, el
226# `getenv` del cargador revienta antes de imprimir un carácter — y el síntoma no nombra nada de
227# esto. Se comprueba ANTES de ejecutar, porque si hay COPY el segfault de abajo es consecuencia.
228ncopy=$(readelf -rW /out/usr/bin/gdb 2>/dev/null | grep -c 'R_X86_64_COPY' || true)
229[ "${ncopy:-0}" -eq 0 ] || {
230  echo "gdb: el binario trae $ncopy relocación(es) R_X86_64_COPY — volvió a enlazarse NO-PIE y el cargador de musl segfaultea en getenv al arrancar" >&2
231  readelf -rW /out/usr/bin/gdb 2>/dev/null | grep 'R_X86_64_COPY' | head -5 >&2
232  exit 1
233}
234# Y que sea DYN (PIE) y no EXEC, que es la misma cosa vista desde la cabecera.
235readelf -h /out/usr/bin/gdb 2>/dev/null | grep -qE 'Type:\s+DYN' || { echo "gdb: el ELF no es DYN (PIE) — las COPY volverían" >&2; readelf -h /out/usr/bin/gdb 2>/dev/null | grep 'Type:' >&2; exit 1; }
236
237esperados="libexpat.so.1 liblzma.so.5 libz.so.1 libzstd.so.1 libncursesw.so.6 libmpfr.so.6 libgmp.so.10 libc.so"
238reales=$(readelf -d /out/usr/bin/gdb 2>/dev/null | grep NEEDED | sed 's/.*\[\(.*\)\].*/\1/' | sort -u)
239sobra=""
240for so in $reales; do
241  case " $esperados " in *" $so "*) ;; *) sobra="$sobra $so" ;; esac
242done
243if [ -n "$sobra" ]; then
244  echo "gdb: NEEDED inesperado(s):$sobra" >&2
245  echo "  — puede ser una dep nueva legítima o una fuga a la .so del LAB (que no viaja en ninguna" >&2
246  echo "    imagen y es de otra versión: así segfaulteó el primer build). REVISAR antes de sellar." >&2
247  printf '  reales: %s\n' "$(printf '%s ' $reales)" >&2
248  exit 1
249fi
250echo "gdb: superficie dinámica fijada ($(printf '%s' "$reales" | wc -w) sonames, todos esperados)"
251
252# Ejecución y truncado SEPARADOS: el estado de una tubería es el del último comando, y eso ya me
253# selló un fallo hoy (ver `recipes/automake.toml`).
254sal=$(/out/usr/bin/gdb --version 2>&1) || { echo "gdb: no arranca" >&2; printf '%s\n' "$sal" >&2; exit 1; }
255printf '%s\n' "$sal" | head -1 | grep -qE '^GNU gdb .*16\.3' || { echo "gdb: --version inesperado: $(printf '%s\n' "$sal" | head -1)" >&2; exit 1; }
256
257# PRUEBA FUNCIONAL: abrir un ELF ajeno y leer su DWARF.
258t=$(mktemp -d)
259printf 'int main(void){return 0;}\n' > "$t/p.c"
260/opt/zig/zig cc -mcpu=baseline -target x86_64-linux-musl -g -O0 -o "$t/p" "$t/p.c" || { echo "gdb: no pude compilar el programa de prueba (problema del lab, no de gdb)" >&2; exit 1; }
261out=$(/out/usr/bin/gdb --batch -nx -ex 'info line main' "$t/p" 2>&1) || { echo "gdb: falló al leer el programa de prueba" >&2; printf '%s\n' "$out" >&2; exit 1; }
262printf '%s\n' "$out" | grep -q 'p\.c' || {
263  echo "gdb: no pudo resolver 'main' a su fichero fuente — el lector de DWARF no funciona, y un depurador que no lee símbolos es un binario inerte" >&2
264  printf '%s\n' "$out" >&2
265  exit 1
266}
267# El TUI es lo que hace a gdb usable sin IDE: si la perilla no se materializó, no está.
268printf '%s\n' "$(/out/usr/bin/gdb --configuration 2>&1)" | grep -q -- '--enable-tui' || echo "gdb: aviso — --configuration no confirma el TUI; revisar si ncurses se detectó" >&2
269echo "gdb: $(printf '%s\n' "$sal" | head -1) · lee DWARF de un ELF ajeno"
270'''
271
272[deps]
273build = ["make", "pkgconf", "gmp", "mpfr", "ncurses", "zlib", "expat", "xz", "zstd",
274         "binutils", "python3", "libtool", "m4",
275         # ⚠ **LAS CUATRO `-shared`, Y NO SON UN LUJO: SIN ELLAS gdb ENLAZA CONTRA EL LAB.**
276         # Medido en el primer build, que compiló, instaló y **segfaulteó en `--version`**:
277         #
278         #     readelf -d gdb | grep NEEDED
279         #     libexpat.so.1 · liblzma.so.5 · libz.so.1 · libncursesw.so.6 · libzstd.so.1 ·
280         #     libmpfr.so.6 · libgmp.so.10 · libc.so
281         #
282         # y el corpus publica `expat`, `zlib`, `xz` y `ncurses` **SÓLO como `.a`** ⇒ esas cuatro
283         # `.so` salieron de `.dev-fs/alpine`. gdb compiló contra las CABECERAS del corpus y enlazó
284         # contra las bibliotecas del LAB, que son de otra versión: desajuste de ABI, y el síntoma
285         # es un segfault al arrancar que no nombra nada de esto.
286         #
287         # ⇒ Las variantes `-shared` existen justo para este caso y **ya estaban selladas**;
288         # `zlib-shared`, `xz-shared` y `ncurses-shared` viajan además en los 9 perfiles, así que
289         # declararlas no agrega peso a ninguna imagen. `zstd` NO necesita variante: publica `.a`
290         # **y** `.so`, así que su NEEDED es legítimo.
291         #
292         # Es la TERCERA receta del 2026-10-20 que pisa esto (antes `exiv2` y el intento estático de
293         # gdb). Ver [[corpus-no-es-uniformemente-estatico]].
294         "expat-shared", "zlib-shared", "xz-shared", "ncurses-shared"]
295# `gmp` y `mpfr` quedan como NEEDED del binario (son las únicas deps shared-only), así que tienen
296# que estar en la CLAUSURA de cualquier imagen que lleve gdb — si no, `vigia-sonames.py` marcaría
297# un soname sin proveedor, que es justo lo que ese vigía existe para cazar.
298runtime = ["gmp", "mpfr", "zstd", "expat-shared", "zlib-shared", "xz-shared", "ncurses-shared"]