Цікава археологія. Або PVS-Studio перевіряє Microsoft Word 1.1a

  Ð§ÐµÑ€ÐµÐ¿ единорога
Нещодавно компанія Microsoft зробила подарунок всім програмістам, які хочуть покопатися в чомусь цікавому. Microsoft відкрили вихідний код MS-DOS v 1.1, v 2.0 і Word for Windows 1.1a. Операційна система MS-DOS написана на асемблері, і до неї аналізатор не застосовується. А ось Word написаний на мові Сі. Вихідним кодами Word 1.1a майже 25 років, однак нам якось вдалося їх перевірити. Звичайно ніякої практичної цінності в цій перевірці немає. Just for fun.
 
 Де поживитися исходниками
Можливо, багатьом буде цікава не скільки ця стаття, а сам факт, що можна скачати вихідні коди MS-DOS v 1.1, v 2.0 і Word for Windows 1.1a. Тим, кому цікаво самим покопатися у вихідних кодах, відправляю до першоджерела.
 
Прес-реліз: Computer History Museum Makes Historic MS-DOS and Word for Windows Source Code Available to the Public .
  
 Перевірка Word 1.1a
 Ð Ð¸ÑÑƒÐ½Ð¾Ðº 1. Word for Windows 1.1a (нажмите на картинку для увеличения).
 
 Ð Ð¸ÑÑƒÐ½Ð¾Ðº 1. Word for Windows 1.1a (нажмите на картинку для увеличения).
 
Малюнок 1. Word for Windows 1.1a (натисніть на картинку для збільшення).
 
Word for Windows 1.1a був випущений в 1990 році. 25 березня 2014 код цього продукту став доступний публіці. Word був і залишається флагманським продуктом компанії Microsoft. Мені і багатьом іншим цікаво подивитися на нутрощі програмного продукту, який так сильно посприяв комерційним успіхам компанії Microsoft.
 
Я вирішив перевірити код Word 1.1a за допомогою нашого інструменту PVS-Studio . Це статичний аналізатор Сі / Сі + + коду. Природно, це не так просто. Аналізатор розрахований на роботу з проектами, що розробляються як мінімум в Visual Studio 2005. А зараз переді мною исходники на мові Сі, яким більше 20 років. Можна сказати, що це доісторичні часи. Принаймні, тоді не існувало стандарту мови Сі. Кожен компілятор був сам по собі. На щастя, у вихідних кодах Word 1.1a не виявилося якихось незвичайних моментів і використання великої кількості нестандартних розширень компілятора.
 
Для аналізу необхідні препроцессірованние файли (*. I). Маючи препроцессірованние файли, можна скористатися інструментом PVS-Studio Standalone . З його допомогою можна виконати аналіз і вивчити діагностичні повідомлення. Звичайно, аналізатор не розрахований на аналіз 16-бітних програм. Але цих результатів аналізу буде цілком достатньо для задоволення цікавості. Уважно аналізувати проект 24 річної давності, немає ніякого практичного сенсу.
 
Отже, основна заковика полягала в тому, як отримати препроцессірованние файли. Я попросив свого колегу поворожити у цьому напрямку. Він підійшов до вирішення вельми творчо. Він виконав препроцессірованіе за допомогою GCC 4.8.1. Навряд чи хтось ще так знущався над исходниками Word 1.1. Використовувати GCC — адже треба було таке придумати. Фантазер.
 
Найцікавіше, що вийшло цілком вдало. Була написана маленька утиліта, яка запускала препроцессірованіе за допомогою GCC 4.8.1 на кожен файл з директорії, в якій він лежав. У міру виведення помилок, пов'язаних з включенням заголовних файлів, в параметри запуску додавалися ключі-I з шляхом до потрібних файлів. Парочка Незнайдений заголовних фалів були створені порожніми. Всі інші проблеми розкриття # include були пов'язані з включенням ресурсів, тому були закоментовані. При препроцессірованіі визначався макрос WIN, т.к. в коді є гілка для WIN і MAC.
 
Далі в справу вступив PVS-Studio Standalone і ваш покірний слуга. Я виписав підозрілі фрагменти коду і готовий вам їх показати. Але спочатку ще дещо що про проект.
 
 Різне про код Word 1.1a
  
 Найскладніші функції
Найбільша цикломатическая складність у таких функцій:
     
  1. CursUpDown — 219;
  2.  
  3. FIdle — 192;
  4.  
  5. CmdDrCurs1 — 142.
  6.  
 # ifdef WIN23
Переглядаючи вихідні коди і зустрівши "# ifdef WIN23", я заусміхався. І навіть виписав це місце. Я подумав, що це помилка і повинно бути написано # ifdef WIN32.
 
Коли я побачив WIN23 вдруге я засумнівався. А потім раптом усвідомив, що я дивлюся исходники 24 річної давності. WIN23 означає версію Windows 2.3.
 
 Суворі часи
У коді мені попалася ось така цікава рядок.
 
Assert((1 > 0) == 1);

Здається неймовірним, що ця умова може не виконатися. Однак, якщо є така перевірка, то був і привід їй написати. У ті часи не було стандарту на мову. Як я розумію, було хорошим тоном перевіряти, наскільки робота компілятора відповідає очікуванням програмістів.
 
Звичайно, якщо вважати K & R стандартом, то по ідеї умова ((1> 0) == 1) завжди виконується. Але K & R це був лише стандарт де-факто і не більше. Це перевірка на адекватність компілятора.
 
 Результати перевірки
Тепер поговоримо про підозрілі місцях, знайдених мною в коді. Думаю, заради цього ви і читаєте цю статтю. Приступимо.
 
 Нескінченний цикл
 
void GetNameElk(elk, stOut)
ELK elk;
unsigned char *stOut;
{
  unsigned char *stElk = &rgchElkNames[mpelkichName[elk]];
  unsigned cch = stElk[0] + 1;

  while (--cch >= 0)
    *stOut++ = *stElk++;
}

Попередження PVS-Studio: V547 Expression '- cch> = 0' is always true. Unsigned type value is always> = 0. mergeelx.c 1188
 
Цикл «while (- cch> = 0)» ніколи не зупиниться. Мінлива 'cch' має тип unsigned. Значить, скільки не зменшується цю змінну, вона завжди залишиться> = 0.
 
 Вихід за кордон масиву через друкарські помилки
 
uns rgwSpare0 [5];

DumpHeader()
{
  ....
  printUns ("rgwSpare0[0]   = ", Fib.rgwSpare0[5], 0, 0, fTrue);
  printUns ("rgwSpare0[1]   = ", Fib.rgwSpare0[1], 1, 1, fTrue);
  printUns ("rgwSpare0[2]   = ", Fib.rgwSpare0[2], 0, 0, fTrue);
  printUns ("rgwSpare0[3]   = ", Fib.rgwSpare0[3], 1, 1, fTrue);
  printUns ("rgwSpare0[4]   = ", Fib.rgwSpare0[4], 2, 2, fTrue);
  ....
}

Попередження PVS-Studio: V557 Array overrun is possible. The '5 'index is pointing beyond array bound. dnatfile.c 444
 
Якось так вийшло, що в першому рядку написано: Fib.rgwSpare0 [5]. Це неправильно. У масиві всього 5 елементів, а значить максимальний індекс повинен бути рівний 4. Значення '5 'це результат друкарські помилки. По всій видимості в першому рядку мав використовуватися нульовий індекс:
 
printUns ("rgwSpare0[0]   = ", Fib.rgwSpare0[0], 0, 0, fTrue);

 неініціалізованих мінлива
 
FPrintSummaryInfo(doc, cpFirst, cpLim)
int doc;
CP cpFirst, cpLim;
{
  int fRet = fFalse;
  int pgnFirst = vpgnFirst;
  int pgnLast = vpgnLast;
  int sectFirst = vsectFirst;
  int sectLast = sectLast;
  ....
}

Попередження PVS-Studio: V573 Uninitialized variable 'sectLast' was used. The variable was used to initialize itself. print2.c 599
 
Мінлива 'sectLast' присвоюється сама собі:
 
int sectLast = sectLast;

Здається, для ініціалізації повинна була бути використана змінна 'vsectLast':
 
int sectLast = vsectLast;

Знайшлося ще одна ідентична помилка. Мабуть наслідок Copy-Paste:
 
V573 Uninitialized variable 'sectLast' was used. The variable was used to initialize itself. print2.c 719
 
 Невизначене поведінку
 
CmdBitmap()
{
  static int  iBitmap = 0;
  ....
  iBitmap = ++iBitmap % MAXBITMAP;
}

Попередження PVS-Studio: V567 Undefined behavior. The 'iBitmap' variable is modified while being used twice between sequence points. ddedit.c 107
 
Не знаю, як до такого коду ставилися 20 років тому. Але зараз це вважається хуліганством, оскільки призводить до невизначеного поведінки.
 
Аналогічно:
     
  • V567 Undefined behavior. The 'iIcon' variable is modified while being used twice between sequence points. ddedit.c 132
  •  
  • V567 Undefined behavior. The 'iCursor' variable is modified while being used twice between sequence points. ddedit.c 150
  •  
 Невдалий виклик функції printf ()
 
ReadAndDumpLargeSttb(cb,err)
  int     cb;
  int     err;
{
  ....
  printf("\n - %d strings were read, "
         "%d were expected (decimal numbers) -\n");
  ....
}

Попередження PVS-Studio: V576 Incorrect format. A different number of actual arguments is expected while calling 'printf' function. Expected: 3. Present: 1. dini.c 498
 
