Автор: Эли Дэ Брувэр (Elie De Brauwer)
До сих пор я представлял вам некий код и инструкции, как его скомпилировать и выполнить. Вероятно, до этого момента вам нужны были только редактор (emacs, vi, …) и компилятор (gcc). Тем не менее, существует ещё множество других утилит, облегчающих разработку кода (ведь разработка это не только набор исходного когда, а также компиляция, тестирование, и прочее). Есть даже IDE (интегрированные среды разработки), комбинирующие некоторые из этих утилит в красивый графический интерфейс (например CDT для Eclipse, kdevelop, Code::blocks, anjuta, и другие), но, по моему мнению, начинающий программист должен иметь представление о том, как они работают, изнутри, прежде чем он начнёт использовать горячие клавиши. Несмотря на существование большого количества утилит, покрывающих множество категорий, в этой статье мы сфокусируемся на поиске и устранении ошибок в коде/приложении.
strace and ltrace
strace - один из моих лучших друзей. ltrace - также отличный инструмент, но я нечасто им пользуюсь. Вы можете установить их, набрав:
sudo apt-get install strace ltrace
Что же они делают? Strace перехватывает системные вызовы процесса. Системный вызов - это процедура, которая переносит управление в режим ядра для функций, выполняющихся в пространстве пользователя. Например, инкремент переменной транслируется в простую команду ассемблера, но когда вам нужно обратиться к ресурсам системы, это всегда приводит в режим ядра. Прочитав 'man 2 syscalls', вы получите список системных вызовов, поддерживаемых ядром. Итак, почему же заглядывать сюда - хорошая идея? Если знать какие системные вызовы делает ваше приложение, можно пройтись по его логической цепочке, и это хорошо, потому что не является вторжением в программу, и вы можете сделать это с любым исполняемым файлом в системе. В качестве примера, я рассмотрю вывод wget, устанавливаемой по команде:
sudo apt-get install wget
wget - это приложение, которое загружает данные по URL-адресу из интернета и записывает их на диск. Если мы посмотрим на вывод:
strace wget -q http://www.google.com
который показан на Схеме 1, то увидим во время выполнения несколько интересных частей:
execve("/usr/bin/wget", ["wget", "-q", "http://www.google.com"], [/* 38 vars */]) = 0 ... stat64("/etc/wgetrc", {st_mode=S_IFREG|0644, st_size=4221, ...}) = 0 open("/etc/wgetrc", O_RDONLY|O_LARGEFILE) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=4221, ...}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7ad2000 read(3, "###\n### Sample Wget initializati"..., 4096) = 4096 read(3, "on:\n#backup_converted = off\n\n# T"..., 4096) = 125 read(3, "", 4096) = 0 close(3) = 0 ... stat64("/home/edb/.wgetrc", 0xbfe57a48) = -1 ENOENT (No such file or directory) ...
Здесь мы видим, что всё начинается с вызова 'execve()' (посмотрите man execve; и для любого системного вызова - первое слово в строке напечатанной strace), который загружает исполняемый файл. Чуть позже приложение проверяет, существует ли файл конфигурации '/etc/wgetrc/'. Он существует и он читается. Далее мы видим, что процесс пытается открыть '.wgetrc' в моей домашней директории, но этот файл не существует, поэтому и не открывается. Следующий пример (Схема 2) показывает, что '/etc/resolv.conf' в данный момент открыт, а также открыт сокет для DNS сервера, для того чтобы определить адрес по моему запросу:
stat64("/etc/resolv.conf", {st_mode=S_IFREG|0644, st_size=88, ...}) = 0 socket(PF_INET, SOCK_DGRAM, IPPROTO_IP) = 4 connect(4, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("195.130.131.5")}, 28) = 0 fcntl64(4, F_GETFL) = 0x2 (flags O_RDWR) fcntl64(4, F_SETFL, O_RDWR|O_NONBLOCK) = 0 gettimeofday({1234091526, 549043}, NULL) = 0 poll([{fd=4, events=POLLOUT, revents=POLLOUT}], 1, 0) = 1 send(4, "\372\312\1\0\0\1\0\0\0\0\0\0\3www\6google\2be\0\0\1\0\1"..., 31, MSG_NOSIGNAL) = 31 poll([{fd=4, events=POLLIN, revents=POLLIN}], 1, 5000) = 1 ioctl(4, FIONREAD, [367]) = 0 recvfrom(4, "\372\312\201\200\0\1\0\6\0\7\0\7\3www\6google\2be\0\0\1"..., 1024, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("195.130.131.5")}, [16]) = 367 close(4)
Разве это не прекрасно? Мы изучили внутреннее устройство приложения, не взглянув ни на одну строчку кода; тут же мы узнали где оно хранит свои файлы конфигурации, один из которых не существует, и как оно перевело запись DNS в ip-адрес. ltrace работает подобным образом, но, вместо трассировки системных вызовов, показывает, какие функции вызывались и какие из них находятся в динамически связанных библиотеках (Схема 3):
edb@lapedb:~$ whereis wget wget: /usr/bin/wget /usr/share/man/man1/wget.1.gz edb@lapedb:~$ ldd /usr/bin/wget linux-gate.so.1 => (0xb7f12000) libdl.so.2 => /lib/tls/i686/cmov/libdl.so.2 (0xb7ed8000) librt.so.1 => /lib/tls/i686/cmov/librt.so.1 (0xb7ecf000) libssl.so.0.9.8 => /usr/lib/i686/cmov/libssl.so.0.9.8 (0xb7e88000) libcrypto.so.0.9.8 => /usr/lib/i686/cmov/libcrypto.so.0.9.8 (0xb7d3c000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7bde000) /lib/ld-linux.so.2 (0xb7ef8000) libpthread.so.0 => /lib/tls/i686/cmov/libpthread.so.0 (0xb7bc5000) libz.so.1 => /usr/lib/libz.so.1 (0xb7baf000)
ldd говорит нам, что wget использует среди прочих libssl (безопасные соединения), libpthread (для создания потоков), libz (сжатие), и libc. Libc по существу является основой вашей системы. Она реализует основные функции С, такие как printf(), malloc(), и free(), часто связывая их с системными вызовами (например, printf() с write()). Теперь ltrace расскажет нам, где наше приложение использует функции, предоставляемые библиотеками. Итак, если мы рассмотрим вывод:
ltrace wget -q http://www.google.com
мы можем найти следующий кусок (некоторые пробелы пропущены): strlen(«www.google.com») = 14*dcgettext(0, 0x8075c8a, 5, 0x804e66d, 0xbf8e1761) = 0x8075c8a*getaddrinfo(«www.google.com», NULL, 0xbf8e1780, 0xbf8e17b4) = 0*calloc(1, 20) = 0x909c1e0*malloc(96) = 0x909c1f8*freeaddrinfo(0x909c100) = <void> Это то, что ltrace видит на этапе разрешения имени (DNS), рассмотренном нами ранее с помощью strace. Все сетевые коммуникации скрыты за простым вызовом 'getaddrinfo()'. Я надеюсь, что теперь вы оценили значение strace и ltrace. Они позволяют вам изучить внутреннее устройство исполняемых файлов - без дополнительных усилий - с одной лишь разницей: выполнение происходит немного медленнее, это позволяет вам понять, что делает приложение, и где что-то пошло не так.
Valgrind
Valgrind можно установить, набрав:
sudo apt-get install valgrind
Это набор утилит, которые выполняют продвинутую проверку приложений. Для дополнительной информации о доступных утилитах зайдите на сайт http://www.valgrind.org. В этой статье я рассмотрю только самую используемую утилиту под названием 'memcheck'. Эта утилита переопределяет вызовы libc, которые занимаются обработкой памяти. Получается система учёта использования ресурсов - вся ли память (выделенная динамически) возвращена обратно в систему, и вся ли выделенная память по-прежнему доступна?
Листинг 1:
01. #include <stdio.h> 02. #include <stdlib.h> 03. void leak() 04. { 05. char * ptr = malloc(10); 06. printf("malloc(10) указывает на: %p\n",ptr); 07. } 08. int main() 09. { 10. int i=0; 11. for(i=0;i<10;i++) 12. { 13. leak(); 14. } 15. char * ptr = malloc(15); 16. printf("malloc(15) в main: %p\n",ptr); 17. while(1){} 18. return 0; 19.} Листинг 1: leak.c
Посмотрите на листинг 1. Это пример плохого кода. Происходит вызов функции leak() (строки 3-7) 10 раз, которая выделяет 10 байтов и не освобождает их. Затем выделяется некоторое количество памяти в функции main, и выполнение переходит в бесконечный цикл. Во-первых, я хочу, чтобы перед запуском кода вы заменили цикл for на while(1), и malloc(10) на malloc(1000). Запустив приложение, вы увидите что произойдёт с вашей системой. Ваша физическая память заполнится, затем будет заполнен своп, и, в конечном счёте, oom_killer (служба завершения процессов, пожирающих всю память) закроет раздобревший процесс. Такие вещи являются разрушительными для системы и для её производительности. Вы только что наблюдали эффект утечки памяти. Проблемная особенность динамического запроса памяти - память всегда нужно возвращать обратно! Это пример утечки памяти «в ускоренном воспроизведении». Некоторые приложения, которые теряют несколько байт в час, могут идеально работать годами - прежде чем всё упадет к чёртовой бабушке. Вот почему valgrind очень полезен. Вот вывод Листинга 1 на моей системе после компиляции:
gcc -Wall -g leak.c -o memleak
Вывод на Схеме 4:
edb@lapedb:~/fullcircle/c-6$ valgrind --leak-check=full --show-reachable=yes ./memleak ==7257== Memcheck, a memory error detector. ==7257== Copyright (C) 2002-2007, and GNU GPL'd, by Julian Seward et al. ==7257== Using LibVEX rev 1854, a library for dynamic binary translation. ==7257== Copyright (C) 2004-2007, and GNU GPL'd, by OpenWorks LLP. ==7257== Using valgrind-3.3.1-Debian, a dynamic binary instrumentation framework. ==7257== Copyright (C) 2000-2007, and GNU GPL'd, by Julian Seward et al. ==7257== For more details, rerun with: -v ==7257== malloc(10) now points to: 0x41a2028 malloc(10) now points to: 0x41a2068 malloc(10) now points to: 0x41a20a8 malloc(10) now points to: 0x41a20e8 malloc(10) now points to: 0x41a2128 malloc(10) now points to: 0x41a2168 malloc(10) now points to: 0x41a21a8 malloc(10) now points to: 0x41a21e8 malloc(10) now points to: 0x41a2228 malloc(10) now points to: 0x41a2268 malloc(15) in main: 0x41a22a8 ^C==7257== ==7257== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 11 from 1) ==7257== malloc/free: in use at exit: 115 bytes in 11 blocks. ==7257== malloc/free: 11 allocs, 0 frees, 115 bytes allocated. ==7257== For counts of detected errors, rerun with: -v ==7257== searching for pointers to 11 not-freed blocks. ==7257== checked 52,132 bytes. ==7257== ==7257== 15 bytes in 1 blocks are still reachable in loss record 1 of 2 ==7257== at 0x4025D2E: malloc (vg_replace_malloc.c:207) ==7257== by 0x8048459: main (memleak.c:15) ==7257== ==7257== 100 bytes in 10 blocks are definitely lost in loss record 2 of 2 ==7257== at 0x4025D2E: malloc (vg_replace_malloc.c:207) ==7257== by 0x8048405: leak (memleak.c:5) ==7257== by 0x8048443: main (memleak.c:13) ==7257== ==7257== LEAK SUMMARY: ==7257== definitely lost: 100 bytes in 10 blocks. ==7257== possibly lost: 0 bytes in 0 blocks. ==7257== still reachable: 15 bytes in 1 blocks. ==7257== suppressed: 0 bytes in 0 blocks.
Когда я прерываю цикл while(1) нажав ctrl+c, он мне сообщает сколько вызовов malloc() я сделал, сколько памяти я получил, и сколько вернул обратно. В итоге делается вывод, что я потерял 100 байт памяти в 10 блоках. Это значит, что я запрашивал память, которая теперь мне недоступна, потому что у меня нет на неё указателя (в выводе: «definitely lost”), а также, что я получил 15 байт в одном блоке, который, на момент завершения, всё ещё могу освободить, потому что у меня есть на него указатель. Вот почему я написал цикл while(1). Если бы я этого не сделал, valgrind сообщил бы, что я потерял 115 байт в 11 блоках (проверьте это!), потому что valgrind ведёт учёт того, что в действительности произошло; он не смотрит в будущее для того, чтобы узнать, что может произойти в системе. Ещё одна вещь, о которой стоит упомянуть: я говорил, что cкомпилировал код с ключом »-g«, который добавляет отладочную информацию в исполняемый файл. Вот откуда valgrind знает, в каком файле и на какой строке произошла ошибка. Если скомпилировать следующим образом:
gcc -Wall leak.c -o memleak
то вывод будет выглядеть так:
==7339== 100 bytes in 10 blocks are definitely lost in loss record 2 of 2* ==7339== at 0x4025D2E: malloc (vg_replace_malloc.c:207)* ==7339== by 0x8048405: leak (in /home/edb/fullcircle/c-6/memleak)* ==7339== by 0x8048443: main (in /home/edb/fullcircle/c-6/memleak)
Он по-прежнему говорит нам, что происходит утечка памяти, но уже не сообщает, в каком файле и в какой строке что-то идёт не так. Итак, хорошая новость - valgrind сообщает нам, есть утечки памяти или нет. Плохая новость - нам нужен исполняемый файл с отладочной информацией, если мы хотим локализовать утечку. Мы можем перекомпилировать исполняемый файл для поиска и устранения неисправностей - для этого нам нужен исходный код!
Выводы
В этой статье я рассказал об утилитах, позволяющих легко найти и устранить неисправности в исполняемых файлах, без необходимости иметь их исходники или дополнительные знания о файлах. В следующий раз мы попытаемся немножко углубиться и затем посмотрим на настоящий отладчик.
Упражнения:
- vmstat - утилита, печатающая отчёт об использовании виртуальной памяти; используя strace, определите, какие файлы из /proc/ используются при генерации вывода.
- Элемент ненумерованного спискаПовторите пример с ltrace/strace и wget, но с неправильным URL. Чей вывод позволит быстрее определить, что выведена неправильная DNS-запись?
- Прочтите man-страницу strace. Проследует ли strace автоматически в дочерний процесс? Какие меры вы должны принять при трассировке многопоточных приложений?
- Трассирует ли автоматически valgrind дочерние процессы?
- Попробуйте запустить valgrind с какой-нибудь из ваших любимых утилит командной строки. И проверьте, правильно ли она управляет памятью.
- Какие ещё утилиты являются частью набора valgrind, и как они могут вам помочь в написании высококачественных приложений?