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

23

В straceвыходных данных пути к библиотекам, которые вызывают исполняемые файлы, находятся в вызовах open(). Это системный вызов, используемый исполняемыми файлами, которые динамически связаны? Как насчет dlopen()? open()не вызов, который я бы предположил, сыграл бы роль в исполнении программ.

MELAB
источник

Ответы:

33

dlopenэто не системный вызов, это библиотечная функция в библиотеке libdl . В системе отображаются только системные вызовы strace.

В Linux и на многих других платформах (особенно тех, которые используют формат ELF для исполняемых файлов), dlopenреализуется путем открытия целевой библиотеки с помощью open()и отображения ее в памяти с помощью mmap(). mmap()здесь действительно важная часть, это то, что включает библиотеку в адресное пространство процесса, поэтому процессор может выполнять свой код. Но вы должны open()файл, прежде чем вы можете mmap()это сделать!

Celada
источник
2
«mmap () действительно важная часть»: и затем динамический компоновщик должен выполнить перемещения, инициализацию и т. д. (но это не видно на уровне системных вызовов).
ysdx
1
Поскольку загрузка библиотек выполняется библиотечной функцией, я считаю уместным добавить, что этот исполняемый файл сам по себе и ld-linuxотображается ядром как часть execveсистемного вызова.
Касперд
Mmap согласно этому ответу. Также обратите внимание, что после «открытия» каждой библиотеки некоторые (832) байта считываются перед вызовом mmap, я предполагаю проверить, что библиотека действительна.
Йохан
@kasperd Так ядро ​​Linux знает о динамическом загрузчике? Вызывает ли это, когда приложение запущено? Или само приложение это делает? Если последнее, как другой исполняемый файл имеет доступ к памяти приложения?
Мелаб
@Melab Да, ядро ​​знает о динамическом компоновщике. Ядро будет читать путь к динамическому компоновщику из заголовка исполняемого файла. И ядро ​​отобразит оба в память. Я не знаю, находится ли точка входа, к которой ядро ​​передало управление вначале, внутри компоновщика или исполняемого файла. Если бы я его реализовывал, то, вероятно, имел бы контроль передачи ядра в точку входа в компоновщике с адресом возврата в стеке, указывающим на точку входа исполняемого файла.
Касперд
5

dlopen не имеет ничего общего с общими библиотеками, как вы о них думаете. Существует два способа загрузки общего объекта:

  1. Вы сообщаете компоновщику времени компиляции (ld, хотя обычно он вызывается через компилятор), что вы хотите использовать функции из определенной разделяемой библиотеки. При таком подходе вы должны знать, каким будет имя библиотеки при запуске компоновщика во время компиляции, но вы можете вызывать функции библиотеки так, как если бы они были статически связаны с вашей программой. Когда приложение запускается, динамический компоновщик времени выполнения (ld.so) будет вызываться непосредственно перед вызовом mainфункции и настраивать пространство процесса приложения, чтобы приложение могло найти функции библиотеки. Это включает open()в себя lubrary, а затем mmap()его, а затем настроить некоторые таблицы поиска.
  2. Вы указываете компоновщику времени компиляции libdl, с которым хотите связать , из которого вы затем (используя первый метод) можете вызвать dlopen()иdlsym()функции. С помощью dlopen вы получаете дескриптор библиотеки, который затем можете использовать с dlsym, чтобы получить указатель на конкретную функцию. Этот метод гораздо сложнее для программиста, чем первый (поскольку вам нужно выполнить настройку вручную, а не компоновщик, чтобы сделать это автоматически), и он также более хрупок (так как вы не получаете компиляцию -time проверяет, что вы вызываете функции с правильными типами аргументов, как вы получаете в первом методе), но преимущество заключается в том, что вы можете решить, какой общий объект загружать во время выполнения (или даже загружать ли его вообще), делая Этот интерфейс предназначен для функциональности типа плагина. Наконец, интерфейс dlopen также менее переносим, ​​чем другие, так как его механизм зависит от точной реализации динамического компоновщика (отсюда и libtoollibltdl, который пытается абстрагироваться от этих различий).
Воутер Верхелст
источник
интересный; поэтому динамически загружаемую библиотеку лучше называть динамически связанными библиотеками, поскольку загрузка двоичных файлов в память - не сложная задача, поэтому целесообразно использовать адреса, используемые в ней. Когда я прошу загрузить динамическую библиотеку, я на самом деле прошу связать (или отменить связь) библиотеку с (или вне) моего адресного пространства.
Дмитрий
4

Сегодня большинство операционных систем используют метод для разделяемых библиотек, представленный в конце 1987 года SunOS-4.0. Этот метод основан на отображении памяти через mmap ().

Учитывая тот факт, что в начале 1990-х годов Sun даже пожертвовала старый код на основе a.out (в то время Solaris был уже основан на ELF) людям, работающим с FreeBSD, и что этот код позже был передан многим другим системам (включая Linux). Вы можете понять, почему между платформами нет большой разницы.

Шили
источник
3

ltrace -SАнализ минимального примера показывает, что mmapиспользуется в Glibc 2,23

В glibc 2.23, Ubuntu 16.04, работающий latrace -Sна минимальной программе, которая использует dlopen:

ltrace -S ./dlopen.out

шоу:

dlopen("libcirosantilli_ab.so", 1 <unfinished ...>
SYS_open("./x86_64/libcirosantilli_ab.so", 524288, 06267650550)      = -2
SYS_open("./libcirosantilli_ab.so", 524288, 06267650550)             = 3
SYS_read(3, "\177ELF\002\001\001", 832)                              = 832
SYS_brk(0)                                                           = 0x244c000
SYS_brk(0x246d000)                                                   = 0x246d000
SYS_fstat(3, 0x7fff42f9ce30)                                         = 0
SYS_getcwd("/home/ciro/bak/git/cpp-cheat"..., 128)                   = 54
SYS_mmap(0, 0x201028, 5, 2050)                                       = 0x7f1c323fe000
SYS_mprotect(0x7f1c323ff000, 2093056, 0)                             = 0
SYS_mmap(0x7f1c325fe000, 8192, 3, 2066)                              = 0x7f1c325fe000
SYS_close(3)                                                         = 0
SYS_mprotect(0x7f1c325fe000, 4096, 1)                                = 0

поэтому мы сразу видим, что dlopenзвонит open+ mmap.

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

Более внимательный анализ показывает, что openвозвращается дескриптор файла 3(следующий свободный после stdin, out и err).

readзатем использует этот файловый дескриптор, но аргументы TODO Why mmapограничены четырьмя, и мы не можем видеть, какой fd там использовался, поскольку это 5-й аргумент . straceкак и ожидалось, подтверждает, что 3это тот, и порядок вселенной восстановлен.

Храбрые души также могут рисковать в glibc-коде, но я не смог найти его mmapпосле быстрого поиска, и я ленив.

Протестировано на этом минимальном примере со сборкой шаблонов на GitHub .

Ciro Santilli 新疆 改造 中心 法轮功 六四 事件
источник
2

straceотчеты о системных вызовах (т.е. функциях, реализованных непосредственно ядром). Динамические библиотеки не являются функцией ядра; dlopenявляется частью библиотеки C, а не ядра. Реализация dlopenвызовет open(это системный вызов), чтобы открыть файл библиотеки, чтобы его можно было прочитать.

CJM
источник
5
Библиотечные звонки можно увидеть с помощью ltrace.
Касперд
@kasperd ltrace -Sидеально подходит для анализа этого, поскольку он также показывает системные вызовы
Сиро Сантилли 新疆 改造 中心 法轮功 六四 事件