7

我在ubuntu下使用nasm。顺便说一句,我需要从用户的键盘获取单个输入字符(例如当程序要求您输入 y/n 时?),因此在按下键且不按 Enter 的情况下,我需要读取输入的字符。我用谷歌搜索了很多,但我发现的所有内容都与这条线(int 21h)有关,导致“分段错误”。请帮我弄清楚如何获得单个字符或如何克服这个分段错误。

4

3 回答 3

16

它可以从组装中完成,但这并不容易。您不能使用 int 21h,这是一个 DOS 系统调用,在 Linux 下不可用。

要在类 UNIX 操作系统(如 Linux)下从终端获取字符,请从 STDIN(文件编号 0)读取。通常,读取系统调用将阻塞,直到用户按下回车键。这称为规范模式。要在不等待用户按下回车的情况下读取单个字符,您必须首先禁用规范模式。当然,如果您想稍后在程序退出之前输入行,则必须重新启用它。

要在 Linux 上禁用规范模式,请使用 ioctl 系统调用将 IOCTL (IO ControL) 发送到 STDIN。我假设您知道如何从汇编程序进行 Linux 系统调用。

ioctl 系统调用具有三个参数。第一个是将命令发送到的文件(STDIN),第二个是 IOCTL 编号,第三个通常是指向数据结构的指针。ioctl 成功时返回 0,失败时返回负错误代码。

您需要的第一个 IOCTL 是 TCGETS(编号 0x5401),它在 termios 结构中获取当前终端参数。第三个参数是一个指向 termios 结构的指针。从内核源代码来看,termios 结构定义为:

struct termios {
    tcflag_t c_iflag;               /* input mode flags */
    tcflag_t c_oflag;               /* output mode flags */
    tcflag_t c_cflag;               /* control mode flags */
    tcflag_t c_lflag;               /* local mode flags */
    cc_t c_line;                    /* line discipline */
    cc_t c_cc[NCCS];                /* control characters */
};

其中 tcflag_t 为 32 位长,cc_t 为 1 字节长,NCCS 当前定义为 19。请参阅 NASM 手册了解如何方便地为此类结构定义和保留空间。

因此,一旦您获得了当前的 termios,就需要清除规范标志。此标志位于 c_lflag 字段中,掩码为 ICANON (0x00000002)。要清除它,请计算 c_lflag AND(不是 ICANON)。并将结果存储回 c_lflag 字段。

现在您需要通知内核您对 termios 结构的更改。使用 TCSETS (number 0x5402) ioctl,使用第三个参数设置您的 termios 结构的地址。

如果一切顺利,终端现在处于非规范模式。您可以通过设置规范标志(通过将 c_lflag 与 ICANON 进行 ORing)并再次调用 TCSETS ioctl 来恢复规范模式。退出前始终恢复规范模式

正如我所说,这并不容易。

于 2010-08-01T01:32:40.673 回答
7

我最近需要这样做,并且受到 Callum出色答案的启发,我写了以下内容(适用于 x86-64 的 NASM):

DEFAULT REL

section .bss
termios:        resb 36

stdin_fd:       equ 0           ; STDIN_FILENO
ICANON:         equ 1<<1
ECHO:           equ 1<<3

section .text
canonical_off:
        call read_stdin_termios

        ; clear canonical bit in local mode flags
        and dword [termios+12], ~ICANON

        call write_stdin_termios
        ret

echo_off:
        call read_stdin_termios

        ; clear echo bit in local mode flags
        and dword [termios+12], ~ECHO

        call write_stdin_termios
        ret

canonical_on:
        call read_stdin_termios

        ; set canonical bit in local mode flags
        or dword [termios+12], ICANON

        call write_stdin_termios
        ret

echo_on:
        call read_stdin_termios

        ; set echo bit in local mode flags
        or dword [termios+12], ECHO

        call write_stdin_termios
        ret

; clobbers RAX, RCX, RDX, R8..11 (by int 0x80 in 64-bit mode)
; allowed by x86-64 System V calling convention    
read_stdin_termios:
        push rbx

        mov eax, 36h
        mov ebx, stdin_fd
        mov ecx, 5401h
        mov edx, termios
        int 80h            ; ioctl(0, 0x5401, termios)

        pop rbx
        ret

write_stdin_termios:
        push rbx

        mov eax, 36h
        mov ebx, stdin_fd
        mov ecx, 5402h
        mov edx, termios
        int 80h            ; ioctl(0, 0x5402, termios)

        pop rbx
        ret

(编者注:不要int 0x80在 64 位代码中使用:如果在 64 位代码中使用 32 位 int 0x80 Linux ABI 会发生什么? -它会在 PIE 可执行文件中中断(其中静态地址不在低 32 位),或者使用堆栈上的 termios 缓冲区。它确实可以在传统的非 PIE 可执行文件中工作,并且此版本可以轻松移植到 32 位模式。)

然后你可以这样做:

call canonical_off

如果您正在阅读一行文本,您可能还想这样做:

call echo_off

这样每个字符在输入时都不会被回显。

可能有更好的方法可以做到这一点,但它适用于 64 位 Fedora 安装。

更多信息可以在手册页termios(3)termbits.h源代码中找到。

于 2010-10-30T15:52:31.847 回答
-2

最简单的方法:对于文本模式程序,使用libncurses访问键盘;对于图形程序,请使用Gtk+

困难的方法:假设一个文本模式的程序,你必须告诉内核你想要单字符输入,然后你必须做大量的簿记和解码。这真的很复杂。没有与旧的 DOSgetch()例程相当的东西。您可以在此处开始学习如何操作:终端 I/O。图形程序更加复杂;最低级别的 API 是Xlib

无论哪种方式,无论汇编中的内容是什么,您都会疯狂编码;改用 C。

于 2010-07-22T01:02:35.753 回答