How to run a program without an operating system?
如何在没有运行操作系统的情况下自行运行程序?
您是否可以创建计算机可以在启动时加载和运行的汇编程序,例如 从闪存驱动器启动计算机,它运行CPU上的程序?
可运行的例子
让我们创建并运行一些在没有操作系统的情况下运行的小型裸机hello world程序:
我们也将尽可能在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进入启动菜单:
然后,在这里我必须按F12选择USB作为启动设备:
从那里,我可以选择USB作为启动设备,如下所示:
或者,要更改启动顺序并选择USB具有更高的优先级,所以我不必每次都手动选择它,我会在"启动中断菜单"屏幕上点击F1,然后导航到:
引导扇区
在x86上,您可以做的最简单和最低级别的事情是创建一个主引导扇区(MBR),它是一种引导扇区,然后将其安装到磁盘上。
这里我们用一个
1 2 3 | printf '\364%509s\125\252' > main.img sudo apt-get install qemu-system-x86 qemu-system-x86_64 -hda main.img |
结果:
请注意,即使没有做任何事情,屏幕上也会打印出几个字符。它们由固件打印,用于识别系统。
在T430上我们只得到一个带有闪烁光标的空白屏幕:
因此我们的程序不会做任何事情:只能启动和停止。
我们使用八进制,因为POSIX没有指定
我们可以通过以下方式轻松获得此编
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 |
但它当然也记录在英特尔手册中。
这些是2个必需的魔术字节,必须是字节511和512。
BIOS通过我们所有寻找可启动磁盘的磁盘,它只考虑具有这两个魔术字节的可启动磁盘。
如果不存在,硬件将不会将其视为可引导磁盘。
如果您不是
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 |
其中
BIOS固件从磁盘读取这512个字节,将它们放入内存,并将PC设置为第一个字节以开始执行它们。
你好世界引导部门
现在我们已经制作了一个最小的程序,让我们转移到一个hello世界。
显而易见的问题是:如何做IO?一些选择:
串行端口。这是一种非常简单的标准化协议,可以从主机终端发送和接收字符。
在桌面上,它看起来像这样:
资源。
遗憾的是,大多数现代笔记本电脑都没有曝光,但这是开发板的常用方法,请参阅下面的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 |
结果:
在T430上:
经测试:联想Thinkpad T430,UEFI BIOS 1.16。在Ubuntu 18.04主机上生成的磁盘。
除了标准的用户态组装说明,我们还有:
重要的链接标志是:
为了更好地理解链接器脚本部分,请熟悉链接的重定位步骤:链接器做什么?
酷冷的x86裸机程序
以下是我已经实现的一些更复杂的裸机设置:
使用C而不是汇编
总结:使用GRUB multiboot,这将解决你从未想过的很多恼人的问题。请参阅以下部分。
x86的主要难点在于BIOS只从磁盘加载512个字节到内存,使用C时你可能会炸掉512个字节!
为了解决这个问题,我们可以使用两阶段引导加载程序。这会进一步调用BIOS,从磁盘向内存加载更多字节。这是一个使用int 0x13 BIOS调用从头开始的最小阶段2汇编示例:
或者:
仅出于教育目的,这里是一个最小的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为您实现所有无聊的非操作系统特定的东西,例如
然后,它为您提供了一些存根,以实现您自己需要的系统调用。
例如,我们可以通过半主机在ARM上实现
1 2 3 | void _exit(int status) { __asm__ __volatile__ ("mov r0, #0x18; ldr r1, =#0x20026; svc 0x00123456"); } |
如本例所示。
例如,您可以将
嵌入式操作系统,如FreeRTOS和Zephyr。
此类操作系统通常允许您关闭先发制人的调度,从而使您可以完全控制程序的运行时间。
它们可以被视为一种预先实现的Newlib。
GNU GRUB Multiboot
引导扇区很简单,但它们不是很方便:
出于这些原因,GNU GRUB创建了一种称为multiboot的更方便的文件格式。
最小的工作示例:https://github.com/cirosantilli/x86-bare-metal-examples/tree/d217b180be4220a0b4a453f31275d38e697a99e0/multiboot/hello-world
我还在我的GitHub示例repo上使用它,以便能够轻松地在真实硬件上运行所有示例而无需烧录USB一百万次。
QEMU结果:
T430:
如果您将操作系统准备为多重引导文件,则GRUB可以在常规文件系统中找到它。
这是大多数发行版所做的,将操作系统映像置于
多重启动文件基本上是一个带有特殊标题的ELF文件。它们由GRUB指定:https://www.gnu.org/software/grub/manual/multiboot/multiboot.html
您可以使用
固件
实际上,您的引导扇区不是第一个在系统CPU上运行的软件。
实际上首先运行的是所谓的固件,它是一个软件:
众所周知的固件包括:
固件执行以下操作:
循环遍历每个硬盘,USB,网络等,直到找到可引导的内容。
当我们运行QEMU时,
将前512个字节加载到RAM存储器地址
在显示屏上显示启动菜单或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
像
请注意,某些仿真器比实际硬件更好,并为您提供良好的初始状态。然后,当你在真正的硬件上运行时,一切都会中断。
El Torito
可刻录到CD的格式:https://en.wikipedia.org/wiki/El_Torito_%28CD-ROM_standard%29
还可以生成适用于ISO或USB的混合图像。这可以使用
臂
在ARM中,一般的想法是相同的。
没有广泛可用的半标准化预安装固件(如BIOS)供我们用于IO,因此我们可以做的两种最简单的IO类型是:
我上传了:
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
另请参阅:如何在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它看起来像这样:
确保使用正确的引脚,否则你可以将你的UART刻录到USB转换器,我已经通过短路接地和5V完成了两次......
最后从主机连接到串口:
1 | 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是通过直接写入魔术地址完成的,没有
这称为内存映射IO。
对于某些真正的硬件,比如Raspberry Pi,您可以自己将固件(BIOS)添加到磁盘映像中。
这是一件好事,因为它使更新固件更加透明。
资源
好。
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)以及如何使用它?