NASM和QEMU上最简单的命令行

图片


所以,正确地讲。 我们将在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 

然后我们得到输出:


图片

Source: https://habr.com/ru/post/zh-CN442428/


All Articles