Автор: Эли Дэ Брувэр (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, и как они могут вам помочь в написании высококачественных приложений?