如何创建一个可执行文件在特定处理器架构上运行(而不是特定操作系统)?

7
所以我在Visual Studio中编写了C++程序,编译后就会生成一个漂亮的可执行文件(EXE)。但是EXE只能在Windows上运行,我听说过很多关于C/C++编译成汇编语言的事情,汇编语言可以直接在处理器上运行。这个EXE需要Windows的帮助才能运行,或者我可以编写一个程序生成Mac上运行的可执行文件。但是我不是把C++代码编译成特定于处理器的汇编语言吗?
我的见解:
  1. 我猜我可能没有。我知道有一个英特尔C++编译器,那么它会生成特定于处理器的汇编代码吗?EXE在Windows上运行,因此它们利用了已经设置好的许多东西,从图形包到庞大的.NET框架。特定于处理器的可执行文件将从零开始,仅具有处理器的指令集。
  2. 这个可执行文件是否是一种文件类型?我们可以在Windows上运行它,但是控制权是否会转移到处理器上?我假设这个可执行文件类似于操作系统,必须在启动其他任何内容之前运行,并且只能“使用”处理器指令集。

3
英特尔编译器并不是你想象中的那样——它只是另一个由英特尔编写的Windows(或Linux)编译器,可以生成EXE或标准Linux二进制文件。 - Brooks Moses
10个回答

18

让我们思考一下"运行"的含义...

需要将二进制代码加载到内存中。 这是操作系统的功能。 .EXE或二进制可执行文件或包或其他格式非常特定于操作系统,以便操作系统可以将其加载到内存中。

需要将控制权交给这些二进制代码。 这又是操作系统。

I / O例程(在C ++中,但在大多数地方都是如此)只是一个封装操作系统API的库。该死的操作系统, 到处都是它。

追忆往昔

从前(是的,我那么老)我曾经使用没有操作系统的机器工作过。 我们还没有C。

我们使用诸如“汇编器”和“链接器”之类的工具编写机器代码,以创建我们可以加载到机器中的大型二进制图像。 我们必须通过痛苦的引导过程加载这些二进制图像。

我们使用前面板键将足够的代码加载到内存中,以读取像打孔纸带读取器这样的便利设备。 这将加载一个相当标准的引导链接加载器软件的小片段。(我们使用mylar胶带,所以不会磨损。)

然后,当我们将这个链接加载器加载到内存中时,就可以提供之前使用汇编器准备的磁带。

我们编写了自己的设备驱动程序。 或者我们使用源代码形式的库例程, punched 在纸带上。

"修补程序"实际上是修补了一些纸带。 另外,由于也存在小错误,因此我们必须根据手写说明调整内存映像-尚未放入磁带的修补程序。

后来,我们有了简单的操作系统,其具有简单的API、简单的设备驱动程序和一些实用程序,例如“文件系统”、“编辑器”和“编译器”。 这是针对一种称为Jovial的语言,但我们有时也使用Fortran。

我们必须焊接串行接口板,以便插入设备。 我们必须编写设备驱动程序。

底线

您可以轻松地编写不需要操作系统的C++程序。

  1. 了解处理器芯片组中的硬件BIOS(或类似BIOS)功能。大多数现代硬件都有一个简单的操作系统固化在ROM中,用于执行加电自检(POST)、加载一些简单的驱动程序并查找启动块。

  2. 学习如何编写自己的引导块。这是在POST之后加载的第一个真正的“软件”东西。这并不难。您可以使用各种分区工具将引导块程序强制写入磁盘,并完全控制硬件,无需使用操作系统。

  3. 学习GRUB、LILO或BootCamp如何启动操作系统。这并不复杂。一旦它们启动,它们就可以加载您的程序,您就可以开始运行了。这比较简单,因为您创建了一个引导加载程序要加载的类型的分区。以Linux内核为基础,您会更开心。不要试图弄清楚Windows的启动过程——它太复杂了。

  4. 阅读有关ELF的信息。http://en.wikipedia.org/wiki/Executable_and_Linkable_Format

  5. 学习如何编写设备驱动程序。如果您不使用操作系统,那么您需要编写设备驱动程序。


一个附录:“您可以轻松编写不需要操作系统的C++程序”。您最容易使用微控制器来实现,大多数(也许全部)微控制器今天都有C++或至少C编译器。 - vsz

