Quando o gfix não resolve: lendo um banco Firebird corrompido na unha
TL;DR: O banco Firebird corrompeu, o gfix não resolve, o gbak reclama, e os dados eram para ontem. Escrevi uma ferramenta de código aberto que lê o arquivo .fdb direto, sem o motor do Firebird, e neste texto mostro como o arquivo é por dentro e como dá para tirar os dados de lá na unha.
Aviso: português não é a minha língua nativa (sou desenvolvedor russo). Se alguma frase soar estranha, me corrijam nos comentários.
Se você já trabalhou com Firebird ou InterBase — e no Brasil muito ERP, sistema de automação comercial e software de clínica roda nele —, conhece este momento: o banco não abre, o gfix mostra erros, o gbak não termina o backup, e o cliente está ligando.
O caminho padrão:
gfix -v -full— erros de validação.gfix -mend -full -ignore— "agora vai".gbak -b -ig -g—Exiting before completion due to errors.- Google — ferramenta paga por banco ou "serviço de recuperação".
- Choramos.
O caminho alternativo: entender como o Firebird guarda os dados no disco e ler o arquivo diretamente. O formato não é segredo — o código do Firebird é aberto —, só não é muito documentado.
Contexto
A primeira versão dessa ferramenta eu escrevi em Delphi, para um caso concreto, e publiquei no Habr (o "TabNews russo"). Ela resolvia o meu caso, mas era Windows-only, sem testes e cheia de suposições sobre o formato que eu nunca tinha conferido.
Então reescrevi do zero em Go. O resultado se chama fbrescue: um executável sem dependências para Windows, Linux e macOS, que lê bancos do Firebird 2.5 ao 5.0 e nunca grava no arquivo. E, o mais importante, cada suposição sobre o formato agora é testada contra bancos criados pelos motores reais do Firebird — sobre isso falo no fim.
Como é o arquivo do Firebird
Um banco Firebird é um arquivo dividido em páginas de tamanho fixo (4, 8, 16 ou 32 KB). Cada página começa com um cabeçalho, e o primeiro byte diz o tipo:
| Tipo | Página | Para que serve |
|---|---|---|
| 1 | header | o "passaporte" do banco: tamanho de página, versão do formato (ODS), contadores de transações |
| 2 | PIP | page inventory: quais páginas estão livres |
| 3 | TIP | transaction inventory: o estado de cada transação |
| 4 | pointer | a lista das páginas de dados de uma tabela |
| 5 | data | os registros |
| 6, 7 | index root, index | índices (B-tree) |
| 8 | blob | BLOBs grandes |
| 9 | generator | valores dos generators |
Um detalhe que depois salva a vida: a partir do Firebird 3 (ODS 12), cada página guarda o próprio número no offset 0x0C. No Firebird 2.5 (ODS 11), no offset 0x02 fica um "checksum" que é sempre 12345. Com essas duas assinaturas dá para descobrir o tamanho de página mesmo quando a página de cabeçalho está destruída: basta testar 4K, 8K, 16K, 32K e ver em qual delas as páginas "se reconhecem".
TIP — quem está vivo, quem está morto
O Firebird é MVCC: uma linha pode ter várias versões, e qual delas uma consulta enxerga depende do estado da transação que gravou cada versão. Esse estado fica nas páginas TIP, dois bits por transação:
| Bits | Estado |
|---|---|
| 0 | active |
| 1 | limbo |
| 2 | dead (rollback) |
| 3 | committed |
Numa página de 4K cabem (4096 − 20) × 4 = 16 304 transações. E tem uma regra útil: toda transação mais antiga que a OIT (oldest interesting transaction, que está no cabeçalho) está confirmada por definição. Ou seja, mesmo com a TIP destruída, para a maior parte das linhas dá para saber qual versão vale.
Data page — onde moram os registros
A página de dados tem a tabela a que pertence (0x14), a sua posição na tabela (0x10) e uma lista de slots: offset e tamanho de cada registro. Cada registro começa com um cabeçalho de 13 bytes:
transaction u32 quem gravou esta versão
back page u32 onde está a versão anterior
back line u16
flags u16 0x01 apagado, 0x02 versão antiga, 0x04 fragmento,
0x08 continua em outra página, 0x20 versão anterior guardada como diferença,
0x800 não comprimido (Firebird 5)
format u8 qual layout da tabela usar
Depois do cabeçalho vêm os dados, comprimidos com um RLE bem simples: um byte de controle positivo n significa "copie os próximos n bytes"; negativo significa "repita o próximo byte −n vezes". Simplificado, em Go:
func unpack(src []byte) []byte {
var out []byte
for i := 0; i < len(src); {
n := int(int8(src[i]))
i++
switch {
case n > 0: // n bytes literais
out = append(out, src[i:i+n]...)
i += n
case n < 0: // um byte repetido -n vezes
out = append(out, bytes.Repeat([]byte{src[i]}, -n)...)
i++
}
}
return out
}
O Firebird 5 mudou isso um pouco: −1 passou a significar "o tamanho da repetição vem nos próximos 2 bytes", e registros que não comprimem são guardados como estão, com a flag 0x800. Quem escreveu um leitor para o 3.0 e testou só nele leva um susto aqui.
O format aponta para RDB$FORMATS: a cada ALTER TABLE nasce um formato novo, e os registros antigos continuam no formato antigo até serem regravados. Por isso uma tabela alterada sete vezes pode ter registros em sete layouts diferentes no mesmo arquivo — e cada um precisa ser lido com o seu.
Por que linhas apagadas podem ser recuperadas
Um DELETE não apaga nada fisicamente. Ele grava uma nova versão "toco" com a flag 0x01, que aponta (back page/back line) para a versão anterior — a linha inteira. Um UPDATE faz o mesmo, e muitas vezes guarda a versão antiga só como diferença em relação à nova (flag 0x20).
Tudo isso fica no disco até a coleta de lixo passar. Então, se alguém rodou um DELETE sem WHERE e ninguém mexeu no banco depois, as linhas ainda estão lá. O fbrescue exporta isso com --include-deleted (linhas apagadas em arquivo separado) e --versions all (todas as versões de cada linha, com a transação e o estado dela).
Juntando tudo
O algoritmo de leitura de um banco saudável:
- Página 0 → tamanho de página, ODS, OIT.
RDB$PAGES→ onde estão as páginas TIP e as páginas de ponteiros de cada tabela.RDB$RELATIONS,RDB$RELATION_FIELDS,RDB$FORMATS→ tabelas, colunas e layouts.- Para cada tabela: páginas de ponteiros → páginas de dados → registros.
- Para cada registro: percorre a cadeia de versões e pega a primeira confirmada.
- Descomprime, aplica o layout do formato, converte os valores.
Detalhe divertido: as tabelas de sistema (RDB$...) são tabelas comuns, guardadas do mesmo jeito que as suas. Para ler o catálogo você já precisa saber ler registros.
Quando a estrutura quebra
Aqui está a parte que dá trabalho de verdade. O gbak lê pelo motor, e o motor confia na estrutura: se a página de ponteiros de uma tabela está zerada, para ele a tabela inteira sumiu. Mas os dados quase sempre continuam no arquivo:
- Página de ponteiros perdida: cada página de dados sabe a que tabela pertence e qual a sua posição. Uma passada pelo arquivo inteiro encontra as páginas "órfãs" e preenche as lacunas.
- Página fora do lugar (cópia malfeita, reparo do sistema de arquivos): no Firebird 3+ a página diz qual é o seu número, então é lida de onde está — e nunca é lida como se fosse outra.
- TIP danificada: transações abaixo da OIT estão confirmadas; para as mais novas cujo estado se perdeu, vale a versão mais nova, com um aviso de quantas linhas foram decididas assim.
- Cabeçalho destruído: o tamanho de página vem das assinaturas acima; a versão do formato, de testar qual ODS faz os registros de
RDB$RELATIONSterem os nomes certos (RDB$PAGES,RDB$RELATIONS, ...). - Catálogo danificado: tabelas sem nome saem como
TABLE_131, colunas comoFIELD_2. Se até os layouts se perderam, você passa o scriptCREATE TABLEdo banco (--ddl schema.sql) e o fbrescue calcula o layout pelas regras de alinhamento do motor.
Caso prático
Peguei um banco de teste de uma "loja" no Firebird 5.0 — CUSTOMERS (2 000 linhas), ORDERS (5 000) e ITEMS (14 230) — e, numa cópia, zerei duas páginas: a página de ponteiros de ORDERS e uma das suas páginas de dados. Coisa que um disco com defeito faz sem pedir licença.
O caminho padrão, com o próprio Firebird 5.0:
$ gfix -mend -full -ignore loja-danificada.fdb
$ gbak -b -ig -g loja-danificada.fdb loja.fbk
gbak: ERROR:database file appears corrupt (loja-danificada.fdb)
gbak: ERROR: wrong page type
gbak: ERROR: page 286 is of wrong type (expected pointer, found purposely undefined)
gbak: ERROR:gds_$compile_request failed
gbak:Exiting before completion due to errors
O gfix -mend não ajuda, e o backup não sai — nem das tabelas que estão perfeitas.
O caminho alternativo:
$ fbrescue scan loja-danificada.fdb
...
129 problems:
page 286 invalid-type page is marked as used but has type 0
page 528 invalid-type page is marked as used but has type 0
page 345 orphan-data-page data page of table 129 is not referenced by any pointer page
...
$ fbrescue export loja-danificada.fdb --out resgate --format csv
CUSTOMERS 2000 rows ✓
ORDERS 4948 rows ⚠ 1 skipped
ITEMS 14230 rows ✓
Wrote resgate: schema.sql and 3 csv files
1 rows or pages could not be exported: details in _errors.jsonl
⚠ relation 129: 127 data pages not reachable through its pointer pages were found by scanning the file
21 178 de 21 230 linhas. As 52 que faltam estavam na página zerada — essas não voltam, nenhuma ferramenta inventa bytes. O resto, incluindo as 127 páginas de ORDERS que o motor já não enxergava, saiu inteiro. O código de saída é 2 ("parte dos dados ficou de fora"), e o _errors.jsonl diz exatamente o quê.
Como eu sei que isso funciona
Ferramenta de recuperação que "parece funcionar" é pior que nenhuma: você só descobre o erro quando o cliente reclama de um valor errado seis meses depois. Então:
- 79 bancos de teste são gerados no CI pelos motores reais do Firebird 2.5, 3.0, 4.0 e 5.0: todos os tipos de coluna com valores-limite, tabelas alteradas sete vezes, registros maiores que uma página, BLOBs de todos os níveis, banco multiarquivo, transações desfeitas, linhas apagadas.
- Teste de ida e volta: a exportação em SQL é carregada num banco novo pelo mesmo Firebird, e o
SELECTno banco novo precisa imprimir exatamente o mesmo que no original, valor por valor. - Teste de dano: o banco da loja é danificado de 13 jeitos diferentes, 10 cópias por tipo em cada uma das 4 versões — 520 bancos quebrados —, e cada linha exportada é comparada com a original pela chave primária. A tabela completa está no README; alguns números:
| Dano | Linhas recuperadas |
|---|---|
| página de índice zerada | 100% |
| página de ponteiros zerada | 100% |
| página TIP zerada | 100% |
| página de cabeçalho zerada | 100% |
| página de dados zerada | 99,8% |
| bloco de 64 KB zerado | 90,3% (97,8% com --ddl) |
| arquivo truncado | 68,3% |
Uma linha dessa tabela me incomoda: com 16 bits trocados aleatoriamente, alguns valores mudam sem aviso. As páginas do Firebird não têm checksum, então um byte alterado dentro de um registro é indistinguível de um dado legítimo. Prefiro deixar isso escrito do que esconder.
- E um teste que roda todos os comandos em cópias somente leitura e confere que o arquivo não mudou nem um byte.
Na prática
Baixe o executável em Releases (Windows, Linux, macOS) e trabalhe numa cópia do arquivo, com o servidor parado:
fbrescue scan copia.fdb # o que está danificado, por tabela
fbrescue export copia.fdb --out resgate # cada tabela em CSV + schema.sql
fbrescue export copia.fdb --out resgate --format sql # INSERTs para carregar com o isql
fbrescue export copia.fdb --out resgate --include-deleted # mais as linhas apagadas
Duas coisas bem brasileiras:
- Acentos quebrados: muito sistema antigo grava texto em
CHARACTER SET NONE, com os bytes na codificação do Windows. Acrescente--charset WIN1252(ouISO8859_1) e o CSV sai em UTF-8 correto. - Excel: o Excel brasileiro espera
;e vírgula decimal, então abrir o CSV com dois cliques junta tudo numa coluna. Use Dados → De Texto/CSV, origem UTF-8 e delimitador vírgula.
E o fbrescue não substitui as ferramentas do motor: sempre tente gfix e gbak primeiro. Se o backup sair, você recupera o banco inteiro, com índices, triggers e procedures. O fbrescue é para quando eles param.
Limitações e TODO
✅ Firebird 2.5, 3.0, 4.0 e 5.0, incluindo os tipos do Firebird 4 (INT128, DECFLOAT, fusos horários)
✅ BLOBs, bancos multiarquivo, registros maiores que uma página
✅ Linhas apagadas e versões antigas
✅ Páginas perdidas, fora do lugar, TIP e cabeçalho danificados
✅ Relatório sem dados (fbrescue report) para pedir ajuda sem expor o banco do cliente
⏳ Colunas do tipo array
⏳ Números de transação de 48 bits (bancos com mais de 4 bilhões de transações)
⏳ Carregar direto num banco novo, sem passar pelo isql
❌ InterBase e Firebird 1.x–2.1 — formato em disco mais antigo, não suportado
Índices, triggers, procedures e generators não são exportados: a ferramenta extrai dados, não conserta o arquivo.
Conclusões
O Firebird é um formato bem amigável para engenharia reversa: páginas simples, registros com cabeçalho fixo, RLE de uma linha, e o código-fonte aberto para tirar dúvidas. A parte difícil não é ler um banco saudável — é não confiar em nada quando o banco está quebrado, e provar com testes que o que você leu está certo.
Quando as ferramentas padrão não funcionam, o acesso direto ao arquivo pode salvar os dados. Mas o principal continua o mesmo: façam backup. E testem o restore.
GitHub: formeo/fbrescue — licença MIT. Se tiverem um banco quebrado de um jeito que a ferramenta não aguenta, o fbrescue report gera um relatório sem nenhum dado; abram uma issue, pode ser em português.
Recursos úteis:
- Guia de recuperação em português — passo a passo com
gfix,gbake o que fazer depois - Mensagens de erro do Firebird e o que significam
- Notas sobre o formato em disco — cada fato e o teste que o confirma (em inglês)