gdb: la distro ya puede mirar un core — y el segfault salió de leer un core
El censo marcaba «depurar y medir» 6 de 6 ausente: «se construye la distro
entera y no hay con qué mirar un core». El 2026-10-20 eso pasó de nota a
factura: libime_pinyindict segfaulteaba con cualquier entrada y el diagnóstico
hubo que sacarlo de dmesg → ip → binario no-PIE → nm --numeric-sort → símbolo,
para descubrir que saltaba a un typeinfo. Con un `bt` eran diez segundos.
Sella con gdb 16.3 + gdbserver + gcore + gstack + gdb-add-index, y el guardián
NO se conforma con que el fichero exista: compila un programa con -g y le pide
`info line main`, lo que ejercita bfd, el lector de DWARF y la tabla de
símbolos. Verificado. (No intenta run/bt: el sandbox va con --unshare-all y no
se puede contar con ptrace — pedirlo rechazaría un gdb correcto.)
⚠⚠ CUATRO VUELTAS, Y LAS DOS PRIMERAS HIPÓTESIS ERAN MÍAS Y EQUIVOCADAS:
1. «son las .so del lab» — el primer build tenía 4 NEEDED colgantes
(libexpat/liblzma/libz/libncursesw) porque el corpus publica esas sólo
estáticas y salieron de .dev-fs/alpine. Declaré las variantes -shared, el
control de superficie dinámica pasó (8 sonames resolviendo desde el store,
verificado con `ld-musl --list`) y SIGUIÓ SEGFAULTEANDO. Era un problema
real y no era ESTE problema.
2. «18.1 no está probada en musl; Alpine empaqueta 16.3» — 16.3 segfaultea
igual. La versión tampoco era.
Lo que sirvió fue dejar de razonar sobre el síntoma y LEER UN CORE:
`ulimit -c unlimited`, y parsear el ELF a mano — NT_PRSTATUS da rip, NT_FILE
dice en qué mapeo cae.
señal=11 rip=0x7fc99ebe8fa5 → ld-musl-x86_64.so.1 +0x41fa5 → getenv +0x38
No muere en el código de gdb: muere leyendo el ENTORNO al arrancar. Y el ELF lo
explicaba:
readelf -rW gdb | grep -c R_X86_64_COPY ⇒ 12
… R_X86_64_COPY environ + 0 ← ésta
(las otras once: curscr, COLORS, COLOR_PAIRS, acs_map, BC… del TUI)
Un ejecutable NO-PIE que referencia un dato global de una .so recibe una
relocación COPY: el cargador duplica el objeto en su .bss y a partir de ahí hay
DOS `environ` que divergen. getenv lee la copia y revienta.
⇒ Por eso los demás binarios no-PIE del corpus arrancan bien (smartctl,
swayimg, las herramientas de libime): NINGUNO referencia datos globales de una
compartida. La diferencia no es «no-PIE», es «no-PIE Y usa globales de una .so».
-fPIE/-pie las elimina por construcción. Y choca con la única .so que gdb
construye —libinproctrace.so, el agente en-proceso de gdbserver—, que tiene
perilla propia: --disable-inprocess-agent. Lo que se pierde son los fast
tracepoints; el bt y los breakpoints no los usan.
El guardián ahora exige CERO relocaciones COPY y que el ELF sea DYN, ANTES de
ejecutar nada — si vuelven, el segfault sería consecuencia y no causa.
⚠ NO SE DECLARA EN NINGÚN PERFIL, y es decisión: son 232 MB (~128 desnudado), y
hornear eso en ocho imágenes para una herramienta que se usa el día que algo
falla no es proporcionado. El modelo del repo publica igual una receta sin perfil
(Etapa F), así que queda instalable desde el repo. En el censo aparecerá como
`tiene-sin-viajar`, que es la verdad y es visible.
Semántico sayk'uy
Mana ima sutichaypas t'ikrakurqanchu: qillqalla, yachachiylla utaq willaylla t'ikrakurqan.
1 qillqakuna · +298 −0
- recipes/gdb.toml +298 −0
diff --git a/recipes/gdb.toml b/recipes/gdb.toml
new file mode 100644
index 00000000..c703c141
--- /dev/null
+++ b/recipes/gdb.toml
@@ -0,0 +1,298 @@
+# gdb 16.3 — el depurador. Entra porque su ausencia me costó tiempo HOY, no por completar una fila.
+#
+# ══ EL ARGUMENTO, Y ES MEDIDO SOBRE ESTA SESIÓN ════════════════════════════════════════════════
+# `censo-distro.py` marca «depurar y medir» **6 de 6 ausente**, con el comentario «se construye la
+# distro entera y no hay con qué mirar un core». El 2026-10-20 eso pasó de nota a factura:
+# `libime_pinyindict` segfaulteaba con cualquier entrada, y el diagnóstico hubo que sacarlo de
+#
+# dmesg → `ip` del fallo → el binario es no-PIE → `nm --numeric-sort` → el símbolo
+#
+# para descubrir que saltaba a `typeinfo for std::__1::collate<wchar_t>`. Con un `bt` eso eran diez
+# segundos. **Una distro que se compila entera a sí misma y no puede depurarse es una distro que
+# arregla sus bugs a ciegas.**
+#
+# ══ CUESTA UNA RECETA: SUS DEPS YA ESTÁN TODAS ═════════════════════════════════════════════════
+# gdb ≥14 exige `gmp` y `mpfr` —los dos en el corpus— además de `ncurses`, `zlib`, `expat`, `xz`,
+# `zstd` y `binutils`, todos presentes. `libiconv` es la única que falta y **no hace falta**: musl
+# trae iconv en libc.
+#
+# ══ ⚠⚠ LA VERSIÓN ES 16.3 PORQUE 18.1 **NO ARRANCA** EN MUSL — Y ESTO ES MEDIDO ════════════════
+# Empecé con 18.1, la última. Compiló entera (dos veces, ~50 min cada una), instaló, y **segfaulteó
+# antes de imprimir un solo carácter**: `--version`, `--help`, `--configuration` y
+# `-nx --batch -ex quit` dan los cuatro `rc=139` con salida vacía ⇒ el crash es en el ARRANQUE
+# (inicializadores estáticos o el principio de `main`), no en el manejo de opciones.
+#
+# ⚠ **Y mi primera hipótesis era equivocada, conviene dejarlo escrito.** El primer build tenía
+# cuatro `NEEDED` colgantes a las `.so` del lab, y supuse un desajuste de ABI. Declaré las variantes
+# `-shared` del corpus, el control de superficie dinámica pasó —los 8 sonames resuelven **desde el
+# store**, verificado con `ld-musl --list`— y **siguió segfaulteando igual**. Las fugas al lab eran
+# un problema real y no eran ESTE problema.
+#
+# ⇒ La salida fue preguntar quién tiene gdb andando sobre musl: **Alpine empaqueta 16.3**, con
+# exactamente el parche de señales de abajo. 18.1 sobre musl es terreno no probado, y diagnosticar
+# un crash de arranque **sin depurador** (el que estoy construyendo) es caro de verdad.
+#
+# La lección de método, que es la misma que destrabó el chino: para un paquete con fricción conocida
+# en musl, la versión no se elige por «la última» sino por **la que una distro musl ya sostiene**.
+#
+# ══ ⚠ EL PARCHE DE MUSL: TRECE LÍNEAS Y SE SABE EXACTAMENTE POR QUÉ ════════════════════════════
+# `gdbsupport/signals.cc` usa `__SIGRTMIN`/`__SIGRTMAX`, que son nombres **internos de glibc**; musl
+# no los define y la compilación corta. Se comprobó mirando qué parchea Alpine: de sus siete
+# parches, para x86_64 sólo aplica `musl-signals.patch` (los otros son ppc/s390x o compatibilidad
+# con GCC 15, y acá el compilador es clang). El contenido es definirlos si no existen, con los
+# valores que musl usa de hecho.
+#
+# ══ LAS PERILLAS ═══════════════════════════════════════════════════════════════════════════════
+# --disable-binutils --disable-ld --disable-gas --disable-gprof --disable-gprofng
+# el tarball de gdb trae el árbol de binutils entero. **Ya tenemos
+# `binutils`**, y construirlo otra vez son minutos y un segundo juego de
+# `ar`/`ld`/`as` en el artefacto que nadie quiere
+# --disable-sim el simulador de arquitecturas ajenas; no se depura un MIPS acá
+# --enable-tui la interfaz de pantalla completa sobre ncurses. Es lo que hace a gdb usable
+# sin un IDE, que es el caso de esta distro
+# --with-expat descripciones de objetivo y tablas de syscalls en XML: sin esto `catch
+# syscall` no sabe nombres
+# --disable-nls la cadena de gettext, cuyo `msgfmt` en este lab tiene huecos medidos
+# MAKEINFO=true no hay `texinfo` en el corpus (el censo lo marca) y gdb intentaría generar
+# su manual. Lo que se pierde es el `.info`, no el programa
+#
+# ⚠ **`--without-python`, Y ES UNA DECISIÓN CON COSTE DICHO, NO UN OLVIDO.** Con soporte de Python
+# gdb trae *pretty-printers*: `std::string` se ve como texto en vez de como una estructura, y en un
+# corpus con libc++ eso importa de verdad. Queda afuera en esta primera pasada por una razón
+# concreta: `libpython3.12.a` es estática, así que gdb la enlazaría dentro, pero **seguiría
+# necesitando la biblioteca estándar de Python EN LA IMAGEN** para cargar los printers — o sea que
+# encender esto cambia lo que un rootfs con gdb tiene que llevar. Primero el depurador que funciona;
+# los printers son su propia decisión, y ahora está escrita en vez de ser un hueco que nadie eligió.
+# ══ ⚠ NO SE DECLARA EN NINGÚN PERFIL, Y ES UNA DECISIÓN ════════════════════════════════════════
+# El artefacto son **232 MB** (226 MB de ELF, de los que 98 son `.debug_*` ⇒ ~128 MB desnudado).
+# gdb es grande de verdad: lleva BFD, los analizadores de expresiones de una docena de lenguajes y
+# las tablas de todas las arquitecturas que soporta.
+#
+# Hornear eso en las ocho imágenes para una herramienta que se usa el día que algo falla no es
+# proporcionado. Y el modelo del repo ya tiene la respuesta: **una receta sin perfil se publica
+# igual** —la Etapa F la empaqueta— así que gdb queda **instalable desde el repo** sin pesar en
+# ningún rootfs. Es el mismo argumento que `[solo-build]` usa para `cmake` («que se pueda instalar
+# desde el repo es correcto; que venga horneado, no»), aunque gdb no sea una herramienta de build y
+# por eso no va en esa lista.
+#
+# ⇒ En el censo aparecerá como `tiene-sin-viajar`, que es **la verdad** y es visible. El día que
+# alguien quiera un perfil de desarrollo con depurador, es una línea en su `paquetes`.
+#
+# Instala cinco cosas, no una: `gdb`, `gdbserver` (depurar por red o en otra máquina), `gcore`
+# (volcar un core de un proceso vivo), `gstack` (su traza sin pararlo) y `gdb-add-index`.
+name = "gdb"
+version = "16.3"
+license = "GPL-3.0-or-later"
+
+[source]
+tarball = "https://ftp.gnu.org/gnu/gdb/gdb-16.3.tar.xz"
+sha256 = "bcfcd095528a987917acf9fff3f1672181694926cc18d609c99d0042c00224c5"
+
+[build]
+compiler = "zig-cc"
+target = "x86_64-linux-musl"
+# ⚠ **`dynamic` Y NO `static`, Y ESTO ES MEDIDO: `gmp` y `mpfr` del corpus son SÓLO COMPARTIDAS.**
+# Con `link = "static"` el lab pasa `-static` y el test de configure murió así:
+#
+# hammer-zig-cc -o conftest -g -O2 -static conftest.c -lmpfr -lgmp
+# error: unable to find static system library 'mpfr' using strategy 'no_fallback'
+# error: unable to find static system library 'gmp'
+# configure: error: Building GDB requires GMP 4.2+, and MPFR 3.1.0+.
+#
+# El mensaje de configure culpa a las versiones y el problema era la FORMA: `ls <artefacto>/usr/lib`
+# da `libgmp.so{,.10,.10.5.0}` y `libmpfr.so{,.6,.6.2.2}`, sin una sola `.a`.
+#
+# ⚠ **Y es el INVERSO EXACTO del caso de `exiv2` del mismo día**, que vale recordar junto: allá el
+# corpus publicaba sólo `.a` y **el LAB** tenía la `.so`, así que el enlace dinámico se fue en
+# silencio a la imagen base y sellé un `NEEDED` colgante. Acá el corpus publica sólo `.so` y el
+# estático falla **ruidosamente**. ⇒ **el corpus NO es uniformemente estático**, y `link` no es una
+# preferencia de estilo: tiene que coincidir con lo que las deps de ESTA receta publican de hecho.
+# Con `dynamic` cada dep entra como pueda —`ncurses`, `readline` y `zlib` siguen siendo `.a` y
+# entran estáticas; `gmp` y `mpfr` quedan en NEEDED, y las dos las provee el corpus.
+link = "dynamic"
+flags = []
+
+[build.phases]
+configure = '''
+set -e
+# El parche de musl. Se comprueba ANTES (que el marcador exista) y DESPUÉS (que se aplicó), para que
+# el día que upstream lo arregle o lo mueva esto falle diciéndolo en vez de parchear a ciegas.
+SC=gdbsupport/signals.cc
+[ -f "$SC" ] || { echo "gdb: no existe $SC — upstream reorganizó el árbol; revisar el parche de musl" >&2; exit 1; }
+grep -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; }
+grep -q 'ifndef __SIGRTMIN' "$SC" && { echo "gdb: $SC ya trae el guard de __SIGRTMIN — upstream lo arregló, quitar este parche" >&2; exit 1; }
+python3 - "$SC" <<'PYEOF'
+import sys
+p = sys.argv[1]
+s = open(p, encoding='utf-8').read()
+ancla = '#if defined(__SIGRTMIN)'
+if ancla not in s:
+ sys.exit("gdb: no encuentro '%s' en %s" % (ancla, p))
+guard = ('#ifndef __SIGRTMIN\n# define __SIGRTMIN 32\n#endif\n'
+ '#ifndef __SIGRTMAX\n# define __SIGRTMAX _NSIG\n#endif\n\n')
+open(p, 'w', encoding='utf-8').write(s.replace(ancla, guard + ancla, 1))
+PYEOF
+grep -q 'ifndef __SIGRTMIN' "$SC" || { echo "gdb: el parche de musl no se aplicó" >&2; exit 1; }
+echo "gdb: parche de señales de musl aplicado"
+
+# ⚠⚠ **`-fPIE`/`-pie` ES LA CURA DEL SEGFAULT, Y SALIÓ DE LEER UN CORE — no de conjeturar.**
+#
+# Las dos primeras hipótesis fueron equivocadas y conviene dejarlas escritas, porque cada una costó
+# un build de ~50 min:
+# 1. «son las `.so` del lab» → declaré las variantes `-shared`, el control de superficie dinámica
+# pasó (los 8 sonames resuelven desde el store, verificado con `ld-musl --list`) y **siguió
+# segfaulteando**. Las fugas al lab eran reales y NO eran esto.
+# 2. «18.1 no está probada en musl, bajar a la 16.3 que empaqueta Alpine» → **16.3 segfaultea
+# igual**. La versión tampoco era.
+#
+# El dato salió de provocar un core (`ulimit -c unlimited`) y parsearlo a mano: `NT_PRSTATUS` da
+# `rip`, y `NT_FILE` dice en qué mapeo cae.
+#
+# señal=11 rip=0x7fc99ebe8fa5 → ld-musl-x86_64.so.1 +0x41fa5 → **getenv +0x38**
+#
+# O sea: **no muere en el código de gdb, muere leyendo el ENTORNO durante el arranque.** Y la causa
+# está en el ELF:
+#
+# readelf -rW gdb | grep -c R_X86_64_COPY ⇒ 12
+# … R_X86_64_COPY environ + 0 ← ésta
+# (las otras once son globales de ncurses: curscr, COLORS, COLOR_PAIRS, acs_map, BC…)
+#
+# Un ejecutable **no-PIE** que referencia un dato de una biblioteca compartida obtiene una
+# relocación **COPY**: el cargador copia el objeto dentro del `.bss` del programa, y a partir de ahí
+# hay DOS `environ` que pueden divergir. `getenv` lee la copia y revienta.
+#
+# ⚠ Y por eso los demás binarios del corpus no lo sufren aunque también sean no-PIE (`smartctl`,
+# `swayimg`, las herramientas de libime): **ninguno referencia datos globales de una `.so`**. gdb sí,
+# por `environ` y por el TUI de ncurses. La diferencia no es «no-PIE», es «no-PIE **y** usa datos
+# globales de una compartida».
+#
+# ⇒ `-fPIE`/`-pie` elimina las COPY por construcción: el ejecutable llega al `environ` de libc por
+# el GOT en vez de copiarlo. El guardián de abajo comprueba que no quede ninguna.
+#
+# ⚠ **Y `-pie` global choca con la ÚNICA biblioteca compartida que gdb construye.** El primer
+# intento con PIE murió así:
+#
+# CXXLD libinproctrace.so
+# error: dynamic libraries cannot be position independent executables
+#
+# `libinproctrace.so` es el **agente en-proceso de gdbserver** (los *fast tracepoints*: inyectar
+# código en el proceso depurado para trazar sin parar). Tiene perilla propia, así que no hay que
+# pelearse con los LDFLAGS por subdirectorio: `--disable-inprocess-agent` y listo.
+#
+# Lo que se pierde, dicho: las trazas rápidas de gdbserver. Depurar en local, los breakpoints, el
+# `bt` y todo lo que motivó esta receta no las usan.
+CFLAGS="-fPIE" CXXFLAGS="-fPIE" LDFLAGS="-pie" \
+./configure --build=$CBUILD --host=$CHOST --prefix=/usr --sysconfdir=/etc \
+ --disable-werror --disable-nls --disable-sim \
+ --disable-inprocess-agent \
+ --disable-binutils --disable-ld --disable-gas --disable-gprof --disable-gprofng \
+ --enable-tui --with-expat --without-python \
+ --with-system-zlib \
+ MAKEINFO=true
+'''
+compile = 'make -j"$(nproc)" MAKEINFO=true'
+# ⚠ EL GUARDIÁN LO EJECUTA, Y SOBRE UN BINARIO CON DWARF DE VERDAD.
+#
+# Que `/usr/bin/gdb` exista no dice nada: un depurador que no puede abrir un ELF ni leer su DWARF es
+# un binario inerte, y ésa es justo la figura que este catálogo persigue. Así que el control compila
+# un programa mínimo con `-g` y le pide a gdb la línea de `main`: eso ejercita bfd (leer el ELF), el
+# lector de DWARF y la tabla de símbolos, que es la capacidad entera.
+#
+# **No se intenta `run`/`bt`**: el sandbox va con `--unshare-all` y `ptrace` sobre un proceso hijo no
+# es algo con lo que se pueda contar ahí. Pedirlo haría fallar un gdb correcto — el error que cometí
+# cinco veces hoy escribiendo guardianes contra mi modelo del entorno y no contra el entorno.
+install = '''
+set -e
+make install DESTDIR=/out MAKEINFO=true
+[ -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; }
+
+# ⚠⚠ **PRIMERO SE FIJA LA SUPERFICIE DINÁMICA, Y ESTE CONTROL YA SE ESCRIBIÓ MAL UNA VEZ.**
+#
+# El primer build compiló, instaló y **segfaulteó en `--version`**, y la causa estaba acá: cuatro
+# `NEEDED` hacia `.so` que el corpus no publica —salieron del lab, de otra versión— ⇒ desajuste de
+# ABI. Un artefacto así sella, reproduce y no corre.
+#
+# Mi primer intento de control fue **vetar nombres** (`libexpat`, `libz.so`, `libncursesw`…), y eso
+# habría rechazado el build CORRECTO: con las variantes `-shared` declaradas esas mismas `.so` son
+# legítimas y vienen del corpus. Desde dentro del sandbox **no se puede distinguir el origen** de
+# una `.so`, porque el lab y las deps se montan en el mismo `/usr/lib`. Pretender distinguirlo era
+# el error.
+#
+# ⇒ Lo que SÍ se puede, y es lo correcto: **fijar el conjunto exacto de sonames**. Si aparece uno
+# que no está en la lista, alguien tiene que mirarlo — puede ser una dep nueva legítima o una fuga
+# al lab, y la receta no debe adivinar cuál. La provenencia la verifica `vigia-sonames.py` sobre la
+# IMAGEN, que es su sitio.
+# ⚠ CERO relocaciones COPY: es la condición que hace que este binario arranque. Si vuelven, el
+# `getenv` del cargador revienta antes de imprimir un carácter — y el síntoma no nombra nada de
+# esto. Se comprueba ANTES de ejecutar, porque si hay COPY el segfault de abajo es consecuencia.
+ncopy=$(readelf -rW /out/usr/bin/gdb 2>/dev/null | grep -c 'R_X86_64_COPY' || true)
+[ "${ncopy:-0}" -eq 0 ] || {
+ 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
+ readelf -rW /out/usr/bin/gdb 2>/dev/null | grep 'R_X86_64_COPY' | head -5 >&2
+ exit 1
+}
+# Y que sea DYN (PIE) y no EXEC, que es la misma cosa vista desde la cabecera.
+readelf -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; }
+
+esperados="libexpat.so.1 liblzma.so.5 libz.so.1 libzstd.so.1 libncursesw.so.6 libmpfr.so.6 libgmp.so.10 libc.so"
+reales=$(readelf -d /out/usr/bin/gdb 2>/dev/null | grep NEEDED | sed 's/.*\[\(.*\)\].*/\1/' | sort -u)
+sobra=""
+for so in $reales; do
+ case " $esperados " in *" $so "*) ;; *) sobra="$sobra $so" ;; esac
+done
+if [ -n "$sobra" ]; then
+ echo "gdb: NEEDED inesperado(s):$sobra" >&2
+ echo " — puede ser una dep nueva legítima o una fuga a la .so del LAB (que no viaja en ninguna" >&2
+ echo " imagen y es de otra versión: así segfaulteó el primer build). REVISAR antes de sellar." >&2
+ printf ' reales: %s\n' "$(printf '%s ' $reales)" >&2
+ exit 1
+fi
+echo "gdb: superficie dinámica fijada ($(printf '%s' "$reales" | wc -w) sonames, todos esperados)"
+
+# Ejecución y truncado SEPARADOS: el estado de una tubería es el del último comando, y eso ya me
+# selló un fallo hoy (ver `recipes/automake.toml`).
+sal=$(/out/usr/bin/gdb --version 2>&1) || { echo "gdb: no arranca" >&2; printf '%s\n' "$sal" >&2; exit 1; }
+printf '%s\n' "$sal" | head -1 | grep -qE '^GNU gdb .*16\.3' || { echo "gdb: --version inesperado: $(printf '%s\n' "$sal" | head -1)" >&2; exit 1; }
+
+# PRUEBA FUNCIONAL: abrir un ELF ajeno y leer su DWARF.
+t=$(mktemp -d)
+printf 'int main(void){return 0;}\n' > "$t/p.c"
+/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; }
+out=$(/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; }
+printf '%s\n' "$out" | grep -q 'p\.c' || {
+ 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
+ printf '%s\n' "$out" >&2
+ exit 1
+}
+# El TUI es lo que hace a gdb usable sin IDE: si la perilla no se materializó, no está.
+printf '%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
+echo "gdb: $(printf '%s\n' "$sal" | head -1) · lee DWARF de un ELF ajeno"
+'''
+
+[deps]
+build = ["make", "pkgconf", "gmp", "mpfr", "ncurses", "zlib", "expat", "xz", "zstd",
+ "binutils", "python3", "libtool", "m4",
+ # ⚠ **LAS CUATRO `-shared`, Y NO SON UN LUJO: SIN ELLAS gdb ENLAZA CONTRA EL LAB.**
+ # Medido en el primer build, que compiló, instaló y **segfaulteó en `--version`**:
+ #
+ # readelf -d gdb | grep NEEDED
+ # libexpat.so.1 · liblzma.so.5 · libz.so.1 · libncursesw.so.6 · libzstd.so.1 ·
+ # libmpfr.so.6 · libgmp.so.10 · libc.so
+ #
+ # y el corpus publica `expat`, `zlib`, `xz` y `ncurses` **SÓLO como `.a`** ⇒ esas cuatro
+ # `.so` salieron de `.dev-fs/alpine`. gdb compiló contra las CABECERAS del corpus y enlazó
+ # contra las bibliotecas del LAB, que son de otra versión: desajuste de ABI, y el síntoma
+ # es un segfault al arrancar que no nombra nada de esto.
+ #
+ # ⇒ Las variantes `-shared` existen justo para este caso y **ya estaban selladas**;
+ # `zlib-shared`, `xz-shared` y `ncurses-shared` viajan además en los 9 perfiles, así que
+ # declararlas no agrega peso a ninguna imagen. `zstd` NO necesita variante: publica `.a`
+ # **y** `.so`, así que su NEEDED es legítimo.
+ #
+ # Es la TERCERA receta del 2026-10-20 que pisa esto (antes `exiv2` y el intento estático de
+ # gdb). Ver [[corpus-no-es-uniformemente-estatico]].
+ "expat-shared", "zlib-shared", "xz-shared", "ncurses-shared"]
+# `gmp` y `mpfr` quedan como NEEDED del binario (son las únicas deps shared-only), así que tienen
+# que estar en la CLAUSURA de cualquier imagen que lleve gdb — si no, `vigia-sonames.py` marcaría
+# un soname sin proveedor, que es justo lo que ese vigía existe para cazar.
+runtime = ["gmp", "mpfr", "zstd", "expat-shared", "zlib-shared", "xz-shared", "ncurses-shared"]