7
您所谈论的是嵌入式世界中所知为“裸机”应用程序。这在ARM Cortex-M3等设备上非常常见,例如用于借记卡验证器或交互玩具,因为它们没有足够的内存或能力来运行完整的操作系统。因此,您需要获取一个“ARM bare-metal”编译器,将应用程序编译为在没有操作系统的ARM处理器上运行。 (我使用ARM而不是x86作为示例,因为现在x86裸机应用程序真的很少见。)
正如您的问题和其他答案所述,您的应用程序将需要执行一些操作,否则这些操作将由操作系统负责。
首先,它需要初始化内存系统、中断向量和各种其他板块信息。通常,这是裸机编译器会为您完成的,但如果您有一个奇怪的板子,您可能需要告诉它如何进行初始化。这将使得从开机到您的main()函数开始的过程顺利进行。
然后,您需要与CPU和RAM之外的东西进行交互。操作系统包括各种功能,用于执行此操作 - 磁盘I / O,屏幕输出,键盘和鼠标输入,网络等等。没有操作系统,您必须从其他地方获取这些功能。您可能会从硬件制造商的库中获取其中一部分;例如,我最近使用的板子有一个40x200像素的LED屏幕,并带有一个代码库,用于打开它并在上面设置单个像素值。还有几家公司出售实现TCP / IP堆栈和诸如此类的库,用于进行网络或其他操作。
例如,请考虑即使是基本的printf也变得困难。当您有操作系统时,printf只向操作系统发送一个消息,该消息指示“将此字符串放在控制台上”,操作系统找到控制台上的当前光标位置,并完成所有内容以确定要更改哪些像素以及要使用哪些CPU指令来更改这些像素,以此来实现。
哦,我们提到了吗?你首先必须弄清楚如何将程序加载到CPU中。一台典型的计算机有一小块可编程ROM,当它启动时会从中加载指令。在x86上,这就是BIOS,通常已经包含一个方便的程序,可以启动CPU、设置显示器、查找磁盘并从磁盘中加载程序。在嵌入式系统中,这通常是您的程序所在的位置,这意味着您需要某种方式将程序放在那里。通常,这意味着您有一个名为“调试器”的设备,它物理连接到您的嵌入式板上,负责加载程序,并且还可以执行一些操作,使您能够暂停处理器并确定其状态,以便您可以像在计算机上运行软件调试器一样逐步执行程序。但我跑题了。
无论如何,回答你的第二个问题,你要创建的可执行文件是存储在嵌入式板子上的ROM中的东西,或者你只能将其中一部分存储在ROM中(毕竟它非常小),并将其余部分存储在闪存驱动器中,而在ROM中的那一部分将包括获取闪存驱动器上其余部分的指令。它可能被存储为主计算机上的一个文件(也就是你创建它的Linux或Windows计算机),但这只是为了存储,它不会在那里运行。
你会注意到,当你有很多这些库在一起时,它们做了操作系统的相当一部分工作,而且在库堆和真正的操作系统之间有一种空间。在那个空间里,有一个叫做RTOS("实时操作系统")的东西。其中较小的那些实际上只是一些库的集合,它们一起工作来完成所有操作系统的事情,有时还包括一些可以同时运行多个线程的东西(然后你可以让不同的线程像不同的程序一样运行) - 尽管所有这些都编译成相同的编译“程序”,RTOS实际上只是你已经包含的库。较大的RTOS开始将代码存储在不同的位置,并且我认为其中一些甚至可以从磁盘加载代码片段 - 就像Windows和Linux运行程序时所做的那样。这是一种连续的过程,而不是非此即彼的关系。

FreeRTOS系统是一个开源的实时操作系统,属于较小型的RTOS空间,如果你对此比较感兴趣,可以去看看。他们有一些x86应用程序的示例,这将让你了解什么样的x86系统可以运行裸机或基于RTOS的程序以及如何编译可在其上运行的程序; 链接在这里: http://www.freertos.org/a00090.html#186


6
问题在于操作系统在启动程序时确实做了很多工作。EXE文件本身有头部信息,Windows能够识别并将其标识为EXE文件。你的应用程序通过操作系统进行所有操作,从文件系统访问到内存分配。
但是,是的,你可以在其他平台上运行为Windows / Intel编译的应用程序而无需仿真。如果你想在Mac或UNIX上运行你的EXE,你需要安装更多的软件来完成Windows运行程序所需的工作--看看"Wine"项目。

3
计算机并不仅仅是CPU。要执行任何有用的操作,CPU必须连接到内存、IO控制器和其他设备。操作系统负责从运行程序中抽象出所有这些内容。因此,如果您想编写一个无需操作系统即可运行的程序,您的程序将必须复制一些操作系统的功能:在启动过程中接管BIOS,初始化设备,与磁盘控制器通信以加载代码和数据,与显示控制器通信以向用户显示信息,与键盘控制器和鼠标控制器通信以读取用户输入等等。
除非您正在构建具有专门硬件的嵌入式系统,否则没有必要这样做。此外,运行您的程序意味着用户必须放弃运行其他程序。虽然这对于今天的自动柜员机或1984年的WordStar可能是可以接受的,但如今人们不喜欢在听音乐的同时无法查看电子邮件。

1
当然,它们存在。它们被称为交叉编译器。例如,我可以使用Xcode来为iPhone平台编程。
一种相关的编译器类型是为虚拟平台编译的编译器。Java就是这样工作的

