Ассемблер примеры программ – Ассемблер для начинающих. Примеры простых программ | Perl, Python

Содержание

Простая программа на ассемблере 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 блоков, оформленных в виде подпрограмм:
  1. input_max_number — с помощью консоли запрашивает у пользователя максимальное число, до которого производится поиск простых; во избежание ошибок значение ограничено константами MIN_MAX_NUMBER и MAX_MAX_NUMBER
  2. allocate_flags_memory — запросить у ОС выделение памяти для массива пометок чисел (простое/составное) в куче; в случае успеха возвращает указатель на выделенную память через регистр eax
  3. find_primes_with_eratosthenes_sieve — отсеять составные числа с помощью классического решета Эратосфена;
  4. print_primes — вывести в консоль список простых чисел;
  5. free_flags_memory — освободить память, выделенную для флагов

Для функций было условлено такое правило: значение возвращается через регистр eax, регистр edx содержит статус. В случае успеха он содержит значение SUCCESS, то есть, 0, в случае неудачи — адрес строки с сообщением об ошибке, которое будет выведено пользователю.

Файл string_constants.asm содержит определение строковых переменных, значения которых, как намекает название файла, менять не предполагается. Только ради этих переменных было сделано исключение к правилу «не использовать глобальные переменные». Я так и не нашел более удобного способа доставлять строковые константы функциям ввода-вывода – подумывал даже записывать на стек непосредственно перед вызовами функций, но решил, что эта идея куда хуже идеи с глобальными переменными.

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 секунд с Решетом Эратосфена.

Это, конечно, скорее байка, чем научный факт, поскольку не было серьезного тестирования не было выяснения причин, но как интересный факт для завершения статьи подойдет, как мне кажется.

Полезные ссылки

  1. Список ресурсов для изучения ассемблера
  2. Организация памяти
  3. Решето Эратосфена
  4. Решето Аткина
  5. Стек
  6. Стековый кадр

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

Простейшая программа на ассемблере (beeper)

  Макс Петров май 2013

      Ниже приведена программа на языке ассемблера 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, она описывается следующим образом:
      Function Beep Lib "kernel32" Alias DWORD "Beep" (DWORD dwFreq, DWORD dwDuration).
Из описания видно, что эта функция содержится в системном (входящем в состав операционной системы Windows) файле kernel32.dll. Поэтому, выше в программе нами были подключены файлы kernel32.inc и kernel32.lib, где содержатся необходимые ассемблеру инструкции для работы с библиотекой kernel32.dll.

      Функции Beep должны быть переданы два значения - DWORD dwFreq и DWORD dwDuration. Запись DWORD (double word) конкретизирует размер памяти, которую занимает переменная. Double word - это два машинных слова (четыре байта или 32 бит). Указание размера памяти, отводимой под переменную, важно потому, что обмен данными с API-функциями программы производят через стек, в стек перед вызовом API-функции программа должна поместить как раз столько байт, сколько из него затем будет извлечено API-функцией. В противном случае вызов API-функции или исполнение программы после вызова может завершиться ошибкой.

      И, наконец, dwFreq - это частота звука в Герцах, dwDuration - длительность звука в миллисекундах. В ассемблере передаваемые функции параметры записываются после ее имени через запятую, в том же порядке, что и в описании функции. Например, запись invoke Beep, 784, 900 после компиляции даст ноту соль второй октавы (ее частота 784 Гц) с длительностью звучания 0,9 секунды.

     


Добавить комментарий
   Вовка   12.11.2016   20:38

Ну наконец то нашёл нормальный код, удалось без проблем скомпилировать и отлинковать пример кода ассемблера в masm32. Спасибо конечно большое, особенно спасибо за подробный комментарий в коде.

   Макс   13.11.2016   08:53

Спасибо за отзыв, рад, что удалось помочь.

   1   08.01.2017   16:27

Хорошие статьи, спасибо 🙂

   MASM32   29.05.2017   03:02

=)

   вася   16.03.2018   04:05

скопировал код, вставил, а он без переносов.

   Макс   16.03.2018   04:32

Не соображу, какие переносы?

   Иван   20.06.2019   21:34

