Чтобы остановиться на одной строке, пришлось пройти чужое рукопожатие
Чтобы остановиться на одной строке, пришлось пройти чужое рукопожатие

Когда в моей Среде разработки К дошла очередь до отладчика, выбирать пришлось из двух вариантов.

По HTTP, через сервер отладки, подключиться проще. Но по мобильной разработке я помнил, чем за это платят: задержками и обрывами связи - именно тогда, когда отладчик нужен.

По TCP штатная среда говорит с тонким клиентом напрямую, без сервера, и заметно быстрее. Но протокол закрытый, документации к нему нет.

Я выбрал TCP. Цель - в своём окне поставить точку, запустить клиент и остановиться на строке: со стеком, локальными переменными и шагами. Путь - подсмотреть, как подключается штатная среда, понять обмен и повторить его.

Два варианта подключения отладчика: через HTTP-сервер и напрямую по TCP
Два варианта подключения отладчика: через HTTP-сервер и напрямую по TCP

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

Смотрим, что передаётся по сети

Между средой и клиентом я поставил прокси и записал обмен в обе стороны, байт в байт. Потом добавил перехват через Frida, чтобы видеть данные до отправки в сокет.

Каналов оказалось два. По первому клиент сообщает отладчику события - “остановился”, “жив”. По второму отладчик подключается к самому клиенту и шлёт команды. Порт клиента он не берёт из сообщений, а перебирает диапазон 1560-1591 из настроек отладки. На соседних портах этого диапазона сидят процессы сервера - rphost на 1560, dbda на 1561-1563, - и они отвечают на те же запросы. Перепутать легко.

Каждое сообщение заканчивается меткой, соединение открывается приветствием, а раз в пять секунд клиент шлёт “жив”:

53 f5 c6 1a 7b   приветствие
66 53 b2 a6      конец сообщения
21 47 54 b3      пинг; молчание дольше 5 с - и платформа рвёт соединение

После входа поток открытый: тот же скобочный формат, в котором платформа хранит всё остальное, строки в UTF-16, GUID как есть. Шифруется только вход.

Кадры с метками начала и конца, пинг каждые пять секунд
Кадры с метками начала и конца, пинг каждые пять секунд

Вход

Сначала я решил, что шифра нет и вход можно заглушить. Ошибся. Повтор чужого входа из записи клиент отверг за 19 мс.

Схема оказалась взаимной. Подключающийся шлёт свой открытый ключ RSA-1024 открытым текстом. Принимающий шифрует под этот ключ свой профиль - RSA с дополнением PKCS#1 v1.5 - и в том же сообщении отдаёт ещё 192 байта, зашифрованные тройным DES. Ключ и вектор для них лежат в профиле. Без этих 192 байт клиент отвергал даже правильный блок - за 2 мс.

В библиотеках платформы рядом лежит OAEP, и он сбивал с толку: к отладке он отношения не имеет. Правильную схему подтвердил опыт - я зашёл к Конфигуратору со своим ключом и расшифровал его ответ своим приватным.

Поверх - NTLM через системный SSPI, в обе стороны. Токены едут в скобочном конверте.

“Рассогласование транспорта интерфейсов”

Вход проходил, а дальше клиент отвечал этой фразой. Причин нашлось четыре, и ни одну нельзя было угадать - только найти в записи.

Идентификатор создателя - константа. Я генерировал новый GUID на каждый запуск. Клиент ждёт всегда один и тот же. Со случайным он отвечает “успех”, но вместо номера соединения кладёт пустышку.

Номер соединения меняется. Мы шлём общеизвестный, клиент в ответе выдаёт свежий. Дальше во всех вызовах нужен свежий.

Контрольная сумма считается по полям GUID. Не по байтам в печатном порядке, а по числам, как GUID хранит Windows:

/// Data1+Data2+Data3+ΣData4 — слагаемое контрольной суммы для GUID (порядок Microsoft).
pub fn gsum(g: &[u8; 16]) -> u64 {
    u32::from_le_bytes(g[..4].try_into().unwrap()) as u64
        + u16::from_le_bytes([g[4], g[5]]) as u64
        + u16::from_le_bytes([g[6], g[7]]) as u64
        + g[8..].iter().map(|b| *b as u64).sum::<u64>()
}

Сумма ответа - gsum(соединение) + номер вызова + вид + gsum(интерфейс) + метод.

На привязку надо ответить “семь методов”. У интерфейса IDebugConnection их семь: transmitDebugCommand, debugAuthenticate, testConnection, testTargetConnection, setCreatorSideInfoTargetID, setNotifyAboutDestroyFlag, getNotifyAboutDestroyFlag. Не ответишь - ошибки нет, клиент просто ждёт и цель не регистрирует.

Стена с дверью и три ключа к ней
Стена с дверью и три ключа к ней

Почему нельзя просто повторить запись

