
所以,正确地讲。 我们将在Linux上,在NASM上并使用QEMU进行编写。 这很容易安装,因此跳过此步骤。
可以理解,读者至少在基本级别上熟悉NASM的语法(但是,这里没有什么特别复杂的内容),并且了解什么是寄存器。
基础理论
当计算机打开电源时,启动处理器的第一件事是BIOS代码(或UEFI,但在这里我仅谈论BIOS),它“连接”在主板的内存中(具体为0xFFFFFFF0)。
打开BIOS后,立即启动开机自检(POST)-开机后进行自检。 BIOS检查内存的运行状况,检测并初始化连接的设备,检查寄存器,确定内存的大小,依此类推。
下一步是确定可以用来引导OS的引导盘。 引导磁盘是具有第一个扇区的后2个字节(第一个扇区表示该驱动器的前512个字节,因为1个扇区= 512字节)的磁盘(或其他驱动器)为55,AA为16进制格式。 一旦找到启动盘,BIOS会将其前512个字节加载到地址0x7c00的RAM中,并将控制权转移到该地址处的处理器。
当然,在这512字节中,它不能适合完整的操作系统。 因此,通常在该扇区中放置主加载程序,该主加载程序将主OS代码加载到RAM中并将控制权转移给它。
从一开始,处理器就一直在实模式(= 16位模式)下运行。 这意味着它只能使用16位数据并使用分段存储器寻址,也只能寻址1 MB存储器。 但是我们在这里不使用第二个。 下图显示了将控制权转移到我们的代码时RAM的状态(图是从此处获取的 )。

