Модераторы: Daevaorn
  

Поиск:

Ответ в темуСоздание новой темы Создание опроса
> К тридцатилетию первого C++ компилятора: ищем ошибки в Cfront 
:(
    Опции темы
Thunderbolt
Дата 5.11.2015, 14:06 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


DevRel
*


Профиль
Группа: Участник
Сообщений: 122
Регистрация: 7.11.2007
Где: Тула

Репутация: 10
Всего: 16



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.

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

Код
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.

Спасибо всем читателям, и хочу пожелать поменьше багов.
--------------------
Карпов Андрей, DevRel в PVS-Studio.
PM MAIL WWW   Вверх
  
Ответ в темуСоздание новой темы Создание опроса
Правила форума "С++:Общие вопросы"
Earnest Daevaorn

Добро пожаловать!

  • Черновик стандарта C++ (за октябрь 2005) можно скачать с этого сайта. Прямая ссылка на файл черновика(4.4мб).
  • Черновик стандарта C (за сентябрь 2005) можно скачать с этого сайта. Прямая ссылка на файл черновика (3.4мб).
  • Прежде чем задать вопрос, прочтите это и/или это!
  • Здесь хранится весь мировой запас ссылок на документы, связанные с C++ :)
  • Не брезгуйте пользоваться тегами [code=cpp][/code].
  • Пожалуйста, не просите написать за вас программы в этом разделе - для этого существует "Центр Помощи".
  • C++ FAQ

Если Вам понравилась атмосфера форума, заходите к нам чаще! С уважением, Earnest Daevaorn

 
0 Пользователей читают эту тему (0 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | C/C++: Общие вопросы | Следующая тема »


 




[ Время генерации скрипта: 0.0754 ]   [ Использовано запросов: 23 ]   [ GZIP включён ]


Реклама на сайте     Информационное спонсорство

 
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности     Powered by Invision Power Board(R) 1.3 © 2003  IPS, Inc.