当前位置:首页>Linux>Linux 程序到底是怎么跑起来的

Linux 程序到底是怎么跑起来的

  • 2026-10-11 06:33:11
Linux 程序到底是怎么跑起来的

Windows 里双击 .exe,程序就开了。Linux 桌面也能双击图标启动,但终端里敲 ./hello 和 ./test.sh 背后发生的事完全不同——一个由内核直接加载,一个要解释器代劳。用 strace 和 readelf 扒开来看,能清楚地看到两者的分界线。同一主题此前还写过一篇《上电≠跑 main:MCU 和 Linux 启动前到底偷偷干了多少事?》,那篇讲的是"上电之后到 main 之前":硬件复位、向量表、启动文件、Bootloader 接力。这篇则接着讲"main 之后":内核怎么识别 ELF 和脚本、execve() 内部做了什么。两篇合起来,覆盖从芯片上电到程序跑起来的完整路径。


一、先看现象:双击图标背后发生了什么

在 Windows 里,双击 notepad.exe,资源管理器调用 CreateProcess,系统加载 PE 文件,创建进程,程序就跑起来了。这套流程用户感知不到,只有一个沙漏转两圈。

Linux 桌面环境(GNOME / KDE)也能双击图标启动程序。桌面管理器读取 .desktop 文件里的 Exec= 字段,最终调用的还是同一个东西:

$ /usr/bin/firefox

但如果你在终端里跑一个自己编译的程序,就得这样:

$ ./helloHello, World!

注意那个 ./——它告诉 shell:"别去 PATH 里找,就执行当前目录下这个文件"。去掉试试:

$ hellobash: hello: command not found

Windows 会自动搜索当前目录,Linux 不会。这是安全设计,不是 bug。

本节要点:不管双击还是命令行,最终都汇入同一条路——execve() 系统调用。Linux 和 Windows 的关键区别不在 GUI,在内核怎么处理这个调用。


二、Linux 程序分两类:二进制与脚本

在 Linux 里,"程序"只有两种形态:

类型
本质
例子
怎么看
二进制可执行文件
CPU 直接执行的机器码
/bin/ls
、./hello
file
 显示 ELF
脚本文件
文本,需要解释器执行
./test.sh
、./run.py
file
 显示 script

用 file 命令一眼分辨:

$ file /bin/ls/bin/ls: ELF 64-bit LSB pie executable, x86-64, dynamically linked, stripped$ file hello.chello.c: C source, ASCII text$ file hellohello: ELF 64-bit LSB pie executable, x86-64, dynamically linked, not stripped$ file test.shtest.sh: Bourne-Again shell script, ASCII text executable

ELF 就是 Linux 下的可执行文件格式,相当于 Windows 的 PE(.exe)。脚本则是不带二进制头的纯文本。

本节要点:内核靠文件头几个字节判断类型。\x7fELF 走二进制路径,#! 走脚本路径。这个判断发生在 execve() 系统调用内部。


三、二进制程序:内核直接加载的 ELF

3.1 ELF 文件长什么样

一个 C 程序编译后变成 ELF 文件,结构如下:

$ readelf -h helloELF Header:  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00  Class:                             ELF64  Data:                              2's complement, little endian  OS/ABI:                            UNIX - System V  Type:                              DYN (Position-Independent Executable file)  Machine:                           Advanced Micro Devices X86-64  Entry point address:               0x1060

关键字段:Entry point address——内核加载完 ELF 后,就跳到这个地址开始执行。这个地址指向 _start,不是你的 main。

注意 Type 字段:现代 GCC 默认编译 PIE(Position-Independent Executable),Type 显示为 DYN 而非 EXEC。PIE 配合 ASLR(地址空间随机化)让程序每次加载到不同地址,是安全加固的一部分。如果用 gcc -no-pie,就会回到传统的 EXEC 类型,入口地址也变成 0x401000 级别。

ELF 文件在磁盘上是一段一段的,内核按照 Program Header 的指示,把各段映射到进程的虚拟地址空间。

3.2 strace 跟踪:从命令行到 main

用 strace 跟踪 ./hello,看看内核到底做了什么:

