64-битный конь, который умеетсчитатьАвтор: Андрей КарповДата: 11.05.2009АннотацияСтатья посвящена особенностям поведения к...
Но есть и еще одна, куда более тонкая причина называть ошибки "потенциальными". Дело в том,проявит себя ошибка или нет, за...
продолжалось это час, два, три ... А потом срубил Илья Муромец Змею Горынычу 32768 голов иумер Змей Горыныч, ибо был он 16...
000000014000104F         jne             wmain+40h (140001040h)Происходит то самое переполнение, которое должно было быть ...
printf("Errorn");        break;    }}Использование вместо index++ выражения "index += volatileVar;" приводит к тому, что в...
0000000140001052        test            rsi,rsi0000000140001055        je              wmain+6Ch (14000106Ch)0000000140001...
0000000140001062        test            ebx,ebx0000000140001064        cmovne          eax,r8d0000000140001068        add ...
Альтернативный путь диагностики 64-битных ошибок состоит в использовании инструментовстатического анализа. Он куда более р...
7. Андрей Карпов. Сравнение диагностических возможностей анализаторов при проверке 64-   битного кода. http://www.viva64.c...
Upcoming SlideShare
Loading in …5
×

64-битный конь, который умеет считать

262 views
149 views

Published on

Статья посвящена особенностям поведения компилятора Visual C++ при генерации 64-битного кода и связанными с этим потенциальными ошибками.

Published in: Technology, Business
0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total views
262
On SlideShare
0
From Embeds
0
Number of Embeds
2
Actions
Shares
0
Downloads
2
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

