ChatGPT解决这个技术问题 Extra ChatGPT

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

在没有操作系统运行的情况下如何单独运行程序?您能否创建计算机可以在启动时加载和运行的汇编程序,例如从闪存驱动器启动计算机并运行 CPU 上的程序?

在哪个架构上? x86?手臂?
我说的是一般性,但很可能是 x86 或 x64
是的,这正是处理器启动的方式。不必是汇编,C 通常与一点 asm 一起用于引导程序,也许还有其他一些支持。
想一想:如果没有这样的能力,操作系统本身将如何启动和运行? :)

C
Community

可运行的示例

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

带有 UEFI BIOS 1.16 固件的 x86 Lenovo Thinkpad T430 笔记本电脑

基于 ARM 的树莓派 3

我们也会尽量在 QEMU 模拟器上试用,这样更安全、更方便开发。 QEMU 测试是在带有预打包 QEMU 2.11.1 的 Ubuntu 18.04 主机上进行的。

this GitHub repo 上提供了以下所有 x86 示例的代码以及更多内容。

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

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

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

将映像刻录到 U 盘(会破坏您的数据!): sudo dd if=main.img of=/dev/sdX 将 USB 插入计算机上打开它,告诉它从 USB 启动。这意味着让固件在硬盘之前选择 USB。如果这不是您机器的默认行为,请在开机后继续按 Enter、F12、ESC 或其他类似的奇怪键,直到您获得一个启动菜单,您可以在其中选择从 USB 启动。通常可以在这些菜单中配置搜索顺序。

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

开机后,这是我必须按Enter进入启动菜单的时候:

https://i.stack.imgur.com/WfLBl.jpg

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

https://i.stack.imgur.com/S9zBv.jpg

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

https://i.stack.imgur.com/fxoDn.jpg

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

https://i.stack.imgur.com/8pIGP.jpg

引导扇区

在 x86 上,您可以做的最简单和最低级别的事情是创建一个 Master Boot Sector (MBR),它是 boot sector 的一种类型,然后将其安装到磁盘上。

在这里,我们使用单个 printf 调用创建一个:

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

结果:

https://i.stack.imgur.com/CvZ7r.png

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

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

https://i.stack.imgur.com/Gn8nH.jpg

main.img 包含以下内容:

\364 in octal == 0xf4 in hex:hlt 指令的编码,它告诉 CPU 停止工作。因此我们的程序不会做任何事情:只是启动和停止。我们使用八进制是因为 POSIX 没有指定 \x 十六进制数字。我们可以通过以下方式轻松获得此编码: echo hlt > aS as -o ao aS objdump -S ao 输出: ao: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <.text>: 0: f4 hlt but当然,它也记录在英特尔手册中。

%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

其中 20 是 ASCII 中的空格。

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

你好世界引导扇区

现在我们已经制作了一个最小的程序,让我们进入一个 hello world。

显而易见的问题是:如何做 IO?几个选项:

要求固件(例如 BIOS 或 UEFI)为我们执行此操作

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

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

串行端口。这是一个非常简单的标准化协议,可以从主机终端发送和接收字符。在台式机上,它看起来像这样:Source。不幸的是,它没有在大多数现代笔记本电脑上公开,但它是开发板的常用方法,请参见下面的 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 upstream

链接.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

结果:

https://i.stack.imgur.com/qRPUW.png

在 T430 上:

https://i.stack.imgur.com/auien.jpg

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

除了标准的用户区组装说明,我们还有:

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

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

int $0x10:进行 BIOS 调用。这就是一个一个地打印字符。

重要的链接标志是:

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

为了更好地理解链接描述文件部分,请熟悉链接的重定位步骤:What do linkers do?

Cooler x86 裸机程序

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

multicore:多核汇编语言是什么样的?

分页:x86 分页是如何工作的?

使用 C 而不是汇编

总结:使用 GRUB 多重引导,它会解决很多你从未想过的恼人问题。请参阅下面的部分。

x86 的主要困难在于 BIOS 仅将 512 字节从磁盘加载到内存,而使用 C 时您很可能会炸毁这 512 字节!

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

或者:

如果您只需要它在 QEMU 中而不是真正的硬件中工作,请使用 -kernel 选项,它将整个 ELF 文件加载到内存中。这是我使用该方法创建的 ARM 示例。

对于 Raspberry Pi,默认固件会为我们从名为 kernel7.img 的 ELF 文件加载图像,就像 QEMU -kernel 所做的那样。

仅出于教育目的,这里有一个 one stage minimal C example

主程序

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");
    };
}

条目.S

.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

链接器.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 标准库的大部分功能through POSIX

