程序如何执行?操作系统在哪里发挥作用?

27
一个程序从某种语言编译成 ASM --> 机器码(可直接执行)。当人们说这是平台相关的时候,他们是指所形成的二进制文件只能在具有相同指令集架构(如 x86、x86-64)的 CPU 上运行(正确)。由于 ISA 的差异,它可能(不正确地)/可能不会(根本不会)在其他进程上运行。现在,二进制概念让我感到困惑。一切都关于“机器语言代码”和“CPU”。操作系统在哪里发挥作用?我的意思是,当编译后的二进制文件被加载到内存中时,它对 CPU 有直接的指令。CPU 每次执行一条指令。除了进程管理link text之外,我无处看到操作系统的作用。它应该在具有相同 ISA 的 CPU 上运行,而与操作系统无关。对吗?但事实并非如此。如果我在 Windows 机器上构建一个 x86 代码,它将无法在 Mac x86 机器或 Linux x86 机器上运行。我在这里缺少什么,请帮我解决困惑。
9个回答

20
首先,一台现代CPU有(至少)两种模式,一种是运行操作系统本身核心的模式(“内核模式”),另一种是运行程序的模式(“用户模式”)。当处于用户模式时,CPU无法完成很多操作。
例如,鼠标点击通常在内核中被注意到而不是在用户模式下。但是,操作系统将事件分派到用户模式,然后分派到正确的程序。反过来也需要合作:程序不能自由地向屏幕绘制,而需要通过操作系统和内核模式进行绘制。
类似地,启动程序的行为通常是一种合作。操作系统的shell部分也是一个用户模式程序。它接收您的鼠标点击,并确定这是一个意图启动进程的鼠标点击。然后,shell告诉操作系统内核模式部分启动该程序的新进程。
当内核模式需要启动新进程时,首先为簿记分配内存,然后开始加载程序。这涉及检索二进制指令,以及将程序连接到操作系统。这通常需要找到二进制文件的入口点(经典的int main(int argc, char** argv)),以及程序想要调用操作系统的所有点。
不同的操作系统使用不同的方式将程序与操作系统连接起来。因此,加载过程不同,并且二进制文件格式也可能不同。这并非绝对;ELF二进制格式用于许多操作系统,而Microsoft在所有当前的操作系统上使用PE格式。在这两种情况下,格式都描述了二进制文件的精确格式,因此操作系统可以决定是否将程序连接到操作系统。例如,如果是Win32二进制文件,则将以PE格式存在,因此Linux不会加载它,Windows 2000会加载它,Windows 7-64也会加载它。另一方面,Win64二进制文件也以PE格式存在,但Windows 2000将拒绝它。

“因此Linux无法加载它”,但是什么阻止了Linux软件具有运行它的能力? - Pacerier
实际上,是什么阻止了Mac实现一个PE运行器,以便所有Windows应用都可以在Mac上直接运行? - Pacerier
1
@Pacifier:主要是钱。话虽如此,微软确实实现了一个 ELF 子系统(Windows Subsystem for Linux)。 - MSalters

16
由于01010110011在x86和ARM体系结构中表示的意义不同,因此它不能在其他处理器上运行。 x86-64恰好与x86向后兼容,因此可以运行x86程序。
该二进制文件采用特定格式,你的操作系统能够理解(Windows=PE,Mac / Linux = ELF)。
对于任何普通二进制文件,操作系统会将其加载到内存中,并填充一些字段以填入某些值。这些“某些值”是指指向共享库(dll、so)中存在的api函数的地址,例如kernel32或libc等。 需要API地址,因为二进制本身不知道如何访问硬盘驱动器、网络卡、游戏手柄等。程序使用这些地址调用存在于操作系统或其他库中的某些函数。
从本质上讲,二进制文件缺少一些必要的部分,需要操作系统填充才能使所有内容正常工作。如果操作系统填写了错误的部分,二进制文件将无法正常工作,因为它们之间无法通信。如果您尝试使用另一个文件替换user32.dll,或者在mac osx上尝试运行linux可执行文件,就会发生这种情况。
那么libc是如何知道如何打开文件的呢? libc使用系统调用(syscalls),这是对操作系统核心功能的低级访问。它类似于函数调用,但是你需要通过填充某些CPU寄存器来触发中断(特殊的CPU指令)。
那么操作系统是如何知道如何打开文件的呢?
这是操作系统所做的一件事情。但是它如何知道如何与硬盘交流呢? 我不知道这些东西确切的工作原理,但我想象操作系统通过写/读某些内存位置来实现这一点,这恰好被映射到BIOS功能上。
那么BIOS是如何知道如何与硬盘交流的呢?
我也不知道,我从未在那个层次上进行过编程。我想象BIOS连接了硬盘驱动器接口,并能够发送正确的1和0序列以使用硬盘进行“SATA”通信。它可能只能说一些简单的话,例如“读取此扇区”。
那么硬盘是如何知道如何读取扇区的呢?

