计算机 programs/OSes 是否只包含低级别的 X86-64 指令?
Do computer programs/OSes consist of only the X86-64 instructions at low level?
很抱歉提出 newbie/stupid 问题,但这困扰了我一段时间,而且似乎很难找到直接的答案。问题是关于计算机如何在低级别工作——更具体地说,是否有计算机可以执行的命令不包含在 x86-64 指令中。换句话说,您可能会问 OS 是否仅使用 x86-64 指令进行编程,OS 运行的程序也是如此。请注意,我不是在询问隐藏命令或特定于处理器的其他命令,我们可以假设这些不存在。
提问的动机:
经常给出的说法是编译器将特定语言的程序编译成机器码。但是,有许多命令不能(据我所知)仅使用 x86-64 指令以汇编语言编写。甚至像“malloc”这样简单的东西。所以看起来为 OS 编写的实际程序由机器代码和 OS 指令组成?
如果查看x86-64指令集,似乎I/O命令如访问键盘、鼠标、硬盘、GPU、音频接口、时间、显示器, speakers 等等 并不是所有的都有命令,尽管 INT 命令可以用来完成一些任务。根据 this answer“在现代架构中,外围设备的访问方式与内存类似:通过总线上的映射内存地址。”,无论这在代码方面意味着什么。所以看起来甚至 OS 也不只写在 x86-64 指令中?
是的,CPUs 只能 运行 机器码(你可以 1:1 通过 asm 表示)。对于某些语言,ahead-of-time 编译器将源代码转换为可执行文件中的机器代码。
对于其他人,例如Java,通常 JIT-compile 在内存中的缓冲区中动态地机器代码,然后调用它。 (执行 JIT 编译的代码最初是用 C 语言编写的,但在 java
可执行文件本身中被编译 ahead-of-time 为机器代码)。
在其他语言实现中,您只有一个解释器:它是一个程序(通常用 ahead-of-time 编译语言编写,如 C 或 C++)读取文件(例如 bash
或 python
script) 并解析它,根据文件的内容决定用什么参数调用它的哪些现有函数。 运行s 的每条指令最初都在二进制文件中,但在该解释器代码中有条件 b运行ches 取决于文件中的 high-level-language 代码 运行 它上。
malloc
不是基本操作,它是一个库函数(编译为机器代码),可能会进行一些系统调用(涉及 运行在内核中加入一些机器代码)。
使用像 BOCHS 这样的 full-system 仿真器,您可以从字面上 single-step 机器指令通过任何程序进入系统调用,甚至用于中断处理程序。你永远不会发现 CPU 执行任何不是机器代码指令的东西;从字面上看,这是它的逻辑电路在从内存中获取后唯一知道如何解码的东西。 (能够被 CPU 解码是它成为机器码的原因)。
机器代码总是由一系列指令组成,每个 ISA 都有一种汇编语言,我们可以使用它来表示机器代码的 human-readable。 (相关:Why do we even need assembler when we have compiler? 回复:汇编语言的存在而不仅仅是机器代码)。
此外,任何给定 ISA 的指令格式至少在某种程度上是一致的。在 x86-64 上,它是一个 byte-stream 操作码、ope运行ds(modrm + 可选的其他字节)和可选的立即数。 (还有前缀……x86-64 有点乱。)在 AArch64 上,机器指令是 fixed-width 4 字节,在 4 字节边界上对齐。
"On modern architectures, peripherals are accessed in a similar way to memory: via mapped memory addresses on a bus."
这意味着执行像 x86-64 这样的存储指令 mov [rdi], eax
将 4 个字节存储到内存地址 = RDI。 CPU(或旧系统中的北桥)内部的逻辑根据地址而不是指令来决定给定的物理地址是 DRAM 还是 I/O。
或者 x86-64 有访问 I/O space 的指令(与内存 space 分开),比如 in
和 out
.
回复:新标题:
Do computer programs/OSes consist of only the x86-64 instructions at low level?
不,大多数程序和 OSes 还包含一些静态 read-write 数据(.data
)和 read-only 常量(.rodata
部分),而不是 纯 代码与常量仅作为直接 ope运行ds.
但是数据当然不是“运行”,所以这可能不是您的意思。所以是的,除非你想用固件玩语义。
一些现代 I/O 设备的驱动程序需要固件二进制 blob(其中一部分是嵌入在 GPU、声卡或其他任何东西中的微控制器的机器代码) .
从OS的角度来看,这只是二进制数据,它必须在响应 MMIO 操作之前发送到 PCIe 设备它的文档说它会的方式。 OS non-CPU 设备如何在内部使用该数据并不重要,无论它实际上是微控制器的指令还是只是声卡 MIDI 合成器的查找表和样本。
我认为你把这个复杂化了。处理器非常愚蠢,非常非常愚蠢,他们只做指令告诉他们做的事情。程序员最终负责在处理器前面铺设一条有效的、理智的指令路径,就像火车是愚蠢的,只沿着它的轨道行驶,如果我们不正确铺设轨道,火车就会脱轨。
编译器作为程序通常会从一种语言转换为另一种语言,不一定从 C 语言转换为机器代码。它可能来自谁知道 JAVA 到 C++ 或其他什么。并不是所有的C编译器都输出机器码,有些输出汇编语言然后调用汇编程序。
gcc hello.c -o hello
gcc 该程序主要只是一个调用预解析器的 shell 程序,它以递归方式执行诸如替换包含和定义之类的操作,以便该解析器的输出是单个文件可以提供给编译器。然后将该文件提供给编译器,编译器可能会生成其他文件或内部数据结构,最终实际编译器输出汇编语言。如上所示,然后 gcc 调用汇编程序将汇编语言转换为目标文件,其中包含尽可能多的机器代码,一些外部引用留给链接器,生成代码以按照理智的方式处理这些指令集。
链接器然后按照准备此工具链的人的指示将来自 binutils 的链接器与与工具链捆绑在一起的 C 库结合起来,或者由工具链指向并将 hello 目标文件与任何其他所需的库链接起来,包括 bootstrap,如上图所示,链接描述文件已准备好 by/for 使用了有问题的 C 库,因为在命令行中未指明。链接器的工作是将项目放置在要求的位置以及解析外部,有时会添加指令以将这些单独的对象粘合在一起,然后以构建工具链时设置为默认文件格式输出文件。然后 gcc 开始清理中间文件,无论是在运行过程中还是在结束时,无论如何。
直接编译为机器代码的编译器只是跳过了调用汇编程序的步骤,但是仍然需要将单独的对象和库与某种形式的关于地址 space 的指令链接到链接器。
malloc 不是一条指令,它是一个在编译该函数后以机器代码完全实现的函数,出于性能原因,C 库手动以汇编语言创建该函数的情况并不少见,要么它只是链接进来的一些其他代码。处理器只能执行在该处理器逻辑中实现的指令。
软件中断只是指令,当你执行软件中断时,它实际上无非是一个专门的函数调用,你调用的代码更多的是别人写的代码,编译成机器码,没有魔法.
处理器完全不知道 usb 或 pcie 或 gpu 等是什么。它只知道实现执行的指令集,仅此而已。编程语言甚至不知道所有其他高级概念,即使是 C、C++、JAVA 等高级概念,处理器也有一些加载和存储、内存或 I/O在 x86 的情况下,那些的序列和地址是程序员的工作,对处理器来说,它只是带有地址的指令,没什么特别的。地址是电路板系统设计的一部分,您在哪里以及如何到达 USB 控制器、PCIe 控制器、DRAM、视频等,board/chip 设计师和软件人员都知道这些地址在哪里,将代码写入 read/write 这些地址以使外围设备工作。
处理器只知道它被设计用来执行的指令,仅此而已,一般没有魔法。像 x86 这样的 CISC 处理器,由于每条指令过于复杂,历史上出于各种原因使用微代码实现。所以这是无魔法交易的例外。使用微代码在很多方面都比使用状态机分立地实现每条指令更便宜。该实现是状态机的某种组合,如果您将一些其他指令集与其他一些处理器结合使用,它并不是真正的解释性交易,而是一种从业务和工程角度来看都有意义的混合体。
RISC 概念是基于 CISC 几十年的历史以及产品和工具生产的改进,以及程序员能力的提高等。所以你现在看到许多 RISC 处理器是在没有微编码的情况下实现的,根据需要,小型状态机,但通常无法与 CISC 指令集要求相提并论。指令数量和代码 space 与芯片尺寸和性能(功率、速度等)之间存在权衡。
"On modern architectures, peripherals are accessed in a similar way to memory: via mapped memory addresses on a bus."
如果单纯看指令集最好看8088/86硬件和软件参考手册。然后检查现代处理器总线,今天总线上有许多控制信号,不仅指示读取与写入、地址和数据,还指示访问类型、可缓存与否等。回到 8088/86 时代,设计人员拥有外围设备有两种类型的控制的正确概念,一种是控制和状态寄存器,我想设置一个图形模式,即这么多像素乘这么多像素。我希望它有这么多颜色,并使用这种深度的调色板。然后你就有了你想要在大组中访问的实际像素,理想情况下,在 loop/burst 副本中一次一个扫描线一个帧。因此,对于控制寄存器,您通常会随机地一次访问一个。对于像素内存,您通常会一次访问多个字节。
所以在总线上有一个位指示 I/O 与内存比较有意义,记住我们还没有 fpgas,asics 几乎是 unobtanium,所以你想最好地帮助胶合逻辑你可以,所以在这里或那里添加一个控制信号会有所帮助。今天的部分原因是相对而言,生产 asics 的成本和风险更便宜,工具更好,程序员的技能和他们做事的方式都有所进步。过去帮助我们的事情可能会成为障碍,因此控制与内存的概念在外围设备中仍然非常普遍,但我们不一定需要控制信号或单独的指令。如果你在 8088/86 之前回退到一些 DEC 处理器,你有针对外围设备的特定指令,你想输出一个字符到 tty,那里有一个指令,而不仅仅是你写入的地址。这是自然的进展,今天只是让所有内容都映射到内存并使用通用加载和存储指令。
我不明白你是如何得到 I/O 与内存的,这意味着没有 x86 机器代码,只需查看指令集即可看到 I/O 指令和内存指令。它们在那里,出于反向兼容性的原因,这正是 Wintel pc 世界几十年来保持活力的原因它们仍然可以工作,但它们被合成为更接近内存映射解决方案的东西,同时程序员已经从 I/O映射,理想情况下只有非常非常旧的代码会尝试这样做,并且硬件和软件的组合仍然可以使某些代码在现代 pc 上运行。
处理器执行指令流。这些指令流是机器代码:由处理器执行的以机器语言编写的程序。
各种指令流有多种用途:一些加载程序,一些将处理器从一个指令流(程序)切换到另一个,一些保护其他代码,一些处理设备 i/o,一些是用户应用程序,如数据库,或汇编器、编译器、链接器、调试器。
处理器只知道机器语言,以及如何执行它。它甚至不知道变量声明 — 由机器代码序列来确保 proper/consistent 处理程序变量。
malloc
是用一种算法(作为参数化函数)实现的,它被编码为一个指令流,可以被另一个指令流“调用”/调用。
很抱歉提出 newbie/stupid 问题,但这困扰了我一段时间,而且似乎很难找到直接的答案。问题是关于计算机如何在低级别工作——更具体地说,是否有计算机可以执行的命令不包含在 x86-64 指令中。换句话说,您可能会问 OS 是否仅使用 x86-64 指令进行编程,OS 运行的程序也是如此。请注意,我不是在询问隐藏命令或特定于处理器的其他命令,我们可以假设这些不存在。
提问的动机:
经常给出的说法是编译器将特定语言的程序编译成机器码。但是,有许多命令不能(据我所知)仅使用 x86-64 指令以汇编语言编写。甚至像“malloc”这样简单的东西。所以看起来为 OS 编写的实际程序由机器代码和 OS 指令组成?
如果查看x86-64指令集,似乎I/O命令如访问键盘、鼠标、硬盘、GPU、音频接口、时间、显示器, speakers 等等 并不是所有的都有命令,尽管 INT 命令可以用来完成一些任务。根据 this answer“在现代架构中,外围设备的访问方式与内存类似:通过总线上的映射内存地址。”,无论这在代码方面意味着什么。所以看起来甚至 OS 也不只写在 x86-64 指令中?
是的,CPUs 只能 运行 机器码(你可以 1:1 通过 asm 表示)。对于某些语言,ahead-of-time 编译器将源代码转换为可执行文件中的机器代码。
对于其他人,例如Java,通常 JIT-compile 在内存中的缓冲区中动态地机器代码,然后调用它。 (执行 JIT 编译的代码最初是用 C 语言编写的,但在 java
可执行文件本身中被编译 ahead-of-time 为机器代码)。
在其他语言实现中,您只有一个解释器:它是一个程序(通常用 ahead-of-time 编译语言编写,如 C 或 C++)读取文件(例如 bash
或 python
script) 并解析它,根据文件的内容决定用什么参数调用它的哪些现有函数。 运行s 的每条指令最初都在二进制文件中,但在该解释器代码中有条件 b运行ches 取决于文件中的 high-level-language 代码 运行 它上。
malloc
不是基本操作,它是一个库函数(编译为机器代码),可能会进行一些系统调用(涉及 运行在内核中加入一些机器代码)。
使用像 BOCHS 这样的 full-system 仿真器,您可以从字面上 single-step 机器指令通过任何程序进入系统调用,甚至用于中断处理程序。你永远不会发现 CPU 执行任何不是机器代码指令的东西;从字面上看,这是它的逻辑电路在从内存中获取后唯一知道如何解码的东西。 (能够被 CPU 解码是它成为机器码的原因)。
机器代码总是由一系列指令组成,每个 ISA 都有一种汇编语言,我们可以使用它来表示机器代码的 human-readable。 (相关:Why do we even need assembler when we have compiler? 回复:汇编语言的存在而不仅仅是机器代码)。
此外,任何给定 ISA 的指令格式至少在某种程度上是一致的。在 x86-64 上,它是一个 byte-stream 操作码、ope运行ds(modrm + 可选的其他字节)和可选的立即数。 (还有前缀……x86-64 有点乱。)在 AArch64 上,机器指令是 fixed-width 4 字节,在 4 字节边界上对齐。
"On modern architectures, peripherals are accessed in a similar way to memory: via mapped memory addresses on a bus."
这意味着执行像 x86-64 这样的存储指令 mov [rdi], eax
将 4 个字节存储到内存地址 = RDI。 CPU(或旧系统中的北桥)内部的逻辑根据地址而不是指令来决定给定的物理地址是 DRAM 还是 I/O。
或者 x86-64 有访问 I/O space 的指令(与内存 space 分开),比如 in
和 out
.
回复:新标题:
Do computer programs/OSes consist of only the x86-64 instructions at low level?
不,大多数程序和 OSes 还包含一些静态 read-write 数据(.data
)和 read-only 常量(.rodata
部分),而不是 纯 代码与常量仅作为直接 ope运行ds.
但是数据当然不是“运行”,所以这可能不是您的意思。所以是的,除非你想用固件玩语义。
一些现代 I/O 设备的驱动程序需要固件二进制 blob(其中一部分是嵌入在 GPU、声卡或其他任何东西中的微控制器的机器代码) .
从OS的角度来看,这只是二进制数据,它必须在响应 MMIO 操作之前发送到 PCIe 设备它的文档说它会的方式。 OS non-CPU 设备如何在内部使用该数据并不重要,无论它实际上是微控制器的指令还是只是声卡 MIDI 合成器的查找表和样本。
我认为你把这个复杂化了。处理器非常愚蠢,非常非常愚蠢,他们只做指令告诉他们做的事情。程序员最终负责在处理器前面铺设一条有效的、理智的指令路径,就像火车是愚蠢的,只沿着它的轨道行驶,如果我们不正确铺设轨道,火车就会脱轨。
编译器作为程序通常会从一种语言转换为另一种语言,不一定从 C 语言转换为机器代码。它可能来自谁知道 JAVA 到 C++ 或其他什么。并不是所有的C编译器都输出机器码,有些输出汇编语言然后调用汇编程序。
gcc hello.c -o hello
gcc 该程序主要只是一个调用预解析器的 shell 程序,它以递归方式执行诸如替换包含和定义之类的操作,以便该解析器的输出是单个文件可以提供给编译器。然后将该文件提供给编译器,编译器可能会生成其他文件或内部数据结构,最终实际编译器输出汇编语言。如上所示,然后 gcc 调用汇编程序将汇编语言转换为目标文件,其中包含尽可能多的机器代码,一些外部引用留给链接器,生成代码以按照理智的方式处理这些指令集。
链接器然后按照准备此工具链的人的指示将来自 binutils 的链接器与与工具链捆绑在一起的 C 库结合起来,或者由工具链指向并将 hello 目标文件与任何其他所需的库链接起来,包括 bootstrap,如上图所示,链接描述文件已准备好 by/for 使用了有问题的 C 库,因为在命令行中未指明。链接器的工作是将项目放置在要求的位置以及解析外部,有时会添加指令以将这些单独的对象粘合在一起,然后以构建工具链时设置为默认文件格式输出文件。然后 gcc 开始清理中间文件,无论是在运行过程中还是在结束时,无论如何。
直接编译为机器代码的编译器只是跳过了调用汇编程序的步骤,但是仍然需要将单独的对象和库与某种形式的关于地址 space 的指令链接到链接器。
malloc 不是一条指令,它是一个在编译该函数后以机器代码完全实现的函数,出于性能原因,C 库手动以汇编语言创建该函数的情况并不少见,要么它只是链接进来的一些其他代码。处理器只能执行在该处理器逻辑中实现的指令。
软件中断只是指令,当你执行软件中断时,它实际上无非是一个专门的函数调用,你调用的代码更多的是别人写的代码,编译成机器码,没有魔法.
处理器完全不知道 usb 或 pcie 或 gpu 等是什么。它只知道实现执行的指令集,仅此而已。编程语言甚至不知道所有其他高级概念,即使是 C、C++、JAVA 等高级概念,处理器也有一些加载和存储、内存或 I/O在 x86 的情况下,那些的序列和地址是程序员的工作,对处理器来说,它只是带有地址的指令,没什么特别的。地址是电路板系统设计的一部分,您在哪里以及如何到达 USB 控制器、PCIe 控制器、DRAM、视频等,board/chip 设计师和软件人员都知道这些地址在哪里,将代码写入 read/write 这些地址以使外围设备工作。
处理器只知道它被设计用来执行的指令,仅此而已,一般没有魔法。像 x86 这样的 CISC 处理器,由于每条指令过于复杂,历史上出于各种原因使用微代码实现。所以这是无魔法交易的例外。使用微代码在很多方面都比使用状态机分立地实现每条指令更便宜。该实现是状态机的某种组合,如果您将一些其他指令集与其他一些处理器结合使用,它并不是真正的解释性交易,而是一种从业务和工程角度来看都有意义的混合体。
RISC 概念是基于 CISC 几十年的历史以及产品和工具生产的改进,以及程序员能力的提高等。所以你现在看到许多 RISC 处理器是在没有微编码的情况下实现的,根据需要,小型状态机,但通常无法与 CISC 指令集要求相提并论。指令数量和代码 space 与芯片尺寸和性能(功率、速度等)之间存在权衡。
"On modern architectures, peripherals are accessed in a similar way to memory: via mapped memory addresses on a bus."
如果单纯看指令集最好看8088/86硬件和软件参考手册。然后检查现代处理器总线,今天总线上有许多控制信号,不仅指示读取与写入、地址和数据,还指示访问类型、可缓存与否等。回到 8088/86 时代,设计人员拥有外围设备有两种类型的控制的正确概念,一种是控制和状态寄存器,我想设置一个图形模式,即这么多像素乘这么多像素。我希望它有这么多颜色,并使用这种深度的调色板。然后你就有了你想要在大组中访问的实际像素,理想情况下,在 loop/burst 副本中一次一个扫描线一个帧。因此,对于控制寄存器,您通常会随机地一次访问一个。对于像素内存,您通常会一次访问多个字节。
所以在总线上有一个位指示 I/O 与内存比较有意义,记住我们还没有 fpgas,asics 几乎是 unobtanium,所以你想最好地帮助胶合逻辑你可以,所以在这里或那里添加一个控制信号会有所帮助。今天的部分原因是相对而言,生产 asics 的成本和风险更便宜,工具更好,程序员的技能和他们做事的方式都有所进步。过去帮助我们的事情可能会成为障碍,因此控制与内存的概念在外围设备中仍然非常普遍,但我们不一定需要控制信号或单独的指令。如果你在 8088/86 之前回退到一些 DEC 处理器,你有针对外围设备的特定指令,你想输出一个字符到 tty,那里有一个指令,而不仅仅是你写入的地址。这是自然的进展,今天只是让所有内容都映射到内存并使用通用加载和存储指令。
我不明白你是如何得到 I/O 与内存的,这意味着没有 x86 机器代码,只需查看指令集即可看到 I/O 指令和内存指令。它们在那里,出于反向兼容性的原因,这正是 Wintel pc 世界几十年来保持活力的原因它们仍然可以工作,但它们被合成为更接近内存映射解决方案的东西,同时程序员已经从 I/O映射,理想情况下只有非常非常旧的代码会尝试这样做,并且硬件和软件的组合仍然可以使某些代码在现代 pc 上运行。
处理器执行指令流。这些指令流是机器代码:由处理器执行的以机器语言编写的程序。
各种指令流有多种用途:一些加载程序,一些将处理器从一个指令流(程序)切换到另一个,一些保护其他代码,一些处理设备 i/o,一些是用户应用程序,如数据库,或汇编器、编译器、链接器、调试器。
处理器只知道机器语言,以及如何执行它。它甚至不知道变量声明 — 由机器代码序列来确保 proper/consistent 处理程序变量。
malloc
是用一种算法(作为参数化函数)实现的,它被编码为一个指令流,可以被另一个指令流“调用”/调用。