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


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

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

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

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

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

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

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

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

可运行的例子

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

一台搭载UEFI BIOS 1.16固件的x86联想Thinkpad T430笔记本电脑 基于arm的树莓派3

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

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

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

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

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

将图像刻录到u盘(会破坏你的数据!): Sudo dd if=main。img = / dev /有关 插上电脑的USB接口 打开它 告诉它从USB引导。 这意味着固件选择USB而不是硬盘。 如果这不是你的机器的默认行为,在开机后继续按Enter, F12, ESC或其他类似的奇怪键,直到你看到一个可以选择从USB启动的启动菜单。 通常可以在这些菜单中配置搜索顺序。

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

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

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

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

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

引导扇区

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

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

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

结果:

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

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

主要。Img包含以下内容:

\364 in octal == 0xf4 in hex: the encoding for a hlt instruction, which tells the CPU to stop working. Therefore our program will not do anything: only start and stop. We use octal because \x hex numbers are not specified by POSIX. We could obtain this encoding easily with: echo hlt > a.S as -o a.o a.S objdump -S a.o which outputs: a.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <.text>: 0: f4 hlt but it is also documented in the Intel manual of course. %509s produce 509 spaces. Needed to fill in the file until byte 510. \125\252 in octal == 0x55 followed by 0xaa. These are 2 required magic bytes which must be bytes 511 and 512. The BIOS goes through all our disks looking for bootable ones, and it only considers bootable those that have those two magic bytes. If not present, the hardware will not treat this as a bootable disk.

如果您不是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?有几个选择:

ask the firmware, e.g. BIOS or UEFI, to do it for us VGA: special memory region that gets printed to the screen if written to. Can be used in Protected Mode. write a driver and talk directly to the display hardware. This is the "proper" way to do it: more powerful, but more complex. serial port. This is a very simple standardized protocol that sends and receives characters from a host terminal. On desktops, it looks like this: Source. It is unfortunately not exposed on most modern laptops, but is the common way to go for development boards, see the ARM examples below. This is really a shame, since such interfaces are really useful to debug the Linux kernel for example. use debug features of chips. ARM calls theirs semihosting for example. On real hardware, it requires some extra hardware and software support, but on emulators it can be a free convenient option. Example.

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

结果:

关于T430:

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

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

.code16:告诉GAS输出16位代码 Cli:关闭软件中断。这些可以使处理器在hlt后重新开始运行 int $0x10:进行BIOS调用。这是一个接一个打印字符的方法。

重要的链接标志是:

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

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

更酷的x86裸金属程序

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

多核:多核汇编语言是什么样子的? 分页:x86分页如何工作?

使用C来代替汇编

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

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

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

另外:

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

run

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这样成熟的操作系统,有以下几种可能性:

Write your own. It's just a bunch of headers and C files in the end, right? Right?? Newlib Detailed example at: https://electronics.stackexchange.com/questions/223929/c-standard-libraries-on-bare-metal/223931 Newlib implements all the boring non-OS specific things for you, e.g. memcmp, memcpy, etc. Then, it provides some stubs for you to implement the syscalls that you need yourself. For example, we can implement exit() on ARM through semihosting with: void _exit(int status) { __asm__ __volatile__ ("mov r0, #0x18; ldr r1, =#0x20026; svc 0x00123456"); } as shown at in this example. For example, you could redirect printf to the UART or ARM systems, or implement exit() with semihosting. embedded operating systems like FreeRTOS and Zephyr. Such operating systems typically allow you to turn off pre-emptive scheduling, therefore giving you full control over the runtime of the program. They can be seen as a sort of pre-implemented 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就能够在常规文件系统中找到它。

这是大多数发行版所做的,将操作系统映像放在/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表示主。Img是一个连接到硬件的硬盘,hda是第一个要尝试的,并且使用它。 加载前512字节到RAM内存地址0x7c00,将CPU的RIP放在那里,并让它运行 在显示屏上显示像启动菜单或BIOS打印调用之类的东西

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

有人认为固件与操作系统没有区别,固件是唯一“真正的”裸金属编程。

正如这位CoreOS开发者所言:

The hard part 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. 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.

后BIOS初始状态

与硬件中的许多东西一样,标准化是很薄弱的,您不应该依赖的东西之一是当您的代码在BIOS之后开始运行时寄存器的初始状态。

所以,帮你自己一个忙,使用一些初始化代码,如:https://stackoverflow.com/a/32509555/895245

像%ds和%es这样的寄存器有重要的副作用,所以即使没有显式地使用它们,也应该将它们归零。

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

该Torito

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

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

ARM

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

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

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

我已上传:

a few simple QEMU C + Newlib and raw assembly examples here on GitHub. The prompt.c example for example takes input from your host terminal and gives back output all through the simulated 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 See also: How to make bare metal ARM programs and run them on QEMU? a fully automated Raspberry Pi blinker setup at: https://github.com/cirosantilli/raspberry-pi-bare-metal-blinker See also: How to run a C program with no OS on the Raspberry Pi? To "see" the LEDs on QEMU you have to compile QEMU from source with a debug flag: https://raspberrypi.stackexchange.com/questions/56373/is-it-possible-to-get-the-state-of-the-leds-and-gpios-in-a-qemu-emulation-like-t Next, you should try a UART hello world. You can start from the blinker example, and replace the kernel with this one: https://github.com/dwelch67/raspberrypi/tree/bce377230c2cdd8ff1e40919fdedbc2533ef5a00/uart01 First get the UART working with Raspbian as I've explained at: https://raspberrypi.stackexchange.com/questions/38/prepare-for-ssh-without-a-screen/54394#54394 It will look something like this: Make sure to use the right pins, or else you can burn your UART to USB converter, I've done it twice already by short circuiting ground and 5V... Finally connect to the serial from the host with: screen /dev/ttyUSB0 115200 For the Raspberry Pi, we use a Micro SD card instead of an USB stick to contain our executable, for which you normally need an adapter to connect to your computer: Don't forget to unlock the SD adapter as shown at: 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 looks like the most popular bare metal Raspberry Pi tutorial available today.

与x86的一些区别包括:

IO是通过直接写入魔法地址来完成的,没有in和out指令。 这被称为内存映射IO。 对于一些真正的硬件,比如树莓派,你可以自己将固件(BIOS)添加到磁盘镜像中。 这是一件好事,因为它使固件更新更加透明。

资源

http://wiki.osdev.org是一个很好的来源。 https://github.com/scanlime/metalkit是一个更加自动化/通用的裸金属编译系统,它提供了一个微小的自定义API

以操作系统为灵感

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

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

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

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

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

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

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

引导加载程序作为灵感

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

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

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

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

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

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

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

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

以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:

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

也可能是另一个。

系统启动前的攻击

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

不同的启动方式

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

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

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

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