一些可能性,无需使用像 Linux 这样的成熟操作系统,包括:

写你自己的。最后只是一堆头文件和C文件,对吧?正确的??

Newlib 详细示例位于:https://electronics.stackexchange.com/questions/223929/c-standard-libraries-on-bare-metal/223931 Newlib 为您实现了所有无聊的非操作系统特定的东西,例如 memcmp、memcpy、等等。然后,它为您提供了一些存根来实现您自己需要的系统调用。例如,我们可以通过半主机在 ARM 上实现 exit(): void _exit(int status) { __asm__ __volatile__ ("mov r0, #0x18; ldr r1, =#0x20026; svc 0x00123456"); } 如本例所示。例如,您可以将 printf 重定向到 UART 或 ARM 系统,或者使用半主机实现 exit()。

嵌入式操作系统,如 FreeRTOS 和 Zephyr。此类操作系统通常允许您关闭先发制人的调度,因此您可以完全控制程序的运行时间。它们可以被看作是一种预先实现的 Newlib。

GNU GRUB 多重引导

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

每个磁盘只能有一个操作系统

加载代码必须非常小并且适合 512 字节

你必须自己做很多启动,比如进入保护模式

正是出于这些原因,GNU GRUB 创建了一种更方便的文件格式,称为多重引导。

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

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

QEMU 结果:

https://i.stack.imgur.com/zWfWl.png

T430:

https://i.stack.imgur.com/q7Bfx.jpg

如果您将操作系统准备为多重引导文件,那么 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的时候,-hda表示main.img是连接硬件的硬盘,hda是第一个尝试的,就用了。

将前 512 个字节加载到 RAM 内存地址 0x7c00,将 CPU 的 RIP 放在那里,然后让它运行

在显示屏上显示启动菜单或 BIOS 打印调用等内容

固件提供了大多数操作系统所依赖的类似操作系统的功能。例如,已移植 Python 子集以在 BIOS/UEFI 上运行:https://www.youtube.com/watch?v=bYQ_lq5dcvM

可以说固件与操作系统没有区别,固件是唯一可以做的“真正的”裸机编程。

作为这个 CoreOS dev puts it

困难的部分 当您启动 PC 时,构成芯片组(北桥、南桥和 SuperIO)的芯片尚未正确初始化。即使 BIOS ROM 尽可能远离 CPU,CPU 也可以访问它,因为它必须如此,否则 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 (example) 完成,也可以通过 make isoimage 上的 Linux 内核使用 isohybrid 完成。

手臂

在 ARM 中,大体思路是一样的。

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

串行,在开发板上广泛可用

闪烁 LED

我已上传:

GitHub 上有一些简单的 QEMU C + Newlib 和原始汇编示例。例如,prompt.c 示例从您的主机终端获取输入,并通过模拟的 UART 全部返回输出:输入一个字符得到:在地址 0x0x4000a1c0 处新分配 1 个字节输入一个字符得到:b 在地址处新分配 2 个字节0x0x4000a1c0 输入字符 另请参阅:如何制作裸机 ARM 程序并在 QEMU 上运行它们?

一个完全自动化的 Raspberry Pi 闪光灯设置:https://github.com/cirosantilli/raspberry-pi-bare-metal-blinker 另请参阅:如何在 Raspberry Pi 上运行没有操作系统的 C 程序?要“查看”QEMU 上的 LED,您必须使用调试标志从源代码编译 QEMU:https://raspberrypi.stackexchange.com/questions/56373/is-it-possible-to-get-the-state-of- the-leds-and-gpios-in-a-qemu-emulation-like-t 接下来,您应该尝试 UART hello world。您可以从闪烁器示例开始,并用这个替换内核:https://github.com/dwelch67/raspberrypi/tree/bce377230c2cdd8ff1e40919fdedbc2533ef5a00/uart01 首先让 UART 与 Raspbian 一起使用,正如我在以下位置解释的那样:https:// /raspberrypi.stackexchange.com/questions/38/prepare-for-ssh-without-a-screen/54394#54394 它看起来像这样:确保使用正确的引脚,否则您可以将 UART 烧录到 USB转换器,我已经通过短路接地和 5V 完成了两次... 最后从主机连接到串行: screen /dev/ttyUSB0 115200 对于 Raspberry Pi,我们使用 Micro SD 卡而不是 USB 记忆棒包含我们的可执行文件,您通常需要一个适配器来连接到您的计算机:不要忘记解锁 SD 适配器,如下所示:https://askubuntu.com/questions/213889/microsd-card-is-set -to-read-only-state-how-can-i-write-data-on-it/814585#814585 https://github.com/dwelch67/raspberrypi 看起来像最流行的裸机Raspberry Pi 教程今天可用。

