『7x24小时有问必答』
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

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

Hello, World!

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

bash: 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.c

hello.c: C  source, ASCII text

$ file hello

hello: ELF 64-bit LSB pie executable, x86-64, dynamically linked, not stripped

$ file test.sh

test.sh: Bourne-Again shell script, ASCII text executable

ELF 就是 Linux 下的可执行文件格式,相当于 Windows 的 PE(.exe)。脚本则是不带二进制头的纯文本。
本节要点:内核靠文件头几个字节判断类型。\x7fELF  走二进制路径,#!  走脚本路径。这个判断发生在  execve()  系统调用内部。

---

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

3.1 ELF 文件长什么样

一个 C 程序编译后变成 ELF 文件,结构如下:
$ readelf -h hello

ELF 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 ./hello

execve("./hello", ["./hello"], 0x7ffd12345678 /* 50 vars */) = 0

brk(NULL)                                              = 0x55555576a000

mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1234567000

access("/etc/ld.so.preload", R_OK)         = -1 ENOENT (No such file or directory)

openat(AT_FDCWD,  "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3

fstat(3, {st_mode=S_IFREG|0644, st_size=123456, ...}) = 0

mmap(NULL, 123456, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f1234548000

close(3)                                                = 0

openat(AT_FDCWD,  "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3

read(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) = 832

mmap(NULL, 1990256, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f1234361000

mmap(0x7f1234388000, 1449984, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x27000) = 0x7f1234388000

mmap(0x7f12344ea000, 348160, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x189000) = 0x7f12344ea000

mmap(0x7f123453f000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1dd000) = 0x7f123453f000

close(3)                                                = 0

write(1,  "Hello, World!\n", 14)             = 14

exit_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/bash

echo  "Hello from bash"

#!/usr/bin/env python3

print("Hello from Python")

#!  后面跟的是解释器的绝对路径。/usr/bin/env  是变通方案——它会在 PATH 里找  python3,避免硬编码路径。
没有 shebang 会怎样?shell 会自己上阵,用默认 shell 解释它。但这是不可靠的行为,别依赖。

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

$ strace ./test.sh

execve("./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  -20

execve("./test.sh", ["./test.sh"], ...) = 0

brk(NULL)                                              = 0x55555576a000

...(bash 自身的初始化)

openat(AT_FDCWD,  "./test.sh", O_RDONLY) = 3

read(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  -5

0000000000001060 <_start>:

      1060:   f3 0f 1e fa                  endbr64

      1064:   31 ed                           xor      %ebp,%ebp

# 脚本:直接就能看

$  cat  test.sh

#!/bin/bash

echo  "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 个字节里。

延伸阅读

《上电≠跑main:MCU和Linux启动前到底偷偷干了多少事?》:如果你想了解芯片上电后、进 main 之前发生了什么——硬件复位、向量表、启动文件、Bootloader 接力,可以从这篇开始,再回头读上面的程序执行细节。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

上一主题上一主题         下一主题下一主题
QQ手机版小黑屋粤ICP备17165530号

关于我们·投诉举报· 用户帮助· 联系我们 · 本站服务 · 版权声明· 隐私政策 · 投搞指南

法律保护:PLC技术网,plcjs.com,plcjs.net等字样
Copyright 2010-2030. All rights reserved. 


微信公众号二维码 抖音二维码 百家号二维码 今日头条二维码哔哩哔哩二维码