为什么需要为每个操作系统重新编译 C/C++?

Nas*_*saf 71 c c++ winapi compilation

这更像是一个理论问题。我是 Comp sci 专业,对低级编程非常感兴趣。我喜欢了解引擎盖下的工作原理。我的专长是编译器设计。

无论如何,当我正在开发我的第一个编译器时,我遇到了一些令人困惑的事情。

当您用 C/C++ 编写程序时,人们知道的传统事情是,编译器神奇地将您的 C/C++ 代码转换为该机器的本机代码。

但有些事情并没有在这里加起来。如果我针对 x86 架构编译我的 C/C++ 程序,那么相同的程序似乎应该在具有相同架构的任何计算机上运行。但这不会发生。您需要为 OS X 或 Linux 或 Windows 重新编译您的代码。(再次为 32 位与 64 位)

我只是想知道为什么会这样?我们在编译 C/C++ 程序时不是针对 CPU 架构/指令集吗?Mac 操作系统和 Windows 操作系统可以在完全相同的架构上运行。

(我知道 Java 和类似的目标是 VM 或 CLR,所以这些不算数)

如果我对此给出了最佳答案,我会说 C/C++ 必须编译为特定于操作系统的指令。但是我读到的每个来源都说编译器针对机器。所以我很困惑。

Nic*_*las 88

我们在编译 C/C++ 程序时不是针对 CPU 架构/指令集吗?

不,你没有。

我的意思是,是的,您正在编译 CPU 指令集。但这并不是编译的全部

考虑最简单的“你好,世界!” 程序。它所做的只是 call printf,对吧?但是没有“printf”指令集操作码。那么……究竟发生了什么?

嗯,那是 C 标准库的一部分。它的printf函数对字符串和参数做一些处理,然后...显示它。怎么会这样?好吧,它将字符串发送到标准输出。好吧……谁控制的?

操作系统。并且也没有“标准输出”操作码,因此将字符串发送到标准输出涉及某种形式的操作系统调用。

并且操作系统调用在操作系统之间没有标准化。几乎每一个标准库函数,如果你不能用 C 或 C++ 自己构建的话,它都会与操作系统对话,至少完成它的一些工作。

malloc? 记忆不属于你;它属于操作系统,你也许可以拥有一些。scanf? 标准输入不属于你;它属于操作系统,您可以从中读取。等等。

您的标准库是通过调用 OS 例程构建的。并且这些操作系统例程是不可移植的,因此您的标准库实现是不可移植的。所以你的可执行文件中有这些不可移植的调用。

最重要的是,不同的操作系统对“可执行文件”的外观有不同的看法。毕竟,可执行文件不仅仅是一堆操作码;您认为所有这些常量和预初始化static变量都存储在哪里?不同的操作系统有不同的启动可执行文件的方式,可执行文件的结构是其中的一部分。

  • ABI 也有所不同 (19认同)
  • @NassimAssaf 操作系统将字节插入 GPU RAM 来告诉它你想要打印的字符的像素。(简化) (7认同)
  • 它已经在最后一段中暗示过,但值得明确提及:语言通常需要语言支持库。该库负责初始化具有静态存储持续时间的对象(如最后一段所述),或将 C++ 异常映射到操作系统和编译器特定的基础设施。语言支持库是特定于编译器和操作系统的。 (5认同)
  • 您谈到了这一点,但没有明确提及*链接器*,这是“编译”过程中相当重要的一部分,因为大多数人在说“编译”时真正的意思是“构建可执行二进制文件”,而不仅仅是编译步骤。即使编译器完成其特定于环境的工作之后,链接器也非常特定于环境。 (5认同)
  • @NassimAssaf你开始问的问​​题对于这个问题来说有点偏离主题,但它们是好问题!操作系统知道连接到计算机的所有硬件,因此它知道要打印文字,例如,它需要与某些显示器连接。它有一个用于该显示器的设备驱动程序,它说“嘿驱动程序,打印这些东西”,驱动程序就会执行它。因此,即使操作系统也不负责打印“hello world”之类的“简单”事情。基本上,一路下来都是海龟。 (5认同)

Dav*_*rtz 17

你如何分配内存?没有用于分配动态内存的 CPU 指令,您必须向操作系统询问内存。但是参数是什么?你如何调用操作系统?

你如何打印输出?你如何打开文件?你如何设置计时器?你如何显示一个用户界面?所有这些事情都需要从操作系统请求服务,不同的操作系统提供不同的服务,需要不同的调用来请求它们。

  • @NassimAssaf 这就是内核开发人员花大钱来解决的问题。 (7认同)
  • @NassimAssaf CPU 必须能够向硬件发出命令,但它不必知道如何执行。它不需要知道存在什么硬件、它们的寄存器在哪里、在这些寄存器中放入什么值来完成什么功能等等。这是操作系统软件(通常是驱动程序)的工作。我可以让我的手机传达诗歌,所以我的手机必须能够发送诗歌,但它不必知道什么是诗歌或英语的结构是什么。 (2认同)
  • @NassimAssaf 有关于这个主题的整本书和课程,比这里讨论的要详细得多。 (2认同)

Rus*_*lan 12

如果我针对 x86 架构编译我的 C/C++ 程序,那么相同的程序似乎应该在具有相同架构的任何计算机上运行。

这是非常正确的,但有一些细微差别。

让我们考虑几个从 C 语言的角度来看与操作系统无关的程序案例。


  1. 假设您的程序从一开始所做的就是通过在没有任何 I/O 的情况下进行大量计算来对 CPU 进行压力测试。

所有操作系统的机器代码可能完全相同(前提是它们都在相同的 CPU 模式下运行,例如 x86 32 位保护模式)。您甚至可以直接用汇编语言编写它,不需要针对每个操作系统进行调整。

但是每个操作系统都希望包含此代码的二进制文件具有不同的标头。例如 Windows需要PE 格式,Linux 需要ELF,macOS 使用Mach-O格式。对于您的简单程序,您可以将机器代码准备为一个单独的文件,并为每个操作系统的可执行格式准备一堆标头。然后你需要“重新编译”实际上是连接标题和机器代码,并可能添加对齐“页脚”。

因此,假设您将 C 代码编译为机器代码,如下所示:

offset:  instruction  disassembly
    00:  f7 e0        mul eax
    02:  eb fc        jmp short 00
Run Code Online (Sandbox Code Playgroud)

这是简单的压力测试代码,eax它自己反复进行寄存器的乘法运算。

现在您想让它在 32 位 Linux 和 32 位 Windows 上运行。您将需要两个标头,以下是示例(十六进制转储):

  • 对于 Linux:
000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00  >.ELF............<
000010 02 00 03 00 01 00 00 00 54 80 04 08 34 00 00 00  >........T...4...<
000020 00 00 00 00 00 00 00 00 34 00 20 00 01 00 28 00  >........4. ...(.<
000030 00 00 00 00 01 00 00 00 54 00 00 00 54 80 04 08  >........T...T...<
000040 54 80 04 08 04 00 00 00 04 00 00 00 05 00 00 00  >T...............<
000050 00 10 00 00                                      >....<
Run Code Online (Sandbox Code Playgroud)
  • 对于 Windows(*只需重复上一行,直到*到达下面的地址):
000000 4d 5a 80 00 01 00 00 00 04 00 10 00 ff ff 00 00  >MZ..............<
000010 40 01 00 00 00 00 00 00 40 00 00 00 00 00 00 00  >@.......@.......<
000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  >................<
000030 00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 00  >................<
000040 0e 1f ba 0e 00 b4 09 cd 21 b8 01 4c cd 21 54 68  >........!..L.!Th<
000050 69 73 20 70 72 6f 67 72 61 6d 20 63 61 6e 6e 6f  >is program canno<
000060 74 20 62 65 20 72 75 6e 20 69 6e 20 44 4f 53 20  >t be run in DOS <
000070 6d 6f 64 65 2e 0d 0a 24 00 00 00 00 00 00 00 00  >mode...$........<
000080 50 45 00 00 4c 01 01 00 ee 71 b4 5e 00 00 00 00  >PE..L....q.^....<
000090 00 00 00 00 e0 00 0f 01 0b 01 01 47 00 02 00 00  >...........G....<
0000a0 00 02 00 00 00 00 00 00 00 10 00 00 00 10 00 00  >................<
0000b0 00 10 00 00 00 00 40 00 00 10 00 00 00 02 00 00  >......@.........<
0000c0 01 00 00 00 00 00 00 00 03 00 0a 00 00 00 00 00  >................<
0000d0 00 20 00 00 00 02 00 00 40 fb 00 00 03 00 00 00  >. ......@.......<
0000e0 00 10 00 00 00 10 00 00 00 00 01 00 00 00 00 00  >................<
0000f0 00 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00  >................<
000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  >................<
*
000170 00 00 00 00 00 00 00 00 2e 66 6c 61 74 00 00 00  >.........flat...<
000180 04 00 00 00 00 10 00 00 00 02 00 00 00 02 00 00  >................<
000190 00 00 00 00 00 00 00 00 00 00 00 00 60 00 00 e0  >............`...<
0001a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  >................<
*
000200
Run Code Online (Sandbox Code Playgroud)

现在,如果您将机器代码附加到这些标头,并且对于 Windows,还附加一堆空字节以使文件大小为 1024 字节,您将获得将在相应操作系统上运行的有效可执行文件。


  1. 现在假设您的程序在进行了一些计算后想要终止。

    现在它有两个选择:

    1. 崩溃——例如通过执行无效指令(在 x86 上可能是UD2)。这很简单,独立于操作系统,但并不优雅。

    2. 要求操作系统正确终止进程。在这一点上,我们需要一个依赖于操作系统的机制来做到这一点。

在 x86 Linux 上,它将是

xor ebx, ebx ; zero exit code
mov eax, 1   ; __NR_exit
int 0x80     ; do the system call (the easiest way)
Run Code Online (Sandbox Code Playgroud)

在 x86 Windows 7 上,它将是

    ; First call terminates all threads except caller thread, see for details:
    ; http://www.rohitab.com/discuss/topic/41523-windows-process-termination/
    mov eax, 0x172  ; NtTerminateProcess_Wind7
    mov edx, terminateParams
    int 0x2e        ; do the system call
    ; Second call terminates current process
    mov eax, 0x172
    mov edx, terminateParams
    int 0x2e
terminateParams:
    dd 0, 0 ; processHandle, exitStatus
Run Code Online (Sandbox Code Playgroud)

请注意,在其他 Windows 版本上,您需要另一个系统调用号。调用的正确方法NtTerminateProcess是通过操作系统依赖的另一个细微差别:共享库。


  1. 现在你的程序想要加载一些共享库以避免重新发明一些轮子。

好的,我们已经看到我们的可执行文件格式是不同的。假设我们已经考虑到这一点,并为针对每个目标操作系统的文件准备了导入部分。还有一个问题:每个操作系统调用函数的方式——所谓的调用约定——是不同的。

例如,假设您的程序需要调用的 C 语言函数返回一个包含两个int值的结构。在 Linux 上,调用者必须分配一些空间(例如在堆栈上)并将指向它的指针作为第一个参数传递给被调用的函数,如下所示:

sub esp, 12 ; 4*2+alignment: stack must be 16-byte aligned
push esp    ;                right before the call instruction
call myFunc
Run Code Online (Sandbox Code Playgroud)

在Windows中,你会得到第一个int结构中的价值EAX,并在第二EDX,不传递任何附加参数的功能。


还有其他细微差别,例如不同的名称修改方案(尽管即使在同一操作系统上,编译器之间也可能有所不同)、不同的数据类型(例如long double在 MSVC 上与long doubleGCC 上)等,但上面提到的是操作系统之间最重要的区别从编译器和链接器的角度来看。


Nat*_*ica 9

不,您不仅仅是针对 CPU。您还针对操作系统。假设您需要使用cout. cout最终会为运行程序的操作系统调用 API 函数。对于不同的操作系统,该调用可以并且将会有所不同,因此这意味着您需要为每个操作系统编译程序,以便它进行正确的操作系统调用。


jwd*_*hue 5

  1. 标准库和 C 运行时必须与 OS API 交互。
  2. 不同目标操作系统的可执行格式是不同的。
  3. 不同的操作系统内核可以以不同的方式配置硬件。字节顺序、堆栈方向、寄存器使用约定以及可能许多其他事物在物理上可能不同。


归档时间:

查看次数:

4415 次

最近记录:

5 年,4 月 前