64-битный конь, который умеет считать

  1. 1. 64-битный конь, который умеетсчитатьАвтор: Андрей КарповДата: 11.05.2009АннотацияСтатья посвящена особенностям поведения компилятора Visual C++ при генерации 64-битногокода и связанными с этим потенциальными ошибками.ВведениеФеномен "Умного Ганса", коня мистера фон Остена, был описан в 1911 году [1]. Умный Ганс былзнаменит тем, что умел читать и решал математические задачки, выстукивая ответ переднимкопытом. Конечно, было много скептиков. Поэтому способности Ганса проверялись комиссиейэкспертов, которая установила, что конь демонстрирует их без помощи мистера фон Остена. Нокак мог существовать такой — человеческий! — уровень интеллекта у простой лошади? ПсихологО. Пфангст с чрезвычайной тщательностью выполнил серию экспериментов, в результате которыхобнаружил, что Ганс получал едва уловимые неумышленные подсказки от тех, кто задавал емувопросы. Например, после того как Ганса о чем-то спрашивали, люди устремляли свой взгляд наего переднее копыто, с помощью которого конь "отвечал". Но как только Ганс ударял копытомнужное число раз, спрашивающие совсем чуть-чуть поднимали свои глаза или голову в ожиданиизавершения его ответа. И конь, который был натренирован замечать и использовать эти почтинеуловимые для наблюдателей движения, воспринимал их как сигналы к прекращению своихдействий. Со стороны это всегда выглядело как правильный ответ на вопрос.Вот такой был замечательный конь, который считал и решал задачки, хотя и не умел этого делать.Цифровыми конями начала 21-ого века стали 64-битные программы, многие из которых тоже неумеют считать, хотя успешно делают вид. Рассмотрим этот феномен более подробно.1. Потенциальные ошибкиЯ явлюсь автором и соавтором ряда статей, посвященных проблематике разработки 64-битныхприложений. Статьи вы можете найти на нашем сайте: http://www.viva64.com/ru/articles/64-bit-development. В этих статьях, я стараюсь использовать термин "потенциальная ошибка" или"скрытая ошибка", а не просто "ошибка" [2, 3, 4].Я объясняю это тем, что один и тот же код можно рассматривать как корректным, так инекорректным в зависимости от его назначения. Простой пример - использование для индексацииэлементов массива переменной типа int. Если с помощью этой переменной мы обращаемся кмассиву графических окон, то все корректно. Не бывает нужно, да и не получится работать смиллиардами окон. А вот индексация с использованием переменной типа int к элементаммассива в 64-битных математических программах или базах данных, вполне может представлятьсобой проблему, когда количество элементов выйдет из диапазона 0..INT_MAX.
  2. 2. Но есть и еще одна, куда более тонкая причина называть ошибки "потенциальными". Дело в том,проявит себя ошибка или нет, зависит не только от входных данных, но и от настроенияоптимизатора компилятора. Я долго обходил эту тему, поскольку большинство этих ошибокхорошо проявляют себя в debug-версии, и "потенциальны" только в release-версиях. Однако невсякую программу, собранную как debug, можно отлаживать на больших объемах данных.Возникает ситуация, когда debug-версия тестируется только на самых простых наборах данных. Анагрузочное тестирование и тестирование конечными пользователями на реальных данных,выполняется на release-версиях, где ошибки могут быть временно скрыты. Поэтому я решилподелиться теми знаниями, которые есть. Надеюсь, я смогу убедить, что при переносе программына другую платформу опасно полагаться только на проверки этапа исполнения (юнит-тесты,динамический анализ, ручное тестирование). Вы скажете, все это чтобы продвигать инструментViva64. Да, для этого, но все-таки послушайте страшные истории, которые я сейчас буду вамрассказывать. Я люблю их рассказывать.2. С чего все началось- А почему у тебя в коде подряд два одинаковых JMPа стоят?- А вдруг первый не сработает.Впервые я столкнулся с особенностями оптимизации компилятора Visual C++ 2005 при подготовкепрограммы PortSample. Это проект, который входит в состав дистрибутива Viva64 и предназначендля демонстрации всех ошибок, которые диагностирует анализатор Viva64. Примеры, которыесодержатся в этом проекте должны корректно работать в 32-битном режиме и приводить кошибкам в 64-битном варианте. В отладочной версии все работало замечательно, а вот с releaseверсией возникли затруднения. Тот код, который в 64-битном режиме должен был зависать илиприводить к падению - успешно работал! Причина оказалась в оптимизации. Решением сталодополнительное избыточное усложнение кода примеров и расстановка ключевых слов "volatile",которые вы во множестве сможете наблюдать в проекте PortSample.То же самое относится и к Visual C++ 2008. Код будет конечно несколько разным, но все что будетнаписано в этой статье можно отнести как к Visual C++ 2005, так и к Visual C++ 2008. И далее встатье различий делаться не будет.Если вам покажется, что это только хорошо, если некоторые ошибки не проявляют себя, то гонитескорее эту мысль. Код с подобными ошибками становится крайне нестабильным. И малейшееизменение кода, напрямую не связанное с ошибкой, может приводить к изменению поведения.На всякий случай подчеркну, что виноват в этом не компилятор, а скрытые дефекты кода. Далеебудут показаны примерные фантомные ошибки, которые исчезают и появляются в release-версияхпри малейших изменениях кода и за которыми можно долго охотиться.3. ФантомыГлава будет длинная и скучная, поэтому начну с анекдота, который является ее краткимсодержанием:Шел Илья Муромец по лесу и вышел на поляну, на которой Змей Горыныч сидел. Подбежал ИльяМуромец к Змею Горынычу и срубил ему единственную голову. А у Змея Горыныча вместо этойголовы две выросло. Срубил Илья две головы, выросло 4. Срубил 4, выросло 8... И так
  3. 3. продолжалось это час, два, три ... А потом срубил Илья Муромец Змею Горынычу 32768 голов иумер Змей Горыныч, ибо был он 16-ти разрядный.Как и в анекдоте, проблемы кроятся в переполнении типов, которое может произойти, а может ине произойти в зависимости от того какой код сгенерирует компилятор при включеннойоптимизации. Рассмотрим первый пример кода, который работает в release режиме, хотя делатьэтого не должен:int index = 0;size_t arraySize = ...;for (size_t i = 0; i != arraySize; i++) array[index++] = BYTE(i);Данный код корректно заполняет весь массив значениями, даже если размер массива гораздобольше INT_MAX. Теоретически это невозможно, поскольку переменная index имеет тип int. Черезнекоторое время из-за переполнения должен произойти доступ к элементам по отрицательномуиндексу. Однако оптимизация приводит к генерации следующего кода:0000000140001040 mov byte ptr [rcx+rax],cl0000000140001043 add rcx,10000000140001047 cmp rcx,rbx000000014000104A jne wmain+40h (140001040h)Как видите, используются 64-битные регистры и переполнение не происходит. Но сделаем совсеммаленькое исправление кода:int index = 0;for (size_t i = 0; i != arraySize; i++){ array[index] = BYTE(index); ++index;}Будем считать, что так код выглядит более красиво. Согласитесь, что функционально он осталсяпрежним. А вот результат будет существенным - произойдет аварийное завершение программы.Рассмотрим сгенерированный компилятором код:0000000140001040 movsxd rcx,r8d0000000140001043 mov byte ptr [rcx+rbx],r8b0000000140001047 add r8d,1000000014000104B sub rax,1
  4. 4. 000000014000104F jne wmain+40h (140001040h)Происходит то самое переполнение, которое должно было быть и в предыдущем примере.Значение регистра r8d = 0x80000000 расширяется в rcx как 0xffffffff80000000. И как следствие -запись за пределами массива.Рассмотрим другой пример оптимизации и как легко все испортить. Пример:unsigned index = 0;for (size_t i = 0; i != arraySize; ++i) { array[index++] = 1; if (array[i] != 1) { printf("Errorn"); break; }}Ассемблерный код:0000000140001040 mov byte ptr [rdx],10000000140001043 add rdx,10000000140001047 cmp byte ptr [rcx+rax],1000000014000104B jne wmain+58h (140001058h)000000014000104D add rcx,10000000140001051 cmp rcx,rdi0000000140001054 jne wmain+40h (140001040h)Компилятор решил использовать 64-битный регистр rdx для хранения переменной index. Врезультате код может корректно обрабатывать массивы размером более UINT_MAX.Но мир хрупок. Достаточно немного усложнить код и он станет неверен:volatile unsigned volatileVar = 1;...unsigned index = 0;for (size_t i = 0; i != arraySize; ++i) { array[index] = 1; index += volatileVar; if (array[i] != 1) {
  5. 5. printf("Errorn"); break; }}Использование вместо index++ выражения "index += volatileVar;" приводит к тому, что в коденачинают участвовать 32-битные регистры, из-за чего происходят переполнения:0000000140001040 mov ecx,r8d0000000140001043 add r8d,dword ptr [volatileVar (140003020h)]000000014000104A mov byte ptr [rcx+rax],1000000014000104E cmp byte ptr [rdx+rax],10000000140001052 jne wmain+5Fh (14000105Fh)0000000140001054 add rdx,10000000140001058 cmp rdx,rdi000000014000105B jne wmain+40h (140001040h)Напоследок приведу интересный, но большой пример. К сожалению, я не смог его сократить,чтобы сохранить необходимее поведение. Именно этим и опасны такие ошибки, так какневозможно предугадать к чему приводит простейшее изменение кода.ptrdiff_t UnsafeCalcIndex(int x, int y, int width) { int result = x + y * width; return result;}...int domainWidth = 50000;int domainHeght = 50000;for (int x = 0; x != domainWidth; ++x) for (int y = 0; y != domainHeght; ++y) array[UnsafeCalcIndex(x, y, domainWidth)] = 1;Данный код не может корректно заполнить массив, состоящий из 50000*50000 элементов.Невозможно это по той причине, что при вычислении "int result = x + y * width;" должнопроисходить переполнение.Благодаря чуду массив все же корректно заполняется в release-варианте. Функция UnsafeCalcIndexвстраивается внутрь цикла, используются 64-битные регистры:
  6. 6. 0000000140001052 test rsi,rsi0000000140001055 je wmain+6Ch (14000106Ch)0000000140001057 lea rcx,[r9+rax]000000014000105B mov rdx,rsi000000014000105E xchg ax,ax0000000140001060 mov byte ptr [rcx],10000000140001063 add rcx,rbx0000000140001066 sub rdx,1000000014000106A jne wmain+60h (140001060h)000000014000106C add r9,10000000140001070 cmp r9,rbx0000000140001073 jne wmain+52h (140001052h)Все это произошло из-за того, что функция UnsafeCalcIndex проста и может быть легко встроена.Стоит ее немного усложнить или компилятору посчитать, что встраивать ее не стоит, и возникнетошибка, которая проявит себя на больших объемах данных.Немного модифицируем (усложним) функцию UnsafeCalcIndex. Обратите внимание, что логикафункции ничуть не изменилась:ptrdiff_t UnsafeCalcIndex(int x, int y, int width) { int result = 0; if (width != 0) result = y * width; return result + x;}Результат - аварийное завершение программы, при выходе за границы массива:0000000140001050 test esi,esi0000000140001052 je wmain+7Ah (14000107Ah)0000000140001054 mov r8d,ecx0000000140001057 mov r9d,esi000000014000105A xchg ax,ax000000014000105D xchg ax,ax0000000140001060 mov eax,ecx
  7. 7. 0000000140001062 test ebx,ebx0000000140001064 cmovne eax,r8d0000000140001068 add r8d,ebx000000014000106B cdqe000000014000106D add rax,rdx0000000140001070 sub r9,10000000140001074 mov byte ptr [rax+rdi],10000000140001078 jne wmain+60h (140001060h)000000014000107A add rdx,1000000014000107E cmp rdx,r120000000140001081 jne wmain+50h (140001050h)Я думаю, вы уже заскучали. Прошу прощения. Просто хотелось показать, как легко работающая64-битная программа может стать неработающей, после того как вы внесете в нее самыебезобидные правки или соберете другой версией компилятора.4. Диагностика потенциальных ошибок Программа - это последовательность обработки ошибок. (с) Неизвестный авторЯ предполагаю, что многие уже существующие 64-битные приложения или те, которые будутвскоре перенесены на 64-битные системы могут неожиданно начать преподносить все новые иновые неприятные сюрпризы. В них может быть выявлено большое количество дефектов приувеличении объема входных данных, который был недоступен для обработки на 32-битныхсистемах. Скрытые дефекты могут неожиданно проявлять себя в ходе дальнейшей модернизациикода программы или при смене версии библиотек или компилятора.Как и в истории с конем, первое впечатление может быть обманчиво. И то, что ваша программауспешно начала обрабатывать большой объем данных, вам может только казаться. Необходимакуда более тщательная проверка, которая сможет точно показать, считает ваш 64-битный конь насамом деле или нет.Чтобы быть уверенным в корректности 64-битной программы, самым минимальным шагом будетиспользование на всех этапах тестирования не только release, но и debug версии. Учтите, что этоявляется необходимым, но вовсе не достаточным условием. Если в тестах не используютсянаборы данных, которые, например, не задействуют большой объем оперативной памяти, тоошибка может не проявить себя и в release и в debug-версии [5]. Необходимо расширение юнит-тестов, расширение наборов данных для нагрузочного и ручного тестирования. Необходимозаставить алгоритмы обрабатывать новые сочетания данных, которые доступны только на 64-битных системах [6].
  8. 8. Альтернативный путь диагностики 64-битных ошибок состоит в использовании инструментовстатического анализа. Он куда более радикален и надежен, чем гадание о том, достаточнодобавлено тестов или нет. Он удобен, так как не требует использования debug-версии дляперемалывания гигабайт данных.Смысл метода состоит в том, чтобы единожды при переносе программы выполнить полныйанализ проекта и просмотреть все диагностические сообщения о подозрительных местах в коде.Людей отпугивает список из тысяч и десятков тысяч предупреждений. Но суммарное время, сразупотраченное на их анализ, будет на порядок меньше, чем исправление годами разнообразнейшихбаг-репортов, возникающих буквально ниоткуда. Это будут как раз те самые, описанные ранеефантомы. Вдобавок когда вы начнете работать со списком предупреждений, то быстро выяснится,что большую часть их можно отфильтровать и работы по анализу окажется не так много каккажется. В дальнейшем вам будет достаточно использовать статический анализ, только для вновьсоздаваемого кода, что не занимает много времени.Из инструментария для поиска 64-битных фантомов я конечно предложу инструмент который мыразрабатываем - Viva64. Кстати, скоро этот инструмент войдет в состав PVS-Studio, которая будетобъединять все наши инструменты статического анализа.Чтобы быть более объективным и меня поменьше выгоняли с сайтов с этой статьей, какрекламной, упомяну некоторые другие инструменты. Следует назвать Gimpel PC-Lint и ParasoftC++test. В них также реализованы правила для проверки 64-битных ошибок, хотя в этом ониобладают меньшими диагностическими возможностями, чем узкоспециализированныйинструмент Viva64 [7]. Еще существует Abraxas CodeCheck, в новой версии которого (14.5) такжереализованы функции диагностики 64-битных ошибок, но более подробными сведениями я нерасполагаю.ЗаключениеЯ буду рад если статья поможет вам легче осваивать новые платформы, зная какие скрытыепроблемы могут при этом возникать. Спасибо за внимание.Библиографический список 1. Роджер Р. Хок. 40 исследований, которые потрясли психологию. 4-е межд. изд. СПб.: Прайм-Еврознак, 2006, ISBN: 5-93878-237-6. 2. Андрей Карпов. 64 бита, /Wp64, Visual Studio 2008, Viva64 и все, все, все... http://www.viva64.com/art-1-1-253695945.html 3. Андрей Карпов, Евгений Рыжков. Статический анализ кода для верификации 64-битных приложений. http://www.viva64.com/art-1-1-1630333432.html 4. Андрей Карпов. 7 шагов по переносу программы на 64-битную систему. http://www.viva64.com/art-1-1-1148261225.html 5. Андрей Карпов, Евгений Рыжков. 20 ловушек переноса Си++ - кода на 64-битную платформу. http://www.viva64.com/art-1-1-1958348565.html 6. Андрей Карпов, Евгений Рыжков. Поиск ловушек в Си/Си++ коде при переносе приложений под 64-битную версию Windows. http://www.viva64.com/art-1-1- 329725213.html
  9. 9. 7. Андрей Карпов. Сравнение диагностических возможностей анализаторов при проверке 64- битного кода. http://www.viva64.com/art-1-1-1441719613.html

×