В системах со встроенной в ядро технологией DTrace (Solaris, FreeBSD, MacOS X, QNX) можно получить полную информацию о том, что происходит с приложением при его работе в ОС. DTrace контролирует основные действия трассируемого приложения – выполнение процессорных команд, чтение из памяти и запись в нее – и при этом динамически модифицирует объект исследования: выключенные датчики не оказывают никакого влияния на производительность и включаются только при явном указании этого.
В отличие от утилиты truss, позволяющей трассировать только системные вызовы и сигналы, DTrace позволяет получить информацию почти обо всех составляющих работы системы, а использование дополнительных статистических утилит можно указать в скрипте на языке D. Truss для сбора информации пользуется файловой системой proc, предназначенной для стандартных средств отладки, поэтому в большинстве случаев трассируемый процесс останавливается, фиксируется нужная информация о его состоянии и процесс запускается заново (чего в DTrace при нормальной работе не наблюдается, хотя методы трассировки могут быть как статическими, так и динамическими).
Основные компоненты DTrace:
1. Потребители (consumers) – это процессы, обращающиеся к корневому модулю DTrace, передающие ему при этом спецификации трассировки и обрабатывающие данные, полученные в ее процессе. Для этого используется интерфейс, предоставляемый библиотекой libdtrace, а данные между потребителем и ядром (корневой модуль находится в ядре) передаются с помощью вызова ioctl для псевдоустройства dtrace.
Канонический потребитель – одноименная утилита, являющаяся также драйвером для компилятора языка D (драйвер – утилита, управляющая процессом компиляции, например, сс для языка С). Сам компилятор находится в составе библиотеки libdtrace.
2. Корневой модуль DTrace.
Он находится в ядре ОС и необходим для доступа к средствам модификации кода, а также буферизации и обработки информации, получаемой от датчиков. Но модуль DTrace не добавляет датчики в код – это задача провайдеров.
3. Провайдеры.
По запросу модуля DTrace провайдер определяет в системе точки, куда могут быть установлены датчики, и для каждой найденной точки осуществляет обратный вызов модуля, чтобы создать датчик. Провайдеры являются модулями ядра.
4. Датчики (probes).
Каждый датчик определяется провайдером, модулем (в данном контексте – программа, которой он принадлежит), функцией и семантическим именем.
Общая архитектура DTrace
После создания датчика модуль DTrace возвращает его ID провайдеру, но код при этом еще не модифицируется. Затем DTracе создает блок управления датчиком (ECB – enabling control block), в котором определяется, что и при каких условиях будет выполнено – т.е. действия и предикаты. Если предикат есть, но его условие не выполняется, DTrace переходит к следующему ECB. Если действие предполагает запись каких-то данных, то они будут сохранены в специальном буфере, привязанному к создавшему блок потребителю (действия не могут содержать явную запись в память ядра и изменение регистров; это можно сделать косвенно, если способ явно указан и у пользователя есть права на выполнение таких действий – например, остановить текущий процесс). После этого провайдеру поступает команда о необходимости включения датчика, а если ECB у датчика уже был, новый блок становится последним из имеющихся у датчика блоков управления. Именно в этот момент провайдер модифицирует систему так, чтобы при срабатывании датчика управление переходило к модулю DTrace и первым аргументом был ID датчика.
После срабатывания датчика прерывания на процессоре запрещаются, чтобы дать возможность DTrace выполнить действия, определенные в каждом ECB сработавшего датчика. Когда управление снова переходит к провайдеру, прерывания разрешаются.
ECB, предикаты и действия
Пример модификации кода провайдером fbt (платформы SPARC и AMD64)
В одной вкладке терминала запускается отладчик модулей ядра mdb и дизассемблируется функция – например, zfs_read (), после выхода из mdb при помощи Ctrl-d в другой вкладке включаются датчики на модуле zfs:
#dtrace -m 'zfs { trace(execname);}'
В процессе работы DTrace дизассемблирование повторяется.
SPARC
До включения датчиков:
# mdb -k
Loading modules: [ unix krtld genunix specfs dtrace ufs sd pcipsy ip
hook neti sctp arp usba fctl nca zfs random ptm cpc sppp
crypto fcip nfs audiosup ipc ]
> zfs_read::dis -n 5
zfs_read: save %sp, -0xb0, %sp
zfs_read 4: ldx [%i0 0x10], %l1
zfs_read 8: ldx [%l1], %l3
zfs_read 0xc: add %l3, 0x54, %l2
zfs_read 0x10: ldx [%l3 0x10], %l4
zfs_read 0x14: mov %l2, %o0
Во время работы DTrace:
# mdb -k
Loading modules: [ unix krtld genunix specfs dtrace ufs sd pcipsy ip
hook neti sctp arp usba fctl nca zfs random ptm cpc sppp
crypto fcip nfs audiosup ipc ]
> zfs_read::dis -n 5
zfs_read: ba,a -0x24276c
zfs_read 4: ldx [%i0 0x10], %l1
zfs_read 8: ldx [%l1], %l3
zfs_read 0xc: add %l3, 0x54, %l2
zfs_read 0x10: ldx [%l3 0x10], %l4
zfs_read 0x14: mov %l2, %o0
AMD 64
До включения датчиков:
# mdb -k
Loading modules: [ unix genunix specfs dtrace cpu.generic uppc
pcplusmp scsi_vhci zfs random ip hook neti sctp arp usba fctl md lofs
mpt sppp ptm ipc fcip fcp cpc crypto logindmux
nsctl ii sdbc ufs rdc nsmb sv ]
> zfs_read::dis -n 5
zfs_read: pushq %rbp
zfs_read+1: movq %rsp,%rbp
zfs_read+4: subq $0x58,%rsp
zfs_read+8: pushq %rbx
zfs_read+9: pushq %r12
zfs_read+0xb: pushq %r13
Во время работы DTrace:
# mdb -k
zLoading modules: [ unixfs genunix specfs dtrace cpu.generic uppc
pcplusmp scsi_vhci zfs random ip hook neti sctp arp usba fctl md lofs
mpt sppp ptm ipc fcip fcp cpc crypto logindmux
nsctl ii sdbc ufs rdc nsmb sv ]
> zfs_read::dis -n 5
zfs_read: int $0x3
zfs_read+1: movq %rsp,%rbp
zfs_read+4: subq $0x58,%rsp
zfs_read+8: pushq %rbx
zfs_read+9: pushq %r12
zfs_read+0xb: pushq %r13
На платформе AMD64 вызывается программное прерывание – int, а на SPARC выполняется команда безусловного перехода ba,a +offset (offset – ее операнд). В результате прерывания управление перехватывается обработчиком прерываний и передается DTrace, а выполнение команды перехода приводит к исполнению послдующих команд с произвольно заданного адреса (DTrace - dt=0x90ac).
Модификация таблицы системных вызовов провайдером syscall
# mdb -k
Loading modules: [ unix genunix specfs dtrace cpu.generic uppc pcplusmp
scsi_vhci zfs random ip hook neti sctp arp usba fctl md
lofs mpt sppp ptm ipc fcip fcp cpc crypto logindmux
nsctl ii sdbc ufs rdc nsmb sv ]
> ::sizeof struct sysent
sizeof (struct sysent) = 0x20
> sysent+1*0x20::array struct sysent 1 |::print struct sysent
{
sy_narg = '\001'
sy_flags = 0
sy_call = 0
sy_lock = 0
sy_callc = rexit
}
В другой вкладке включается датчик для вызова rexit:
#dtrace -n 'syscall::rexit:entry'
После включения:
> sysent+1*0x20::array struct sysent 1 |::print struct sysent
{
sy_narg = '\001'
sy_flags = 0
sy_call = 0
sy_lock = 0
sy_callc = dtrace_systrace_syscall
}
Замена системного вызова на dtrace_systrace_syscall позволяет передать управление DTrace, снимающему необходимую для трассировки информацию до и после системного вызова, передав управление системного вызову (в этом примере – rexit) в нужный момент – это метод работы провайдера syscall.
В DTrace имеется виртуальная машина, имеющая собственный набор команд RISC, не зависящий от архитектуры, - DIF (D Intermediate Format). D-скрипты транслируются в DIF и эмулируются в ядре при срабатывании датчика. Это требуется для безопасной обработки возможных ошибок, способных нарушить работу системы.
Используя ключ -S команды dtrace, можно посмотреть на генерируемые DIF-объекты (DIFO) при выполнении D-скрипта:
difo.d
syscall::open:entry
/ execname == "ls" /
{
self->follow = 1;
printf( "open (%s)\n ", copyinstr(arg0) );
{
}
fbt:::
/self->follow/
}
syscall::open:return
/self->follow && errno == 0/
{
self->follow = 0;
}
syscall::open:return
/self->follow && errno != 0/
{
self->follow = 0;
printf( "ERROR" );
exit(0);
}
# dtrace -S -s difo.d
DIFO 0x8b2860 returns D type (integer) (size 4)
OFF OPCODE INSTRUCTION
00: 29011801 ldgs DT_VAR(280), %r1 ! DT_VAR(280) = "execname"
01: 26000102 sets DT_STRING[1], %r2 ! "ls"
02: 27010200 scmp %r1, %r2
03: 12000006 be 6
04: 0e000001 mov %r0, %r1
05: 11000007 ba 7
06: 25000001 setx DT_INTEGER[0], %r1 ! 0x1
07: 23000001 ret %r1
NAME ID KND SCP FLAG TYPE
execname 118 scl glb r string (unknown) by ref (size 256)
DIFO 0x8b5ff0 returns D type (integer) (size 4)
OFF OPCODE INSTRUCTION
00: 25000001 setx DT_INTEGER[0], %r1 ! 0x1
01: 2d050001 stts %r1, DT_VAR(1280) ! DT_VAR(1280) = "follow"
02: 23000001 ret %r1
NAME ID KND SCP FLAG TYPE
follow 500 scl tls w D type (integer) (size 4)
DIFO 0x8aa100 returns string (unknown) by ref (size 256)
OFF OPCODE INSTRUCTION
00: 29010601 ldgs DT_VAR(262), %r1 ! DT_VAR(262) = "arg0"
01: 33000000 flushts
02: 25000002 setx DT_INTEGER[0], %r2 ! 0x0
03: 04010201 sll %r1, %r2, %r1
04: 05010201 srl %r1, %r2, %r1
05: 25000102 setx DT_INTEGER[1], %r2 ! 0x8
06: 31000201 pushtv DT_TYPE(0), %r2, %r1 ! DT_TYPE(0) = D type
07: 2f000901 call DIF_SUBR(9), %r1 ! copyinstr
08: 23000001 ret %r1
NAME ID KND SCP FLAG TYPE
arg0 106 scl glb r D type (integer) (size 
DIFO 0x8b9920 returns D type (integer) (size 4)
OFF OPCODE INSTRUCTION
00: 2c050001 ldts DT_VAR(1280), %r1 ! DT_VAR(1280) = "follow"
01: 23000001 ret %r1
NAME ID KND SCP FLAG TYPE
follow 500 scl tls r D type (integer) (size 4)
DIFO 0x8bb520 returns D type (integer) (size 4)
OFF OPCODE INSTRUCTION
00: 2c050001 ldts DT_VAR(1280), %r1 ! DT_VAR(1280) = "follow"
01: 10010000 tst %r1
02: 1200000e be 14
03: 29012001 ldgs DT_VAR(288), %r1 ! DT_VAR(288) = "errno"
04: 25000002 setx DT_INTEGER[0], %r2 ! 0x0
05: 0f010200 cmp %r1, %r2
06: 12000009 be 9
07: 0e000001 mov %r0, %r1
08: 1100000a ba 10
09: 25000101 setx DT_INTEGER[1], %r1 ! 0x1
10: 10010000 tst %r1
11: 1200000e be 14
12: 25000101 setx DT_INTEGER[1], %r1 ! 0x1
13: 1100000f ba 15
14: 0e000001 mov %r0, %r1
15: 23000001 ret %r1
NAME ID KND SCP FLAG TYPE
follow 500 scl tls r D type (integer) (size 4)
errno 120 scl glb r D type (integer) (size 4)
DIFO 0x897ac0 returns D type (integer) (size 4)
OFF OPCODE INSTRUCTION
00: 25000001 setx DT_INTEGER[0], %r1 ! 0x0
01: 2d050001 stts %r1, DT_VAR(1280) ! DT_VAR(1280) = "follow"
02: 23000001 ret %r1
NAME ID KND SCP FLAG TYPE
follow 500 scl tls w D type (integer) (size 4)
DIFO 0x8c7510 returns D type (integer) (size 4)
OFF OPCODE INSTRUCTION
00: 2c050001 ldts DT_VAR(1280), %r1 ! DT_VAR(1280) = "follow"
01: 10010000 tst %r1
02: 1200000e be 14
03: 29012001 ldgs DT_VAR(288), %r1 ! DT_VAR(288) = "errno"
04: 25000002 setx DT_INTEGER[0], %r2 ! 0x0
05: 0f010200 cmp %r1, %r2
06: 13000009 bne 9
07: 0e000001 mov %r0, %r1
08: 1100000a ba 10
09: 25000101 setx DT_INTEGER[1], %r1 ! 0x1
10: 10010000 tst %r1
11: 1200000e be 14
12: 25000101 setx DT_INTEGER[1], %r1 ! 0x1
13: 1100000f ba 15
14: 0e000001 mov %r0, %r1
15: 23000001 ret %r1
NAME ID KND SCP FLAG TYPE
follow 500 scl tls r D type (integer) (size 4)
errno 120 scl glb r D type (integer) (size 4)
DIFO 0x8ce740 returns D type (integer) (size 4)
OFF OPCODE INSTRUCTION
00: 25000001 setx DT_INTEGER[0], %r1 ! 0x0
01: 2d050001 stts %r1, DT_VAR(1280) ! DT_VAR(1280) = "follow"
02: 23000001 ret %r1
NAME ID KND SCP FLAG TYPE
follow 500 scl tls w D type (integer) (size 4)
DIFO 0x75a6f0 returns D type (integer) (size 4)
OFF OPCODE INSTRUCTION
00: 25000001 setx DT_INTEGER[0], %r1 ! 0x0
01: 23000001 ret %r1
dtrace: script 'difo.d' matched 63013 probes
<…>
Провайдер fbt (function boundary tracing)
Провайдер трассировки границ функции создает датчики для момента входа во все функции и выхода из всех функций ядра и механизм его реализации привязан к конкретной архитектуре набора команд (см. сравнение SPARC и AMD64).
Он позволяет наблюдать за тем, что происходит непосредственно в ядре. Скрипт write.d показывает, какую последовательность вызовов функций ядра генерирует системный вызов write; вывод ограничен только вызовами, происходящими в том же потоке команд, что и системный вызов (для указания ограничения используется thread-local переменная self->follow):
syscall::write:entry
{
self->follow = 1;
}
fbt:::
/self->follow/
{
}
syscall::write:return
/self->follow/
{
self->follow = 0;
exit(0);
}
# dtrace -s write.d
dtrace: script 'write.d' matched 63248 probes
CPU ID FUNCTION:NAME
0 34187 write32:entry
0 26927 write:entry
0 26477 getf:entry
0 22938 set_active_fd:entry
0 22939 set_active_fd:return
0 26478 getf:return
0 27385 nbl_need_check:entry
Провайдер pid
С его помощью можно контролировать входы и выходы почти из всех функций любого приложения без дополнительных средств и датчики для этого провайдера создаются динамически, поэтому для просмотра их списка нужно указать приложение, которое будет трассироваться:
#dtrace -P pid'$target' -c dtrace -l
Например, с помощью этого провайдера можно увидеть, каким образом происходит взаимодействие приложений с библиотеками:
lib.d
pid$target:$1::entry
{
@func[probefunc] = count();
}
# dtrace -s lib.d -c gimp libc
dtrace: script 'lib.d' matched 2870 probes
___lwp_private 1
__charmap_init 1
__collate_init 1
__ctype_init 1
__forkx 1
__getcontext 1
__locale_init 1
__lwp_sigmask 1
__messages_init 1
__monetary_init 1
__numeric_init 1
__priocntlset 1
__schedctl 1
__sigfillset 1
__so_connect 1
__time_init 1
__tls_static_mods 1
__waitid 1
_clear_internal_mbstate 1
_fpstart 1
<…>
cleanfree 4653
strchr 5849
memccpy 6341
fgets 6343
_realbufend 6358
getxfdat 6366
mutex_lock 14453
mutex_lock_impl 14453
mutex_unlock 14506
<…>
Результат работы скрипта – информация о том, сколько раз и какие функции вызываются из библиотеки libc при запуске gimp без аргументов.
Если считать, что в программе выделение и освобождение памяти происходит только при помощи вызовов malloc() и free(), можно использовать этот провайдер для поиска утечек памяти.
malloc.d
pid$target:libc:malloc:entry
{
ustack();
}
pid$target:libc:malloc:return
{
printf("alloc: %x\n", arg1);
@mem[ arg1 ] = count();
}
pid$target:libc:free:entry
{
printf("free: %x\n", arg0);
@mem[ arg0 ] = count();
}
END
{
printa(@mem);
}
Действие ustack() на входе в malloc позволяет увидеть стек вызовов на момент выделения памяти, следовательно, можно определить, какие выделенные вызовом malloc() участки памяти не были впоследствии освобождены при помощи вызова free():
0 66081 malloc:return alloc: 847ace0
0 66080 malloc:entry
libc.so.1`malloc
libX11.so.4`store_to_database+0x91
libX11.so.4`f_newline+0x3b
libX11.so.4`CreateDatabase+0x1e1
libX11.so.4`_XlcCreateLocaleDataBase+0x8a
libX11.so.4`initialize+0x1db
libX11.so.4`initialize+0x2e
libX11.so.4`_XlcCreateLC+0x9a
xlcUTF8Load.so.2`_XlcUtf8Loader+0x24
libX11.so.4`_XlcDynamicLoad+0x427
libX11.so.4`_XlcCurrentLC+0xee
libX11.so.4`XSupportsLocale+0x18
libgdk-x11-2.0.so.0.1200.3`_gdk_x11_initialize_locale+0x6b
libgdk-x11-2.0.so.0.1200.3`_gdk_windowing_init+0x18
libgdk-x11-2.0.so.0.1200.3`gdk_pre_parse_libgtk_only+0x57
libgtk-x11-2.0.so.0.1200.3`pre_parse_hook+0x61
libglib-2.0.so.0.1400.4`g_option_context_parse+0x8c
gimp-2.4`main+0x1e2
gimp-2.4`_start+0x7a
Провайдер proc
Нужен для трассировки системных событий, отображающих процесс исполнения кода (запуск и завершение, отправка и обработка сигналов), потому что управление процессами и сигналами осуществляется не только системными вызовами, но и функциями из стандартной библиотеки С.
Список датчиков (из названий понятно их назначение):
# dtrace -l -P proc
ID PROVIDER MODULE FUNCTION NAME
15653 proc unix lwp_rtt_initial start
15654 proc unix trap fault
15672 proc unix lwp_rtt_initial lwp-start
15748 proc genunix cfork create
15750 proc genunix proc_exit exit
15751 proc genunix lwp_exit lwp-exit
15752 proc genunix proc_exit lwp-exit
15753 proc genunix exec_common exec-success
15754 proc genunix exec_common exec-failure
15755 proc genunix exec_common exec
15785 proc genunix lwp_create lwp-create
15851 proc genunix sigtimedwait signal-clear
15852 proc genunix psig signal-handle
15853 proc genunix sigtoproc signal-discard
15854 proc genunix sigtoproc signal-send
Дерево запуска процессов при вызове определенной команды:
calls.d
proc:::exec
{
self->parent = execname;
}
proc:::exec-success
/self->parent != NULL/
{
@gs[self->parent, execname] = count();
self->parent = NULL;
}
proc:::exec-failure
/self->parent != NULL/
{
@gf[self->parent, execname] = count();
self->parent = NULL;
}
END
/self->start/
{
@gs=quantize (timestamp-self->start);
self->start =0;
printa(" %s -> %s (%@d)\n", @gs);
}
#dtrace -s calls.d -c 'gimp'
CPU ID FUNCTION:NAME
gimp-2.4 -> AlienMap2 (1)
gimp-2.4 -> CEL (1)
gimp-2.4 -> CML_explorer (1)
gimp-2.4 -> FractalExplorer (1)
gimp-2.4 -> Lighting (1)
gimp-2.4 -> MapObject (1)
gimp-2.4 -> align_layers (1)
gimp-2.4 -> animationplay (1)
gimp-2.4 -> animoptimize (1)
gimp-2.4 -> antialias (1)
gimp-2.4 -> apply_lens (1)
gimp-2.4 -> autocrop (1)
gimp-2.4 -> autostretch_hsv (1)
gimp-2.4 -> blinds (1)
gimp-2.4 -> blur (1)
gimp-2.4 -> bmp (1)
gimp-2.4 -> borderaverage (1)
gimp-2.4 -> bumpmap (1)
gimp-2.4 -> c_astretch (1)
gimp-2.4 -> cartoon (1)
gimp-2.4 -> ccanalyze (1)
gimp-2.4 -> channel_mixer (1)
gimp-2.4 -> checkerboard (1)
gimp-2.4 -> color_enhance (1)
gimp-2.4 -> colorify (1)
gimp-2.4 -> colormap-remap (1)
gimp-2.4 -> colortoalpha (1)
gimp-2.4 -> compose (1)
Трассировка удачных запусков (зависимости приложений не учитываются):
# dtrace -n 'proc:::exec-success {
trace(curpsinfo->pr_psargs); }'
dtrace: description 'proc:::exec-success ' matched 1 probe
CPU ID FUNCTION:NAME
0 15753 exec_common:exec-success ps -ef
0 15753 exec_common:exec-success ps -ef
0 15753 exec_common:exec-success hostname
<…>
Провайдер profile
Источник событий для profile - прерывания по времени с заданным интервалом.
Например, в результате выполнения
tick-1s
{
exit(0);
}
скрипт автоматически закончит работу через 1 секунду, когда сработает датчик tick-1s и действие exit() с целочисленным аргументом, что вызовет срабатывание датчика END (работающего только после того, как отработали все другие датчики) и, следовательно, нормальное завершение работы. Если в скрипте нет подобных конструкций, для просмотра итоговых данных используется Ctrl-C.