15.10.2026 Технический разбор RU

ionozond: консоль ЛЧМ-ионозонда, восстановленная по бинарнику

Исходники консоли наклонного ЛЧМ-зондирования были утеряны, остался только собранный бинарник. Как по нему восстановили программу и доказали совпадение с оригиналом, какие причуды пришлось сохранить, зачем рядом с 80-мегабайтной записью лежит файл в 37 КБ и что консоль умеет теперь — от ионограммы, которая строится во время приёма, до проверки расписания станций.

Никита Конкин

Проект Репозиторий Интерактивные модели
Языковые версии: RU EN

Наклонное ЛЧМ-зондирование измеряет ионосферу на трассе между передатчиком и приёмником. Передатчик проходит КВ-диапазон линейно меняющейся частотой, приёмник перемножает принятый сигнал с таким же опорным и записывает разностный сигнал. Из записи получается ионограмма — сила сигнала в зависимости от частоты и задержки эха, — а по ней видно, какой диапазон частот трасса пропускает. Здесь основная трасса — Кипр — Йошкар-Ола.

передатчик  ──▶  приёмник  ──▶  запись .lfs  ──▶  продукты .lfp  ──▶  консоль
                  (приём)         80 МБ             ~40 КБ

Консоль оператора — программа на Qt5 и Qwt, которая показывает ионограммы, ведёт расписание станций и запускает приём. Её исходники были утеряны: осталась только собранная программа dsChirp. ionozond начался 25 августа с её восстановления и за полтора месяца вырос в комплект из приёма, консоли, форматов, инструментов и документации — 105 коммитов.

Восстановление по бинарнику

Восстановить программу помогло то, что бинарник не был очищен от символов: в нём сохранились имена классов, сигнатуры методов и метаданные Qt. Программа к тому же собрана не как позиционно-независимая, поэтому адреса функций известны заранее.

Это позволило сделать оригинал эталоном. Он запускается в том же контейнере, что и восстановленная программа, и его функции вызываются прямо внутри него под gdb. Отладочной информации нет, поэтому вызов приходится приводить к типу вручную: set $f = (double (*)(double*, int*))getMedian. Отдельный сценарий запускает весь интерфейс оригинала без экрана с настройками из тестов.

Проверка шла на трёх уровнях. Расчёт спектра переписан и сверен с независимой реализацией на NumPy на настоящей записи — расхождение 1,7·10⁻¹³ в относительных единицах, то есть только порядок суммирования в БПФ. Создаваемый программой файл конфигурации совпадает с оригинальным побайтно. Отрисовка ионограмм сравнивалась на реальных записях.

Причуды, сохранённые нарочно

Точная копия должна повторять и то, что естественная реализация сделала бы иначе. Каждое такое место записано в docs/reverse-engineering.md и закреплено тестом.

  • Медиана, которая не медиана при нечётном числе элементов. Функция getMedian всегда считает полусумму максимума нижней половины и элемента n/2. Вызов оригинала под gdb для {5, 1, 9, 3, 7, 2, 8} вернул 4, хотя медиана — 5. Длины спектров — степени двойки, поэтому на практике это не проявляется, но повторено в точности.
  • Периодическое окно Ханна: деление на n, а не на n − 1, как в scipy.signal.get_window('hann', n, fftbins=True), а не numpy.hanning(). Шаг окна сужается до float так же, как в оригинале, поэтому окно совпадает бит в бит.

Одна ошибка оригинала исправлена, а не повторена. Диалог параметров записывал ключ colormap_gradient, а главное окно читало colormap_gradinet — с опечаткой. Выбор цветовой шкалы сохранялся, но на графики не влиял. Исправление помечено в коде DSCHIRP_FIX_GRADIENT_TYPO, а исходное поведение описано в комментарии.

Позже нашлась ещё одна унаследованная неточность: в формуле длины трассы у оригинала во втором слагаемом нет cos(Δλ). Для Кипра это 2542 км вместо 2591 по большому кругу, а для Магадана — 4414 км вместо 5476. Подписи оси задержки на панели ПЗМ из-за этого расходились с данными на несколько миллисекунд. Теперь консоль считает правильный большой круг, а формула оригинала сохранена отдельной функцией, и тест показывает, что она по-прежнему воспроизводит окно оригинала.

