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

在没有操作系统运行的情况下,如何单独运行一个程序? 你能创建计算机在启动时可以加载和运行的汇编程序吗?例如,从闪存驱动器启动计算机,它运行CPU上的程序?< / p >
120825 次浏览

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

你把你的二进制代码放在处理器重新启动后寻找的地方(例如ARM上的地址0)。

你能创建计算机在启动时可以加载和运行的组装程序吗(例如,从闪存驱动器启动计算机,它运行驱动器上的程序)?

这个问题的一般答案是:这是可以做到的。 它通常被称为“裸金属编程”。 从u盘读取,你想知道什么是USB,你想有一些驱动程序与这个USB工作。这个驱动器上的程序也必须是某种特定的格式,在某种特定的文件系统上……这是引导加载程序通常会做的事情,但是如果固件只加载一小块代码,那么你的程序可以包括它自己的引导加载程序,这样它是自包含的

许多ARM板允许您做其中一些事情。有些有引导加载程序来帮助你进行基本设置。

在这里你可以找到一个关于如何在树莓派上做一个基本操作系统的很好的教程。

< p >编辑: 本文和整个wiki.osdev.org将回答您的大部分问题 http://wiki.osdev.org/Introduction < / p >

此外,如果不想直接在硬件上进行试验,可以使用qemu之类的管理程序将其作为虚拟机运行。了解如何直接在虚拟ARM硬件在这里上运行“hello world”。

可运行的例子

让我们创建并运行一些小型的裸机hello world程序,在没有操作系统的情况下运行:

我们还将尽可能多地在QEMU模拟器上进行测试,因为这样更安全,更方便开发。QEMU测试是在Ubuntu 18.04主机上进行的,并预先打包了QEMU 2.11.1。

下面所有x86示例的代码和更多代码都在这个GitHub回购上。

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

记住,在真正的硬件上运行示例可能是危险的,例如,您可能会错误地擦除磁盘或砖化硬件:只在不包含关键数据的旧机器上执行此操作!或者更好的是,使用便宜的半一次性开发板,如树莓派,参见下面的ARM示例。

对于一台典型的x86笔记本电脑,你必须这样做:

  1. 将图像刻录到u盘(会破坏你的数据!):

    sudo dd if=main.img of=/dev/sdX
    
  2. 插入电脑USB

  3. 打开它

  4. 告诉它从USB引导。

    这意味着固件选择USB而不是硬盘。

    如果这不是你的机器的默认行为,在开机后继续按Enter, F12, ESC或其他类似的奇怪键,直到你看到一个可以选择从USB启动的启动菜单。

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

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

开机后,我必须按Enter键进入启动菜单:

enter image description here

然后,在这里我必须按F12选择USB作为启动设备:

enter image description here

从那里,我可以选择USB作为启动设备,就像这样:

enter image description here

或者,要改变引导顺序,并选择USB具有更高的优先级,这样我就不必每次都手动选择它,我会在“启动中断菜单”上按F1。筛选,然后导航到:

enter image description here

引导扇区

在x86上,你能做的最简单和最低级别的事情是创建一个主引导扇区(MBR),它是引导扇区的一种类型,然后将它安装到磁盘上。

这里我们用一个printf调用创建了一个:

printf '\364%509s\125\252' > main.img
sudo apt-get install qemu-system-x86
qemu-system-x86_64 -hda main.img

结果:

enter image description here

请注意,即使什么都不做,一些字符已经打印在屏幕上。这些信息由固件打印出来,用于识别系统。

在T430上,我们只有一个空白的屏幕和闪烁的光标:

enter image description here

main.img包含以下内容:

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

    因此,我们的程序不会做任何事情:只有启动和停止。

    我们使用八进制是因为POSIX没有指定\x十六进制数。

    我们可以很容易地得到这个编码:

    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 in octal == 0x55后跟0xaa

    这是两个必需的神奇字节,必须是字节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

其中20是ASCII中的空格。

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

Hello world引导区

现在我们已经创建了一个最小程序,让我们转移到一个hello世界。

显而易见的问题是:如何进行IO?有几个选择:

  • 请固件,例如BIOS或UEFI,为我们做这件事

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

  • 写一个驱动程序,并直接与显示硬件对话。这是“适当”;方法:更强大,但更复杂。

  • < p > 串行端口。这是一个非常简单的标准化协议,用于从主机终端发送和接收字符。

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

    enter image description here

    不幸的是,它没有在大多数现代笔记本电脑上公开,但在开发板上是常见的方法,请参阅下面的ARM示例。

    这真的很遗憾,因为这样的接口非常有用例如调试Linux内核

  • 使用芯片调试特性。例如ARM调用他们的semihosting。在真正的硬件上,它需要一些额外的硬件和软件支持,但在模拟器上,它可以是一个免费的方便的选择。例子

这里我们将做一个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上游

link.ld

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

结果:

enter image description here

关于T430:

enter image description here

测试环境:联想Thinkpad T430, UEFI BIOS 1.16。Ubuntu 18.04主机生成的磁盘。

除了标准的用户区汇编指令,我们还有:

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

  • cli:禁用软件中断。这些可以使处理器在hlt之后重新开始运行

  • int $0x10:进行BIOS调用。这是一个接一个打印字符的方法。

重要的链接标志是:

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

为了更好地理解链接器脚本部分,请熟悉链接的重定位步骤:链接器做什么?

更酷的x86裸金属程序

