Пример резидентной программы на ассемблере

Глава 10. Программирование в DOS рабатывающая системный вызов DOS, сама выполнит системный вызов, то ОС рухнет. Существуют, правда, способы обойти это ограничение, но они слишком сложны для того, чтобы рассматривать их в этой книге. Напишем небольшую резидентную программу, которая активизируется при нажатии клавиши Scroll Lock.

Программа изменяет цвет всех символов, ото­ браженных на экране, на красный. До сих пор мы старались избегать прямого взаимодействия с периферийными устройствами и использовали функции операционной системы. В случае с резидентной программой использовать DOS мы больше не можем, поэтому ничего другого нам не остается — мы должны взаимодействовать с устрой­ ствами напрямую.

Примите пока как данность, что из порта 0x60 можно получить так называе­ мый скан-код нажатой клавиши, то есть не ASCII-код посылаемого ею символа, а просто число, указываюгцее ее положение на клавиатуре. Скан-код клавиши Scroll Lock равен 0x46.

Каждое нажатие клавиши генерирует аппаратное прерывание IRQ1, которое принимает скан-код от клавиатуры, преобразовывает его в ASCII-код и ста­ вит в очередь непрочитанных клавиш. Эта очередь доступна операционной системе и прикладным программам через вызовы BIOS.

Пишем тетрис на ассемблере под DOS (x86)

Чтобы активизировать программу, мы должны перехватить прерывание IRQ 1 (int 0x9), то есть сделать нашу программу его обработчиком. Затем мы будем в цикле читать скан-код из порта 0x60. Если нажатая клавиша — не Scroll Lock, то мы вернем ее скан-код первоначальному обработчику прерывания.

В противном случае мы выполним намеченное действие (сменим цвет символов) и вернем управление первоначальному обработчику прерывания. Выход из прерывания осуш;ествляется по команде iret. Во время обработки прерывания мы должны сохранить контекст выполняемой программы, то есть сохранить все регистры «внутри» нашей подпрограммы.

Если этого не сделать, последствия предугадать будет сложно, но в большин­ стве случаев крах выполняемой программы будет обеспечен. Первым делом напишем подпрограмму color, изменяющую цвет всех симво­ лов на красный. Организация видеопамяти была рассмотрена (хоть и очень кратко) в начале этой главы.

Символы, выведенные на экран, хранятся начиная с адреса сегмента 0хВ800 в формате символщвет. Наша задача — модифици­ ровать значения байтов, находящихся по нечетным адресам: 0хВ800:0х0001, 0хВ800:0х0003, 0хВ800:0х0005 и т.д. — это и есть цвет символов. Не вдаваясь в подробности, скажем, что красному цвету соответствует значение 0x04. Подпрограмма заполняет 80×25 байтов памяти, что соответствует стандарт­ ному разрешению текстового режима.

Ассемблер на примерах. Базовый курс
color: ;сохраняем в стеке значения
push ах
push СХ /регистров, которые
;будем использовать
push si
push es /сбрасываем SI
xor si,si
mov ax, OxBSOO ;загружаем адрес сегмента в АХ
mov es,ax ;и в сегментный регистр
mov ex,80*25 /количество повторений
.repeat: /увеличиваем SI на 1
inc si
mov byte [es:si],0x4 /записываем цвет 0x04 — красный
inc si /увеличиваем на 1
dec ex /уменьшаем СХ на 1
jnz .repeat /переходим к .repeat, пока СХ > О
pop es /восстанавливаем все регистры
pop si
pop ex
pop ax
ret

Мы можем протестировать нашу подпрограмму. Допишем к ней заголовок секции кода и завершим обычную, не резидентную, программу системным вызовом 0х4С:

Как я стал программистом на ассемблере / #itstory2019

SECTION . t e x t
c a l l c o l o r
mov ax, 0x4c00
i n t 0x21

c o l o r : Теперь напишем обработчик прерывания IRQ 1:
new_handler:

push ax
in a l , 0x60
cmp a l , 0x4 6
jnz pass_on
c a l l c o l o r
pass _ on:
pop ax
jmp far [ c s : o l d _ v e c t o r ]

/сохраняем значение AX /читаем скан-код клавиши /сравниваем с 0x4 6 (Scroll — Lock) /если нет, переходим к pass_on /вызываем подпрограмму восстанавливаем значение АХ передаем управление первоначальному обработчику

