Простая программа на ассемблере x86: Решето Эратосфена / Habr
Вступительное слово
По своей профессии я не сталкиваюсь с низкоуровневым программированием: занимаюсь программированием на скриптовых языках. Но поскольку душа требует разнообразия, расширения горизонтов знаний или просто понимания, как работает машина на низком уровне, я занимаюсь программированием на языках, отличающихся от тех, с помощью которых зарабатываю деньги – такое у меня хобби.
И вот, я хотел бы поделиться опытом создания простой программы на языке ассемблера для процессоров семейства x86, с разбора которой можно начать свой путь в покорение низин уровней абстракции.
До ее написания я сформулировал такие требования к будущей программе:
- Моя программа не должна быть программой под DOS. Слишком много примеров ориентировано на нее в связи с простым API. Моя программа обязательно должна была запускаться на современных ОС.
- Программа должна использовать кучу – получать в свое распоряжение динамически распределяемую память.
Задачей для своей программы я выбрал поиск простых чисел с помощью Решета Эратосфена. В качестве ассемблера я выбрал nasm.
Код я писал с упором больше на стиль и понятность, чем на скорость его выполнения. К примеру, обнуление регистра я проводил не с помощью xor eax, eax
, а с помощью mov eax, 0
в связи с более подходящей семантикой инструкции. Я решил, что поскольку программа преследует исключительно учебные цели, можно распоясаться и заниматься погоней за стилем кода в ассемблере.
Итак, посмотрим, что получилось.
С чего начать?
Пожалуй, самая сложная вещь, с которой сталкиваешься при переходе от высокоуровневых языков к ассемблеру, это организация памяти. К счастью, на эту тему на Хабре уже была хорошая статья.
Так же встает вопрос, каким образом на таком низком уровне реализуется обмен данными между внутренним миром программы и внешней средой. Тут на сцену выходит API операционной системы. В DOS, как уже было упомянуто, интерфейс был достаточно простой. К примеру, программа «Hello, world» выглядела так:
SECTION .text
org 0x100
mov ah, 0x9
mov dx, hello
int 0x21
mov ax, 0x4c00
int 0x21
SECTION .data
hello: db "Hello, world!", 0xD, 0xA, '$'
В Windows же для этих целей используется Win32 API, соответственно, программа должна использовать методы соответствующих библиотек:
%include "win32n.inc" extern MessageBoxA import MessageBoxA user32.dll extern ExitProcess import ExitProcess kernel32.dll SECTION code use32 class=code ..start: push UINT MB_OK push LPCTSTR window_title push LPCTSTR banner push HWND NULL call [MessageBoxA] push UINT NULL call [ExitProcess] SECTION data use32 class=data banner: db "Hello, world!", 0xD, 0xA, 0 window_title: db "Hello", 0
Здесь используется файл win32n.inc, где определены макросы, сокращающие код для работы с Win32 API.
Я решил не использовать напрямую API ОС и выбрал путь использования функций из библиотеки Си. Так же это открыло возможность компиляции программы в Linux (и, скорее всего, в других ОС) – не слишком большое и нужное этой программе достижение, но приятное достижение.
Вызов подпрограмм
Потребность вызывать подпрограммы влечет за собой несколько тем для изучения: организация подпрограмм, передача аргументов, создание стекового кадра, работа с локальными переменными.
Подпрограммы представляют собой метку, по которой располагается код. Заканчивается подпрограмма инструкцией ret
. К примеру, вот такая подпрограмма в DOS выводит в консоль строку «Hello, world»:
print_hello:
mov ah, 0x9
mov dx, hello
int 0x21
ret
Для ее вызова нужно было бы использовать инструкцию call
:
call print_hello
Для себя я решил передавать аргументы подпрограммам через регистры и указывать в комментариях, в каких регистрах какие аргументы должны быть, но в языках высокого уровня аргументы передаются через стек. К примеру, вот так вызывается функция printf
из библиотеки Си:
push hello
call _printf
add esp, 4
Аргументы передаются справа налево, обязанность по очистке стека лежит на вызывающей стороне.
При входе в подпрограмму необходимо создать новый стековый кадр. Делается это следующим образом:
print_hello:
push ebp ;сохраняем указатель начала стекового кадра на стеке
mov ebp, esp ;теперь началом кадра является вершина предыдущего
Соответственно, перед выходом нужно восстановить прежнее состояние стека:
mov esp, ebp
pop ebp
ret
Для локальных переменных так же используется стек, на котором после создания нового кадра выделяется нужное количество байт:
print_hello:
push ebp
mov ebp, esp
sub esp, 8 ;опускаем указатель вершины стека на 8 байт, чтобы выделить память
Так же архитектура x86 предоставляет специальные инструкции, с помощью которых можно более лаконично реализовать эти действия:
print_hello:
enter 8, 0 ;создать новый кадр, выделить 8 байт для локальных переменных
leave ;восстановить стек
ret
Второй параметр инструкции enter
– уровень вложенности подпрограммы. Он нужен для линковки с языками высокого уровня, поддерживающими такую методику организации подпрограмм. В нашем случае это значение можно оставить нулевым.
Непосредственно программа
Проект содержит такие файлы:
main.asm
– главный файл,functions.asm
– подпрограммы,string_constants.asm
– определения строковых констант,Makefile
– сценарий сборки
Рассмотрим код основного файла:main.asm
%define SUCCESS 0 %define MIN_MAX_NUMBER 3 %define MAX_MAX_NUMBER 4294967294 global _main extern _printf extern _scanf extern _malloc extern _free SECTION .text _main: enter 0, 0 ;ввод максимального числа call input_max_number cmp edx, SUCCESS jne .custom_exit mov [max_number], eax ;выделяем память для массива флагов mov eax, [max_number] call allocate_flags_memory cmp edx, SUCCESS jne .custom_exit mov [primes_pointer], eax ;отсеять составные числа mov eax, [primes_pointer] mov ebx, [max_number] call find_primes_with_eratosthenes_sieve ;вывести числа mov eax, [primes_pointer] mov ebx, [max_number] call print_primes ;освободить память от массива флагов mov eax, [primes_pointer] call free_flags_memory ;выход .success: push str_exit_success call _printf jmp .return .custom_exit: push edx call _printf .return: mov eax, SUCCESS leave ret %include "functions.asm" SECTION .data max_number: dd 0 primes_pointer: dd 0 %include "string_constants.asm"
Видно, что программа поделена по смыслу на 5 блоков, оформленных в виде подпрограмм:
input_max_number
— с помощью консоли запрашивает у пользователя максимальное число, до которого производится поиск простых; во избежание ошибок значение ограничено константамиMIN_MAX_NUMBER
иMAX_MAX_NUMBER
allocate_flags_memory
— запросить у ОС выделение памяти для массива пометок чисел (простое/составное) в куче; в случае успеха возвращает указатель на выделенную память через регистрeax
find_primes_with_eratosthenes_sieve
— отсеять составные числа с помощью классического решета Эратосфена;print_primes
— вывести в консоль список простых чисел;free_flags_memory
— освободить память, выделенную для флагов
Для функций было условлено такое правило: значение возвращается через регистр
eax
, регистр edx
содержит статус. В случае успеха он содержит значение SUCCESS
, то есть, 0
, в случае неудачи — адрес строки с сообщением об ошибке, которое будет выведено пользователю.Файл string_constants.asm
содержит определение строковых переменных, значения которых, как намекает название файла, менять не предполагается. Только ради этих переменных было сделано исключение к правилу «не использовать глобальные переменные». Я так и не нашел более удобного способа доставлять строковые константы функциям ввода-вывода – подумывал даже записывать на стек непосредственно перед вызовами функций, но решил, что эта идея куда хуже идеи с глобальными переменными.
;подписи ввода-вывода, форматы str_max_number_label: db "Max number (>=3): ", 0 str_max_number_input_format: db "%u", 0 str_max_number_output_format: db "Using max number %u", 0xD, 0xA, 0 str_print_primes_label: db "Primes:", 0xD, 0xA, 0 str_prime: db "%u", 0x9, 0 str_cr_lf: db 0xD, 0xA, 0 ;сообщения выхода str_exit_success: db "Success!", 0xD, 0xA, 0 str_error_max_num_too_little: db "Max number is too little!", 0xD, 0xA, 0 str_error_max_num_too_big: db "Max number is too big!", 0xD, 0xA, 0 str_error_malloc_failed: db "Can't allocate memory!", 0xD, 0xA, 0
Для сборки применяется такой сценарий:Makefile
ifdef SystemRoot
format = win32
rm = del
ext = .exe
else
format = elf
rm = rm -f
ext =
endif
all: primes.o
gcc primes.o -o primes$(ext)
$(rm) primes.o
primes.o:
nasm -f $(format) main.asm -o primes.o
Подпрограммы (функции)
input_max_number
Код подпрограммы
; Ввести максимальное число
; Результат: EAX - максимальное число
input_max_number:
;создать стек-фрейм,
;4 байта для локальных переменных
enter 4, 1
;показываем подпись
push str_max_number_label ;см. string_constants.asm
call _printf
add esp, 4
;вызываем scanf
mov eax, ebp
sub eax, 4
push eax
push str_max_number_input_format ;см. string_constants.asm
call _scanf
add esp, 8
mov eax, [ebp-4]
;проверка
cmp eax, MIN_MAX_NUMBER
jb .number_too_little
cmp eax, MAX_MAX_NUMBER
ja .number_too_big
jmp .success
;выход
.number_too_little:
mov edx, str_error_max_num_too_little ;см. string_constants.asm
jmp .return
.number_too_big:
mov edx, str_error_max_num_too_big ;см. string_constants.asm
jmp .return
.success:
push eax
push str_max_number_output_format ;см. string_constants.asm
call _printf
add esp, 4
pop eax
mov edx, SUCCESS
.return:
leave
ret
Подпрограмма призвана ввести в программу максимальное число, до которого будет производиться поиск простых. Ключевым моментов тут является вызов функции
scanf
из библиотеки Си:mov eax, ebp sub eax, 4 push eax push str_max_number_input_format ;см. string_constants.asm call _scanf add esp, 8 mov eax, [ebp-4]
Таким образом, сначала в
eax
записывается адрес памяти на 4 байта ниже указателя базы стека. Это память, выделенная для локальных нужд подпрограммы. Указатель на эту память передается функции scanf
как цель для записи данных, введенных с клавиатуры.После вызова функции, в eax
из памяти перемещается введенное значение.
allocate_flags_memory и free_flags_memory
Код подпрограмм
; Выделить память для массива флагов
; Аргумент: EAX - максимальное число
; Результат: EAX - указатель на память
allocate_flags_memory:
enter 8, 1
;выделить EAX+1 байт
inc eax
mov [ebp-4], eax
push eax
call _malloc
add esp, 4
;проверка
cmp eax, 0
je .fail
mov [ebp-8], eax
;инициализация
mov byte [eax], 0
cld
mov edi, eax
inc edi
mov edx, [ebp-4]
add edx, eax
mov al, 1
.write_true:
stosb
cmp edi, edx
jb .write_true
;выход
mov eax, [ebp-8]
jmp .success
.fail:
mov edx, str_error_malloc_failed ;см. string_constants.asm
jmp .return
.success:
mov edx, SUCCESS
.return:
leave
ret
; Освободить память от массива флагов
; Аргумент: EAX - указатель на память
free_flags_memory:
enter 0, 1
push eax
call _free
add esp, 4
leave
ret
Ключевыми местами этих подпрограмм являются вызовы функций
malloc
и free
из библиотеки Си.malloc
в случае удачи возвращает через регистр eax
адрес выделенной памяти, в случае неудачи этот регистр содержит 0
. Это самое узкое место программы касательно максимального числа. 32 бит вполне достаточно для поиска простых чисел до 4 294 967 295, но выделить разом столько памяти не получится.
find_primes_with_eratosthenes_sieve
Код подпрограммы
;Найти простые числа с помощью решета Эратосфена
;Аргументы: EAX - указатель на массив флагов, EBX - максимальное число
find_primes_with_eratosthenes_sieve:
enter 8, 1
mov [ebp-4], eax
add eax, ebx
inc eax
mov [ebp-8], eax
;вычеркиваем составные числа
cld
mov edx, 2 ;p = 2
mov ecx, 2 ;множитель с = 2
.strike_out_cycle:
;x = c*p
mov eax, edx
push edx
mul ecx
pop edx
cmp eax, ebx
jbe .strike_out_number
jmp .increase_p
.strike_out_number:
mov edi, [ebp-4]
add edi, eax
mov byte [edi], 0
inc ecx ;c = c + 1
jmp .strike_out_cycle
.increase_p:
mov esi, [ebp-4]
add esi, edx
inc esi
mov ecx, edx
inc ecx
.check_current_number:
mov eax, ecx
mul eax
cmp eax, ebx
ja .return
lodsb
inc ecx
cmp al, 0
jne .new_p_found
jmp .check_current_number
.new_p_found:
mov edx, ecx
dec edx
mov ecx, 2
jmp .strike_out_cycle
.return:
leave
ret
Подпрограмма реализует классический алгоритм для вычеркивания составных чисел, решето Эратосфена, на языке ассемблера x86. Приятна тем, что не использует вызовы внешних функций и не требует обработки ошибок 🙂
print_primes
Код подпрограммы
; Вывести простые числа
; Параметры: EAX - указатель на массив флагов, EBX - максимальное число
print_primes:
enter 12, 1
mov [ebp-4], eax
mov [ebp-8], ebx
push str_print_primes_label
call _printf
add esp, 4
cld
mov esi, [ebp-4]
mov edx, esi
add edx, [ebp-8]
inc edx
mov [ebp-12], edx
mov ecx, 0
.print_cycle:
lodsb
cmp al, 0
jne .print
jmp .check_finish
.print:
push esi
push ecx
push str_prime ;см. string_constants.asm
call _printf
add esp, 4
pop ecx
pop esi
mov edx, [ebp-12]
.check_finish:
inc ecx
cmp esi, edx
jb .print_cycle
push str_cr_lf
call _printf
add esp, 4
leave
ret
Подпрограмма выводит в консоль простые числа. Ключевым моментом тут является вызов функции
printf
из библиотеки Си.Заключение
Что ж, программа отвечает всем сформулированным требованиям и, кажется, проста для понимания. Хочется надеяться, кому-нибудь ее разбор поможет вникнуть в программирование на низком уровне и он получит от него такое же удовольствие, какое получил я.
Так же привожу полные исходники программы.
Могу так же привести интересный факт. Поскольку с детства нас учили, что программы на языке ассемблера выполняются быстрее, я решил сравнить скорость выполнения этой программы со скоростью программы на C++, которую я писал когда-то и которая искала простые числа с помощью Решета Аткина. Программа на С++, скомпилированная в Visual Studio с /O2
выполняла поиск до числа 230 примерно за 25 секунд на моей машине. Программа же на ассемблере показала 15 секунд с Решетом Эратосфена.
Это, конечно, скорее байка, чем научный факт, поскольку не было серьезного тестирования не было выяснения причин, но как интересный факт для завершения статьи подойдет, как мне кажется.
Полезные ссылки
- Список ресурсов для изучения ассемблера
- Организация памяти
- Решето Эратосфена
- Решето Аткина
- Стек
- Стековый кадр
habr.com
Пример простейшей программы на ассемблере
Ниже приведен исходный текст полностью законченной программы на ассемблере, выполняющей некоторые арифметические операции.
CODE SEGMENT ; (1) определение программного сегмента
ASSUME DS:CODE ; (2)
Start: ; (4) точка входа в программу
MOV AX, a ; (3) запись в AX значения переменной a
MOV BX, b ; (4) запись в BX значения переменной b
IMUL BX ; (5) умножение AX на BX
ADD AX, c ; (6) добавление к переменной c значения
; (7) из AX
MOV d, AX ; (8) запись значения AX в переменную d
MOV AX, 4C00h ; (9) запись в AH номера функции DOS
INT 21h ; (10) вызов функции DOS завершения
; (11) программы
a DW 5 ; (12) определение переменной a
b DW -7 ; (13) определение переменной b
c DW 120 ; (14) определение переменной c
d DW ? ; (15) определение переменной d
CODE ENDS ; (16) конец программного сегмента
END Start ; (17) определение точки входа в программу
Программа выполняет вычисление значения выражения a * b + c и записывает результат в переменную d. После этого происходит завершение программы и передача управления операционной системе.
В строке 1 определяется программный сегмент с именем “CODE”, в котором находятся команды программы и данные (переменные a, b, c, d).
В строке 2 с помощью директивы ASSUME определяется, что при обращении к переменным программы будет использоваться сегментный регистр DS.
В строке 3 в регистр AX записывается значение из переменной a, т. е. в регистр AX записывается число 5.
В строке 4 в регистр BX записывается значение из переменной b, т. е. в регистр BX записывается число -7.
В строке 5 значение регистра AX умножается на значение регистра BX и результат записывается в так называемую регистровую пару DX:AX, т. е. в регистр DX записывается старшая часть произведения двух 16-битных регистров (биты 16 – 31), а в регистр AX записывается младшая часть произведения (биты 0 – 15). При умножении операнды рассматриваются как числа со знаком.
В строке 6 к младшей части произведения добавляется значение переменной c, т. е. значение регистра AX увеличивается на 120.
В строке 8 значение регистра AX записывается в переменную d, т. е. в переменную d записывается значение выражения 5 * (-7) + 120.
В строке 9 в регистр AH записывается номер функции DOS завершения программы 4Ch, а в регистр AL записывается код возврата 00h.
В строке 10 вызывается функция DOS, номер которой был записан в регистре AH – это функция завершения программы и передачи управления операционной системе. После выполнения этой команды работа программы завершается.
В строках 12 – 15 определяются переменные a, b, c, d и их начальные значения. Переменная d не инициализируется, так как в нее записывается результат.
В строке 16 директивой ENDS закрывается программный сегмент с именем “CODE”.
В строке 17 директивой END транслятору указывается точка входа в программу, на чем текст программы завершается.
В правой части программы к каждой строке подписаны комментарии. Комментарий в языке ассемблера начинается с символа ‘;’ и продолжается до конца текущей строки.
Для выполнения лабораторной работы необходимо:
открыть любой текстовый редактор (например, встроенный в программу-оболочку Norton Commander) и набрать текст программы на ассемблере. При наборе текста программы можно использовать следующий шаблон:
CODE SEGMENT
ASSUME CS:CODE
ORG 100H
Start:
<команда1>
<команда2>
. . .
CODE ENDS
END Start
После метки Start и до конца сегмента команд размещаются арифметические команды, необходимые для вычисления значения выражения.
Значения четырех переменных (a, b,c и d), участвующих в выражении, необходимо занести в четыре регистра общего назначения, например, AX, BX, CX и DX с использованием команды MOV. Значения переменных могут быть любыми, но среди них должны быть как положительные, так и отрицательные числа.
Оттранслировать программу с использованием транслятора Turbo Assembler. Для этого необходимо ввести в командной строке DOS следующую команду: TASM.EXE <имя_программы>, где имя_программы – имя файла программы с расширением .ASM, например «MyProg.asm».
Если в процессе трансляции транслятором были выданы ошибки, необходимо исправить текст программы и провести повторную трансляцию. В случае успешной трансляции в текущей директории должен появиться объектный файл с именем программы и расширением .OBJ, например «MyProg.obj».
Скомпоновать программу как COM, используя компоновщик Turbo Linker. Для этого необходимо ввести в командной строке DOS следующую команду: TLINK.EXE /t <имя_объектного_файла>, где имя_ объектного_файла – имя объектного файла программы с расширением .OBJ, например «MyProg.obj».
Если в процессе компоновки компоновщиком были выданы ошибки, необходимо исправить текст программы и провести повторную трансляцию и компоновку. В случае успешной компоновки в текущей директории должен появиться исполнительный файл с именем программы и расширением .COM, например «MyProg.com».
Загрузить программу в отладчик. Для этого необходимо ввести в командной строке DOS следующую команду: TD.EXE <имя_исполнительного_файла>, где имя_исполнительного_файла – имя исполнительного файла программы с расширением .COM, например «MyProg.com».
Проверить работу программы в пошаговом режиме. Для выполнения программы в пошаговом режиме используется клавиша F7. При этом при каждом нажатии клавиши F7 выполняется очередная команда программы. Для возвращения программы в исходное состояние нужно нажать комбинацию клавиш Ctrl+F12.
При контроле правильности работы программы после выполнения каждой команды требуется сверять результаты выполнения арифметических операций с тем, что должно получиться в результате выполнения конкретной арифметической операции.
Правильность работы программы необходимо проверить для нескольких значений переменных a, b, c и d. При этом каждый раз при изменении значений переменных в программе необходимо выполнять трансляцию и компоновку программы.
studfiles.net
Примеры программирования на Ассемблере — Благин Константин
Простой пример обработки сообщения от мыши.
Для обработки сообщения от мыши в DOS`е нам потребуется прерывание 33h.
Инициализации мыши:
int 33h
Вход: ax =0000h
Выход: ax =0000h, если мышь или драйвер мыши не установлены.
ax =0ffffh, драйвер и мышь установлены.
Bx=число кнопок:
0002 или 0ffffh – две
0003 – три
0000 – другое количество
Показать курсор:
int 33h
Вход: ax=0001h
Спрятать курсор:
int 33h
Вход: ax=0002h
Установить обработчик событий:
int 33h
Вход: ax=000сh
es:dx = адрес обработчика
cx = условие вызова
бит 0: любое перемещение
бит 1: нажатие левой копки
бит 2: отпускание левой копки
бит 3: нажатие правой копки
бит 4: отпускание правой копки
бит 5: нажатие средней копки
бит 6: отпускание средней копки
cx = 0000h – отменить обработчик
Обработчик оформляется как дальняя процедура, на входе ax — содержит условие вызова, bx – состояние кнопок, cx и dx – x и y координаты курсора, si и di – счетчик последнего перемещения по горизонтали и вертикали, ds – сегмент данных драйвера мыши.
Делать будем com программу, используя TASM, параметры транслятора и компоновщика такие:
bin\tasm mouse.asm
bin\tlink /t /x mouse.obj
/t – создать файл типа .com
/x – не создавать файл карты(map)
.model tiny ; код, данные и стек размещаются в одном сегменте, размером 64 кб
.code ; основной сегмент кода
org 100h ; счетчик для com
start:
mov ax,12h ;установка видеорежима 640х480, 16 цветов
int 10h
mov ax,0000h ;инициализация мыши
int 33h
mov ax,0ch ; установка обработчика мыши
mov cx,0001h ; любое перемещение
lea dx,handler_I ; смещение обработчика
int 33h
;——————————————
mov ah,10h ; ждем нажатие любой кнопки
int 16h
mov ax,000ch
mov cx,0000h ; отменяем обработчик
int 33h
ret ; конец программы
handler_I: ; наш обработчик
; cx и dx – x и y координаты курсора, а для int 10h это номера строки и столбца
push cs
pop ds ; в ds сегмент кода и данные программы
mov bh,0 ; номер видеостраницы
mov ah,0ch ; вывести точку на экран
mov al,color_m ; цвет точки
int 10h
retf ; выход из процедуры
color_m db 0000010
end start
Здесь необходимо заметить, что в режиме 12h возвращаемые координаты совпадают с координатами пикселов. Если использовать режим 13h, то необходимо координату X разделить на 2. Программу можно оптимизировать, необходимо в обработчике мыши использовать прямую запись в видеопамять вместо прерывания 10h.
Массивы на Ассемблере
Создание одномерного массива на Ассемблере.
.model tiny
.code
org 100h
start:
push cs
pop ds
;---------------------------------------
mov cx,99 ;Значение счетчика циклов для команды loop
mov si,0 ;Индекс первого элемента, si так же будет и значением
ARR_loop:
mov array[si],si;array[0]=0,array[1]=1...array[n]=n
inc si
loop ARR_loop ;цикл
int 20h
;---------------------------------------
array dw 99 dup (?) ;Не инициализированный массив
end start
Создание двухмерного массива на Ассемблере.
.model tiny
.code
org 100h
start:
push cs
pop ds ;в сегмент данных заносим сегмент кода
mov si,0 ;Начальная строка
mov bx,0 ;Начальный столбец
;---------------------------------------
array_loop:
mov array[bx][si],bx ;Заполняем элементы массива текущим индексом столбца
inc si ;На следующий элемент строки
cmp si,10 ;Конец строки?
jz NextLine ;если да, переходим на метку NextLine
jmp array_loop ;иначе, продолжаем заполнять строку
NextLine:
mov si,0 ;Обнуляем индекс элемента строки
inc bx ;Переходим на следующий столбец
cmp bx,10 ;Последний столбец?
jz exit ;если да,выход
jmp array_loop ;иначе, продолжаем заполнять следующею строку
exit:
;---------------------------------------
int 20h ;Выход из com программы
;---------------------------------------
array dw 10 dup (10 dup (?))
end start
Поиск числа в двухмерном массиве на Ассемблере.
.model tiny
.code
org 100h
start:
push cs
pop ds ;в сегмент данных заносим сегмент кода
mov si,0
mov bx,0
;Поиск----------------------------------
array_find:
mov ax,array[bx][si]
call Proverka
inc si ;На следующий элемент строки
cmp si,2 ;Конец строки?
jz NLine ;если да, переходим на метку NextLine
jmp array_find ;иначе, продолжаем заполнять строку
NLine:
mov si,0 ;Обнуляем индекс элемента строки
inc bx ;Переходим на следующий столбец
cmp bx,3 ;Последний столбец?
jz exit ;если да,выход
jmp array_find ;иначе, продолжаем заполнять следующею строку
exit:
;---------------------------------------
int 20h ;Выход из com программы
;---------------------------------------
array dw 2 dup (3 dup (0))
message db "Yes ",0dh,0ah,'$'
;---------------------------------------
Proverka proc
cmp ax,0
jz YES
ret
YES: mov ah,9
mov dx,offset message
int 21h
ret
Proverka endp
end start
Пример расчета факториала на Ассемблере.
Пример расчета факториала, на мой взгляд, очень полезная программа для понимания работы стека.
.model small
.486
.stack 100h
.code
start:
mov ax,@data
mov ds,ax
mov res,1
push 5
call factorial
;-----------------------------------------------------
mov ax,4c00h
int 21h
;-----------------------------------------------------
factorial proc
push bp
mov bp,sp
mov cx,[bp+4]
mov ax,cx
mul res
mov res,ax
dec cx
jcxz end_p
push cx
call factorial
end_p:
mov sp,bp
pop bp
ret
factorial endp
;-----------------------------------------------------
.data
res dw 0
end start
Прямая запись в видео память на ассемблере.
Рисование горизонтальной линии, с помощью прямой записи в видео память.
.model tiny
.code
org 100h
start:
mov al,13h
int 10h
mov ax,0A000h
mov es,ax
mov dx,320*100+160 ;320*y1+x1(начальная точка)
mov cx,13 ;Длина линии
call gline
mov ah,10h
int 16h
ret
;————————————————————
gline proc
mov di,dx
mov al,111b ;color
rep stosb ;копируем al в ES:DI, dec DI
ret
gline endp
;————————————————————
end start
Вывод ASCII кодов на ассемблере.
.model tiny
.code
org 100h
start:
mov ax,13h
int 10h
mov cx,256 ;Счетчик кругов для loop
mov ax,0003h ;Установка видеорижима 3, курсор в 0,0
int 10h ;и очистка экрана
mov ax,0b800h
mov es,ax ;Загружаем в дополнительный сегментный регистр абсол.адрес
mov di,0 ;Смещение относительно адреса 0b800h
mov ah,010b ;Атрибуты, цвет текста зеленый
mov al,00h ;ASCII код
mov es:[di],ax ;Грузим не в регистр а по адресу который наход. в регистре
;———————-
cloop:
add di,4 ;Смещение на 4 байта, чтобы выглядело нормально
inc al ;Следущий ASCII код
mov es:[di],ax ;Грузим по адресу в видеопамять
loop cloop ;Дальше…
;———————-
mov ah,10h ;Ждем нажатие Any Key
int 16h
ret
end start
blagin.ru
7.4. Пример полной программы на Ассемблере
Прежде, чем написать нашу первую полную программу на Ассемблере, нам необходимо научиться выполнять операции ввода/вывода, без которых ни одна сколько-нибудь серьёзная программа обойтись не может. В самом языке машины, в отличие от языка нашей учебной машины УМ-3, нет команда ввода/вывода,1чтобы, например, ввести целое число, необходима достаточно большаяпрограммана машинном языке.
Для организации ввода/вывода мы в наших примерах будем использовать макрокоманды из учебника [5]. Вместо каждой макрокоманды Ассемблер будет подставлять соответствующий этой макрокоманде набор команд и констант (этот набор, как мы узнаем позже, называется макрорасширениемдля макрокоманды).
Нам понадобятся следующие макрокоманды ввода/вывода.
outch op1
где операнд op1может быть в форматеi8,r8илиm8. Значение операнда трактуется как код символа, этот символ выводится в текущую позицию экрана. Для задания кода символа удобно использовать символьную константу языка Ассемблер, например,′A′. Такая константа преобразуется программой Ассемблера именно в код этого символа. Например, outch ′*′выведет символ звёздочки на место курсора.
inch op1
где операнд op1может быть в форматеr8илиm8. Код введённого символа записывается в место памяти, определяемое операндом.
outint op1[,op2]
outword op1[,op2]
Здесь, как всегда, квадратные скобки говорят о том, что второй операнд может быть опущен. В качестве первого операнда op1можно использоватьi16,r16илиm16, а второго –i8,r8илиm8. Действие макрокоманды outint op1,op2полностью эквивалентно процедуре вывода языка Паскальwrite(op1:op2), а действие макрокоманды с именемoutwordотличается только тем, что первый операндтрактуетсякак беззнаковое (неотрицательное) число.
inint op1
где операнд op1может иметь форматr16илиm16, производит ввод с клавиатуры на место первого операнда целого значения из диапазона–215..+216. Особо отметим, что операнды форматовr8иm8недопустимы.
newline
предназначена для перехода курсора к началу следующей строки экрана и эквивалентна вызову процедуры без параметров writelnязыка Паскаль. Этого же эффекта можно достичь, если вывести на экран служебные символы с кодами 10 и 13, т.е. выполнить, например, макрокоманды
outch 10
outch 13
flush
предназначена для очистки буфера ввода и эквивалентна вызову процедуры без параметров readlnязыка Паскаль.
outstr
Эта макрокоманда выводит на экран строку текста из того сегмента, на который указывает сегментный регистр DS, причём адрес начала этой строки в сегменте должен находится в регистреDX. Таким образом, физический адрес начала выводимого текста определяется по формуле
Афиз = (DS*16 + DX)mod 220
Заданный таким образом адрес принято записывать в виде так называемой адресной пары<DS,DX>. В качестве признака конца выводимой строки символов должен быть задан символ$(он рассматривается как служебный признак конца и сам не выводится). Например, если в сегменте данных есть текст
Data segment
. . .
T db ′Текст для вывода на экран$’
. . .
data ends
то для вывода этого текста на экран можно выполнить следующий фрагмент программы
. . .
mov DX,offset T; DX:=адрес T
outstr
. . .
Рассмотрим теперь пример простой полнойпрограммы на Ассемблере. Эта программа должна вводить значение целой переменнойA и реализовывать оператор присваивания (в смысле языка Паскаль)
X := (2*A — 241 div (A+B)2) mod 7
где B–параметр, т.е. значение, которое не вводится, а задаваётся в самой программе. ПустьA,BиС–знаковыецелые величины, описанные в сегменте данных так:
A dw ?
B db –8; это параметр, заданный программистом
X dw ?
Вообще говоря, результат, заносимый в переменную Xкороткий(это остаток от деления на 7), однако мы выбрали дляXформат слова, т.к. его надо выдавать в качестве результата, а макрокомандаoutintможет выводить толькодлинныецелые числа.
Наша программа будет содержать три сегмента с именами data,codeиstackи выглядеть следующим образом:
include io.asm
; вставить в программу файл с макроопределениями
; для макрокоманд ввода-вывода
data segment
A dw ?
B db -8
X dw ?
Data ends
stack segment stack
db 128 dup (?)
stack ends
code segment
assume cs:code, ds:data, ss:stack
start:mov ax,data; это команда формата r16,i16
mov ds,ax ; загрузка сегментного регистра DS
inint A ; макрокоманда ввода целого числа
mov bx,A ; bx := A
mov al,B ; al := B
cbw ; ax := длинное B
add ax,bx ; ax := B+A=A+B
add bx,bx ; bx := 2*A
imul ax ; (dx,ax) := (A+B)2
mov cx,ax ; cx := младшая часть(A+B)2
mov ax,241
cwd ; <dx,ax> := сверхдлинное 241
idiv cx ; ax := 241 div (A+B)2 , dx := 241 mod (A+B)2
sub bx,ax ; bx := 2*A — 241 div (A+B)2
mov ax,bx
cwd
mov bx,7
idiv bx ; dx := (2*A — 241 div (A+B)2) mod 7
mov X,dx
outint X
finish
code ends
end start
Прокомментируем текст нашей программы. Во-первых, заметим, что сегмент стека мы нигде явно не используем, однако он необходим в любойпрограмме. Как мы узнаем далее из нашего курса, во время выполнения любой программы возможно автоматическое (без нашего ведома) переключение на выполнение некоторой другой программы, при этом используется сегмент стека. Подробно этот вопрос мы рассмотрим при изучениипрерываний.
В начале сегмента кода расположена директива assume, она говорит программе Ассемблера, на какие сегменты будут указывать соответствующие сегментные регистры при выполнении команд,обращающихсяк этим сегментам. Сама эта директива не меняет значения ни одного сегментного регистра, подробно про неё необходимо прочитать в учебнике [5].
Заметим, что сегментные регистры SSиCSдолжны быть загруженыперед выполнениемсамой первойкоманды нашей программы. Ясно, что сама наша программа этого сделать не в состоянии, так как для этого необходимо выполнить хотя бы одну команду, что требует доступа к сегменту кода, и, в свою очередь, уже установленного на этот сегмент регистраCS. Получается замкнутый круг, и единственным решением будет попросить какую-тодругуюпрограмму загрузить значения этих регистров,передвызовомнашей программы. Как мы потом увидим, эту операцию будет делать служебная программа, которая называетсязагрузчиком.
Первые две команды нашей программы загружают значение сегментного регистра DS, в младшей модели для этого необходимы именнодвекоманды, так как одна команда имела бы несуществующий формат:
mov ds,data; формат SR,i16 такого формата нет!
Пусть, например, при счёте нашей программы сегмент данных будет располагаться, начиная с адреса 10000010оперативной памяти. Тогда команда
mov ax,data
будет во время счёта иметь вид
mov ax,6250 ; 100000 div 16 = 6250
Макрокоманда
inint A; макрокоманда ввода целого числа
вводит значение целого числа в переменную A.
Далее начнём непосредственное вычисление правой части оператора присваивания. Задача усложняется тем, что величины A и Bимеют разную длину и непосредственно складывать их нельзя. Приходится командами
mov al,B ; al := B
cbw ; ax := длинное B
преобразовать короткое целое B, которое сейчас находится на регистреal, в длинное целое на регистреax. Далее вычисляется значение выражения(A+B)2и можно приступать к выполнению деления. Так как делитель является длинным целым числом (мы поместили его на регистрcx), то необходимо применить операциюдлинногоделения, для чего делимое (число241 на регистреax) командой
cwd
преобразуем в сверхдлинное целое и помещаем на два регистра (dx,ax). Вот теперь всё готово для команды целочисленного деления
idiv cx; ax:= 241 div (A+B)2 , dx:= 241 mod (A+B)2
Далее мы присваиваем остаток от деления (он в регистре dx) переменнойXи выводим значение этой переменной по макрокоманде
outint X
которая эквивалентна процедуре WriteLn(X)языка Паскаль. Последним предложением в сегменте кода является макрокоманда
finish
Эта макрокоманда заканчивает выполнение нашей программы, она эквивалентна выходу программы на Паскале на конечный end.
И, наконец, директива
end start
заканчивает описание всего модуля на Ассемблере. Обратите внимание на параметр этой директивы – метку start. Она указываетвходную точкупрограммы, т.е. её первую выполняемую команду программы.
Сделаем теперь важные замечания к нашей программе. Во-первых, мы не проверяли, что команды сложения и вычитания дают правильный результат (для этого, как мы знаем, после выполнения этих команд нам было бы необходимо проверить флаг переполнения OF, т.к. наши числа мы считаем знаковыми). Во-вторых, команда длинного умножения располагает свой результат в двух регистрах (dx,ax), а в нашей программе мы брали результат произведения только из регистра ax, предполагая, что на регистре dx находятся только незначащие цифры произведения. По-хорошему надо было бы проверить, что в dx содержаться только нулевые биты, если ax 0, и только двоичные “1”, если
ax < 0. Другими словами, знак числа в регистре dx должен совпадать со знаком числа в регистре ax, для знаковых чисел это и есть признак того, что в регистре dx содержится незначащая часть произведения. И, наконец, мы не проверили, что не производим деления на ноль (в нашем случае что A<>8). В наших учебных программах мы иногда не будем делать таких проверок, но в “настоящих” программах, которые Вы будете создавать на компьютерах и предъявлять преподавателям, эти проверки являются обязательными.
Продолжая знакомство с языком Ассемблера, решим следующую задачу. Напишем фрагмент программы, в котором увеличивается на единицу целое число, расположенное в 23456710 байте оперативной памяти. Мы уже знаем, что запись в любой байт памяти возможна только тогда, когда этот байт расположен в одном из четырёх текущих сегментах. Сделаем, например, так, чтобы наш байт располагался в сегменте данных. Главное здесь – не путать сегменты данных, которые мы описываем в программе на Ассемблере, с активными сегментами, на начала которых установлены сегментные регистры. Описываемые в программе сегменты обычно размещаются загрузчиком на свободных участках оперативной памяти, и, как правило, при написании текста программы неизвестно их будущего месторасположение.1 Однако ничто не мешает нам любой участок оперативной памяти сделать сегментом, установив на него какой-либо сегментный регистр. Так мы и сделаем для решения нашей задачи, установив сегментный регистр DS на начало ближайшего сегмента, в котором будет находиться наш байт с адресом 23456710. Так как в сегментный регистр загружается адрес начала сегмента, делённый на 16, то нужное нам значение сегментного регистра можно вычислить по формуле: DS := 234567 div 16 = 14660. При этом адрес A нашего байта в сегменте (его смещение от начала сегмента) вычисляется по формуле: A := 234567 mod 16 = 7. Таким образом, для решения нашей задачи можно предложить следующий фрагмент программы:
mov ax,14660
mov ds,ax; Начало сегмента
mov bx,7; Смещение
inc byte ptr [bx]
Теперь, после изучения арифметических операций, перейдём к рассмотрению команд переходов, которые понадобятся нам для программирования условных операторов и циклов. После изучения нашего курса мы должны уметь отображать на Ассемблер любые конструкции языка Паскаль.
studfiles.net
Ниже приведена программа на языке ассемблера MASM32, которая выводит на системный динамик первые восемь тактов вальса Грибоедова. Программа может быть скомпилирована в исполняемый файл (.exe). Для компиляции создайте новый текстовый документ в директории, расположенной на том же диске, где у вас установлен MASM32. Файл переименуйте в beeper.asm, скопируйте в него текст программы. Затем откройте этот файл с помощью MASM32 Editor, после чего в меню MASM32 Editor выберите опцию Project >> Assemble & Link. В той папке, где находится beeper.asm, обнаружится и скомпилированная программа, она будет называться beeper.exe. .386 ; 32-битный режим .model flat, stdcall ; компиляция в exe-файл с возможностью вызова API option casemap :none ; неразличение прописных и строчных символов include ; подключаем файл прототипов функций includelib ; подключаем файл библиотек .data ; начинает сегмент данных (и завершает предыдущий сегмент) ; здесь могло бы быть описание переменных .code ; начинает сегмент кода (и завершает предыдущий сегмент) ; здесь могло бы быть описание процедур start: ; сюда операционная система передаст управление invoke Beep, 3951, 200 ; си invoke Beep, 4186, 200 ; до invoke Beep, 3951, 200 ; си invoke Beep, 3136, 200 ; соль invoke Beep, 2637, 200 ; ми invoke Beep, 3951, 200 ; си invoke Beep, 3136, 200 ; соль invoke Beep, 3951, 200 ; си invoke Beep, 1975, 200 ; си invoke Beep, 3951, 200 ; си invoke Beep, 3136, 200 ; соль invoke Beep, 3951, 200 ; си invoke Beep, 3440, 200 ; ля invoke Beep, 3951, 200 ; си invoke Beep, 3440, 200 ; ля invoke Beep, 3015, 200 ; фа invoke Beep, 2489, 200 ; ре-диез invoke Beep, 3440, 200 ; ля invoke Beep, 3015, 200 ; фа invoke Beep, 3440, 200 ; ля invoke Beep, 1975, 200 ; си invoke Beep, 3440, 200 ; ля invoke Beep, 3015, 200 ; фа invoke Beep, 3440, 200 ; ля invoke Beep, 3136, 200 ; соль invoke Beep, 3440, 200 ; ля invoke Beep, 3136, 200 ; соль invoke Beep, 2637, 200 ; ми invoke Beep, 1975, 200 ; си invoke Beep, 3136, 200 ; соль invoke Beep, 3015, 200 ; фа invoke Beep, 3136, 200 ; соль invoke Beep, 3015, 200 ; фа invoke Beep, 2098, 200 ; до invoke Beep, 1720, 200 ; ля invoke Beep, 3015, 200 ; фа invoke Beep, 2637, 200 ; ми invoke Beep, 3015, 200 ; фа invoke Beep, 2637, 200 ; ми invoke Beep, 2217, 200 ; до-диез (2217,40) invoke Beep, 1568, 200 ; соль invoke Beep, 2637, 200 ; ми invoke Beep, 2489, 250 ; ре-диез invoke Beep, 1975, 250 ; си invoke Beep, 3729, 250 ; ля-диез (3729,20) invoke Beep, 1975, 250 ; си invoke Beep, 3951, 250 ; си invoke Beep, 1975, 250 ; си invoke ExitProcess, 0 ; сообщаем Windows о завершении программы end start ; завершает сегмент кода Эта простейшая ассемблерная программа имеет набор директив, которые будут присутствовать в любой программе для MASM32. В первой строке записано указание компилятору на тип процессора, для которого создана программа. 386 — это третье поколение (1985) процессоров Intel, у которых регистры, внутренняя и внешняя шина данных впервые стали 32-разрядными. Таким образом, директива .386 означает, что программа написана для 32-разрядных процессоров Intel. Поскольку Intel-процесоры совместимы сверху вниз, программный код будет работать на всех версиях процессоров Intel, выпущенных после 1985 года. Другие возможные варианты директивы, указывающей на тип процессора: .8086, .186, .286 (16 бит), .486, .586, .686 (32 бит). Дополнительно (отдельной строкой) могут быть указаны расширения системы команд процессора (прописными или строчными символами): .MMX, .XMM. Комментарием считается любой набор символов, начинающийся с точкой с запятой и до конца строки. Например, в первой строке нашей программы запись ; 32-битный режим — это комментарий. Комментарии компилятор не обрабатывает. Директива .model в записи flat указывает компилятору, что программу следует создать в плоской модели памяти, при этом в одном 32-разрядном сегменте будут содержаться и программа, и ее данные. Именно в таком формате операционная система Windows понимает исполняемые файлы, соответственно, указание компилятору на плоскую модель памяти подразумевает создание в конечном итоге .exe-файла. Плоская модель памяти применима только для 32-разрядных процессоров и выше. Все понимаемые MASM32 модели пямяти: tiny, small, compact, medium, large, huge, flat. При стандартном вызове функций параметры передаются через стек. Именно через стек обмениваются данными с программами встроенные функции операционной системы Windows (API-функции), поэтому, если мы планируем использовать в своей программе такие функции, мы должны записать в директиве .model опцию stdcall. Директива option casemap :none дает указание компилятору неразличать прописные и строчные символы в написанной нами программе. Все варианты option casemap: all, none, notpublic. Директива include предписывает компилятору вставить в проект ассемблерный код из указанного в ней файла. Путь к файлу может быть указан в директиве не полностью, в таком случае компилятор будет исходить из текущего каталога. Угловые скобки необходимым, если путь к файлу содержит обратный слеш, точку с запятой, одинарные или двойные кавычки. Аналогично, директива includelib подключает к нашему проекту библиотечный файл. Директивы .data и .code начинают сегмент данных и сегмент исполняемого кода соответственно и заканчивают предыдущий сегмент, если таковой имеется.
Директива invoke дает задание компилятору совершить
вызов функции. В нашем случае вызываемая функция Beep -
это API-функция Windows, она описывается следующим образом:
Функции Beep должны быть переданы два значения — DWORD dwFreq и DWORD dwDuration. Запись DWORD (double word) конкретизирует размер памяти, которую занимает переменная. Double word — это два машинных слова (четыре байта или 32 бит). Указание размера памяти, отводимой под переменную, важно потому, что обмен данными с API-функциями программы производят через стек, в стек перед вызовом API-функции программа должна поместить как раз столько байт, сколько из него затем будет извлечено API-функцией. В противном случае вызов API-функции или исполнение программы после вызова может завершиться ошибкой. И, наконец, dwFreq — это частота звука в Герцах, dwDuration - длительность звука в миллисекундах. В ассемблере передаваемые функции параметры записываются после ее имени через запятую, в том же порядке, что и в описании функции. Например, запись invoke Beep, 784, 900 после компиляции даст ноту соль второй октавы (ее частота 784 Гц) с длительностью звучания 0,9 секунды.
Добавить комментарий
Ассемблер MASM32
Простейшая программа на ассемблере (beeper)
|
sadda.ru
Примеры небольших подпрограмм на Ассемблере | Assembler | Статьи | Программирование Realcoding.Net
Чтение строки с клавиатуры
Следующая процедура считывает строку ASCIIZ с клавиатуры.
KbdInput$ proc ;POW35
; Входные данные: смещение строки в AX
; Выходные данные: строка ASCIIZ, прочитанная с клавиатуры. Регистры не сохраняются.
mov DI,AX ;смещение строки
mov DX,AX ;смещение буфера
mov CX,255 ;максимальное количество читаемых символов
mov BX,0 ;файловый хэндл клавиатуры
mov AH,3Fh ;читаем из файла (фактически - с клавиатуры)
int 21h
jc Input$_error ;если ошибка
dec AX ;убираем символ RETURN
add DI,AX ;смещение байта, расположенного в конце строки
Input$_error:
mov [DI],BL ;завершаем строку, записывая 0 в конец строки
ret
KbdInput$ endp
Перевод чисел в двоичную форму (в виде строки)
Данная процедура конвертирует 16-битное слово в строку ASCIIZ, т.е. число 7 преобразовывается в строку 0000000000000111. Лидирующие нули включаются в строку. Строка ASCIIZ — это набор символов, завершающихся 0.
NmbrToBi$ proc ;POW36
;Входные данные: AX - смещение строки, BX - число, которое необходимо преобразовать
;Выходные данные: Строка ASCIIZ. Регистры не сохраняются.
mov DI,AX ;смещение строки
mov DX,8000h ;проверочное слово, 1 в позиции 15
mov CX,16 ;обрабатываем 16 бит
NumberTo_B0:
mov AL,48 ;символ '0'
test BX,DX ;бит равен 1?
jz NumberTo_B
inc AL ;символ '1'
NumberTo_B:
stosb ;записываем в строку '1' или '0'
shr DX,1 ;сдвигаем тестовый бит вправо
loop NumberTo_B0
mov [DI],DL ;завершаем строку 0
ret
NmbrToBi$ endp
Чтение значения счетчика времени
В памяти по адресу 40:6C расположено двойное слово, которое увеличивается на единицу приблизительно 18.2 раза в секунду. Системное время можно получить, считывая это слово. Младший байт может быть использован для многих «временных» задач, в т.ч. в качестве исходного значения для генератора псевдослучайных чисел (а в некторых случаях и заменить его).
GetTicks proc ;POW37
; Входные данные: нет
; Выходные данные: Младший байт счетчика времени в AX
; Регистры не сохраняются.
mov BX,ES ;Сохраняем адрес дополнительного сегмента
mov AX,40h ;сегмент данных BIOS
mov ES,AX
mov AX,ES:[6Ch] ;читаем счетчик
mov ES,BX ;восстанавливаем регистр ES
ret
GetTicks endp
Определяем тип процессора
Следующая процедура WhatCPU определяет тип процессора, установленного в системе. Результат возвращается в регистре AX. Процедура может быть откомпилирована и 16-битным компилятором, несмотря на то, что в ней используются 32-битные инструкции для определения различия между 386, 486 и Pentium.
WhatCPU proc ;POW38
;Результат в AX
;0: i88,i86, 1: i186, 2: i286, 3: i386, 4: i486, 5: Pentium
pushf ;сохраняем флаги
mov DX,0F000h
sub AX,AX
push AX ;записываем 0 в верхушку стека
popf ;восстанавливаем регистр флагов из стека
pushf ;записываем флаги в стек
pop AX
popf ;восстанавливаем флаги
and AX,DX ;выделяем четыре старших байта
cmp AX,DX ;они равны 1 ?
jne CPU_ei8088
mov AX,0 ;результат 0 (8088 или 8086)
ret
CPU_ei8088:
push SP
pop BX
cmp BX,SP ;изменяется ли указатель стека перед записыванием в него?
je CPU_ei186
mov AX,1 ;результат 1 (80186)
ret
CPU_ei186:
pushf ;сохраняем флаги
mov AX,DX ;0F000h
push AX
popf
pushf
pop AX
popf ;оригинальные флаги
and AX,DX
jne CPU_ei286
mov AX,2 ;результат 2 (80286)
ret
CPU_ei286:
db 66h
pushf ;pushfd
db 66h
pushf ;pushfd
db 66h
pop AX ;pop EAX
db 66h
or AX,0000h
db 04h,00h ;или EAX,00040000h
db 66h
push AX ;push EAX
db 66h
popf ;popfd
db 66h
pushf ;pushfd
db 66h
pop AX ;pop EAX
db 66h
popf ;popfd
db 66h
test AX,0000h
db 04h,00h ;test EAX,00040000h
jnz CPU_ei386
db 66h
mov AX,3 ;результат AX=00000003h (80386)
db 0h,0h
ret
CPU_ei386:
db 66h
pushf ;pushfd
db 66h
pushf ;pushfd
db 66h
pop AX ;pop EAX
db 66h
mov BX,AX ;mov EBX,EAX
db 66h
xor AX,0000h
db 20h,00h ;xor EAX,00200000h
db 66h
push AX ;push EAX
db 66h
popf ;popfd
db 66h
pushf ;pushfd
db 66h
pop AX ;pop EAX
db 66h
popf ;popfd
db 66h
and AX,0000h
db 20h,00h ;and EAX,00200000h
db 66h
and BX,0000h
db 20h,00h ;and EBX,00200000h
db 66h
cmp AX,BX ;cmp EAX,EBX
jne CPU_ei486
db 66h
mov AX,4 ;результат EAX=00000004h (80486)
db 0h,0h
db 66h ;обнуление 32 битных регистров
xor BX,BX ;xor EBX,EBX
ret
CPU_ei486: ;Pentium
db 66h
mov AX,5 ;результат EAX=00000005h (Pentium)
db 0h,0h
db 66h
xor BX,BX ;xor EBX,EBX
ret
WhatCPU endp
Установка видеорежимов VGA
Видеорежимы, поддерживаемые BIOS’ом адаптеров VGA BIOS: Экран
Режим Текст Графика Цвета Размер Адрес
0 CGA 25*40 only text 16 B&W 2000 0B800h
1 CGA 25*40 only text 16 2000 0B800h
2 CGA 25*80 only text 16 B&W 4000 0B800h
3 CGA 25*80 only text 16 4000 0B800h
4 CGA 25*40 320*200 4 16000 0B800h
5 CGA 25*40 320*200 2 B&W 8000 0B800h
6 CGA 25*80 640*200 2 16000 0B800h
7 MDA 25*80 only text 2 4000
0Dh EGA 25*40 320*200 16 32000 0A000h
0Eh EGA 25*80 640*200 16 64000 0A000h
0Fh EGA 25*80 640*350 2 28000 0A000h
10h EGA 25*80 640*350 16 112000 0A000h
11h VGA 30*80 640*480 2 38400 0A000h
12h VGA 30*80 640*480 16 153600 0A000h
13h VGA 25*40 320*200 256 64000 0A000h
Требуемый видеорежим устанавливается вызовом функции BIOS
mov AH,0 ;POW39
mov AL,ScreenModeNumber
int 10h
Этот фрагмент также очищает экран. Содержимое AX не сохраняется. Стандартный BIOS не возвращает никакой информации, сигнализирующей об ошибке. В подерживаемых режимах можно читать и писать в видеопамять путем вызовов соответствующих функций (функции 8,9,0Ch,0Dh). Нормальный текстовый режим DOS — это режим 3.
Следующий фрагмент загружает набор символов из ROM в RAM и соответственно
корректирует высоту отображения символов.
mov AH,11h ;изменить используемый набор символов и корректировать высоту их отображения
;mov AL,11h ;выбрать набор символов 8*14, 28 строк в режиме VGA
;mov AL,12h ;выбрать набор символов 8*8, 50 строк
mov AL,14h ;выбрать набор символов 8*16, 25 строк
mov BX,0 ;банк памяти генератора символов
int 10h
Линейные преобразования в системах с фиксированной точкой
Следующая подпрограмма переводит дюймы в миллиметры, но также она может быть использована для любых линейных преобразований, для чего достаточно изменить коэффициент преобразования.
Числа предствалены в 32-битном формате с фиксированной точкой. Старшее слово содержит целую часть числа, а младшее слово — дробную часть. Предполагается, что используются только положительные числа.
Код использует 32-битные инструкции, но может быть откомпилирован и 16-битным компилятором.
; данные
ConvFactor dw 26214 ;младший байт коэффициента преобразования 25.4
dw 25 ;старший байт
Inches dw 32768 ;младший байт представления 12.5 дюймов
dw 12 ;старший байт
mMeters dw ? ;младший байт результата в мм
dw ? ;старший байт
; код
db 66h
mov AX,Inches ;mov EAX,dword ptr ConvFactor
db 66h
mul ConvFactor ;результат в EDX:EAX
mov CL,16
db 66h
shr AX,CL ;shr EAX,16
mov mMeters,AX ;младший байт результата
mov mMeters+2,DX ;старший байт
Таблица размещения файлов FAT
Первый сектор (с номером 0) диска — это загрузочный сектор. Его первые байты содержат следующую информацию:byte 0-2 Переход на загрузочную программу 3-10 Имя в ASCII формате или что-нибудь еще 11-12 Байт на сектор 13 Секторов на кластер 14-15 Секторов в загрузочной записи =B 16 Количество копий FAT 17-18 Количество каталогов в корне диска 19-20 Секторов на диск 21 Тип диска =xx 22-23 Секторов на FAT =F и т.д.
Первая таблица FAT начинается с B. Ее копия располагается в секторе B+F
etc.
Можно детально рассмотреть FAT используя утилиту DEBUG. Не вносите
изменений в таблицу FAT на жестком диске, если вы не уверены, что вы
делаете.
Первая запись таблицы FAT выглядит так:
12 bit FAT: xx 0FFh 0FFh 16 bit FAT: xx 0FFh 0FFh 0FFh xx - тип диска.Затем, с кластера 2 начинаются элементы таблицы. Возможные значения перечислены в следующей таблице:
12-бит.FAT 16-бит. FAT 000h 0000h пусто 002h-0FEFh 0002h-0FFEFh использовано кластеров. Значение-указатель на следующую запись в цепочке. 0FF0h-0FF6h 0FFF0h-0FFF6h зарезервировано 0FF7h 0FFF7h bad 0FF8h-0FFFh 0FFF8-0FFFFh последний кластер в цепочкеВы можете читать сектора, используя прерывание 25h. Отметим, что это прерывание сохраняет флаги в стеке, так что после выполнения прерывания они должны быть восстановлены
Запуск дочерней программы
DOS выделяет всю доступную память текущей программе, независимо от того,
какой объем реально необходим. Поэтому вы должны освободить часть памяти для
того, чтобы загрузить и выполнить дочернюю программу. Это выполняется процедурой
Setmem. Каждый параграф занимает 16 байт. Пространство, необходимое текущей
программе вычисляется как размер в параграфах = Lseg — Psp + 1
где Lseg —
сегмент, расположенный после последнего байта программы, а Psp — сегмент, в
котором расположен psp программы.
Setmem proc
;Выделяет AX параграфов памяти текущей программе
:и очищает всю остальную память.
;Входные данные: количество выделяемых параграфов в AX
;Выходные данные: число реально выделенных параграфов в AX
mov BX,AX ;объем выделяемой памяти в 16-битных параграфах
mov AH,4Ah
int 21h ;ES должен указыват на сегмент PSP программы
mov AX,BX ;число выделенных параграфов
ret
Setmem endp
Следующий фрагмент кода запускает программу CHILD.COM с параметром /HELP.
;сегмент данныхt:
ChildName db 'CHILD.COM',0 ;имя файла в виде строки ASCIIZ
; сегмент кода:
mov AX,CS
mov SegCmdLine,AX
mov SegFCB1,AX
mov SegFCB2,AX
push DS ;сохраняем регистры
push ES
mov CS:Shell_SS,SS ;сохраняем только регистр CS
mov CS:Shell_SP,SP
;exec-function
mov DX,offset ChildName ;DS:DX - указатель на строку, содержащую имя файла
mov AX,CS
mov ES,AX
mov BX,offset CS:Parm_Table ;таблица параметров ES:BX
mov AX,4B00h ;загрузить и выполнить программу
int 21h
cli ;запрещаем прерывания
mov SS,CS:Shell_SS ;восстанавливаем регистры
mov SP,CS:Shell_SP
sti ;разрешаем прерывания
pop ES
pop DS
cld ;флаг направления (direction flag) = 0
jc ThereWasError ;ошибка
; эти данные должны быть определены в сегменте кода
CmdLineTail db 6,' /HELP',13 ;6 - число символов
even ;faster this way
Shell_SS dw 0 ;указатель стека
Shell_SP dw 0
Parm_Table dw 0 ;наследуем переменные окружения родительской программы
dw offset CmdLineTail
SegCmdLine dw 0 ;сюда будет записан CS
dw 5Ch ;блок управления файлом (FCB) #1
SegFCB1 dw 0 ;сюда будет записан CS
dw 6Ch ;блок управления файлом (FCB) #2
SegFCB2 dw 0 ;сюда будет записан CS
Чтение параметров командной строки
Параметры командной строки (сразу после имени файла) могут быть прочитаны с помощью следующей процедуры ReadCL.
Например, если ваша программа называется KOE.COM и вы запускаете ее, набрав команду
KOE 4abcs
в командной строке DOS, то процедура ReadCL вернет строку 4abcs в формате ASCIIZ.
ReadCL proc ;чтение параметров командной строки в буфер по адресу ES:[DI]
;DS должен остаться неизменным после запуска программы (=PSP)
mov SI,80h ;адрес парамтеров
xor CX,CX
mov CL,[SI] ;длина в байтах
inc SI ;игнорируем байт длины
rep movsb ;перемещаем строку в буфер
mov AL,0
stosb ;завершаем строку ASCIIZ нулем
ret
ReadCL endp
TSR: Завершаемся и остаемся в памяти
Инсталляция TSR-программы выполняется в три этапа:
- Загрузка резидентной части в память. Проверка, не находится ли наша программа уже в памяти. Сохранение необходимой информации для дальнейшего удаления резидента из памяти. Освобождение памяти, занятой копией переменных окружения для экономии.
- Установка параметров для работы резидентной части. Обычно на этом этапе перехватываются прерывания.
- Завершение установочной программы, при этом резидентная часть остается в памяти.
;Структура программы TSR
Begin: ;Здесь начинается .COM-программа
jmp Install
;Сюда нужно поместить резидентную часть
Install:
;сюда поместите код установки
mov AH,31h ;завершиться и остаться резидентом
mov AL,0 ;возвращает результат =OK
mov DX,offset Install
mov CL,4
shr DX,CL ;делим на 16
add DX,1 ;объем резидентной части программы
int 21h
Рисование в SVGA
Пикселы расположены линейно в памяти видеоадаптера. В 256-цветных режимах пиксел представляется одним байтом. Поэтому смещение точки с координатами (x,y) можно вычислить как 640*y+x в режиме с 640 пикселами по горизонтали. Единственное ограничение, связанное с такими вычисленими, — это то, что последний доступный пиксел, к которому может быть получен доступ, имеет координаты x=255, y=102, его смещение 65535. Это известное ограничение 64Kбайтных сегментов.
Чтобы обойти это ограничение, применяется переключение банков памяти. При этом переопределяется расположение физического адреса, которое соответствует логическому адресу. Так, логический адрес 0 соответствует физическому адресу 65536 если активен первый банк в видеодаптером с размером «окна» (granularity) 64 KB.
Логический адрес точки с координатами (x,y) определяется как 640*y+x-B*WG где B — номер банка и WG — размер «окна». Банк памяти может быть переключен с помощью функции AX=4F05h прерывания 10h в видеоадаптерах, поддерживающих стандарт VESA.
Следующая процедура рисует пиксел на экране. Координаты пиксела находятся в регистрах AX и BX, а в регистре CX передается цвет пиксела. В процедуре предполагается, что размер «окна» равен 64 KB, что справедливо, например, для чипов S3.
SVGA_bank dw 0 ;номер активного банка памяти
S_rivi dw 640 ;длина строки в байтах
VGA_seg dw 0A000h ;сегмент памяти экрана VGA
CBpxl$ proc ;рисует пиксел с координатами x=AX, y=BX, цвет=CX
xchg AX,BX ;теперь x=BX, y=AX
mul S_rivi
add AX,BX
adc DX,0 ;в DX помещается требуемый номер банка
mov DI,AX ;логический адрес
cmp DX,SVGA_bank ;банк корректен?
je Cxl256_OK
mov SVGA_bank,DX ;новый банк
mov AX,4F05h
xor BX,BX ;функция: устанавливаем банк DX, окно A
int 10h
Cxl256_OK:
mov BX,ES ;сохраняем сегмент
mov AL,CL ;цвет
mov ES,VGA_seg
stosb ;рисуем пиксел
mov ES,BX
ret
CBpxl$ endp
Пишем напрямую в видеопамять
; полностью завершенная COM-программа
codeseg segment
assume cs:codeseg, ds:codeseg, es:codeseg
org 100h
Code: jmp Start
x dw 50 ;координата x выводимого текста
y dw 20 ;координата y выводимого текста
Text db 'string to be printed',0 ;не забываем 0
Start:
mov AX,80 ;вычисляем адрес
mul y
add AX,x
shl AX,1 ;адрес в AX=160*y+2*x
mov DI,AX
mov SI,offset Text
push ES ;сохраняем ES
mov AX,0B800h ;сегмент экранной памяти в текстовом режиме
mov ES,AX
Print:
lodsb ;загружаем AL из DS:[SI]
or AL,AL ;конец строки?
jz Ready ;да, AL=0
mov ES:[DI],AL ;символ для отображения
add DI,2 ;пропускаем байт атрибутов
jmp Print
Ready: pop ES ;восстанавливаем ES
;---------------------------------------
int 20H
codeseg ends
end Code
Рисуем пиксел в графическом режиме
Графические режимы могут быть разбиты на шесть групп в зависимости от количества бит, отводимых каждому пикселу:
- 1 бит/пиксел, 2 цвета, одна битовая плоскость:
- CGA mode 6 разрешение 640*200
- 2 бит/пиксел, 4 цвета, одна битовая плоскость:
- CGA mode 4 разрешение 320*200
- 4 бит/пиксел, 16 цветов, четыре битовых плоскости:
- EGA mode 0Dh разрешение 320*200
- EGA mode 0Eh разрешение 640*200
- EGA mode 10h разрешение 640*350
- VGA mode 12h разрешение 640*480
- VESA mode 102h разрешение 800*600
- VESA mode 104h разрешение 1024*768
- VESA mode 106h разрешение 1280*1024
- EGA mode 0Eh разрешение 640*200
- 8 бит/пиксел, 256 цветов, одна битовая плоскость:
- VGA mode 13h разрешение 320*200
- VESA mode 100h разрешение 640*400
- VESA mode 101h разрешение 640*480
- VESA mode 103h разрешение 800*600
- VESA mode 105h разрешение 1024*768
- VESA mode 100h разрешение 640*400
- 16 бит/пиксел, 65536 цветов, одна битовая плоскость(существуют также 32768-цветные режимы):
- VESA mode 111h разрешение 640*480
- VESA mode 114h разрешение 800*600
- 24 бит/пиксел, 16777216 цветов, одна битовая плоскость:
- VESA mode 112h разрешение 640*480
Исключая 4-битные режимы пикселы в памяти располагаются на одной плоскости
(plane), т.е., если координаты пиксела (x,y), то адрес, по которому
располагается этот пиксел в памяти может быть вычислен как
Address =
LineLength*y + Bits*x/8
где LineLength — количество байтов, занимаемых
каждой строкой пикселов, а Bits — количество бит, занимаемым пикселом.
Исключениями являются режимы CGA номер 4 и 6, у которых четные и нечетные линии расположены в различных сегментах памяти.
В шестнадцатицветных режимах экранная память разделяется на 4 битовые
плоскости. Каждый бит значения цвета пиксела расположен на своей плоскости.
Адрес байта, хранящего пиксел с координатами x,y можно вычислить как
Address
= LineLength*y + x/8
где LineLength — число байтов, занимаемых одной
строкой.
Рисование пиксела с координатами x,y в 16-цветных режимах подразумевает установку бита во всех четырех плоскостях. Активная в данный момент плоскость выбирается записью в соответствующие порты видеокарты.
Режимы CGA, EGA и VGA поддерживаются всеми стандартными BIOS. Переключение в эти режимы обычно осуществляется простым вызовом функций BIOS.
Pixel$
Во всех режимах VGA следующая процедура Pixel$ может нарисовать пиксел. Нужно отметить, что процедура достаточно медленная, т.к. используются вызовы функций BIOS.
Pixel$ proc
;Рисует пиксел во всех режимах VGA.
;Входные данные: x в AX, y в BX, цвет в CX
;Выходные данные: регистры не сохраняются
mov DX,BX ;строка y
xchg AX,CX ;CX - колонка x, AL - цвет
sub BH,BH ;0 страница
mov AH,0Ch ;выводим пиксел
int 10h
ret
Pixel$ endp
Самый интересный режим VGA — это режим 13h с возможностью отображения 256 цветов и разрешением 320*200. Номер цвета 0…255 соответствуют значениям в палитре, где все цвета представлены в виде определенных сочетаний красной, зеленой и синей компонент. Следующая процедура VGApxl$ рисует пиксел в этом режиме. Она работает достаточно быстро, однако существуют еще более быстрые варианты.
;данные:
VGA_seg dw 0A000h ;сегмент памяти экрана VGA
VGApxl$ proc
;Рисует пиксел в режиме VGA 13h.
;Входные данные: x в AX, y в BX, цвет в CX
;Выходные данные: регистры AX и BX не сохраняются
xchg BH,BL ;умножаем y на 256, BL=0
add AX,BX ;AX = x+256y
shr BX,1 ;делим 256y на два
shr BX,1 ;BX = 256y/4 = 64y
add BX,AX ;BX = x+320y
mov AX,ES ;сохраняем значение ES
mov ES,VGA_seg ;сегмент памяти экрана VGA
mov ES:[BX],CL ;выводим байт на экран
mov ES,AX ;восстанавливаем значение регистра ES
ret
VGApxl$ endp
Функция синуса в 32-битной системе с фиксированной точкой
Процедура Rsin$ вычисляет тригонометрическую функцию sin от 32-битного аргумента. 32-битная система с фиксированной точкой определяется следующим образом:
;значение переменной F32bit = 4.750
F32bit dw 49152 ;дробная часть (0.75*65536)
dw 4 ;целая часть
Использование процедуры:
Входные данные: смещение аргумента в BX, смещение
результата в AX. Аргумент задает угол в градусах.
Выходные данные: значение
функции sin, записываемое в переменную, смещение которой определяется регистром
AX. Значения регистров не сохраняются.
Например, sin(30.5°) вычисляется так:
Angle dd 001E8000h ;старший байт=30, младший байт=32768
Result dd ? ;сюда будет записан результат
....
mov AX,offset Result
mov BX,offset Angle
call Rsin$
В результате такого вызова вы получите результат 0.50752 в то время как правильное значени еравно 0.50754
Rsin$ proc
; значение синуса аргумента (двойное слово по смещению BX) вычисленное как двойное слово по смещению AX.
; Угол (по смещению BX) в градусах в диапазоне -360...360.
push AX ;сохраняем смещение результата
mov AX,[BX+2] ;целая часть угла
mov CX,[BX] ;дробная часть угла
mov DX,1 ;знак результата - +1 или -1
or AX,AX ;какой знак?
jns Rsin_1
not AX ;меняем знак
not CX
add CX,1
adc AX,0
neg DX ;также меняется знак результата
Rsin_1: ;теперь имеем угол в диапазоне 0...360
; уменьшаем диапазон до 0...180
cmp AX,180 ;угол больше 180?
jl Rsin_2
sub AX,180
neg DX ;изменяем знак результата
Rsin_2: ;теперь угол AX:CX в диапазоне 0...179.99998
cmp AX,90 ;угол больше 90?
jl Rsin_3
mov BX,180 ;вычисляем 180-угол
sub SI,SI
sub SI,CX
sbb BX,AX
mov AX,BX
mov CX,SI
Rsin_3: ;угол в AX:CX в диапазоне 0...90
push DX ;сохраняем знак результата в стеке
cmp AX,90 ;угол равен 90?
jne R_sin4
mov AX,1 ;возвращаем 1.0000
sub BX,BX
jmp short Rsin_9
R_sin4:
mov SI,offset Rsin_t ;таблица значений синусов
add SI,AX
add SI,AX
sub AX,AX ;целая часть значения синуса
mov BX,[SI] ;дробная часть
or CX,CX ;дробная чать равна 0?
jz Rsin_9
; интерполяция между значениями [SI] и [SI+2]
mov AX,[SI+2]
sub AX,BX ;AX = sin(a+1)-sin(a)
mul CX ;CX=0... 0.9999, результат DX= AX*CX/65536
add BX,DX ;добавим получившийся результат к значению из таблицы
sub AX,AX ;целая часть результата
Rsin_9:
pop DX ;корректируем знак результата AX:BX
or DX,DX
jns Rsin_loppu
not AX ;изменяем знак результата
not BX
add BX,1
adc AX,0
Rsin_loppu:
pop DI ;смещение результата
mov [DI+2],AX ;записываем целую часть
mov [DI],BX ;дробная часть
ret
Rsin$ endp
;таблица синусов
Rsin_t dw 0,1144,2287, 3430, 4572, 5712, 6850, 7987, 9121,10252
dw 11380,12505,13626,14742,15855,16962,18064,19161,20252,21336
dw 22415,23486,24550,25607,26656,27697,28729,29753,30767,31772
dw 32768,33754,34729,35693,36647,37590,38521,39441,40348,41243
dw 42126,42995,43852,44695,45525,46341,47143,47930,48703,49461
dw 50203,50931,51643,52339,53020,53684,54332,54963,55578,56175
dw 56756,57319,57865,58393,58903,59396,59870,60326,60764,61183
dw 61584,61965,62328,62672,62997,63303,63589,63856,64104,64332
dw 64540,64729,64898,65048,65177,65287,65376,65446,65496,65526
dw 0 ;для интерполяции
Проверка готовности накопителя
Программа проверяет готовность устройства. Если устройство не готово, программа просит нажать клавишу ESC.
; Проверяем корректность и готовность устройства.
; Полностью завершенная COM-программа.
codeseg segment
assume CS:codeseg, DS:codeseg, ES:codeseg
org 100h
Begin: jmp Start
; ----переменные----
Intvec dd ? ;старый вектор прерывания 24h
Luukku db 'Disk not valid or ready. Hit Esc!',10,13,'$'
Start:
;------------ Основная программа -----------
;Перехватываем прерывание 24h
push ES
mov AX,3524h ;вектор int 24h записывается в ES:BX
int 21h
mov word ptr Intvec,BX ;смещение
mov word ptr Intvec[2],ES ;сегмент
pop ES
;load a new int 24h
mov AX,2524h ;новый вектор 24h
mov DX,offset CError ;адрес
int 21h
;код для проверки готовности устройства
mov DL,1 ;1 - A:, 2 - B: и т.д..
mov AH,36h ;функция определения свободного места на диске
int 21h
cmp AX,-1 ;AX - число секторов в кластере -1
je Loppu ;выход если нет диска или не готов
;устройство готово
;здесь ваш код....
Loppu: int 20h ;завершаем COM-программу
;-------- новое прерывание int 24h -----------------
assume DS:nothing ;будут использоваться дальние вызовы
CError proc far
pushf ;сохраняем флаги
or AH,AH
js EiLevyke
push DX
push DS
mov AX,CS
mov DS,AX
assume DS:Codeseg
mov DX,offset Luukku
mov AH,9 ;выводим строку DS:DX
int 21h
mov AH,0
int 16h ;ждем нажатия клавиши
cmp AL,27 ;это Esc ?
jne EiEsc
mov AH,4Ch ;завершаем программу
int 21h
EiEsc: pop DS
assume DS:nothing
pop DX
popf
mov AL,1 ;еще раз
iret ;возвращаем управление главной программе
EiLevyke: popf ;восстанавливаем флаги
jmp CS:Intvec ;вызываем старый обработчик int 24h
CError endp
codeseg ends
end Begin
www.realcoding.net
Статья 1. Простейшая программа на языке ассемблера
Статья 1. Простейшая программа на языке ассемблераНачнем изучение языка ассемблера с рассмотрения простой, возможно, даже наипростейшей программы (пример 1.1), которая выводит на экран терминала строку с текстом. Вопросы ввода в компьютер текста программы, ее трансляции и компоновки мы рассмотрим в следующей статье, а пока сосредоточимся на структуре программы.
Пример 1.1. Простейшая программа
text segment 'code' ; (1) Начало сегмента команд assume CS:text, DS:text ; (2) Сегментные регистры CS и DS ; будут указывать на сегмент команд begin: mov AX,text ; (3) Адрес сегмента команд загрузим mov DS,AX ; (4) сначала в AX, затем в DS mov AH,09h ; (5) Функция DOS 09h вывода на экран mov DX,offset message ; (6) Адрес выводимого сообщения int 21h ; (7) Вызов DOS mov AH,4Ch ; (8) Функция 4Ch завершения программы mov AL,00h ; (9) Код 0 успешного завершения int 21h ; (10) Вызов DOS message db 'Наука умеет много гитик$' ; (11) Выводимый текст text ends ; (12) Конец сегмента команд end begin ; (13) Конец текста с точкой входа
Следует заметить, что при вводе исходного текста программы с клавиатуры можно использовать как прописные, так и строчные буквы: транслятор воспринимает, например, строки text segment и TEXT SEGMENT одинаково. Однако, с помощью ключа /ML можно заставить транслятор различать прописные и строчные буквы в именах. Тогда строки text segment и TEXT segment уже не будут эквивалентны. фактически они будут описывать два разных сегмента. Неэквивалентность прописных и строчных букв касается только имен; строки
mov ds,ax MOV DS,AX mov DS,AX
во всех случаях воспринимаются одинаково.
В настоящей книге в программах и их описаниях мы используются преимущественно строчные буквы. прописными буквами выделены обозначения регистров и, иногда, имена программных и иных файлов.
Наша программа содержит 13 строк — предложений языка ассемблера. Первое предложение с помощью оператора segment открывает сегмент команд программы. Сегменту дается произвольное имя text. Описатель ‘code’ (так называемый класс сегмента) говорит о том, что это сегмент команд (слово code в переводе может означать и коды, и команды программы). В конце предложения после точки с запятой располагается комментарий. Таким образом, предложение языка ассемблера может состоять из четырех полей: имени, оператора, операндов и комментария, располагаемых в перечисленном порядке.
Любая программа должна обязательно состоять из сегментов — без сегментов программ не бывает. Обычно в программе задаются три сегмента: команд, данных и стека, но мы в нашей простой программе пока ограничились одним сегментом команд.
В предложении 2 мы с помощью оператора assume сообщаем ассемблеру (программе-транслятору), что сегментные регистры CS и DS будут указывать на один и тот же сегмент text. Сегментные регистры (а всего их в процессоре четыре) играют очень важную роль. Когда программа загружается в память и становится известно, по каким адресам памяти она располагается, в сегментные регистры заносятся начальные адреса закрепленных за ними сегментов. В дальнейшем любые обращения к ячейкам программы осуществляются путем указания сегмента, в котором находится интересующая нас ячейка, а также номера того байта внутри сегмента, к которому мы хотим обратиться. Этот номер носит название относительного адреса, или смещения. Поскольку в единственном сегменте нашей программы будут размещаться и команды, и данные, мы указываем ассемблеру оператором assume (assume — предположим), что и сегментный регистр команд CS, и сегментный регистр данных DS будут указывать на сегмент text. При этом в регистр CS адрес начала сегмента будет загружен автоматически, а регистр DS нам придется инициализировать вручную.
Строго говоря, в приведенной программе, где нет прямых обращений к ячейкам сегмента данных, не было необходимости сопоставлять в операторе assume сегмент text с сегментным регистром DS (сопоставление сегмента команд с сегментным регистром команд CS обязательно во всех случаях). Учитывая, однако, что практически в любой разумной программе обращения к полям данных имеются, мы с самого начала написали оператор assume в том виде, в каком он используется в реальных программах.
Первые два предложения программы служат для передачи служебной информации программе ассемблера. Ассемблер воспринимает и запоминает эту информацию и пользуется ею в своей дальнейшей работе, однако в состав выполнимой программы, состоящей из машинных кодов, эти строки не попадут, так как процессору, выполняющему программу, они не нужны. Другими словами, операторы segment и assume не транслируются в машинные коды, а используются лишь самим ассемблером на этапе трансляции программы. Такие нетранслируемые операторы иногда называют псевдооператорами, или директивами ассемблера в отличие от истиных операторов — команд языка.
Предложение 3, начинающееся с метки begin, является первой выполнимой строкой программы. Для того, чтобы процессор знал, с какой строки начать выполнять программу после ее загрузки в память, начальная метка программы указывается в качестве операнда самого последнего оператора программы end (см. предложение 13). Можно подумать, что указание точки входа в программу излишне: ведь как будто и так ясно, что программу надо начать выполнять с начала, а закончить, дойдя до конца. Однако в действительности для программ, написанных на языке ассемблера, это совсем не так! Текст программы может начинаться с описания подпрограмм или полей данных. В этом случае предложение программы, с которого нужно начать ее выполнение, может располагаться где-то в середине текста программы. И завершается выполнение программы совсем не обязательно в ее последних строках, а там, где стоят предложения вызова специальной программы операционной системы, предназначенной именно для завершения текущей программы и передачи управления системе (см. предложения 8…10). Однако начиная от точки входа, программа выполняется строка за строкой точно в том порядке, в каком эти строки написаны программистом.
В предложениях 3 и 4 выполняется инициализация сегментного регистра DS. Сначала значение имени text (т.е. адрес сегмента text) загружается командой mov (от move, переместить) в регистр общего назначения процессора AX, а затем из регистра AX переносится в регистр DS. Такая двухступенчатая операция нужна потому, что процессор в силу некоторых особенностей своей архитектуры не может выполнить команду непосредственной загрузки адреса в сегментный регистр. Приходится пользоваться регистром AX в качестве «перевалочного пункта». Кстати, обратите внимание на то, что операнды в командах языка ассемблера записываются в несколько неестественном для европейца порядке — действие команды осуществляется справа налево.
Предложения 5, 6 и 7 реализуют существо программы — вывод на экран строки текста. Делается это не непосредственно, а путем обращения к служебным программам операционной системы MS-DOS, которую мы для краткости будем в дальнейшем называть просто DOS. Дело в том, что в составе команд процессора и, соответственно, операторов языка ассемблера нет команд вывода данных на экран (как и команд ввода с клавиатуры, записи в файл на диске и т.д.). Вывод даже одного символа на экран в действительности представляет собой довольно сложную операцию, для выполнения которой требуется длинная последовательность команд процессора. Конечно, эту последовательность команд можно было бы включить в нашу программу, однако гораздо проще обратиться за помощью к операционной системе. В состав DOS входит большое количество программ, осуществляющих стандартные и часто требуемые функции — вывод на экран и ввод с клавиатуры, запись в файл и чтение из файла, чтение или установка текущего времени, выделение или освобождение памяти и многие другие.
Для того, чтобы обратиться к DOS, надо загрузить в регистр общего назначения AH номер требуемой функции, в другие регистры — исходные данные для выполнения этой функции, после чего выполнить команду int 21h (int — от interrupt, прерывание), которая передаст управление DOS. Вывод на экран строки текста можно осуществить функцией 09h, которая требует, чтобы в регистре DX содержался адрес выводимой строки. В предложении 6 адрес строки message загружается в регистр DX, а в предложении 7 осуществляется вызов DOS.
После того, как DOS выполнит затребованные действия, в данном случае выведет на экран текст «Наука умеет много гитик» (помните одноименный карточный фокус?), выполнение программы продолжится. Вообще-то нам вроде ничего больше делать не нужно. Однако на самом деле это не так. После окончания работы программы DOS должна выполнить некоторые служебные действия. Надо освободить занимаемую нашей программой память, чтобы туда можно было загрузить следующую программу. Надо вызвать системную программу, которая выведет на экран запрос DOS и будет ждать следующей команды оператора. Все эти действия выполняет функция DOS с номером 4Ch. Эта функция предполагает, что в регистре AL находится код завершения нашей программы, который она передаст DOS. При желании код завершения только что закончившейся программы можно «выловить» в DOS и проанализировать, но сейчас мы этим заниматься не будем. Если программа завершилась успешно, код завершения должен быть равен 0, поэтому в предложении 9 мы загружаем 0 в регистр AL и вызываем DOS уже знакомой нам командой int 21h.
После последнего выполнимого предложения программы можно описывать используемые в ней данные. У нас в качестве данных выступает строка текста. Текстовые строки вводятся в программу с помощью директивы ассемблера db (от define byte, определить байт), и заключаются в апострофы. Для того, чтобы в программе можно было обращаться к данным, поля данных, как правило, предваряются именами. В нашем случае таким именем является вполне произвольное обозначение message, с которого начинается предложение 11.
Выше, в предложении 6, мы через регистр DX передали DOS адрес начала выводимой на экран строки текста. Но как DOS определит, где эта строка закончилась? Хотя нам конец строки в программе отчетливо виден, однако в машинных кодах, из которых состоит выполнимая программа, он никак не отмечен, и DOS, выведя на экран слово «гитик», продолжит вывод байтов памяти, расположенных за нашей фразой. Поэтому DOS следует передать информацию о том, где кончается строка текста. Некоторые функции DOS требуют указания в одном из регистров длины выводимой строки, однако функция 09h работает иначе. Она выводит текст до символа $, которым мы и завершили нашу фразу.
Директива ends (end segment, конец сегмента) в предложении 12 указывает ассемблеру, что сегмент text закончился.
Последняя строка программы содержит директиву end, которая говорит программе ассемблера, что закончился вообще весь текст программы, и больше ничего транслировать не нужно. В качестве операнда этой директивы, как уже отмечалось, обычно указывается точка входа в программу, т.е. адрес первой выполнимой программной строки. В нашем случае это метка begin.
Сайт управляется системой uCoz
netlib.narod.ru