- Ёбаный рот этого assembler, блядь! Ты кто такой, сука, чтоб это сделать?
– Я всегда это делал, когда..
– ВЫ ЧЁ, ДЕБИЛЫ? Вы чё, ебанутые, что ли? Действи.. вы в натуре ебанутые? Эта сидит там, чешет колоду, блядь. Этот стоит, грит: "Я те щас тут тоже раздам"..
– Ну посмотрите..
– ЁБ ТВОЮ МАТЬ! У вас дилер есть, чтобы это делать на моих глазах, мудак ёбаный!
– Хорошо, будет делать дилер. Раньше это делал всегда я..
– ДЕГЕНЕРАТ ЕБУЧИЙ! Вот пока ты это делал, дебил, ебаная сука, БЛЯДЬ, так все и происходило!
– В ВИПе?
–  В ХУИПЕ! Блядь, вы чё, действительно идиоты, что ли, а? Бля, дифиченты какие-то, ёбаный ваш рот, а.. А ты-то чё делаешь?
– Они разложены просто в другом порядке..
– ЁБАНЫЙ ТВОЙ РОТ! КАКОГО ХУЯ ОНИ В ДРУГОМ ПОРЯДКЕ РАЗЛОЖЕНЫ? Ты распечатала колоду на моих глазах, БЛЯДЬ! Как они могут быть там разложены в другом порядке?!
– В другом! Вот смотрите..
– ЁБАНЫЙ ТВОЙ РОТ, БЛЯДЬ! Вы чё, в киосках их заряжаете?! Сука ёбаная, падла блядская!
– Производители карт..
– ТЫ, МУДИЛА ГОРОХОВАЯ! Как заряжен.. Как запечатанная переменная может быть в другом порядке нн.. разложена?! Ты, долбоёб ёбаный!
– Докажу! докажу!
– ТЫ, МУДИЛА ЕБУЧАЯ, ВЫ ВО ЧТО ИГРАЕТЕ, СУКА ЁБАНАЯ, ПАДЛА?!
– Вот смотрите..
– Я РОТ ТВОЙ ЕБАЛ! Так вы зззаря.. вы, БЛЯДИ, покупайте регистры не в киосках! Вы чё, ебанутые, сука?!
– Фабрика в таком их виде..
– ТЫ МУДИЛА! Как может в assembler быть переменная зааа.. разложена в другом порядке?! Ты чё, бредишь, что ли?! Ёбаный твой рот, а!..
– Так вот и разложены..
– ТЫ ЧЁ, БРЕДИШЬ, СУКА?!
– Успокойтесь.. Вот, посмотрите, как они разложены..
– БЛЯДЬ! ДЕГЕНЕРАТИВНОЕ ХУЙЛО! Ты бредишь, что ли?! Ты чё, бредишь, блядь?! Как в assembler могут быть регистры по-другому разложены?! Ты чё, дурак, блядь?
– Если я разложу вот так вот, да?..
– Ёбаный козел! Ай фак ю булщит! ЩИТ!
– Вы специально..
– Я специально! Я щас им расскажу, что вы тут исполняете! Вы чё, дебилы, блядь?!
– Как вы хотите?..
– ВЫ ЧЁ, ДЕБИЛЫ, СУКА?! Как в assembler в запечатанной пачке может быть разложены по-другому регистры?! Вы чё?!
– Посмотрите..
– ТЫ, МУДИЛО ГОРОХОВОЕ! Вы их где берете, бляди?!
– Покупаем у официальных..
– ВЫ МУДИЛЫ!! Вы чё, е.. блять.. Ёб твою мать, в assembler, сука, регистры разложены по-другому.. ТЫ ЧЁ, ДУРАК, ЕБАНЫЙ ТВОЙ РОТ, А?! Ты чё, кретин, что ли?
– Как вы хотели, скажите мне? Как вы хотите?
– Ты, дегенеративный кретин, ты не понимаешь, что ты говоришь вообще!
– Что есть, то и говорю..
– Ты говоришь, что в assembler в запечатанных переменных регистры разложены по-другому?!
– Да, смотрите. Туз не на месте..


Ассемблер MASM32

      Простейшая программа на ассемблере (beeper)
      Переменные и типы данных ассемблера
      Регистры процессора IA32
      Консоль ввода-вывода
      API-функция CharToOem и строки ассемблера
      API-функция ReadConsoleInput
      API-функция PeekConsoleInput
      События консоли (таблица)
      Системы счисления, тэги ассемблера, перевод чисел
      Отрицательные числа
      Инкремент и декремент
      Деление (DIV, IDIV)
      VKDEBUG
      Макросы ассемблера
      Воспоминание об Альгамбре на системном динамике
      Командная строка
      Пузырьковая сортировка. Эстафета шариков
      Сортировка расческой
      Быстрая сортировка

     

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
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
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

Отправить ответ

avatar
  Подписаться  
Уведомление о