Переменная old_vector должна содержать адрес первоначального обработ­ чика прерывания (старое значение вектора прерывания вектора). В качестве

Глава 10. Программирование в DOS сегментного регистра мы используем CS, потому что другие сегментные реги­ стры в ходе обработки прерывания могут получить произвольные значения. Сохраним значение старого вектора прерывания в переменную old_vector, а указатель на наш обработчик поместим вместо него в таблицу прерываний. Назовем эту процедуру setup.

setup: cli xor ax, ax moV e s , ax mov ax,new_handler xchg ax,[es:0x9*4] mov [ds:old_vector],ax mov ax,cs xchg ax,[es:0x9*4+2] mov [ds:old_vector+2],ax sti ret

;отключаем прерывания ;сбрасываем AX ;таблица прерываний находится ;в сегменте О ;с метки new_handler начинается ;новый обработчик прерывания ;вычисляем адрес старого вектора ;и меняем местами со значением АХ. ;Теперь в таблице смещение нового /обработчика, в АХ — старого ;сохраняем старый адрес в /переменной old_vector /адрес текущего сегмента — CS /пишем в таблицу сегмент нового /обработчика, /в АХ загружаем сегмент старого /сохраняем сегмент на 2 байта /дальше old_vector /включаем прерывания /выходим из подпрограммы

Теперь мы попросим операционную систему не разрушать программу после ее завершения, а хранить ее в памяти. Для этого используется системный вызов 0x31.

Ввод: Вывод:
Сохраняем программу резидентной
АН = 0x31 Ничего
AL = выходной код
DX = число параграфов памяти, необходимых для
хранения резидентной программы

Код всей нашей резидентной программы resident . asm приведен в листинге 10.10. Листинг 10.10. Пример резидентной программы SECTION .text org 0x100 jmp initialize
Ассемблер на примерах. Базовый курс

new„handler: push ах in al, 0x60 cmp al, 0x4 6 jnz pass_on call color pass_on: pop ax jmp far [cs:old_vector] color: push ax push ex push si push es xor si,si mov ax, OxBSOO mov es,ax mov ex, 80*25 .repeat: inc si mov byte [es:si],0x4 inc si dec ex jnz .repeat pop es pop si pop ex pop ax ret old_vector dd 0 initialize: call setup mov ax,0x3100 mov dx,initialize shr dx,4 inc dx int 0x21 setup: cli

Читайте также:
Программа как в колледже тесты

;сохраняем значение AX ;читаем скан-код клавиши /сравниваем с 0x4 6 (Scroll—Lock) ;если нет, переходим к pass_on ;вызываем подпрограмму ;восстанавливаем значение АХ /передаем управление /первоначальному обработчику ;сохраняем в стеке значения /регистров, которые /будем использовать /сбрасываем SI /загружаем адрес сегмента в АХ /И в сегментный регистр /количество повторений /увеличиваем SI на 1 /записываем цвет 0x04 — красный /увеличиваем на 1 /уменьшаем СХ на 1 /переходим к .repeat, пока СХ > О /восстанавливаем все регистры вызываем регистрирацию своего обработчика функция DOS: делаем программу резидентной вычисляем число параграфов: в памяти нужно разместить весь код вплоть до метки initialize делим на 16 добавляем 1 завершаем программу и остаемся резидентом ;отключаем прерывания

Глава 10. Программирование в DOS
хог а х , а х сбрасываем AX
mov e s , a x таблица прерываний находится в
mov ax,new_handler сегменте О
с метки new_handler начинается
xchg ax,[es:0x9*4] новый обработчик прерывания
вычисляем адрес старого вектора
и меняем
местами со значением АХ.
Теперь в таблице
смещение нового обработчика,
mov [ds:old_vector],ax в АХ — старого адрес
сохраняем старый
mov a x , c s в переменной old_vector
xchg ax Л е з :0x9*4+2] адрес текущего сегмента — CS
пишем в таблицу сегмент нового
обработчика,
mov [ds:old_vector+2],ax в АХ загружаем сегмент старого
сохраняем сегмент на 2 байта
sti дальше old_vector
включаем прерывания
ret
выходим из подпрограммы

Теперь откомпилируем программу: nasm -f bin -о resident.com resident.asm. После ее запуска мы ничего не увидим, но после нажатия клавиши Scroll Lock весь текст на экране «покраснеет». Избавиться от этого резидента (выгрузить программу из памяти) можно только перезагрузкой компьютера или закрыти­ ем окна эмуляции DOS, если программа запущена из-под Windows. 10.13. Свободные источники информации Дополнительную информацию можно найти на сайтах: • www.ctyme.com/rbrown.htiTi — HTML-версия списка прерываний Ральфа Брауна (Ralf Brown’s Interrupt List); • http://programmistu.narod.ru/asm/lib__l/index.htm — «Ассемблер и про­ граммирование для IBM PC» Питер Абель.

11 Программирование в Windows «Родные» Windows-приложения Программная совместимость Запуск DOS-приложений од Windows Свободные источники информации

11.1. Введение Когда-то Microsoft Windows была всего лишь графической оболочкой для операционной системы DOS. Но со временем она доросла до самостоятельной полнофункциональной операционной системы, которая использует защищен­ ный режим процессора.

В отличие от UNIX-подобных операционных систем (Linux, BSD и др.), в Windows функции графического интерфейса пользова­ теля (GUI) встроены непосредственно в ядро операционной системы. 11.2. «Родные» Windows-приложения «Родные» Windows-приложения взаимодействуют с ядром операционной системы посредством так называемых API-вызовов.

Через API (Application Programming Interface) операционная система предоставляет все услуги, в том числе управление графическим интерфейсом пользователя. Графические элементы GUI имеют объектную структуру, и функции API часто требуют аргументов в виде довольно сложных структур со множеством параметров.

Поэтому исходный текст простейшей ассемблерной программы, управляющей окном и парой кнопок, займет несколько страниц. В этой главе мы напишем простенькую программу, которая отображает окошко со знакомым текстом «Hello, World!» и кнопкой ОК. После нажатия ОК окно будет закрыто. 11.2.1. Системные вызовы API в операционной системы DOS мы вызывали ядро с помощью прерывания 0x21.

В Windows вместо этого нам нужно использовать одну из функций API. Функции API находятся в различных динамических библиотеках (DLL). Мы должны знать не только имя функции и ее параметры, но также и имя библи­ отеки, которая содержит эту функцию: user32.dll, kernel32.dll и т.д. Описание API можно найти, например, в справочной системе Borland Delphi (файл win32.hlp). Если у вас нет Delphi, вы можете скачать только файл win32.zip (это архив, содержащий файл win32.hlp): ftp://ftp.borland.com/pub/delphi/techpubs/delphi2/win32.zip

Ассемблер на примерах. Базовый курс 11.2.2. Программа «Hello, World!» с кнопкой под Windows Наша программа должна отобразить диалоговое окно и завершить работу. Для отображения диалогового окна используется API-функция MessageBoxA, а для завершения программы можно использовать функцию ExitProcess. В документации по Windows API MessageBoxA написано:

int MessageBox( // дескриптор окна владельца
HWND hWnd,
LPCTSTR IpText, // адрес текста окна сообщения
LPCTSTR IpCaption, // адрес текста заголовка окна сообщения
UINT иТуре // стиль окна

); Первый аргумент — это дескриптор окна владельца, то есть родительского окна. Поскольку у нас нет никакого родительского окна, мы укажем значение 0. Второй аргумент — это указатель на текст, который должен быть отображен в диалоговом окне. Обычно это строка, заканчивающаяся нулевым байтом. Третий аргумент аналогичен второму, только он определяет не текст окна, а текст заголовка.

Последний аргумент — это тип (стиль) диалогового окна, мы будем использовать символическую константу МВ_ОК. Второй вызов API — это ExitProcess, ему нужно передать всего один аргумент (как в DOS), который определяет код завершения программы. Чтобы упростить разработку Windows-программы на языке ассемблера, под­ ключим файл Win32.inc, который определяет все типы аргументов API-функ­ ций (например, HWND и LPCTSTR соответствуют простому типу dword) и значения констант. Подключим этот файл: %include «win32n . inc»; Мы будем использовать АРТфункции, которые находятся в динамических библиотеках, поэтому нам нужно воспользоваться директивами EXTERN и IMPORT:

Читайте также:
Служебная компьютерная программа 7 букв сканворд
EXTERN MessageBoxA ;MessageBoxA определен вне программы
IMPORT MessageBoxA u s e r 3 2 . d l l именно — в u s e r 3 2 . d l l
EXTERN E x i t P r o c e s s / E x i t P r o c e s s определен вне программы
IMPORT E x i t P r o c e s s k e r n e l 3 2 . d l l именно в k e r n e l 3 2 . d l l

Точно так же, как в DOS, нужно создать две секции: кода и данных.

SECTION CODE USE32 CLASS^CODE ;наш код
SECTION DATA USE32 ;наши с т а т и ч е с к и е данные

Осталось понять, как можно вызвать функции API. Подробнее мы обсудим это в главе 13, посвященной компоновке программ, написанных частью на ассемблере, а частью на языках высокого уровня, а сейчас рассмотрим только правила передачи аргументов функциям API.
/строка сообщения /с символом EOL /строка заголовка
Глава 11.

Программирование в Windows Соглашение о передаче аргументов называется STDCALL. Аргументы пере­ даются через стек в порядке справа налево (так же, как в языке С), а очистка стека входит в обязанности вызванной функции (как в Паскале). Мы помещаем аргументы в стек по команде PUSH, а затем вызываем нужную функцию по команде CALL. Не нужно беспокоиться об очистке стека. Код нашей программы приведен в листинге ILL Листинг 11.1. Програц с кнопкб ^y^j^jilig^iaiiMiiBiii

%include «win32n. о год^кттщча^^ заголовочный (^ai J
EXTERN MessageBoxA ;MessageBoxA определен вне программы
IMPORT MessageBoxA u s e r 3 2 . d l l ;a именно — в u s e r 3 2 . d l l
EXTERN E x i t P r o c e s s ; E x i t P r o c e s s определен вне программы
IMPORT E x i t P r o c e s s k e r n e l 3 2 . d l l ;a именно — в k e r n e l 3 2 . d l l
SECTION CODE USE32 CLASS-CODE /начало кода
. . s t a r t : ;метка для компоновщика,
/указывающая точку входа
push UINT МВ_ОК /помещаем в стек последний аргумент.
;тип окна: с единственной кнопкой ОК
push LPCTSTR t i t l e /теперь помещаем в стек адрес
/нуль-завершенной строки заголовка
push LPCTSTR banner /адрес нуль-завершенной строки,
/которая будет отображена в окне
push HWND NULL /указатель на родительское окно —
/нулевой: нет такого окна
c a l l [MessageBoxA] /вызываем функцию API. Она выведет
/окно сообщения и вернет управление
/после нажатия ОК
push UINT NULL /аргумент E x i t P r o c e s s — код возврата
c a l l [ E x i t P r o c e s s ] /завершаем процесс
SECTION DATA USE32 CLASS^DATA

banner db ‘ H e l l o world!’,OxD,OxA,0 t i t l e db ‘ H e l l o ‘ , 0 Для того, чтобы откомпилировать эту программу, нам нужна версия NASM для Windows, которую можно скачать по адресу: http://nasm.sourceforge.net. NASM создаст объектный файл, который нужно будет скомпоновать в исполняемый файл. Для компоновки мы используем свободно распространяемый компонов­ щик alink, который можно скачать по адресу: http://alink.sourceforge,net. Назовем наш файл msgbox.asm. Теперь запустим nasmw с параметром -fobj: C:WIN32>NASMW — f o b j msgbox.asm

Источник: studfile.net

Пример резидентной программы на ассемблере

Привет! Рассматриваю пример резидентной программы для DOS из книги «Ассемблер на примерах. Базовый курс» Рудольф Марек.
Вот код программы:

.8086 code segment assume cs:code, ds:code, es:code, ss:code org 100h start: jmp Initialize ; НОВЫЙ ОБРАБОТЧИК ПРЕРЫВАНИЯ New_handler: push ax in al, 60h ; Читаем скан-код клавиши. cmp al, 46h ; Сравниваем полученное значение с 46h(Scroll-Lock). jnz Pass_on call Color Pass_on: pop ax jmp dword ptr cs:[Old_vector] ; Передаём управление первоначальному обработчику. Old_vector dd 0 ; ИЗМЕНЕНИЕ ЦВЕТА СИМВОЛОВ В КОНСОЛИ Color: push ax push cx push si push es mov ax, 3 ; Перевод видеоконтроллера в 3-й текстовый видеорежим. int 10h xor si, si mov ax, 0B800h mov es, ax mov cx, 80*25 RepeatC: inc si mov byte ptr es:[si], 4h inc si dec cx jnz RepeatC pop es pop si pop cx pop ax ret ; ИНИЦИАЛИЗАЦИЯ РЕЗИДЕНТНОЙ ЧАСТИ ПРОГРАММЫ В ПАМЯТЬ Initialize: call Setup mov ax, 3100h mov dx, offset Initialize mov cl, 4 shr dx, cl inc dx int 21h ; ЗАМЕНА ВЕКТОРА ПРЕРЫВАНИЯ Setup: cli xor ax, ax mov es, ax mov ax, offset New_handler xchg ax, word ptr es:[9h*4] mov word ptr ds:[Old_vector], ax mov ax, cs xchg ax, word ptr es:[9h*4+2] mov word ptr ds:[Old_vector+2], ax sti ret code ends end start

Программу компилирую вот так:
C:MASMBIN>ML.EXE /c C:ProgProg.asm
папки C:MASMBIN в папку C:Prog
C:MASMBIN>link16 C:ProgProg.obj, C:ProgProg.exe, C:ProgProg.map
C:MASMBIN>exe2bin C:ProgPROG.EXE C:ProgProg.com

Затем запускаю командную строку. В ней запускаю получившуюся программу. При нажатии на клавишу Scroll Lock цвет символов в консоли не меняется, некоторые клавиши(например стрелки вверх и вниз) перестают работать в открытой командной строке.
Подскажите в чём может быть дело.

Форумчанин
Регистрация: 25.01.2015
Сообщений: 472

Скомпилировал и запустил в DOSBox, единственно, заменил код ScrollLock на CapsLock — на моей клавиатуре нет такой клавиши.

Заметил, что в подпрограмме Color сильно не по делу установка текстового видеорежима — я бы удалил эти две строки.

Запускайте в DOSBox — будет работать.

Пользователь
Регистрация: 13.04.2015
Сообщений: 32

Спасибо за совет! Получается командная строка не поддерживает всего функционала DOS? В командной строке плохо работает резидентная часть программы?

Последний раз редактировалось Тимох; 05.01.2022 в 22:03 .
Форумчанин
Регистрация: 25.01.2015
Сообщений: 472

Читайте также:
Что такое shredder программа

Не буду врать — не знаю.
Резидент из книги Касаткина, запускаемый из командной строки WinXP, сильно затормаживал машину с i486, а на той же машине из под «чистого» DOS работал хорошо. Возможно, была не полная совместимость.

Вы — запустите в DOSBox и проверьте.

Пользователь
Регистрация: 30.03.2015
Сообщений: 17
Попробовал, в досбокс, эффекта ноль, хоть не висит, уже хорошо )
Форумчанин
Регистрация: 25.01.2015

Сообщений: 472

Странно — тему начинал один пользователь, а продолжил другой.
Ну, да ладно — работа с клонами на усмотрение администрации.

Собственно, помимо запуска в DOSBox ещё советовал

Заметил, что в подпрограмме Color сильно не по делу установка текстового видеорежима — я бы удалил эти две строки.

Форумчанин
Регистрация: 25.01.2015
Сообщений: 472

Нашёл книгу, набрал из неё программу — работает.
Её код частично отличается от вашего, в частности отсутствует установка текстового режима в теле резидента.

nasm 10_10.asm -fbin -l 10_10.lst -O9 -o 10_10.com
SECTION .text org 0x100 jmp initialize new_handler: push ax ;сохраняем значение АХ in al, 0x60 ;читаем скан-код клавиши cmp al, 0x46 ;сравниваем с 0x46 (Scroll-Lock) jnz pass_on ;если нет,переходим к pass_on call color ;вызываем подпрограмму pass_on: pop ax ;восстанавливаем значение АХ jmp far [cs:old_vector] ;передаем управление ;первоначальному обработчику color: push ax ;сохраняем в стеке значения push cx ;регистров, которые push si ;будем использовать push es xor si, si ;сбрасываем SI mov ax, 0xB800 ;загружаем адрес сегмента в АХ mov es, ax ;и в сегментный регистр mov cx, 80*25 ;количество повторений .repeat: inc si ;-увеличиваем SI на 1 mov byte [es:si], 0x4 ;записываем цвет 0x04 — красный inc si ;увеличиваем на 1 dec cx ;уменьшаем CX на 1 jnz .repeat ;переходим к .repeat, пока CX>0 pop es ;восстанавливаем все регистры pop si pop cx pop ax ret old_vector dd 0 initialize: call setup ;вызываем регистрирацию своего ;обработчика mov ax, 0x3100 ;функция DOS: делаем программу ;резидентной mov dx, initialize ;вычисляем число параграфов: ;в памяти нужно разместить ;весь код вплоть до метки initialize shr dx, 4 ;делим на 16 inc dx ;добавляем 1 int 0x21 ;завершаем программу и остаемся ;резидентом setup: cli ;отключаем прерывания xor ax, ax ;сбрасываем AX mov es, ax ;таблица прерываний находится в ;сегменте 0 mov ax, new_handler ;с метки new_handler начинается ;новый обработчик прерывания xchg ax, [es:0x9*4] ;вычисляем адрес старого вектора ;и меняем ;местами со значением AX. ;Теперь в таблице ;смещение нового обработчика, ;в AX — старого mov [ds:old_vector],ax ;сохраняем старый адрес ;в переменной old_vector mov ax, cs ;адрес текущего сегмента — CS ;пишем в таблицу сегмент нового ;обработчика, xchg ax, [es:0x9*4+2] ;в AX загружаем сегмент старого mov [ds:old_vector+2],ax ;сохраняем сегмент на 2 байта ;дальше old_vector sti ;включаем прерывания ret ;выходим из подпрограммы
Пользователь
Регистрация: 13.04.2015
Сообщений: 32

FPaul С клонами Вы не общались, я временно отсутствовал на форуме.
Текстовый видеорежим в код добавил я. Встречал программу выводящую символ в видеобуфер, она
не работала пока не добавил текстовый режим отображения. Думал и в данной программе это поможет.

В этой программе удалил текстовый видеорежим и заменил скан код клавиши ScrollLock — 46h на
скан код клавиши F7 — 41h(можно было и другую клавишу). При запуске в командной строке
Windows зависания уже не было(вроде все клавиши работали), а изменения цвета не происходило
(хотя при повторном запуске компилятора или линковщика(не помню точно) в этой же консоли
некоторые их строки стали красными).
В DOSBox все работала правильно — цвет шрифта изменился. Совет использовать DOSBox помог. Я думал что командная строка Windows полностью эмулирует DOS, а про DOSBox был не в курсе.
Вероятно что командная строка Windows не очень корректно поддерживает DOS резиденты в памяти.

Вот получившийся код:

.8086 code segment assume cs:code, ds:code, es:code, ss:code org 100h start: jmp Initialize ; НОВЫЙ ОБРАБОТЧИК ПРЕРЫВАНИЯ New_handler: push ax in al, 60h ; Читаем скан-код клавиши. cmp al, 41h ; Сравниваем полученное значение с 41h(F7). jnz Pass_on call Color Pass_on: pop ax jmp dword ptr cs:[Old_vector] ; Передаём управление первоначальному обработчику. Old_vector dd 0 ; ИЗМЕНЕНИЕ ЦВЕТА СИМВОЛОВ В КОНСОЛИ Color: push ax push cx push si push es xor si, si mov ax, 0B800h mov es, ax mov cx, 80*25 RepeatC: inc si mov byte ptr es:[si], 4h inc si dec cx jnz RepeatC pop es pop si pop cx pop ax ret ; ИНИЦИАЛИЗАЦИЯ РЕЗИДЕНТНОЙ ЧАСТИ ПРОГРАММЫ В ПАМЯТЬ Initialize: call Setup mov ax, 3100h mov dx, offset Initialize mov cl, 4 shr dx, cl inc dx int 21h ; ЗАМЕНА ВЕКТОРА ПРЕРЫВАНИЯ Setup: cli xor ax, ax mov es, ax mov ax, offset New_handler xchg ax, word ptr es:[9h*4] mov word ptr ds:[Old_vector], ax mov ax, cs xchg ax, word ptr es:[9h*4+2] mov word ptr ds:[Old_vector+2], ax sti ret code ends end start
Последний раз редактировалось Тимох; 19.01.2022 в 19:12 .

Источник: www.programmersforum.ru

Рейтинг
( Пока оценок нет )
Загрузка ...
EFT-Soft.ru