К тридцатилетию первого C++ компилятора: ищем ошибки в Cfront

Бьёрн Страуструп
Авторы: Андрей Карпов, Бьёрн Страуструп.

Cfront это компилятор для С++, существующий примерно с 1983 года и разработанный Бьёрном Страуструпом. В то время он был известен как «C с классами». Cfront имел полноценный парсер, таблицы символов, строил дерево для каждого класса, функции и т.д. Cfront был основан на CPre. Cfront определял развитие языка приблизительно до 1990г. Многие неясные моменты, имеющие место в С++, связаны с ограничениями реализации Cfront. Причина в том, что Cfront осуществлял трансляцию с C++ в C. Одним словом, Cfront — это священный артефакт для любого C++ программиста. И я просто не мог пройти мимо, не проверив этот проект.

Введение

На идею проверить Cfront меня натолкнула заметка, приуроченная к 30-летию первой Release версии этого компилятора: "30 YEARS OF C++". Мы связались с Бьёрном Страуструпом, чтобы заполучить исходные коды Cfront. Я почему-то думал, что достать их будет целая история. Оказалось, всё просто. Эти исходники лежат в открытом доступе по адресу http://www.softwarepreservation.org/projects/c_plus_plus/ и доступны всем желающим.

Для проверки была выбрана первая коммерческая версия Cfront, выпущенную в октябре 1985 года. Ведь именно её исполнилось 30 лет.

Бьёрн предупредил нас, что с проверкой может оказаться не всё так просто:

Please remember this is *very* old software designed to run on a 1MB 1MHz machine and also used on original PCs (640KB). It was also done by one person (me) as only part of my full time job.

И действительно. Вот так просто взять и проверить проект оказалось невозможным. Например, в те времена для отделения имени класса от имени функции использовалось не четыре точки (::), а просто точка (.). Пример:

inline Pptr type.addrof() { return new ptr(PTR,this,0); }


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

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

  1. Проект имеет небольшой размер. Всего 100 KLOC в 143 файлах.
  2. Код качественный.
  3. Анализатор PVS-Studio все-таки далеко не всё смог проверить.


«Это все слова. Покажи мне код.» Линус Торвальдс


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

Первый фрагмент

typedef class classdef * Pclass;

#define PERM(p) p->permanent=1

Pexpr expr.typ(Ptable tbl)
{
  ....
  Pclass cl;
  ....
  cl = (Pclass) nn->tp;
  PERM(cl);
  if (cl == 0) error('i',"%k %s'sT missing",CLASS,s);
  ....
}


Предупреждение PVS-Studio: V595 The 'cl' pointer was utilized before it was verified against nullptr. Check lines: 927, 928. expr.c 927

Указатель 'cl' может быть равен NULL. Об этом свидетельствует проверка if (cl == 0). Беда в том, что ещё до этой проверки этот указатель разыменовывается. Это происходит в макросе PERM.

Т.е. если раскрыть макрос, то получаем:

cl = (Pclass) nn->tp;
cl->permanent=1
if (cl == 0) error('i',"%k %s'sT missing",CLASS,s);


Второй фрагмент

То же самое. Разменивали указатель, и только потом его проверили:

Pname name.normalize(Pbase b, Pblock bl, bit cast)
{
  ....
  Pname n;
  Pname nn;
  TOK stc = b->b_sto;
  bit tpdf = b->b_typedef;
  bit inli = b->b_inline;
  bit virt = b->b_virtual;
  Pfct f;
  Pname nx;
  if (b == 0) error('i',"%d->N.normalize(0)",this);
  ....
}


Предупреждение PVS-Studio: V595 The 'b' pointer was utilized before it was verified against nullptr. Check lines: 608, 615. norm.c 608

Третий фрагмент

int error(int t, loc* lc, char* s ...)
{
  ....
  if (in_error++)
    if (t!='t' || 4<in_error) {
      fprintf(stderr,"\nUPS!, error while handling error\n");
      ext(13);
    }
  else if (t == 't')
    t = 'i';
  ....
}


Предупреждение PVS-Studio: V563 It is possible that this 'else' branch must apply to the previous 'if' statement. error.c 164

Не знаю, есть здесь ошибка или нет, но код оформлен неправильно. 'else' относится к ближайшему 'if'. Поэтому код работает не так, как выглядит. Если отформатировать его правильно, то получится:

if (in_error++)
  if (t!='t' || 4<in_error) {
    fprintf(stderr,"\nUPS!, error while handling error\n");
    ext(13);
  } else if (t == 't')
    t = 'i';


Четвертый фрагмент

extern
genericerror(int n, char* s)
{
  fprintf(stderr,"%s\n",
          s?s:"error in generic library function",n);
  abort(111);
  return 0;
};


Предупреждение PVS-Studio: V576 Incorrect format. A different number of actual arguments is expected while calling 'fprintf' function. Expected: 3. Present: 4. generic.c 8

Обратите внимание на format specifiers: "%s". Будет распечатана строка. А вот переменная 'n' осталась не при деле.

Прочее

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

extern int Nspy, Nn, Nbt, Nt, Ne, Ns, Nstr, Nc, Nl;


Предупреждение PVS-Studio: V707 Giving short names to global variables is considered to be bad practice. It is suggested to rename 'Nn' variable. cfront.h 50

Или, например, для распечатки значений указателей функцией fprintf() использует спецификатор "%i". В современной версии языка для этого служит "%p". Но как я понимаю, 30 лет назад никакого "%p" ещё не было, и код совершенно корректен.

Интересные наблюдения


Указатель this

Обратил внимание, что раньше с 'this' работали на порядок более смело и грубо. Пара примеров на эту тему:

expr.expr(TOK ba, Pexpr a, Pexpr b)
{
  register Pexpr p;

  if (this) goto ret;
  ....
  this = p;
  ....
}

inline toknode.~toknode()
{
  next = free_toks;
  free_toks = this;
  this = 0;
}


Как видите, в те времена не считалось чем-то запретным, взять и поменять значение 'this'. Сейчас запрещается не только менять указатель, но и даже потеряли смысл сравнения this с nullptr.

This is the place for paranoia

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

/* this is the place for paranoia */
if (this == 0) error('i',"0->Cdef.dcl(%d)",tbl);
if (base != CLASS) error('i',"Cdef.dcl(%d)",base);
if (cname == 0) error('i',"unNdC");
if (cname->tp != this) error('i',"badCdef");
if (tbl == 0) error('i',"Cdef.dcl(%n,0)",cname);
if (tbl->base != TABLE) error('i',"Cdef.dcl(%n,tbl=%d)",
                              cname,tbl->base);


Комментарий Бьёрна Страуструпа


  • Cfront был создан на основе Cpre, но при этом полностью переписан. От Cpre в коде Cfront не осталось ни строчки.
  • В ошибке use-before-test-of-0 (использование до проверки на 0), конечно, нет ничего хорошего, однако, что любопытно, конфигурация, на которой я преимущественно работал (машина DEC и ОС Research Unix), реализовали защиту от записи нулевой страницы (и здесь тоже), так что этот баг не смог бы сработать, не будучи обнаруженным.
  • С багом (если это действительно баг) с if-then-else вышло необычно. Я посмотрел исходный код: это не просто опечатка, а именно ошибка. Однако, что интересно, она никак не влияет на результат: будет лишь небольшая разница в сообщении об ошибке, которое выведется перед завершением. Неудивительно, что я ее не заметил.
  • Да, мне следовало использовать более удобочитаемые имена. Просто изначально я не рассчитывал на то, что программу в течение многих лет будут поддерживать другие люди (и еще я плохо печатаю).
  • Да, спецификаторов %p в те времена еще не было.
  • Да, правила для «this» поменялись.
  • В основном цикле компилятора использовался «параноидальный тест». Я исходил из соображений, что, случись что-то с ПО или железом, один из этих тестов будет провален. Как минимум однажды он выявил последствия одного бага в генераторе кода, который использовался для сборки Cfront. Я считаю, что все серьезные приложения должны использовать такой «параноидальный тест» для отлова «невозможных» ошибок.


Выводы

Значение Cfront сложно переоценить. Он оказал влияние на развитие целой отрасли программирования и подарил миру вечно живой и развивающийся язык C++. Выражаю Бьёрну благодарность за всю проделанную им работу в создании С++. Спасибо. Мне в свою очередь было приятно хотя бы «постоять рядом» с Cfront.

Спасибо всем читателям, и хочу пожелать поменьше багов.

© Habrahabr.ru