Урок 24. Фантомные ошибки

430 views

Published on

Мы закончили рассмотрение паттернов 64-битных ошибок. Последнее на чем мы остановимся в связи с этими ошибками, является то, как они могут проявляться в программах.

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

  • Be the first to like this

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

No notes for slide

Урок 24. Фантомные ошибки

  1. 1. Урок 24. Фантомные ошибкиМы закончили рассмотрение паттернов 64-битных ошибок. Последнее на чем мы остановимся всвязи с этими ошибками, является то, как они могут проявляться в программах.Дело в том, что не так просто показать в примере, что приведенный 64-битный код приведет кошибке при большом значении N:size_t N = ...for (int i = 0; i != N; ++i){ ...}Вы можете попробовать подобный простой пример и увидеть, что код работает. Дело в том,каким образом построит код оптимизирующий компилятор. Будет работать код или нет зависит отразмера тела цикла. В примерах он всегда маленький, и для счетчиков могут использоваться 64-битные регистры. В реальных программах, с большими телами циклов, ошибка легко возникает,когда компилятор будет сохранять значение переменной "i" в памяти. А теперь давайтепопробуем разобраться с непонятным текстом, который вы только что прочитали.При описании ошибок, очень часто использовался термин "потенциальная ошибка" илисловосочетание "возможно возникновение ошибки". В основном это объясняется тем, что один итот же код можно считать как корректным, так и некорректным в зависимости от его назначения.Простой пример - использование для индексации элементов массива переменной типа int. Если спомощью этой переменной мы обращаемся к массиву графических окон, то все корректно. Небывает нужно, да и не получится работать с миллиардами окон. А вот индексация сиспользованием переменной типа int к элементам массива в 64-битных математическихпрограммах или базах данных, вполне может представлять собой проблему, когда количествоэлементов выйдет из диапазона 0..INT_MAX.Но есть и еще одна, куда более тонкая причина называть ошибки "потенциальными". Дело в том,проявит себя ошибка или нет, зависит не только от входных данных, но и от настроенияоптимизатора компилятора. Большинство из рассмотренных в уроках ошибок хорошо проявляютсебя в debug-версии, и "потенциальны" в release-версиях. Однако не всякую программу,собранную как debug, можно отлаживать на больших объемах данных. Возникает ситуация, когдаdebug-версия тестируется только на самых простых наборах данных. А нагрузочное тестированиеи тестирование конечными пользователями на реальных данных, выполняется на release-версиях,где ошибки могут быть временно скрыты.Впервые мы столкнулись с особенностями оптимизации компилятора Visual C++ 2005 приподготовке программы OmniSample. Это проект, который входит в состав дистрибутива PVS-Studioи предназначен для демонстрации всех ошибок, которые диагностирует анализатор Viva64.Примеры, которые содержатся в этом проекте, должны корректно работать в 32-битном режиме иприводить к ошибкам в 64-битном варианте. В отладочной версии все работало замечательно, а
  2. 2. вот с release версией возникли затруднения. Тот код, который в 64-битном режиме должен былзависать или приводить к аварийному завершению программы - успешно работал! Причинаоказалась в оптимизации. Решением стало дополнительное избыточное усложнение кодапримеров и расстановка ключевых слов "volatile", которые вы сможете наблюдать в коде проектаOmniSample.То же самое относится и к Visual C++ 2008/2010. Код будет конечно несколько разным, но все чтобудет написано здесь можно отнести как к Visual C++ 2005, так и к Visual C++ 2008.Если вам покажется, что это только хорошо, если некоторые ошибки не проявляют себя, то гонитескорее эту мысль прочь. Код с подобными ошибками становится крайне нестабильным. Ималейшее изменение кода, напрямую не связанное с ошибкой, может приводить к изменениюповедения. На всякий случай подчеркну, что виноват в этом не компилятор, а скрытые дефектыкода. Далее будут показаны примерные фантомные ошибки, которые исчезают и появляются вrelease-версиях при малейших изменениях кода, и на которых можно долго и утомительноохотиться.Рассмотрим первый пример кода, который работает в 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;size_t arraySize = ...;for (size_t i = 0; i != arraySize; i++){ array[index] = BYTE(index); ++index;
  3. 3. }Будем считать, что так код выглядит более красиво. Согласитесь, что функционально он осталсяпрежним. А вот результат будет существенным - произойдет аварийное завершение программы.Рассмотрим сгенерированный компилятором код:0000000140001040 movsxd rcx,r8d0000000140001043 mov byte ptr [rcx+rbx],r8b0000000140001047 add r8d,1000000014000104B sub rax,1000000014000104F 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.
  4. 4. Но мир хрупок. Достаточно немного усложнить код и он станет неверен:volatile unsigned volatileVar = 1;...unsigned index = 0;for (size_t i = 0; i != arraySize; ++i) { array[index] = 1; index += volatileVar; if (array[i] != 1) { 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;
  5. 5. 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-битные регистры: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;}
  6. 6. Результат - аварийное завершение программы, при выходе за границы массива:0000000140001050 test esi,esi0000000140001052 je wmain+7Ah (14000107Ah)0000000140001054 mov r8d,ecx0000000140001057 mov r9d,esi000000014000105A xchg ax,ax000000014000105D xchg ax,ax0000000140001060 mov eax,ecx0000000140001062 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-битная программа может легкостать неработающей, после того как вы внесете в нее самые безобидные правки или соберетедругой версией компилятора.Также вам теперь будут понятны некоторые странности и причудливости кода в проектеOmniSample, которые сделаны для того, чтобы продемонстрировать ошибку в простых примерахдаже в режиме оптимизации кода.Авторы курса: Андрей Карпов (karpov@viva64.com), Евгений Рыжков (evg@viva64.com).Правообладателем курса "Уроки разработки 64-битных приложений на языке Си/Си++"является ООО "Системы программной верификации". Компания занимается разработкойпрограммного обеспечения в области анализа исходного кода программ. Сайт компании:http://www.viva64.com.Контактная информация: e-mail: support@viva64.com, 300027, г. Тула, а/я 1800.

×