Как прочитать ввод одного символа с клавиатуры с помощью nasm (сборки) под Ubuntu?

Я использую nasm под ubuntu. Кстати, мне нужно получить один символ ввода с клавиатуры пользователя (например, когда программа запрашивает у вас y / n?), Так что при нажатии клавиши и без нажатия ввода мне нужно прочитать введенный символ. Я много гуглил, но все, что я нашел, было так или иначе связано с этой строкой (int 21h), что приводит к «Ошибке сегментации». Пожалуйста, помогите мне понять, как получить один символ или как преодолеть эту ошибку сегментации.


person Roozbeh Sharafi    schedule 22.07.2010    source источник


Ответы (3)


Это можно сделать из сборки, но это непросто. Вы не можете использовать int 21h, это системный вызов DOS, и он недоступен в Linux.

Чтобы получить символы из терминала под UNIX-подобными операционными системами (такими как Linux), вы читаете из STDIN (номер файла 0). Обычно системный вызов read блокируется до тех пор, пока пользователь не нажмет Enter. Это называется каноническим режимом. Чтобы прочитать один символ, не дожидаясь, пока пользователь нажмет Enter, вы должны сначала отключить канонический режим. Конечно, вам придется повторно включить его, если вы захотите использовать линейный ввод позже, и до того, как ваша программа завершит работу.

Чтобы отключить канонический режим в Linux, вы отправляете IOCTL (IO ControL) на STDIN, используя системный вызов ioctl. Я предполагаю, что вы знаете, как делать системные вызовы 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, чтобы узнать, как удобно определить и зарезервировать пространство для подобных структур.

Итак, как только вы получили текущие термины, вам нужно очистить канонический флаг. Этот флаг находится в поле c_lflag с маской ICANON (0x00000002). Чтобы очистить его, вычислите c_lflag И (НЕ ICANON). и сохраните результат обратно в поле c_lflag.

Теперь вам нужно уведомить ядро ​​о ваших изменениях в структуре termios. Используйте ioctl TCSETS (номер 0x5402), в третьем параметре задайте адрес вашей структуры termios.

Если все пойдет хорошо, терминал теперь находится в неканоническом режиме. Вы можете восстановить канонический режим, установив канонический флаг (используя ИЛИ c_lflag с ICANON) и снова вызвав TCSETS ioctl. всегда восстанавливать канонический режим перед выходом

Как я уже сказал, это непросто.

person Callum    schedule 01.08.2010

Мне нужно было сделать это недавно, и меня вдохновил отличный ответ, я написал следующее (NASM для x86-64):

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 в 64-битном код? - это сломается в исполняемом файле PIE (где статические адреса не находятся в младших 32 битах) или с буфером termios в стеке. Это действительно работает в традиционном исполняемом файле, отличном от PIE, и эта версия может быть легко перенесена в 32-битный режим.)

Затем вы можете сделать:

call canonical_off

Если вы читаете строку текста, вы, вероятно, также захотите сделать:

call echo_off

чтобы каждый символ не повторялся при вводе.

Возможно, есть лучшие способы сделать это, но у меня это работает на 64-битной установке Fedora.

Дополнительную информацию можно найти на справочной странице для termios(3) или в termbits.h источник.

person Richard Fearn    schedule 30.10.2010
comment
Что произойдет, если вы используете 32-битный int 0x80 Linux ABI в 64-битном коде? - не делайте этого, это будет разрыв с адресами за пределами младших 32, например пространство стека. Конечно, так же будет и mov edx, termios - это работает только в исполняемом файле, отличном от PIE, на x86-64 Linux, в противном случае вам нужен lea rdx, [rel termios]. (См. Как загрузить адрес функции или метку в регистр в GNU Assembler, который также охватывает NASM) - person Peter Cordes; 17.06.2020

Простой способ: для программы в текстовом режиме используйте libncurses для доступа к клавиатуре. ; для графической программы используйте Gtk+.

Трудный путь: предполагая, что программа работает в текстовом режиме, вы должны сообщить ядру, что вам нужен односимвольный ввод, а затем вам придется выполнить массу операций по учету и декодированию. Это действительно сложно. Нет эквивалента старой доброй подпрограмме DOS getch(). Вы можете начать изучение того, как это сделать, здесь: Терминальный ввод/вывод. Графические программы еще сложнее; API самого низкого уровня для этого — Xlib.

В любом случае, вы сойдете с ума, кодируя что бы это ни было на ассемблере; вместо этого используйте C.

person zwol    schedule 22.07.2010
comment
Хотя все, что вы сказали, верно для C, на самом деле это неуместный ответ, если ОП пытается изучить ассемблер. - person Tyler McHenry; 22.07.2010
comment
Это потому, что OP не должен программировать на ассемблере. Единственная веская причина для ручного кодирования чего-либо на языке ассемблера — если это критически важная для производительности вычислительная подпрограмма или одна из очень немногих низкоуровневых частей ядра операционной системы, которые не могут быть закодировано любым другим способом. Взаимодействие с пользователем не соответствует требованиям. То, что пытается сделать OP, даже не является хорошим учебным упражнением в Unix. - person zwol; 22.07.2010
comment
Сказав это, ничто не мешает ОП писать язык ассемблера, который вызывает libncurses, хотя мне это кажется пустой тратой времени и здравого смысла (но это было бы не так плохо, как язык ассемблера, который делает терминал Unix I/ О вручную). - person zwol; 22.07.2010
comment
Я бы не назвал это хорошим упражнением для обучения программированию Unix или ассемблеру, но это, безусловно, хорошее упражнение для обучения. Хотя сборка не является практичным выбором для подавляющего большинства приложений, я не думаю, что изучение того, как делать то, что вы обычно не делаете на ассемблере, является пустой тратой времени. Это дает вам интересное представление о том, что возможно на каких уровнях абстракции, и о том, как ваша машина и ОС работают вместе. - person jdd; 01.08.2010