Linux下可执行程序的生成和运行详解(编译链接汇编图解)

Linux可执行程序的生成和运行详解

01.可执行程序的生成流程

我们已知用高级语言编写的程序无法直接被机器识别,需要被编译成机器指令才能被机器识别,在此涉及四个过程:

1.1 预处理

头文件包含,宏定义替换,注释删除操作。

bash 复制代码
gcc -E myexe.c -o myexe.i  # 仅执行预处理相关操作

1.2 编译

将预处理后的代码转换为汇编代码,并进行语义/语法分析 和符号汇总。

bash 复制代码
gcc -S myexe.i -o myexe.s  # 执行编译生成汇编代码

1.3 汇编

将汇编代码转换为机器码,生成可重定位目标文件。

bash 复制代码
gcc -c myexe.s -o myexe.o  # 汇编为机器指令

1.4 链接

将多个目标文件(.o)和库文件(.a 或 .so) 合并为可执行文件,解决符号引用。

1.4.1 动/静态库
  • 动态链接:节省内存,支持库更新;
  • 静态链接:部署简单,无外部依赖。

动态库也称为共享库,系统一般自带动态库,静态库需要手动安装。系统给我们提供了标准库.h(告诉怎么用),标准的动静态库.so/.a(告诉我们,方法实现我有,来找我)。可执行程序=我的代码+库的代码。

1.4.2 多文件编译

链接过程示意图:

在不用makefile条件下的多文件编译。如果出现了问题 使用gcc -v ,--help,-g排除问题。

c 复制代码
// process.c
#include"process.h"
void ProcessOn(){//函数的定义
  //循环101次
  int cnt=0;
  char bar[NUM];
  char style[S_NUM]={'-','+','=','_','.'};
  const char *lable="|\\-/";
  memset(bar,'\0',sizeof(bar));
  while(cnt<=100){
  printf("[%-100s][%-3d%%][%c]\r",bar,cnt,lable[cnt%4]);
  fflush(stdout);
  bar[cnt++]=style[Npp];
  usleep(50000);  
  }
  printf("\n");
}
// process.h
#pragma once
#include<stdio.h>
#include<string.h>
#define NUM 101
#define S_NUM 5
extern void ProcessOn();//函数声明
// main.c
#include"process.h"
int main(){
  ProcessOn();//函数调用
  printf("hello world!\n");
  return 0;
}

通过gcc -v process.c可知文件在当前目录根/usr/这些路径下搜索process.c。如果编译器找不到在末尾加上I/目录指定即可。


02.操作系统执行程序步骤

在Linux环境下可执行文件的格式是ELF,在Windows下可执行程序是EXE。

直接 exec 而不用 fork可以吗?

exec 会替换当前进程的代码,若直接调用,父进程(如Shell)会被覆盖,无法继续运行。

fork + execve 的设计优势是什么?

COW 机制下,fork 的实际开销很小,后紧随 execve 时,未修改的内存页无需复制性能高效。子进程的失败不会影响父进程。

2.1 fork创建子进程

  • 分配PCB设置并子进程的PID和状态
  • 复制父进程fd,页表等资源
  • 使用COW技术(下面有介绍),父子共享内存,知道一方写操作
MERMAID 复制代码
sequenceDiagram
    participant 用户
    participant Shell
    participant 操作系统内核
    用户 ->> Shell: 输入 ./myprocess
    Shell ->> Shell: 解析命令,检查路径
    Shell ->> 操作系统内核: fork() 创建子进程
    操作系统内核 -->> Shell: 返回子进程PID
    Shell ->> 操作系统内核: 父进程调用 wait()

后续父进程P(wait)操作等待子进程执行完毕,避免产生僵尸进程。

2.2 execve加载ELF文件

c 复制代码
int execve(const char *path, char *const argv[], char *const envp[]);//man 2execve详见该系统调用 
//eg
char* argv[] = {"./a.out", "arg1", NULL};
execve("./a.out", argv, environ); // environ 为全局环境变量
  • 参数 :
    • path:可执行文件的完整路径
    • argv:参数数组, NULL 结尾。 0位置一般是程序名称
    • envp:自定义环境变量数组(详见进程控制末介绍)
  • 返回值 :
    • 成功:替换原进程的代码段
    • 失败:返回 -1,并设置 errno
2.2 .1 权限检测

检查调用者和目标文件 (如ELF)的有效性。

2.2.2 加载ELF

从磁盘读取目标文件到内存并解析ELF Header,获取入口地址等信息。

2.2.3 替换进程地址空间
  • 验证ELF
  • 以不创建新进程的方式,销毁当前进程的代码段、数据段、堆栈,加载新程序到内存,替换当前进程的映像并且。
  • 初始化新程序执行环境
  • 动态链接
2.2.4 设置执行环境与跳转

将 argv 和 envp 复制 到新程序的栈中。从内核返回,重置PC(下一条将被执行指令的地址),跳转到ELF文件的入口地址_start。 execve是唯一能够执行程序的系统调用。

调用流程: _start → __libc_start_main → main

调用流程 :main → callq ProcessOn → ProcessOn 执行 → retq 返回 main → main 继续执行。

2.3 调用main函数

调用main函数进入c代码。

完整流程图:
用户 Shell 操作系统内核 磁盘 物理内存 子进程 输入 ./myprocess 解析命令,检查路径 fork() 创建子进程 返回子进程PID 父进程调用 wait() 写时复制(COW)优化 execve("./myprocess", argv, envp) 读取myprocess的ELF头 释放子进程原内存 按需加载代码/数据页 填充缺页内容 初始化寄存器,跳转到_start 执行myprocess的main() 执行完毕,exit() 父进程wait()返回 恢复提示符 用户 Shell 操作系统内核 磁盘 物理内存 子进程

进程切换内核代码:

相关推荐
律宏阔3 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔3 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴3 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千3 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei373 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei373 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei373 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
judezh3 小时前
api 容器一直 unhealthy?我把一个 Agent 运行时的健康检查逐条拆了,查出四个问题
运维·docker
GeW3 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
GeW3 小时前
工业场景中的Linux与数据库选型:高端化转型的技术底座
linux