当一段代码被编译和 运行 时,计算机内部发生了什么

What physically happens inside a computer when a piece of code is compiled and run

我很好奇一段代码是如何转化为计算机可以理解的东西的。在许多类似的问题中,一个常见的答案是声明从人类可读代码到机器级指令的链或多或少遵循编译链:

high-level code --> assembly code --> machine binary code

尽管这个答案出现的频率很高,但对我来说,计算机中到底发生了什么仍然有点神秘。我在某种程度上了解计算机和 CPU 背后的基本电子设备,但这个特殊案例对我来说仍然有点神秘。

举例来说,我在

中编写了经典的 "Hello, world!" 示例
#include <iostream>
using namespace std;
int main() 
{
    cout << "Hello, world!";
    return 0;
}

然后将其转换为汇编代码,例如

section     .text
global      _start                              ;must be declared for linker (ld)

_start:                                         ;tell linker entry point

    mov     edx,len                             ;message length
    mov     ecx,msg                             ;message to write
    mov     ebx,1                               ;file descriptor (stdout)
    mov     eax,4                               ;system call number (sys_write)
    int     0x80                                ;call kernel

    mov     eax,1                               ;system call number (sys_exit)
    int     0x80                                ;call kernel

section     .data

msg     db  'Hello, world!',0xa                 ;our dear string
len     equ $ - msg                             ;length of our dear string

最终转化为二进制序列

10100010101001111...................

然后 "Hello, world!" 出现在屏幕上。

因此我的问题是:在这个过程中,计算机内部这个过程的每个阶段发生的电信号电平物理上是什么?

我知道我的问题可能太宽泛而无法完整回答,所以如果您能指出计算机内部发生的主要物理现象(例如硬盘驱动器、CPU 和内存)。

此外,如果我的问题离题,请告诉我,因为计算机工程不同学科之间的界限并不总是 100% 明确。在这种情况下,您能否推荐另一个 SE 站点,这个问题可能更适合于此,谢谢。

一台计算机有 CPU 和 RAM 以及可以连接的设备。

一个compiler translates program (source) code into machine code, where the program code & data are encoded as numbers — see also Instruction Set Architecture.

一个operating system loads (see loader) 机器代码进入一个进程,并在其开头启动CPU。

CPU 是将数字解释为机器代码指令的硬件,这些指令告诉它机器代码程序的每一步要做什么,以及在每一步中,下一条机器代码指令应该是什么。

一些机器代码指令告诉计算机在内存中加载或存储数据,或者与设备通信。还有一个 interrupt mechanism 允许设备引起 CPU 的注意。

这个过程非常元,因为 CPU 在编译、链接和 运行 操作系统期间对机器代码进行相同的解释 — 有像 context switching 这样的机制允许 CPU 切换 jobs/programs 并扮演不同的角色(操作系统、用户进程 A、B 等)。除了空闲时,CPU 总是在执行一些程序,也就是说一些程序化步骤序列作为机器代码指令。

晶体管实现 CPU 和 RAM。在 CPU 内部有用于执行加法、减法、条件分支等的功能单元。这些功能单元由大量晶体管组成。 CPU 也有寄存器存储器和高速缓存。它们都根据当前正在执行的机器代码指令切换值,因为 CPU 的唯一工作是在指令后执行机器代码指令。

例如,假设机器代码程序指示计算机将恰好在 CPU 寄存器中的两个数字相加,并将答案写回其中之一。硬件将首先获取要执行的指令,然后解码其中的数字,从命名寄存器中提取值,将这些值提供给 ALU 的输入,指示 ALU 执行加法,然后存储 ALU 的输出返回寄存器——为下一条机器代码指令做好准备。

我们还知道加法可以通过晶体管可以执行的许多非常简单的布尔逻辑来执行,请参阅 adder。 CPU 解释机器代码指令所需的将晶体管组装成功能单元的相同想法适用于处理器的所有功能。

大多数数字电路都是使用一种单一的门构建的,今天,NAND gate.  Gates can be arranged to implement any boolean function.  These transistors are arranged into two broad kinds of circuits: combinational and sequential。组合电路仅根据提供的输入计算输出值,而时序电路有一个反馈回路,使它们能够记住事物。设计者交替使用组合电路和时序电路来形成CPU的功能单元。简单来说,寄存器和内存使用时序逻辑,而 ALU 将使用组合逻辑。

您还可以查看 RAM,以及晶体管的构造方法。

我们还可以注意到,在执行任何给定的一组机器代码指令期间,一些电路未被使用,因此我们不关心这些晶体管是否触发,除非试图节省电力;其他晶体管应该简单地保持它们的状态,例如当前机器代码指令中不涉及的寄存器和 RAM。

首先,这是非常广泛的,即使是四年的电气工程课程,也只涵盖开始尝试理解所有内容所需的基础知识工具箱。显然不适合任何 SE 答案。

如果你想了解物理电子学,那么编译任务是无关紧要的,与问题无关。

