示例代码

#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	内核设置为动态链接器入口,动态链接器后续跳转到 _start
符号解析	编译时完成	运行时由动态链接器完成
重定位	编译时完成	运行时由动态链接器完成

执行流程示意

┌─────────────────┐
│ 内核加载阶段    │
└────────┬────────┘
         ↓
┌─────────────────┐     完成依赖加载和
│ 动态链接器执行  │────→ 符号解析重定位
└────────┬────────┘
         ↓
┌─────────────────┐
│ 跳转到 _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 的。整个流程体现了动态链接的灵活性和延迟绑定的优势,同时也展示了内核与用户态链接器的协作机制。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