我正在Ubuntu下使用nasm。顺便说一下,我需要从用户的键盘获取单个输入字符(就像程序要求您输入y/n?)因此,按下按键而不按回车键,我需要读取输入的字符。我在谷歌上搜索了很多,但是我找到的所有内容都与这行代码(int 21h
)有关,结果是“分段错误”。请帮助我弄清楚如何获取单个字符或如何克服这个分段错误。
虽然可以用汇编语言实现,但这并不容易。你不能使用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 来恢复规范模式。 始终在退出之前恢复规范模式
就像我说的,这并不容易。
最近我需要做这件事情,受到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位代码中使用32位int 0x80 Linux ABI会发生什么? - 在PIE可执行文件(其中静态地址不在低32位中)或堆栈上的termios缓冲区中会出现问题。它实际上可以在传统的非PIE可执行文件中工作,并且这个版本可以很容易地移植到32位模式。)call canonical_off
如果你正在阅读一行文本,你可能也想要做以下操作:
call echo_off
为了避免每个字符在键入时都被回显,可以采用以下方法。虽然可能有更好的方法,但在64位Fedora安装中,这种方法对我有效。
更多信息可以在termios(3)
的手册页面或termbits.h
源代码中找到。
简单的方法:对于文本模式程序,使用libncurses来访问键盘;对于图形程序,使用Gtk+。
困难的方法:假设是一个文本模式程序,您需要告诉内核您想要单字符输入,然后您需要进行大量的簿记和解码。这真的很复杂。没有好旧的DOS getch()
例程的等价物。您可以从这里开始了解如何做到这一点:Terminal I/O。图形程序更加复杂;最基础的API是Xlib。
无论如何,您在汇编中编写这个内容都会发疯;请改用C语言。
mov edx, termios
也会出现同样的问题 - 这只适用于非PIE可执行文件在x86-64 Linux上,否则你需要使用lea rdx, [rel termios]
。(参见如何在GNU Assembler中将函数或标签的地址加载到寄存器中,该问题还涵盖了NASM) - Peter Cordes