Два формата

.lfs — сырая запись: заголовок в 512 байт и поток комплексных отсчётов I/Q во float32. Один сеанс в 250 секунд занимает 80 МБ, а при периоде повторения 300 секунд одна станция даёт около 23 ГБ в сутки. Структура заголовка сверена поле за полем с двумя настоящими записями — 31 проверка смещений. Файлы более новой версии 1.1 программа отклоняет — так же, как оригинал, и это повторено намеренно.

.lfp — файл продуктов рядом с записью: ионограмма, спектр сигнала и шума, профиль мощности по задержке (ПЗМ) и границы диапазона. Консоль читает его в первую очередь, поэтому тяжёлая обработка выполняется один раз:

cyprus1_20191023_071510.lfs    76,3 МБ
cyprus1_20191023_071510.lfp    37,0 КБ      в 2112 раз меньше

Рядом с каждой записью пишется и архив HDF5 в формате, совместимом с chirpsounder2; теперь он основной, а сырая запись необязательна. Для .lfs задаётся срок хранения, и записи удаляются только после того, как проверены их продукты. Синхронизация с сетевым хранилищем тоже удаляет файлы только после проверки копии.

Приём

Путь сигнала от радио до файла переписан на Python 3. Большая часть коммитов конца августа — о том, что на реальном оборудовании всё оказалось сложнее. Часы радио ставятся и без GPS, а приёмник проверяет, каким часам верить, вместо того чтобы считать GPS правым по умолчанию. Потери отсчётов, как выяснилось, вызывало переполнение кольца приёма сетевой карты — его размер теперь увеличивается и сохраняется после перезагрузки. Пропавшие при переполнении отсчёты заполняются нулями, чтобы не сбивалась шкала времени, а запись с переполнениями считается ухудшенной, а не испорченной. Приёмник работает как служба systemd.

Консоль сегодня

Окно консоли: две ионограммы с осями частоты 8–32 МГц и задержки 8–13,5 мс; на дневной видны два следа около 9 и 9,4 мс до 32 МГц, на ночной — короткий след до 13 МГц. Справа панель «Сигнал/шум» по частоте и времени суток, где днём полоса поднимается до 30 МГц, и панель ПЗМ по задержке и времени. Внизу журнал загрузки 278 записей, справа часы UTC, кнопки управления и диаграмма диска
Консоль на сутках записей 6 февраля 2026 года, трасса Кипр — Йошкар-Ола. Слева — контрольная ионограмма за 10:00 UTC и текущая за 23:55, справа — отношение сигнал/шум и ПЗМ за сутки.

Консоль показывает каждую станцию двумя ионограммами — контрольной и текущей — и двумя суточными панелями. Время записи подписано в UTC и по Москве: записи названы и расписаны по UTC, а на станции живут по московскому времени.

Как в оригинале, текущая ионограмма теперь строится во время приёма, слева направо. Консоль каждые две секунды находит растущую запись и считает только новые столбцы — той же арифметикой, что и построитель файлов продуктов. На записи 2019 года результат совпадает с построителем до 6·10⁻⁶ дБ, поэтому, когда в конце сеанса готовый .lfp заменяет живую картинку, она не скачет. Тест подаёт запись неровными кусками и требует, чтобы итог совпал с картинкой, построенной за один проход, бит в бит.

Ионограмма 06 Feb 10:00:10 UTC (13:00:10 МСК): по частоте от 8 до 32 МГц и задержке от 8 до 13,5 мс два протяжённых следа около 9,0 и 9,4 мс, выше 15 МГц; ниже 15 МГц — пятна помех. Сверху кнопки навигации, флажок «Следить за новыми сеансами» и «Сохранить PNG…»
Ионограмма в отдельном окне: 6 февраля, 10:00 UTC. Выделение мышью увеличивает участок, стрелки листают соседние сеансы, картинку можно сохранить в PNG.