与 x86 的一些区别包括:

IO 是通过直接写入幻地址来完成的,没有 in 和 out 指令。这称为内存映射 IO。

对于一些真实的硬件,比如 Raspberry Pi,您可以自己将固件 (BIOS) 添加到磁盘映像中。这是一件好事,因为它使更新固件更加透明。

资源

http://wiki.osdev.org 是这些问题的重要来源。

https://github.com/scanlime/metalkit 是一个更自动化/通用的裸机编译系统,它提供了一个微小的自定义 API


Unikernels 对于不能/不想达到如此低水平但仍希望从其极低的足迹中受益的人来说是一种替代方案。
@AndreLDM 我即将添加基于 Linux 的 Unikernel 新闻,但感觉太前卫了:next.redhat.com/2018/11/14/ukl-a-unikernel-based-on-linux
非常详细的答案,但“没有操作系统运行的程序就是操作系统”不是真的。您可以编写一个仅使 LED 闪烁开/关但不能使其成为操作系统的程序。在您的闪存驱动器上运行微控制器的某些固件代码不会使其成为操作系统。操作系统至少是一个抽象层,可以更轻松地编写其他软件。这些天至少我会说如果没有调度程序,它可能不是操作系统。
很好的答案,除了任何不在操作系统中运行的程序都是操作系统的绝对废话。
@MichaelPetch 嘿,只是为了在引导扇区中保存 null :-) 可能不值得。
P
Peter Cordes

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

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

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

问题的一般答案:可以做到。它通常被称为“裸机编程”。要从闪存驱动器读取,您想知道什么是 USB,并且您希望有一些驱动程序可以与此 USB 一起使用。该驱动器上的程序还必须采用某种特定格式,在某个特定文件系统上......这是引导加载程序通常会做的事情,但您的程序可以包含自己的引导加载程序,因此它是独立的,如果固件只会加载一小段代码。

许多 ARM 板可以让你做一些这样的事情。有些有引导加载程序来帮助您进行基本设置。

Here 您可能会找到关于如何在 Raspberry Pi 上构建基本操作系统的精彩教程。

编辑:这篇文章和整个 wiki.osdev.org 将回答您的大部分问题http://wiki.osdev.org/Introduction

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


s
simhumileco

操作系统为灵感

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

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

How to write a simple operating system

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

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

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

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

以引导加载程序为灵感

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

How to develop your own Boot Loader

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

通过 0000:7C00 地址正确加载到内存。调用使用高级语言开发的 BootMain 函数。在显示屏上显示““Hello, world...”,来自低级”消息。

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

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

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

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

UEFI 应用为灵感

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

除了加载操作系统之外,UEFI 还可以运行 UEFI 应用程序,这些应用程序作为文件驻留在 EFI 系统分区上。它们可以从 UEFI 命令外壳、固件的引导管理器或其他 UEFI 应用程序执行。 UEFI 应用程序可以独立于系统制造商进行开发和安装。 UEFI 应用程序的一种类型是操作系统加载程序,例如 GRUB、rEFInd、Gummiboot 和 Windows Boot Manager;它将操作系统文件加载到内存中并执行它。此外,OS 加载程序可以提供用户界面以允许选择另一个 UEFI 应用程序来运行。 UEFI shell 等实用程序也是 UEFI 应用程序。

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

Programming for EFI: Creating a "Hello, World" Program / UEFI Programming - First Steps

以探索安全问题为灵感

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

其中很大一部分在 MBR 扇区或 UEFI 应用程序上运行,就像上述所有解决方案一样,但也有一些使用另一个入口点,例如 Volume Boot Record (VBR) 或 BIOS

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

或者也许是另一个。

Attacks before system startup

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

不同的启动方式

我还认为,在这种情况下,还值得一提的是various forms of booting操作系统(或为此而设计的可执行程序)。有很多,但我想注意使用网络启动选项(PXE)从网络加载代码,这允许我们在计算机上运行程序无论其操作系统如何,甚至无论任何存储介质直接连接到计算机:

What Is Network Booting (PXE) and How Can You Use It?


H
Hoven

我编写了一个基于 Win32 的 c++ 程序,将程序集写入笔式驱动器的引导扇区。当计算机从笔式驱动器启动时,它会成功执行代码 - 看看这里 C++ Program to write to the boot sector of a USB Pendrive

该程序是几行代码,应该在配置了 Windows 编译的编译器上编译 - 例如 Visual Studio 编译器 - 任何可用版本。