Upload
computer-science-club
View
1.210
Download
1
Embed Size (px)
DESCRIPTION
Citation preview
Динамический анализ программ на C++Алексей Самсонов, Google
Екатеринбург, УрФУ, 29.04.2013
Команда (Google RU-MSK)
Константин Серебряный (TL)Дмитрий ВьюковТимур ИсходжановСергей МатвеевАлександр ПотапенкоАлексей СамсоновЕвгений Степанов
Содержание
Часть 1● Ошибки работы с памятью● AddressSanitizer
Часть 2● Гонки● ThreadSanitizer
В мире много кода на C и C++
● Операционные системы● Браузеры (Chromium, Firefox, Safari)● Виртуальные машины (Java)● Базы данных (MySQL)● Backend веб-сервисов (поиск)● Веб-серверы (Apache, nginx)
Ошибки работы с памятью в C/C++
● Кодеки и рендереры (ffmpeg, pdf)● Использование освобождённой памяти
(use-after-free)● Использование неинициализированной
памяти (uninitialized-memory-read)● Утечки памяти (memory leaks)● Неверный порядок
инициализации/уничтожения глобальных объектов
Ошибки работы с памятью в C/C++
● Выход за границы буфера (out-of-bound access)
● Использование освобождённой память (use-after-free)
● Использование неинициализированной памяти (uninitialized-memory-read)
● Утечки памяти (memory leaks)● Неверный порядок
инициализации/уничтожения глобальных объектов
Выход за границы буфера (стек)void authorize() { ... char username[128]; gets(username); // 128 bytes should be // enough for everyone. ...}
Использование освобожденной памяти~UserTable() { ... delete all_users; LOG() << "Deleted " << all_users->size() << " users!";}
Можно ли сделать этот код ещё хуже?
Использование освобожденной памяти~UserTable() { ... delete all_users; RunAction(all_users->action_on_delete);}
Всегда в моде: racy use-after-free
Поток 1
void DeleteEntries() { have_entries = false; delete entries;}
Поток 2
Entry *GetEntry(int i) { if (have_entries) return entries[i]; return 0;}
Инструменты для поиска ошибок
● Valgrind/Memcheck● DynamoRIO/Dr. Memory● Purify● Insure++● Mudflap● Electric Fence / Page Heap / Guard MallocПроблемы:● Замедление программы (в 10 и более раз)● Поиск ошибок только для динамически
выделенной памяти (heap)
Статический анализ программ
● Формальная верификация?● Clang Static Analyzer?
Проблемы:● Баланс между эффективностью и
полезностью● Баланс между false negative и false
positive
Есть ли в программе ошибка?
int array[10] = {...};
int main(int argc, char *argv[]) { return array[2 + argc];}
Пример отчёта об ошибке (1)int global_array[100] = {-1};int main(int argc, char **argv) { return global_array[argc + 100];}
==10538== ERROR: AddressSanitizer global-buffer-overflowREAD of size 4 at 0x000000415354 thread T0 #0 0x402481 in main a.cc:3<...>0x000000415354 is located 4 bytes to the right of global variable 'global_array' (0x4151c0) of size 400
Пример отчёта об ошибке (2)int main(int argc, char **argv) { int stack_array[100]; stack_array[1] = 0; return stack_array[argc + 100];}
==10589== ERROR: AddressSanitizer stack-buffer-overflowREAD of size 4 at 0x7f5620d981b4 thread T0 #0 0x4024e8 in main a.cc:4Address 0x7f5620d981b4 is located at offset 436 in frame <main> of T0's stack:
This frame has 1 object(s): [32, 432) 'stack_array'
Пример отчёта об ошибке (3)int main(int argc, char **argv) { int *array = new int[100]; int res = array[argc + 100]; delete [] array; return res;}==10565== ERROR: AddressSanitizer heap-buffer-overflowREAD of size 4 at 0x7fe4b0c76214 thread T0 #0 0x40246f in main a.cc:30x7fe4b0c76214 is located 4 bytes to the right of 400-byte region [0x7fe..., 0x7fe...)
allocated by thread T0 here: #0 0x402c36 in operator new[](unsigned long) #1 0x402422 in main a.cc:2
Пример отчёта об ошибке (4)int main(int argc, char **argv) { int *array = new int[100]; delete [] array; return array[argc];}==30226== ERROR: AddressSanitizer heap-use-after-freeREAD of size 4 at 0x7faa07fce084 thread T0 #0 0x40433c in main a.cc:40x7faa07fce084 is located 4 bytes inside of 400-byte regionfreed by thread T0 here: #0 0x4058fd in operator delete[](void*) #1 0x404303 in main a.cc:3previously allocated by thread T0 here: #0 0x405579 in operator new[](unsigned long) #1 0x4042f3 in main a.cc:2
Что такое AddressSanitizer?
● Компиляторная инструментация○ Проверка всех обращений к памяти○ Добавление редзон("redzones") вокруг
глобальных и локальных переменных● Runtime-библиотека
○ Аллокатор (выделение/освобождение памяти)○ Отслеживание всех потоков○ Печать отчётов об ошибках○ Обёртки функций из стандартной библиотеки
Теневой байт (shadow byte)
Если адреса всех переменных кратны 8, то у любых выровненных восьми байт есть 9 состояний:
07654321-1
Тень адресного пространства (*)
Приложение0xFFFFFFFF0x20000000
Тень0x1FFFFFFF0x04000000
Mprotect0x03FFFFF0x0000000
Инструментация доступа к памяти
int *a = ...;*a = ...;
char *shadow = a >> 3;if (*shadow != 0 && *shadow < (a&7) + 4) ReportError(a);*a = ...;
Инструментация стекаvoid foo() { char a[328]; <------------- CODE -------------> }
Инструментация стекаvoid foo() { char redzone1[32]; // 32-byte aligned char a[328]; char redzone2[24]; char redzone3[32]; int *shadow = (&rz1 >> 3); shadow[0] = 0xffffffff; // poison rz1 shadow[11] = 0xffffff00; // poison rz2 shadow[12] = 0xffffffff; // poison rz3 <------------- CODE -------------> shadow[0] = shadow[11] = shadow[12] = 0;}
Инструментация глобальных объектов
int global;
struct { int original; char redzone[60];} global; // 32-byte aligned
LLVM спешит на помощь!
Интеграция в LLVM
● Clang (или другой фронтенд) генерирует промежуточное представление программы;
● ASan добавляет инструментацию;● Бэкенд оптимизирует код и генерирует
объектный файл;● Исполняемый файл линкуется с runtime-
библиотекой.
Runtime-библиотека
● Аллокатор○ Добавление редзон вокруг блоков динамически
выделенной памяти○ Карантин для освобождённой памяти○ Хранение стека вызовов для каждой аллокации
● Печать отчётов об ошибках● Перехват библиотечных функций char bad_string[2] = {'h', 'i'}; int len = strlen(bad_string);
Производительность
● Замедление программы: 2x● Использование памяти: 1,5-3x● "Just works" для большинства тестов и
реальных программ● Незначительное замедление для GUI-
программ (Chromium+ASan работает без проблем)
Трофеи
1000+ найденных ошибок повсюду:● Chromium, Firefox, Safari● Сервисы Google● Perl, PHP, MySQL, ffmpeg, webrtc, vim● LLVM, GCC
Google заплатил $130000+ внешним исследователям за security-ошибки, найденные с помощью AddressSanitizer.
Что будет дальше?
● Поиск других видов ошибок (в ближайшее время: поиск утечек памяти)
● Поиск ошибок в системных и прекомпилированных библиотеках
● AddressSanitizer для ядра Linux● AddressSanitizer для Windows (нужен
компилятор).
Попробуйте AddressSanitizer!
● LLVM/Clang 3.1+clang++ -fsanitize=address a.cc● GCC 4.8+g++ -fsanitize=address a.cc● Платформы:Linux, Mac OS X, Android
code.google.com/p/address-sanitizer
Гонки (data races)
Что такое гонка?
Гонка происходит, когда два потока одновременно обращаются к одной и той же переменной, и хотя бы одно из обращений является записью.
Если в программе на C++ могут возникнуть гонки, её поведение не определено.
Компилятор имеет право считать, что в программе нет гонок.
Что напечатает этот код?
Поток 1
cout << x << "\n";x = 1;cout << y << "\n";
Поток 2
cout << y << "\n";y = 1;cout << x << "\n";
int x = 0, y = 0;
"Безвредные" гонки
● Синхронизация замедляет программу.● Может возникнуть желание допустить
гонки на "не очень важных" данных.● Не нужно этого делать. В C++ не может
быть "безвредных" (benign) гонок.
H-J. Boehm "How to miscompile programs with "benign" data races"D. Vyukov "Benign data races: what could possibly go wrong?"
Пример "безвредной гонки"long long get_value() { return value;}
void set_value( long long x) { value = x;}
Является ли 64-битное чтение / запись атомарной операцией?
Пример "безвредной гонки" (2)
...int my_counter = counter; // read globalif (my_counter > my_old_counter) { my_action = print_stats;}...
if (my_counter > my_old_counter) { run_action(my_action);}
Пример "безвредной гонки" (3)my_action = launch_nuclear_rocket;...int my_counter = counter; // read globalif (my_counter > my_old_counter) { my_action = print_stats;}...my_counter = counter;if (my_counter > my_old_counter) { run_action(my_action);}
А как же настоящие гонки?
Метод-лжец// Returns the user with a given IDUser& getUser(int id) { MutexLock(&mutex); CHECK(0 <= id && id < users.size()); return users[id];}
Метод-лжец// Returns the user with a given IDUser& getUser(int id) { MutexLock(&mutex); CHECK(0 <= id && id < users.size()); return users[id];}
Что не так с этим кодом?class AbstractAction { public: AbstractAction(Executor *executor) { executor->add(this); ... } ...
}
Что не так с этим кодом?class AbstractAction { public: AbstractAction(Executor *executor) { executor->add(this); ... } ... virtual void Run() = 0;}
Гонка на vptrclass AbstractAction { public: AbstractAction(Executor *executor) { executor->add(this); // переключение контекста } ... virtual void Run() = 0;}
Pure virtual call!
ThreadSanitizer спешит на помощьWARNING: ThreadSanitizer: data race on vptr (ctor/dtor vs virtual call) Read of size 8 at 0x7fff103327f0 by thread T3: #0 ExecutorThread::RunAction() executor.cc:1112 #1 ExecutorThread::Start() executor.cc:1283
Previous write of size 8 at 0x7fff103327f0 by main thread: #0 MyAction::MyAction()vptr_race.cc:48 #1 main vptr_race.cc:59
Location is stack of main thread.
Что такое ThreadSanitizer?
● Компиляторная инструментация○ Проверка всех обращений к памяти○ Отслеживание событий: вход/выход в функцию,
обновление vptr, атомарные операции● Runtime-библиотека
○ Аллокатор (выделение/освобождение памяти)○ Отслеживание всех потоков○ Печать отчётов об ошибках○ Обёртки функций из стандартной библиотеки○ Поддержка синхронизационных примитивов
Инструментация (1)int *p = ...;*p = 42;
int *p = ...;__tsan_write4(p);*p = 42;
Инструментация (2)void foo() { ...}
void foo() { __tsan_func_entry( __builtin_return_address(0)); ... __tsan_func_exit();}
Инструментация (3)A::~A() { // this->vptr = A::vptr;}
A::~A() { __tsan_vptr_update(&this->vptr, A::vptr); // this->vptr = A::vptr;}
Runtime-библиотека
● Проверка всех доступов в память● Выделение/освобождение памяти● Поддержка синхронизации (мьютексы,
атомарные операции)● Перехват функций работы с потоками
(libpthread)● Перехват функций из стандартной
библиотеки● Печать отчётов об ошибках
Абстрактное время потока
Увеличивается после каждого события:
void foo() { // time = 1 Lock(&mutex); // time = 2 x = 1; // time = 3 Unlock(&mutex); // time = 4} // time = 5
Векторные часы (vector clock)
VC[1..N], где VC[i] - время i-го потока.
Векторные часы есть у каждого потока:VC[1..N], где VC[i] - время i-го потока в момент последней синхронизации с ним.
Векторные часы есть у каждого мьютекса:VC[1..N], VC[i] - время i-го потока, когда он в последний раз отпустил мьютекс.
Обновление векторных часов
Доступ к памяти в потоке T:
T->VC[T->id]++;
Обновление векторных часов (2)
Взятие мьютекса M в потоке T:для всех i:T->VC[i] = max(T->VC[i], M->VC[i]);
Освобождение мьютекса M в потоке T:для всех i:M->VC[i] = max(M->VC[i], T->VC[i]);
Как кодировать обращение к памяти?
Обращение к выровненным 8 байтам адресного пространства кодируется через:● ID потока (TID)● Время этого потока (TS)● позиция (0..7) и размер (1,2,4,8)
обращения● является ли доступ чтением или
записью?
Вся эта информация помещается в 8 байт.
Пример обращений к памяти
Приложение Тень
Запись из потока T1
Приложение Тень
T1
TS1
W
0:2
Чтение из потока T2
Приложение Тень
T1
TS1
W
0:2
T2
TS2
4:4
R
Чтение из потока T3
Приложение Тень
T1
TS1
W
0:2
T2
TS2
4:4
R
T3
TS3
0:4
R
Гонка?
Тень
T1
TS1
W
0:2
T2
TS2
4:4
R
T3
TS3
0:4
R
● Доступы пересекаются?
● Разные потоки?● Хотя бы одно из
обращений - запись?
● Есть ли синхронизация?
Гонка, если T3->VC[T1] < TS1
Как узнать стек вызовов для каждого обращения к памяти?
● Циклическая очередь всех событий в каждом потоке:○ доступ к памяти○ вход/выход из функции○ взятие/освобождение мьютекса
● "Повторение" событий перед печатью отчёта:○ Через некоторое время события "забываются"○ Неограниченный размер стека вызовов в отчёте○ Отчёт содержит множество мьютексов, взятых в
каждом потоке.
Результаты
● Замедление программы: 4-10x (в 10 раз быстрее аналогов).
● Использование памяти: 5-8x
● 600+ найденных ошибок● Работает для больших серверных
приложений
Что будет дальше?
● Портирование на другие системы (сейчас - только 64-битный Linux).
● Поиск ошибок в системных и прекомпилированных библиотеках
● Больше оптимизаций, больше пользователей
● Поиск гонок в коде на Java
Попробуйте ThreadSanitizer!
● LLVM/Clang 3.2+clang++ -fsanitize=thread a.cc● GCC 4.8+g++ -fsanitize=thread a.cc● Gogo build -race
code.google.com/p/thread-sanitizer