По какой-то причине эта задачка получила множество откликов и споров, хотя она относительно проста.
Пожалуй, я начну подводить к решению разбором оставленных намёков:
- Для начала замечу, что задача заключалась в объяснении, почему показания отладчика не соответствуют реальному положению дел. Мне показалось, что не все это уловили.
- Несколько первых подсказок ("не менял значение ZF", нет антивируса/антиотладочного кода и т.п.) говорят нам о том, что ситуация не сфабрикована специально (я не рисовал снимок экрана в фотошопе! :) ), а естественно получилась в программе.
- Вторая большая подсказка ("это проявляется в пустом VCL приложении") решительно намекает на то, что код в вопросе тут не при чём. Если код не причём, внешнего кода нет (других потоков, антивируса и т.п.), то это в явном виде оставляет нам только саму сессию отладки.
- Позже добавленный намёк "Это поведение, которое вы используете постоянно по время отладки" ещё больше указывает на что-то, связанное с отладчиком. Причём - абсолютно типичное.
- Снимок экрана указывает, что байт в вопросе находится в секции кода (
$004C3B8F
принадлежит .exe-файлу). - А код в вопросе указывает на то, что производится проверка кода на соответствие шаблону машинной инструкции
CALL
.
Что же это за слово?
Подумайте, это ваш последний шанс. Что может делать с кодом программы отладчик, да ещё почти в каждой сессии отладки?
И слово это... breakpoint
К этому моменту ответ уже очевиден. Что может менять код без явного вмешательства программы, её потоков или внешнего кода? Конечно же, отладчик. Когда отладчик может менять секцию кода, да ещё с учётом подсказки, что вы постоянно этим пользуетесь? Когда вы ставите точку останова на код.
Как типичный отладчик пользовательского режима работает с точками останова? Ну, он изменяет код в нужной точке, запоминая то, что там было раньше, а взамен записывая байт
$CC
, что на языке x86 означает int 3
- программная точка останова.Когда процессор выполняет код и встречает инструкцию
int 3
- он генерирует прерывание, которое ловится отладчиком. Отладчик видит, что это произошло прерывание "STATUS_BREAKPOINT", видит, что сейчас произошло выполнение инструкции вот по этому адресу, где он поставил точку останова, так что отладчик может восстановить код и запустить выполнение с этого же места.Конечно же, когда отладчик так вмешивается в программу, он компенсирует это, показывая вам те значения, которые были бы в случае реального выполнения программы без отладчика. Т.е. хотя в памяти на самом деле записана программная точка останова (
$CC
), отладчик это учитывает и показывает вам исходные значения - вот почему все evaluate покажут вам то, что произошло бы при выполнении программы без отладчика, но не текущее положение дел.К сожалению, отладчик не достаточно умён, чтобы учитывать этот момент, когда к памяти обращается не он сам, а выполняемый им код - потому что обращения самого себя отладчик отследить в состоянии, но вот код выполняется аппаратно, процессором, а не эмулируется отладчиком, так что сфабриковать исходную ситуацию отладчик в этом случае не может. Вот откуда идёт расхождение показаний отладчика и реального выполнения.
Итак, это был очередной пример того, как программа может менять своё поведение под отладкой.
Осталось только пояснить вот это: "Об этом не знают многие новички, а те, кто знает - часто забывают (даже опытные программисты). Кажется, сложность решения этой задачи как раз и происходит от этого факта". Что же это такое, о чём часто забывают?
Многие забывают о том, что отладчик Delphi - инвазивный (invasive). Что означает, что отладчик активно вмешивается в программу, он не просто пассивно следит за ней. В дополнение к таким очевидностям как пошаговое выполнение, в голову приходят и более яркие примеры - скажем, изменение значение переменных. И хотя это на первый взгляд может показаться не очень волнующим (к примеру, изменение
Integer
-переменной) - но подумайте о случае, когда вы меняете значение строковой переменной. Ведь изменение значения строки будет означать не только изменение байтов содержимого строки в памяти, но и вызов функций менеджера памяти для освобождения старого значения и выделения памяти под новое значение - вызов функций в то время, пока программа стоит на паузе!Что ж, теперь, когда ответ озвучен, у вас есть шансы высказать своё мнение, что ещё можно было сказать, чтобы не раскрыть ответ. Замечу, что с задачкой вполне справились некоторые люди - ответ был дан в комментариях к посту о задачке (поздравления khan.malign). Однако его (ответ) опознали не все, что говорит о том, что дело не столько в сложности задачки как таковой, сколько в том, что область знаний отвечающих не покрывает те, которые нужны для решения задачи. В частности, как мне кажется, не все разбираются в принципах и типичных подходах в работе прикладных отладчиков в Windows. Эй, только не надо винить в этом меня!
Кстати, было высказано множество версий, но все они, так или иначе, не принимали в расчёт какой-то факт из указанной картины, а некоторые были и вовсе ошибочными. Я быстренько пробегусь по ним и кратко укажу на несостыковки (поскольку я не обладаю глубокими знаниями ассемблера, я допускаю, что я мог что-то неверно понять или наврать):
- "Сравнение с регистром идет словом, а не байтом" - на снимке явно указан префикс байта.
- "Либо ошибка выравнивания проца" - на x86 ошибки выравнивания исправляются автоматически. На других архитектурах - возбуждается исключение. Оба случая не соответствуют картине.
- "Либо некогерентность кеша процессора и работы оборудования в режиме DMA mastering" - не уверен, насколько это реально, но чтобы сильно не думать (я ленив), я просто дополнительно добавил примечание, что ситуация стабильна на длительном участке.
- "Либо кривая компиляция именно этого блока" - в CPU отладчике мы видим уже скомпилированный код.
- "Либо включён режим процессора прерываний по не выравненным переменным и эта переменная в стеке неправильно размещена и стек изменился" - кажется, явно упущен момент, что адрес принадлежит секции кода.
- "$B2 = 13 = перенос строки" - здесь вижу отсутствие понимания, что для машинного уровня нет понятия "тип" и "имя", а лишь "адрес", "размер", да атрибуты страницы памяти.
- "Команда CMP сравнивает два значения, и в случае их равенства ставит ZF=0" - явная ошибка. Наоборот.
- "Оптимизация условия AND" - во-первых, идёт выполнение первого куска (первого условия). Во-вторых, это никак не объясняет видимую разницу в поведении кода и показаниях отладчика.
- "Кажется понял, отладчик имеет права доступа по чтению на сегмент кода, а реальная программа может и не иметь, особенно если включены средства защиты от вирусов или переполнения буферов и т.д." - сегмент кода по-умолчанию имеет атрибуты чтение+выполнение, но даже если что-то сбросит "чтение", оставив только "выполнение", то попытка выполнить CMP возбудит Access Violation, а не даст неверный результат, как на снимке экрана.
P.S. Замечание по исходному проекту, откуда это взялось. Собственно, я производил отладку совершенно другого вопроса, но неожиданно при запуске программы под отладчиком стал заходить в другую ветку кода. После пошагового выполнения программы до точки ветвления и наблюдения ровно той же картины, что была показана в вопросе, я и обнаружил причину, которая заключалась в том, что мне для отладки нужно было установить точку останова ровно на строчку кода, содержащую вызов функции (инструкцию
CALL
), которая и проверялась кодом в вопросе.Установка точки останова была необходима потому, что проверочный код вызывается зиллионы раз, а меня интересовал только вызов по конкретной ветке - поэтому было установлено две точки: первая - в интересующем меня месте, но неактивная, а вторая - в точке инструкции вызова, как идентифицирующая ветку кода. Точка была non-stop, а просто активировавшая вторую (неактивную) точку. Таким образом, они должны были работать парой, останавливая сессию отладки именно в нужный момент, на нужной ветке.
Соответственно, не поставив точку останова, я не получал неверного поведения, но я и не мог остановиться в нужном месте. А остановка в нужном месте означала установку точки останова. Таким образом, со стороны казалось, что это вообще баг отладчика: если выполнять программу без него или под ним, но простым прогоном без остановки - всё OK, но если выполнять по шагам конкретный участок (что требовало установки точки останова) - то программа начинает вести себя неверно.
Проблему с
$CC
я обошёл просто вставив NOP
перед CALL
и установив бряк на NOP. Мне было важно идентифицировать лишь ветку кода точкой останова - не имеет значения, на какой инструкции ветки кода она будет установлена. Как я уже заметил выше, здесь нет никакой проблемы при реальном выполнении кода (вне отладчика) - мы просто получаем странное поведение во время отладочной сессии. Разумеется, после решения исходной проблемы (не имеющей отношения к вопросу задачки), NOP был удалён.
Ещё раз замечу, что секция "Задачки" не является каким-либо конкурсом, соревнованием или попытками что-то кому-то доказать. Я просто делюсь увиденным и интересным. Здесь нет никаких призов, выигравших или проигравших - комментарии с (попытками) ответов к посту вы пишете добровольно.
ОтветитьУдалитьВозможно, мне стоит запрещать комментарии к постам-задачкам, чтобы это было более очевидно?
Или давать призы :)
ОтветитьУдалить> Я быстренько пробегусь по ним и кратко укажу на несостыковки (поскольку я не обладаю глубокими знаниями ассемблера, я допускаю, что я мог что-то неверно понять или наврать):
ОтветитьУдалить...
Всё понял верно и не наврал. В целом. Я Qraizer, если чё :)
> Возможно, мне стоит запрещать комментарии к постам-задачкам, чтобы это было более очевидно?
Не надо. Читать забавно. А троллей везде хватает, относись к этому философски.
Спасибо за feedback.
ОтветитьУдалитьПосле подсказки "Это поведение, которое вы используете постоянно по время отладки", ответ был уже очевиден.
ОтветитьУдалитьА вообще задачки у вас интересные, буду ждать следующую.