Записанный поток воспроизводится байт в байт: тест прогоняет 1125 сообщений и 446 вызовов из записей без единого расхождения. Но отладчик повтором не построить. Команда с кодом 2, посланная не вовремя, роняет dbgtgt.dll - отладочную библиотеку клиента. А клиенту, который ничего не исполняет, негде остановиться.

Все действия идут через один метод transmitDebugCommand, отличаются кодом. Коды пришлось понять по смыслу, а не по байтам:

Код

Что делает

29

список точек останова

26

событие “остановился” (от клиента)

9

список остановленных целей; Конфигуратор шлёт его только после остановки

8, 12, 13

шаг в, через, из

10, 11, 7

продолжить (F5): 10 и 11 всем целям, 7 текущей

14

вычислить выражение

15

локальные переменные

34

остановка по ошибке; текст ошибки приходит событием 25

Смысл полей брал из записей, а где их не хватало - из XML-схем самой платформы (debugBreakpoints.xsd, debugRTEFilter.xsd, dbgtgtCommands.xsd лежат в её ресурсах) и из разбора большого switch по кодам в dbgtgt.dll.

Повтор записи байт в байт: клиент падает
Повтор записи байт в байт: клиент падает

Первая остановка

Точке нужна версия модуля - она лежит в самом файле конфигурации. Клиенту нужен код, который точно выполнится: модуль приложения с циклом.

Первое время клиент вставал на первой строке, где бы ни стояла точка. Настройки “остановиться на входе” нет. Виноват код 9: я слал его при подключении, а Конфигуратор шлёт только после первой остановки, всем целям сразу. Переставил - клиент встал на точке.

Код 9 уходил раньше времени - и клиент вставал на первой строке
Код 9 уходил раньше времени - и клиент вставал на первой строке
Остановка на строке 21: стек вызовов, локальные переменные и табло
Остановка на строке 21: стек вызовов, локальные переменные и табло

Цикл “клавиша - команда - событие - обновление окон” занимает около 13 мс, вычисление выражения - 1 мс туда и обратно.

Остальное

После первой остановки приём один: посмотреть в записи, как делает Конфигуратор, повторить.

  • Шаги - в, через, из.

  • Вычисление выражений. Свойства объекта приходят в том же ответе, если попросить, как Конфигуратор: другой флаг и буфер 307 200 байт вместо 10 240.

  • Локальные переменные на каждой остановке.

  • Точки с условием и отключённые, “Идти до курсора” - временная точка, снимается на следующей остановке.

  • Точки-сообщения. Особого события у них нет: клиент просто встаёт, а сообщение собирает отладчик - выражение, стек, число попаданий - и сам продолжает.

  • Остановка по ошибке с фильтром по тексту.

Вычисление выражения: Квадрат(Номер) = 4
Вычисление выражения: Квадрат(Номер) = 4

Диалоги точки и фильтра ошибок - формы самой платформы, со всеми полями.

Параметры точки останова: условие Номер = 2
Параметры точки останова: условие Номер = 2

Точка-сообщение на строке Счетчик = Счетчик + Значение с выражением Значение печатает квадраты, и клиент на ней не встаёт. Остановился он только на точке с условием Номер = 2:

Точка-сообщение: 1 и 4 в служебных сообщениях, остановка - на точке с условием
Точка-сообщение: 1 и 4 в служебных сообщениях, остановка - на точке с условием

Остановку по ошибке проверял на делении на ноль: с фильтром “Деление” отладчик встаёт, с “XYZ” - нет.

Остановка по ошибке: Деление на 0
Остановка по ошибке: Деление на 0
Что заработало после первой точки
Что заработало после первой точки

В Среде разработки К

Клавиши - как в штатной среде: F5 - начать и продолжить, F9 или щелчок по полю - точка, F10, F11, Shift+F11 - шаги, Shift+F9 - вычислить. Стрелка на текущей строке, стек, локальные, табло, панель целей. F5 сам запускает тонкий клиент с ключами /DEBUG -tcp /DEBUGGERURL. Серверную сторону тоже отлаживает - проверял точку в модуле сеанса.

Те же клавиши, что в штатной среде
Те же клавиши, что в штатной среде

Что не получилось

Команды завершения в протоколе нет. Код 36 по смыслу похож на неё, но вешает клиент. Поэтому “Завершить” закрывает процесс клиента, если его запустила Среда разработки К. Клиент, запущенный со стороны, так не закрыть.

В подписи цели Конфигуратор показывает число в скобках. У меня там единица, у него двойка, а что оно значит, я пока не понял.

Итог

Среда разработки К подключается к живому тонкому клиенту по штатному TCP-протоколу: точки, шаги, стек, локальные, выражения, остановка по ошибке. База настоящая, сервер отладки не нужен.

Теперь F9, F5 - и клиент стоит на моей строке.

Комментарии (0)