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 设置执行环境与跳转

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

调用流程: _start__libc_start_mainmain

调用流程maincallq ProcessOnProcessOn 执行 → retq 返回 mainmain 继续执行。

2.3 调用main函数

调用main函数进入c代码。

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

进程切换内核代码:

相关推荐
脏脏a1 小时前
【Linux】进程优先级:谁先 “上车” 谁说了算?
linux·运维·服务器
要站在顶端2 小时前
Jenkins 多分支流水线配置教程
运维·servlet·jenkins
松涛和鸣2 小时前
22、双向链表作业实现与GDB调试实战
c语言·开发语言·网络·数据结构·链表·排序算法
666HZ6668 小时前
C语言——高精度加法
c语言·开发语言·算法
666HZ6669 小时前
C语言——黑店
c语言·开发语言
屿行屿行9 小时前
【Linux】Socket编程(基于实际工程分析)
linux·服务器·网络
天才程序YUAN10 小时前
从零开始、保留 Windows 数据、安装Ubuntu 22.04 LTS双系统
linux·windows·ubuntu
Evan芙10 小时前
Rocky Linux 9 网卡改名及静态IP地址配置完整步骤
linux·网络·智能路由器
Zeku10 小时前
20251125 - 韦东山Linux第三篇笔记【上】
linux·笔记·单片机
企鹅侠客10 小时前
Linux性能调优 详解磁盘工作流程及性能指标
linux·运维·服务器·性能调优