Кот — собака, программист хренов!

Как обычно это бывает — прошелся кот по клавиатуре…

Ночь.Тишина. Я сижу, переписываю на новый лад ассемблер для MSP430. И тут эта скотина прыгает ко мне на стол… Скучно ему стало, видите ли! … А то, что потом я тупил над исходником несколько часов — это вроде как ничего, да?

Баг оказался на столько злой, что я даже создал тестовый проект, где попытался воспроизвести этот баг. Тестовый проект показал — бага нет, все нормально компилируется и работает. О-о, как! Однако, в рабочем проекте мина-то заложена, и она не дает коду работать правильно. Я чуть с ума не сошел, пока нашел ее!

Вот так вглядит исправный код:

#include <stdio.h>

void broken_function(const char *str)
{
  const char *p = NULL;

  for (p = str; *p != '\0'; p++)
  {
    printf("(%c)", *p);
  }
}

int main(void)
{
  char *test_string = "This is a bug!";

  printf("\nGood code says:\n");
  broken_function(test_string);

  return (0);
}

Я убрал из проекта все детали и упростил код до предела. То есть ничего познавательного. В функцию broken_function передаем строку, где мы ее должны по-символьно обработать. Делов-то!

Кот наступил на клаву, текст программы изменился. Бывает! Я ссадил кота на пол, удалил символы, которые он впечатал мне и продолжил работать далее. (Мне даже в голову не пришло, что может где-то еще остаться один ма-аленький такой символьчик!)

Обычная процедура. Через некоторое время я снова собираю проект, выискиваю баги, правлю текст, снова компилирую… и натыкаюсь на очень странный баг.

Внешне все выглядит так как надо, но, собака, не работает! Проблема в функции broken_function, она не может толком обработать полученную строку.

void broken_function(const char *str)
{
  const char *p = NULL;

  for (p = str; *p != '\0'; p++)                       ;
  {
    printf("(%c)", *p);
  }
}

Если ваш зоркий глаз сразу увидел, в чем тут проблема, то это очень хорошо. Опережая события, скажу, что проблема в лишнем символе ‘;’ (точка с запятой), которую ввел кот. Кот видимо встал на пробел, а потом добавил этот символ. Таким образом этот чудесный патч-символ ускользнул за границу экрана от моего зрения.

Попробуйте и вы его сейчас не замечать. Что бы вы делали на моем месте? Баг устранять надо по любому. Но как? Что можно предположить?

Скорее всего, что указатель ‘p’ не правильно проинициализирован. Проверим. Для начала тупо воткнем в код printf-ы и посмотрим на адреса:

void broken_function(const char *str)
{
  const char *p = NULL;

  printf("str = %p\n", str);

  for (p = str; *p != '\0'; p++)                                         ;
  {
    printf("p   = %p\n", p);
    printf("(%c)", *p);
  }
}

Адреса разные… Интересно, как это может быть? Какая то чудесная неправильная инициализация указателя ‘p’ в операторе цикла что ли? Дурь, конечно, но проверить не помешает. Проверяем. Вынесем инициализацию ‘p’ за оператор цикла:

void broken_function(const char *str)
{
  const char *p = NULL;

  printf("1. str = %p\n", str);
  printf("1. p   = %p\n", p);
  p = str;
  printf("2. str = %p\n", str);
  printf("   p   = %p\n", p);

  for (; *p != '\0'; p++)                                         ;
  {
    printf("3. str = %p\n", str);
    printf("   p   = %p\n", p);
    printf("(%c)", *p);
  }
}

Инициялизация правильная, но баг остался. Мы видим, что указатель ‘p’ портится при входе в цикл. Похоже баг в компиляторе gcc… Не-е. ты что, родной! Ты 100500 раз подумай, прежде чем такое заявлять! Думаем. Может быть, все-таки вход в цикл как-то влияет на указатель? Но как такое возможно?!

Давайте вообще все поубираем из оператора цикла. Сделаем цикл вообще с другой переменной, например, — i. Нам достаточно одной итерации чтобы посмотреть на «порчение» указателя p.

