ld.so linux 动态链接器

1. 概述

在 Linux 中生成进程时会执行几个步骤。如果程序依赖于共享库,则除了可执行二进制程序之外,还会将动态链接器加载到内存中。

在本教程中,我们将讨论动态链接器 /lib64/ld-linux-x86-64.so.2。(译者注:动态链接器名字不是固定的,也可能是 /lib/ld-linux.so.2 等等,但一般都是以 ld- 开头的一个 so 文件或符号链接)

首先,我们将简要讨论运行程序的内部结构以及动态链接在此过程中发生的位置。然后,我们将看到一个显示动态链接器用法的示例。

2. 动态链接器简介

在了解动态链接器之前,我们首先简要讨论程序的执行,以了解动态链接器的作用。

2.1. 可执行二进制文件是如何执行的?

当我们从命令行启动一个进程时,shell 首先调用系统调用 execve() 来执行程序。它打开可执行文件,并经过一些准备后,将可执行文件加载到内存中。

如果可执行文件具有共享库依赖项,则还会加载并运行解释器来组装依赖的共享库。这个解释器就是动态链接器。

一旦动态链接器完成其工作,控制权就会传递给用户程序。

2.2. 动态链接器

ELF(可执行和可链接格式)是 Linux 中的标准二进制文件格式。

可执行文件的 ELF 文件始终有一个称为程序头表的部分。此部分提供运行可执行文件时所需的信息。

运行可执行文件时程序头表中重要的条目之一是 PT_INTERP 条目。此条目指定在运行之前组装完整程序所需的运行时或动态链接器。

动态链接器执行多项任务:

  • 找到必要的共享库
  • 将共享库加载到内存中
  • 使用共享库中的符号解析程序的未定义符号
  • 组装程序,以便进程可以在运行时调用共享库中的函数

换句话说,程序头表中 PT_INTERP 条目指定的解释器在运行时管理可执行文件依赖的共享库。

(译者注:在某些系统下)gcc 默认将动态链接器设置为 /lib64/ld-linux-x86-64.so.2。我们可以使用 readelf 命令检查它的存在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
readelf -l /usr/bin/ls | head -20

Elf file type is DYN (Shared object file)
Entry point 0x6b10
There are 13 program headers, starting at offset 64

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040 0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318 0x000000000000001c 0x000000000000001c R 0x1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000003510 0x0000000000003510 R 0x1000
LOAD 0x0000000000004000 0x0000000000004000 0x0000000000004000 0x0000000000013111 0x0000000000013111 R E 0x1000
LOAD 0x0000000000018000 0x0000000000018000 0x0000000000018000 0x0000000000007530 0x0000000000007530 R 0x1000
LOAD 0x000000000001ff70 0x0000000000020f70 0x0000000000020f70

我们使用 head 命令来获得更短的输出。 readelf 的 -l 选项列出 ELF 文件中的程序头。我们列出 /usr/bin/ls 的程序头作为示例。正如我们在程序头的 INTERP 条目中看到的,可执行文件需要动态链接器 /lib64/ld-linux-x86-64.so.2 作为解释器。

因此,当我们运行具有共享库依赖项的可执行文件时,动态链接器会间接运行。但是,也可以通过调用动态链接器来直接运行动态链接的程序。我们将在下一节中看到一个示例。

ldd 命令也可以列出动态链接器:

1
2
3
4
5
6
7
ldd /usr/bin/ls
linux-vdso.so.1 (0x00007fff54f89000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f1000b17000)
libcap.so.2 => /lib64/libcap.so.2 (0x00007f1000b0d000)
libc.so.6 => /lib64/libc.so.6 (0x00007f1000903000)
libpcre2-8.so.0 => /lib64/libpcre2-8.so.0 (0x00007f100086c000)
/lib64/ld-linux-x86-64.so.2 (0x00007f1000b74000)

动态链接器是输出中的最后一个条目。

3. 一个例子

在本节中,我们将使用示例来试验动态链接器。

3.1. 示例代码

我们将在示例中使用以下 C 程序 hello.c:

1
2
3
4
5
6
include <stdio.h>

void main(int ac, char **av)
{
printf("Hello %s\n", av[1]);
}

构建了可执行文件后,让我们运行它:

1
2
./hello Baeldung
Hello Baeldung

可执行文件按预期运行。

3.2. 显式使用动态链接器

现在,让我们使用动态链接器 /lib64/ld-linux-x86-64.so.2 运行 hello:

1
2
/lib64/ld-linux-x86-64.so.2 ./hello Baeldung
Hello Baeldung

可执行文件再次按预期运行。我们只需将可执行文件及其参数传递给动态链接器。

即使二进制文件没有执行权限,我们也可以使用动态链接器运行二进制文件:

1
2
3
4
5
chmod -x hello
ls -l hello
-rw-rw-r-- 1 centos centos 25680 Aug 27 05:17 hello
/lib64/ld-linux-x86-64.so.2 ./hello Baeldung
Hello Baeldung

如输出所示,使用 chmod -x hello 删除二进制 hello 的执行权限后,我们仍然可以使用动态链接器运行应用程序。

3.3. 显式设置动态链接器

正如我们已经看到的,(译者注:在某些系统下)gcc 默认将 /lib64/ld-linux-x86-64.so.2 设置为动态链接器。但是,可以在链接期间显式设置动态链接器。

首先,我们将 /lib64/ld-linux-x86-64.so.2 复制到 /tmp,名称为 my_ld.so

1
cp /lib64/ld-linux-x86-64.so.2 /tmp/my_ld.so

我们将在构建 hello 时指定 /tmp/my_ld.so 作为动态链接器:

1
gcc -o hello hello.c -Wl,-I/tmp/my_ld.so

gcc 的 -Wl 选项让我们可以传递链接器选项。在我们的例子中,-Wl,-I/tmp/my_ld.so 指定 /tmp/my_ld.so 用作动态链接器。让我们使用 readelf 检查程序头表中的 PT_INTERP 条目:

1
2
readelf -l ./hello | grep -B2 interpreter
INTERP 0x0000000000000318 0x0000000000400318 0x0000000000400318 0x000000000000000e 0x000000000000000e R 0x1 [Requesting program interpreter: /tmp/my_ld.so]

我们使用 grep 来优化 readelf 的输出。从输出中可以明显看出,动态链接器现在是 /tmp/my_ld.so。让我们使用 /tmp/my_ld.so 运行 hello:

1
2
/tmp/my_ld.so ./hello Baeldung
Hello Baeldung

程序按预期运行。因此,我们成功地更改了默认的动态链接器。

4. 结论

在本文中,我们讨论了动态链接器。我们看到动态链接器在生成进程时管理其共享库依赖关系方面发挥着至关重要的作用。

我们了解到 /lib64/ld-linux-x86-64.so.2 (译者注:在某些系统下)是默认的动态链接器。然后,我们讨论了一个例子。我们看到我们可以在链接程序时更改动态链接器。

翻译自:https://www.baeldung.com/linux/dynamic-linker