如何在没有操作系统的情况下运行程序?


239

如何在不运行操作系统的情况下独自运行程序?您是否可以创建计算机可以在启动时加载并运行的程序集,例如从闪存驱动器启动计算机并运行CPU上的程序?


4
在哪种架构上?x86?臂?
基西尔

1
我一般地说,但是最有可能是x86或x64
user2320609 2014年

2
是的,这正是处理器启动的方式。不必一定要汇编,C经常与一些asm一起使用来进行引导和其他支持。
old_timer 2014年

24
想一想:如果没有这种功能,操作系统本身将如何启动和运行?:)
Seva Alekseyev 2015年

Answers:


153

如何在不运行操作系统的情况下独自运行程序?

您将二进制代码放置在重新引导后处理器要查找的位置(例如,ARM上的地址0)。

您是否可以创建计算机可以在启动时加载并运行的程序集(例如,从闪存驱动器启动计算机并运行该驱动器上的程序)?

这个问题的一般答案:可以做到。它通常被称为“裸机编程”。要从闪存驱动器中读取数据,您想知道什么是USB,并且您需要一些驱动程序来使用该USB。该驱动器上的程序还必须采用某种特定格式,在某种特定文件系统上……这是引导加载程序通常会执行的操作,但是您的程序可以包括自己的引导加载程序,因此它是自包含的(如果仅固件)加载一小段代码。

许多ARM板使您可以执行其中的某些操作。有些具有引导加载程序,可以帮助您进行基本设置。

在这里,您可以找到有关如何在Raspberry Pi上执行基本操作系统的出色教程。

编辑:本文以及整个wiki.osdev.org将回答您的大多数问题 http://wiki.osdev.org/Introduction

另外,如果您不想直接在硬件上进行实验,则可以使用qemu等虚拟机管理程序将其作为虚拟机运行。在此处了解如何直接在虚拟化ARM硬件上运行“ hello world” 。


723

可运行的例子

让我们创建并运行一些没有OS的微型裸机hello world程序:

我们还将尽可能在QEMU仿真器上对其进行尝试,因为这样做更安全,更方便开发。QEMU测试已在具有预打包QEMU 2.11.1的Ubuntu 18.04主机上进行。

GitHub存储库中提供了以下所有x86示例以及更多示例的代码。

如何在x86真实硬件上运行示例

请记住,在真实的硬件上运行示例可能很危险,例如,您可能会误擦磁盘或对硬件进行砌块:仅在不包含关键数据的旧机器上这样做!甚至更好的是,使用便宜的半一次性开发板,例如Raspberry Pi,请参见下面的ARM示例。

对于典型的x86笔记本电脑,您必须执行以下操作:

  1. 将图像刻录到USB记忆棒(会破坏您的数据!):

    sudo dd if=main.img of=/dev/sdX
    
  2. 将USB插入计算机

  3. 打开它

  4. 告诉它从USB启动。

    这意味着使固件在硬盘之前选择USB。

    如果这不是计算机的默认行为,请在开机后继续按Enter,F12,ESC或其他类似的怪异键,直到获得启动菜单,您可以在其中选择从USB引导。

    通常可以在那些菜单中配置搜索顺序。

例如,在我的T430上,我看到以下内容。

打开电源后,这是我必须按Enter进入启动菜单的时间:

在此处输入图片说明

然后,在这里我必须按F12键选择USB作为引导设备:

在此处输入图片说明

从那里,我可以选择USB作为引导设备,如下所示:

在此处输入图片说明

另外,要更改启动顺序并选择优先级更高的USB,这样我就不必每次都手动选择它,我将在“启动中断菜单”屏幕上按F1,然后导航至:

在此处输入图片说明

引导区

在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

结果:

在此处输入图片说明

请注意,即使不执行任何操作,屏幕上也已经打印了几个字符。这些由固件打印,并用于识别系统。

在T430上,我们只是得到一个空白屏幕,光标闪烁:

在此处输入图片说明

main.img 包含以下内容:

  • \364八进制== 0xf4十六进制:hlt指令的编码,告诉CPU停止工作。

    因此,我们的程序不会执行任何操作:仅启动和停止。

    我们使用八进制,因为\xPOSIX未指定十六进制数。

    我们可以通过以下方式轻松获得此编码:

    echo hlt > a.S
    as -o a.o a.S
    objdump -S a.o
    

    输出:

    a.o:     file format elf64-x86-64
    
    
    Disassembly of section .text:
    
    0000000000000000 <.text>:
       0:   f4                      hlt
    

    但是它当然也记录在英特尔手册中。

  • %509s产生509个空格。需要填写文件,直到字节510。

  • \125\252以八进制== 0x55后跟0xaa

    这是2个必需的魔术字节,必须是字节511和512。

    BIOS会遍历我们所有的磁盘以寻找可引导磁盘,并且仅将具有这两个魔术字节的磁盘视为可引导磁盘。

    如果不存在,则硬件不会将其视为可引导磁盘。

如果您不是printf管理员,则可以使用以下命令确认其内容main.img

hd main.img

显示预期的结果:

00000000  f4 20 20 20 20 20 20 20  20 20 20 20 20 20 20 20  |.               |
00000010  20 20 20 20 20 20 20 20  20 20 20 20 20 20 20 20  |                |
*
000001f0  20 20 20 20 20 20 20 20  20 20 20 20 20 20 55 aa  |              U.|
00000200

其中20ASCII空格。

BIOS固件从磁盘读取这512个字节,将它们放入内存,然后将PC设置为第一个字节以开始执行它们。

你好世界引导部门

现在我们已经制作了一个最小的程序,让我们进入一个问候世界。

显而易见的问题是:如何做IO?一些选择:

  • 要求固件(例如BIOS或UEFI)为我们做

  • VGA:特殊的内存区域,如果写入,则会打印到屏幕上。可以在保护模式下使用。

  • 编写驱动程序并直接与显示硬件对话。这是做到这一点的“正确”方法:功能更强大,但更复杂。

  • 串口。这是一个非常简单的标准化协议,可以从主机终端发送和接收字符。

    在台式机上,它看起来像这样:

    在此处输入图片说明

    来源

    不幸的是,它没有在大多数现代笔记本电脑上公开,但是是开发板的常用方法,请参见下面的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"

GitHub上游

链接

SECTIONS
{
    /* The BIOS loads the code from the disk to this location.
     * We must tell that to the linker so that it can properly
     * calculate the addresses of symbols we might jump to.
     */
    . = 0x7c00;
    .text :
    {
        __start = .;
        *(.text)
        /* Place the magic boot bytes at the end of the first 512 sector. */
        . = 0x1FE;
        SHORT(0xAA55)
    }
}

组装并链接到:

as -g -o main.o main.S
ld --oformat binary -o main.img -T link.ld main.o
qemu-system-x86_64 -hda main.img

结果:

在此处输入图片说明

在T430上:

在此处输入图片说明

经过测试:Lenovo Thinkpad T430,UEFI BIOS 1.16。在Ubuntu 18.04主机上生成的磁盘。

除了标准的Userland组装说明,我们还有:

  • .code16:告诉GAS输出16位代码

  • cli:禁用软件中断。这些可能会使处理器在hlt

  • int $0x10:执行BIOS调用。这就是逐个打印字符的过程。

重要的链接标志是:

  • --oformat binary:输出原始二进制汇编代码,不要像常规用户级可执行文件那样将其包装在ELF文件中。

为了更好地了解链接描述文件,请熟悉链接的重定位步骤:链接描述是做什么的?

较酷的x86裸机程序

这是我已经完成的一些更复杂的裸机设置:

使用C代替汇编

简介:使用GRUB multiboot,它将解决许多您从未想到的烦人的问题。请参阅以下部分。

x86上的主要困难是BIOS仅将磁盘中的512个字节加载到内存中,使用C时您很可能会炸破这512个字节!

为了解决这个问题,我们可以使用两阶段的bootloader。这将导致进一步的BIOS调用,从而将更多的字节从磁盘加载到内存中。这是使用int 0x13 BIOS调用从头开始的最小的第2阶段组装示例:

或者:

  • 如果您只需要它在QEMU中工作而不在真正的硬件中工作,请使用该-kernel选项,该选项会将整个ELF文件加载到内存中。这是我使用该方法创建的一个ARM示例
  • 对于Raspberry Pi,默认固件将从名为的ELF文件中为我们加载图像kernel7.img,这与QEMU -kernel一样。

仅出于教育目的,这是一个单阶段的最低C示例

main.c

void main(void) {
    int i;
    char s[] = {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'};
    for (i = 0; i < sizeof(s); ++i) {
        __asm__ (
            "int $0x10" : : "a" ((0x0e << 8) | s[i])
        );
    }
    while (1) {
        __asm__ ("hlt");
    };
}

入口

.code16
.text
.global mystart
mystart:
    ljmp $0, $.setcs
.setcs:
    xor %ax, %ax
    mov %ax, %ds
    mov %ax, %es
    mov %ax, %ss
    mov $__stack_top, %esp
    cld
    call main

链接器

ENTRY(mystart)
SECTIONS
{
  . = 0x7c00;
  .text : {
    entry.o(.text)
    *(.text)
    *(.data)
    *(.rodata)
    __bss_start = .;
    /* COMMON vs BSS: /programming/16835716/bss-vs-common-what-goes-where */
    *(.bss)
    *(COMMON)
    __bss_end = .;
  }
  /* /programming/53584666/why-does-gnu-ld-include-a-section-that-does-not-appear-in-the-linker-script */
  .sig : AT(ADDR(.text) + 512 - 2)
  {
      SHORT(0xaa55);
  }
  /DISCARD/ : {
    *(.eh_frame)
  }
  __stack_bottom = .;
  . = . + 0x1000;
  __stack_top = .;
}

set -eux
as -ggdb3 --32 -o entry.o entry.S
gcc -c -ggdb3 -m16 -ffreestanding -fno-PIE -nostartfiles -nostdlib -o main.o -std=c99 main.c
ld -m elf_i386 -o main.elf -T linker.ld entry.o main.o
objcopy -O binary main.elf main.img
qemu-system-x86_64 -drive file=main.img,format=raw

C标准库

但是,如果您还想使用C标准库,事情会变得更加有趣,因为我们没有Linux内核,该内核通过POSIX实现了许多C标准库功能。

在没有使用像Linux这样的成熟OS的情况下,可能的几种方法包括:

  • 自己写。最后只是一堆标题和C文件,对吗?对??

  • 新库

    有关详细示例,访问:https//electronics.stackexchange.com/questions/223929/c-standard-libraries-on-bare-metal/223931

    Newlib实现了所有为你枯燥的非OS具体的事情,比如memcmpmemcpy

    然后,它为您提供了一些存根,以实现您需要的系统调用。

    例如,我们可以exit()通过半主机在ARM上实现:

    void _exit(int status) {
        __asm__ __volatile__ ("mov r0, #0x18; ldr r1, =#0x20026; svc 0x00123456");
    }
    

    本例所示。

    例如,您可以重定向printf到UART或ARM系统,或exit()使用semihosting实现。

  • 嵌入式操作系统,如FreeRTOSZephyr

    此类操作系统通常允许您关闭抢先式调度,因此可以完全控制程序的运行时间。

    可以将它们视为一种预先实现的Newlib。

GNU GRUB多重启动

引导扇区很简单,但是它们并不十分方便:

  • 每个磁盘只能有一个操作系统
  • 加载代码必须非常小且适合512字节
  • 您必须自己做很多启动工作,例如进入保护模式

出于这些原因,GNU GRUB创建了一种更方便的文件格式,称为multiboot。

最小的工作示例:https : //github.com/cirosantilli/x86-bare-metal-examples/tree/d217b180be4220a0b4a453f31275d38e697a99e0/multiboot/hello-world

我还在GitHub示例存储库中使用了它,从而能够轻松地在真实硬件上运行所有示例,而无需烧掉USB一百万次。

QEMU结果:

在此处输入图片说明

T430:

在此处输入图片说明

如果您将操作系统准备为多重引导文件,则GRUB可以在常规文件系统中找到它。

这是大多数发行版所做的,将OS映像置于下/boot

基本上,多重引导文件是带有特殊标头的ELF文件。GRUB在以下位置指定了它们:https : //www.gnu.org/software/grub/manual/multiboot/multiboot.html

您可以使用将多引导文件转换为可引导磁盘grub-mkrescue

固件

实际上,引导扇区并不是在系统CPU上运行的第一个软件。

实际上首先运行的是所谓的固件,它是一种软件:

  • 由硬件制造商制造
  • 通常是封闭源,但可能基于C
  • 存储在只读存储器中,因此未经供应商同意很难/无法修改。

众所周知的固件包括:

  • BIOS:老式的x86固件。SeaBIOS是QEMU使用的默认开源实现。
  • UEFI:BIOS的后续产品,标准化程度更高,但功能更强大,而且膨胀得令人难以置信。
  • Coreboot:高贵的交叉拱门开源尝试

固件执行以下操作:

  • 遍历每个硬盘,USB,网络等,直到找到可启动的设备。

    当我们运行QEMU时,它-hdamain.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也可以访问BIOS ROM,因为必须这样做,否则CPU将没有执行指令。这通常并不意味着BIOS ROM已完全映射。但是只有足够的映射才能使启动过程继续进行。任何其他设备,只需忘记它即可。

在QEMU下运行Coreboot时,可以尝试使用Coreboot的较高层和有效负载,但是QEMU提供的机会很少,无法尝试使用低级启动代码。一方面,RAM从一开始就可以正常工作。

后BIOS初始状态

像硬件中的许多事情一样,标准化很弱,并且您的代码在BIOS之后开始运行时,您应该依赖的事情之一就是寄存器的初始状态。

因此,请帮自己一个忙,并使用类似以下的初始化代码:https : //stackoverflow.com/a/32509555/895245

喜欢%ds%es具有重要副作用的寄存器,因此即使您没有明确使用它们,也应将它们归零。

请注意,有些模拟器比真实的硬件更好,并且为您提供了良好的初始状态。然后,当您在真实的硬件上运行时,一切都会中断。

埃尔托里托

可以刻录到CD的格式:https : //en.wikipedia.org/wiki/El_Torito_%28CD-ROM_standard%29

也可以生成在ISO或USB上均可使用的混合图像。这可以用grub-mkrescue示例)完成,也可以由Linux内核make isoimage使用来完成isohybrid

