Как я могу проверить, какой динамический компоновщик используется при запуске программы?

#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 , что в результате пути RTLD ldd не указывалось на правильный динамический компоновщик, который у меня был в моем кросс-скомпилированном пути. Мне интересно, не является ли этот путь неправильным, потому что я неправильно --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 команд or ldd (или даже hexdump , если на то пошло) в программе, которую вы собираетесь выполнить, и посмотреть, какой компоновщик она использует, но OP уже упоминал об этом.

5. @UnslanderMonica спасибо за ваше дополнение. Определяет ли область видимости, где объект (как показано выше) может искать разрешение своих символов?