以下是我所实现的一些更复杂的裸金属设置:

使用C来代替汇编

总结:使用GRUB multiboot,它将解决许多您从未想过的烦人问题。请参阅下面的部分。

x86上的主要困难是BIOS只从磁盘加载512字节到内存,而当使用C!

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

另外:

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

仅出于教育目的,这里是一个阶段最小的C示例:

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

linker.ld

ENTRY(mystart)
SECTIONS
{
. = 0x7c00;
.text : {
entry.o(.text)
*(.text)
*(.data)
*(.rodata)
__bss_start = .;
/* COMMON vs BSS: https://stackoverflow.com/questions/16835716/bss-vs-common-what-goes-where */
*(.bss)
*(COMMON)
__bss_end = .;
}
/* https://stackoverflow.com/questions/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内核,它实现了C标准库的大部分功能通过POSIX

不需要像Linux这样成熟的操作系统,有以下几种可能性:

  • 自己写。它只是一堆头文件和C文件,对吧?对吧? ?

  • < p > Newlib

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

    Newlib为你实现了所有无聊的非操作系统特定的东西,例如memcmpmemcpy等。

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

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

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

    在这个例子中所示。

    例如,你可以将printf重定向到UART或ARM系统,或者用semihosting实现exit()

  • 嵌入式操作系统,如FreeRTOS西风

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

    它们可以被视为某种预先实现的Newlib。

GNU GRUB Multiboot

引导扇区很简单,但是不是很方便:

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

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

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

我还在我的GitHub示例回购上使用它,以便能够轻松地在真实硬件上运行所有示例,而无需燃烧USB一百万次。

QEMU的结果:

enter image description here

T430:

enter image description here

如果您将操作系统准备为一个多引导文件,那么GRUB就能够在常规文件系统中找到它。

这是大多数发行版所做的,将操作系统映像放在/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打印调用之类的东西

固件提供了大多数操作系统所依赖的类似操作系统的功能。例如,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这样的寄存器有重要的副作用,所以即使你没有显式地使用它们,你也应该把它们归零。

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

El Torito

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

也可以生成在ISO或USB上工作的混合映像。这可以通过grub-mkrescue (例子)来完成,也可以通过Linux内核在make isoimage上使用isohybrid来完成。

手臂

在ARM中,一般的思想是相同的。

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

  • Serial,在开发板上广泛可用
  • 闪烁LED

我已上传:

与x86的一些区别包括:

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

    这被称为内存映射IO

  • 对于一些真正的硬件,如树莓派,您可以自己将固件(BIOS)添加到磁盘映像中。

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

资源

以操作系统为灵感

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

例如,这个页面可以作为一个起点:

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

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

这样或类似的简单操作系统可以用于创建一个简单的框架,允许我们:

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

例如,要查看更大的x86汇编语言操作系统,我们可以探索MykeOS, x86操作系统,它是一个学习工具,以显示简单的16位实模式操作系统,使用了良好的注释代码丰富的文档

引导加载程序作为灵感

其他常见类型的在没有操作系统的情况下运行的程序也是引导加载程序。我们可以创建一个受这样一个概念启发的程序,例如使用这个网站:

如何开发自己的引导加载程序

上面的文章还介绍了这样一个程序的基本架构:

  1. 正确加载到内存的000:7c00地址。
  2. 调用BootMain函数是用高级语言开发的。
  3. 显示“Hello, world…”,from low-level”信息。

正如我们所看到的,这种架构非常灵活,允许我们实现任何程序不一定是引导加载程序。

特别地,它展示了如何使用“混合代码”技术,因此可以实现结合高层建筑(来自Cc++) 使用低级命令(来自汇编程序)。这是一个非常有用的方法,但我们必须记住:

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

本文还展示了如何查看创建的程序的运行情况,以及如何执行它的测试和调试。

以UEFI应用为灵感

上面的例子使用了在数据介质上加载扇区MBR的事实。例如,将然而,我们可以深入到深处UEFI applications放置:

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

一种类型的UEFI应用程序是操作系统加载程序,如GRUB, rEFInd, Gummiboot和Windows引导管理器;它将操作系统文件加载到内存中并执行。此外,操作系统加载器可以提供一个用户界面,以允许选择另一个UEFI应用程序运行。像UEFI shell这样的实用程序也是UEFI应用程序。

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

__abc0 / __abc1

以探索安全问题为灵感

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

他们中的一大部分操作在MBR部门或UEFI应用程序上,就像上面所有的解决方案一样,但也有一些使用另一个入口点,如卷引导记录 (VBR)或BIOS:

至少有四种已知的BIOS攻击病毒,其中两个是为了演示目的。

也可能是另一个。

系统启动前的攻击

Bootkits已经从概念验证发展到大规模分布和现在已经有效地成为开源软件了

不同的启动方式

我还认为在这种情况下,还值得一提的是各种形式的引导操作系统(或用于此的可执行程序)。有很多,但我想注意从网络加载代码使用网络引导选项(PXE),它允许我们在计算机不管它的操作系统,甚至不管任何存储介质上运行程序,直接连接到计算机:

什么是网络启动(PXE)和如何使用它?< / >

我写了一个基于Win32的c++程序,写一个程序集到笔盘的引导扇区。当计算机从笔驱动启动时,它会成功地执行代码-看看这里写入USB Pendrive引导扇区的程序

这个程序是几行,应该在配置了windows编译器的编译器上编译——比如visual studio编译器——任何可用版本。