在实际操作之前要说的最后一件事是打扰。 中断是一种特殊的信号(例如,从输入设备,如键盘或鼠标)发给处理器的信号,该信号表示必须立即中断当前代码的执行并执行中断处理程序代码。 中断处理程序的所有地址都位于主存储器的中断描述符表(IDT)中。 每个中断都有自己的中断处理程序。 例如,当按下键盘键时,会触发一个中断,处理器停止运行,记住被中断指令的地址,将其寄存器的所有值保存在堆栈中,然后继续执行中断处理程序。 执行结束后,处理器将恢复寄存器的值,并跳回被中断的指令并继续执行。
例如,为了在屏幕上显示某些内容,BIOS使用0x10中断(十六进制格式),而0x16中断用于等待按键被按下。 实际上,这些都是我们在这里需要的中断。
同样,每个中断都有其自己的子功能,该子功能确定其行为的特殊性。 要以文本格式(!)显示内容,您需要在AH寄存器中输入值0x0e。 此外,中断具有自己的参数。 0x10从ah(定义特定的子功能)和al(要打印的字符)中获取值。 这样
mov ah, 0x0e mov al, 'x' int 0x10
显示字符“ x”。 0x16从ah(特定的子功能)中获取值,并将输入的键的值加载到寄存器al中。 我们将使用0x0函数。
实践部分
让我们从助手代码开始。 我们需要比较两条线的功能以及在屏幕上显示一条线的功能。 我试图在注释中尽可能清楚地描述这些功能的操作。
str_compare.asm:
compare_strs_si_bx: push si ; push bx push ax comp: mov ah, [bx] ; , cmp [si], ah ; ah jne not_equal ; , cmp byte [si], 0 ; , je first_zero ; inc si ; bx si inc bx jmp comp ; first_zero: cmp byte [bx], 0 ; bx != 0, , jne not_equal ; , not_equal mov cx, 1 ; , cx = 1 pop si ; pop bx pop ax ret ; not_equal: mov cx, 0 ; , cx = 0 pop si ; pop bx pop ax ret ;
该函数接受SI和BX寄存器作为参数。 如果行相等,则将CX设置为1,否则设置为0。
还值得注意的是,寄存器AX,BX,CX和DX分为两个单字节部分:高字节为AH,BH,CH和DH,低字节为AL,BL,CL和DL。
最初,可以理解的是,在bx和si中有指向行首所在的内存中某个地址的指针(!)(即,将地址存储在内存中)。 操作[bx]将从bx获取一个指针,它将转到该地址并从该地址获取一些值。 inc bx意味着现在指针将指向原始地址之后的地址。
print_string.asm:
print_string_si: push ax ; ax mov ah, 0x0e ; ah 0x0e, call print_next_char ; pop ax ; ax ret ; print_next_char: mov al, [si] ; cmp al, 0 ; si jz if_zero ; int 0x10 ; al inc si ; jmp print_next_char ; ... if_zero: ret
作为参数,该函数使用SI寄存器,然后逐字节打印字符串。
现在,让我们继续主代码。 首先,让我们定义所有变量(此代码将在文件的最后):
; 0x0d - , 0xa - wrong_command: db "Wrong command!", 0x0d, 0xa, 0 greetings: db "The OS is on. Type 'help' for commands", 0x0d, 0xa, 0xa, 0 help_desc: db "Here's nothing to show yet. But soon...", 0x0d, 0xa, 0 goodbye: db 0x0d, 0xa, "Goodbye!", 0x0d, 0xa, 0 prompt: db ">", 0 new_line: db 0x0d, 0xa, 0 help_command: db "help", 0 input: times 64 db 0 ; - 64 times 510 - ($-$$) db 0 dw 0xaa55
回车符将其移动到屏幕的左边缘,即行的开头。
input: times 64 db 0
表示我们在缓冲区下分配64个字节用于输入,并用零填充它们。
需要其余的变量来显示一些信息,在代码的更下方,您将了解为什么都需要它们。
times 510 - ($-$$) db 0 dw 0xaa55
意味着我们将输出文件的大小(扩展名为.bin)显式设置为512字节,将前510个字节填充为零(当然,它们会在执行整个代码之前填充),而后两个字节则使用相同的“魔术”字节55和AA 。 $表示当前指令的地址,而$$是我们代码中第一条指令的地址。
让我们继续实际的代码:
org 0x7c00 ; (1) bits 16 ; (2) jmp start ; start %include "print_string.asm" ; %include "str_compare.asm" ; ==================================================== start: mov ah, 0x00 ; (3) mov al, 0x03 int 0x10 mov sp, 0x7c00 ; (4) mov si, greetings ; call print_string_si ; mainloop
(1)。 该命令使NASM清楚我们正在执行从0x7c00开始的代码。 这样一来,它就可以自动对所有相对于该地址的地址进行偏向操作,这样我们就不会明确地这样做。
(2)。 此命令指示NASM我们正在16位模式下运行。
(3)。 启动后,QEMU会打印很多我们不需要的信息。 为此,将ah 0x00设置为al 0x03,然后调用0x10清除所有内容。
(4)。 要将寄存器保存在堆栈上,必须使用SP堆栈指针指定其顶点位于哪个地址。 SP将指示内存中将写入下一个值的区域。 将值添加到堆栈-SP将内存减少2个字节(因为我们处于实模式,其中所有寄存器操作数均为16位(即双字节)值)。 我们指定了0x7c00,因此堆栈上的值将直接存储在内存中的代码旁边。 再次-堆栈变小(!)。 这意味着堆栈上的值越多,SP堆栈的指针将指示的内存越少。
mainloop: mov si, prompt ; call print_string_si call get_input ; jmp mainloop ; mainloop...
主循环。 在这里,每次迭代时,我们都打印“>”字符,然后调用get_input函数,该函数实现了键盘中断的工作。
get_input: mov bx, 0 ; bx input_processing: mov ah, 0x0 ; 0x16 int 0x16 ; ASCII cmp al, 0x0d ; enter je check_the_input ; , , ; cmp al, 0x8 ; backspace je backspace_pressed cmp al, 0x3 ; ctrl+c je stop_cpu mov ah, 0x0e ; - ; int 0x10 mov [input+bx], al ; inc bx ; cmp bx, 64 ; input je check_the_input ; , enter jmp input_processing ;
(1)[input + bx]表示我们获取输入缓冲区输入的开头的地址,并向其添加bx,即,我们得到bx +缓冲区的第一个元素。
stop_cpu: mov si, goodbye ; call print_string_si jmp $ ; ; $
这里的一切都很简单-如果按Ctrl + C,计算机将无休止地执行jmp $功能。
backspace_pressed: cmp bx, 0 ; backspace , input , je input_processing ; mov ah, 0x0e ; backspace. , int 0x10 ; , mov al, ' ' ; , int 0x10 ; mov al, 0x8 ; int 0x10 ; backspace dec bx mov byte [input+bx], 0 ; input jmp input_processing ;
为了避免在按退格键时擦除'>'字符,我们检查输入是否为空。 如果没有,则什么也不做。
check_the_input: inc bx mov byte [input+bx], 0 ; , ; ( '\0' ) mov si, new_line ; call print_string_si mov si, help_command ; si help mov bx, input ; bx - call compare_strs_si_bx ; si bx ( help) cmp cx, 1 ; compare_strs_si_bx cx 1, ; je equal_help ; => ; help jmp equal_to_nothing ; , "Wrong command!"
在这里,我认为所有评论都清楚。
equal_help: mov si, help_desc call print_string_si jmp done equal_to_nothing: mov si, wrong_command call print_string_si jmp done
根据所输入的内容,我们显示help_desc变量的文本或错误的command变量的文本。
; done input done: cmp bx, 0 ; input je exit ; , mainloop dec bx ; , mov byte [input+bx], 0 jmp done ; exit: ret
实际上,整个代码是:
提示:
org 0x7c00 bits 16 jmp start ; start %include "print_string.asm" %include "str_compare.asm" ; ==================================================== start: cli ; , ; mov ah, 0x00 ; mov al, 0x03 int 0x10 mov sp, 0x7c00 ; mov si, greetings ; call print_string_si ; mainloop mainloop: mov si, prompt ; call print_string_si call get_input ; jmp mainloop ; mainloop... get_input: mov bx, 0 ; bx input_processing: mov ah, 0x0 ; 0x16 int 0x16 ; ASCII cmp al, 0x0d ; enter je check_the_input ; , , ; cmp al, 0x8 ; backspace je backspace_pressed cmp al, 0x3 ; ctrl+c je stop_cpu mov ah, 0x0e ; - ; int 0x10 mov [input+bx], al ; inc bx ; cmp bx, 64 ; input je check_the_input ; , enter jmp input_processing ; stop_cpu: mov si, goodbye ; call print_string_si jmp $ ; ; $ backspace_pressed: cmp bx, 0 ; backspace , input , je input_processing ; mov ah, 0x0e ; backspace. , int 0x10 ; , mov al, ' ' ; , int 0x10 ; mov al, 0x8 ; int 0x10 ; backspace dec bx mov byte [input+bx], 0 ; input jmp input_processing ; check_the_input: inc bx mov byte [input+bx], 0 ; , ; ( '\0' ) mov si, new_line ; call print_string_si mov si, help_command ; si help mov bx, input ; bx - call compare_strs_si_bx ; si bx ( help) cmp cx, 1 ; compare_strs_si_bx cx 1, ; je equal_help ; => ; help jmp equal_to_nothing ; , "Wrong command!" equal_help: mov si, help_desc call print_string_si jmp done equal_to_nothing: mov si, wrong_command call print_string_si jmp done ; done input done: cmp bx, 0 ; input je exit ; , mainloop dec bx ; , mov byte [input+bx], 0 jmp done ; exit: ret ; 0x0d - , 0xa - wrong_command: db "Wrong command!", 0x0d, 0xa, 0 greetings: db "The OS is on. Type 'help' for commands", 0x0d, 0xa, 0xa, 0 help_desc: db "Here's nothing to show yet. But soon...", 0x0d, 0xa, 0 goodbye: db 0x0d, 0xa, "Goodbye!", 0x0d, 0xa, 0 prompt: db ">", 0 new_line: db 0x0d, 0xa, 0 help_command: db "help", 0 input: times 64 db 0 ; - 64 times 510 - ($-$$) db 0 dw 0xaa55
要编译所有这些,请输入命令:
nasm -f bin prompt.asm -o bootloader.bin
然后,在输出中获得带有代码的二进制文件。 现在使用此文件运行QEMU仿真器(-monitor stdio允许您使用print $ reg命令随时显示寄存器值):
qemu-system-i386 bootloader.bin -monitor stdio
然后我们得到输出:
