Один простой, но эффективный отладочный прием

22 ноября 2021

Бывает так, что ошибка в коде воспроизводится лишь при определенном стечение обстоятельств. Эти обстоятельства могут быть довольно сложными, особенно если приложение распределенное и каждый его экземпляр состоит из N процессов. Подцепиться отладчиком к правильному процессу в правильный момент времени практически невозможно. В подобных случаях я использую один незамысловатый прием, речь о котором и пойдет далее.

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

#include <stdio.h>
#include <unistd.h>

int main() {
  /* if( some condition violated ) { */
  int ret;
  printf("Attach with the debugger, pid = %d\n", getpid());
  do {
    ret = sleep(1);
  } while(ret == 0);
  /* } */
  printf("sleep() returned %d\n", ret);
  return 0;
}

В реальном коде вместо printf(...) будет какой-нибудь log(WARNING, ...). Логи приложения можно мониторирить командой watch:

watch "grep -r 'Attach with the debugger' /some/patch && grep -r ..."

Компилируем с отладочными символами, запускаем, гоняем тесты, на которых воспроизводится проблема, ждем. (Бывает и так, что нет нормальных шагов для воспроизведения проблемы, но подобный сценарий выходит за рамки статьи.) Когда проблема воспроизвелась, цепляемся к процессу отладчиком:

gdb -p 1234

Ставим бряк на while(ret == 0):

(gdb) b test.c:9
Breakpoint 1 at 0x10498: file test.c, line 9.

… и возобновляем работу приложения:

(gdb) c
Continuing

Когда встали на бряке, модифицируем переменную ret:

Breakpoint 1, main () at test.c:9
9   } while(ret == 0);
(gdb) p ret=1

Поздравляю, мы оказались в правильном процессе в правильный момент времени. Проблема решена. Далее приложение можно отлаживать, как обычно. Кстати, проверок в коде может быть больше одной. Источник ошибки можно быстро локализовать по принципу бинарного поиска.

Fun fact! В MacOS, в отличие от Linux, системный вызов sleep() сразу возвращает управление при подключении к процессу с отладчиком. На этой платформе можно написать sleep(1000) и не добавлять цикл.

Уверен, что опытные разработчики не узнали для себя ничего нового. Но я подумал, что кому-то данная информация все же может пригодится. В примерах были использованы C и GDB, но прием работает с любым стеком технологий, где есть отладчик.

Метки: , , .