TASM (Turbo Assembler)

__NUMBEREDHEADINGS__

Введение. С чего начать?

Прежде чем приступать непосредственно к программированию под архитектуру х86, для лучшего понимания, следует ознакомиться со статьями, описывающими её основные особенности. Это решит большую часть вопросов, возникающих в процессе написания кода, например : каким образом на таком низком уровне реализуется обмен данными между внутренним миром программы и внешней средой? Как организованна память? Как процессор понимает, что делать, если…?

Простые программы. И что дальше?

Рассмотрим общую структуру программы на ассемблере под х86, она выглядит примерно следующим образом:

;тип процессора, например 386р

.model flat, stdcall  ; модель памяти и вызова подпрограмм

;объявление включаемых (заголовочных) файлов, макросов, макроопределений, внешних определений

.data ; Инициализированные данные

.data? ; неинициализированные данные

.const ; константы

.code ; исполняемый код 

End;"метка точки входа"

Процессоры могут быть различны, при тип написании программы следует указать тот, с которым мы непосредственно будем работать.

Секция .data содержит инициализированные данные и включается в исполняемый файл.

Секция .data? содержит неинициализированные данные, она не включается в исполняемый файл и появляется только тогда, когда программа загружается в память. Секции .data, .data? имеют полный доступ к памяти.

Секция .const секция констант. Редко встречается в программах, потому что константы удобно задавать с помощью макроопределений.

Секция .code содержит исполняемый код программы. Секции .const и .code имеют атрибут доступа - только чтение.

В конце программы всегда нужно указывать end, что задаёт точку входа программы, т.е. место с которого начнётся выполняться программа.

При программировании под Win32 мы не имеем доступа к портам ввода вывода и не можем вызывать прерывания (основные команды в DOS), у нас есть только WIN API, которые экспортируют системные библиотеки. Вызывая API функцию, мы посути передаём управление точке входа функции (точка входа функции это её первая инструкция). API функции находятся в библиотеках kernel32.dll, user32.dll, gdi32.dll, advapi32.dll и т.д., для того чтобы использовать некоторую функцию этих библиотек надо сначала загрузить нужную библиотеку в свою память, либо включить используемые функции в таблицу импорта и тогда загрузчик загрузит библиотеки за нас. Каждая программа в Win32 в конце своего выполнения должна вызвать функцию ExitProcess,это легко объясняется тем, что после выполнения последней инструкции программы, дальше идёт пустота, даже если там что-то осмысленное, все равно у выделенная память не бесконечна, а после конца идёт невыделенная (т.е. несуществующая) память.

Помимо использования API ОС существует возможность использования функций из других библиотек, например из библиотеки языка Си. Такой вариант дает нам возможность компиляции программы в Linux.

Пример

Приступим непосредственно к коду. В качестве упражнения напишем знаменитую программу "Hello, World!"

.386                                
.model  flat

extrn ExitProcess:PROC  	
extrn MessageBoxA:PROC

.data    

Ttl	db "First ASSEMBLER program",0h
Msg	db 'Hello, World!!!!',0h

.code

start:
	push 0h
	push offset Msg
	push offset Ttl
	push 0h	
	call MessageBoxA
	push 0h	
	call ExitProcess
end     start

По сути, в здесь мы только вызываем функцию MessageBoxA, для вывода окна сообщения потом выходим из процесса.

Компиляция

В качестве примера рассмотрим компиляцию нашей программы, написанной на TASM (классическом языке). Для этого сохраним написанный код в файл с расширением *.asm и ассемблируем его. Для этого нам понадобится программа tasm32.exe она находится в папке %tasmdir%\bin\tasm32.exe, формат её вызова такой: TASM [options] source [,object] [,listing] [,xref] (доп сведения смотрите в tasm32 /?) для большинства программ ассемблирование проходит так:

 %tasmdir%\BIN\tasm32 /m3 /ml asmfile,,;asmfile - имя нашего файла БЕЗ расширения

Потом будет создан объектный файл, файл листинга и т.д. Теперь нам ннужно всё это слинковать. Для понадобится программа tlink32.exe. Вот формат её вызова:

 TLINK objfiles, exefile, mapfile, libfiles, deffile, resfiles

Теперь после вызова

 Compile sample

После чего мы получим исполняемый файл.

Усложним задачу

Пример Рассмотрим задачу поиска простых чисел с помощью Решета Эратосфена в версии языка NASM

Наш проект будет содержать следующие файлы:

  • main.asm – главный файл,
  • functions.asm – подпрограммы,
  • string_constants.asm – определения строковых констант,
  • Makefile – сценарий сборки

Рассмотрим код основного файла:


 %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 — освободить память, выделенную для флагов

О подпрограммах

Подпрограммы представляют собой метку, по которой располагается код. Заканчивается подпрограмма инструкцией ret.

input_max_number. Подпрограмма предназначена для ввода в программу максимального число, до которого будет производиться поиск простых. Ключевым моментов тут является вызов функции scanf из библиотеки Си

; Ввести максимальное число
; Результат: 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

Cначала в eax записывается адрес памяти на 4 байта ниже указателя базы стека. Это память, выделенная для локальных нужд подпрограммы. Указатель на эту память передается функции scanf как цель для записи данных, введенных с клавиатуры. После вызова функции, в eax из памяти перемещается введенное значение.

Не будем вдаваться в подробности и приводить примеры кода остальных подпрограмм, остановимся на их основных моментах.

allocate_flags_memory и free_flags_memory. Ключевыми местами этих подпрограмм являются вызовы функций malloc и free из библиотеки Си. malloc в случае удачи возвращает через регистр eax адрес выделенной памяти, в случае неудачи этот регистр содержит 0. Это самое узкое место программы касательно максимального числа. 32 бит вполне достаточно для поиска простых чисел до 4 294 967 295, но выделить разом столько памяти не получится.

find_primes_with_eratosthenes_sieve. Подпрограмма реализует классический алгоритм для вычеркивания составных чисел, решето Эратосфена, на языке ассемблера x86. Приятна тем, что не использует вызовы внешних функций и не требует обработки ошибок :)

print_primes. Подпрограмма выводит в консоль простые числа. Ключевым моментом тут является вызов функции printf из библиотеки Си.

Определение строковых констант

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

Сценарий сборки

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