清除输入缓冲区组件 x86 (NASM)

Clear input buffer Assembly x86 (NASM)

编辑:这与此类似:。但这不是同一个问题。

从另一个 post,我能够防止打印额外的字符。但是,当程序返回到要求用户输入的位置时,我仍然无法阻止读取这些额外的字符。


我正在创建一个程序,要求用户输入,然后打印出来。之后,它要求用户输入 'y' 如果他们想打印另一个文本,或者按其他任何键关闭程序。

我的问题是,如果用户输入的字符多于预期,那些多余的字符不会消失,当程序返回要求用户输入时,没有机会输入,因为程序需要上次接收输入的剩余字符。

例如:

要求用户输入要打印的文本,他们输入:“Heyyyyyyyyyyyyyyyyyyyyyyyyyyyy

剩下的是“yyy”

此时,程序应要求用户输入 'y' 以重复该过程,或输入其他任何内容以关闭程序。

输出:

输出:yy

现在它再次要求用户输入

由于“yyy”仍在缓冲区中,因此在这种情况下用户没有机会实际输入。

我该怎么做才能解决这个问题?我最近才开始了解这个,所以我很感激任何帮助。

谢谢。

这就是我所做的

prompt db "Type your text here.", 0h
retry db "Wanna try again? If yes, enter y. If not, enter anything else to close the program"

section .bss
text resb 50
choice resb 2

section .text
    global _start

_start:

    mov rax, 1      ;Just asking the user to enter input
    mov rdi, 1
    mov rsi, prompt
    mov rdx, 21
    syscall


    mov rax, 0      ;Getting input and saving it on var text
    mov rdi, 0
    mov rsi, text
    mov rdx, 50
    syscall

    mov r8, rax     ;This is what I added to prevent additional characters
                    ;from being printed

    mov rax, 1      ;Printing the user input
    mov rdi, 1
    mov rsi, text
    mov rdx, r8
    syscall

    mov rax, 1      ;Asking if user wants to try again
    mov rdi, 1
    mov rsi, retry
    mov rdx, 83
    syscall

    mov rax, 0      ;Getting input and saving it on var choice
    mov rdi, 0
    mov rsi, choice
    mov rdx, 2
    syscall

    mov r8b, [choice] ;If choice is different from y, go to end and close the program. Otherwhise, go back to start.
    cmp byte r8b, 'y'
    jne end
    jmp _start

    end:
    mov rax, 60      
    mov rdi, 0       
    syscall

清除 stdin 的简单方法是检查 choice 中的第二个字符是否为 '\n' (0xa)。如果不是,那么字符将保留在 stdin 未读状态。您已经知道如何从 stdin 读取,所以在那种情况下,只需读取 stdin 直到 '\n' 被读取 1,例如

    mov rax, 0      ;Getting input and saving it on var choice
    mov rdi, 0
    mov rsi, choice
    mov rdx, 2
    syscall

    cmp byte [choice], 'y'  ; check 1st byte of choice, no need for r8b
    jne end
    
    cmp byte [choice + 1], 0xa  ; is 2nd char '\n' (if yes done, jump start)
    je _start
    
    empty:          ; chars remain in stdin unread
    mov rax, 0      ; read 1-char from stdin into choice
    mov rdi, 0
    mov rsi, choice
    mov rdx, 1
    syscall
    
    cmp byte [choice], 0xa  ; check if char '\n'?
    jne empty               ; if not, repeat
    
    jmp _start

除此之外,您应该在声明时确定提示长度,例如

prompt db "Type your text here. ", 0h
plen equ $-prompt
retry db "Try again (y/n)? ", 0h
rlen equ $-retry

这样您就不必硬编码长度以防您更改提示,例如

_start:

    mov rax, 1      ;Just asking the user to enter input
    mov rdi, 1
    mov rsi, prompt
    mov rdx, plen
    syscall


    mov rax, 0      ;Getting input and saving it on var text
    mov rdi, 0
    mov rsi, text
    mov rdx, 50
    syscall

    mov r8, rax

    mov rax, 1      ;Printing the user input
    mov rdi, 1
    mov rsi, text
    mov rdx, r8
    syscall

    mov rax, 1      ;Asking if user wants to try again
    mov rdi, 1
    mov rsi, retry
    mov rdx, rlen
    syscall

如果你把它放在一起,你可以这样做:

prompt db "Type your text here. ", 0h
plen equ $-prompt
retry db "Try again (y/n)? ", 0h
rlen equ $-retry

section .bss
text resb 50
choice resb 2

section .text
    global _start

