View on GitHub

Methodics of GNU/Linux application software development

Practical materials for learning application software development on Linux

Данный раздел посвящён трассировке системных вызовов и работе работе с утилитой Strace. Также будет рассмотрен алгоритм предзагрузки библиотек

Общая теория

Разберёмся, что из себя вообще представляет трассировка. В разных областях это понятие может интерпретироваться по-разному.

Трассировка кода, а именно ей мы будем заниматься глобально — это метод тестирования программного обеспечения, при котором отслеживается исполнение инструкций программы, а также состояния переменных и объектов во время её работы. По сути, это тот механизм, который помогает нам отлаживать программу, выявлять проблемы, просматривать непосредственное исполнение кода. Подробнее про отладку в целом можно почитать тут. Трассировка системных вызовов, как ясно из названия, отслеживает выполнение обращений программы к ОС, параметры вызовов и результат их выполнения.

Strace

Проект Strace — один из классических инструментов Linux, который позволяет трассировать системные вызовы. При этом сама утилита тоже работает за счёт системных вызовов, а, точнее, с системным вызовом ptrace. Основная задача strace — это отслеживание возвращаемых значений от ptrace и их обработка в соответствии с поставленными фильтрами и действиями. Для того, чтобы разобраться с работой strace, мы рекомендуем поисследовать man, изучить статьи с Хабра (про сам strace, и про его реальное использование)

Попробуем с его помощью поизучать разные Linux — утилиты. Например, сколько системных вызовов в утилите date

user@localhost:~> date
Tue Oct 22 04:32:30 PM MSK 2024
user@localhost:~> strace date
execve("/usr/bin/date", ["date"], 0x7fff61594470 /* 83 vars */) = 0
brk(NULL)                               = 0x56286a0db000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc5e25ad000

<...>

write(1, "Tue Oct 22 04:32:37 PM MSK 2024\n", 32Tue Oct 22 04:32:37 PM MSK 2024
) = 32
close(1)                                = 0
close(2)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++
user@localhost:~> strace date |& wc # Смотрим количество 
    120     735    8213
user@localhost:~> 

Современные утилиты, вообще говоря, дёргают множество системных вызовов: проверки доступа, кеширование, локализация, подтягивание библиотек и т. д.

user@localhost:~> strace ls |& wc
    174    1028   11490
user@localhost:~> 

Во всём этом множестве вызовов довольно трудно ориентироваться, поэтому одна из основных функций strace — сокращать количество выводов информации о системных вызовах путём указания области фильтрования. Ключ фильтрации -e позволяет выделить группу, системные вызовы связанные с которой и будут отображаться.

Отследим системные вызовы, связанные с обработкой файлов, в процессе выполнения date

user@localhost:~> strace -e file date 
execve("/usr/bin/date", ["date"], 0x7ffe15ffef28 /* 83 vars */) = 0
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)

<...>

