好了,让我们编写我们的第一个引导加载程序。为了做到这一点,我们可能需要知道寻址在我们的程序中是如何工作的。
有趣的是,汇编程序不知道程序在内存中的位置。
但是,我们可以在程序代码中使用标签,并通过跳转指令引用它们。在这种情况下,汇编程序将标签替换为指向标记指令位置的地址。
linux程序设计与安全开发
-
恶意软件开发
-
-
-
windows网络安全防火墙与虚拟网卡(更新完成)
-
-
windows文件过滤(更新完成)
-
-
USB过滤(更新完成)
-
-
游戏安全(更新中)
-
-
ios逆向
-
-
windbg
-
-
还有很多免费教程(限学员)
-
-
-
更多详细内容添加作者微信
-
-
为了计算地址,汇编程序使用默认基址并计算从该基址到标签的偏移量。通常,在程序代码的开头将基址设置为 0。
下面是一个名为 main.asm 的示例源代码:
mov eax, 5
mov ebx, 7
jmp sum
add eax, 8
sum:
add eax, ebx
test eax, 12
我们可以使用 nasm 组装它来生成一个平面二进制文件:
nasm -f bin -o main.bin main.asm
我们使用平面二进制来省略程序地址中的链接器重定位。
因此,我们有一个平面二进制文件,我们可以使用以下命令对其进行检查:
objdump -b binary -m i8086 -M intel -D boot.bin
此命令的输出将显示二进制文件的内容:
0: 66 b8 05 00 00 00 mov eax,0x5
6: 66 bb 07 00 00 00 mov ebx,0x7
c: eb 04 jmp 0x12
e: 66 83 c0 08 add eax,0x8
12: 66 01 d8 add eax,ebx
15: 66 a9 0c 00 00 00 test eax,0xc
正如你所看到的,汇编程序为指令的每个操作码分配一个地址。
基址从零开始,并随着操作码大小步长的增加而增加。
因此,当跳转到名为“sum”的标签时,汇编程序将“sum”替换为“sum”标签后的第一条指令所在的地址(即add eax,ebx)。
也可以通过 org 指令更改此基址。
我可以通过org指令轻松更改基址。
例如,如果我们希望我们的基址从 0x100 而不是 0x0 开始,我们只需要在源代码中添加“org 0x100”:
org 0x100
mov eax, 5
mov ebx, 7
jmp sum
add eax, 8
sum:
add eax, ebx
test eax, 12
组装代码并使用对象转储检查代码后,将看到更新的基址:
0: 66 b8 05 00 00 00 mov eax,0x5
6: 66 bb 07 00 00 00 mov ebx,0x7
c: eb 04 jmp 0x12
e: 66 83 c0 08 add eax,0x8
12: 66 01 d8 add eax,ebx
15: 66 a9 0c 00 00 00 test eax,0xc
这有意义吗?我们更改了程序源地址,但操作码保持不变。这就像我们什么都不做,nasm 不尊重组织指令。
这似乎令人困惑,但实际上对此有一个解释。
如果你看一下 objdump 结果,在第 0x0c 行,你可能会看到 “eb” 是跳转的操作码,因此 “04” 与它应该跳转的位置有关。
如果仔细观察,您会注意到“04”是跳转指令和标签地址之间的距离 (0x12–0x0e = 0x04)。
这意味着跳转到标签不涉及跳转到标签的绝对地址,而是通过从跳转到标签的指令到标签的偏移量跳到标签的相对地址。
因此,我们何时使用 org 指令和何时不使用 org 指令之间没有区别。
但是在其他情况下,使用 org 指令确实很重要,我们可以看到区别:
跳远。
org 0x800
mov eax, 9
jmp 0x00:sum
mov ebx, 2
mov eax, 8
sum:
add eax, 1
mov ebx, eax
objdump 结果:
0: 66 b8 09 00 00 00 mov eax,0x9
6: ea 17 08 00 00 jmp 0x0:0x817
b: 66 bb 02 00 00 00 mov ebx,0x2
11: 66 b8 08 00 00 00 mov eax,0x8
17: 66 83 c0 01 add eax,0x1
1b: 66 89 c3 mov ebx,eax
如您所见,org 指令会影响跳转寻址。
另一个例子是:
org 0x100
mov eax, 5
mov ebx, 7
mov al, 0
txt: db "Hello"
mov al, 0
msg: db "world"
mov al, 0
jmp sum
add eax, 8
mov ecx, txt
mov ecx, [txt]
mov ebx, msg
mov ebx, [msg]
sum:
add eax, ebx
test eax, 12
然后组装并检查它:
0: 66 b8 05 00 00 00 mov eax,0x5
6: 66 bb 07 00 00 00 mov ebx,0x7
c: b0 00 mov al,0x0
e: 48 dec ax
f: 65 6c gs ins BYTE PTR es:[di],dx
11: 6c ins BYTE PTR es:[di],dx
12: 6f outs dx,WORD PTR ds:[si]
13: b0 00 mov al,0x0
15: 77 6f ja 0x86
17: 72 6c jb 0x85
19: 64 b0 00 fs mov al,0x0
1c: eb 1a jmp 0x38
1e: 66 83 c0 08 add eax,0x8
22: 66 b9 0e 01 00 00 mov ecx,0x10e
28: 66 8b 0e 0e 01 mov ecx,DWORD PTR ds:0x10e
2d: 66 bb 15 01 00 00 mov ebx,0x115
33: 66 8b 1e 15 01 mov ebx,DWORD PTR ds:0x115
38: 66 01 d8 add eax,ebx
3b: 66 a9 0c 00 00 00 test eax,0xc
这里有一个问题:对象转储将我们的数据假定为指令,因此它将其反汇编为不正确的指令。我们可以忽略:
e: 48 dec ax
f: 65 6c gs ins BYTE PTR es:[di],dx
11: 6c ins BYTE PTR es:[di],dx
12: 6f outs dx,WORD PTR ds:[si]
和
15: 77 6f ja 0x86
17: 72 6c jb 0x85
它们只是数据,而不是指令。
正如您在说明中看到的:
22: 66 b9 0e 01 00 00 mov ecx,0x10e | mov ecx, txt
28: 66 8b 0e 0e 01 mov ecx,DWORD PTR ds:0x10e | mov ecx, [txt]
2d: 66 bb 15 01 00 00 mov ebx,0x115 | mov ebx, msg
33: 66 8b 1e 15 01 mov ebx,DWORD PTR ds:0x115 | mov ebx, [msg]
txt 放在地址“0x0e”上,msg 放在地址“0x15”上。
由于组织0x100,程序的起源发生了变化,0x0e我们有了 mov ecx,而不是 mov ecx,0x10e它是 0x100+e。
注意:在 64 位模式下,有一个名为“rel”的 NASM 指令,可以创建相对地址(相对于 rip)而不是绝对地址。
现在让我们开始我们的代码冒险:
在这个阶段,我们将设计一个简单的引导加载程序。同样重要的是,我们正在创建一个基于 x86 架构的微型操作系统。
x86 CPU 为了向后兼容,以 16 位模式启动,称为实模式。是的,即使是现在,现代 64 位 CPU 也从传统的 16 位实模式开始,然后切换到保护模式,我们稍后会讨论。
注意:
我正在阅读有关 x86 处理器的历史,我意识到即使像 Intel i7 这样的 64 位 CPU 使用 36 位地址总线,它仍然被归类为 64 位处理器。这让我想知道如果它的地址大小不是 64 位,为什么它有一个 64 位类别。你能澄清一下吗?答案是,CPU 的位数由其寄存器的大小决定,而不是由其地址总线的大小决定。64 位 CPU 意味着它可以一次存储和操作 64 位数据。
因此,第一步是告诉汇编程序使用 16 位操作码汇编代码。
为此,请打开一个新文件:
vim boot.asm
然后在源文件中,我们简单地写:
bits 16
保存并退出(只有图例知道如何退出 VIM)
提醒:
第 16 位不是汇编语言的一部分,而是汇编程序指令。指令只是一种修改汇编程序行为的预处理命令。
还行。让我们演示一下我们的情况:
- 我们有一个名为 boot.asm 的汇编程序。
- 另一个预先编写的程序称为 BIOS、扫描磁盘、软盘和......找到引导加载程序并将其放在内存中的某个位置。
这是一张很好的图片,描述了 BIOS 如何从此资源工作。
- CPU 处于实模式,这意味着它对正在运行的程序的每个段都有段寄存器。并且由于一些设计限制,它的硬件重定位并不像以下那么简单:
physical address = base + logical address
就像:
physical address = (base * 16) + logical address
CPU 可供您使用,它为程序的每个逻辑部分(段)都有一对寄存器,因此您可以拥有一个具有不同段(如文本、数据和......并将其加载到RAM的不同部分,然后在cpu中设置这些寄存器以指向RAM中的这些程序部分。
但在这里我们错过了两件主要的事情:
首先,我们没有链接器和链接器脚本来管理对不同部分和重新定位的程序地址引用。
第二个是BIOS将程序作为一个整体进行复制以解决0x7c00。
BIOS 不会将程序的每个部分复制到不同的地址。为此,需要一个名为“Loader”的程序。(这是操作系统的重要组成部分)
因此,我们的引导加载程序被BIOS复制到RAM,并且它没有任何逻辑段(或部分)
[虽然 CPU 支持它,但 BIOS 软件不使用此功能,老实说,为什么我们需要在内存的不同部分加载引导加载程序?它只是一个简单的程序,不需要如此复杂。
对于此会话,我只想有一个简单的引导加载程序,它不执行任何与引导加载操作系统相关的操作。我只想有一个引导加载程序,它利用 bios 中断将文本打印到屏幕上。
为此,我们需要定义要在源文件中打印的数据。
让我们把它写下来:
bits 16
msg: db “Hello, world!”
因为我们在程序中定义了以后要引用的数据,所以我们必须设置一个名为 DS(数据段)的 CPU 寄存器,以在内存中寻址我们的程序由 bios 加载。为什么?
让我们看看这个程序:
bits 16
msg: db "Hello, world!"
mov ax, msg
mov ax, [msg]
并使用 objdump 检查它:
0: 48 data
1: 65 6c data
3: 6c data
4: 6f data
5: 2c 20 data
7: 77 6f data
9: 72 6c data
b: 64 21 b8 00 00 and WORD PTR fs:[bx+si+0x0],di
10: a1 00 00 mov ax,ds:0x0
因此,正如您看到的 MOV 指令引用我们定义的数据,地址从 0 开始,并且由于我们的数据是在文件开头定义的,因此它的地址为 0。
当 CPU 参考数据执行 mov 指令时,它使用带有 DS 寄存器的硬件重定位。
所以 CPU 处理的数据地址如下:
physical address of data = (Data segment *16) + logical address (which is 0x0 here)
我们知道我们的程序由BIOS加载以解决0x7c00。
因此,实际保存的数据是为了解决 0x7c00 + 0x0
0x7c00 = (data segment*16) + 0x0
因此,数据段应设置为 0x7c0 (0x7c0*16 = 0x7c00)
注意:
是的,我们可以使用 org 指令而不是设置数据段,它会产生相同的结果:
物理地址 =(段地址 * 16)+ 逻辑地址
0x7c00 = (0x0*16) + 0x7c00
到目前为止,我们的程序如下所示:
bits 16
mov ax, 0x7C0
mov ds, ax
msg: db “Hello, world!”
现在是时候使用 BIOS 中断功能将内容写入屏幕了。
为此,我们利用 BIOS 中断0x10。(http://www.ctyme.com/intr/rb-0106.htm)
int 0x10/AH=0x0e:
AH = 0Eh
AL = character to write
BH = page number
BL = foreground color (graphics modes only)
mov ah, 0x0E ; print character to TTY
mov al, [char]
mov bh, 0x00 ; page number 0
mov bl, 0x00 ; foreground color, irrelevant - in text mode
此代码将字符打印到屏幕上。所以我们需要遍历它:
bits 16
mov ax, 0x7c0
mov ds, ax
msg: db "Hello, world!"
mov ah, 0x0E
mov bh, 0x00
mov bl, 0x00
mov si, 0
mov cx, 0
print_loop:
cmp cx, 14
je exit
lea si, msg
add si, cx
mov al, [si]
int 0x10
inc cx
jmp print_loop
exit:
mov ax, 0
整个文件将是这样的:
bits 16
mov ax, 0x7c0
mov ds, ax
msg: db "Hello, world!"
mov ah, 0x0E
mov bh, 0x00
mov bl, 0x00
mov si, 0
mov cx, 0
print_loop:
cmp cx, 14
je exit
lea si, msg
add si, cx
mov al, [si]
int 0x10
inc cx
jmp print_loop
exit:
mov ax, 0
times 510-($-$$) db 0
dw 0xAA55
代码如下:
times 510-($-$$) db 0
dw 0xAA55
在字节号 511 和 512 处添加了一个双字节签名(0xAA 0x55),以便 BIOS 可以识别我们的引导加载程序。
若要使用 Nasm 进行编译,请运行以下命令:
nasm -f bin -o boot.bin boot.asm
它生成我们引导加载程序的扁平二进制文件。
然后我们需要测试我们的引导加载程序。我更喜欢使用 qemu。
QEMU 是一个快速模拟器,可以提供一个主机来运行我们的低级程序。
要使用 qemu 在 16 位机器上运行我们的程序,我们可以使用以下命令:
qemu-system-i386 -fda boot.bin
当你这样做时,你应该看到下面的图片:
你在qemu中看到“你好,世界!
我也没有在我的身上看到这一点。
为什么即使我们做的一切都是正确的,它仍然不起作用?
这背后没有神话。实际上,对此有一个很好的解释。
如果我们检查汇编程序生成的二进制代码:
objdump -b binary -m i8086 -M intel -D boot.bin
我们看到:
0: b8 c0 07 mov ax,0x7c0
3: 8e d8 mov ds,ax
5: 48 dec ax
6: 65 6c gs ins BYTE PTR es:[di],dx
8: 6c ins BYTE PTR es:[di],dx
9: 6f outs dx,WORD PTR ds:[si]
a: 2c 20 sub al,0x20
c: 77 6f ja 0x7d
e: 72 6c jb 0x7c
10: 64 21 b4 0e b7 and WORD PTR fs:[si-0x48f2],si
15: 00 b3 00 be add BYTE PTR [bp+di-0x4200],dh
19: 00 00 add BYTE PTR [bx+si],al
1b: b9 00 00 mov cx,0x0
1e: 83 f9 0e cmp cx,0xe
21: 74 0d je 0x30
23: 8d 36 05 00 lea si,ds:0x5
27: 01 ce add si,cx
29: 8a 04 mov al,BYTE PTR [si]
2b: cd 10 int 0x10
2d: 41 inc cx
2e: eb ee jmp 0x1e
30: b8 00 00 mov ax,0x0
...
1fb: 00 00 add BYTE PTR [bx+si],al
1fd: 00 55 aa add BYTE PTR [di-0x56],dl
我们看到我们的数据位于地址之间,0x05到0x10。
“Hello, world!”的十六进制等效值是“48 65 6C 6C 6F 2C 20 77 6F 72 6C 64 21”
但这些十六进制值也可以表示指令的操作码。
如果 CPU 执行它们,则会发生不可预测的行为。
例如,在我们的例子中,这个十六进制链表示这些组装指令:
5: 48 dec ax
6: 65 6c gs ins BYTE PTR es:[di],dx
8: 6c ins BYTE PTR es:[di],dx
9: 6f outs dx,WORD PTR ds:[si]
a: 2c 20 sub al,0x20
c: 77 6f ja 0x7d
e: 72 6c jb 0x7c
10: 64 21 b4 0e b7 and WORD PTR fs:[si-0x48f2],si
这正是我们的程序不起作用的原因。正如您在地址 0x0c 和 0x0e 上看到的,我们有两个条件跳转(其中一个触发)。
所以 CPU 跳转到一个我们不知道那里有什么的地址,并且从不执行我们的代码。这就是我们的程序不起作用的原因。
没有办法对 CPU 说“嘿,CPU,这是数据,不要像指令一样行事。
因此,要解决这个问题,我们只需要将数据从 CPU 执行路径中移出即可。
为此,只需将数据移动到代码的末尾:
bits 16
mov ax, 0x7c0
mov ds, ax
mov ah, 0x0E
mov bh, 0x00
mov bl, 0x00
mov si, 0
mov cx, 0
print_loop:
cmp cx, 14
je exit
lea si, msg
add si, cx
mov al, [si]
int 0x10
inc cx
jmp print_loop
exit:
mov ax, 0
msg: db "Hello, world!"
times 510-($-$$) db 0
dw 0xAA55
是时候确定它是否有效了:
恭喜!它正在工作。我们刚刚编写了第一个在没有任何操作系统的 16 位机器上运行的程序。
我认为是时候结束这篇文章了。
原文始发于微信公众号(安全狗的自我修养):第 2 集:编写我们的第一个微型引导加载程序
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论