今天,我想向你展示开始编写自己的操作系统是多么容易。我们从引导加载程序开始。当典型的笔记本电脑启动时,它会从硬盘加载第一个扇区并执行它。这只是 512 个字节,因此您需要将一个例程塞入其中,以加载更多扇区并执行它们。
但是,让我们从小处着手,让我们从一个只输出“HELLO”的引导扇区开始。以下内容已在 Ubuntu Linux 22.04 上进行了测试。
linux程序设计与安全开发
-
恶意软件开发
-
-
-
windows网络安全防火墙与虚拟网卡(更新完成)
-
-
windows文件过滤(更新完成)
-
-
USB过滤(更新完成)
-
-
游戏安全(更新中)
-
-
ios逆向
-
-
windbg
-
-
还有很多免费教程(限学员)
-
-
-
更多详细内容添加作者微信
-
-
我们从文件开始
hello-1.s:
start:
; this should print H
mov ax, 0xe48
int 0x10
; E
mov ax, 0xe45
int 0x10
; L
mov ax, 0xe4C
int 0x10
; L
mov ax, 0xe4C
int 0x10
; O
mov ax, 0xe4F
int 0x10
.end:
jmp .end
编译它:
nasm -f bin hello-1.s
你得到的是一个 27 字节的机器代码文件 hello-1,它只是转换为机器代码的汇编程序。让我们用 hexdump 看一下:
$ hexdump -C hello-1
00000000 b8 48 0e bb 07 00 cd 10 b8 45 0e cd 10 b8 4c 0e |.H.......E....L.|
00000010 cd 10 b8 4c 0e cd 10 b8 4f 0e cd 10 eb fe |...L....O.....|
很漂亮,不是吗?在左侧,您可以找到十六进制表示法的字节。在右侧,您可以找到写入的字节(如果可写),否则为点。例如,48(十六进制)= 78 是 ASCII 字符“H”,依此类推。
使用 objdump 进行反汇编
下一步:让我们验证代码是否已从汇编程序正确转换为机器语言 — 让我们使用命令 objdump 再次反汇编文件 hello。请注意,当您的典型笔记本电脑启动时,它将处于 16 位模式,即英特尔 8086 处理器模式,因此我们使用以下命令:
$ objdump -b binary -D hello-1 -mi8086
hello: file format binary
Disassembly of section .data:
00000000 <.data>:
0: b8 48 0e mov $0xe48,%ax
3: cd 10 int $0x10
5: b8 45 0e mov $0xe45,%ax
8: cd 10 int $0x10
a: b8 4c 0e mov $0xe4c,%ax
d: cd 10 int $0x10
f: b8 4c 0e mov $0xe4c,%ax
12: cd 10 int $0x10
14: b8 4f 0e mov $0xe4f,%ax
17: cd 10 int $0x10
19: eb fe jmp 0x19
看起来不错,跳转到 .end 标签已被相对跳转到 -1(有符号十六进制表示法中的 fe)所取代。
创建虚拟引导磁盘
下一步是创建名为 bootdisk.img 的虚拟 bootdisk 映像。我们从 1 兆字节的空文件开始:
$ dd if=/dev/zero of=bootdisk.img seek=2K count=1 bs=512
现在将二进制文件 hello-1 放在虚拟引导磁盘的开头,而不截断文件:
$ dd if=hello-1 of=bootdisk.img conv=notrunc
使用十六进制编辑器
现在将魔术签名字节 55 AA 放在引导扇区的末尾。调用十六进制编辑器,例如 okteta:
$ okteta bootdisk.img
并将字节 511 和 512 设置为 55 AA:
保存并退出 okteta。模拟计算机以从虚拟硬盘启动:
qemu-system-i386 bootdisk.img
结果如下:
寄存器 AX、AH 和 AL
它比这更容易。hello 有 27 个字节,我们可以减少它。
了解寄存器 AX 由寄存器 AL(A Lower)和 AH(A Higher)组成,其方式为 AX=256*AH+AL。因此,AX 的高值字节是 AH,低值字节是 AL。通过将 AX 设置为 0xe48,我们设置了有效的 AH=0xe 和 AL=ox48。AH=0xe 触发 INT 0x10 输出 AL 的值。 AH 永远不会改变,只需要设置一次。只是 AL 需要为 HELLO 的每个字符进行更改,因此,这是更简单的版本:
hello-2.s:
start:
; this should print H
mov ax, 0xe48
int 0x10
; E
mov al, 0x45
int 0x10
; L
mov al, 0x4C
int 0x10
; L
mov al, 0x4C
int 0x10
; O
mov al, 0x4F
int 0x10
.end:
jmp .end
而现在,在用通常的 nasm -f bin hello.s 编译后,hello 只有 23 个字节。
恭喜,这是我们在汇编语言中的第一次优化。
分段冲突
我们可以在 Linux 下运行这个程序吗?不可以,因为 Linux 具有适当的安全手段,可以防止执行 BIOS 中断,例如 int 0x10。看看会发生什么:
$ nasm -felf64 hello-2.s
$ ld -o hello-2 hello-2.o
ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000
$ ./hello-2
Segmentation fault (core dumped)
“分段错误”或“分段冲突”是指“您的程序位于内存段中,该段没有特权执行此类调用。
高架特权
现在我们已经了解到,在 i8086 模式下作为 bootsector/kernel/运行时,我们已经提升了权限,我们可以使用它来直接与设备交互。当然,现代操作系统不允许您将数据发送到打印机 - 打印机可能已经在打印,打印作业必须由操作系统协调。但作为内核,我们可以做到这一点。为了演示它,让我们听听键盘。要对设备执行 I/O,您需要知道其端口号。对于键盘,其端口为 60(十六进制)。让我们只读取传入的字节,并在它们发生变化时打印。为此,我们使用
hello-3.s:
start:
; this should print H
mov ax, 0xe48
int 0x10
; E
mov al, 0x45
int 0x10
; L
mov al, 0x4C
int 0x10
; L
mov al, 0x4C
int 0x10
; O
mov al, 0x4F
int 0x10
.end:
mov bl,al ;store the old value from the keyboard into bl
in al,0x60 ;read what's coming from the keyboard into al
cmp bl,al ;compare the old and the new value
je .end ;if the old and the new value are equal, jump to .end
int 0x10 ;print what has come from the keyboard, al
jmp .end
编译,构建到引导盘中,并使用以下命令模拟引导:
nasm -f bin hello-3.s &&
dd if=hello-3 of=bootdisk.img conv=notrunc &&
qemu-system-i386 bootdisk.img
当我输入“hello”时,它看起来像这样:
对于我键入的每个键,都有两个字节。一个是按键时,另一个是松开键时。这样,计算机就可以理解我何时按下shift键以及何时松开它。实际字节与我按下的键不匹配,相反,它们代表键盘的扫描码——键所在的位置。
例如,英语和德语键盘混淆了“Y”和“Z”键(以及其他一些键)。但是,键盘处理器的工作方式相同,它只知道“按下 20 键”左右。它不知道此键是标记为 Y 还是 Z。这就是为什么你总是必须告诉你的操作系统你的键盘布局。呵呵,多亏了对汇编程序和机器语言的关心,你学到了一些东西!!
你学到了什么
-
在汇编程序中编写“Hello World”程序
-
将其编译为机器语言
-
使用 hexdump 逐字节查看其内容
-
使用 objdump 反汇编它
-
汇编语言和机器语言的区别。在我们的示例中,jmp .end 被转换为 eb (jump) fe (-1)
-
使用 DD 创建虚拟 Bootdisk
-
使用 conv=notrunc 插入文件
-
使用 HexEditor Okteta 修改二进制文件
-
靴子的神奇数字 55AA(十六进制)
-
使用 QEMU 模拟计算机启动
-
寄存器 AX、AH 和 AL 的关系
-
有关内存分段的引导过程的特权
-
通过端口 60(十六进制)访问键盘
-
为什么你必须告诉你的操作系统你的键盘布局
原文始发于微信公众号(安全狗的自我修养):编写自己的引导加载程序
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论