openat(AT_FDCWD, "/usr/lib/locale/en_US.utf8/LC_CTYPE", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/etc/localtime", O_RDONLY|O_CLOEXEC) = 3
Tue Oct 22 04:36:27 PM MSK 2024
+++ exited with 0 +++
user@localhost:~> strace -e file date |& wc
     35     264    3171
user@localhost:~> 

Также можно отслеживать возвращаемые значения системных вызовов. Ключи -z и -Z фильтруют успешные и неуспешные вызовы соответственно.

user@localhost:~> strace -z -e file date
execve("/usr/bin/date", ["date"], 0x7fff03ace970 /* 83 vars */) = 0
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3

<...>

O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/etc/localtime", O_RDONLY|O_CLOEXEC) = 3
Tue Oct 22 04:37:22 PM MSK 2024
+++ exited with 0 +++
user@localhost:~> strace -z -e file date |& wc
     21     111    1547
user@localhost:~> 

Для трассировки вызовов, связанных с конкретными файлами, можно воспользоваться ключом -P для задания пути до исследуемого объекта. При этом strace отследит всю работу с ним, в том числе и в рамках работы с описывающим его файловым дескриптором.

user@localhost:~> strace -P /usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES date
openat(AT_FDCWD, "/usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=57, ...}) = 0
mmap(NULL, 57, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f9fc9acf000
close(3)                                = 0
Sat Oct 26 08:49:38 PM MSK 2024
+++ exited with 0 +++
user@localhost:~> 

Как мы видим, strace срезает часть вывода больших данных об объекте. Чтобы получать информацию без сокращения, можно воспользоваться ключом -v.

user@localhost:~> strace -P /usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES -v date
openat(AT_FDCWD, "/usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_dev=makedev(0, 0x2a), st_ino=5991, st_mode=S_IFREG|0644, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=8, st_size=57, st_atime=1729957009 /* 2024-10-26T18:36:49.302689708+0300 */, st_atime_nsec=302689708, st_mtime=1722239432 /* 2024-07-29T10:50:32+0300 */, st_mtime_nsec=0, st_ctime=1725309643 /* 2024-09-02T23:40:43.066310486+0300 */, st_ctime_nsec=66310486}) = 0
mmap(NULL, 57, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f6a459a4000
close(3)                                = 0
Sat Oct 26 08:51:37 PM MSK 2024
+++ exited with 0 +++
user@localhost:~> 

Чтобы не отслеживать каждый файл отдельно, под каким дескриптором и когда он обрабатывался, или чтобы просто иметь понимание, какие объекты под какими дескрипторами или переменными окружения скрыты, можно воспользоваться ключами -y и -yy, которые раскрывают информацию о соответствии дескрипторов файлам.

user@localhost:~> strace -P /usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES date 2> info_not_y
Sat Oct 26 09:00:26 PM MSK 2024
user@localhost:~> strace -P /usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES -y date 2> info_with_y
Sat Oct 26 09:00:37 PM MSK 2024

user@localhost:~> cat info_not_y | grep -E "(openat)|(fstat)"
openat(AT_FDCWD, "/usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=57, ...}) = 0

user@localhost:~> cat info_with_y | grep -E "(openat)|(fstat)"
openat(AT_FDCWD</home/user>, "/usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES", O_RDONLY|O_CLOEXEC) = 3</usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES>
fstat(3</usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES>, {st_mode=S_IFREG|0644, st_size=57, ...}) = 0
user@localhost:~> 

Чтобы отследить время выполнения системных вызовов, их количество и количество ошибок выполнения, можно воспользоваться ключом -w, показывающим таблицу выполнения системных вызовов. Ключ идёт в обязательной паре с ключами -С/-c, которые указывают, выводить или не выводить при этом саму трассировку.

user@localhost:~> LC_ALL=en_US.utf8 strace -P /usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES -w -C date 
openat(AT_FDCWD, "/usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=57, ...}) = 0
mmap(NULL, 57, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f9e556cc000
close(3)                                = 0
Sat Oct 26 09:03:54 PM MSK 2024
+++ exited with 0 +++
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 29.31    0.000007           7         1           mmap
 29.07    0.000007           7         1           openat
 21.59    0.000005           5         1           fstat
 20.03    0.000005           5         1           close
------ ----------- ----------- --------- --------- ----------------
100.00    0.000025           6         4           total

user@localhost:~> strace -w -c date
Sat Oct 26 09:04:58 PM MSK 2024
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 35.47    0.000554         553         1           execve
 16.76    0.000262           8        31        13 openat
 14.02    0.000219          10        21           mmap
  9.34    0.000146           7        20           close
  7.71    0.000120           6        20           fstat
  3.39    0.000053          17         3           mprotect
  2.81    0.000044          43         1           write
  2.24    0.000035           6         5           read
  1.70    0.000027           8         3           brk
  1.15    0.000018          17         1           munmap
  0.67    0.000010           5         2           pread64
  0.67    0.000010          10         1           set_tid_address
  0.64    0.000010          10         1           rseq
  0.57    0.000009           8         1           prlimit64
  0.57    0.000009           8         1           futex
  0.54    0.000008           8         1         1 access
  0.53    0.000008           8         1           set_robust_list
  0.52    0.000008           8         1           getrandom
  0.37    0.000006           5         1           lseek
  0.33    0.000005           5         1           arch_prctl
------ ----------- ----------- --------- --------- ----------------
100.00    0.001561          13       117        14 total
user@localhost:~> 

Ключ -o позволит сохранить результаты трассировки в отдельный файл (если вы хотите делать перенаправление вывода вручную, необходимо помнить, что трассировочная информация направляется в stderr, соответственно перенаправлять надо поток ошибок через 2>{cм. пример в описании ключа -y}).

user@localhost:~> ls | grep Date
user@localhost:~> strace -w -c -o Date_exec_wall date
Sat Oct 26 09:12:11 PM MSK 2024
user@localhost:~> ls | grep Date
Date_exec_wall
user@localhost:~> cat Date_exec_wall 
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 41.26    0.000619         619         1           execve
 15.70    0.000236          11        21           mmap
 15.58    0.000234           7        31        13 openat
  7.50    0.000113           5        20           fstat
  7.23    0.000109           5        20           close
  2.12    0.000032           6         5           read
  1.75    0.000026           8         3           mprotect
  1.65    0.000025           8         3           brk
  1.05    0.000016           7         2           pread64
  1.05    0.000016          15         1           write
  0.95    0.000014          14         1           munmap
  0.66    0.000010           9         1         1 access
  0.49    0.000007           7         1           set_tid_address
  0.47    0.000007           7         1           arch_prctl
  0.46    0.000007           6         1           rseq
  0.46    0.000007           6         1           set_robust_list
  0.45    0.000007           6         1           futex
  0.41    0.000006           6         1           lseek
  0.38    0.000006           5         1           prlimit64
  0.37    0.000005           5         1           getrandom
------ ----------- ----------- --------- --------- ----------------
100.00    0.001502          12       117        14 total
user@localhost:~> 

Раз уж мы упоминали трассировку в качестве одного из механизмов, которым пользуются отладчики, будет неправильным не рассказать про возможности чтения стека и данных, над которыми оперируют системные вызовы.

Для просмотра стека в момент исполнения какого-либо из системных вызовом можно воспользоваться ключом -k.

user@localhost:~> strace -P /usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES -k date 
openat(AT_FDCWD, "/usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES", O_RDONLY|O_CLOEXEC) = 3
 > /usr/lib64/libc.so.6(__open_nocancel+0x44) [0x107102]
 > /usr/lib64/libc.so.6(_nl_load_locale+0x2b9) [0x37317]
 > /usr/lib64/libc.so.6(_nl_find_locale+0x773) [0x360f1]
 > /usr/lib64/libc.so.6(setlocale+0x128) [0x38586]
 > /usr/bin/date() [0x3567]
 > /usr/lib64/libc.so.6(__libc_start_call_main+0x81) [0x2a33f]
 > /usr/lib64/libc.so.6(__libc_start_main@@GLIBC_2.34+0x8a) [0x2a408]
 > /usr/bin/date() [0x4254]
fstat(3, {st_mode=S_IFREG|0644, st_size=57, ...}) = 0
 > /usr/lib64/libc.so.6(__fstat64+0xd) [0x101ffb]
 > /usr/lib64/libc.so.6(_nl_load_locale+0x2cd) [0x3732b]
 > /usr/lib64/libc.so.6(_nl_find_locale+0x773) [0x360f1]
 > /usr/lib64/libc.so.6(setlocale+0x128) [0x38586]
 > /usr/bin/date() [0x3567]
 > /usr/lib64/libc.so.6(__libc_start_call_main+0x81) [0x2a33f]
 > /usr/lib64/libc.so.6(__libc_start_main@@GLIBC_2.34+0x8a) [0x2a408]
 > /usr/bin/date() [0x4254]
mmap(NULL, 57, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f313281d000
 > /usr/lib64/libc.so.6(__mmap+0x28) [0x10fd86]
 > /usr/lib64/libc.so.6(_nl_load_locale+0xaa) [0x37108]
 > /usr/lib64/libc.so.6(_nl_find_locale+0x773) [0x360f1]
 > /usr/lib64/libc.so.6(setlocale+0x128) [0x38586]
 > /usr/bin/date() [0x3567]
 > /usr/lib64/libc.so.6(__libc_start_call_main+0x81) [0x2a33f]
 > /usr/lib64/libc.so.6(__libc_start_main@@GLIBC_2.34+0x8a) [0x2a408]
 > /usr/bin/date() [0x4254]
close(3)                                = 0
 > /usr/lib64/libc.so.6(__close_nocancel+0xd) [0x106ddb]
 > /usr/lib64/libc.so.6(_nl_load_locale+0xba) [0x37118]
 > /usr/lib64/libc.so.6(_nl_find_locale+0x773) [0x360f1]
 > /usr/lib64/libc.so.6(setlocale+0x128) [0x38586]
 > /usr/bin/date() [0x3567]
 > /usr/lib64/libc.so.6(__libc_start_call_main+0x81) [0x2a33f]
 > /usr/lib64/libc.so.6(__libc_start_main@@GLIBC_2.34+0x8a) [0x2a408]
 > /usr/bin/date() [0x4254]
Sat Oct 26 09:19:09 PM MSK 2024
+++ exited with 0 +++
user@localhost:~> 

Для просмотра данных (которые, например, считаны системным вызовом read) можно воспользоваться ключом -s с указанием количества выводимых байт.

user@localhost:~> cat > Hello.txt
Try to read me, programmer, HAHAHA            
user@localhost:~> strace -e read -s100 cat Hello.txt 
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0N\245\2\0\0\0\0\0@\0\0\0\0\0\0\0\310x$\0\0\0\0\0\0\0\0\0@\08\0\16\0@\0C\0B\0\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0\20\3\0\0"..., 832) = 832
read(3, "# Locale name alias data base.\n# Copyright (C) 1996-2024 Free Software Foundation, Inc.\n#\n# This pro"..., 4096) = 2940
read(3, "", 4096)                       = 0
read(3, "Try to read me, programmer, HAHAHA\n", 262144) = 35
Try to read me, programmer, HAHAHA
read(3, "", 262144)                     = 0
+++ exited with 0 +++
user@localhost:~> 

Управление вызовами

Одна из важных задач отладки — исследование поведения программы в случаях некорректной работы системных вызовов. У нас может не быть прав доступа к файлам, может произойти ошибка при считывании или записи данных. Правильно написанная программа должна уметь обрабатывать такие случаи, даже если на практике за всё время использования она с ними не столкнётся. Хуже того, такую ситуацию — завершение определённого системного вызова с заданным кодом ошибки — далеко не всегда легко создать даже в чисто тестовых целях. Соответственно, для проверки нам необходим механизм управления системными вызовами, чтобы собственноручно менять результат их работы и проверять обрабатываемость тех или иных ошибок.

ptrace, на основе работы которого построен strace, отлавливает процессы дважды — при вызове и при возврате из системного вызова. И в момент возврата мы можем подменить результат его работы, причём вплоть до указания пришедшего в процесс сигнала или конкретного кода возврата.

Поисследуем подмену результатов. Возьмём команду cat /dev/null, обращающуюся к пустому устройству, и ничего не возвращающую на вывод (Чтение из /dev/null эквивалентно считыванию конца файла EOF ) . Вызовем её три раза (с помощью развёртки имён Shell {,,}) и будем трассировать обращение на открытие файла.

user@localhost:~> cat /dev/null{,,}
user@localhost:~> strace -P /dev/null -e openat cat /dev/null{,,}
openat(AT_FDCWD, "/dev/null", O_RDONLY) = 3
openat(AT_FDCWD, "/dev/null", O_RDONLY) = 3
openat(AT_FDCWD, "/dev/null", O_RDONLY) = 3
+++ exited with 0 +++
user@localhost:~> 

Теперь воспользуемся ключом -e fault(или --fault) для подмены результата системного вызова.

user@localhost:~> strace -P /dev/null -e openat -e fault=openat:error=EACCES cat /dev/null{,,}
openat(AT_FDCWD, "/dev/null", O_RDONLY) = -1 EACCES (Permission denied) (INJECTED)
cat: /dev/null: Permission denied
openat(AT_FDCWD, "/dev/null", O_RDONLY) = -1 EACCES (Permission denied) (INJECTED)
cat: /dev/null: Permission denied
openat(AT_FDCWD, "/dev/null", O_RDONLY) = -1 EACCES (Permission denied) (INJECTED)
cat: /dev/null: Permission denied
+++ exited with 1 +++
user@localhost:~> 

-e fault \ --fault являются упрощёнными флагами более масштабного флага подмены -e inject \ --inject. Базовый синтаксис подмены выглядит как $<место\ подмены>:<error=ошибка>:<when=какой\ по\ счёту\ системный\ вызов\ подменяется>$

inject также позволяет указать сигнал, который получил процесс.

user@localhost:~> strace -P /dev/null -e openat --fault=openat:error=EACCES:when=3 cat /dev/null{,,}
openat(AT_FDCWD, "/dev/null", O_RDONLY) = 3
openat(AT_FDCWD, "/dev/null", O_RDONLY) = 3
openat(AT_FDCWD, "/dev/null", O_RDONLY) = -1 EACCES (Permission denied) (INJECTED)
cat: /dev/null: Permission denied
+++ exited with 1 +++

user@localhost:~> strace -P /dev/null -e openat -e fault=openat:error=EACCES:when=2 cat /dev/null{,,}
openat(AT_FDCWD, "/dev/null", O_RDONLY) = 3
openat(AT_FDCWD, "/dev/null", O_RDONLY) = -1 EACCES (Permission denied) (INJECTED)
cat: /dev/null: Permission denied
openat(AT_FDCWD, "/dev/null", O_RDONLY) = 3
+++ exited with 1 +++

user@localhost:~> strace -P /dev/null -e openat --inject=openat:error=EACCES:when=2:signal=SIGINT cat /dev/null{,,}
openat(AT_FDCWD, "/dev/null", O_RDONLY) = 3
openat(AT_FDCWD, "/dev/null", O_RDONLY) = -1 EACCES (Permission denied) (INJECTED)
--- SIGINT {si_signo=SIGINT, si_code=SI_KERNEL} ---
+++ killed by SIGINT +++

user@localhost:~> 

Существует ещё множество возможностей для применения strace, и некоторые из них сама утилита предлагает в подсказках:

user@localhost:~> strace --tips
  ______________________________________________         ____
 /                                              \       /    \
 | It is possible to use regular expressions    |      |-. .-.|
 | for syscall names in the -e trace            \      (_@)(_@)
 | expression, for example:                      \     .---_  \
 |                                               _\   /..   \_/
 |     strace -e trace=/^sched_.*               /     |__.-^ /
 |                                              |         }  |
 | will trace all scheduling-related syscalls.  |        |   [
 \______________________________________________/        [  ]
                                                        ]   |
user@localhost:~> strace --tips
  ______________________________________________         ____
 /                                              \       /    \
 | You can control what columns are shown       |      |-. .-.|
 | in the call summary table produced by -c/-C  \      (_@)(_@)
 | options with -U/--summary-columns option.     \     .---_  \
 | It is a way to get minimum/maximum call       _\   /..   \_/
 | duration printed, for example:               /     |__.-^ /
 |                                              |         }  |
 |     strace -c -U name,min-time,max-time ls   |        |   [
 \______________________________________________/        [  ]
                                                        ]   |
user@localhost:~> 

Предзагрузка библиотек

Немного отвлечёмся от трассировки и изучим другую тему — предзагрузку библиотек.

Напишем программу, которая просто выводит псевдослучайные числа и что-то ещё:

rand.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
        int i;
        char *s;
        s = strdup(argv[argc-1]);
        for(i=0; i<4; i++)
                printf("%s: %d, %d\n", s, rand(), i*2+1);
        return 0;
}
user@localhost:~/LinuxAppDev/06_Tracing/Lection> cc rand.c -o rand
user@localhost:~/LinuxAppDev/06_Tracing/Lection> ./rand 
./rand: 1804289383, 1
./rand: 846930886, 3
./rand: 1681692777, 5
./rand: 1714636915, 7
user@localhost:~/LinuxAppDev/06_Tracing/Lection> ./rand q w e
e: 1804289383, 1
e: 846930886, 3
e: 1681692777, 5
e: 1714636915, 7
user@localhost:~/LinuxAppDev/06_Tracing/Lection> 

Вспомним, как работает динамический загрузчик: при компоновке программы собирается пространство имён используемых в ней объектов, а при запуске программы с использованием переменной окружения LD_LIBRARY_PATH, указывающей на расположение динамических библиотек, программа собирает сами объекты.

user@localhost:~/LinuxAppDev/06_Tracing/Lection> ldd rand
	linux-vdso.so.1 (0x00007f7e00de3000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f7e00a00000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f7e00de5000)
user@localhost:~/LinuxAppDev/06_Tracing/Lection> 

Кроме используемого нами LD_LIBRARY_PATH есть также переменная LD_PRELOAD, заранее подгружающая библиотеку для монтирования объектов из неё. И подгруженные таким образом символы оказываются более приоритетными, благодаря чему мы можем провернуть один трюк.

Опишем программу, в которой напишем функцию с сигнатурой, совпадающей с rand()

ura.c

int rand() {
	return 42;
}

Смонтируем из неё динамическую библиотеку и скомандуем системе загрузить эту библиотеку в память перед тем, как будет загружена основная программа. Это приведёт к тому, что в процессе динамической компоновки программы со всеми нужными ей библиотеками (например, libc), символ rand из нашей библиотеки — если он нужен программе — будет найден первым.

user@localhost:~/LinuxAppDev/06_Tracing/Lection> cc -shared -fPIC ura.c -o ura.so
user@localhost:~/LinuxAppDev/06_Tracing/Lection> LD_PRELOAD=`pwd`/ura.so ./rand 
./rand: 42, 1
./rand: 42, 3
./rand: 42, 5
./rand: 42, 7
user@localhost:~/LinuxAppDev/06_Tracing/Lection> 

Мы ничего не меняли в основной программе, при этом получили работу того rand(), который сами написали, поскольку при поиске объекта по имени программа поймала предзагруженную функцию, а не изначально задумывающуюся.

Для того, чтобы контролировать механизм загрузки объектов динамических библиотек, стоит воспользоваться функциями libc, описанными в dlfcn.h. Функция dlsym применяется для поиска символа по имени, а с описателем RTLD_NEXT — находит следующий такой символ после текущего.

ura.c

#include <dlfcn.h>

typedef int (*rnd_type)(void);
int rand(void) {
	int ret;
		// найти ещё один символ с таким же именем
	ret = ((rnd_type)(dlsym(RTLD_NEXT, "rand")))();
	return ret % 2 ? ret : 42;
}

Предполагается, что наша функция сначала вызовет «настоящую» rand(), а затем подменит её результат на 42, если он был чётный.

user@localhost:~/LinuxAppDev/06_Tracing/Lection> cc -shared -fPIC ura.c -o ura.so
user@localhost:~/LinuxAppDev/06_Tracing/Lection> LD_PRELOAD=`pwd`/ura.so ./rand 
./rand: 1804289383, 1
./rand: 42, 3
./rand: 1681692777, 5
./rand: 1714636915, 7
user@localhost:~/LinuxAppDev/06_Tracing/Lection> ./rand 
./rand: 1804289383, 1
./rand: 846930886, 3
./rand: 1681692777, 5
./rand: 1714636915, 7
user@localhost:~/LinuxAppDev/06_Tracing/Lection> 

Домашнее задание

Написать программу move.c, принимающую два параметра в командной строке: move infile outfile, которая перемещает содержимое файла infile в файл outfile (если таковой был, его содержимое удаляется), и в случае успеха удаляет infile.