Можно ли однозначно идентифицировать динамически импортированные функции по их имени?

Я использовал

readelf --dyn-sym my_elf_binary | grep FUNC | grep UND

для отображения динамически импортированных функций my_elf_binary, из таблицы динамических символов в разделе .dynsym, чтобы быть точным. Пример вывода:

 [...]
 3: 00000000     0 FUNC    GLOBAL DEFAULT  UND [email protected]_2.0 (3)
 4: 00000000     0 FUNC    GLOBAL DEFAULT  UND [email protected]_2.0 (3)
 5: 00000000     0 FUNC    GLOBAL DEFAULT  UND [email protected]_2.0 (3)
 6: 00000000     0 FUNC    GLOBAL DEFAULT  UND [email protected]_2.0 (3)
 7: 00000000     0 FUNC    GLOBAL DEFAULT  UND [email protected]_2.2 (4)
 [...]

Можно ли предположить, что имена, связанные с этими символами, например, tcsetattr или access, всегда уникальны? Или возможно, или разумно *) иметь таблицу динамических символов (отфильтрованную для FUNC и UND), которая содержит две записи с одинаковой связанной строкой

Я задаю вопрос, что я ищу уникальный идентификатор для динамически импортируемых функций...

*) Не будет ли динамический компоновщик разрешать все символы "UND FUNC" с тем же именем в одну и ту же функцию?

Ответ 1

Да, учитывая имя символа и набор библиотек, с которыми связан исполняемый файл, вы можете однозначно идентифицировать функцию. Такое поведение требуется для связывания и динамической привязки к работе.


Иллюстративный пример

Рассмотрим следующие два файла:

librarytest1.c:

#include <stdio.h>
int testfunction(void)
{
   printf("version 1");
   return 0;
}

и librarytest2.c:

#include <stdio.h>
int testfunction(void)
{
   printf("version 2");
   return 0;
}

Оба скомпилированы в разделяемые библиотеки:

% gcc -fPIC -shared -Wl,-soname,liblibrarytest.so.1 -o liblibrarytest.so.1.0.0 librarytest1.c -lc 
% gcc -fPIC -shared -Wl,-soname,liblibrarytest.so.2 -o liblibrarytest.so.2.0.0 librarytest2.c -lc

Обратите внимание, что мы не можем поместить обе функции с тем же именем в одну общую библиотеку:

% gcc -fPIC -shared -Wl,-soname,liblibrarytest.so.0 -o liblibrarytest.so.0.0.0 librarytest1.c librarytest2.c -lc                                                                                                     
/tmp/cctbsBxm.o: In function `testfunction':
librarytest2.c:(.text+0x0): multiple definition of `testfunction'
/tmp/ccQoaDxD.o:librarytest1.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status

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

% readelf --dyn-syms liblibrarytest.so.1.0.0 | grep testfunction 
12: 00000000000006d0    28 FUNC    GLOBAL DEFAULT   10 testfunction
% readelf --dyn-syms liblibrarytest.so.2.0.0 | grep testfunction 
12: 00000000000006d0    28 FUNC    GLOBAL DEFAULT   10 testfunction

Теперь позвольте связать наши общие библиотеки с исполняемым файлом. Рассмотрим linktest.c:

int testfunction(void);
int main()
{
  testfunction();
  return 0;
}

Мы можем скомпилировать и связать это с общей библиотекой:

% gcc -o linktest1 liblibrarytest.so.1.0.0 linktest.c 
% gcc -o linktest2 liblibrarytest.so.2.0.0 linktest.c 

И запустите каждый из них (обратите внимание, что я устанавливаю путь динамической библиотеки, чтобы динамический компоновщик мог найти библиотеки, которые не находятся в стандартном пути библиотеки):

% LD_LIBRARY_PATH=. ./linktest1                    
version 1%                                                                                                              
% LD_LIBRARY_PATH=. ./linktest2
version 2%

Теперь свяжем наш исполняемый файл с обеими библиотеками. Каждый из них экспортирует один и тот же символ testfunction, и каждая библиотека имеет другую реализацию этой функции.

% gcc -o linktest0-1 liblibrarytest.so.1.0.0 liblibrarytest.so.2.0.0 linktest.c
% gcc -o linktest0-2 liblibrarytest.so.2.0.0 liblibrarytest.so.1.0.0 linktest.c

Единственное различие - это порядок, на который библиотеки ссылаются на компилятор.

% LD_LIBRARY_PATH=. ./linktest0-1                                              
version 1%                                                                                                             
% LD_LIBRARY_PATH=. ./linktest0-2
version 2%    

Вот соответствующий вывод ldd:

% LD_LIBRARY_PATH=. ldd ./linktest0-1 
    linux-vdso.so.1 (0x00007ffe193de000)
    liblibrarytest.so.1 => ./liblibrarytest.so.1 (0x00002b8bc4b0c000)
    liblibrarytest.so.2 => ./liblibrarytest.so.2 (0x00002b8bc4d0e000)
    libc.so.6 => /lib64/libc.so.6 (0x00002b8bc4f10000)
    /lib64/ld-linux-x86-64.so.2 (0x00002b8bc48e8000)
% LD_LIBRARY_PATH=. ldd ./linktest0-2
    linux-vdso.so.1 (0x00007ffc65df0000)
    liblibrarytest.so.2 => ./liblibrarytest.so.2 (0x00002b46055c8000)
    liblibrarytest.so.1 => ./liblibrarytest.so.1 (0x00002b46057ca000)
    libc.so.6 => /lib64/libc.so.6 (0x00002b46059cc000)
    /lib64/ld-linux-x86-64.so.2 (0x00002b46053a4000)

Здесь мы видим, что, хотя символы не уникальны, определяется способ, которым решает их компоновщик (кажется, что он всегда разрешает первый символ, с которым он сталкивается). Обратите внимание, что это немного патологический случай, поскольку вы обычно этого не делали. В тех случаях, когда вы отправляетесь в этом направлении, есть более эффективные способы обработки имен символов, чтобы они были уникальны при экспорте (управление версиями символов и т.д.).


Таким образом, да, вы можете однозначно идентифицировать функцию, учитывая ее имя. Если по этому имени имеется несколько символов, вы определяете правильный, используя порядок, в котором разрешены библиотеки (от ldd или objdump и т.д.). Да, в этом случае вам потребуется немного больше информации, которая только его имя, но это возможно, если у вас есть исполняемый файл для проверки.

Ответ 2

Обратите внимание, что в вашем случае имя первого импорта функции не просто tcsetattr, но [email protected]_2.0. @ заключается в том, как программа readelf отображает импорт символа с версией.

GLIBC_2.0 - тег версии, который glibc использует, чтобы оставаться бинарным, совместимым со старыми двоичными файлами в (необычном, но возможном) случае, когда бинарный интерфейс с одной из его функций должен измениться. Оригинальный файл .o, созданный компилятором, будет просто импортировать tcsetattr без информации о версии, но при статической привязке компоновщик заметил, что фактический символ, экспортируемый lic.so, содержит тег GLIBC_2.0, и поэтому он создает бинарный, который настаивает на импорте конкретного символа tcsetattr, который имеет версию GLIBC_2.0.

В будущем может существовать libc.so, который экспортирует один [email protected]_2.0 и другой [email protected]_2.42, а затем тег версии будет использоваться для определения того, к какому объекту относится элемент элементарного элемента ELF.

Возможно, что один и тот же процесс может также использовать [email protected]_2.42 одновременно, например, если он использует другую динамическую библиотеку, которая была связана с libc.so, достаточно новым, чтобы обеспечить ее. Теги версии гарантируют, что как старая двоичная, так и новая библиотека получат функцию, которую они ожидают от библиотеки C.

Большинство библиотек не используют этот механизм и вместо этого просто переименовывают всю библиотеку, если им необходимо внести изменения в свои двоичные интерфейсы. Например, если вы дамп /usr/bin/pngtopnm, вы обнаружите, что символы, которые он импортирует из libnetpbm и libpng, не являются версиями. (Или, по крайней мере, то, что я вижу на своей машине).

Стоимость этого заключается в том, что вы не можете иметь двоичный файл, который ссылается на одну версию libpng, а также ссылается на другую библиотеку, которая сама ссылается на другую версию libpng; экспортируемые имена из двух libpng будут сталкиваться.

В большинстве случаев это достаточно управляемо благодаря тщательной практике упаковки, что сохранение источника библиотеки для создания полезных тегов версии и сохранение обратной совместимости не стоит проблем.

Но в частном случае библиотеки C и нескольких других жизненно важных системных библиотек изменение имени библиотеки было бы настолько чрезвычайно болезненным, что разработчикам было бы удобно перепрыгнуть через некоторые обручи, чтобы это никогда не было необходимо повторить.

Ответ 3

Хотя в большинстве случаев каждый символ уникален, существует несколько исключений. Моим любимым является многократный идентичный импорт символа, используемый PAM (подключаемые модули аутентификации) и NSS (Name Service Switch). В обоих случаях все модули, написанные для любого интерфейса, используют стандартный интерфейс со стандартными именами. Обычный и часто используемый пример - это то, что происходит, когда вы вызываете get host по имени. Библиотека nss вызовет одну и ту же функцию в нескольких библиотеках, чтобы получить ответ. Общая конфигурация вызывает ту же функцию в трех библиотеках! Я видел ту же функцию, которая вызывается в пяти разных библиотеках из одного вызова функции, и это не было пределом того, что было полезно. Специальные вызовы динамическому компоновщику необходимо сделать, и я не ознакомился с механиками этого, но нет ничего особенного в связи с таким образом загруженным библиотечным модулем.