«Чистый» язык ассемблера
Выбери формат для чтения
Загружаем конспект в формате docx
Это займет всего пару минут! А пока ты можешь прочитать работу в формате Word 👇
Глава 4. «Чистый» язык ассемблера
§ 4.1. Директивы
Инструкция (команда) - транслируется в исполняемый код.
Директива - не приводит к появлению нового кода, а управляет работой самого ассемблера.
Разные ассемблеры используют различные наборы директив!!!
Определение данных
имя_переменной dx значение
dx:
db определить байт;
dw определить слово (2 байта);
dd определить двойное слово (4 байта);
df определить 6 байт;
dq определить учетверенное слово (8 байт);
dt определить 10 байт (типы данных, используемые FPU).
text_string db 'Hello world!'
message db "Hi, people!!!"
number dw 7
table db 1,2,3,4,5,6,7,8,9,0Dh,0Ah,0
float_number dd 3.5e7
Имя переменной соответствует адресу первого из указанных значений.
mov al, text_string ;al == 48h (код 'H')
Переменная считается неинициализированной и ее значение на момент запуска программы может оказаться любым, если ее определить следующим образом:
i db ?
Если нужно заполнить участок памяти повторяющимися данными, используется специальный оператор dup:
massiv db 512 dup(?)
Создается массив из 512 неинициализированных байт, адрес первого байта хранится в переменной massiv.
Модели памяти и объявление сегментов
1. Модель памяти задается директивой
.model модель, язык, модификатор
Модель:
tiny код, данные и стек размещаются в одном сегменте размером до 64 килобайт.
small код размещается в одном сегменте, а данные и стек — в другом.
medium код размещается в нескольких сегментах, а все данные — в одном, поэтому для доступа к данным используется только смещение, а для вызова подпрограмм полные адреса;
lage, huge и код, и данные могут занимать несколько сегментов;
flat то же, что и tiny, но регистры 32-битные.
Язык (необязательный операнд):
c, pascal, basic, fortran если язык указан, то процедуры рассчитаны на вызов из программ на соответствующем языке высокого уровня.
Модификатор (необязательный операнд):
nearstack (по умолчанию)
farstack (сегмент стека не объединяется в одну группу с сегментами данных)
2. Сегмент кода описывается директивой .code
3. Сегмент стека описывается директивой .stack размер
Необязательный параметр указывает размер стека.
4. Сегмент данных описывается директивой .data
Структура программы
;Общая структура 16-ти разрядной программы на
;ассемблерае TASM/MASM имеет нижеследующий вид
;(для 32-разрядных приложений – другая модель памяти ;(например, flat))
.model small ;модель памяти
.stack 256 ;объем стека 256 байт
.data ;сегмент данных
;данные
.code ;сегмент кода
start:
;команды
end start
.model small ;модель памяти
.stack 256 ;объем стека 256 байт
.data ;начало сегмента данных
message db "Hi, people!!!$" ;$ \0
.code ;начало сегмента кода
start: ;ВЫВОД НА ЭКРАН ЗАДАННОЙ СТРОКИ
mov ax, @data
mov ds, ax ;в ds адрес сегмента данных
lea dx, message ;адрес message в ds:dx
mov ah, 9 ;9-я функция
int 21h ;21-го прерывания
;вывод на консоль информации по адресу из dx
mov ax, 4C00h
int 21h
;завершение выполнения программы
end start
Процесс разработки программы
Создание exe-файла:
TASM:
Трансляция: tasm.exe имя_файла.asm
Компоновка: link16.exe имя_файла.obj ,,,,,
MASM:
Трансляция: ml.exe /c имя_файла.asm
Компоновка: link16.exe имя_файла.obj ,,,,,
§ 4.2. Несколько решений одной задачи
Задача: даны две строки s1 и s2. Скопировать содержимое s1 в s2 и вывести s2 на экран.
Ассемблерная вставка в С++ (рассмотрена ранее)
#include
#include
void main(){
char s1[] = "Hi, people!!!\n";
int i = strlen(s1) + 1;
char s2[] = "123456789123456789\n";
_asm{
cld
mov ecx, i
lea esi, s1
lea edi, s2
rep movsb
}
std::cout << s2;
}
16-разрядная программа на ассемблере TASM/MASM
.model small
.stack 256
.data
s1 db 'Hi, people!!!',0Dh,0Ah,'$'
len equ $-s1
;equ - аналог #define
;len == адрес '$' минус адрес 'Н'
;в итоге len == длина строки s1
s2 db '123456789123456789',0Dh,0Ah,'$'
adr_s1 dd s1
;в adr_s1 адрес s1
adr_s2 dd s2
;в adr_s1 адрес s2
.code
start:
mov ax, @data
mov ds, ax
;теперь в ds адрес сегмента данных
cld
;флаг направления df = 0
mov cx, len
;cx = len
lds si, adr_s1
;поместить адрес из adr_s1 в ds:si
les di, adr_s2
;поместить адрес из adr_s2 в es:di
rep movsb
;rep повторяет команду movsb столько раз,
;сколько указано в cx
;movsb – копирование байта из si в di
lea dx, s2
;поместить адрес s2 в dx
mov ah, 9
int 21h
;вывод на экран информации по адресу из dx
mov ax, 4C00h
int 21h
;завершение выполнения программы
end start
Графическое win32-приложениe на ассемблере MASM
include def32.inc
include kernel32.inc
include user32.inc
.386
.model flat
.data
S db "My message",0
s1 db "Hi, people!!!",0
len equ $-s1
s2 db "123456789123456789",0
.code
_start:
;метка точки входа должна начинаться с подчеркивания
cld
mov ecx, len
mov esi, offset s1
;поместить адрес строки s1 в esi
mov edi, offset s2
;поместить адрес строки s2 в edi
rep movsb
push MB_ICONINFORMATION
;стиль окна
push offset s
;адрес строки с заголовком окна
push offset s2
;адрес строки с сообщением
push 0
;идентификатор предка
call MessageBox
;вызов системной функции
;«окно с указанным сообщением»
push 0
;код выхода
call ExitProcess
;вызов системной функции
;«завершение программы»
end _start
1. Чтобы вызвать системную функцию Windows, программа должна поместить в стек все параметры от последнего к первому и передать управление командой call. Все эти функции сами освобождают стек (завершаясь инструкцией ret n) и возвращают результат работы в регистре eax. Такая договоренность о передаче параметров называется STDCALL.
Соглашение CDECL (принято по умолчанию в C/C++): параметры также перечисляются справа налево, но стек освобождает вызывающая процедура (в этом случае вызываемая процедура должна завершаться инструкцией ret без параметра).
2. Прежде чем скомпилировать asm-файл, нужно создать inc-файлы, в которые необходимо поместить директивы, описывающие вызываемые системные функции.
Имена всех системных функций Windows модифицируются так, что перед именем функции ставится подчеркивание, а после - знак «@» и число байт, которое занимают параметры, передаваемые ей в стеке:
ExitProcess()_ExitProcess@4()__imp__ExitProcess@4
Соглашение CDECL (принято по умолчанию в С/C++):
ExitProcess()_ExitProcess()
В примерах мы будем обращаться напрямую к __imp__ExitProcess@4.
;===def32.inc===
MB_ICONINFORMATION equ 40h
;включаемый файл с определениями констант и типов
;для программ под win32 из winuser.h
;===файл kernel32.inc===
includelib kernel32.lib
;включаемый файл с определениями функций
;из kernel32.dll
extrn __imp__ExitProcess@4:dword
;истинные имена используемых функций
ExitProcess equ __imp__ExitProcess@4
;присваивания для облегчения читаемости кода
;===user32.inc===
includelib user32.lib
;включаемый файл с определениями функций
;из user32.dll
;это библиотека, в которую входят основные функции,
;отвечающие за оконный интерфейс
extrn __imp__MessageBoxA@16:dword
;истинные имена используемых функций
MessageBox equ __imp__MessageBoxA@16
;присваивания для облегчения читаемости кода
Для компиляции потребуются файлы kernel32.lib и user32.lib.
Создание exe-файла на ассемблере MASM:
Трансляция: ml /c /coff /Cp имя_файла.asm
Компоновка: link имя_файла.obj /subsystem:windows
Консольное win32-приложениe на ассемблере MASM
include def32.inc
include kernel32.inc
.386
.model flat
.data
mes dd ? ;переменная для функции WriteConsole
s1 db "Hi, people!!!",0Dh,0Ah,0
len equ $-s1
s2 db "123456789123456789",0Dh,0Ah,0
.code
_start:
;метка точки входа должна начинаться с подчеркивания
cld
mov ecx, len
mov esi, offset s1
;поместить адрес строки s1 в esi
mov edi, offset s2
;поместить адрес строки s2 в edi
rep movsb
push STD_OUTPUT_HANDLE
call GetStdHandle
;вызов системной функции
;«возврат идентификатора stdout в eax»
mov ebx, len
;в ebx длина строки для вывода
push 0
push offset mes
;адрес переменной, в которую будет занесено
;число байт, действительно выведенных на консоль
push ebx
;сколько байт надо вывести на консоль
push offset s2
;адрес строки для вывода на консоль
push eax
;идентификатор буфера вывода
call WriteConsole
;вызов системной функции
;«вывод строки на консоль»
push 0
;код выхода
call ExitProcess
;вызов системной функции
;«завершение программы»
end _start
1. Все функции, работающие со строками (как, например,
WriteConsole()), существуют в двух вариантах. Если строка рассматривается в обычном смысле, как набор символов ASCII, к имени функции добавляется «A» (WriteConsoleA()). Другой вариант функции, использующий строки в формате UNICODE (два байта на символ), заканчивается буквой «U». В примерах будем использовать обычные ASCII-функции.
2. Прежде чем скомпилировать asm-файл, нужно создать inc-файлы, в которые необходимо поместить директивы, описывающие вызываемые системные функции.
;===def32.inc===
STD_OUTPUT_HANDLE equ -11
; включаемый файл с определениями констант и типов для
;программ под win32 из winbase.h
;===kernel32.inc===
includelib kernel32.lib
;включаемый файл с определениями функций из
;kernel32.dll
extrn __imp__ExitProcess@4:dword
extrn __imp__GetStdHandle@4:dword
extrn __imp__WriteConsoleA@20:dword
;истинные имена используемых функций
ExitProcess equ __imp__ExitProcess@4
GetStdHandle equ __imp__GetStdHandle@4
WriteConsole equ __imp__WriteConsoleA@20
;присваивания для облегчения читаемости кода
Создание exe-файла на ассемблере MASM:
Трансляция: ml /c /coff /Cp имя_файла.asm
Компоновка: link имя_файла.obj /subsystem:console
Отладка win32-приложения в Microsoft Visual Studio
1. Создайте новый консольный проект.
2. Добавьте в него уже существующий asm-файл.
3. Заголовочные inc-файлы должны находиться в одной папке с asm-файлом.
Процедуры
;процедура на ассемблере TASM/MASM
имя_процедуры proc
;точка входа в процедуру
;имя_процедуры считается меткой
;...
ret
;возврат в вызывающую процедуру
имя_процедуры endp
Если процедура должна возвращать результат, он помещается в регистр eax
имя_процедуры proc
;...
mov eax, результат
ret
имя_процедуры endp
Как передать параметры процедуре?
1. Передача параметров процедуре через стек (этот способ рассмотрен ранее).
2. Передача параметров процедуре через регистры (см. пример).
;консольное win32-приложениe, демонстрирует передачу
;параметров процедуре через регистры
_start:
...
mov ebx, len
;в ebx длина строки для вывода
lea esi, s2
;в esi адрес строки s2
call output_string
;вызов нашей процедуры
push 0
;код выхода
call ExitProcess
;вызов системной функции
;«завершение программы»
;процедура вывода строки на экран
;в eax - идентификатор буфера вывода
;в esi - адрес строки
;в ebx - длина строки
output_string proc
push 0
push offset mes
;адрес переменной, в которую будет занесено
;число байт, действительно выведенных на консоль
push ebx
;сколько байт надо вывести на консоль
push offset s2
;адрес строки для вывода на консоль
push eax
;идентификатор буфера вывода
call WriteConsole
;вызов системной функции
;«вывод строки на консоль»
ret
output_string endp
end _start
Интерфейс с C/С++
В файле str.asm содержится код процедуры, копирующей содержимое одной строки в другую. Имя процедуры соответствует требованиям соглашения STDCALL («0» в конце имени означает, что процедуре параметры не передаются).
;===str.asm===
.386
.model flat
.data
s1 db "Hi, people!!!",0Dh,0Ah,0
len equ $-s1
s2 db "123456789123456789",0Dh,0Ah,0
.code
_str2str@0 proc
cld
mov ecx, len
mov esi, offset s1
;поместить адрес строки s1 в esi
mov edi, offset s2
;поместить адрес строки s2 в edi
rep movsb
mov eax, offset s2
ret
_str2str@0 endp
end
В заголовочном файле str.h содержится объявление функции, написанной на ассемблере. Ключевое слово extern означает, что функция является внешней; оператор "C" необходим для компилятора C/C++, директива __stdcall устанавливает соглашение об именовании.
//===str.h===
extern "C" char* __stdcall str2str();
В файле main.cpp вызывается функция, реализованная на ассемблере.
//===main.cpp===
#include
#include "str.h"
void main(){
printf("%s",str2str());}
§ 4.3. Пример процедуры с параметрами
Реализуем теперь функцию intMint(int a, int b), которая имеет два целочисленных параметра и в качестве результата возвращает их разность (a – b).
Первым помещается в стек параметр b;
вторым – параметр a;
A – адрес команды, следующей после вызова функции, это точка возврата.
1. Вызов функции intMint(int a, int b):
Старшие адреса
esp+8 esp
Младшие адреса
…
…
b
a
A
esp+4
Так как указатель стека esp в процессе выполнения функции может изменяться, его значение сохраняется в регистре ebp (предварительно сохранив ebp в стеке), чтобы иметь возможность работать с параметрами функции:
push ebp
mov ebp,esp
2. Содержимое стека после push ebp и mov ebp,esp:
Старшие адреса
esp+12 esp+4
Младшие адреса
…
…
b
a
A
ebp
esp+8 esp==ebp
3. Содержимое стека после завершения функции:
Старшие адреса
esp+4
Младшие адреса
…
…
b
a
A
ebp
esp
4. Надо сместить esp на 8 байт в сторону старших адресов:
либо ret 8 - в вызываемой процедуре (STDCALL)
либо add esp 8 - в вызывающей процедуре (CDECL)
Старшие адреса
Младшие адреса
…
…
b
a
esp
;======Файл functions.asm======
.386
.model flat
.data
.code
_intMint@8 proc
;8 байт приходится на два целочисленных параметра
push ebp
mov ebp,esp
;если esp изменится, через ebp можно
;добраться до параметров
mov eax,[ebp+8]
;eax = a
sub eax,[ebp+12]
;eax = eax – b (eax == (a - b))
;теперь в eax результат работы процедуры
pop ebp
;вернули ebp в исходное состояние
ret 8
;очистка стека
_intMint@8 endp
//=====Файл functions.h=====
extern "C" int __stdcall intMint(int a, int b);
//=====Файл main.cpp=====
#include
#include "functions.h"
int a = 10, b = 2;
void main(){
printf("%i\n",intMint(a,b));
}
В заключении реализуем аналогичную функцию, также вычисляющую разность, но уже вещественных чисел. Основное отличие заключается в том, что результат вычислений не входит в регистр eax (вещественное число занимает 8 байт), а значит функция должна вернуть адрес вещественного числа.
;======Файл functions.asm======
.386
.model flat
.data
rezult dq ?
.code
_doubleMdouble@16 proc
;16 байт приходится на два вещественных параметра
push ebp
mov ebp,esp
;если esp изменится, через ebp можно
;добраться до параметров
finit
;инициализация сопроцессора
fld qword ptr [ebp+8]
;st(0) = a
fsub qword ptr [ebp+16]
;st(0) = st(0) – b теперь st(0) == (a - b)
fstp rezult
;rezult = st(0) и вытолкнуть
;теперь rezult == (a - b)
lea eax,rezult
;поместить адрес rezult в eax
;теперь в eax результат работы процедуры
pop ebp
;вернули ebp в исходное состояние
ret 16
;очистка стека
_doubleMdouble@16 endp
//=====Файл functions.h=====
extern "C" double* __stdcall doubleMdouble
(double a, double b);
//функция возвращает адрес вещественного числа
//=====Файл main.cpp=====
#include
#include "functions.h"
double x = 10.5, y = 2.5;
void main(){
printf("%lf\n",*doubleMdouble(x,y));
//* - это операция разадресации
}
ТЕСТ № 2