[TIL] 二进制是如何被执行的

行文会比较乱,因为 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 差异有时可以完美翻译,但有时也会像这个例子一样有缺憾。
  • 二进制格式是啥

    • 把内容组织进一个二进制文件的规则
    • 假设一个二进制格式 A,规则如下

      • 开头N个字节:我是格式 A、我的目标架构是 AMD64、我使用大端/小端、应该从哪个地址开始执行,吧啦吧啦
      • 接下来N个字节:可执行指令的范围
      • 接下来N个字姐:数据的范围
      • 可执行指令
      • 数据
    • OS 在执行这个二进制的时候会有这样的步骤

      • 解读开头 N 个字节,得到各种信息
      • 创建内存空间,加载可执行指令到内存
      • 从指定地址开始执行指令
  • 前面说 POSIX 没有 GUI 相关的 API,wine 是怎么把 windows 的 gui 应用跑在 linux 中的

    • wine 把 GUI 相关的 API 翻译为 X11
    • 想单独开一篇讲 X11(QT、GTK、GNOME 等一众小弟),感觉也很有意思

参考资料

https://www.geeksforgeeks.org...

你可能感兴趣的:([TIL] 二进制是如何被执行的)