Функція printf (), це функція з змінною кількістю аргументів . Їй можна передати аргументи, а можна і не передати. Ось тут про аргументи забули, в результаті чого буде роздрукований сміття.
 
 неініціалізованих покажчики
В одній з допоміжних утиліт, яка мається на складі початкових кодів Word, можна зустріти щось взагалі незрозуміле.
 
main(argc, argv)
int argc;
char * argv [];
{
  FILE * pfl;
  ....
  for (argi = 1; argi < argc; ++argi)
  {
    if (FWild(argv[argi]))
    {
      FEnumWild(argv[argi], FEWild, 0);
    }
    else
    {
      FEWild(argv[argi], 0);
    }

    fclose(pfl);
  }
  ....
}

Попередження PVS-Studio: V614 Uninitialized pointer 'pfl' used. Consider checking the first actual argument of the 'fclose' function. eldes.c 87
 
Мінлива 'pfl "не ініціалізується до циклу і в самому циклі. Зате багато разів викликається функція fclose (pfl). Втім, все це цілком могло успішно працювати. Функція поверне статус помилки, і програма продовжить свою роботу.
 
А ось ще одна небезпечна функція. Швидше за все, її виклик призведе до аварійного завершення програми.
 
FPathSpawn( rgsz )
char *rgsz[];
{ /* puts the correct path at the beginning of rgsz[0]
     and calls FSpawnRgsz */
  char *rgsz0;

  strcpy(rgsz0, szToolsDir);
  strcat(rgsz0, "\\");
  strcat(rgsz0, rgsz[0]);
  return FSpawnRgsz(rgsz0, rgsz);
}

Попередження PVS-Studio: V614 Uninitialized pointer 'rgsz0' used. Consider checking the first actual argument of the 'strcpy' function. makeopus.c 961
 
Покажчик 'rgsz0' нічим не ініціалізується. Це не заважає почати копіювати в нього рядок.
 
 Друкарська помилка в умові
 
....
#define wkHdr    0x4000
#define wkFtn    0x2000
#define wkAtn    0x0008
....
#define wkSDoc    (wkAtn+wkFtn+wkHdr)

CMD CmdGoto (pcmb)
CMB * pcmb;
{
  ....
  int wk = PwwdWw(wwCur)->wk;
    if (wk | wkSDoc)
      NewCurWw((*hmwdCur)->wwUpper, fTrue);
  ....
}

Попередження PVS-Studio: V617 Consider inspecting the condition. The '(0x0008 + 0x2000 + 0x4000)' argument of the '|' bitwise operation contains a non-zero value. dlgmisc.c 409
 
Умова (wk | wkSDoc) завжди істинно. Насправді, тут, швидше за все, хотіли написати:
 
if (wk & wkSDoc)

Загалом, переплутали оператор | і &.
 
 І під кінець довгий, але простий приклад
 
int TmcCharacterLooks(pcmb)
CMB * pcmb;
{
  ....
  if (qps < 0)
  {
    pcab->wCharQpsSpacing = -qps;
    pcab->iCharIS = 2;
  }
  else  if (qps > 0)
  {
    pcab->iCharIS = 1;
  }
  else
  {
    pcab->iCharIS = 0;
  }
  ....
  if (hps < 0)
  {
    pcab->wCharHpsPos = -hps;
    pcab->iCharPos = 2;
  }
  else  if (hps > 0)
  {
    pcab->iCharPos = 1;
  }
  else
  {
    pcab->iCharPos = 1;
  }
  ....
}

Попередження PVS-Studio: V523 The 'then' statement is equivalent to the 'else' statement. dlglook1.c 873
 
Коли працюють із змінною 'qps', то записують у 'pcab-> iCharIS' такі значення: 2, 1, 0.
 
Аналогічно працюють із змінною 'hps'. Але при цьому в змінну 'pcab-> iCharPos' поміщаються підозрілі числа: 2, 1, 1.
 
Швидше за все, це помилка. У самому кінці, напевно, слід було використовувати нуль.
 
 Висновок
Знайдено зовсім трохи дивних місць. Причини дві. По-перше, код мені здався написаний якісно і вельми зрозуміло. По-друге, аналіз був таки неповноцінним. Вчити ж аналізатор особливостям старого Сі немає практичної потреби.
 
Сподіваюся, я подарував вам кілька хвилин цікавого читання. Спасибі за увагу. І спробуйте аналізатор PVS-Studio на своєму коді.
 
 цю статтю англійською
Якщо хочете поділитися цією статтею з англомовною аудиторією, то прошу використовувати посилання на переклад: Andrey Karpov. Archeology for Entertainment, or Checking Microsoft Word 1.1a with PVS-Studio .
 
 Прочитали статтю і є питання? Часто до наших статей ставлять одні і ті ж питання. Відповіді на них ми зібрали тут: Відповіді на питання читачів статей про PVS-Studio і CppCat, версія 2014 . Будь ласка, ознайомтеся зі списком.
 
  
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.