在ARM中,总体思路是相同的。

我们没有可用于IO的广泛使用的半标准化预安装固件(例如BIOS)供我们使用,因此我们可以做的两种最简单的IO类型是:

  • 序列号,广泛用于devboards
  • 闪烁LED

我上传了:

与x86的一些区别包括:

  • IO是通过直接写入魔术地址来完成的,没有inout指令。

    这称为内存映射IO

  • 对于某些真正的硬件,例如Raspberry Pi,您可以自己将固件(BIOS)添加到磁盘映像中。

    这是一件好事,因为它使更新固件更加透明。

资源资源


3
对于无法/不想走到如此低的水平并且仍然希望从其极小的占地面积中受益的人们,Unikernels是一种替代选择。
AndreLDM

1
@AndreLDM我濒临添加基于Linux的Unikernel新闻,但感觉还很
Ciro Santilli郝海东冠状病六四事件法轮功

14
答案确实很详细,但是“没有操作系统运行的程序就是操作系统”是不正确的。您可以编写一个仅使LED闪烁的程序,但不能使其成为操作系统。一些在闪存驱动器上运行微控制器的固件代码无法使其成为操作系统。操作系统至少是抽象层,可以更轻松地编写其他软件。这些天我至少要说的是,如果没有调度程序,则可能不是操作系统。
维塔利

