行文会比较乱,因为 TIL 主要目的是组织自己的想法而非分享。如果凑巧能帮到别人就更好了,有感兴趣的部份觉得没讲清楚的,可以留言,我可以尝试进一步说明。
要执行一个二进制,同时需要 CPU 和操作系统的配合。
CPU
- CPU 可以解读针对某种指令集的二进制指令(比如x86、amd64)
- 有些 CPU 支持扩展指令集,即不包含在指令集规范中的额外的指令。在编译二进制时如果启用了这些扩展,就可以在支持扩展指令集的 CPU 中用上这些特性。可以在运行时检查 CPU 是否支持某种扩展,所以编译器通常会提供一条不支持的情况下用于降级的分支。
OS
- 一个二进制可能会用到一些别的库,比如用了 windows api 的二进制就不能运行在 linux 上。Unix 兼容系统中,关键的系统 api 被标准化为 POSIX 标准。如果一个二进制只用了 POSIX api,它就可以在任何 Unix 系统中运行,比如 Mac OS X 或 Solaris 等
二进制格式
- 除此之外,二进制必须选择遵守某种 OS 要求的二进制格式,才能被 OS 加载、执行。如 windows 中广泛使用的 Portable Executable format(exe 文件),Linux 中广泛使用的ELF format(Executable and Linkable Format)
最终,如果两个系统:
- 有同样的 System API、系统库
- 同样的指令集
- 使用同样的二进制格式
为一个系统编写的二进制就可以在另一个系统中运行。
x86 指令集的二进制可以运行在 AMD64 指令集中
- 二进制可以指定 CPU 用什么模式运行自己
有一些二进制格式允许把多个程序放在同一个文件中,每个程序针对一种不同的指令集
- 苹果从 PowerPC 架构转移到 x86 架构时就用上了 “fat binaries”
有一些程序会编译成中间形式,而非最终在 CPU 上执行的二进制
- 比如 java 的字节码、v8 也有字节码的优化
- 这种方案要求在不同的指令集上实现各自的虚拟机,以运行中间代码,转换成 CPU 能运行的真实指令集
杂记
本文相关的一些概念,想到的都放这
- 当 OS 加载一个二进制的时候,它会决定二进制被如何运行。可以在编译时指定一个目标 CPU,如果不指定编译器通常使用当前的 CPU 并只使用这个 CPU 和它更低版本支持的指令集。如果你想用只在你的 CPU 上支持的新指令,则需要告知编译器,或使用汇编自行编写。但这样一来,你得到的二进制如果在不支持改指令的 CPU 上运行,就会崩溃。
指令集的由来
- 最初的二进制都是为特定的某一款 CPU 编写的,无法执行在另一款中,为了解决这个问题提出了指令集,不同的 CPU 只要支持了同样的指令集,同样的二进制就可以运行在这些不同的 CPU 中
交叉编译
- 在一个系统中为另一个不同的系统编译二进制
- 比如在 windows 中编译出 linux 中可以运行的二进制
只要两个设备的 CPU 支持相同的指令集,相同的二进制就可以在两个设备上运行么
- 不是,还要看 OS 的 System API 是否兼容
- 比如同样的硬件可以安装 linux、windows 系统,windows 中的应用就不能在 linux 中运行
指令集是啥
CPU 支持的一系列指令,比如我们有一个 CPU 支持以下指令
- 写入寄存器a
- 读取寄存器a
- 写入寄存器b
- 读取寄存器b
- 将第一个、第二个数相加,结果写入寄存器a
我们写了一段代码“1x3+2=?”它为目标,编译出来的二进制就是:
- 将 1 写入寄存器a
- 将 1 写入寄存器b
- 相加
- 将 1 写入寄存器b
- 相加
- 将 2 写入寄存器b
- 相加
- 读取寄存器a
上面的指令是可以在这个 CPU 中执行的,但如果你写了这样的二进制
- 将 1 写入寄存器a
- 将 3 写入寄存器b
- 相乘 <<<<
- 将 3 写入寄存器b
- 相加
- 读取寄存器a
- 这个二进制就不能在该 CPU 中执行,因为用到了一个它不支持的指令“相乘”
System API 是啥
- 操作系统提供的 API
常见的有
- POSIX:UNIX、LINUX、Mac OS X 等
- Win32: Windows
- Java 虚拟机提供给字节码的 API 也算是一种 System API
- 并不完全对标,比如 Win32 中包含 GUI 相关的 API,创建窗口之类的,但 POSIX 中没有
假设有这样一个 OS A,他的窗口必须有,且只有一个标题,当二进制想要创建一个自己的窗口,可以调用这个 OS 提供的一个 API
create_window("Hello world")
- 这样就能创建出一个标题为 Hello world 的窗口
然后再假设另一个 OS B,它的窗口必须有标题和副标题,它提供的 API 可能就是
- `create_window("Hello world", "subtitle")
- 如果在这个 OS 中执行前面的二进制,显然缺少参数,它们是不兼容的
- 因此尽管 CPU 可以执行二进制,但 System API 不兼容的情况下,程序依然可能崩溃
wine 可以让 windows 应用在 linux 中执行,一部分原理就是翻译这种系统调用
- 以前面的创建窗口为例,为了在 OS B 中运行 OS A 的应用,wine 这种应用可能在收到
create_window("Hello world")
时,将它翻译为create_window("Hello world", "__SPOOF__")
,这样一来 OS B 得到了自己想要的两个参数。但缺陷是所有通过兼容层运行的 OS A 的应用,都会同样的副标题“__SUB_TITLE__
”。这种 System API 差异有时可以完美翻译,但有时也会像这个例子一样有缺憾。
- 以前面的创建窗口为例,为了在 OS B 中运行 OS A 的应用,wine 这种应用可能在收到
二进制格式是啥
- 把内容组织进一个二进制文件的规则
假设一个二进制格式 A,规则如下
- 开头N个字节:我是格式 A、我的目标架构是 AMD64、我使用大端/小端、应该从哪个地址开始执行,吧啦吧啦
- 接下来N个字节:可执行指令的范围
- 接下来N个字姐:数据的范围
- 可执行指令
- 数据
OS 在执行这个二进制的时候会有这样的步骤
- 解读开头 N 个字节,得到各种信息
- 创建内存空间,加载可执行指令到内存
- 从指定地址开始执行指令
前面说 POSIX 没有 GUI 相关的 API,wine 是怎么把 windows 的 gui 应用跑在 linux 中的
- wine 把 GUI 相关的 API 翻译为 X11 的
- 想单独开一篇讲 X11(QT、GTK、GNOME 等一众小弟),感觉也很有意思
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。