Linux可执行程序的生成和运行详解
01.可执行程序的生成流程
我们已知用高级语言编写的程序无法直接被机器识别,需要被编译成机器指令才能被机器识别,在此涉及四个过程:
1.1 预处理
头文件包含,宏定义替换,注释删除操作。
gcc -E myexe.c -o myexe.i # 仅执行预处理相关操作
1.2 编译
将预处理后的代码转换为汇编代码,并进行语义/语法分析和符号汇总。
gcc -S myexe.i -o myexe.s # 执行编译生成汇编代码
1.3 汇编
将汇编代码转换为机器码,生成可重定位目标文件。
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
排除问题。
// 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
技术(下面有介绍),父子共享内存,知道一方写操作
sequenceDiagram
participant 用户
participant Shell
participant 操作系统内核
用户 ->> Shell: 输入 ./myprocess
Shell ->> Shell: 解析命令,检查路径
Shell ->> 操作系统内核: fork() 创建子进程
操作系统内核 -->> Shell: 返回子进程PID
Shell ->> 操作系统内核: 父进程调用 wait()
后续父进程P(wait
)操作等待子进程执行完毕,避免产生僵尸进程。
2.2 execve加载ELF文件
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代码。
完整流程图:
进程切换内核代码: