10 Commits

Author SHA1 Message Date
8fc6112f58 docs: пояснил по поводу Makefile в README 2024-09-24 12:17:40 +03:00
23b82657d4 docs: внес маленькое пояснение в README 2024-09-24 12:11:54 +03:00
78a4638401 fix: исправлено форматирование вывода 2024-09-24 12:06:11 +03:00
e1aeb3f2df feat: сделан первый прототип решения
Прототип еще не тестировался
2024-09-23 20:45:46 +03:00
ed21dfe07a fix: исправил работу с циклами 2024-09-23 18:49:23 +03:00
6c1198df73 sync: прописал часть логики для 4 пункта 2024-09-22 23:57:16 +03:00
58897fedb7 feat: добавлена процедура pollin 2024-09-22 22:07:18 +03:00
51e220410a feat: добавлено несколько полезных функций для решения 4 пункта 2024-09-22 17:55:49 +03:00
49d3c02d7d feat: task2 создан
Решен второй пункт в виде ассемблерного кода, однако он отформатирован вполне определенным образом
2024-09-22 15:27:21 +03:00
daec968bcb feat(utils): для чуть большего удоства написал Makefile
По факту он просто линкует одиночный файл ассемблерного кода и не подшивает никаких зависимостей. Весьма ситуативная фигня, но немного экономит время
2024-09-22 13:18:59 +03:00
8 changed files with 323 additions and 300 deletions

View File