我真的一窍不通,所以我就让一些硬件专家来继续。


2
非常好的帖子,谢谢。一个小注:我认为OS X使用MACH-O而不是ELF:http://en.wikipedia.org/wiki/Mach-O 由于我对这个主题还很陌生,所以不太确定。 - Stephen
@Martin,关于“如果您用另一个文件替换user32.dll”的问题,但问题是,如果您在程序中嵌入了“user32.dll”的函数,为什么程序不能正常工作呢? - Pacerier
@Pacerier user32.dll 依赖于其他 DLL,因此您必须将它们全部嵌入到程序中。如果这样做,您将获得一个“静态链接”的可执行文件,我相信 Windows 中的加载器会拒绝它。您可以在 Linux 上运行静态链接的可执行文件,但它不可避免地包含系统调用来执行任何有用的操作(通过 x86 上的 syscall 指令),这本质上是对内核模式的函数调用。内核 API 必须按预期运行(即实现 Linux 内核接口),才能使可执行文件正常工作。 - Martin
@Martin,是的,请包含所有所需的代码等等,在循环中,这不应该很难。为什么Winloader会拒绝它?此外,是否有一些系统调用的子集可供Linux和Mac使用? - Pacerier

8

两种方法:

首先,答案是“系统调用”。每当您调用需要进行任何I/O、与设备交互、分配内存、fork进程等操作的函数时,该函数都需要执行“系统调用”。虽然syscall指令本身是X86的一部分,但可用的系统调用及其参数是特定于操作系统的。

即使您的程序不进行任何系统调用(我不确定是否可能,而且肯定不会很有用),包装机器代码的格式也因不同操作系统而异。因此,exe(PE)文件和Linux可执行文件(通常为ELF)的文件格式是不同的,这就是为什么exe文件无法在Linux上执行的原因。

编辑:这些是低级细节。更高级别的答案是说,需要访问文件、控制台/GUI、分配内存等的任何内容都是特定于操作系统的。


2
所以,
  1. 编译器在将高级语言编译为机器语言时,会将例如fopen()或访问打印机函数更改为特定于操作系统的“系统调用”,这对于不同的操作系统是不同的。对吗?
  2. 编译器不仅将高级语言编译为“CPU ISA”和“OS系统调用”特定的机器语言代码,还根据操作系统执行PE / ELF文件格式转换的工作。对吗?
- claws
2
不,它仍然调用fopen()。在fopen()中有一个“syscall”指令。该syscall指令将处理器切换到“内核模式”,这会删除所有类型的保护并允许系统实际访问硬件。您的程序在受保护模式下运行,无法访问任何硬件。 - Robert Fraser
1
虽然syscall指令本身是X86的一部分,但可用的系统调用及其参数是特定于操作系统的。我在哪里可以找到它们?我只想浏览不同操作系统的相同功能(比如“打开文件”)的不同系统调用。我正在谷歌搜索,但找不到我确切需要的内容。 - claws
对于Linux:http://www.kernel.org/doc/man-pages/online/pages/man2/syscalls.2.html -- 对于Windows:http://www.metasploit.com/users/opcode/syscalls.html - Robert Fraser
@RobertFraser,关于“它们的参数是特定于操作系统的”,但肯定有一种简单的方法可以在它们之间进行转换吧? - Pacerier

3
当您尝试在硬件级别访问“服务”时,操作系统会为您抽象出它,例如在名为文件系统的“数据库”中打开文件,生成随机数(每个现代操作系统都具有此功能)。在GNU / Linux中,您需要填写寄存器并调用int 80h来访问“服务”(实际上称为“syscall”)。由于可执行文件有不同的文件格式,例如Win有COFF / PE,Linux具有ELF文件格式,因此您的程序也无法在另一个操作系统上运行(就像任何其他文件格式一样,这也包含“元数据”,例如HTML(或SGML)文件格式)。

3
NB:该“服务”是一种在内核模式下可用的低级功能,不要与“Windows服务”(*nix操作系统上的守护进程)混淆。 - Dirk Vollmar

2
操作系统提供了两个主要功能:(a) 提供您的机器码运行所需的环境,以及 (b) 标准服务。没有(a),您的代码将永远无法执行,没有(b),您必须自己实现所有内容并直接访问硬件。

那么为什么不直接编写硬件代码呢?这样它就可以跨操作系统工作了吗? - Pacerier
@Pacerier:它在任何操作系统下都无法运行,因此无法与其他程序协作。你是想重新启动而不是使用alt-tab吗?(或者回到DOS时代,运行程序基本上拥有整个计算机)。同时,一个自由站立的程序也需要为每种可能的硬件编写自己的驱动程序。 - Peter Cordes

1
一个比喻:
假设你雇了一个来自另一个国家的管家。他听不懂你说的话,所以你使用了类似于星际迷航的翻译设备。现在他可以理解你的高级语言,因为当你说话时,他听到的是他自己(相当粗糙的)语言。
现在假设你想让他从A走到B。你不会直接对他的腿或脚说话,而是会对着他的脸问他!他掌控着自己的身体。如果1)你正确地传达了你的请求,并且2)他决定这是他的职责范围内,他就会从A走到B。
现在你有了一个新的仆人,来自上一个仆人的同一个国家(因为你不想购买一个新的星际翻译器)。你也想让他从A走到B。但是这个仆人要求你大声说话并说“请”。你忍受这一点,因为他更加灵活:你可以要求他通过C从A走到B——之前的管家也能做到,但是他会拖拖拉拉地抱怨。
另一个幸运的事情是你可以调整你的翻译器设置来处理这个问题,所以从你的语言角度来看,什么都没有改变。但是如果你用新的设置和老管家说话,他会感到困惑,即使你在说他的语言。
如果不清楚的话,管家是具有相同ISA但不同操作系统的计算机。翻译器是针对它们的ISA的交叉编译工具链。

“因为你不想购买新的星际远程翻译器”,所以这里提到的翻译器是指什么? - Pacerier
我认为,将这个类比扩展到当你有不同的ISA时会很有意义。 - Pacerier
@Pacerier,翻译器将是针对其ISA的交叉编译器工具链。重点是,即使翻译器生成x86或其他机器语言,您也需要以不同的方式表达指令,因为内核有自己的接口。这意味着链接到不同的库并使用它们使用的任何二进制格式。用户程序不能自行运行,您需要与内核/管家交流以完成任务。 - jiggunjer

1

高级语言生成的机器指令将适合于提供您所做调用的库的调用约定,包括任何系统调用(尽管这些通常包装在用户空间库中,因此可能不需要关于如何进行系统调用的具体信息)。

此外,它将适合于目标指令集架构,但有一些例外情况(例如,必须小心关于指针大小、原始类型、结构布局、C++类实现等方面的假设)。

文件格式将规定必要的挂钩/公开可见函数和数据,以使操作系统能够将您的代码作为进程执行,并将进程引导到所需状态。如果您熟悉在Windows下进行C/C++开发的话,子系统的概念规定了引导级别、提供的资源以及入口点签名(在大多数系统上通常为main(int, char **))。

选择高级语言、指令集架构和可执行文件格式如何影响在任何给定系统上运行二进制文件的能力有一些很好的例子:

汇编语言必须为特定ISA编写代码。它们使用特定于CPU类型系列的指令。如果这些CPU支持给定的指令集,则这些指令可以在其他CPU系列上运行,比如x86代码将在amd64操作系统上部分地运行,并且在运行x86操作系统的amd64 CPU上肯定工作。
C语言抽象了ISA的许多细节。一些明显的例外包括指针大小和字节顺序。各种著名接口将通过libc提供到预期的水平,例如printf、main、fopen等。这些包括调用所需的预期寄存器和堆栈状态,使C代码能够在不同的操作系统和架构上运行而无需更改。可以直接提供其他接口,也可以通过将特定于平台的内容包装成预期接口来增加C代码的可移植性。
Python和其他类似的“虚拟化”语言在另一层次的抽象上运行,在一些例外情况下(例如特定平台上不存在的特性或字符编码差异),可以在许多不同的系统上运行而不需要修改。这是通过为许多不同的ISA和操作系统组合提供统一接口来实现的,但代价是性能和可执行文件大小。

0
操作系统提供工具和API来访问某些功能和硬件。
例如,要在Microsoft Windows上创建窗口,您需要使用操作系统的DLL来创建窗口。
除非您希望自己编写API,否则您将使用操作系统提供的API。这就是操作系统发挥作用的地方。

2
从高层次来看,这是正确的。但是,由于操作系统防止您直接访问硬件或页面表,因此您无法“自己编写API”。因此,在某个层面上,您仍然需要进行特定于操作系统的系统调用。 - Robert Fraser

0
此外,我想要强调的是操作系统处理程序的启动。它准备进程空间并对其进行初始化,以便程序可以开始运行,加载程序指令并将控制权交给程序。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接