pdpd

неудачный выбор приоритетов на PDP-11 и его наследие на Си/Си++

крис касперски, ака мыщъх, no-emal

Отлаживал как-то мыщъх одну свою программу, написанную на Си? и периодически делающую из чисел винегрет или выдающую критическую ошибку accessviolation при трудно воспроизводимых обстоятельствах. Тщательная проверка исходного текста «глазами» ровным счетом ничего не дала. Программа продолжала выпендриваться, сроки сдачи проекта поджимали, дедлайн нависал над головой Дамокловым мечом, мыщъх нервничал, много курил, нервничал, закидывался ноотропами, не спал ночами, высаживался на жуткую измену, а глубоко укоренившийся баг игнорировал всякие попытки вытащить его из норы.

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

Виновницей оказалась мерзопакостная конструкция типа «*p[a]++», которая вопреки логике увеличивает отнюдь _не_ содержимое ячейки, на которую указывает «*(p+a)», а значение самого указателя p, то есть транслируется в следующий ассемблерный код:

moveax, dwordptr [p]; прочитать адрес переменной p

movecx, dwordptr [eax]; прочитать значение указателя, на который указывает p

addecx, 1; увеличить содержимое указателя на кот. указывает p

movedx, dwordptr [p]; прочитать адрес переменной p

movdwordptr [edx], ecx; занести в переменную p ее обновленное значение

Листинг 1 ассемблерный код в который транслируется *p[a]++

Специально написанный для этого дела демонстрационный пример (см. листинг 3) в отладчике выглядел так:

Рисунок 1 откомпилированная конструкция *p[a]++ под лупой отладчика

В то время, как _ожидаемый_ вариант трансляции должен был выглядеть так:

moveax, dwordptr [p]; прочитать адрес переменной p

movecx, dwordptr [eax]; прочитать значение указателя на который указывает p

movdl, byteptr [ecx]; прочитать значение переменной на которую указывает *p

adddl, 1; увеличить значение переменной на 1

moveax, dwordptr [p]; прочитать адрес переменной p

movecx, dwordptr [eax]; прочитать значение указателя на который указывает p

movbyteptr [ecx], dl; занести обновленное значение переменной *p[a]

Листинг 2 ожидаемый вариант трансляции конструкции *p[a]++

Мыщь решил проблему очень просто — явно навязав свое намерение компилятору путем расстановки скобок: «(*p)[a]++». Аналогичного результата было можно достичь заменой оператора «++» на оператор «+=» и тогда коварная конструкция принимала вид «*p[a]+=1»

Выгнав бага из его норы, мыщъх решил провести широкомасштабные археологические раскопки, чтобы добраться до _смысла_ происходящего. Причастность компилятора была отведена сразу, как только остальные компиляторы выдали идентичный результат. Значит, собака зарыта вовсе не в компиляторе, а в самом языке Си.

Странно. Очень странно. Ведь основное кредо Си — краткость. И тут… вдруг такое расточительство! Ведь, чтобы использовать оператор «*» необходимо расставлять скобки, а это — целых два нажатия на Клаву. Зачем? Может быть, есть такие ситуации, где именно такой расклад приоритетов дает выигрыш? Вообще: о чем думали в этот момент разработчики языка? В доступных мне книжках никаких вразумительных объяснений мыщъх так и не нашел.

…прозрение наступило внезапно и причина, как выяснилась, оказалась даже не в самом языке, а… в особенностях косвенной автоинкрементной/автодекрементной адресации процессора PDP-11, из которого, собственно, и вырос Си. Команда «MOV @(p)+,xxx» пересылала содержимое p в xxx, а затем увеличивала значение p. Да! Именно p, а отнюдь не ячейки, на которую p ссылается!!!

Так стоит ли удивляться тому, что люди, взращенные на идеологии PDP-11, перенесли ее поведение и на разрабатываемый ими язык?!

Ниже приводится демонстрационный пример, который можно погонять на различных Си/Си++ компиляторов, с неизменностью получая один и тот же результат.

main()

{

char buf; char* p_buf[2]; char p; #define INIT buf=0x66; *p_buf=&buf; *(p_buf+1)=&buf; p=&p_buf; INIT; printf(«char p;\n»);

printf(«p = %p; *p = %p; p = %x\n\n»,p, *p, p);

*p[0]++; printf(«*p[0]++;\n»);

printf(«p = %p; *p = %p; p = %x\n»,p, *p, p);

printf(«смотрите, увеличилось _не_ содержимое p,\n»); printf(«а указатель, на который ссылается *p!\n»); printf(«т.е. мы получили _совсем_ не то, что хотели!\n\n»); INIT; (*p)[0]++; printf(«(*p)[0]++;\n»); printf(«p = %p; *p = %p; p = %x;\n»,p, *p, p); printf(«хорошо, заключаем *p в скобки, тем самым явно\n»); printf(«навязывая компилятору последовательность действий\n\n»); INIT; *p[0]+=1; printf(«*p[0]+=1;\n»); printf(«p = %p; *p = %p; p = %x;\n»,p, *p, **p);

printf(«забавно, но замена оператора ++ на оператор +=\n»);

printf(«эту проблему как рукой снимает!\n»);

}

Листинг 3 демонстрационный пример pdp.c

Рисунок 2результат прогона pdp.exe