$ strace ./helloexecve("./hello", ["./hello"], 0x7ffd12345678 /* 50 vars */) = 0brk(NULL)                               = 0x55555576a000mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1234567000access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3fstat(3, {st_mode=S_IFREG|0644, st_size=123456, ...}) = 0mmap(NULL, 123456, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f1234548000close(3)                                = 0openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\220\223\2\0\0\0\0\0"..., 832) = 832mmap(NULL, 1990256, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f1234361000mmap(0x7f1234388000, 1449984, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x27000) = 0x7f1234388000mmap(0x7f12344ea000, 348160, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x189000) = 0x7f12344ea000mmap(0x7f123453f000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1dd000) = 0x7f123453f000close(3)                                = 0write(1, "Hello, World!\n", 14)         = 14exit_group(0)                           = ?

关键步骤拆解:

步骤
系统调用
做什么
1
execve
内核加载 ELF,创建进程地址空间
2
mmap
 × N
把 ELF 各段 + libc.so 映射到内存
3
brk
设置堆(heap)起始地址
4
跳转到 _start
C 运行时初始化,最终调用 main
5
write
你的 printf("Hello") 变成了 write(1, ...)
6
exit_group
进程退出

核心认知:execve 一做,老进程的地址空间全被替换,新程序接管一切。fork 创建子进程,execve 装载新程序——这是 Linux 创建进程的标准组合。

本节要点:二进制程序的执行链路是 execve → ELF 解析 → mmap 映射 → 动态链接 → _start → main。内核只认 ELF 格式,不关心你用什么语言写的。


四、脚本程序:解释器代劳的文本文件

4.1 脚本的结构

一个脚本文件就是普通文本,头顶必须有一行 shebang:

#!/bin/bashecho "Hello from bash"
#!/usr/bin/env python3print("Hello from Python")

#! 后面跟的是解释器的绝对路径。/usr/bin/env 是变通方案——它会在 PATH 里找 python3,避免硬编码路径。

没有 shebang 会怎样?shell 会自己上阵,用默认 shell 解释它。但这是不可靠的行为,别依赖。

4.2 strace 跟踪:脚本怎么跑起来的

$ strace ./test.shexecve("./test.sh", ["./test.sh"], 0x7ffd12345678 /* 50 vars */) = 0

看上去只有一个 execve,但实际上内核在 execve 内部做了这些事情:

strace 只看到一次 execve,因为内核在同一个系统调用内部完成了 shebang 解析和解释器加载。这个机制叫 binfmt_script,是 Linux 内核内置的二进制格式处理器之一,和 binfmt_elf(处理 ELF)平级。

# 内核注册的二进制格式处理器一览$ ls /proc/sys/fs/binfmt_misc/WSLInterop  python3.12  register  status

binfmt_misc 目录下还能看到其他注册的格式处理器——比如 WSL 就注册了 WSLInterop 来支持在 Linux 里直接运行 Windows 的 .exe。register 和 status 是管理接口,可以向内核动态注册新的二进制格式。

用 strace -f 跟踪子进程,可以看到 bash 启动后的完整行为:

$ strace -f ./test.sh 2>&1 | head -20execve("./test.sh", ["./test.sh"], ...) = 0brk(NULL)                               = 0x55555576a000...(bash 自身的初始化)openat(AT_FDCWD, "./test.sh", O_RDONLY) = 3read(3, "#!/bin/bash\necho \"Hello from b"..., 80) = 42

注意最后两行:bash 打开并读取了 test.sh 的内容。这说明解释器把脚本当数据读进去,然后逐行执行。

本节要点:脚本不是被内核直接执行的,而是被解释器读进去逐行处理的。shebang 只是告诉内核"去找谁读",真正的执行者是解释器。


五、二进制 vs 脚本:一张表说清区别

维度
二进制程序
脚本程序
文件格式
ELF(机器码)
纯文本(ASCII/UTF-8)
内核怎么识别
ELF 魔数 \x7fELF
shebang #!
谁执行
CPU 直接执行机器码
解释器逐行解释
execve
 行为
内核直接加载 ELF
内核启动解释器,脚本当参数
依赖
动态链接库(.so)
解释器必须已安装
启动速度
快(mmap + 跳转)
慢(启动解释器 + 解析文本)
跨平台
需重新编译
解释器能跑就能跑
可读性
二进制,不可读
文本,可直接编辑
典型后缀
无后缀 / .elf
.sh
.py.pl

一个更直观的对比:

# 二进制:查看需要反汇编$ objdump -d hello | head -50000000000001060 <_start>:    1060:  f3 0f 1e fa            endbr64    1064:  31 ed                  xor    %ebp,%ebp# 脚本:直接就能看$ cat test.sh#!/bin/bashecho "Hello from bash"

本节要点:二进制是"给你指令,CPU 自己跑";脚本是"给你脚本,解释器帮你跑"。一条 execve,内核走的却是两条完全不同的分支。


六、注意边界

坑
现象
解法
脚本没有执行权限
bash: ./test.sh: Permission deniedchmod +x test.sh
脚本没有 shebang
用当前 shell 执行,行为不可控
永远加 #!/bin/bash 或 #!/usr/bin/env python3
shebang 路径写错
bad interpreter: No such file or directory
确认解释器路径:which bash
Windows 换行符
/bin/bash^M: bad interpreterdos2unix
 或 sed -i 's/\r$//'
二进制架构不匹配
cannot execute binary file: Exec format errorfile
 查架构,交叉编译或用 QEMU
动态库缺失
error while loading shared libraries: libxxx.soldd
 查依赖,安装缺失的库
依赖库版本不匹配
version GLIBC_2.34 not found
升级 glibc 或静态链接编译

本节要点:脚本最大的坑是 shebang 和环境(换行符、权限);二进制最大的坑是依赖库和架构匹配。


七、总结

Linux 下执行一个程序,无论你是双击图标还是敲命令行,最终都汇入 execve() 这一条系统调用。内核打开文件,读前几个字节:

  • 看到 \x7fELF——二话不说,mmap 映射各段,加载动态链接库,跳到 _start,你的 main 就跑起来了。
  • 看到 #!——提取解释器路径,启动解释器,把脚本塞给它当参数,解释器逐行读、逐行执行。

下次在终端里敲 ./program 时,不妨想想内核正在读文件头的那几个字节,然后决定走哪条路。Linux 程序执行的秘密,藏在前 4 个字节里。

延伸阅读

最新文章

随机文章