• Like
Урок 24. Фантомные ошибки
Upcoming SlideShare
Loading in...5
×

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

  • 178 views
Uploaded on

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

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

  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
    Be the first to like this
No Downloads

Views

Total Views
178
On Slideshare
0
From Embeds
0
Number of Embeds
0

Actions

Shares
Downloads
0
Comments
0
Likes
0

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 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. вот с 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. }Будем считать, что так код выглядит более красиво. Согласитесь, что функционально он осталсяпрежним. А вот результат будет существенным - произойдет аварийное завершение программы.Рассмотрим сгенерированный компилятором код: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. Но мир хрупок. Достаточно немного усложнить код и он станет неверен: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. 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. Результат - аварийное завершение программы, при выходе за границы массива: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.