我在ubuntu下使用nasm。顺便说一句,我需要从用户的键盘获取单个输入字符(例如当程序要求您输入 y/n 时?),因此在按下键且不按 Enter 的情况下,我需要读取输入的字符。我用谷歌搜索了很多,但我发现的所有内容都与这条线(int 21h
)有关,导致“分段错误”。请帮我弄清楚如何获得单个字符或如何克服这个分段错误。
3 回答
它可以从组装中完成,但这并不容易。您不能使用 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 来恢复规范模式。退出前始终恢复规范模式
正如我所说,这并不容易。
我最近需要这样做,并且受到 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
源代码中找到。
最简单的方法:对于文本模式程序,使用libncurses访问键盘;对于图形程序,请使用Gtk+。
困难的方法:假设一个文本模式的程序,你必须告诉内核你想要单字符输入,然后你必须做大量的簿记和解码。这真的很复杂。没有与旧的 DOSgetch()
例程相当的东西。您可以在此处开始学习如何操作:终端 I/O。图形程序更加复杂;最低级别的 API 是Xlib。
无论哪种方式,无论汇编中的内容是什么,您都会疯狂编码;改用 C。