如何在不运行操作系统的情况下独自运行程序?您是否可以创建计算机可以在启动时加载并运行的程序集,例如从闪存驱动器启动计算机并运行CPU上的程序?
如何在不运行操作系统的情况下独自运行程序?您是否可以创建计算机可以在启动时加载并运行的程序集,例如从闪存驱动器启动计算机并运行CPU上的程序?
Answers:
如何在不运行操作系统的情况下独自运行程序?
您将二进制代码放置在重新引导后处理器要查找的位置(例如,ARM上的地址0)。
您是否可以创建计算机可以在启动时加载并运行的程序集(例如,从闪存驱动器启动计算机并运行该驱动器上的程序)?
这个问题的一般答案:可以做到。它通常被称为“裸机编程”。要从闪存驱动器中读取数据,您想知道什么是USB,并且您需要一些驱动程序来使用该USB。该驱动器上的程序还必须采用某种特定格式,在某种特定文件系统上……这是引导加载程序通常会执行的操作,但是您的程序可以包括自己的引导加载程序,因此它是自包含的(如果仅固件)加载一小段代码。
许多ARM板使您可以执行其中的某些操作。有些具有引导加载程序,可以帮助您进行基本设置。
在这里,您可以找到有关如何在Raspberry Pi上执行基本操作系统的出色教程。
编辑:本文以及整个wiki.osdev.org将回答您的大多数问题 http://wiki.osdev.org/Introduction
另外,如果您不想直接在硬件上进行实验,则可以使用qemu等虚拟机管理程序将其作为虚拟机运行。在此处了解如何直接在虚拟化ARM硬件上运行“ hello world” 。
可运行的例子
让我们创建并运行一些没有OS的微型裸机hello world程序:
我们还将尽可能在QEMU仿真器上对其进行尝试,因为这样做更安全,更方便开发。QEMU测试已在具有预打包QEMU 2.11.1的Ubuntu 18.04主机上进行。
GitHub存储库中提供了以下所有x86示例以及更多示例的代码。
如何在x86真实硬件上运行示例
请记住,在真实的硬件上运行示例可能很危险,例如,您可能会误擦磁盘或对硬件进行砌块:仅在不包含关键数据的旧机器上这样做!甚至更好的是,使用便宜的半一次性开发板,例如Raspberry Pi,请参见下面的ARM示例。
对于典型的x86笔记本电脑,您必须执行以下操作:
将图像刻录到USB记忆棒(会破坏您的数据!):
sudo dd if=main.img of=/dev/sdX
将USB插入计算机
打开它
告诉它从USB启动。
这意味着使固件在硬盘之前选择USB。
如果这不是计算机的默认行为,请在开机后继续按Enter,F12,ESC或其他类似的怪异键,直到获得启动菜单,您可以在其中选择从USB引导。
通常可以在那些菜单中配置搜索顺序。
例如,在我的T430上,我看到以下内容。
打开电源后,这是我必须按Enter进入启动菜单的时间:
然后,在这里我必须按F12键选择USB作为引导设备:
从那里,我可以选择USB作为引导设备,如下所示:
另外,要更改启动顺序并选择优先级更高的USB,这样我就不必每次都手动选择它,我将在“启动中断菜单”屏幕上按F1,然后导航至:
引导区
在x86上,您可以做的最简单和最低级别的操作是创建一个Master Boot Sector(MBR)(这是一种启动扇区),然后将其安装到磁盘上。
在这里,我们通过一个printf
调用创建一个:
printf '\364%509s\125\252' > main.img
sudo apt-get install qemu-system-x86
qemu-system-x86_64 -hda main.img
结果:
请注意,即使不执行任何操作,屏幕上也已经打印了几个字符。这些由固件打印,并用于识别系统。
在T430上,我们只是得到一个空白屏幕,光标闪烁:
main.img
包含以下内容:
\364
八进制== 0xf4
十六进制:hlt
指令的编码,告诉CPU停止工作。
因此,我们的程序不会执行任何操作:仅启动和停止。
我们使用八进制,因为\x
POSIX未指定十六进制数。
我们可以通过以下方式轻松获得此编码:
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
以八进制== 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设置为第一个字节以开始执行它们。
你好世界引导部门
现在我们已经制作了一个最小的程序,让我们进入一个问候世界。
显而易见的问题是:如何做IO?一些选择:
要求固件(例如BIOS或UEFI)为我们做
VGA:特殊的内存区域,如果写入,则会打印到屏幕上。可以在保护模式下使用。
编写驱动程序并直接与显示硬件对话。这是做到这一点的“正确”方法:功能更强大,但更复杂。
串口。这是一个非常简单的标准化协议,可以从主机终端发送和接收字符。
在台式机上,它看起来像这样:
来源。
不幸的是,它没有在大多数现代笔记本电脑上公开,但是是开发板的常用方法,请参见下面的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"
链接
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上:
经过测试:Lenovo Thinkpad T430,UEFI BIOS 1.16。在Ubuntu 18.04主机上生成的磁盘。
除了标准的Userland组装说明,我们还有:
.code16
:告诉GAS输出16位代码
cli
:禁用软件中断。这些可能会使处理器在hlt
int $0x10
:执行BIOS调用。这就是逐个打印字符的过程。
重要的链接标志是:
--oformat binary
:输出原始二进制汇编代码,不要像常规用户级可执行文件那样将其包装在ELF文件中。为了更好地了解链接描述文件,请熟悉链接的重定位步骤:链接描述是做什么的?
较酷的x86裸机程序
这是我已经完成的一些更复杂的裸机设置:
使用C代替汇编
简介:使用GRUB multiboot,它将解决许多您从未想到的烦人的问题。请参阅以下部分。
x86上的主要困难是BIOS仅将磁盘中的512个字节加载到内存中,使用C时您很可能会炸破这512个字节!
为了解决这个问题,我们可以使用两阶段的bootloader。这将导致进一步的BIOS调用,从而将更多的字节从磁盘加载到内存中。这是使用int 0x13 BIOS调用从头开始的最小的第2阶段组装示例:
或者:
-kernel
选项,该选项会将整个ELF文件加载到内存中。这是我使用该方法创建的一个ARM示例。kernel7.img
,这与QEMU -kernel
一样。仅出于教育目的,这是一个单阶段的最低C示例:
main.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
链接器
ENTRY(mystart)
SECTIONS
{
. = 0x7c00;
.text : {
entry.o(.text)
*(.text)
*(.data)
*(.rodata)
__bss_start = .;
/* COMMON vs BSS: /programming/16835716/bss-vs-common-what-goes-where */
*(.bss)
*(COMMON)
__bss_end = .;
}
/* /programming/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内核,该内核通过POSIX实现了许多C标准库功能。
在没有使用像Linux这样的成熟OS的情况下,可能的几种方法包括:
自己写。最后只是一堆标题和C文件,对吗?对??
有关详细示例,请访问:https://electronics.stackexchange.com/questions/223929/c-standard-libraries-on-bare-metal/223931
Newlib实现了所有为你枯燥的非OS具体的事情,比如memcmp
,memcpy
等
然后,它为您提供了一些存根,以实现您需要的系统调用。
例如,我们可以exit()
通过半主机在ARM上实现:
void _exit(int status) {
__asm__ __volatile__ ("mov r0, #0x18; ldr r1, =#0x20026; svc 0x00123456");
}
如本例所示。
例如,您可以重定向printf
到UART或ARM系统,或exit()
使用semihosting实现。
此类操作系统通常允许您关闭抢先式调度,因此可以完全控制程序的运行时间。
可以将它们视为一种预先实现的Newlib。
GNU GRUB多重启动
引导扇区很简单,但是它们并不十分方便:
出于这些原因,GNU GRUB创建了一种更方便的文件格式,称为multiboot。
最小的工作示例:https : //github.com/cirosantilli/x86-bare-metal-examples/tree/d217b180be4220a0b4a453f31275d38e697a99e0/multiboot/hello-world
我还在GitHub示例存储库中使用了它,从而能够轻松地在真实硬件上运行所有示例,而无需烧掉USB一百万次。
QEMU结果:
T430:
如果您将操作系统准备为多重引导文件,则GRUB可以在常规文件系统中找到它。
这是大多数发行版所做的,将OS映像置于下/boot
。
基本上,多重引导文件是带有特殊标头的ELF文件。GRUB在以下位置指定了它们:https : //www.gnu.org/software/grub/manual/multiboot/multiboot.html
您可以使用将多引导文件转换为可引导磁盘grub-mkrescue
。
固件
实际上,引导扇区并不是在系统CPU上运行的第一个软件。
实际上首先运行的是所谓的固件,它是一种软件:
众所周知的固件包括:
固件执行以下操作:
遍历每个硬盘,USB,网络等,直到找到可启动的设备。
当我们运行QEMU时,它-hda
说main.img
是连接到硬件的硬盘,并且hda
是第一个尝试使用的硬盘。
将前512个字节加载到RAM内存地址0x7c00
,将CPU的RIP放在此处,然后运行
在显示屏上显示启动菜单或BIOS打印呼叫之类的内容
固件提供了大多数OS所依赖的类似于OS的功能。例如,已将Python子集移植到可在BIOS / UEFI上运行:https : //www.youtube.com/watch?v=bYQ_lq5dcvM
可以说固件与操作系统是无法区分的,并且固件是唯一可以做到的“真正的”裸机编程。
正如这个CoreOS开发人员所说:
困难的部分
在为PC通电时,构成芯片组的芯片(北桥,南桥和SuperIO)尚未正确初始化。即使将BIOS ROM尽可能远地从CPU移开,CPU也可以访问BIOS ROM,因为必须这样做,否则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
(示例)完成,也可以由Linux内核make isoimage
使用来完成isohybrid
。
臂
在ARM中,总体思路是相同的。
我们没有可用于IO的广泛使用的半标准化预安装固件(例如BIOS)供我们使用,因此我们可以做的两种最简单的IO类型是:
我上传了:
GitHub上的一些简单QEMU C + Newlib和原始程序集示例。
该prompt.c例子例如从主机终端接受输入,并给出全部通过模拟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
完整的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-像q一样的qemu中的led和gpios
接下来,您应该尝试一个UART hello世界。您可以从眨眼示例开始,然后用以下示例替换内核: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)添加到磁盘映像中。
这是一件好事,因为它使更新固件更加透明。
资源资源
操作系统也是一个程序,因此我们也可以通过从头开始创建或更改(限制或添加)小型操作系统的功能来创建自己的程序,然后在引导过程中运行它(使用ISO映像)。 。
例如,此页面可用作起点:
在这里,整个操作系统完全适合512字节的引导扇区(MBR)!
可以使用此类或类似的简单OS 创建一个简单的框架,该框架可以使我们:
使引导加载程序将磁盘上的后续扇区加载到RAM中,并跳转到该点以继续执行。或者,您可以阅读FAT12(软盘驱动器上使用的文件系统)并实现该文件系统。
但是,有很多可能性。例如,要查看更大的x86汇编语言操作系统,我们可以探索MykeOS,x86操作系统,这是一个学习工具,可通过简单注释的代码和大量文档展示简单的16位,实模式操作系统。
在没有操作系统的情况下运行的其他常见程序类型也是Boot Loader。我们可以使用以下站点来创建一个受这种概念启发的程序:
上面的文章还介绍了此类程序的基本架构:
- 通过0000:7C00地址正确加载到存储器。
- 调用以高级语言开发的BootMain函数。
- 在显示屏上显示“低级”,“您好,世界……”消息。
如我们所见,该体系结构非常灵活,可以使我们实现任何程序,而不必是引导加载程序。
特别是,它显示了如何使用“混合代码”技术,从而可以将高级构造(来自C或C ++)与低级命令(来自Assembler)结合起来。这是一种非常有用的方法,但是我们必须记住:
要构建程序并获取可执行文件,您将需要16位模式的Assembler的编译器和链接器。对于C / C ++,您只需要可以为16位模式创建目标文件的编译器。
本文还显示了如何查看正在运行的程序,以及如何执行其测试和调试。
上面的示例使用了将扇区MBR加载到数据介质上的事实。但是,例如通过UEFI应用程序,我们可以更深入地研究:
除了加载OS,UEFI还可以运行UEFI应用程序,这些应用程序作为文件驻留在EFI系统分区上。它们可以从UEFI命令外壳,固件的启动管理器或其他UEFI应用程序执行。UEFI应用程序可以独立于系统制造商开发和安装。
UEFI应用程序的一种类型是OS加载程序,例如GRUB,rEFInd,Gummiboot和Windows Boot Manager。它将OS文件加载到内存中并执行它。而且,OS加载程序可以提供用户界面,以允许选择另一个要运行的UEFI应用程序。UEFI Shell之类的实用程序也是UEFI应用程序。
如果我们想开始创建这样的程序,例如,我们可以从以下网站开始:
EFI编程:创建“ Hello,World”程序 / UEFI编程-第一步
众所周知,在操作系统启动之前,有一整组恶意软件(程序)正在运行。
就像以上所有解决方案一样,它们中的很大一部分都在MBR扇区或UEFI应用程序上运行,但是也有一些使用其他入口点的应用程序,例如卷启动记录(VBR)或BIOS:
至少有四种已知的BIOS攻击病毒,其中两种用于演示目的。
也许也可以。
Bootkit已从概念验证开发演变为大规模发行,现已有效地成为开源软件。
我还认为,在这种情况下,还值得一提的是,有各种形式的引导操作系统(或用于此目的的可执行程序)引导。有很多,但是我要特别注意使用“网络启动”选项(PXE)从网络加载代码,该选项使我们可以在计算机上运行程序,而无需考虑其操作系统,甚至不考虑任何存储介质。直接连接到计算机: