首页 > 系统相关 >Linux 内核如何装载和启动一个可执行程序

Linux 内核如何装载和启动一个可执行程序

时间:2025-01-02 11:33:58浏览次数:1  
标签:ELF 程序 可执行程序 内核 Linux 动态 链接 execve 加载

Linux 内核如何装载和启动一个可执行程序

1. Linux 可执行程序的加载和启动过程

Linux 加载和启动一个可执行文件的过程涉及以下步骤:

  1. 编译和链接:程序的源代码通过编译生成目标文件(通常为 .o 文件),这些文件包含二进制代码和符号信息。链接器负责将这些目标文件组合成一个可执行文件,链接阶段将不同模块组合并解析符号。最终生成的可执行文件通常为 ELF (Executable and Linkable Format) 格式。

  2. ELF 文件结构:ELF 文件包含多个段(sections),如 .text.data.bss.rodata 等。程序入口点存储在 ELF 头中,执行时会加载该入口点。ELF 支持静态和动态链接,静态链接会将所有库函数嵌入生成的 ELF 文件,而动态链接则将符号解析延迟到程序运行时。

  3. 加载和内存映射:当 execve 系统调用被调用时,内核会进行以下操作:

    • 检查用户对文件的访问权限。
    • 解析 ELF 文件头,并确定加载文件的各个段。
    • 分配内存,将 ELF 文件的各个段加载到对应的内存区域。
    • 如果是动态链接程序,还会加载所需的动态链接库。
    • 配置初始堆栈和环境变量。
    • 将进程的指令指针设置为 ELF 文件的入口点(即开始执行的位置)。
  4. 程序开始执行:内核将程序的入口地址设定在 CPU 的指令指针寄存器中,这样程序的执行从入口点开始。

2. 使用 exec 系列函数加载可执行文件

可以使用 exec 系列函数(例如 execvexecvp 等)来加载和执行可执行文件。exec 系列函数会用新程序替换当前进程映像,但保持原进程的进程 ID 和其他资源。

示例代码:调用 execv 执行另一个程序

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    char *args[] = {"/bin/ls", "-l", "/", NULL};  // 需要执行的命令
    printf("Executing ls -l /\n");
    
    if (execv("/bin/ls", args) == -1) {
        perror("execv failed");
    }
    
    printf("This line will not be printed if execv is successful\n");
    return 0;
}

01

在这段代码中,execv 替换当前进程的映像,如果执行成功,将不会返回,而是从新程序的入口点开始执行。

3. 动态链接库的两种使用方式

动态链接库的使用分为两种方式:

  1. 装载时动态链接:当可执行文件加载时,动态链接器会自动装载并解析所需的动态库。
gcc test2.c -o test2 -ldl
  1. 运行时动态链接:使用 dlopendlsym 等函数在程序运行时加载和使用共享库。

示例:运行时动态链接

#include <stdio.h>
#include <dlfcn.h>

int main() {
    void *handle;
    double (*cosine)(double);
    char *error;

    // 动态加载 math 库
    handle = dlopen("libm.so.6", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "%s\n", dlerror());
        return 1;
    }

    // 查找 cos 函数
    cosine = dlsym(handle, "cos");
    if ((error = dlerror()) != NULL)  {
        fprintf(stderr, "%s\n", error);
        return 1;
    }

    printf("cos(2.0) = %f\n", (*cosine)(2.0));

    dlclose(handle);
    return 0;
}

02

4. 使用 GDB 跟踪 execve 系统调用

可以使用 GDB 跟踪和分析 execve 系统调用。以下是具体步骤:

  1. 启动 GDB 并加载程序:

    gdb ./test
    
  2. execve 系统调用处设置断点:

    break execve
    
  3. 运行程序,并观察 execve 调用的参数和返回信息:

    run
    
  4. execve 执行时,GDB 会暂停程序运行,可以查看 execve 的参数和内核的处理流程。可通过查看寄存器和内存,深入了解进程在内核加载过程中的转换。

5. 可执行程序的启动位置和 execve 返回后的执行

  • 程序开始执行的位置:对于静态链接的可执行程序,程序入口地址由链接器在编译时确定,execve 将控制权转移至该地址。
  • 动态链接程序的加载差异:动态链接程序在 execve 返回时已经完成动态库的装载和符号解析,进入入口点之前动态链接器会完成所有所需的加载配置。
    以下是对 execve 系统调用加载可执行程序后执行流程的分析:

新的可执行程序执行位置

新的可执行程序的执行从其 入口地址 开始。对于 ELF 格式的可执行文件,入口地址在 ELF 头(ELF header)中指定。内核在加载可执行文件时会读取这个入口地址并将其设置为程序的起始指令地址。

  • 对于静态链接程序,入口地址直接指向程序的 main 函数所在位置。
  • 对于动态链接程序,入口地址指向动态链接器(如ld-linux.so)的入口位置,动态链接器会先完成对所需共享库的装载和符号解析,再跳转到程序的实际入口地址。

execve 系统调用返回后,新的可执行程序能顺利执行:

execve 被调用时,内核会将当前进程的用户态部分完全替换成新程序的代码和数据。具体过程如下:

  1. 内存空间清理和重新分配execve 会先清理当前进程的用户空间,将其原有代码段、数据段、堆栈等释放。
  2. 加载新程序:根据 ELF 文件头的信息,内核为新程序分配所需内存空间并将 ELF 各个段加载到内存中。
  3. 设置入口地址:内核将进程的程序计数器(PCEIP)指向 ELF 文件的入口地址,以确保新程序从指定位置开始执行。
  4. 初始化堆栈和环境:内核会将新程序的命令行参数和环境变量压入栈,并为进程设置合适的栈指针。

当这些步骤完成后,内核会将控制权交给新程序的入口地址,旧的程序完全被替换。这样,execve 就不会返回到旧程序,而是直接开始新程序的执行。

静态链接和动态链接程序在 execve 返回时的不同

静态链接动态链接程序的不同主要体现在加载库和符号解析的方式上:

  • 静态链接程序:静态链接程序在编译时将所有依赖的库函数直接嵌入可执行文件中。因此,execve 系统调用加载静态链接程序时,不需要额外的符号解析或库加载,加载完成后即可直接跳转到程序的入口点开始执行。

  • 动态链接程序:动态链接程序在加载时需要先加载动态链接器(如 /lib/ld-linux.so)。execve 系统调用在返回时,先将控制权交给动态链接器,动态链接器会装载和解析所需的共享库符号表,完成符号绑定后,再将控制权转交给程序的实际入口点。

6. 总结

Linux 内核通过 execve 系统调用加载和启动可执行程序,过程包括解析 ELF 文件、加载程序的各个段、配置堆栈和环境变量,并将程序的入口点设置为 CPU 的指令指针。对于静态链接的程序,所有库在编译时就已经嵌入,可直接执行;而对于动态链接的程序,内核需要先加载动态链接器,解析并装载共享库,完成符号解析后才会跳转到程序的入口点。execve 完全替换当前进程映像,使新程序从入口点开始执行,而不返回到旧程序。动态链接和静态链接的程序在 execve 返回时有所不同,静态链接程序无需额外的库加载,直接进入执行,而动态链接程序需要经过动态链接器的配置。

标签:ELF,程序,可执行程序,内核,Linux,动态,链接,execve,加载
From: https://www.cnblogs.com/Arisf/p/18647245

相关文章

  • 阅读教材《庖丁解牛Linux 操作系统分析》 第十章:KVM及虚拟机技术
    阅读教材《庖丁解牛Linux操作系统分析》第十章:KVM及虚拟机技术关于KVM(Kernel-basedVirtualMachine)和虚拟机技术,以下是一些常见的学习内容和问题:学习内容:KVM基础知识:KVM是Linux内核的虚拟化模块,通过硬件虚拟化支持(如IntelVT-x或AMD-V)提供虚拟机支持。理解KV......
  • Linux内核编译
    Linux内核编译实验内容下载内核源码:确定内核版本号uname-r在www.kernel.org选择接近的内核版本下载linux-6.6.60.tar.xz将压缩包解压到虚拟机目录中确认系统位数getconfLONG_BIT确认虚拟机保留足够大硬盘空间(20G)df-h编写自定义系统调用函数修改/kernel/sys......
  • 内核编译与系统调用
    Linux内核编译实验内容下载内核源码:确定内核版本号uname-r在www.kernel.org选择接近的内核版本下载linux-6.6.60.tar.xz将压缩包解压到虚拟机目录中确认系统位数getconfLONG_BIT确认虚拟机保留足够大硬盘空间(20G)df-h编写自定义系统调用函数修改/kernel/sys......
  • Linux模块与系统调用
    模块与系统调用1.编写内核模块代码首先,编写一个简单的“HelloWorld”内核模块文件hello_module.c。#include<linux/init.h>//用于宏__init和__exit#include<linux/module.h>//用于模块编程基本宏#include<linux/kernel.h>//用于printk宏MODULE_LI......
  • linux安装php运行环境
    https://github.com/wintercoder/datamaker开源的mysql做假数据项目 一、linux下安装php集成环境sudoapt-getinstallhttpdservicehttpdstart主配置目录:/etc/httpd/conf/主配置文件:/etc/httpd/conf/httpd.conf/子配置目录:/etc/httpd/conf.d/子配置文件:/etc/http......
  • Dify 框架连接 PGSQL 数据库与 Sandbox 环境下的 Linux 系统调用权限问题
    Dify框架连接PGSQL数据库与Sandbox环境下的Linux系统调用权限问题背景在使用Dify框架进行开发时,遇到了两个主要的技术挑战:代码节点连接到PGSQL(PostgreSQL)数据库。解决沙盒环境中由于系统调用限制导致的“operationnotpermitted”错误。本文档将详细描述如何解......
  • linux ubuntu更改软件源
    更换步骤sudocp/etc/apt/sources.list /etc/apt/sources.list.backsudovim/etc/apt/sources.list替换为下面内容debhttps://mirrors.ustc.edu.cn/ubuntu/bionicmainrestricteduniversemultiversedeb-srchttps://mirrors.ustc.edu.cn/ubuntu/bionicmainrestric......
  • linux mint安装hadoop
    一、安装安装sshopenssh-server 配置jdk环境变量~/.bashrc参考exportJAVA_HOME=/opt/jdk1.7.0_55/exportJRE_HOME=${JAVA_HOME}/jreexportCLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/libexportPATH=${JAVA_HOME}/bin:$PATH exportHADOOP_HOME=/opt/Servers/hadoo......
  • Linux安装nodejs npm
    1、检查whereisnodejswhereisnpm2、下载wget-chttps://npm.taobao.org/mirrors/node/v12.12.0/node-v12.12.0-linux-x64.tar.xztar-xvfnode-v12.12.0-linux-x64.tar.xzmvnode-v12.12.0-linux-x64nodejsmv/root/nodejs//usr/sbin/3、配置软连接ln-s/usr/sbin/......
  • 一文读懂 Linux top 命令
    一文读懂Linuxtop命令在Linux系统管理与性能调优的“武器库”中,top命令无疑是一把极为锋利的“利刃”。它能够实时动态地展示系统关键运行指标,犹如系统运行状态的“监控大屏”,为管理员和开发者快速洞察系统健康程度、资源分配状况提供关键信息。下面,就来详细拆解这个......