如何在Ubuntu下使用NASM汇编从键盘读取单个字符输入?

7

我正在Ubuntu下使用nasm。顺便说一下,我需要从用户的键盘获取单个输入字符(就像程序要求您输入y/n?)因此,按下按键而不按回车键,我需要读取输入的字符。我在谷歌上搜索了很多,但是我找到的所有内容都与这行代码(int 21h)有关,结果是“分段错误”。请帮助我弄清楚如何获取单个字符或如何克服这个分段错误。

3个回答

16

虽然可以用汇编语言实现,但这并不容易。你不能使用int 21h指令,因为它是DOS系统调用,在Linux下不可用。

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

在Linux上禁用规范模式,需要使用ioctl系统调用向标准输入(STDIN)发送IOCTL(IO ControL)。我假设您知道如何从汇编语言中发出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 是一个字节长,NCCS 目前定义为19。请参考 NASM 手册,了解如何方便地定义和保留此类结构的空间。

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

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

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

就像我说的,这并不容易。


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

(编辑注:不要在64位代码中使用int 0x80如果在64位代码中使用32位int 0x80 Linux ABI会发生什么? - 在PIE可执行文件(其中静态地址不在低32位中)或堆栈上的termios缓冲区中会出现问题。它实际上可以在传统的非PIE可执行文件中工作,并且这个版本可以很容易地移植到32位模式。)
然后,您可以执行以下操作:
call canonical_off

如果你正在阅读一行文本,你可能也想要做以下操作:

call echo_off

为了避免每个字符在键入时都被回显,可以采用以下方法。虽然可能有更好的方法,但在64位Fedora安装中,这种方法对我有效。

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


如果在64位代码中使用32位int 0x80 Linux ABI会发生什么?不要这样做,它会在低32位之外的地址(例如堆栈空间)中出现问题。当然,mov edx, termios也会出现同样的问题 - 这只适用于非PIE可执行文件在x86-64 Linux上,否则你需要使用lea rdx, [rel termios]。(参见如何在GNU Assembler中将函数或标签的地址加载到寄存器中,该问题还涵盖了NASM) - Peter Cordes

-1

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

困难的方法:假设是一个文本模式程序,您需要告诉内核您想要单字符输入,然后您需要进行大量的簿记和解码。这真的很复杂。没有好旧的DOS getch()例程的等价物。您可以从这里开始了解如何做到这一点:Terminal I/O。图形程序更加复杂;最基础的API是Xlib

无论如何,您在汇编中编写这个内容都会发疯;请改用C语言。


5
如果OP正在学习汇编语言,那么虽然你所说的对于C语言来说是正确的,但它并不是一个相关的答案。请注意,我的翻译尽可能保持原意,同时使语言更加通俗易懂,没有其他额外信息返回。 - Tyler McHenry
1
这是因为 OP 不应该使用汇编语言进行编程。现在手动编写汇编语言的唯一好理由是如果它是一个性能关键的计算子程序,或者是操作系统内核中极少数无法以其他方式编码的低级部分之一。用户交互不符合条件。在Unix下,OP试图做的甚至不是一个好的学习练习 - zwol
话虽如此,但是并没有什么阻止 OP 编写调用 libncurses 的汇编语言,尽管在我看来这似乎是一种浪费时间和理智的行为(但这不会像手动进行 Unix 终端 I/O 的汇编语言那样糟糕)。 - zwol
3
我不认为这是学习Unix编程或汇编的好练习,但它肯定是一个很好的学习练习。虽然在绝大多数应用程序中汇编并不是一个实际的选择,但我认为学习如何在汇编中做一些通常不会做的事情并不是浪费时间。它可以让你对什么层次的抽象是可能的,以及机器和操作系统如何协同工作有一个有趣的视角。 - jdd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接