5 Commits

4 changed files with 199 additions and 53 deletions

View File

@ -100,6 +100,24 @@ void set_input_mode()
}
```
**Разберем некоторые строки подробнее**
`isatty(STDIN_FILENO)` - в целом `isatty` просто проверяет, является ли дескриптор файла консолью в общем смысле этого слова. Вообще в линуксе `tty` означает teletype - консоли, которые могут использоваться для выполнения команд, восстановления системы и прочего. В некоторых дистрибутивах между ними можно даже переключаться, но так как я на wsl, мне эта роскошь не доступна
`fprintf` - функция, позволяющая делать "форматированный вывод" в поток дескриптора. То есть это как `prinf`, только еще и дескрипторы принимает
`EXIT_FAILURE` и `EXIT_SUCCESS` - обозначают 1 и 0 соответственно. Используются чтобы избежать неявной договоренности между пользователями линукс, что при возвращении нуля из функции - это успех, а другого - ошибка
Очевидным образом если `tcsetattr` устанавливал параметры терминала, то `tcgetattr` должен их получать. В качестве второго параметра принимает указатель на структуру данных, где должен их сохранить. В нашем случае ей выступает упоминавшаяся выше глобальная переменная
После получения параметров мы начинаем c ними играться в флагах. В данном случае я вырубаю `ICANON` тем самым говоря, что вводить `enter` при вводе команд не обязательно, а также рублю флаг `ECHO`, из-за чего набираемые с клавиатуры символы не отображаются
`VMIN` говорит о том, что одного символа в буфере достаточно, чтобы считать, что пользователь ввел все, что хотел. Есть еще параметр `VTIME`, который говорит, что если буфер не меняется какое-то время, то пользователь закончил
Остальное нам вроде бы знакомо =)
<!---Тут следует следить за обновлениями кода в substitutions.py, а то неактуальный код - большая беда-->
## Исправление ассемблерных вставок
Должен сказать, что я не большой поклонник "inline assembly". На мой субъективный взгляд намного лучше, читаемее и стабильнее добавлять ассемблер на этапе линковки. Это дает несколько приятных бонусов:
@ -118,3 +136,11 @@ void set_input_mode()
- Компилятору надо понимать, что будет происходить с переменными и регистрами во время ассемблерной вставки, поэтому и это тоже придется указать отдельно
Собственно видно, что есть ньансы, которые и заставляют меня сделать выбор в пользу обычного ассемблера и линковки, но раз лаба хочет, чтобы использовался именно inline, то будем использовать inline
UPD 12.09.24 22:00: в самый последний момент преподаватель решил в своей методички пингануть адрес в памяти, который в ms-dos отведен для хранени данных BIOS, а конкретнее ту часть, которая отведена под системные часы насколько я понимаю. В случае DOS это вполне себе реальная память, которая вполне себе реально существует более того, в досе процессор находится в режиме реальных адресов. Linux в свою очередь относится к приколам с обращением к произвольному участку памяти как к уязвимостям, поэтому не дает просто почитать или пописать в непромапаную память. Но это пол беды на самом-то деле, ведь вся память у любой программы виртуальная и уже на уровне операционной системы и процессора перегонятся в виртуальную, поэтому даже если я воспользуюсь `mmap` и промапаю соответствующий адрес в памяти, в нем будет просто лежать мусор и не более. Поэтому последнюю часть работы, где достается время из памяти BIOS я пропускаю за невозможностью ее выполнить на машине на базе Linux
## Замеры времени ассемблерной команды
Мне лабораторную работу зачли и без этой части, но если кто-то будет сдавать ему лабу так же как и я, такое возможно не проканает, поэтому для решения задачи замера приложен файлик time.asm (по крайней мере должен быть, если я не забыл). В нем в комментариях я постарался пояснить все этапы замера времени. Но для базового понимания придется сделать некоторые пояснения относительно организационных решений. В методичке преподаватели предпочли обратиться к чтению из промапаной области памяти BIOS, так называемой BIOS data area. Неплохой вариант, если есть прямой доступ к памяти устройства, поскольку BIOS хранит в памяти довольно много полезных данных. Однако из пользовательских программ простучать эту память не получается, потому что linux будет выдавать ошибку даже если такие программы запускать под рутом.
Однако как же тогда получать время и делать другие манипуляции? Довольно просто на самом деле - системными вызовами. Системный вызов - это программное прерывание, которое просит операционную систему в режиме ядра выполнить какую-то работу: получить время, установить время, поменять разрешения на порты, сменить режимы терминала и очень многое другое вплоть до создания X-server'а. Найти системные вызовы можно прогуглив `linux syscalls table`. От себя порекомендую этот [сайт](https://syscalls.mebeim.net/?table=x86/64/x64/latest). Они вытаскивают системные вызовы из каждой версии ядра. Также возможный, пусть и требующий значительно больше знаний вариант - посмотреть стандартную библиотеку вашего компилятора C. Дело в том, что сам язык C довольно мал и весь его огромный функционал завязан на не таком уж и большом количестве зарезервированных слов и конструкций. В целом сопоставление между C и ассемблером, если компилировать без оптимизаций компилятора (`-O0`), выходит довольно однозначное. И эта же особенность заставляет постоянно переписывать те библиотеки в C, которые зависят от системы или архитектуры. **Не мудрено, что и системные вызовы тоже хранятся где-то в заголовках**. Однако я тут ничего не подскажу, так как так и не понял, где эти номера нормально записаны. Если вы знаете - пишите.

View File

@ -46,12 +46,21 @@ int main(void) {
set_input_mode();
while (isKeyPressed() == 0) {
asm {
push ax
in al,0x40
}
unsigned char Tmm = _AL;
asm pop ax
// asm {
// push ax
// in al,0x40
// }
unsigned char Tmm = 0;
asm (
"push rax\n\t"
"in al, 0x40"
"mov %0, al"
"pop rax"
:"=r"(Tmm)
:
:"rax"
);
delay(500);
printf("\n Порт40 = %d", Tmm);
}
@ -69,46 +78,58 @@ int main(void) {
reset_input_mode();
system("pause");
int Time;
set_input_mode();
while (isKeyPressed() == 0) {
asm push ds
asm push si
asm mov ax, 40h
asm mov ds, ax
asm mov si, 0x6C
asm mov ax, [ds : si]
asm mov Time, ax
asm pop si
asm pop ds
// Данная секция закомментирована, поскльку линукс не дает обратиться к
// не промапанной и не аллоцированной памяти. Но даже если ее аллоцировать
// mmem'ом, все равно эта память будет виртуальная, поэтому смысла делать
// это не имеет. Вариант просмотра содержимого условной ячейки памяти на nasm
// приведен в файле time.asm. Объяснить тот код, который я вижу
// на базовом уровне я в состоянии
printf("\n %d", Time);
delay(300);
}
reset_input_mode();
beep(400, 200);
for (lCnt = 0; lCnt < 1000000; lCnt++) {
a1:
asm {
mov ax,iA
mov ax,iA
mov ax,iA
mov ax,iA
mov ax,iA
mov ax,iA
mov ax,iA
mov ax,iA
mov ax,iA
a2:
mov ax,iA
}
}
beep(400, 200);
}
//void beep(unsigned iTone, unsigned iDlit) {
// sound(iTone);
// delay(iDlit);
// nosound();
// int Time;
// set_input_mode();
// while (isKeyPressed() == 0) {
// Здесь происходит операция получения времени суток при
// помощи обращения к специально размеченой области памяти
// Однако можно ли такой фокус сделать в linux это еще надо узнать
// asm push ds
// asm push si
// asm mov ax, 40h
// asm mov ds, ax
// asm mov si, 0x6C
// asm mov ax, [ds : si]
// asm mov Time, ax
// asm pop si
// asm pop ds
// asm(
// "mov "
// );
//
// printf("\n %d", Time);
// delay(300);
// }
// reset_input_mode();
//
// beep(400, 200);
// for (lCnt = 0; lCnt < 1000000; lCnt++) {
// a1:
// asm {
// mov ax,iA
// mov ax,iA
// mov ax,iA
// mov ax,iA
// mov ax,iA
// mov ax,iA
// mov ax,iA
// mov ax,iA
// mov ax,iA
// a2:
// mov ax,iA
// }
// }
// beep(400, 200);
// здесь секция для выполнения замеров времени. Поскольку доступ к звуку
// Я иметь не могу, если не буду использовать pulseaudio, замерим старыми дедовскими методами
// При помощи clock_gettime
}

View File

@ -3,6 +3,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <time.h>
/* Use this variable to remember original terminal attributes. */
@ -32,7 +33,8 @@ void set_input_mode()
/* Set the funny terminal modes. */
tcgetattr (STDIN_FILENO, &tattr);
tattr.c_lflag &= ~(ICANON|ECHO); /* Clear ICANON and ECHO. */
tattr.c_cc[VMIN] = 1;
tattr.c_cc[VMIN] = 0;
tattr.c_cc[VTIME] = 0;
tcsetattr (STDIN_FILENO, TCSAFLUSH, &tattr);
}
@ -52,10 +54,10 @@ char isKeyPressed()
return 0;
}
//int main()
//{
// set_input_mode();
// while (isKeyPressed() == 0) {}
// printf("ok");
// reset_input_mode();
//}
int main()
{
set_input_mode();
while (isKeyPressed() == 0) {printf("hell\n");}
printf("ok\n");
reset_input_mode();
}

97
01-asm-basics/time.asm Normal file
View File

@ -0,0 +1,97 @@
; Эта директива делает функцию видимой.
; По умолчанию в ассемблере используется _start,
; но поскольку для вывода на экран я пользуюсь
; С'шной функцией prinf, для корректного подключения библиотек на этапе линковки
global main
; Объявляю, что буду ссылаться на метку printf, которой нет внутри кода программы
; extern вообще обзначает, что метка объявлена где-то еще
extern printf
; тут объявлен макрос CLOCK_REALTIME, который на этапе ассемблирования заменится на число 0
; Использован он тут, так как является clock_id, о котором будет сказано позже. И я не уверен
; что на всех системах это число будет одинаково. Свое я посмотрел в файлах компилятора.
%define CLOCK_REALTIME 0
; так в ассемблере задаются структуры. Существуют они лишь на уровне препроцессора
; да и применение их весьма специфично. Но подробнее лучше погуглите
; struct timespec { time_t tv_sec; long tv_nsec; } - это шаблон из C
struc timespec
.tv_sec: resq 1
.tv_nsec: resq 1
endstruc
section .note.GNU-stack ; чтобы не жаловался линкер
; Секция с данными, ее особенность в том, что нужно указать лишь сколько нужно зарезервировать
section .bss
; вообще можно было бы использовать istruc и создать эти 2 структуры в .data, но я решил,
; что не хочу тратить время на инициализацию того, что и так будет перезаписано
; обе эти инструкции просто нужны чтобы застолбить по 16 памяти на каждый замер времени
; потому что time_t и long имеют размер 8 байт, а поля 2
start:
resq 2
finish:
resq 2
; Секция с данными, которые заранее заполняются чем-то
section .data
fstring db "Operations took %ul seconds and %ul nanoseconds", 10, 0 ; строки стиля C должны оканчиваться нулем
flen equ $-fstring ; длина строки. $ - это текущий адрес. Подробнее не буду рассказывать - мне лень
section .text
main: ; лично в моей системе time_t представляет из себя long int
mov rax, 228 ; Системный вызов получения времени
mov rdi, CLOCK_REALTIME
mov rsi, start
syscall
; здесь место для кода под замер времени
mov rcx, 20000 ; сколько раз нужно прогнать цикл
; цикл
looper:
mov rax, start
loop looper ; про это чуть позже узнаете
; замеряем время второй раз
mov rax, 228
mov rdi, CLOCK_REALTIME
mov rsi, finish
syscall
; считаем время для секунда и миллисекунд
; секунды
mov rsi, [finish + timespec.tv_sec]
sub rsi, [start + timespec.tv_sec]
; наносекунды
mov rdx, [finish + timespec.tv_nsec]
sub rdx, [start + timespec.tv_nsec]
; вызываем функцию printf. Согласно соглашению о вызовах fastcall
; при вызове функций для передачи аргументов используются регистры по порядку следования аргументов
; rdi, rsi, rdx, rcx, r8, r9, а остальные пушатся в ассемблер.
; Свои заморочки там с числами с плавающей точкой, но об этом не сейчас
mov rdi, fstring
mov rax, 0
; Вот тут все во имя выравнивания стека. Об этом я сейчас рассказывать не буду, только если попросят в readme чиркану
sub rsp, 8
; собственно вызов функции. На самом деле это обычный jmp, который предварительно пушит в стек адрес возврата.
; в будущем будьте аккуратнее с этими приколами, потому что при встрече ключевого слова ret ассемблер всегда.
; подчеркиваю ВСЕГДА прочитает 8 байт со стека и передаст туда управление. И как бы что там будет - одному богу ведомо
; Так что в ваших же интересах следить за тем, чтобы в стеке лежали правильные байты
call printf
; поскольку выравнивание больше не нужно, возвращаем стек в исходное состояние
add rsp, 8
exit:
; Тут происходит системный вызов выхода из приложения. Если его не увидит
; linux, то он решит, что программа завершилась аварийно
mov rax, 60
mov rdi, 0 ; код ошибки. если вернется 0 - считается, что ошибок не произошло
syscall