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

How to run a program without an operating system?

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


可运行的例子

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

  • 带有UEFI BIOS 1.16固件的x86联想Thinkpad T430笔记本电脑
  • 基于ARM的Raspberry Pi 3
  • 我们也将尽可能在QEMU仿真器上试用它们,因为这样更安全,更便于开发。 QEMU测试已经在预先打包的QEMU 2.11.1的Ubuntu 18.04主机上进行。

    以下所有x86示例的代码以及更多内容都存在于此GitHub存储库中。

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

    请记住,在真实硬件上运行示例可能很危险,例如您可能会错误地擦除磁盘或硬件:只在不包含关键数据的旧机器上执行此操作!或者甚至更好,使用廉价的半一次性设备,如Raspberry Pi,请参阅下面的ARM示例。

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

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

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

  • 打开它

  • 告诉它从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调用创建一个:

    1
    2
    3
    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 octal == 0xf4 in hex:hlt指令的编码,告诉CPU停止工作。

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

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

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

    1
    2
    3
    echo hlt > a.S
    as -o a.o a.S
    objdump -S a.o

    哪个输出:

    1
    2
    3
    4
    5
    6
    7
    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的内容:

    1
    hd main.img

    显示预期:

    1
    2
    3
    4
    5
    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世界。

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

  • 询问固件,例如BIOS或UEFI,为我们做
  • VGA:特殊内存区域,如果写入则会打印到屏幕上。可以在保护模式下使用。
  • 编写驱动程序并直接与显示硬件对话。这是实现它的"正确"方式:更强大,但更复杂。
  • 串行端口。这是一种非常简单的标准化协议,可以从主机终端发送和接收字符。

    在桌面上,它看起来像这样:

    enter image description here

    资源。

    遗憾的是,大多数现代笔记本电脑都没有曝光,但这是开发板的常用方法,请参阅下面的ARM示例。

    这真是一个耻辱,因为这样的接口对调试Linux内核非常有用。

  • 使用芯片的调试功能。例如,ARM称他们为半主机。在真正的硬件上,它需要一些额外的硬件和软件支持,但在仿真器上它可以是一个免费的方便选项。例。

  • 这里我们将做一个BIOS示例,因为它在x86上更简单。但请注意,它不是最强大的方法。

    电源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    .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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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)
        }
    }

    组装和链接:

    1
    2
    3
    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裸机程序

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

  • multicore:多核汇编语言是什么样的?
  • 分页:x86分页如何工作?
  • 使用C而不是汇编

    总结:使用GRUB multiboot,这将解决你从未想过的很多恼人的问题。请参阅以下部分。

    x86的主要难点在于BIOS只从磁盘加载512个字节到内存,使用C时你可能会炸掉512个字节!

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

    或者:

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

    main.c中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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");
        };
    }

    entry.S中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    .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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    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 = .;
    }

    1
    2
    3
    4
    5
    6
    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那样成熟的操作系统,包括:

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

  • Newlib

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

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

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

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

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

    如本例所示。

    例如,您可以将printf重定向到UART或ARM系统,或者使用半主机实现exit()

  • 嵌入式操作系统,如FreeRTOS和Zephyr。

    此类操作系统通常允许您关闭先发制人的调度,从而使您可以完全控制程序的运行时间。

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

  • GNU GRUB Multiboot

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

  • 每个磁盘只能有一个操作系统
  • 加载代码必须非常小并且适合512个字节
  • 你必须自己做很多启动,比如进入保护模式
  • 出于这些原因,GNU GRUB创建了一种称为multiboot的更方便的文件格式。

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

    我还在我的GitHub示例repo上使用它,以便能够轻松地在真实硬件上运行所有示例而无需烧录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时,-hda表示main.img是连接到硬件的硬盘,hda是第一个要尝试的硬盘,并且使用它。

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

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

  • 固件提供类似OS的功能,大多数操作系统都依赖于此功能。例如。已将一个Python子集移植到BIOS / UEFI上运行:https://www.youtube.com/watch?v = bYQ_lq5dcvM

    可以说固件与操作系统无法区分,固件是唯一可以做到的"真正的"裸机编程。

    正如这个CoreOS开发者所说:

    The hard part

    Ok.

    When you power up a PC, the chips that make up the chipset (northbridge, southbridge and SuperIO) are not yet initialized properly. Even though the BIOS ROM is as far removed from the CPU as it could be, this is accessible by the CPU, because it has to be, otherwise the CPU would have no instructions to execute. This does not mean that BIOS ROM is completely mapped, usually not. But just enough is mapped to get the boot process going. Any other devices, just forget it.

    Ok.

    When you run Coreboot under QEMU, you can experiment with the higher layers of Coreboot and with payloads, but QEMU offers little opportunity to experiment with the low level startup code. For one thing, RAM just works right from the start.

    Ok.

    发布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(示例)完成,也可以由make isoimage上的Linux内核使用isohybrid完成。

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

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

  • serial,在开发板上广泛使用
  • 闪烁LED
  • 我上传了:

  • GitHub上的一些简单的QEMU C + Newlib和原始汇编示例。

    例如,prompt.c示例从主机终端获取输入,并通过模拟的UART返回输出:

    1
    2
    3
    4
    5
    6
    7
    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

    另请参阅:如何制作裸机ARM程序并在QEMU上运行它们?

  • 完全自动化的Raspberry Pi闪光灯设置:https://github.com/cirosantilli/raspberry-pi-bare-metal-blinker

    enter image description here

    另请参阅:如何在Raspberry Pi上运行没有操作系统的C程序?

    要"查看"QEMU上的LED,您必须使用调试标志从源代码编译QEMU:https://raspberrypi.stackexchange.com/questions/56373/is-it-possible-to-get-the-state-of-该基LED和 - 的GPIO-IN-A-QEMU仿真状叔

    接下来,您应该尝试UART hello world。您可以从blinker示例开始,并用以下内容替换内核:https://github.com/dwelch67/raspberrypi/tree/bce377230c2cdd8ff1e40919fdedbc2533ef5a00/uart01

    首先让UART与Raspbian一起工??作,如我所解释的那样:https://raspberrypi.stackexchange.com/questions/38/prepare-for-ssh-without-a-screen/54394#54394它看起来像这样:

    enter image description here

    确保使用正确的引脚,否则你可以将你的UART刻录到USB转换器,我已经通过短路接地和5V完成了两次......

    最后从主机连接到串口:

    1
    screen /dev/ttyUSB0 115200

    对于Raspberry Pi,我们使用Micro SD卡而不是USB记忆棒来包含我们的可执行文件,您通常需要一个适配器才能连接到您的计算机:

    enter image description here

    不要忘记解锁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是通过直接写入魔术地址完成的,没有inout指令。

    这称为内存映射IO。

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

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

  • 资源

  • http://wiki.osdev.org是这些问题的重要来源。
  • https://github.com/scanlime/metalkit是一个更自动化/通用的裸机编译系统,它提供了一个微小的自定义API
  • 好。


    How do you run a program all by itself without an operating system running?

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

    Can you create assembly programs that the computer can load and run at startup ( e.g. boot the computer from a flash drive and it runs the program that is on the drive)?

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

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

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

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

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


    操作系统为灵感

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

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

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

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

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

    make the bootloader load subsequent sectors on the disk into RAM, and jump to that point to continue execution. Or you could read up on FAT12, the filesystem used on floppy drives, and implement that.

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

    Boot Loader作为灵感来源

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

    如何开发自己的Boot Loader

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

  • Correct loading to the memory by 0000:7C00 address.
  • Calling the BootMain function that is developed in the high-level language.
  • Show""Hello, world…", from low-level" message on the display.
  • 我们可以看到,这种架构非常灵活,允许我们实现任何程序,而不一定是引导加载程序。

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

    to build the program and obtain executable file you will need the compiler and linker of Assembler for 16-bit mode. For C/C++ you will need only the compiler that can create object files for 16-bit mode.

    本文还介绍了如何查看已创建的程序以及如何执行其测试和调试。

    UEFI应用程序作为灵感

    上面的例子使用了在数据介质上加载扇区MBR的事实。但是,我们可以通过使用UEFI应用程序进行更深入的深入研究:

    Beyond loading an OS, UEFI can run UEFI applications, which reside as files on the EFI System Partition. They can be executed from the UEFI command shell, by the firmware's boot manager, or by other UEFI applications. UEFI applications can be developed and installed independently of the system manufacturer.

    A type of UEFI application is an OS loader such as GRUB, rEFInd, Gummiboot, and Windows Boot Manager; which loads an OS file into memory and executes it. Also, an OS loader can provide a user interface to allow the selection of another UEFI application to run. Utilities like the UEFI shell are also UEFI applications.

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

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

    探索安全问题作为灵感

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

    其中很大一部分运行在MBR扇区或UEFI应用程序上,就像上述所有解决方案一样,但也有一些使用其他入口点,如卷引导记录(VBR)或BIOS:

    There are at least four known BIOS attack viruses, two of which were for demonstration purposes.

    或者也许是另一个。

    系统启动前的攻击

    Bootkits have evolved from Proof-of-Concept development to mass distribution and have now effectively become open-source software.

    不同的启动方式

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

    什么是网络引导(PXE)以及如何使用它?