_start:

    mov rax, 1      ;Just asking the user to enter input
    mov rdi, 1
    mov rsi, prompt
    mov rdx, plen
    syscall


    mov rax, 0      ;Getting input and saving it on var text
    mov rdi, 0
    mov rsi, text
    mov rdx, 50
    syscall

    mov r8, rax

    mov rax, 1      ;Printing the user input
    mov rdi, 1
    mov rsi, text
    mov rdx, r8
    syscall

    mov rax, 1      ;Asking if user wants to try again
    mov rdi, 1
    mov rsi, retry
    mov rdx, rlen
    syscall

    mov rax, 0      ;Getting input and saving it on var choice
    mov rdi, 0
    mov rsi, choice
    mov rdx, 2
    syscall

    cmp byte [choice], 'y'  ; check 1st byte of choice, no need for r8b
    jne end
    
    cmp byte [choice + 1], 0xa  ; is 2nd char '\n' (if yes done, jump start)
    je _start
    
    empty:          ; chars remain in stdin unread
    mov rax, 0      ; read 1-char from stdin into choice
    mov rdi, 0
    mov rsi, choice
    mov rdx, 1
    syscall
    
    cmp byte [choice], 0xa  ; check if char '\n'?
    jne empty               ; if not, repeat
    
    jmp _start

    end:
    mov rax, 60      
    mov rdi, 0       
    syscall

示例Use/Output

$ ./bin/emptystdin
Type your text here. abc
abc
Try again (y/n)? y
Type your text here. def
def
Try again (y/n)? yes please!
Type your text here. geh
geh
Try again (y/n)? yyyyyyyyyyyyyeeeeeeeeeesssssssss!!!!
Type your text here. ijk
ijk
Try again (y/n)? n

现在即使一只猫在您的 (y/n)? 提示符下踩键盘也不会造成问题。可能有更优雅的方法来处理这个问题,比重复读取更有效,syscall,但这将解决这个问题。


其他注意事项和 Error-Checks

如上所述,简单地读取和检查 character-at-a-time 并不是一种非常有效的方法,尽管它在概念上是最简单的扩展,无需进行其他更改。 @PeterCordes 在下面的评论中提出了一些与更有效的方法相关的要点,更重要的是关于可能出现的错误情况也应该加以保护。

对于初学者,当您正在寻找有关单个系统调用的信息时,请使用 Anatomy of a system call, part 1 provides a bit of background on approaching their use supplemented by the Linux manual page, for read man 2 read 了解有关参数类型以及 return 类型和值的详细信息。

上面的原始解决方案没有解决如果用户通过按 Ctrl+d 生成手册 end-of-file 会发生什么或者如果实际发生读取错误。它只是解决了 user-input 并清空 stdin 问题。对于任何 user-input,在使用该值之前,您必须通过 检查 return 来验证输入是否成功。 (不仅针对 yes/no 输入,而且针对所有输入)。出于此处的目的,您可以将零输入(手动 end-of-file)或负数 return(读取错误)视为失败输入。

要检查您是否至少有一个有效的输入字符,您可以简单地检查return (read returns 读取的字符数,sys_read 将该值放在系统调用之后的 rax 中)。表示未收到输入的零值或负值。支票可以是:

    cmp rax, 0              ; check for 0 bytes read or error
    jle error

你可以写一个简短的诊断给用户,然后根据需要处理错误,这个例子在输出诊断后简单地退出,例如

readerr db 0xa, "eof or read error", 0xa, 0x0
rderrsz equ $-readerr
...
    ; your call to read here
    cmp rax, 0              ; check for 0 bytes read or error
    jle error
    ...
    error:
    mov rax, 1              ; output the readerr string and jmp to end
    mov rdi, 1
    mov rsi, readerr
    mov rdx, rderrsz
    syscall
    
    jmp end

现在转向更有效的清空方式 stdin。原始答案中指出的最大障碍是重复系统调用 sys_read 一次读取一个字符,重复使用 2 字节 choice 缓冲区。显而易见的解决方案是让 choice 更大,或者只是使用堆栈 space 每次读取更多字符。 (您可以查看一些方法的评论)在这里,例如,我们将 choice 增加到 128 字节,在预期 "y\n" 输入的情况下,将只使用其中的两个字节,但在输入过长的情况下,将一次读取 128 个字节,直到找到 '\n'。对于设置,您有:

choicesz equ 128
...
section .bss
text resb 50
choice resb 128

现在,在您请求 (y/n)? 之后,您的阅读将是:

    mov rax, 0              ; Getting input and saving it on var choice
    mov rdi, 0
    mov rsi, choice
    mov rdx, choicesz
    syscall

    cmp rax, 0              ; check for 0 bytes read (eof) or error
    jle error
    
    cmp byte [choice], 'y'  ; check 1st byte of choice, no need for r8b
    jne end                 ; not a 'y', doesn't matter what's in stdin, end

