Когда в моей Среде разработки К дошла очередь до отладчика, выбирать пришлось из двух вариантов.
По HTTP, через сервер отладки, подключиться проще. Но по мобильной разработке я помнил, чем за это платят: задержками и обрывами связи — именно тогда, когда отладчик нужен.
По TCP штатная среда говорит с тонким клиентом напрямую, без сервера, и заметно быстрее. Но протокол закрытый, документации к нему нет.
Я выбрал 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: я слал его при подключении, а Конфигуратор шлёт только после первой остановки, всем целям сразу. Переставил — клиент встал на точке.
Цикл “клавиша — команда — событие — обновление окон” занимает около 13 мс, вычисление выражения — 1 мс туда и обратно.
Остальное
После первой остановки приём один: посмотреть в записи, как делает Конфигуратор, повторить.
-
Шаги — в, через, из.
-
Вычисление выражений. Свойства объекта приходят в том же ответе, если попросить, как Конфигуратор: другой флаг и буфер 307 200 байт вместо 10 240.
-
Локальные переменные на каждой остановке.
-
Точки с условием и отключённые, “Идти до курсора” — временная точка, снимается на следующей остановке.
-
Точки-сообщения. Особого события у них нет: клиент просто встаёт, а сообщение собирает отладчик — выражение, стек, число попаданий — и сам продолжает.
-
Остановка по ошибке с фильтром по тексту.
Диалоги точки и фильтра ошибок — формы самой платформы, со всеми полями.
Точка-сообщение на строке Счетчик = Счетчик + Значение с выражением Значение печатает квадраты, и клиент на ней не встаёт. Остановился он только на точке с условием Номер = 2:
Остановку по ошибке проверял на делении на ноль: с фильтром “Деление” отладчик встаёт, с “XYZ” — нет.
В Среде разработки К
Клавиши — как в штатной среде: F5 — начать и продолжить, F9 или щелчок по полю — точка, F10, F11, Shift+F11 — шаги, Shift+F9 — вычислить. Стрелка на текущей строке, стек, локальные, табло, панель целей. F5 сам запускает тонкий клиент с ключами /DEBUG -tcp /DEBUGGERURL. Серверную сторону тоже отлаживает — проверял точку в модуле сеанса.
Что не получилось
Команды завершения в протоколе нет. Код 36 по смыслу похож на неё, но вешает клиент. Поэтому “Завершить” закрывает процесс клиента, если его запустила Среда разработки К. Клиент, запущенный со стороны, так не закрыть.
В подписи цели Конфигуратор показывает число в скобках. У меня там единица, у него двойка, а что оно значит, я пока не понял.
Итог
Среда разработки К подключается к живому тонкому клиенту по штатному TCP-протоколу: точки, шаги, стек, локальные, выражения, остановка по ошибке. База настоящая, сервер отладки не нужен.
Теперь F9, F5 — и клиент стоит на моей строке.
ссылка на оригинал статьи https://habr.com/ru/articles/1100832/