Длина БПФ задаётся для каждой станции. Ячейка ионограммы — k·N/f_ПЧ по частоте на f_ПЧ/(k·N) по задержке, поэтому одна длина не подходит станциям с разной скоростью перестройки. 16 384 точки при 100 кГц/с дают ячейку 41 кГц × 7,3 км, а при 500 кГц/с — 205 кГц × 1,5 км: эхо, сместившееся на 5 мкс, уходит из своей ячейки. При 4096 точках ячейка 51 кГц × 5,9 км — близко к форме оригинала.

Диалог «Расписание» для принимающей станции yoshkar-ola: таблица параметров двух станций — период повторения, chirptime, скорость перестройки, продолжительность сеанса, частота гетеродина, координаты, длина БПФ, ячейка ионограммы 41 × 7,3 и 20 × 14,6 и детализация. Внизу два предупреждения: сеанс yoshkar-ola не помещается в период 60 с, а cyprus1 теряет 288 из 288 сеансов в сутки
Расписание станций с проверкой: вторая станция, включённая в то же время, отнимает у первой все сеансы, и проверка говорит об этом сразу.

Одно радио следует за одной перестройкой, поэтому станция, чей сеанс приходится на запись другой, раньше просто не записывалась — и ничто об этом не сообщало. Теперь проверка проигрывает сутки по тем же правилам выбора, что и сам приёмник, называет потерянные сеансы и станцию, которая занимает радио, и предлагает исправление, проверенное повторным моделированием. Она же ловит то, чего не примет файл конфигурации: десятичные запятые, ведущие нули, кавычки в именах, неположительный период.

Документ о физике ЛЧМ-ионограмм

Рядом с кодом лежит документ «Oblique Chirp Ionograms» — о том, как образуется ионограмма, что значат её следы и где консоль может обмануть. В документе 16 частей; новая, одиннадцатая, посвящена дисперсии групповой задержки (GDD): наклону следа dτ/df, от которого зависит, какую полосу трасса пропускает без искажений.

Числа взяты с этой трассы: 19 033 оценки наклона по одному мегагерцу у нижнего луча моды 1F, 583 сеанса 4–9 февраля 2026 года при максимальной наблюдаемой частоте (МНЧ) от 18 до 32,4 МГц. Медиана следует закону 7,5·(1/√(1 − f/МНЧ) − 1) мкс/МГц. На экране консоли эта дисперсия не видна: даже 35 мкс/МГц поднимают след всего на 1,4 мкс поперёк столбца в 41 кГц — около 6 % ячейки. Интерактивные модели к документу вынесены в отдельный практикум — о нём следующий разбор.

Документ правится так же, как код. Последний коммит исправляет места, которым противоречат его собственные таблицы и формулы: например, удвоение длины БПФ удваивает шаг по частоте, а не уменьшает его вдвое. Ещё одна запись объясняет, почему два передатчика ИСЗФ — в Магадане и Норильске — не слышны в Йошкар-Оле: их антенны направлены на Иркутск.

Запуск в браузере

Для запуска нужен только Docker. Консоль работает на виртуальном экране внутри контейнера, а управляется через обычный браузер по noVNC. Каталог с записями подключается только для чтения, поэтому программа не может изменить архив. Скриншоты в этом разборе сняты именно так — на сутках записей 6 февраля.

Выводы

  1. Бинарник, не очищенный от символов, — почти исходник: имена классов и адреса функций превращают оригинал в эталон, который можно вызывать по частям.
  2. Точность копии доказывается не сходством картинок, а сверкой: побайтной для конфигурации, численной для спектров, тестами для каждой причуды.
  3. Причуды оригинала стоит повторять и документировать, а исправлять — явно, с пометкой в коде.
  4. Файл продуктов в 2000 раз меньше записи переносит тяжёлую обработку туда, где она делается один раз.
  5. Проверка расписания превращает молчаливую потерю сеансов в предупреждение с готовым исправлением.

Доступность

Исходный код открыт. Сторонние исходники, по которым сверялся формат записей, в репозиторий не входят: они распространяются под чужой лицензией. Оригинальный бинарник в репозитории тоже не лежит.

Теги

ионосфера ЛЧМ-зондирование c++ qt обратная-разработка цифровая-обработка-сигналов

Другие обновления по проекту