#c #linux #dynamic-linking
#c #linux #динамическое связывание
Вопрос:
Я хотел бы убедиться, что динамический компоновщик, используемый при запуске программы, указан через file
, readelf -l
, или ldd
. Моя мотивация проистекает из наличия нескольких динамических компоновщиков, которые существуют в отдельных пространствах на компьютере, и они никогда не должны смешиваться и совпадать.
До сих пор лучшим способом проверки динамического компоновщика, который я нашел, является via gdb
. Просматривая выходные info proc mappings
данные, я могу определить, какой динамический компоновщик был отображен в адресное пространство и используется. Я стараюсь избегать использования gdb
, поскольку это потребовало бы от меня запуска наборов тестов и других вещей через него.
LD_DEBUG
Кажется, что использование переменной среды может быть альтернативным решением, которое позволило бы мне легко сохранять журналы для проверки после (или во время) выполнения программы. Однако я не уверен, какой вариант даст мне наилучшую информацию. Я думал scopes
, что or libs
может быть хорошим вариантом, но libs
не всегда упоминает динамический компоновщик. Например, это вывод простой программы hello world:
$ LD_DEBUG=libs ./test0
24579: find library=libc.so.6 [0]; searching
24579: search cache=/etc/ld.so.cache
24579: trying file=/lib/x86_64-linux-gnu/libc.so.6
24579:
24579:
24579: calling init: /lib/x86_64-linux-gnu/libc.so.6
24579:
24579:
24579: initialize program: ./test0
24579:
24579:
24579: transferring control: ./test0
24579:
hello world
24579:
24579: calling fini: ./test0 [0]
24579:
$ LD_DEBUG=libs ./test0-gnu-cross
24581: find library=libc.so.6 [0]; searching
24581: search path=/usr/local/gnu-cross/x86_64-linux-gnu/lib/glibc-hwcaps/x86-64-v4:/usr/local/gnu-cross/x86_64-linux-gnu/lib/glibc-hwcaps/x86-64-v3:/usr/local/gnu-cross/x86_64-linux-gnu/lib/glibc-hwcaps/x86-64-v2:/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/haswell/avx512_1/x86_64:/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/haswell/avx512_1:/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/haswell/x86_64:/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/haswell:/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/avx512_1/x86_64:/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/avx512_1:/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/x86_64:/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls:/usr/local/gnu-cross/x86_64-linux-gnu/lib/haswell/avx512_1/x86_64:/usr/local/gnu-cross/x86_64-linux-gnu/lib/haswell/avx512_1:/usr/local/gnu-cross/x86_64-linux-gnu/lib/haswell/x86_64:/usr/local/gnu-cross/x86_64-linux-gnu/lib/haswell:/usr/local/gnu-cross/x86_64-linux-gnu/lib/avx512_1/x86_64:/usr/local/gnu-cross/x86_64-linux-gnu/lib/avx512_1:/usr/local/gnu-cross/x86_64-linux-gnu/lib/x86_64:/usr/local/gnu-cross/x86_64-linux-gnu/lib (RPATH from file ./test0-gnu-cross)
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/glibc-hwcaps/x86-64-v4/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/glibc-hwcaps/x86-64-v3/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/glibc-hwcaps/x86-64-v2/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/haswell/avx512_1/x86_64/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/haswell/avx512_1/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/haswell/x86_64/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/haswell/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/avx512_1/x86_64/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/avx512_1/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/x86_64/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/tls/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/haswell/avx512_1/x86_64/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/haswell/avx512_1/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/haswell/x86_64/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/haswell/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/avx512_1/x86_64/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/avx512_1/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/x86_64/libc.so.6
24581: trying file=/usr/local/gnu-cross/x86_64-linux-gnu/lib/libc.so.6
24581:
24581:
24581: calling init: /usr/local/gnu-cross/x86_64-linux-gnu/lib/ld-linux-x86-64.so.2
24581:
24581:
24581: calling init: /usr/local/gnu-cross/x86_64-linux-gnu/lib/libc.so.6
24581:
24581:
24581: initialize program: ./test0-gnu-cross
24581:
24581:
24581: transferring control: ./test0-gnu-cross
24581:
hello world
24581:
24581: calling fini: ./test0-gnu-cross [0]
24581:
Как вы можете видеть, программа test0
, которая построена со стандартным набором инструментов Debian / GNU и использует динамический компоновщик системы, этого не указывает.
scopes
Опция выглядит более полезной, но я не понимаю, что говорит вывод:
$ LD_DEBUG=scopes ./test0
24577:
24577: Initial object scopes
24577: object=./test0 [0]
24577: scope 0: ./test0 /lib/x86_64-linux-gnu/libc.so.6 /lib64/ld-linux-x86-64.so.2
24577:
24577: object=linux-vdso.so.1 [0]
24577: scope 0: ./test0 /lib/x86_64-linux-gnu/libc.so.6 /lib64/ld-linux-x86-64.so.2
24577: scope 1: linux-vdso.so.1
24577:
24577: object=/lib/x86_64-linux-gnu/libc.so.6 [0]
24577: scope 0: ./test0 /lib/x86_64-linux-gnu/libc.so.6 /lib64/ld-linux-x86-64.so.2
24577:
24577: object=/lib64/ld-linux-x86-64.so.2 [0]
24577: no scope
24577:
hello world
$ LD_DEBUG=scopes ./test0-gnu-cross
24576:
24576: Initial object scopes
24576: object=./test0-gnu-cross [0]
24576: scope 0: ./test0-gnu-cross /usr/local/gnu-cross/x86_64-linux-gnu/lib/libc.so.6 /usr/local/gnu-cross/x86_64-linux-gnu/lib/ld-linux-x86-64.so.2
24576:
24576: object=linux-vdso.so.1 [0]
24576: scope 0: ./test0-gnu-cross /usr/local/gnu-cross/x86_64-linux-gnu/lib/libc.so.6 /usr/local/gnu-cross/x86_64-linux-gnu/lib/ld-linux-x86-64.so.2
24576: scope 1: linux-vdso.so.1
24576:
24576: object=/usr/local/gnu-cross/x86_64-linux-gnu/lib/libc.so.6 [0]
24576: scope 0: ./test0-gnu-cross /usr/local/gnu-cross/x86_64-linux-gnu/lib/libc.so.6 /usr/local/gnu-cross/x86_64-linux-gnu/lib/ld-linux-x86-64.so.2
24576:
24576: object=/usr/local/gnu-cross/x86_64-linux-gnu/lib/ld-linux-x86-64.so.2 [0]
24576: no scope
24576:
hello world
В заключение я хотел бы найти хороший способ проверки используемого динамического компоновщика. Если вы не можете придумать лучший вариант, LD_DEBUG
это кажется хорошей ставкой, но я изо всех сил пытаюсь понять, как эффективно использовать его в этом случае.
Спасибо за вашу помощь 🙂
Комментарии:
1.
info proc mappings
кажется, предоставляет информацию, содержащуюся в/proc/self/maps
, поэтому может быть достаточно просто прочитать последнее2. @CraigEstey при таком решении мне не пришлось бы спешить с чтением
/proc/<pid>/maps
до завершения программы? Есть ли способ предсказать, какой PID будет сгенерирован следующим, а затем дождаться его создания/proc
?
Ответ №1:
Нет необходимости фактически запускать исполняемый файл, чтобы определить интерпретатор ELF, который он будет использовать.
Мы можем использовать статические инструменты и быть уверены, что сможем получить полный путь.
Мы можем использовать комбинацию readelf
и ldd
.
Если мы используем readelf -a
, мы можем проанализировать вывод.
Одна часть readelf
выходных данных:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 00000000000002e0 000002e0
000000000000001c 0000000000000000 A 0 0 1
Обратите внимание на адрес .interp
раздела. Это так 0x2e0
.
Если мы откроем исполняемый файл и выполним поиск по этому смещению, мы сможем прочитать строку интерпретатора ELF. Например, вот [то, что я назову] fileBad
:
000002e0: 2F6C6962 36342F7A 642D6C69 6E75782D /lib64/zd-linux-
000002f0: 7838362D 36342E73 6F2E3200 00000000 x86-64.so.2.....
Обратите внимание, что строка кажется немного странной… Подробнее об этом позже…
В разделе «Заголовки программы:» у нас есть:
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002a0 0x00000000000002a0 R 0x8
INTERP 0x00000000000002e0 0x00000000000002e0 0x00000000000002e0
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/zd-linux-x86-64.so.2]
Опять же, обратите 0x2e0
внимание на смещение файла. Это может быть более простым способом получить путь к интерпретатору ELF.
Теперь у нас есть полный путь к интерпретатору ELF.
Теперь мы можем это сделать ldd /path/to/executable
, и мы получим список разделяемых библиотек, которые он использует / будет использовать. Мы сделаем это для fileGood
. Обычно это выглядит как [отредактировано]:
linux-vdso.so.1 (0x00007ffc96d43000)
libpython3.7m.so.1.0 => /lib64/libpython3.7m.so.1.0 (0x00007f36d1ee2000)
...
libc.so.6 => /lib64/libc.so.6 (0x00007f36d1ac7000)
/lib64/ld-linux-x86-64.so.2 (0x00007f36d23ff000)
...
Это для обычного исполняемого файла. Вот ldd
вывод для fileBad
:
linux-vdso.so.1 (0x00007ffc96d43000)
libpython3.7m.so.1.0 => /lib64/libpython3.7m.so.1.0 (0x00007f36d1ee2000)
...
libc.so.6 => /lib64/libc.so.6 (0x00007f36d1ac7000)
/lib64/zd-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007f3f4f821000)
...
Хорошо, чтобы объяснить …
fileGood
является стандартным исполняемым файлом [ /bin/vi
в моей системе]. Однако fileBad
это копия, которую я сделал, где я исправил путь интерпретатора к несуществующему файлу.
Из readelf
данных мы знаем путь интерпретатора. Мы можем проверить наличие этого файла. Если он не существует, все [очевидно] плохо.
С помощью пути интерпретатора, который мы получили readelf
, мы можем найти выходную строку ldd
для интерпретатора.
Для хорошего файла ldd
предоставлено простое разрешение интерпретатора:
/lib64/ld-linux-x86-64.so.2 (0x00007f36d23ff000)
Для плохого файла, ldd
дал нам:
/lib64/zd-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007f3f4f821000)
Итак, либо ldd
или ядро обнаружило отсутствующий интерпретатор и заменило интерпретатор по умолчанию.
Если мы попытаемся выполнить из оболочки, мы получим fileBad
:
fileBad: Command not found
Если мы попытаемся выполнить из программы на C, мы получим ошибку fileBad
ENOENT
:
No such file or directory
Из этого мы знаем, что ядро не пыталось использовать интерпретатор «по умолчанию», когда мы выполняли exec*
системный вызов.
Итак, теперь мы знаем, что статический анализ, который мы провели для определения пути интерпретатора ELF, действителен.
Мы можем быть уверены, что путь, который мы придумали, [будет] путем к интерпретатору ELF, который ядро отобразит в адресное пространство процесса.
Для дополнительной уверенности, если вам нужно, загрузите исходный код ядра. Посмотрите в файле: fs/binfmt_elf.c
Я думаю, этого достаточно, но чтобы ответить на вопрос в вашем верхнем комментарии
при таком решении мне не пришлось бы спешить с чтением
/proc/<pid>/maps
до завершения программы?
Нет необходимости участвовать в гонке.
Мы можем контролировать fork
процесс. Мы можем настроить дочерний элемент для запуска под [системный вызов] ptrace
, чтобы мы могли контролировать его выполнение (обратите внимание, что ptrace
это то, что gdb
и strace
использовать).
После того, как мы fork
, но до того, как мы exec
, дочерний элемент может запросить целевой режим exec
ожидания, пока процесс не подключится к нему через ptrace
.
Таким образом, родительский файл может проверить /proc/pid/maps
[или что-то еще] до того, как целевой исполняемый файл выполнит одну инструкцию. Он может управлять выполнением через ptrace
[и, в конечном итоге, отсоединить, чтобы позволить цели работать нормально].
Есть ли способ предсказать, какой PID будет сгенерирован следующим, а затем дождаться его создания
/proc
?
Учитывая ответ на первую часть вашего вопроса, это немного спорный вопрос.
Невозможно [точно] предсказать pid
процесс, который мы fork
. Если бы мы могли определить pid
, что система будет использовать следующим, нет никакой гарантии, что мы выиграем гонку против другого процесса, выполняющего fork
[перед нами] и «получающего» pid
то, что мы «думали», будет нашим.
Комментарии:
1. Спасибо за этот подробный ответ, @CraigEstey. Вы правы и помогли мне прийти к другому, дополнительному выводу. Я использовал
ldd
систему, а неldd
мой кросс-компилятор. Кросс-компиляцияldd
продолжала говоритьnot a dynamic executable
, что в результате пути RTLDldd
не указывалось на правильный динамический компоновщик, который у меня был в моем кросс-скомпилированном пути. Мне интересно, не является ли этот путь неправильным, потому что я неправильно--prefix
использовал или что-то в моей сборке toolchain.
Ответ №2:
Вы можете использовать LD_DEBUG=scopes
для этого
Пример вывода с моего компьютера:
LD_DEBUG=scopes ./hello
17513:
17513: Initial object scopes
17513: object=./hello [0]
17513: scope 0: ./hello /lib/x86_64-linux-gnu/libc.so.6 /lib64/ld-linux-x86-64.so.2
17513:
17513: object=linux-vdso.so.1 [0]
17513: scope 0: ./hello /lib/x86_64-linux-gnu/libc.so.6 /lib64/ld-linux-x86-64.so.2
17513: scope 1: linux-vdso.so.1
17513:
17513: object=/lib/x86_64-linux-gnu/libc.so.6 [0]
17513: scope 0: ./hello /lib/x86_64-linux-gnu/libc.so.6 /lib64/ld-linux-x86-64.so.2
17513:
17513: object=/lib64/ld-linux-x86-64.so.2 [0]
17513: no scope
17513:
Hello world
Найдите объект без области видимости.
Кроме того, для LD_DEBUG есть только несколько значений, проверьте их здесь и поэкспериментируйте.
Комментарии:
1. Не могли бы вы подробнее рассказать о параметре области видимости и почему «нет области видимости» означает, что это динамический компоновщик?
2. @peachykeen Похоже, что области определяются динамическим компоновщиком, поэтому динамический компоновщик не будет иметь области видимости, поскольку он не был загружен динамическим компоновщиком.
3. У меня есть динамический компоновщик, который не отвечает на LD_DEBUG.
4.@Joshua Самая очевидная вещь, которую можно сделать, — это запустить любую из
file
readelf -l
команд orldd
(или дажеhexdump
, если на то пошло) в программе, которую вы собираетесь выполнить, и посмотреть, какой компоновщик она использует, но OP уже упоминал об этом.5. @UnslanderMonica спасибо за ваше дополнение. Определяет ли область видимости, где объект (как показано выше) может искать разрешение своих символов?