编译器真的生成机器码吗?

Does the compiler actually produce Machine Code?

我一直在读到,在大多数情况下(如 gcc),编译器以高级语言读取源代码并吐出相应的机器代码。现在,机器代码的定义是处理器可以直接理解的代码。因此,机器代码应该只依赖于机器(处理器)并且 OS 独立。但这种情况并非如此。即使 2 个不同的操作系统 运行ning 在同一个处理器上,我也不能 运行 相同的编译文件(.exe Windows 或 .out Linux)两个操作系统。

所以,我错过了什么? gcc 编译器(和大多数编译器)的输出不是机器代码吗?或者机器代码不是最低级别的代码,OS 将其进一步翻译成处理器可以执行的一组指令?

编译器生成汇编代码,这是机器代码的人类可读版本(例如,您拥有实际命令,而不是 1 和 0)。但是,使您的程序 运行 正确所需的 正确 assembly/machine 代码因操作系统而异。所以处理器使用的语言是一样的,但你的程序需要与操作系统对话,这是不同的。

例如,假设您正在编写一个 Hello World 程序。您需要将短语 "Hello, World" 打印到屏幕上。您的程序需要通过 OS 才能真正做到这一点,不同的 OS 有不同的接口。

我在这里故意避免使用技术术语,以使答案对初学者来说易于理解。更准确地说,您的程序需要通过操作系统才能与计算机上的其他硬件(例如,键盘、显示器)进行交互。这是通过 system calls 完成的,OS.

的每个家族都不同

生成的机器代码可以 运行 在任何与其生成相同类型的处理器上。挑战在于您的代码将与系统上的其他模块或程序交互,为此您需要调用和返回的约定。生成的代码假定 运行 时间环境 (OS) 以及库支持(调用约定)。这些在操作系统之间不一致。

因此,当它们需要转换到并依赖于使用操作系统调用约定定义的约定的其他模块时,事情就会中断。

你混淆了一些事情。我像 gcc 这样的可重定向编译器和其他通用编译器将文件编译为对象,然后链接器稍后根据需要将对象与其他库链接起来,以生成所谓的二进制文件,然后操作系统可以读取、解析、加载可加载块并开始执行。

一个理智的编译器作者会使用汇编语言作为编译器的输出,然后编译器或用户在他们的 makefile 中调用创建对象的汇编器。这就是 gcc 的工作原理。以及 clang 的工作原理,但 llc 现在可以直接创建对象,而不仅仅是组装的组件。

生成可生成原始机器代码的可调试汇编语言更有意义。你真的需要一个像 JIT 这样的好理由来跳过这一步。我会避免直接进入机器代码的工具链,因为它们更难维护,更容易出现错误或需要更长的时间来修复错误。

如果体系结构相同,则没有理由不能让通用工具链为不兼容的操作系统生成代码。例如 gnu 工具可以做到这一点。操作系统差异不是机器代码级别的定义,大多数是高级语言级别的 C 库,您可以创建 gui windows,等等与机器代码或处理器架构无关,对于某些操作系统 可以在 mips 或 arm 或 powerpc 或 x86 上使用相同操作系统特定的 C 代码。体系结构变得具体的地方是调用实际系统调用的机制。通常使用特定指令。并且最终使用机器代码是的,但没有理由不能将其编码为真实或内联汇编。

然后这会导致库,即使是通用 C 调用的 fopen 和 printf 最终也必须进行系统调用,因此很多库支持代码可以采用跨系统兼容的高级语言,因此需要成为最后一英里的系统和体系结构特定代码位。您应该在 glibc 源代码中看到这一点,或者例如在其他库解决方案中挂钩到 newlib。作为例子。

C++ 等其他语言也是如此。解释型语言有额外的层,但它们的虚拟机只是位于相似层上的程序。

低级编程并不意味着机器语言或汇编语言,它只是意味着您使用的任何编程语言都可以访问较低级别,低于应用程序或低于操作系统等...

即使在两个不同的操作系统上编译的程序的机器码指令是相同的(完全不可能,因为不同的操作系统以不同的方式提供不同的服务),机器码需要以一种格式存储主机 OS 可以使用 "load into" 进程执行。这些格式在不同操作系统之间经常不同。