动态链接程序的执行流程分析
本文详细分析了动态链接程序的执行流程。首先内核通过execve系统调用加载动态链接器而非直接执行程序,动态链接器完成库加载、符号解析和重定位等初始化工作后,才跳转到程序的_start函数。_start函数进一步调用__libc_start_main,最终执行用户main函数。文章通过内存布局示例展示了动态链接程序的实际加载情况,并与静态链接程序进行了对比,突出了动态链接在运行时解析和延迟绑定等方面
文章目录
示例代码
#include <stdio.h>
int main() {
printf("Hello Kernel\n");
sleep(2); // 添加 2 秒延迟
return 0;
}
编译成动态链接程序(默认)
gcc -o dlink link.c
执行流程详解
当动态链接程序加载完毕并完成内存地址更新后,程序确实从 _start 开始执行,但这个过程涉及多个步骤:
1. 内核加载阶段
execve 系统调用:内核解析 ELF 文件,发现 PT_INTERP 段
加载动态链接器:内核将动态链接器(如 /lib64/ld-linux-x86-64.so.2)加载到内存
设置入口点:内核将进程的入口点设置为动态链接器的入口地址,而非主程序的 _start
2. 动态链接器执行阶段
初始化:动态链接器自身初始化
加载依赖库:读取 PT_DYNAMIC 段,加载所有依赖的共享库
符号解析:解析所有外部符号引用
重定位:更新 GOT/PLT 表,修正内存地址
初始化全局构造函数:执行 init_array 中的初始化函数
3. 控制权转移
查找主程序入口点:动态链接器查找主程序的 _start 函数地址
跳转到 _start:动态链接器通过 dl_runtime_resolve 等机制,最终跳转到主程序的 _start 函数
4. 主程序执行阶段
_start 函数:
初始化栈帧
设置 argc、argv、envp 等参数
调用 __libc_start_main
__libc_start_main:
初始化 libc
调用全局构造函数
调用用户的 main 函数
处理 main 函数的返回值
调用全局析构函数
调用 exit 函数结束进程
关键技术点
1. 入口点设置
内核视角:内核只负责加载和设置初始入口点为动态链接器
动态链接器视角:动态链接器完成所有准备工作后,负责跳转到主程序的 _start
2. 符号解析与重定位
延迟绑定:默认情况下,动态链接器采用延迟绑定策略,只在函数首次调用时解析
GOT/PLT 机制:通过全局偏移表和过程链接表实现地址的动态解析
3. 与静态链接的对比

执行流程示意
┌─────────────────┐
│ 内核加载阶段 │
└────────┬────────┘
↓
┌─────────────────┐ 完成依赖加载和
│ 动态链接器执行 │────→ 符号解析重定位
└────────┬────────┘
↓
┌─────────────────┐
│ 跳转到 _start │
└────────┬────────┘
↓
┌─────────────────┐
│ __libc_start_main │
└────────┬────────┘
↓
┌─────────────────┐
│ 调用 main 函数 │
└─────────────────┘
技术细节
1. 内存布局示例
creek@:~/Cprogram$ ./dlink &
PID=$!
cat /proc/$PID/maps
kill $PID # 结束进程
[1] 218306
Hello Kernel
63ac752f1000-63ac752f2000 r--p 00000000 08:30 9424 /home/creek/Cprogram/dlink
63ac752f2000-63ac752f3000 r-xp 00001000 08:30 9424 /home/creek/Cprogram/dlink
63ac752f3000-63ac752f4000 r--p 00002000 08:30 9424 /home/creek/Cprogram/dlink
63ac752f4000-63ac752f5000 r--p 00002000 08:30 9424 /home/creek/Cprogram/dlink
63ac752f5000-63ac752f6000 rw-p 00003000 08:30 9424 /home/creek/Cprogram/dlink
63ac9f2d1000-63ac9f2f2000 rw-p 00000000 00:00 0 [heap]
7b5261200000-7b5261228000 r--p 00000000 08:30 17529 /usr/lib/x86_64-linux-gnu/libc.so.6
7b5261228000-7b52613b0000 r-xp 00028000 08:30 17529 /usr/lib/x86_64-linux-gnu/libc.so.6
7b52613b0000-7b52613ff000 r--p 001b0000 08:30 17529 /usr/lib/x86_64-linux-gnu/libc.so.6
7b52613ff000-7b5261403000 r--p 001fe000 08:30 17529 /usr/lib/x86_64-linux-gnu/libc.so.6
7b5261403000-7b5261405000 rw-p 00202000 08:30 17529 /usr/lib/x86_64-linux-gnu/libc.so.6
7b5261405000-7b5261412000 rw-p 00000000 00:00 0
7b526154b000-7b526154e000 rw-p 00000000 00:00 0
7b5261557000-7b5261559000 rw-p 00000000 00:00 0
7b5261559000-7b526155a000 r--p 00000000 08:30 17526 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7b526155a000-7b5261585000 r-xp 00001000 08:30 17526 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7b5261585000-7b526158f000 r--p 0002c000 08:30 17526 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7b526158f000-7b5261591000 r--p 00036000 08:30 17526 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7b5261591000-7b5261593000 rw-p 00038000 08:30 17526 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffe9efbe000-7ffe9efe0000 rw-p 00000000 00:00 0 [stack]
7ffe9effa000-7ffe9effe000 r--p 00000000 00:00 0 [vvar]
7ffe9effe000-7ffe9f000000 r-xp 00000000 00:00 0 [vdso]
[1]+ Terminated ./dlink
2. _start 函数的实现
_start 函数通常由编译器生成,位于 crt1.o 或类似的启动文件中:
void _start() {
// 初始化栈帧
// 设置 argc, argv, envp
// 调用 __libc_start_main
__libc_start_main(main, argc, argv, __libc_csu_init, __libc_csu_fini, environ);
}
3. 动态链接器的跳转机制
动态链接器通过以下步骤跳转到主程序的 _start:
从 ELF 头中获取主程序的入口地址(通常是 _start 的地址)
执行完所有初始化操作后,使用跳转指令(如 jmp)跳转到该地址
主程序从此开始执行,完全接管控制权
4. 地址空间布局
动态链接器:加载到内存的某个位置
主程序:加载到另一个位置(可能受 ASLR 影响)
共享库:加载到各自的位置(地址随机化)
总结
动态链接程序在加载完毕并更新内存地址后,确实从 _start 开始执行,但这个过程是由动态链接器在完成所有准备工作后主动跳转到 _start 的。整个流程体现了动态链接的灵活性和延迟绑定的优势,同时也展示了内核与用户态链接器的协作机制。
更多推荐

所有评论(0)