@ -2,119 +2,3 @@
## Введение в низкоуровневое программирование. Встроенный отладчик. Встроенный Ассемблер
## Переписываем шаблон
Поскольку весь шаблонный текст написан под MS-DOS, мы очевидным образом не можем его использовать для решения задачи под linux.
Замены требуют следующие функции:
- getch
- delay
- inp
- bioskey
### getch
Наиболее простая замена будет для `getch()`, поскольку единственное ее назначение - ожидать нажатия клавиши. В этом контексте у линукса есть полноценная замена в виде `system("pause")`
### delay
Здесь уже несколько посложнее, потому что DOS'овский `delay` использует задержку в миллисекундах, а линуксовый `sleep` - в секундах. Поэтому используем функцию `usleep`. Она принимает время задержки в микросекундах, поэтому для получения миллисекунда нужно просто умножить на 1000. То есть код:
```C
void delay(unsigned ms)
{
usleep(ms * 1000);
}
```
### bioskey
Из всех пока что самая сложная замена. Если вызвать `bioskey(1)`, то она вытаст 1 если какая либо клавиша была нажата и 0 если не была. при этом проверка происходит в моменте и не блокирует выполнение программы.
Для иммитации этого на линуксе нам потребуется неканонический режим ввода в терминал, а также сделать так, чтобы все печатаемое не выводилось в курсор. Этого можно добиться 2 способами:
1. Покурить гигагалактический томик по ассемблеру и узнать про системный вызов ioctl, после чего руками разметить область оперативной памяти, провести все системные вызовы, потом при помощи poll проверять наличие символов в буфере, обрабатывать ошибки и интегрировать функции через прототипы в наш код на C
2. Сдаться и выбрать путь языка C
Я уже сказал, что я из слабых, поэтому писать кусок на ассемблере как-то не горю желанием (хотя может когда-нибудь в будущем по просьбам напишу)
#### Зависимости
Язык программирования C имеет определенный уровень абстракции от конкретных системных вызовов и предоставляет нам несколько вещей:
- `<termios.h>` - структура данных, хранящая информацию о текущем состоянии терминала, а также удобные методы `tcgetattr` и `tcsetattr`
- `<unistd.h>` - Библиотека, используемая для унификации дескрипторов, битов и прочих унификаций
- `<stdlib.h>` - много чего, но нам для безопасности потребуется `atexit`, чтобы если что-то пошло не так, у нас не наебнулся терминал
Опционально берется `<stdio.h>` для целей адекватного вывода ошибок. Не обязательно, но предпочтительно
#### Реализация
Для начала нам необходимо сохранить свой текущий терминал, чтобы без проблем его восстановить в будущем, для этого заводим в памяти переменную (придется сделать ее глобальной, потому что на инкапсуляцию и защиту нет времени, нервов и желания)
```C
struct termios saved_attributes;
```
Далее сразу напишем функцию для восстановления
```C
void reset_input_mode()
{
tcsetattr (STDIN_FILENO, TCSANOW, &saved_attributes);
}
```
Здесь `STDIN_FILENO` - это дескриптор потока стандартного ввода (ввод с консоли по простяге). Вообще это число, но в `<unistd.h>` он вынесен в макрос для хоть какой-то унификации, `TCSANOW` - тоже число. В контексте функции `tcsetattr` оно заставляет изменениям в формате терминала вступить в силу немедленно, вне зависимости от того, есть ли еще в буфере текст на вывод. Другими вариантами могут стать:
- `TCSANOW` - применить изменения сразу при сигнале и продолжать предыдущий вывод с того же места, где он кончился
- `TCSADRAIN` - заставит сначала очистить текущий буфер вывода до дна, а только потом сменит режим. То есть сначала все, что было на момент запроса в буфере, будет выведено, а только потом сменится режим терминала
- `TCSAFLUSH` - то же, что и `TCSADRAIN`, только еще и сносит весь буффер ввода
```C
void set_input_mode()
{
struct termios tattr;
char *name;
// Убеждаемся, что STDIN - это терминал
if (!isatty (STDIN_FILENO))
{
fprintf (stderr, "Not a terminal.\n");
exit (EXIT_FAILURE);
}
// Сохраняем параметры текущего терминала
//для последующего восстановления
tcgetattr (STDIN_FILENO, &saved_attributes);
atexit (reset_input_mode);
// Устанавливаем все режимы, которые
// нас в общем-то интересуют
tcgetattr (STDIN_FILENO, &tattr);
tattr.c_lflag &= ~(ICANON|ECHO); /* Clear ICANON and ECHO. */
tattr.c_cc[VMIN] = 0;
tattr.c_cc[VTIME] = 0;
tcsetattr (STDIN_FILENO, TCSAFLUSH, &tattr);
}
```
## Исправление ассемблерных вставок
Должен сказать, что я не большой поклонник "inline assembly". На мой субъективный взгляд намного лучше, читаемее и стабильнее добавлять ассемблер на этапе линковки. Это дает несколько приятных бонусов:
1. Код можно поддерживать на любимом ассемблере
2. Код ассемблера можно компилить отдельно
3. Код программы на C становится ощутимо чище (*лично на мой взгляд ассемблерные вставки плохо смотрятся в коде*), а также все макросы ассемблера не касаются кода на C
4. Меньше потенциальных ошибок из-за того, что вы что-то не так поняли и откомпилировалось все неправильно
Помимо прочего очень важный момент: я использую gcc для компиляции, а в отличие от clang, он довольно ленивый и наши строки для ассемблера в нетронутом виде отправятся прямо в текст программы, которая затем будет скормлена ассемблеру. Отсюда следует несколько нюансов:
- Стандартный ассемблер, используемый `gcc` - `as` и по умолчанию он использует синтаксис AT&T. Однако я не очень люблю этот синтаксис, предпочитаю работать с синтаксисом intel. Выхода тут 2:
- Дать компилятору флаг -masm=intel, после чего уже собственный ассемблер переключится на intel синтаксис
- В начале каждой ассемблерной вставки ставить ".intel_syntax noprefix", а после вставки но перед параметрами ставить ".att_syntax prefix". Это может периодически плохо работать
- При написании ассемблера необходимо соблюдать все переносы строк и при этом указывать это явно (поэтому в конце строк у меня и появляются `\n\t` - это поддержание табуляции и переноса строки
- Компилятору надо понимать, что будет происходить с переменными и регистрами во время ассемблерной вставки, поэтому и это тоже придется указать отдельно
Собственно видно, что есть ньансы, которые и заставляют меня сделать выбор в пользу обычного ассемблера и линковки, но раз лаба хочет, чтобы использовался именно inline, то будем использовать inline

View File

@ -1,114 +0,0 @@
#include <stdio.h>
#include <sys/io.h>
#include <stdlib.h>
#include <unistd.h>
#include "substitutions.h"
#define PortCan0 0x40
void beep(unsigned iTone, unsigned iDlit);
void delay(unsigned int ms)
{
usleep(ms * 1000);
}
int main(void) {
long int lCnt = 0;
int iA = 0x1234;
char *pT = (char *)0x46C;
printf("\nПечатаем 10 раз значение байта с известным адресом\n");
for (int i = 0; i < 10; i++)
{
printf(" \n %d ", *pT);
}
printf("\n Для продолжения нажмите любую клавишу \n");
system("pause"); // Ждем нажатия клавиши
printf("\n Читаем содержимое порта с адресом 40 с помощью функции Си \n");
printf("\n Для выхода из цикла - нажмите любую клавишу \n");
// Линуксу не сильно нравится, что ты насилуешь порты ввода и вывода процессора, поэтому нужно выдать ему на это дело разрешение
ioperm(PortCan0, 1, 3); // Что означает тройка напишу позже
set_input_mode();
while (isKeyPressed() == 0) {
printf("\n Порт40 = %d", inb(PortCan0));
delay(500);
}
reset_input_mode();
system("pause");
printf("\n Читаем содержимое порта с адресом 40 ассемблером \n");
set_input_mode();
while (isKeyPressed() == 0) {
asm {
push ax
in al,0x40
}
unsigned char Tmm = _AL;
asm pop ax
delay(500);
printf("\n Порт40 = %d", Tmm);
}
reset_input_mode();
system("pause");
printf("\n Для продолжения - нажмите любую клавишу \n");
system("pause");
long *pTime = (long *)0x46C;
set_input_mode();
while (isKeyPressed() == 0) {
printf("\n %ld", *pTime);
delay(1000);
}
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
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();
//}

View File

@ -1,61 +0,0 @@
#include "substitutions.h"
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
/* Use this variable to remember original terminal attributes. */
struct termios saved_attributes;
void reset_input_mode()
{
tcsetattr (STDIN_FILENO, TCSANOW, &saved_attributes);
}
void set_input_mode()
{
struct termios tattr;
char *name;
/* Make sure stdin is a terminal. */
if (!isatty (STDIN_FILENO))
{
fprintf (stderr, "Not a terminal.\n");
exit (EXIT_FAILURE);
}
/* Save the terminal attributes so we can restore them later. */
tcgetattr (STDIN_FILENO, &saved_attributes);
atexit (reset_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;
tcsetattr (STDIN_FILENO, TCSAFLUSH, &tattr);
}
void delay(unsigned int ms)
{
usleep(ms * 1000);
}
char isKeyPressed()
{
char key_handler = 0;
read(STDIN_FILENO, &key_handler, 1);
if (key_handler > 0)
{
return 1;
}
return 0;
}
//int main()
//{
// set_input_mode();
// while (isKeyPressed() == 0) {}
// printf("ok");
// reset_input_mode();
//}

View File

@ -1,9 +0,0 @@
#ifndef SUBSTITUTIONS_H
#define SUBSTITUTIONS_H
void reset_input_mode();
void set_input_mode();
void delay(unsigned int ms);
char isKeyPressed();
#endif

13
03-asm-bios/Makefile Normal file
View File

@ -0,0 +1,13 @@
ASM = nasm
ASM_FLAGS = -felf64 -g
LINK = ld
%: %.o
$(LINK) -o $@ $^
%.o: %.asm
$(ASM) $(ASM_FLAGS) $^ -o $@
clean:
rm -f *.o
rm -f $(subst .asm, $(empty), $(wildcard *.asm))

View File

@ -2,3 +2,13 @@
## Ассемблер и функции BIOS
В этой работе намного проще посмотреть непосредственно решения и почитать комментарии к коду, чем читать теоретическое приложение к работе. Если вам все же что-то не понятно - кидайте в issues
Впрочем зная, что основная масса народу не будет делать эту лабу так, как сделал ее я, сюда вряд ли кто-то заглянет)
### Касаемо Makefile
Для того чтобы не писать много команд для однотипной и монотонной сборки проекта, был написан простой Makefile. Однако работает он следующим образом: он принимает название цели сборки и ищет файл с именем цели и расширением .asm. Если не находит - не собирает цель.
Важно заметить, что он не умеет линковать другие файлы в ассемблер, потому что написан был не для этого. Он просто берет голый файл на NASM (обязательно) и выдает 64-битный ELF из этого единственного файла. Если вам необходимо что-то прилинковать к ассемблеру, то увы, придется собирать проект вручную или менять этот makefile

81
03-asm-bios/task2.asm Normal file
View File

@ -0,0 +1,81 @@
global _start
%define STDIN 0
%define STDOUT 1
%define STDERR 2
section .data
src db 1, 2, 3, 4, 5, 6, 7, 8, 9, 0
src_size equ $-src
; резервируем 1 килобайт для буффера ввода и вывода
; также в отдельной переменной сохраняем размер этого буфера
print_buf: times 1024 db 0
buf_size equ $-print_buf
section .text
%macro DIGIT_TO_ASCII 1 ; макрос, принимающий один регистр
add %1, '0'
%endmacro
%macro PUSH_M 1-* ; push many; пушит в порядке следования
%rep %0
push %1
%rotate 1
%endrep
%endmacro
%macro POP_M 1-* ; pop many. читает в порядке следования
%rep %0
pop %1
%rotate 1
%endrep
%endmacro
%macro RPOP_M 1-* ; pop many. читает в обратном порядке
%rotate -1
%rep %0
pop %1
%rotate -1
%endrep
%endmacro
; Передачу аргументов будем делать при помощи ABI - стандартная практика для linux
; Аргументы передаются в следующем порядке: rdi, rsi, rdx, rcx, r8, r9. Все, что не влезло, пушится в стек
; У передачи через стек тоже есть особенности, но их мы пока касаться не будем
print_from_buf: ; word -> void
PUSH_M rax, rsi, rdx, rdi ; сохраним регистры, которые точно попортим
mov rdx, rdi ; сколько выводить, в rdi содержится единственный аргумент
mov rsi, print_buf ; откуда выводить. Адрес буфера
mov rdi, STDOUT; куда выводить. Дескриптор файла. В нашем случае стандартного вывода
mov rax, 1
syscall
RPOP_M rax, rsi, rdx, rdi ; вернем значения регистров
ret
_start:
mov rcx, src_size
mov rsi, src
mov rdi, print_buf
xor rax, rax ; обнуляем регистр
.transfer: ; в цикле передаем данные, попутно конвертируя их в ascii
lodsb
DIGIT_TO_ASCII rax
stosb
loop .transfer
mov [rdi + 1], BYTE `\n` ; Чтобы система не ругалась на отсутствие переноса
mov rdi, src_size
call print_from_buf
exit:
mov rax, 60
mov rdi, 0
syscall

219
03-asm-bios/task4.asm Normal file
View File

@ -0,0 +1,219 @@
global _start
%define STDIN 0
%define STDOUT 1
%define STDERR 2
section .data
; резервируем 1 килобайт для буффера ввода и вывода
; также в отдельной переменной сохраняем размер этого буфера
print_buf: times 1024 db 0
buf_size equ $-print_buf
input_buf: times 1024 db 0 ; буфер, в который будут читаться символы со стандартного ввода
input_size equ $-input_buf
array: times 512 dq 0 ; молимся, чтобы никому не пришло в голову писать так много
arr_size equ $-array
; Для poll
%define POLLIN 0x001 ; Есть ли что почитать с буфера ввода. Понадобится для продолжения ввода
input_pollfd: dd STDIN
dw POLLIN
revents: dw 0 ; возвращаемые события
section .text
%macro DIGIT_TO_ASCII 1 ; макрос, принимающий один аргумент (регистр или память)
add %1, '0'
%endmacro
%macro ASCII_TO_DIGIT 1 ; макрос, принимающий один аргумент (регистр или память)
sub %1, '0'
%endmacro
%macro PUSH_M 1-* ; push many; пушит в порядке следования
%rep %0
push %1
%rotate 1
%endrep
%endmacro
%macro POP_M 1-* ; pop many. читает в порядке следования
%rep %0
pop %1
%rotate 1
%endrep
%endmacro
%macro RPOP_M 1-* ; pop many. читает в обратном порядке
%rotate -1
%rep %0
pop %1
%rotate -1
%endrep
%endmacro
%macro PUSHR8 1; закинуть восьмибитный регистр в стек
dec rsp
mov [rsp], %1
%endmacro
; Передачу аргументов будем делать при помощи ABI - стандартная практика для linux
; Аргументы передаются в следующем порядке: rdi, rsi, rdx, rcx, r8, r9. Все, что не влезло, пушится в стек
; У передачи через стек тоже есть особенности, но их мы пока касаться не будем
clean_print_buf: ; none -> void
PUSH_M rax, rcx, rdi
mov rcx, buf_size
mov rdi, print_buf
xor rax, rax ; будем заносить нули во всю память
rep stosb
RPOP_M rax, rcx, rdi
ret
print_from_buf: ; qword -> void; пытается вывести данные из буфера. аргумент не может быть больше 1024
PUSH_M rax, rsi, rdx, rdi ; сохраним регистры, которые точно попортим
mov rdx, rdi ; сколько выводить, в rdi содержится единственный аргумент
mov rsi, print_buf ; откуда выводить. Адрес буфера
mov rdi, STDOUT; куда выводить. Дескриптор файла. В нашем случае стандартного вывода
mov rax, 1
push rcx
syscall
pop rcx
RPOP_M rax, rsi, rdx, rdi ; вернем значения регистров
ret
read_to_buf: ; none -> void. Пытается заполнить буфер из стандартного ввода
PUSH_M rdi, rsi, rdx
mov rdi, STDIN ; откуда читать (дескриптор файла)
mov rsi, input_buf ; куда читать
mov rdx, input_size ; Сколько пытаемся читать
mov rax, 0 ; системный вызов чтения
syscall
RPOP_M rdi, rsi, rdx ; rax содержит количество прочитанных байт, а это важно
ret
poll_stdin:
PUSH_M rdi, rsi, rdx
mov rsi, 1 ; следим только за одним потоком
mov rax, 7 ; poll syscall
mov rdi, input_pollfd
mov rsi, 1 ; одна структура данных (изначально просто вызов принимает кучу таких)
mov rdx, 0 ; не ждать
syscall
RPOP_M rdi, rsi, rdx
ret
print_number: ; qword (rdi) -> void
; наша задача - сформировать массив символов.
; Ну а раз мы не знаем точно сколько их будет, формировать его будем прямо в стеке. нам повезло, что он растет вниз
; Нам очень повезло, что он растет вниз
; создадим 2 локальные переменные - одну для размера массива, другую для делителя
push rbp
PUSH_M rdx, rdi, rsi ; сохранять регистры обязательно надо до того, как писать в стек символы
; создаем базу для адресации. Тогда первая будет на rbp - 8 - делитель, а вторая на rbp - 16 - количество
mov rbp, rsp
; [WARNING] тут надо будет сохранить регистры
push rsp ; сохраню, потому что после всей вакханалии я концов не сыщу
sub rsp, 16 ; выделяем место под 3 переменные
mov qword [rbp - 16], 10 ; пусть и жирно, но операнд обязан быть 64 разрядным для корректного деления
mov qword [rbp - 24], 0 ; счетчик
mov rax, rdi
push byte 0 ; при выводе он ориентируется на это как на конец строки
.division_loop:
xor rdx, rdx ; обнулим найденый остаток. (он просто еще и при делении принимает участие)
div qword [rbp - 16]
DIGIT_TO_ASCII dl
PUSHR8 dl ; поскольку в процессор не завезли возможность закинуть в стек 8 битный регистр, я им немного помог макросами
inc qword [rbp - 24] ; увеличиваем счетчик на единицу
test rax, rax ; делает and поразрядное с самим собой. Меня интересует, лежит ли в rax ноль
jnz .division_loop ; если в rax не ноль, то продолжаем цикл
; выводим число
mov rax, 1
mov rdi, STDOUT
mov rsi, rsp
mov rdx, [rbp-24] ; уже не надо очищать, потому что в конце я просто восстановлю как было
push rcx
syscall
pop rcx
mov rsp, [rbp - 8]
RPOP_M rdx, rdi, rsi
pop rbp
ret
_start:
mov rbp, rsp
; Создадим 2 локальные переменные для аккумулятора размером 8 байт и для математических нужд 8 байт.
; аккумулятор будет по адресу rbp - 8, а временная по rbp - 16
sub rsp, 16
; потом я не удержался и завел еще одну переменную - сколько мы успели написать в массив
sub rsp, 2 ; массив все равно размером всего 512, делать переменную больше нет смысла. rbp - 18
mov rsi, input_buf
mov rdi, array
.read_loop:
call read_to_buf ; системный вызов read вернет количество прочитаных байтов
mov rcx, rax ; сколько байтов прочиталось, столько и обработаем
; обработаем информацию
xor rax, rax ; обнулим на всякий пожарный
jmp .read_byte
.separator_occured:
dec rcx
mov rax, [rbp - 8]
stosq
xor rax, rax
inc word [rbp - 18]
mov qword [rbp - 8], 0
test rcx, rcx
jz .check_buf
.read_byte: ; цикл чтения
lodsb
; проверим, цифра ли это. Если нет, то записываем в память то, что хранилось в локальной переменной
cmp al, '0'
jl .separator_occured
cmp al, '9'
jg .separator_occured
ASCII_TO_DIGIT al ; Если цифра, то конвертируем ее из ascii
; Поскольку умножение и деление можно сделать только через регистр, придется извратиться
PUSH_M rax, rdx
mov rax, [rbp - 8]
mov qword [rbp - 16], 10
mul qword [rbp - 16]
mov [rbp - 8], rax
RPOP_M rax, rdx
add [rbp - 8], rax ; результат деления запишем в локальную переменную
loop .read_byte ; читаем буфер ввода до конца
.check_buf:
call poll_stdin
test dword [revents], POLLIN
jnz .read_loop
; Теперь выведем прочитанный массив на экран
xor rcx, rcx
mov cx, [rbp - 18]
mov rsi, array
call clean_print_buf
.output_loop:
lodsq
mov rdi, rax
call print_number
mov byte [print_buf], ' '
mov rdi, 1
call print_from_buf ; печатаем ровно 1 пробел
loop .output_loop
mov byte [print_buf], `\n`
mov rdi, 1
call print_from_buf
exit:
mov rax, 60
mov rdi, 0
syscall