尝试通过 int 16h 读取密钥以 VM 重启结束

Attempt to read key via int 16h ends in VM reboot

我试图将键盘交互添加到来自 this example 的代码中。考虑以下文件:

Cargo.toml

[package]
name = "kernelhello"
version = "0.0.1"

[dependencies]
bootloader = "0.3.12"

[package.metadata.bootimage]
default-target = "build.json"

build.json

{
  "llvm-target": "x86_64-unknown-none",
  "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128",
  "arch": "x86_64",
  "target-endian": "little",
  "target-pointer-width": "64",
  "target-c-int-width": "32",
  "os": "none",
  "executables": true,
  "linker-flavor": "ld.lld",
  "linker": "rust-lld",
  "panic-strategy": "abort",
  "disable-redzone": true,
  "features": "-mmx,-sse,+soft-float"
}

src/main.rs

// src/main.rs
#![feature(asm)]

#![no_std] // don't link the Rust standard library
#![no_main] // disable all Rust-level entry points

use core::panic::PanicInfo;

/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}


#[no_mangle]
pub extern "C" fn _start() -> ! {
    let mut HELLO: &mut [u8] = &mut b"Hello World!".clone();
    let vga_buffer = 0xb8000 as *mut u8;

    let mut z = 0;
    loop {
        for (i, byte) in HELLO.iter_mut().enumerate() {
            unsafe {
                z += 14;
                z %= 4000;
                *vga_buffer.offset(z + i as isize * 2) = *byte;
                *vga_buffer.offset(z + i as isize * 2 + 1) = 0xa;
                asm!("mov $[=12=], %ah\nint $[=12=]x16");
            }
        }
    }
}

不幸的是,bootimage run 的尝试以卡在重启循环中的图像结束 - 如果我注释掉 asm! 调用,则不会发生这种情况。这是一个反汇编:

➜  rust-kernel-hello objdump -D -b binary -Mintel,x86-64  -m i386 target/build/debug/bootimage-kernelhello.bin | grep -C5 'ef01'
    eef0:       00 
    eef1:       48 8b 84 24 d0 00 00    mov    rax,QWORD PTR [rsp+0xd0]
    eef8:       00 
    eef9:       48 89 84 24 00 01 00    mov    QWORD PTR [rsp+0x100],rax
    ef00:       00 
    ef01:       48 8d bc 24 f0 00 00    lea    rdi,[rsp+0xf0]
    ef08:       00 
    ef09:       e8 72 fe ff ff          call   0xed80
    ef0e:       48 89 94 24 20 01 00    mov    QWORD PTR [rsp+0x120],rdx
    ef15:       00 
    ef16:       48 89 84 24 18 01 00    mov    QWORD PTR [rsp+0x118],rax
--
    f119:       48 89 04 24             mov    QWORD PTR [rsp],rax
    f11d:       48 8b 04 24             mov    rax,QWORD PTR [rsp]
    f121:       c6 00 0a                mov    BYTE PTR [rax],0xa
    f124:       b4 00                   mov    ah,0x0
    f126:       cd 16                   int    0x16
    f128:       e9 d4 fd ff ff          jmp    0xef01
    f12d:       48 8d 3d cc 0a 00 00    lea    rdi,[rip+0xacc]        # 0xfc00
    f134:       e8 07 04 00 00          call   0xf540
    f139:       0f 0b                   ud2    
    f13b:       48 8d 3d e6 0a 00 00    lea    rdi,[rip+0xae6]        # 0xfc28
    f142:       e8 f9 03 00 00          call   0xf540

我做错了什么?

我想更详细地解释一下zx485的评论:

我经常看到汇编语言初学者对 int (x86)、syscall (MIPS) 或 SWI (ARM) 等指令的实际作用感到困惑。

实际上,这些指令是 call 指令的一种特殊形式:它们调用一些通常位于操作系统中的子例程。

64 位 x86 CPUs 有不同的操作模式。其中一个名为 "real mode"。在此模式下 CPU 只能执行 16 位代码。

可使用int 0x16调用的BIOS子程序仅在PC运行在"real mode"时有效。

您的程序是 64 位程序(使用 rax 之类的寄存器)这一事实告诉我们您的 CPU 在实模式下不是 运行。

如果您正在编写自己的操作系统,您可以定义自己的子例程,这些子例程由某些 int 指令调用:

您可以定义一个由 int 0x16 调用的读取键盘的子例程和另一个由 int 0x10 写入屏幕调用的子例程。

不过,您也可以在操作系统中自由定义 int 0x16 用于写入屏幕,int 0x10 用于硬盘访问。

并且在任何情况下,您都必须自己编写子程序,因为 BIOS 中现有的子程序不能用于 "real mode" 以外的任何其他操作模式。 (这就是 Ross Ridge 在他的评论中所指出的。)