我认为这不是问题所在。问题是关于什么使可执行文件具有特定于操作系统的属性。 - sleske

1

任何编译器/工具集都会为特定的处理器/操作系统组合生成代码。因此,您的Visual Studio编译示例会为x86/Windows生成代码。该.EXE文件仅能在x86/Windows上运行,而不能在(例如)一些手机使用的ARM/Windows上运行。

要为处理器/操作系统组合生成代码,需要使用通常称为交叉编译器的工具。如果您拥有完整的专业版Visual Studio订阅,则可以获得ARM交叉编译器,这将允许您生成ARM/Windows .EXE文件,这些文件无法在桌面计算机上运行,但可以在基于ARM/Windows的手机或掌上电脑上运行。


0

不要忘记 Windows 库。可以研究一下 QT 和 GTK+。


1
欢迎来到SO。由于它并没有完全回答所提出的问题,因此最好将其作为评论留下。 - Levi Botelho

0

是的,您可以创建一个在处理器的“裸金属”上运行的可执行文件。显然,这就是操作系统内核的工作方式。您需要做的主要事情是创建一个不使用任何库的可执行文件。但是,“没有库”的限制包括C标准库!这意味着没有malloc,没有printf等。您必须基本上成为自己的操作系统,并自己管理内存和I/O。这将不可避免地需要在某个阶段直接使用汇编进行相当多的工作。

您还会失去其他几个奢侈品,例如main(),因为main()不能成为程序的起点,因为main()是由操作系统和C运行时环境调用的。


4
你好像把“库”和“动态加载库”混淆了。每个裸机编译器(或者至少几乎所有的编译器)都包含一个静态链接的C标准库副本,它在编译时链接,使相关函数成为可执行文件的一部分,并且可以正常工作。许多硬件供应商还包括用于与其特定硬件交互的其他库,甚至可以购买TCP/IP库等。许多硬件和编译器供应商还提供必要的基本引导代码,以便板子从“打开电源”到“调用main()”运行。 - Brooks Moses
一个重要的点是,这个程序会取代操作系统。大多数操作系统不允许直接访问一些必需的硬件来运行。因此,你可以编写一个替代Windows或Linux的程序,但不能在现有的操作系统中运行(除非你为一个操作系统编写并在其他操作系统上模拟)。 - KeithB

0
当然可以!这就是嵌入式编程。正如许多人可能已经说过的那样,操作系统为您做了很多事情。即使在没有操作系统的嵌入式世界中,许多开发工具也会提供启动代码,以使处理器运行足够的程序跳转到您的程序。一些/许多提供完整的C/C++库,以便您可以调用诸如memcpy()甚至malloc()和printf()之类的函数。

欢迎您提供每一行代码和每一个指令,不使用开发工具包,但仍然使用编译器,例如gcc。一些二进制格式与在操作系统上运行的格式相同,例如elf。您可以在Linux上执行elf文件,但也可以使嵌入式程序生成elf二进制文件。处理器无法以该格式执行elf,但是启动prom或某些情况下的ram的任何程序都将从elf文件中提取二进制程序,类似于操作系统从elf文件中提取要运行的程序。EXE不是这些文件格式之一。您最喜欢的Windows应用程序编译器可能也不是嵌入式编译器,尽管有时可以使用其中一个来执行高级语言部分,然后使用替代汇编程序和链接器。通常比它值得的更多的工作。例如,您可以编写一个C函数(不进行任何库或系统调用),将其编译为对象。编写自己的实用程序或查找实用程序以从该对象中提取已编译的二进制文件,将其转换为另一种对象格式或汇编程序(反汇编)。添加启动代码和其他汇编代码。将所有内容组合成嵌入式程序进行汇编和链接。我曾经使用Microsoft的嵌入式Visual C进行过一次,只是为了看看它与其他编译器相比如何,它并不可怕,但肯定不值得花费大量精力来获取输出。

从您的计算机到手机或微波炉中的处理器,每个处理器都必须有一些启动代码。该代码不在操作系统上运行。该代码使用与操作系统应用程序使用相同或类似的编译器。对于某些设备,该代码将处理器和内存以及芯片外围设备置于可以启动操作系统的状态。从那里开始,操作系统接管。在您的计算机上,这将是BIOS,然后是引导加载程序,最终是操作系统,如dos、windows、linux等。


0

主要问题是文件格式。PE与UNIX类系统中使用的ELF非常不同。一个有效的PE程序不能是一个有效的ELF。所以,你要么用不同的启动器动态地加载二进制文件,要么放弃。

除此之外,如果了解操作系统服务、启动时寄存器的值等信息,你的代码可以很容易地可靠地检测出你正在运行的操作系统并相应地采取行动(一些恶意软件就是这样做的)。然后,另一个挑战是重复使用代码而不是在同一二进制文件中有两个或更多不同的程序。基本上,你需要编写一个模拟器,至少对于你需要的服务。


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