CPU 非常愚蠢:它们是对输入的位进行操作的状态机。您的计算机有许多软件层,这些软件在某些方面隐藏了较低层的物理项目,例如获取存储、usb、pcie 的可能方式硬盘控制器等。仅 USB 一项,旧的 1.1 规范就有 300 个打印页。超出此处所能容纳的范围,并且不涵盖有关闪存技术或 usb 转 sata 或其他技术的任何详细信息。

cpu 从其总线上的请求中获取机器代码,其总线上的请求由一个或多个内存控制器解码,具体取决于设计。通过总线层,一个适合这里的单一答案的讨论,解决了各种外围设备,usb 控制器,pcie 控制器,dram 控制器等。然后通常通过 pcie 控制器,你现在有硬盘或其他非易失性媒体。因此,大量的指令提供了一个层,允许内核访问特定的硬盘驱动器,再加上无数的指令,在存储和检索媒体位的基础上创建文件系统。

编译器、汇编器或链接器等将成为硬件层之上 kernel/driver 层之上的另一层软件,用于访问媒体文件中的字节。在编译器的情况下,它读取被认为是 ascii 字符的字节,它有解析这些字节的软件来解码定义的语言,然后通常使用表格来处理该信息,将程序分成更小的部分。

x = y + 5;

您至少有两个需要存储的变量(假设没有删除死代码的优化,在您减少死代码之前您仍然必须经历这个过程)。所以至少必须读取 5 的 y 变量生成,一个带有一些中间结果的加法步骤,然后将该中间结果存储到目的地。编译器将对这样的代码行进行处理,将其分解为这些类型的操作。

然后可能会有一个优化步骤,例如编译器可能会意识到程序在这行代码之后没有任何地方使用x变量,所以这行代码没有提供任何价值,它是死代码所以所有与这行代码相关的步骤可以去掉,得到优化。今天,这通常是在 ram 中完成的,但可以创建中间文件来存储被 c运行 存储的信息。

再次理解所有这些代码都是机器代码和数据,它们通过总线、缓存、来自可能基于 dram 的更大内存,所有这些都需要 类 一到多个学期才能完全理解,高层次的理解没那么多。

编译器的前端将ascii转换成一些内部的language/tables,编译器的中间部分can/does c运行ch进一步,做优化等在此时编译器可能会保存一个中间文件,或者一个流行的编译器有一种文件格式和二进制格式,你可以编译到这个阶段,以便稍后完成到目标指令集的后端转换。

编译器的后端然后将操作序列转换为目标支持的一系列指令。您可能有也可能没有窥孔优化器,它是特定于目标的优化,例如目标指令集可能提供 register = regsiter + immediate 指令,其中 5 可能是立即数而不是 register = 5;注册 = 注册 + 注册。完成后,为了理智起见,输出是汇编语言,但有些编译器输出 machine/object 个文件。

编写 ascii 文件只是将字节写入文件,因此您拥有硬件层、操作系统层和语言库层,然后您的应用程序执行相当于 fprintf() 的操作来输出 ascii代表汇编语言的字符,包括标签和指令。如果一个对象和机器代码那么从物理角度来看它几乎是相同的,只是生成了代表对象文件格式和机器代码的不同字节。

汇编程序是一样的,只是简单了一点,它还读取所有层的 ascii 字符字节,解析它们等。作为一种不同的语言,它更侧重于标签等和单独的指令它告诉汇编器在机器代码中用于指令行的位。汇编程序再次使用内存中的临时文件或表来跟踪此信息。数据的 c运行ching 不同于编译器,但仍然有相当多的工作,指令集具有相对偏移量,并且当每条指令开始形成与一条指令的距离时必须计算的东西到很多以后可以进行多次迭代。

然后汇编程序通过软件层写入目标文件。

链接器然后获取目标文件和一些定义物理地址的规则集 space 至少从这个阶段的视图来看,从程序的最终执行来看它可能是一个虚拟地址,但是它是一个地址 space 以某种方式告诉链接器,通常是一些其他格式的文件,必须像程序一样解析为表等,使用软件和硬件层从媒体获取字节构成该链接描述文件。

然后使用所有层的链接器从作为对象的媒体读取字节。外部函数,例如两个 .C 文件之间的外部函数,此时必须解析,因为链接器将对象放入目标内存的虚拟副本 space,其中每个标签最终都会获得其地址,然后链接器可以修补引用此标签的对象,具体取决于整体工具链设计,链接器有时会生成额外的代码或汇编程序留下的 creates/modifies 指令,以促进这些对象连接。

和这里的所有其他程序一样,它通过软件和硬件层将字节写入文件系统,在这种情况下是最终二进制文件,请注意大多数时候最终二进制文件不仅仅是内存要加载的程序的图像和 运行,它的文件结构就像文档或图像文件一样,会产生一些开销以及必须加载到内存中才能创建的程序的二进制 blob 运行.