4
一个很好的答案,除了绝对废话之外,任何不在操作系统中运行的程序都是操作系统。
curiousdannii

3
@MichaelPetch嘿,只是在引导扇区中保存null :-)可能不值得。
西罗Santilli郝海东冠状病六四事件法轮功

3

操作系统为灵感

操作系统也是一个程序,因此我们也可以通过从头开始创建或更改(限制或添加)小型操作系统的功能来创建自己的程序,然后在引导过程中运行它(使用ISO映像)。 。

例如,此页面可用作起点:

如何编写一个简单的操作系统

在这里,整个操作系统完全适合512字节的引导扇区(MBR)!

可以使用此类或类似的简单OS 创建一个简单的框架,该框架可以使我们:

使引导加载程序将磁盘上的后续扇区加载到RAM中,并跳转到该点以继续执行。或者,您可以阅读FAT12(软盘驱动器上使用的文件系统)并实现该文件系统

但是,有很多可能性。例如,要查看更大的x86汇编语言操作系统,我们可以探索MykeOS,x86操作系统,这是一个学习工具,可通过简单注释的代码大量文档展示简单的16位,实模式操作系统。

引导加载程序的灵感

在没有操作系统的情况下运行的其他常见程序类型也是Boot Loader。我们可以使用以下站点来创建一个受这种概念启发的程序:

如何开发自己的Boot Loader

上面的文章还介绍了此类程序基本架构

  1. 通过0000:7C00地址正确加载到存储器。
  2. 调用以高级语言开发的BootMain函数
  3. 在显示屏上显示“低级”,“您好,世界……”消息。

如我们所见,该体系结构非常灵活,可以使我们实现任何程序,而不必是引导加载程序。

特别是,它显示了如何使用“混合代码”技术,从而可以将高级构造(来自CC ++与低级命令(来自Assembler)结合起来。这是一种非常有用的方法,但是我们必须记住:

要构建程序并获取可执行文件,您将需要16位模式的Assembler的编译器和链接器对于C / C ++,您只需要可以为16位模式创建目标文件编译器

本文还显示了如何查看正在运行的程序,以及如何执行其测试和调试。

UEFI应用程序的灵感

上面的示例使用了将扇区MBR加载到数据介质上的事实。但是,例如通过UEFI应用程序,我们可以更深入地研究

除了加载OS,UEFI还可以运行UEFI应用程序,这些应用程序作为文件驻留在EFI系统分区上。它们可以从UEFI命令外壳,固件的启动管理器或其他UEFI应用程序执行。UEFI应用程序可以独立于系统制造商开发和安装。

UEFI应用程序的一种类型是OS加载程序,例如GRUB,rEFInd,Gummiboot和Windows Boot Manager。它将OS文件加载到内存中并执行它。而且,OS加载程序可以提供用户界面,以允许选择另一个要运行的UEFI应用程序。UEFI Shell之类的实用程序也是UEFI应用程序。

如果我们想开始创建这样的程序例如,我们可以从以下网站开始:

EFI编程:创建“ Hello,World”程序 / UEFI编程-第一步

探索安全问题为灵感

众所周知,在操作系统启动之前,有一整组恶意软件(程序)正在运行

就像以上所有解决方案一样,它们中的很大一部分都在MBR扇区或UEFI应用程序上运行,但是也有一些使用其他入口点的应用程序,例如卷启动记录(VBR)或BIOS

至少有四种已知的BIOS攻击病毒,其中两种用于演示目的。

也许也可以。

系统启动前的攻击

Bootkit已从概念验证开发演变为大规模发行,现已有效地成为开源软件

不同的启动方式

我还认为,在这种情况下,还值得一提的是,各种形式的引导操作系统(或用于此目的的可执行程序)引导。有很多,但是我要特别注意使用“网络启动”选项(PXE从网络加载代码,该选项使我们可以在计算机上运行程序,而无需考虑其操作系统,甚至不考虑任何存储介质。直接连接到计算机:

什么是网络启动(PXE),以及如何使用它?

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.