Отправляет email-рассылки с помощью сервиса Sendsay

Assembler - Просто и Эффективно.

  Все выпуски  

Assembler - Просто и Эффективно. Глава #13 - Программирование в MASM


Assembler

Глава #13 - Программирование в MASM

  Программирование в MASM
Компиляторы языка Assembler несколько отличаются друг от друга т.к не существует единого стандарта. Обычно для Intel совместимых процессоров используется синтаксис команд, указанный в документации от Intel и AMD (называемый Intel), сами же команды и их использование описаны в документации с официальных сайтов производителей процессоров.
Но, кроме команд, в исходном коде программы также встречаются специальные символы и директивы, указывающие компилятору как нужно компилировать программу. Обычно, именно эти элементы программы отличаются для разных компиляторов.
Для программирования на Assembler'е под Windows чаще всего используется компилятор MASM, поэтому желательно знать особенности программирования на нем. Многие другие компиляторы пытаются быть совместимыми с MASM, поэтому данный материал частично пригоден и для их использования.

Об использовании чисел можно посмотреть в главе #2, о работе с памятью в главе #4 и компилирование программы показано в главе #8. Повторно этот и другие, уже изложенные материалы рассмотрены не будут.
Дополнительную информацию можно найти в пакете masm32 - \masm32\help\masm32.hlp а также в пакете с компилятором MASM (если он в виде отдельного продукта) - \bin\qh.exe (переменная среды HELPFILES должна указывать на папку с файлами справки - \help в каталоге установленного MASM'а).
Краткое описание MASM есть также в MSDN: .NET Development -> Visual Studio .NET -> Product Documentation -> Visual C++ -> Reference -> Microsoft Macro Assembler Reference.

Данная глава посвящена, в основном, макросредствам компилятора MASM, его элементам хранения данных и улучшения читаемости исходного кода.
   -------------------------------

  Зачем нужны разнообразные макросредства?
Компилятор нужен для автоматизации вычислений, необходимых при создании программы.
Пример: jmp AnyLabel - безусловный переход на метку AnyLabel
  • Если знаковое смещение от метки AnyLabel до конца этой команды поместится в байт, то создается следующий код:
    db 0EBh,AnyLabel-($+1)
  • Если разница больше байта, то:
    db 0E9h
    dd AnyLabel-($+4)
Как видно из примера, код команды и операнд зависят от текущего адреса (обозначен символом "$") и от адреса метки AnyLabel. Поэтому программу удобнее писать не в hex редакторе, а используя компилятор, который будет производить все нужные вычисления при модификации программы.

В отличие от компиляторов языков высокого уровня, Assembler не "думает" за программиста, а только производит "механические" вычисления, тем самым не искажая оригинальные мысли автора программы. Чем больше компилятор будет считать, тем проще будет написание программы, не ухудшая откомпилированный вариант. Также, получится более понятный и организованный исходный код, который будет легче воспринимать и модифицировать в дальнейшем.
Но, самое главное - нужно устанавливать зависимость одних участков программы от других, где это возможно (как в примере с командой jmp). Это избавит вас от рутинной переработки исходника при модификации, и от возможных ошибок, возникающих при этом.
   -------------------------------

  Оформление исходного кода
Исходник может содержать пояснения для программистов каким образом работает та или иная часть программы. Желательно, чтобы вначале исходного файла, перед процедурами и другими оконченными логическими блоками были комментарии, вкратце описывающие принцип работы и способ использования.

Следующий тип комментария удобен для пояснения небольших участков кода. Текст, находящийся за символом ";" и до конца строки игнорируется компилятором. пример:
xchg eax,eax ;поменяет местами значения регистров eax и eax
Также бывают блочные комментарии, исключающие целый блок текста:
comment @ Текст
пояснения @
после директивы comment указывается символ, который будет означать последнюю строку блочного комментария.

Если строка слишком длинная, ее можно разделить на несколько, используя символ "\"
Пример:
xchg eax,\ ;начало строки
    eax ;продолжение - конец

Большие программы удобно разделять на несколько файлов, после чего можно использовать директиву "include имя файла", для их объединения. Файл, указанный этой директиве компилируется также, как будто его содержимое находится вместо директивы include.
   -------------------------------

  Выражения
Вместо непосредственно указанных чисел, в программе можно использовать выражения, автоматически вычисляемые при компиляции. В выражениях можно использовать следующие операторы: +, -, *, /, mod (остаток от деления), div, and, or, xor, not, shr, shl, скобки для указания порядка вычислений и т.д.
Пример:
mov eax,2*(3+4) ;откомпилируется как mov eax,14
mov ecx,3 shl 2 ;mov ecx,12
Также, можно использовать переменные в выражениях. Переменными желательно обозначать числа, многократно используемые в программе. Пример:
WindowSize=100+5 ;создадим переменную WindowSize, имеющую значение 105
mov eax,WindowSize+2 ;mov eax,107
mov ecx,WindowSize+4 ;mov ecx,109
Такие переменные не являются ячейками памяти программы, а существуют только для компиляции. Значение переменной может быть изменено. Переменные обычно объявляются в одном месте программы для удобной настройки ее работы, а в больших программах выносятся в отдельный файл.
При помощи директив equ и textequ можно указать строку в качестве значения переменной. Объявленная при помощи директивы equ переменная может быть как текстовой так и числовой, в зависимости от того сможет ли компилятор интерпретировать ее значение как число или нет при инициализации. Тип такой переменной в последующим не может быть изменен. Для указания строк, их заключают в угловые скобки. Длинна строки не может превышать 255 символов. Для указания самих скобок и других специальных символов в качестве текста, перед ними ставится знак "!"
Пример:
ProgName equ "Any Program"
WindowTitle db "This program name is: ", ProgName
   -------------------------------

  Структуры
Структуры нужны для того чтобы задать формат хранения группы данных, используемых в программе. Используются обычно для упрощения модификации формата хранения данных а также для согласованности форматов данных между разными модулями и частями программы.
Пример:
AnyStruct struct
  AnyDword dd ?
  Any3Words dw 10,20,30
  TString5 db 3 dup ('z')
AnyStruct ends

Данная форма записи объявляет формат группы данных (структуру) под названием AnyStruct (название является типом группы данных). Она не создает данных непосредственно, но позволяет задать их значения по умолчанию, если при инициализации они опущены.

После объявления структуры, например, можно использовать следующие выражения:
sizeof(AnyStruct)- размер структуры в байтах (=13)
AnyStruct.Any3Words- (=4)

Структуры позволяют также задать выравнивание (указывается как число после слова struct) для выравнивания элементов структуры. Также, структура может содержать выражение org Число, указывающее смещение структуры.
После описания, можно объявлять данные указанного типа, например:
  StructTest AnyStruct <>
Создаст следующие данные:
  StructTest.AnyDword dd ?
  StructTest.Any3Words dw 10,20,30
  StructTest.TString5 db 3 dup ('z')
Т.к все параметры структуры не инициализированы (не указаны в угловых скобках), то область памяти StructTest имеет значения всех элементов структуры AnyStruct по умолчанию

Пример инициализации структуры:
  TStruct2 AnyStruct <2,<5,,8>,"ttt">
Получим следующие данные:
  TStruct2.AnyDword dd 2
  TStruct2.Any3Words dw 5,20,8
  TStruct2.TString5 db "ttt"
В данном примере опущено только второе слово из Any3Words, и принимает значение по умолчанию, остальные данные инициализируются значениями, указанными в угловых скобках.

После инициализации, к данным можно обращаться следующим образом:
  mov [TStruct2.AnyDword],10
  inc [TStruct2.Any3Words]
Если адрес структуры, например, находится в регистре eax, то:
  assume eax: ptr AnyStruct
  mov ecx,[eax].AnyDword
  mov bx,[eax].Any3Words
  assume eax:nothing
либо
  mov eсx,[eax].AnyStruct.AnyDword

Кроме структур, можно создать еще и следующие типы данных: union (объединение) и record (запись). Чаще всего они встречаются внутри структур.

  union
Представляет одну и туже область памяти как разные типы данных. Используется обычно в случаях, когда исключено одновременное хранение перечисленных данных а также для ссылки на одну и туже область данных как на разные типы, в зависимости от имени используемого элемента. Размер объединения равен максимальному размеру перечисленного элемента. Указываются объединения почти также, как и структуры:
AnyUnion union
  FullDword dd ?
  LowestWord dw ?
  LowestByte db ?
AnyUnion ends

TUnion AnyUnion <12345678h>
После этого TUnion.FullDword = 12345678h, TUnion.LowestWord=5678h, а TUnion.LowestByte=78h

  record
Используется для описания группы бит. Позволяют удобно использовать выражения с битами, и создавать битовую маску.
Пример - запись, описывающая число Real4 (с.м предыдущую главу):
FpuReal4 record Sign:1, Exponent:8=127, Significand:23
mov eax,FpuReal4 <1,,400000h> ;mov eax,-1.5

record поддерживает следующие выражения: mask - получить маску записи/поля width - узнать размер в битах записи/поля. В выражениях, название элемента записи равно смещению (в битах) от начала записи.
Пример:
  FpuReal4 record Sign:1, Exponent:8=127, Significand:23
  mov ecx,FpuReal4 <0,128,0>> ;2.0
;получим значение поля Exponent в регистр eax
  mov eax,ecx
  shr eax,Exponent
  and eax,mask Exponent shr Exponent ;eax=128
   -------------------------------

  Макросы
Макросы используются для автоматизации создания часто повторяющихся участков программы. Обычно эти участки схожи по смыслу между собой, но несколько отличаются в реализации, либо слишком малы для выноса в процедуру. Макрос представляет из себя описание алгоритма для генерации исходного кода, интерпретируемого при компиляции.
Существует два типа макросов: макро-функции и макро-процедуры.
Макро-процедуры при вызове просто заменяются на последовательность команд, вызываются аналогично обычной команде, и может быть вызваны только в начале строки.
Макро-функции отличаются тем, что могут возвращать некое значение, и могут вызываться в выражениях. Параметры вызова указываются в круглых скобках. Для указания макро-функции внутри макроса используется директива "EXITM <возвращаемый текст>"
Макросы могут иметь локальные переменные. Объявляются они перечислением через запятую после слова "local". Они существуют только в самом макросе, и поэтому не создают конфликт с уже объявленными в других местах переменными с таким же именем.
Внутри макросов можно использовать специальные макро- комментарии, указываемые после символов ";;". Они не попадают в листинг.

Пример макро-процедуры:
ZeroEax macro ;объявим макро-функцию
  xor eax,eax
endm ;конец макроса

После объявления макрос можно использовать также, как и команду процессора.
Пример:
ZeroEax ;откомпилируется как xor eax,eax

Макрос может иметь параметры
Пример макро-функции:
XorAndRet macro reg
  xor reg,reg
  EXITM <reg>
ENDM
  ...
neg XorAndRet(eax) ;Откомпилируется в xor eax,eax и neg eax

Пример #2:
ZeroReg macro param1:=<eax>
  xor param1,param1
ENDM
  ...
RVar equ edx
ZeroReg ecx ;откомпилируется как xor ecx,ecx
ZeroReg RVar ;раскроется как xor RVar,RVar после откомпилируется как xor edx,edx
ZeroReg ;параметр неуказан, используется дефолтное значение (получим xor eax,eax)

Как видно из примера, компилятор просто производит поиск текста "param1" в теле макроса, заменяет его на указанное в качестве параметра значение, и после компилирует. Этот процесс часто называется "раскрытием макроса". Это важно понимать для написания макросов.
Для того, чтобы передать в качестве параметра именно значение переменной нужно поставить перед ней знак %, а для того, чтобы раскрыть макросы во всей строке, нужно поставить этот же знак в начале строки.
Пример:
ProgName equ <Any Program>
%WindowTitle db "This program name is: &ProgName&"
Для того чтобы отделить имя переменной от текста, используется символ "&" Пример:

moveXx macro dest,src
  mov e&dest&x,e&src&x
ENDM
moveXx a,b ;откомпилируется в mov eax,ebx

Параметров может быть несколько, можно задавать значения по умолчанию (как в примере ZeroReg), можно также указывать следующие модификаторы:
-req - обязательный параметр. Если при вызове он не указан то компилятор выдаст ошибку.
-vararg - последний параметр может быть vararg. Это означает, что все, что указанно при вызове макроса в качестве параметров присваивается этому параметру как единая текстовая строка.
Пример:
DisplayNote macro etxt:vararg
% echo Note: @FileCur(@CatStr(%@Line)) etxt
ENDM
  ...
DisplayNote any,test,params ;comment

Переменной etxt присвоится текст "any,test,params" как единый параметр т.к она vararg. Если убрать слово vararg и двоеточие, то переменной etxt присвоится только текст "any", и при компиляции появится предупреждение о том, что указаны лишние параметры при вызове (каковыми компилятор посчитает слова "test" и "params"). Параметры vararg обычно используются при создании макросов с переменным количеством параметров.

Для создания более функциональных макросов, существуют директивы, позволяющие организовывать циклы, ветвления и т.д. Также, большинство из них можно использовать и вне макросов.

Организовать цикл можно при помощи следующих директив:
FOR, FORC, REPEAT, WHILE, и GOTO
FOR parametr[:req | :=значение по умолчанию], <текстовая строка>
Данная директива повторяет цикл столько раз, сколько параметров содержится в указанной текстовой строке. Первый операнд для FOR указывает имя переменной, которой присваивается текущий параметр для каждой итерации. Можно задать также значение по умолчанию, или модификатор req.
Пример:

ZeroRegs macro reglist:vararg
  FOR curparam:=<eax>,<reglist>
    xor curparam,curparam
  ENDM
ENDM
  ...
ZeroRegs ebx,,ecx ;откомпилируется в xor ebx,ebx / xor eax,eax и xor ecx,ecx

Пример#2. Следующая запись в исходном коде обнулит значения регистров, указанных в угловых скобках:
FOR curreg,<eax,ebx,ecx,edx,esi,edi>
   xor curreg,curreg
ENDM

Для преждевременного прерывания цикла или макроса используется директива EXITM Остальные директивы работают схожим образом:
Директива FORC parametr, <строка> организовывает цикл, повторяющийся для каждого символа заданной строки, REPEAT выражение - заданное количество раз, WHILE выражение - пока истинно (не нулевое) значение выражения. Конец блока обозначается директивой ENDM.

GOTO макрометка - осуществляет безусловный переход на указанную макрометку, которая задается следующим образом :LabelName.

Часто в макросах встречаются директивы условного ассемблирования. Эти директивы проверяют заданное условие, и на основе этого решают компилировать указанный блок или нет. Конец блока обозначается директивой ENDIF
IFне нулевой (истина)
IFEнулевой (ложен)
IFBtесли выражение пустое
IFNBесли выражение не пустое
IFDEFесли переменная определена
IFNDEFесли переменная не определена
IFDIFтекстовые строки разные
IFDIFIтекстовые строки разные (без учета регистра)
IFIDNтекстовые строки идентичны
IFIDNIтекстовые строки идентичны (без учета регистра)


Пример:
IFDEF AnyVar
  .. ;откомпилируется если определена переменная AnyVar
ELSEIFDEF AnySecondVar
  .. ;откомпилируется если определена переменная AnySecondVar
ELSE
  .. ;откомпилируется если переменные AnyVar и AnySecondVar не были определены.
ENDIF

IFB и IFNB часто используются для проверки: указан ли параметр макроса при вызове или нет. Ключ компилятора "/Dимя=значение" позволяет определить переменную и задать ей значение. Поэтому, используя директивы условного ассемблирования можно написать исходный код, зависящий от параметров компиляции.

Для каждой из этих директив условного ассемблирования существует аналогичная версия директивы ELSE. Директивы сравнения строк имеют два параметра, а остальные - один.

Существует директива .ERR [текст] для отображения ошибки, и ее версии с окончаниями, как у директивы IF для отображения ошибки при определенном условии.

Для операции сравнения используются следующие операторы:
EQ (равно), NE (не равно), GT (больше чем), LT (меньше чем), GE (больше или равно), LE (меньше или равно), которые возвращают 0 если ложно, или -1 если истинно. Они часто используются в директивах условного ассемблирования.

Пример - макрос, печатающий числовой параметр - слово или другой тип:
IsWord macro number:req
IF number GT 0FFh and number LE 0FFFFh
  echo number is word
ELSE
  echo number is not word
ENDIF
ENDM

Существуют также ф-ции работы со строками, это
названиепараметрыдействие
InStr[начальная позиция,] строка, подстрокапоиск подстроки в строке
возвращает позицию, или 0 при отсутствие подстроки
SubStrстрока, начало [,длинна]выборка подстроки из строки
если длинна не указанна, то возвращает часть от указанной позиции и до конца строки.
SizeStrстрокаполучить размер строки
CatStr[строка, строка, строка, ...]объединение множества строк

Используются эти директивы следующим образом (пример):
StringList texteq @CatStr(<String1>,<String2>)
StringList CatStr <String1>,<String2>
Т.е эти директивы с префиксом "@" возвращают текстовое значение, а без префикса используются для непосредственного объявления текстовых переменных.

Существуют также директивы, возвращающие какие-либо значения программной среды. Ниже перечислены основные из них:
@CurSegИмя текущего сегмента
@DateТекущая дата
@Environ(имя)Получить значение переменной окружения
@FileCurИмя текущего файла
@FileNameИмя главного файла программы
@LineНомер строки исходного кода
@TimeВремя
@VersionВерсия компилятора
@WordSizeРазмер слова процессора

  Листинг
При компиляции программы компилятор может создать т.н листинг - файл, описывающий результат компиляции.
Эта ф-ция часто используется для отладки макросов, и т.д.
Для создания листинга нужно указать компилятору MASM дополнительную опцию: /Flимя файла
Можно использовать следующие директивы для улучшения читаемости листинга:
.LIST;запись в листинг
.LISTALL;запись всего возможного в листинг
.LISTMACRO;запись макросов в листинг
.LISTMACROALL;запись макросов с комментариями
.NOLIST;прекратить запись в листинг
.CREF;запись символов
.NOCREF;прекратить запись символов.

Обычно, при подключении файлов с константами для Windows отключают литинг, чтобы было легче в нем разобраться, а после включают обратно.
   -------------------------------

  Высокоуровневые операторы передачи управления.
В MASM присутствует поддержка высокоуровневых операторов передачи управления. Они часто используются для облегчения восприятия исходного кода:
Директивы .IF, .ELSE, .ELSEIF, .ENDIF используются для организации ветвлений.
.WHILE/ .ENDW и .REPEAT/ .UNTIL[CXZ] позволяют организовать циклы.
.BREAK производит выход из цикла, а .CONTINUE - переход на новую итерацию.

Данным операторам можно задавать условие, используя в выражениях следующие знаки сравнения: == равно, != не равно, > больше, >= больше или равно, < меньше, <= меньше или равно, & значение заданного бита, ! отрицание, && и, || или.
А также: CARRY?, OVERFLOW?, PARITY?, SIGN?, ZERO? для проверки значений соответствующих флагов.
   -------------------------------

  Пример:
Данный пример состоит из двух файлов. MacroTest.mac - файл с макросами-примерами, и программы - использующей эти макросы.

Содержимое файла MacroTest.mac
   -------------------------------
;файл с макросами - примерами

;PStr - макрос- функция, удаляющая из строки - параметра символы:
; пробел, "_", "|", "!" и табуляцию.
;Можно использовать для улучшения читаемости чисел большой длинны:
;Пример использования: AnyDword dd PStr(1011_0000_1011_1010b)

PStr macro NumStr:REQ  ;;объявим макрос с одним обязательным параметром.
  LOCAL PureNumber  ;;локальная переменная для хранения "очищенного" числа
  PureNumber TEXTEQU
  FORC cursym, <NumStr>   ;;цикл для каждого символа строки - параметра
    IFE @InStr(1,< _| !!>, <cursym>) ;;является ли текущий символ разделителем?
      PureNumber CATSTR PureNumber, <cursym> ;;нет - добавим к строке - "чистому" числу
    ENDIF
  ENDM
  EXITM <PureNumber>  ;;вернем полученную строку.
ENDM

;Макро - процедура, позволяющая вводить несколько команд на одной строке,
; используя разделитель - символ "|"
;Пример: LN mov eax,ecx | xor ecx,ecx
;Откомпилируется как mov eax,ecx и после xor ecx,ecx

LN macro CmdLine:VARARG
  LOCAL CurParam,NextParam
  CurParam=1
  NextParam=0
  WHILE NextParam LT @SizeStr(<CmdLine>)
    NextParam INSTR CurParam, <CmdLine>, <|>
    IFE NextParam
      NextParam=@SizeStr(<CmdLine>)+1
    ENDIF
    @SubStr(<CmdLine>, CurParam, NextParam-CurParam) ;;выделенная команда
    CurParam=NextParam+1
  ENDM
ENDM

;макрос, замнняющий команду nop, на nop [число]
OPTION NOKEYWORD: <nop>
nop macro NopCount:=<1>
  REPEAT NopCount
    db 90h  ;;код команды nop
  ENDM
ENDM

;Макро - функция, позволяющая объявлять массивы байт в операнде.
;Первый параметр - название метки (пустой, если не нужна), второй - массив байт.
;Указанный массив размещается в сегменте .data
;Пример: mov eax,ptxt(,"Any Text")

ptxt macro LabelName,TextToDef:VARARG
  LOCAL CurrentSeg,TextOffset
  CurrentSeg TEXTEQU @CurSeg ;;сохраним имя текущего сегмента
  .data   ;;создадим сегмент для указанного массива байт
  IFNB <LabelName>
    LabelName LABEL byte  ;;создадим метку, если указан первый параметр
  ENDIF
  TextOffset db TextToDef
  @CurSeg ENDS   ;;закроем созданный сегмент
  CurrentSeg SEGMENT  ;;восстановим предыдущий сегмент
  EXITM <offset TextOffset>
ENDM

;данный макрос улучшает директиву PURGE
;удаляет список макросов, выводя сообщения при их дальнейшем использовании.
;Пример: PURGE LN

OPTION NOKEYWORD: <PURGE> ;PURGE - это директива компилятора, поэтому,
; чтобы использовать ее имя, нужно сначала отменить ее оригинальное значение.
PURGE macro mlist:VARARG
  FOR dname,<mlist>  ;;перечислим все параметры - имена удаляемых макросов
    dname macro mparams:VARARG ;;создадим новый макрос с именем как у удаляемого
      %echo disabled dname mparams (in: @FileCur at line# @CatStr(%@Line))
    ENDM
  ENDM
ENDM

;макрос, аналогичный PURGE, но удаляет ключевые слова,
;  например, команды процессора.
PKwd macro klist:vararg
  FOR kname,<klist>
    OPTION NOKEYWORD: <kname>
  ENDM
  PURGE klist
ENDM
   -------------------------------

Программа - пример использования макросов.
.386
 .model flat,stdcall
 .NOLIST
 .NOCREF
include MacroTest.mac
include c:\masm32\include\user32.inc
includelib c:\masm32\lib\user32.lib
include c:\masm32\include\windows.inc
LN .CREF | .LISTALL

PKwd int
 .code
start:
  int 3
  invoke MessageBoxA,0,ptxt(,"Нажмите любую кнопку",0),\
    ptxt(MsgTitle,"Пример на MASM (откомпилирован ",@CatStr(<!">,%@Date,<!">),")",0),\
    MB_ICONINFORMATION or MB_YESNO

  .IF eax == IDYES
    mov ecx,ptxt(,"Нажата кнопка 'ДА'",0)
  .ELSEIF eax== IDNO
    mov ecx,ptxt(,"Нажата кнопка 'НЕТ'",0)
  .ELSE
    mov ecx,ptxt(,"Ошибка ф-ции MessageBoxA",0)
  .ENDIF

  invoke MessageBoxA, 0, ecx, offset MsgTitle, MB_ICONINFORMATION or MB_OK
  ret
end start

   -------------------------------

Сайт рассылки перемещен на asm-x86.by.ru.
Теперь вы можете легко задать вопрос относительно любой из размещенных статей в журнале http://asm-x86.livejournal.com/profile
Содержание дальнейших выпусков зависит от вас. Будут разъясняться те темы, которые непонятны большинству читателей.
Присылайте свои вопросы и предложения по адресу: asm-x86@by.ru .
   -------------------------------


Автор рассылки Владимир Пронин. Любое коммерческое использование материалов рассылки без ведома и прямого согласия автора запрещено.
Письма присланные автору рассылки могут быть опубликованы целиком или частично без предварительного уведомления. Если вы не хотите, чтобы ваше письмо было опубликовано укажите это в начале письма.
Перепечатка материалов разрешена только с непосредственной ссылкой на asm-x86.by.ru


В избранное