现在有两个条件需要检查。首先,将读取的字符数与您的缓冲区大小 choicesz 进行比较,如果读取的字符数小于 choicesz,则 stdin 中没有未读字符。其次,如果 return 等于缓冲区大小,您可能会或可能不会在 stdin 中保留字符。您需要检查缓冲区中的最后一个字符,看它是否是 '\n' 以指示您是否已读取所有输入。如果最后一个字符不是 '\n' 字符保持未读(除非用户恰好在第 128 个字符处生成手册 end-of-file)您可以检查为:

    empty:
    cmp eax, choicesz       ; compare chars read and buffer size
    jb _start               ; buffer not full - nothing remains in stdin
    
    cmp byte [choice + choicesz - 1], 0xa   ; if full - check if last byte \n, done
    je _start
    
    mov rax, 0              ; fill choice again from stdin and repeat checks
    mov rdi, 0
    mov rsi, choice
    mov rdx, choicesz
    syscall
    
    cmp rax, 0              ; check for 0 bytes read or error
    jle error

    jmp empty

(注意: 如上所述,还有一种情况需要涵盖,此处未涵盖,例如用户输入有效输入,但随后生成手动 end-of-file 而不是仅在第 128 个字符(或 128 的倍数)后按 Enter。在那里你不能只查找 '\n' 它不存在,如果没有更多的字符并再次调用 sys_read,它将阻止输入。可以想象你将需要使用 non-blocking 读取和回放一个单个字符来打破这种歧义——留给你)

一个完整的改进示例是:

prompt db "Type your text here. ", 0x0
plen equ $-prompt
retry db "Try again (y/n)? ", 0x0
rlen equ $-retry
textsz equ 50
choicesz equ 128
readerr db 0xa, "eof or read error", 0xa, 0x0
rderrsz equ $-readerr

section .bss
text resb 50
choice resb 128

section .text
    global _start

_start:

    mov rax, 1              ; Just asking the user to enter input
    mov rdi, 1
    mov rsi, prompt
    mov rdx, plen
    syscall

    mov rax, 0              ; Getting input and saving it on var text
    mov rdi, 0
    mov rsi, text
    mov rdx, textsz
    syscall
    
    cmp rax, 0              ; check for 0 bytes read or error
    jle error

    mov r8, rax

    mov rax, 1              ; Printing the user input
    mov rdi, 1
    mov rsi, text
    mov rdx, r8
    syscall

    mov rax, 1              ; Asking if user wants to try again
    mov rdi, 1
    mov rsi, retry
    mov rdx, rlen
    syscall

    mov rax, 0              ; Getting input and saving it on var choice
    mov rdi, 0
    mov rsi, choice
    mov rdx, choicesz
    syscall

    cmp rax, 0              ; check for 0 bytes read (eof) or error
    jle error

    cmp byte [choice], 'y'  ; check 1st byte of choice, no need for r8b
    jne end                 ; not a 'y', doesn't matter what's in stdin, end
    
    empty:
    cmp eax, choicesz       ; compare chars read and buffer size
    jb _start               ; buffer not full - nothing remains in stdin
    
    cmp byte [choice + choicesz - 1], 0xa   ; if full - check if last byte \n, done
    je _start
    
    mov rax, 0              ; fill choice again from stdin and repeat checks
    mov rdi, 0
    mov rsi, choice
    mov rdx, choicesz
    syscall
    
    cmp rax, 0              ; check for 0 bytes read or error
    jle error

    jmp empty
    
    end:
    mov rax, 60      
    mov rdi, 0       
    syscall
    
    error:
    mov rax, 1              ; output the readerr string and jmp to end
    mov rdi, 1
    mov rsi, readerr
    mov rdx, rderrsz
    syscall
    
    jmp end

肯定有更有效的方法来优化它,但出于讨论“我如何清空 stdin?”的目的,使用缓冲区大小的第二种方法减少了对 [=33 的重复调用=] 读取一个字符 at-a-time 是向前迈出的一大步。 “它如何完全优化检查?”是一个完全不同的问题。

如果您还有其他问题,请告诉我。

脚注:

1.在在用户键入输入的这种情况下,用户通过按 Enter 生成 '\n',允许您检查 '\n' 作为清空 stdin。用户还可以通过按 Ctrl+d 生成手册 end-of-file,因此无法保证 '\n'。还有许多其他方法 stdin 可以填充,例如将文件重定向为输入,其中应该有一个结尾 '\n' 是 POSIX 兼容的,这也不能保证.