void broken_function(const char *str)
{
  const char *p = NULL;
  int i;

  printf("1. str = %p\n", str);
  printf("1. p   = %p\n", p);
  p = str;
  printf("2. str = %p\n", str);
  printf("   p   = %p\n", p);

  for (i = 0 ; i < 1; i++)                                         ;
  {
    printf("3. str = %p\n", str);
    printf("   p   = %p\n", p);
    printf("(%c)", *p);
  }
}

Блин! Теперь указатель p не портится… Что за хрень такая? Ничего не понимаю…

Создаю с нуля тестовый проект (собственно, вот этот. Ну, который использован в в этой статье) и вижу, что чтобы я не вытворял в нем, все понятно и предсказуемо. Но в моем рабочем проекте — хрен поймешь, что происходит!

И вот тут я удаляю строку

   for (i = 0 ; i < 1; i++)                                         ;

в рабочем проекте и заново набираю ее руками. Баг ушел не попрощавшись. Вообще крыша едет! Чем коды в этой строке — в этой и в сломанной версиях — отличаются? Внимательно сравниваю их по-символьно — ни чем! И только случайное нажатие на клавишу «End» в бажном коде меня вывело в конец строки, где и был замаскирован символ ‘;’.

Для тех, кто не понял сути произошедшего.

Появление символа ‘;’ в операторе цикла отменило прежнее тело цикла, которое было заключено в фигурные скобки. Вместо него тело цикла оказалось пустым. Цикл честно крутился положенное количество раз, при этом честно изменял указатель p. Потом управление в программе передавалось на прежнее тело цикла. Только в этот раз оно воспринималось уже не как тело цикла, а как отдельно стоящий сложный оператор. Понятно, что этот оператор к циклу не имел ни какого отношения.

При входе в этот оператор указатель p имел уже измененное значение (которое он приобретал при работе в цикле), что и водило меня за нос длительное время.

Таким образом, случайное совпадение нескольких разнородных проблем породили сверх-проблему.

Ай-да, кот! А-да, хакер!

Кот Тимон

Кот по имени Тимон и UV-scipe ver.2

Чтоб он … , Ирод! Не умеешь писать компиляторы — не садись на клавиатуру!

Реклама

4 responses to “Кот — собака, программист хренов!

  1. Ctrl+z, только так надо удалять ошибочно введенное!
    А вообще, кот тут не причем, компилятор, собака, должен был выдать ошибку.

    • > Ctrl+z, только так надо удалять ошибочно введенное!
      Всяко бывает. Задним умом мы все крепкие.

      Там много чего было, чтобы удалять откатом. Я не рискнул, так-как опасался с разбегу удалить что-нибудь свое. Да и видно ж было хорошо, что кот там «напрограммировал». Тупо выделил «котовский вклад» и удалил.

      > А вообще, кот тут не причем, компилятор, собака, должен был выдать ошибку.
      Какую ошибку? Тут нет ошибок. Все нормально:

      for (…)
      ;
      {

      }

      • А вот для обратного отката есть волшебная Ctrl+y 🙂
        Какой смысл цикла без тела? Я спецификацию си не знаю, но по логике это бессмыслица.

        1. Я очень редко когда пользуюсь Ctrl-Y. Наверно даже так — практически не пользуюсь. Дело в том, что не во всех редакторах это реализовано, не говоря уже о том, что каждый редактор имеет свои особенности. А Вы какой имеете в виду?

          Иногда смысл пустого тела цикла есть, когда, например, вся работа делается в заголовке цикла. Иногда бывает нужен цикл, чтобы потерять время. Это особенно часто бывает в сфере программирования для микроконтроллеров. Например, тупо ждем поднятия флага, когда PLL «захватит» частоту, и ею можно будет «затактировать» ядро процессора. Да мало ли всяких применений пустого цикла! Жизнь очень разнообразна.

          Беда, однако, в том, что компиляторы за _смыслом_ не следят. Смысл — это чисто человеческое. Компилятор следит только за правильностью использования языка программирования, не более.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s