我现在真的很好奇。我是Python程序员,这个问题令我感到困惑:您编写了一个OS。您如何运行它?它必须以某种方式运行,并且这样可以在另一个OS中运行吗?
没有OS的情况下如何运行应用程序?如果没有要运行的操作系统,如何告诉计算机运行C,并在屏幕上执行这些命令?
它与UNIX内核有关吗?如果是这样,什么是Unix内核,或者一般来说是一个内核?
我敢肯定,操作系统比这更复杂,但是它如何工作?
我现在真的很好奇。我是Python程序员,这个问题令我感到困惑:您编写了一个OS。您如何运行它?它必须以某种方式运行,并且这样可以在另一个OS中运行吗?
没有OS的情况下如何运行应用程序?如果没有要运行的操作系统,如何告诉计算机运行C,并在屏幕上执行这些命令?
它与UNIX内核有关吗?如果是这样,什么是Unix内核,或者一般来说是一个内核?
我敢肯定,操作系统比这更复杂,但是它如何工作?
Answers:
有很多网站需要进行引导(例如,计算机如何引导)。简而言之,它是一个多阶段的过程,一次可以建立一点系统,直到最终可以启动OS进程。
它从主板上的固件开始,该固件试图启动并运行CPU。然后,它加载BIOS,就像启动其他硬件并运行的微型操作系统一样。完成后,它将查找引导设备(磁盘,CD等),一旦找到,它将找到MBR(主引导记录)并将其加载到内存中并执行它。然后,这小段代码就知道如何初始化和启动操作系统(或其他启动加载程序,因为事情变得越来越复杂)。至此,诸如内核之类的东西将被加载并开始运行。
根本不可思议!
“裸机”操作系统无法在任何程序中运行。它在物理机上运行完整的指令集,并可以访问所有物理内存,所有设备寄存器和所有特权指令,包括那些控制虚拟内存支持硬件的指令。
(如果操作系统在虚拟机上运行,则可能会认为它处于与上述相同的情况。区别在于某些事物是由虚拟机监控程序模拟或以其他方式处理的;即运行虚拟机的级别)
无论如何,尽管操作系统可能是用(例如)C语言实现的,但它并没有所有可用的常规C库。特别是,它将没有普通的“ stdio”库。相反,它将实现(例如)允许其读取和写入磁盘块的磁盘设备驱动程序。它将在磁盘块层的顶部实现一个文件系统,并在其之上实现用户应用程序的运行时库为创建,读取和写入文件等而进行的系统调用。
没有OS的情况下如何运行应用程序?
它需要是一种特殊的应用程序(例如操作系统),该应用程序知道如何直接与I / O硬件等进行交互。
如果没有要运行的操作系统,如何告诉计算机运行C,并在屏幕上执行这些命令?
你不知道
该应用程序(出于用C语言编写的目的)是在其他计算机上编译和链接的,以提供本机代码映像。然后将映像写入BIOS可以找到它的位置的硬盘驱动器。BIOS将映像加载到内存中,并执行一条指令以跳转到应用程序的入口点。
除非它是成熟的操作系统,否则在应用程序中(通常)没有任何“运行C和执行命令”。在这种情况下,操作系统有责任实施所有必需的基础架构以实现这一目标。没魔术 只是很多代码。
Bill的答案涵盖了引导程序,引导程序是您从已关闭电源的计算机转到正常操作系统已启动并正在运行的计算机的过程。但是,值得注意的是,BIOS完成任务后,它(通常)将对硬件的完全控制权交给了主要操作系统,并且不再起作用-直到下一个系统重新启动。按照传统意义,主操作系统肯定不在BIOS内运行。
它与UNIX内核有关吗?如果是这样,那么什么是unix内核,或者通常是一个内核?
是的,它确实。
UNIX内核是UNIX操作系统的核心。它是UNIX的一部分,它完成上述所有“裸机”工作。
“内核”的想法是您尝试将系统软件分为核心内容(需要物理设备访问,所有内存等)和非核心内容。内核由核心内容组成。
实际上,内核/内核和非内核/非内核之间的区别要复杂得多。关于什么真正属于内核,什么不属于内核,存在很多争论。(例如,查看微内核。)
The idea of a "kernel" is that you try to separate the system software into core stuff
易于记住,因为该术语kernel
来自German Kern
,意思是核心/核心。
最初,CPU没有电源。
那个人说“放电”,CPU开始从内存中的给定地址读取并执行那里的指令。然后下一个,依此类推,直到电源结束。
这是启动。它的任务是加载另一个软件,以访问并加载主软件所在的环境。
最后,一个友好的屏幕邀请您登录。
0x7C00
适用于任何x86
兼容的体系结构,并且首先必须由BIOS填充,该BIOS通常会加载它喜欢的任何可启动设备的第一个扇区...不错的答案:-7
很抱歉,迟到了,但我将这样描述:
主板通电。
定时电路仅根据其电气特性启动并在必要时稳定。某些较新的设备实际上可能使用非常有限的微处理器或音序器。
应当指出,诸如“定时电路在必要时启动并稳定”之类的事情在硬件中不再真正发生。实际上,大量的工作是在非常有限的子处理器/定序器上运行的非常专业的软件。
电源已提供给CPU和RAM。
CPU从BIOS加载(基于其内部接线)数据。在某些机器上,BIOS可能会镜像到RAM,然后从那里执行,但这是很少发生的IIRC。
打开时,x86兼容的CPU从地址空间中的地址0xFFFFFFF0开始...
-米歇尔·斯蒂尔(Micheal Steil),微软在Xbox安全系统中犯下的17个错误(存档)
BIOS调用主板用于磁盘和其他硬件IO的硬件端口和地址,并旋转磁盘,使RAM的其余部分正常工作。
BIOS代码(通过CMOS设置存储在硬件中)使用低级IDE或SATA命令读取每个磁盘的引导扇区,其顺序由CMOS指定或用户通过菜单覆盖。
具有引导扇区的第一个磁盘将执行其引导扇区。此引导扇区是Assembly,具有从磁盘加载更多数据,加载更大的NTLDR
,稍后阶段GRUB
等的指令。
最后,引导加载程序直接或间接地通过链加载从备用位置或偏移位置加载引导扇区来执行OS机器代码。
然后,您会遇到友好的内核恐慌,令人窒息的企鹅,或者由于磁头崩溃而使磁盘停止运转。=)在替代方案中,您的内核将设置进程表,内存结构,并装入磁盘,装入驱动程序,模块和GUI或一组服务(如果在服务器上)。然后,在读取程序头时执行程序,并将程序集放入内存并进行相应映射。
有很多好的答案,但我想补充一下:您提到您来自Python背景。Python是一种n解释(或“嵌入”或类似的东西,至少在典型的CPython用例中)语言。这意味着您需要其他一些软件(Python解释器)来查看源代码并以某种方式执行它。这是一个很好的模型,并允许从实际硬件中很好地抽象出相当不错的高级语言。缺点是您始终首先需要此解释器软件。
这种解释器软件通常以编译为机器代码的语言(例如C或C ++)编写。机器代码是CPU可以处理的。CPU可以执行的操作是从内存中读取一些字节,然后根据字节值开始特定的操作。因此,一个字节序列是一条命令,用于将内存中的某些数据加载到寄存器中;另一个字节序列是将两个值相加的序列;另一个是将寄存器中的值存储回主存储器中的命令(很快,寄存器是一个特殊的存储区域, (最能发挥作用的CPU)),大多数命令在该级别上都非常低。这些机器代码指令的人类可读性是汇编代码。基本上,此机器代码是Windows或Linux / Unix二进制文件中.exe或.com文件中存储的内容。
现在,如果计算机启动起来很笨,那么它就有一些接线,尽管接线会读取此类机器代码说明。在PC上,这通常(当前)是主板上的EEPROM芯片,其中包含BIOS(基本输入输出系统),该系统不能做很多事情,可以简化对某些硬件的访问等操作,然后执行关键操作:引导并将前几个字节(也称为主引导记录,MBR)复制到内存中,然后告诉CPU“这里有您的程序”,然后CPU将把那里的字节当作机器代码处理并执行。通常,这是一些操作系统加载程序,它将使用一些参数加载内核,然后将控制移交给该内核,然后该控件将加载其所有驱动程序以访问所有硬件,加载某些桌面或Shell程序或其他任何东西,并允许用户登录并使用系统。
您询问“如何在没有操作系统的情况下运行应用程序”。简单的答案是“操作系统不是应用程序”。可以使用与应用程序相同的工具来创建OS,并使用相同的原材料来构建OS,但它们并非同一回事。操作系统不必遵循与应用程序相同的规则。
OTOH,您可以将实际的硬件和固件视为运行操作系统“应用程序”的“操作系统”。硬件是一个非常简单的操作系统,它知道如何运行用机器代码编写的指令,并且知道在启动时应该为第一个指令查看非常特定的内存地址。因此,它将启动,然后立即运行第一个指令,然后再执行第二个指令,依此类推。
因此,操作系统只是存在于已知位置的机器代码,并且可以直接与硬件交互。
要回答您的问题,需要了解本机(用于CPU)代码的外观以及如何由CPU解释。
通常,整个编译过程都是基于将您用C,Pascal甚至Python(使用pypy)和C#编写的内容转换为CPU可以理解的内容,即简单的指令,例如“在[内存地址]中存储内容”,“在寄存器eax下添加存储的数字”和ebx”,“调用函数foo”,“将eax与10进行比较”。这些指令一个接一个地执行,可以完成您想对代码执行的操作。
现在考虑一下:您真的不需要操作系统来执行此本地代码!您需要做的就是将该代码加载到内存中,并告诉CPU它在那里并且您希望它被执行。不过,不要对此太担心。这是BIOS应该担心的工作-它在CPU启动后立即在物理地址0x7C00下加载代码(仅一个扇区和一个扇区)。然后,CPU开始执行代码的这一扇区(512 B)。您可以做任何您想像的!当然,没有操作系统的任何支持。那是因为您是操作系统。酷吧?没有标准库,没有提升,没有python,没有程序,没有驱动程序!您必须自己编写所有内容。
以及您如何与硬件通信?好吧,您有两种选择:
现在您在问什么是内核。很快,内核就是您看不见和直接体验的一切。它可以管理所有内容,以及驱动程序,从键盘到PC内几乎所有硬件。您可以通过图形外壳或终端与之通信。或通过代码内部的功能(幸运的是,现在已在操作系统的支持下执行)。
为了更好地理解,我可以给您一个建议:尝试编写自己的OS。即使要在屏幕上写“ Hello world”。
操作系统的运行方式存在一些差异,这些差异与系统高度相关。为了有用,系统在启动时需要具有一些可预测的行为,例如“从地址X开始执行”。对于将非易失性存储(例如闪存)映射到程序空间的系统,这很容易,因为您只需确保将启动代码放在处理器程序空间内的正确位置即可。这对于微控制器是极为普遍的。某些系统必须在执行之前从其他位置检索其启动程序。这些系统将有一些操作硬连线(或几乎硬连线)到其中。有些处理器通过i2c从另一块芯片检索其启动代码,
使用x86处理器系列的系统通常使用多阶段启动过程,由于其演进和向后兼容性问题,该过程相当复杂。系统执行主板上某些非易失性内存中的某些固件(称为BIOS-基本输入/输出系统或类似固件)。有时,部分或全部固件被复制(重定位)到RAM中以使其执行更快。编写此代码时,要了解会出现什么硬件并将其用于引导。
通常,在编写启动固件时会假设系统上将配备哪些硬件。几年前,在一台286机器上,可能会假设在I / O地址X处将有一个软盘驱动器控制器,并且如果给出了一组特定的命令(以及扇区0处的代码),则会将扇区0加载到某个内存位置。知道如何使用BIOS自身的功能来加载更多代码,并最终加载了足以成为操作系统的代码)。在微控制器上,可能会假设存在一个以某些设置运行的串行端口,在继续引导过程之前,它应等待命令(更新更复杂的固件)X倍的时间。
给定系统的确切启动过程对您而言并不重要,因为知道不同系统上的启动过程不同,而且它们都具有共同点。通常,在启动(引导)代码中,需要完成I / O时,将轮询I / O设备,而不是依赖中断。这是因为中断很复杂,使用堆栈RAM(可能尚未完全设置),并且当您是唯一的操作时,不必担心阻塞其他操作。
第一次加载OS内核(内核是大多数OS的主要部分)时,其起初的作用类似于固件。它需要通过编程来了解或发现存在的硬件,将一些RAM设置为堆栈空间,进行各种测试,设置各种数据结构,可能发现并安装文件系统,然后可能启动一些更多的程序。就像您习惯于编写的程序(依赖于现有操作系统的程序)一样。
OS代码通常以C和汇编语言混合编写。OS内核的第一个代码很可能始终在汇编中,并执行诸如设置堆栈(C代码依赖于此)然后调用C函数之类的事情。其他手写汇编也将出现在其中,因为OS所需执行的某些操作通常无法用C表示(例如上下文切换/交换堆栈)。通常,必须将特殊标志传递给C编译器,以告诉它不要依赖大多数C程序使用的标准库,并且不要期望有一个特殊的标志。int main(int argc, char *argv[])
在程序中。此外,还必须使用大多数应用程序程序员从未使用过的特殊链接器选项。这些可能使内核程序期望被加载到某个地址,或者将其设置为看起来在某些位置存在外部变量,即使这些变量从未在任何C代码中声明过(这对于内存映射的I / O或其他特殊的内存位置)。
最初,整个操作看起来像魔术,但是当您仔细研究并理解其中的一部分后,魔术就变成了一组程序,需要更多的计划和系统知识来实施。但是,调试它们需要魔术。
为了了解操作系统的工作原理,将它们分为两类可能会有所帮助:一类仅根据请求向应用程序提供服务,另一类使用CPU中的硬件功能阻止应用程序执行不应做的事情。MS-DOS是以前的样式。从3.0开始,所有版本的Windows都是后一种风格(至少在运行比8086更强大的功能时)。
运行PC-DOS或MS-DOS的原始IBM PC将是“ OS”以前样式的一个示例。如果应用程序希望在屏幕上显示一个字符,则有几种方法可以实现。它可以调用例程,该例程将要求MS-DOS将其发送到“标准输出”。如果这样做,MS-DOS将检查输出是否被重定向,否则,它将调用存储在ROM中的例程(在IBM称为“基本输入/输出系统”的例程的集合中),该例程将在屏幕上显示一个字符。光标位置并移动光标(“写电传打字机”)。然后,该BIOS例程将在0xB800:0到0xB800:3999范围内的某个位置存储一对字节;彩色图形适配器上的硬件将反复获取该范围内的字节对,使用每对的第一个字节选择字符形状,第二个字节选择前景色和背景色。字节被提取并按顺序产生红色,绿色和蓝色信号,从而产生清晰的文本显示。
IBM PC上的程序可以使用DOS“标准输出”例程,或使用BIOS“写电传打字机”例程,或将其直接存储到显示内存中来显示文本。许多需要显示大量文本的程序很快选择了后一种方法,因为它的速度实际上是使用DOS例程的几百倍。这不是因为DOS和BIOS例程效率极低。除非显示为空白,否则只能在特定时间写入。设计了用于输出字符的BIOS例程,因此可以随时调用它。因此,每个请求都必须重新开始,等待正确的时间来执行写操作。相反,知道需要做什么的应用程序代码可以围绕可用的机会来组织自己,以编写显示内容。
这里的关键是,尽管DOS和BIOS提供了一种将文本输出到显示器的方法,但是对于这种功能并没有什么特别的“魔术”。至少在显示硬件按应用程序预期的方式工作的情况下,想要向显示器写入文本的应用程序也可以同样有效(如果有人安装了单色显示器适配器,该显示器类似于CGA但具有字符存储功能) (位于0xB000:0000-0xB000:3999),BIOS会在此自动输出字符;被编程为与MDA或CGA一起使用的应用程序也可以这样做,但是仅为CGA编程的应用程序在MDA上完全没有用。
在较新的系统上,情况有所不同。处理器具有各种“特权”模式。它们以最特权的模式开始,在该模式下,代码可以执行所需的任何操作。然后,他们可以切换到受限模式,在该模式下,只有选定范围的内存或I / O功能可用。代码无法直接从受限模式切换回特权模式,但是处理器已定义了特权模式入口点,并且受限模式代码可以要求处理器在特权模式下的那些入口点之一处开始运行代码。此外,还有特权模式入口点与在限制模式下禁止的许多操作相关。例如,假设某人想要同时运行多个MS-DOS应用程序,每个应用程序都有自己的屏幕。如果应用程序可以直接在0xB800:0处写入显示控制器,则无法防止一个应用程序覆盖另一应用程序的屏幕。另一方面,操作系统可以在受限模式下运行该应用程序,并在对显示内存的任何访问中陷入陷阱。如果发现某个应在“后台”中的应用程序正在尝试写入0xB800:160,则可以将数据存储到它留作后台应用程序屏幕缓冲区的某个内存中。如果以后将该应用程序切换到前台,则可以将缓冲区复制到实际屏幕。操作系统可以在受限模式下运行该应用程序,并在对显示内存的任何访问中陷入陷阱;如果发现某个应在“后台”中的应用程序正在尝试写入0xB800:160,则可以将数据存储到它留作后台应用程序屏幕缓冲区的某个内存中。如果以后将该应用程序切换到前台,则可以将缓冲区复制到实际屏幕。操作系统可以在受限模式下运行该应用程序,并在对显示内存的任何访问中陷入陷阱;如果发现某个应在“后台”中的应用程序正在尝试写入0xB800:160,则可以将数据存储到它留作后台应用程序屏幕缓冲区的某个内存中。如果以后将该应用程序切换到前台,则可以将缓冲区复制到实际屏幕。
要注意的关键事项是(1)尽管拥有一组标准的例程来执行各种标准服务(例如显示文本)通常很方便,但是它们并没有执行以“特权模式”运行的应用程序无法执行的任何操作是否已正确编程以处理已安装的硬件;(2)尽管今天运行的大多数应用程序都将被其操作系统直接阻止进行此类I / O,但是以特权模式启动的程序可以执行所需的任何操作,并且可以为受限模式设置所需的任何规则程式。
您编写一个操作系统。它必须以某种方式运行,并且这样可以在另一个OS中运行吗?
您的应用程序正在操作系统中运行。该操作系统为您的应用程序提供服务,例如打开文件并向其中写入字节。这些服务通常是通过系统调用提供的。
操作系统在硬件中运行。硬件为操作系统提供服务,例如设置串行端口的波特率并向其写入字节。这些服务通常通过内存映射寄存器或I / O端口提供。
举一个非常简单的例子说明如何工作:
您的应用程序告诉操作系统向文件中写入内容。操作系统为您的应用程序提供文件和目录之类的概念。
在硬件上,这些概念不存在。硬件提供了一些概念,例如将磁盘划分为512字节的固定块。操作系统决定用于文件的块,以及用于元数据的其他一些块,例如文件名,大小和磁盘上的位置。然后,它告诉硬件:将这512个字节与此编号的扇区写入具有该编号的磁盘;将这其他512个字节写入具有相同编号的磁盘上具有该不同编号的扇区;等等。
操作系统告诉硬件执行此操作的方式变化很大。操作系统的功能之一是保护应用程序免受这些差异的影响。对于磁盘示例,在一种硬件上,操作系统必须将磁盘和扇区号写入I / O端口,然后将字节一个字节地写入单独的I / O端口。在另一种硬件上,操作系统将必须将整个512字节的扇区复制到一个内存区域,将该内存区域的位置写入一个特殊的内存位置,然后将磁盘和扇区号写入另一个特殊的内存位置。
当今的高端硬件极其复杂。提供所有编程详细信息的手册是数千页的门挡;例如,最新的英特尔CPU手册有7卷,总共超过4000页-仅适用于CPU。大多数其他组件都公开内存或I / O端口块,操作系统可以告诉它们将CPU映射到其地址空间内的地址。这些组件中的几个组件在几个I / O端口或内存地址后面暴露了更多东西。例如,RTC(实时时钟,在关闭计算机电源时保持计算机时间的组件)在一对I / O端口后面暴露了数百个字节的内存,这是一个非常简单的组件,其历史可以追溯到原始PC / AT。诸如硬盘之类的东西具有完全独立的处理器 操作系统通过标准化命令与之对话的内容。GPU更加复杂。
上面评论中的几个人建议使用Arduino。我同意他们的观点,这很容易理解-ATmega328手册只有几百页,该手册除了将USB连接器公开为串行端口外,还可以在Arduino Uno上进行所有操作。在Arduino上,您可以直接在硬件上运行,而两者之间没有操作系统。只是一些小的库例程,如果您不想使用它们,则不必使用它们。
可运行的示例
从技术上讲,没有OS的程序就是OS。因此,让我们看看如何创建和运行一些小型的hello world操作系统。
以下所有示例的代码均在此GitHub存储库中提供。
引导区
在x86上,您可以做的最简单和最低级别的操作是创建一个Master Boot Sector(MBR)(它是一种启动扇区),然后将其安装到磁盘上。
在这里,我们通过一个printf
调用创建一个:
printf '\364%509s\125\252' > main.img
sudo apt-get install qemu-system-x86
qemu-system-x86_64 -hda main.img
结果:
在Ubuntu 18.04,QEMU 2.11.1上进行了测试。
main.img
包含以下内容:
\364
八进制== 0xf4
十六进制:hlt
指令的编码,告诉CPU停止工作。
因此,我们的程序将不执行任何操作:仅启动和停止。
我们使用八进制,因为\x
POSIX未指定十六进制数字。
我们可以通过以下方式轻松获得此编码:
echo hlt > a.asm
nasm -f bin a.asm
hd a
但是0xf4
编码当然也记录在Intel手册中。
%509s
产生509个空格。需要填写文件,直到字节510。
\125\252
八进制==,0x55
后跟0xaa
:硬件所需的魔术字节。它们必须是字节511和512。
如果不存在,则硬件不会将其视为可引导磁盘。
请注意,即使不执行任何操作,屏幕上也已经打印了几个字符。这些由固件打印,并用于标识系统。
在真实的硬件上运行
模拟器很有趣,但是硬件才是真正的交易。
但是请注意,这很危险,您可能会误擦除磁盘:仅在不包含关键数据的旧机器上这样做!甚至更好的是Raspberry Pi等开发板,请参见下面的ARM示例。
对于典型的笔记本电脑,您必须执行以下操作:
将图像刻录到USB记忆棒(将破坏您的数据!):
sudo dd if=main.img of=/dev/sdX
将USB插入计算机
打开它
告诉它从USB启动。
这意味着使固件在硬盘之前选择USB。
如果这不是计算机的默认行为,请在开机后继续按Enter,F12,ESC或其他类似的怪异键,直到获得启动菜单,您可以在其中选择从USB引导。
通常可以在那些菜单中配置搜索顺序。
例如,在旧的Lenovo Thinkpad T430,UEFI BIOS 1.16上,我可以看到:
你好,世界
既然我们已经制作了一个最小的程序,让我们进入一个问候世界。
显而易见的问题是:如何做IO?一些选择:
串口。这是一个非常简单的标准化协议,可以从主机终端发送和检索字符。
来源。
不幸的是,它并没有在大多数现代笔记本电脑上公开,但是是开发板的常用方法,请参见下面的ARM示例。
这实在令人遗憾,因为此类接口对于调试Linux内核确实非常有用。
使用芯片的调试功能。例如,ARM称其为半主机。在实际硬件上,它需要一些额外的硬件和软件支持,但是在仿真器上,它可以是免费的便捷选择。实例。
在这里,我们将做一个BIOS示例,因为它在x86上更简单。但是请注意,这不是最可靠的方法。
电源
.code16
mov $msg, %si
mov $0x0e, %ah
loop:
lodsb
or %al, %al
jz halt
int $0x10
jmp loop
halt:
hlt
msg:
.asciz "hello world"
链接
SECTIONS
{
. = 0x7c00;
.text :
{
__start = .;
*(.text)
. = 0x1FE;
SHORT(0xAA55)
}
}
组装并链接到:
gcc -c -g -o main.o main.S
ld --oformat binary -o main.img -T linker.ld main.o
结果:
经过测试:Lenovo Thinkpad T430,UEFI BIOS 1.16。在Ubuntu 18.04主机上生成的磁盘。
除了标准的Userland组装说明,我们还有:
.code16
:告诉GAS输出16位代码
cli
:禁用软件中断。这些可能会使处理器在启动后再次开始运行。hlt
int $0x10
:执行BIOS调用。这就是逐个打印字符的过程。
重要的链接标志是:
--oformat binary
:输出原始的二进制汇编代码,不要像普通用户级可执行文件一样在ELF文件中扭曲它。使用C代替汇编
由于C可以编译为汇编程序,因此在没有标准库的情况下使用C非常简单,因此您基本上只需要:
main
,主要是:
TODO:在GitHub上链接一些x86示例。这是我创建的一个ARM。
但是,如果您想使用标准库,事情会变得更加有趣,因为我们没有Linux内核,该内核通过POSIX实现了许多C标准库功能。
在不使用像Linux这样的成熟操作系统的情况下,有几种可能的方法,包括:
有关详细示例,请访问:https://electronics.stackexchange.com/questions/223929/c-standard-libraries-on-bare-metal/223931
在Newlib中,您必须自己实现syscall,但是您得到的系统非常小,实现起来非常容易。
例如,您可以重定向printf
到UART或ARM系统,或exit()
使用semihosting实现。
此类操作系统通常允许您关闭抢先式调度,因此可以完全控制程序的运行时间。
可以将它们视为一种预先实现的Newlib。
臂
在ARM中,总体思路是相同的。我上传了:
GitHub上的一些简单QEMU裸机示例。该prompt.c例如从主机终端需要输入还给输出都通过模拟UART:
enter a character
got: a
new alloc of 1 bytes at address 0x0x4000a1c0
enter a character
got: b
new alloc of 2 bytes at address 0x0x4000a1c0
enter a character
另请参阅:https : //stackoverflow.com/questions/38914019/how-to-make-bare-metal-arm-programs-and-run-them-on-qemu/50981397#50981397
完整的Raspberry Pi眨眼设置程序位于:https : //github.com/cirosantilli/raspberry-pi-bare-metal-blinker
另请参阅:https : //stackoverflow.com/questions/29837892/how-to-run-ac-program-with-no-os-on-the-raspberry-pi/40063032#40063032
对于Raspberry Pi,https://github.com/dwelch67/raspberrypi看起来像是当今最受欢迎的教程。
与x86的一些区别包括:
IO是通过直接写入魔术地址来完成的,没有in
和out
指令。
这称为内存映射IO。
对于某些真正的硬件,例如Raspberry Pi,您可以自己将固件(BIOS)添加到磁盘映像中。
这是一件好事,因为它使更新固件更加透明。
固件
实际上,引导扇区并不是系统CPU上运行的第一个软件。
首先实际运行的是所谓的固件,它是一种软件:
众所周知的固件包括:
固件执行以下操作:
遍历每个硬盘,USB,网络等,直到找到可启动的设备。
当我们运行QEMU时,-hda
说这main.img
是连接到硬件的硬盘,并且
hda
是第一个尝试使用的方法。
将前512个字节加载到RAM存储器地址0x7c00
,将CPU的RIP放在此处,然后运行
在显示屏上显示启动菜单或BIOS打印呼叫之类的内容
固件提供了大多数OS所依赖的类似于OS的功能。例如,已将Python子集移植到可在BIOS / UEFI上运行:https : //www.youtube.com/watch?v=bYQ_lq5dcvM
可以说固件与操作系统是无法区分的,并且固件是唯一可以做到的“真正的”裸机编程。
正如这个CoreOS开发人员所说:
困难的部分
当您打开PC电源时,构成芯片组的芯片(北桥,南桥和SuperIO)尚未正确初始化。即使将BIOS ROM尽可能远地从CPU卸下,它也可以被CPU访问,因为必须将其删除,否则CPU将没有执行指令。这通常并不意味着BIOS ROM已完全映射。但是只有足够的映射才能使启动过程继续进行。任何其他设备,只需忘记它即可。
在QEMU下运行Coreboot时,可以尝试使用Coreboot的较高层和有效负载,但是QEMU提供的机会很少,无法尝试使用低级启动代码。一方面,RAM从一开始就可以正常工作。
BIOS后的初始状态
像硬件中的许多事情一样,标准化很弱,并且您的代码在BIOS之后开始运行时,您不应该依赖的事情之一就是寄存器的初始状态。
因此,请帮自己一个忙,并使用类似以下的初始化代码:https : //stackoverflow.com/a/32509555/895245
寄存器喜欢%ds
和%es
具有重要的副作用,因此即使您没有明确使用它们,也应将它们归零。
请注意,有些模拟器比真实的硬件更好,并且为您提供了良好的初始状态。然后,当您在真实的硬件上运行时,一切都会中断。
GNU GRUB多重启动
引导扇区很简单,但是它们并不十分方便:
出于这些原因,GNU GRUB创建了一种更方便的文件格式,称为multiboot。
最小的工作示例:https : //github.com/cirosantilli/x86-bare-metal-examples/tree/d217b180be4220a0b4a453f31275d38e697a99e0/multiboot/hello-world
我还在GitHub示例存储库中使用了它,从而能够轻松地在真实硬件上运行所有示例,而无需烧掉USB一百万次。在QEMU上看起来像这样:
如果您将操作系统准备为多重引导文件,则GRUB可以在常规文件系统中找到它。
这是大多数发行版所做的,将OS映像置于下/boot
。
基本上,多重引导文件是带有特殊头的ELF文件。GRUB在以下位置指定了它们:https : //www.gnu.org/software/grub/manual/multiboot/multiboot.html
您可以使用将多引导文件转换为可引导磁盘grub-mkrescue
。
埃尔托里托
可以刻录到CD的格式:https : //en.wikipedia.org/wiki/El_Torito_%28CD-ROM_standard%29
也可以生成在ISO或USB上均可使用的混合图像。这可以通过grub-mkrescue
(示例)完成,也可以由Linux内核make isoimage
使用来完成isohybrid
。
资源资源