|
MASM64
Генерация заголовочных файлов
Новые макросы
Новый invoke
Пролог и Эпилог
Макрос v
Тестирование отдельных битов test vs bt
MASM64
Код библиотеки написан на 64-битном ассемблере MASM+RadASM.
Хотя это и редко используемый язык можете скачать мой комплект
заголовочных файлов masm64.zip 3.83 Мб.
64-битный ассемблерный транслятор ml64
не распространяется отдельно, и входит в комплект C++ компиляторов от
Microsoft. Для установки сначала потребуется скачать web-инсталятор SDK
для Windows и .NET Framework 4.0. Полное название
Microsoft Windows SDK for Windows 7 and .NET Framework 4, хотя и
написано 7 у меня получалось установить на XP. Во время установки там
будет предложено выбрать устанавливаемые компоненты, и если для
установки выбрать только C++ компиляторы то потребуется 157 Мб
трафика. Также если у вас не установлен .NET Framework 4.0, то и его
нужно будет установить.
После установки 64-битный ассемблер будет находится в
папках C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\x86_amd64 и
C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\amd64, где C:\
это системный диск, обычно с папкой Windows. link я
использую 64-битный из папки amd64, а ml64 уже
32-битный из папки x86_amd64. В папке amd64 есть 64-битная версия
ml64, но при работе с большим количеством макросов он
почему-то зависает или просто медленно работает.
Генерация заголовочных файлов
В архиве masm64.zip находится много заголовочных файлов,
сделанных для Windows XP. Для их создания я использовал утилиту
dll2asmblank.zip, исходник прилагается. Вообще-то сначала я её
делал как утилиту генерирующую исходники подложных (фэйковых) библиотек
(*.dll), что и неплохо получилось. При запуске подложная библиотека загружает
оригинальный, но уже переименованный модуль, и все экспортируемые функции
оказываются перехваченными. Этот метод лучше подходит для виртуальных
машин, когда можно сравнительно легко переименовывать и
подменять системные библиотеки. В дальнйшем эту утилиту не сложно было
использовать для получения отсутствующих инлайн файлов (*.lib)
Если вам нужно будет создать более полный комплект
заголовочных файлов для своей 64-битной ОС, то для этого следует в
папке с dll2asmblank.exe создать файл коммандной строки (*.bat), со
следующим содержимым.
@echo off
dll2asmblank "%Папка с Windows%\system32\*.dll"
/OUTDIR:"Папка для сохранения заголовочных файлов"
/PATTERN:"pattern1.asm"
dll2asmblank "%Папка с Windows%\system32\*.exe"
/OUTDIR:"Папка для сохранения заголовочных файлов"
/PATTERN:"pattern2.asm"
pause
Файлы pattern1.asm и pattern2.asm, находятся в одной папке
с dll2asmblank.exe. Там следует обратить внимание на последние 4 строки в
обоих файлах.
PatternBat PATTERN
ML64 /c /Cp /I"C:\masm64\include" %s.asm
LINK %s.obj /LIBPATH:"C:\masm64\lib"
/SUBSYSTEM:WINDOWS /ENTRY:DllEntryPoint /DLL /DEF:%s.def
ENDPATTERN
Вместо ML64 и LINK должны быть полные пути к этим файлам, взятые в двойный
кавычки. Вместо "C:\masm64\include" ваш путь к заголовочным файлам.
Вместо "C:\masm64\lib" ваш путь к инлайн файлам (*.lib). После
определения путей нужно будет запустить этот коммандный файл.
Когда коммандный файл завершит работу в "Папка
для сохранения заголовочных файлов" будет находится коммандный файл
build.bat. Выполняться он может очень долго, и когда он завершит работу
в "Папка для сохранения заголовочных файлов"
будут находится также и все инлайн файлы (*.lib). Дальше останется только
разместсить *.inc и *.lib по своему усмотрению.
Для получения заголовочного файла одной библиотеки в
коммандном файле должно быть.
@echo off
dll2asmblank "имя библиотеки.dll" /OUTDIR:"Папка
для сохранения заголовочных файлов" /PATTERN:"pattern1.asm"
pause
Для получения заголовочного файла исполняемого модуля.
@echo off
dll2asmblank "имя исполняемого модуля.exe"
/OUTDIR:"Папка для сохранения заголовочных файлов"
/PATTERN:"pattern2.asm"
pause
Для создания подложных библитек и модулей нужно использовать
аналогичные коммандные файлы, но вместо pattern1.asm нужно использовать
pattern3.asm, и вместо pattern2.asm pattern4.asm соответственно. pattern3.asm и
pattern4.asm тоже находятся в одной папке с dll2asmblank.exe.
32-битные модули могут быть обработаны, но для них
потребуются другие pattern файлы.
Новые макросы
Использовать windows.inc из пакета masm32 не получилось. От
туда время от времени копирую недостающие константы, а структуры приходиться
объявлять в ручную. В архиве masm64.zip/include
есть заголовочный файл с комплектом макросов temphls.inc. Это прежде всего
invoke, .if,
.elseif, .else,
.endif, .while,
.repeat и другие.
Cтиль синтаксиса я в них определил довольно своеобразный.
Ниже краткие правила.
Логический оператор |
Значение |
== |
равно |
{} или ~= |
не равно |
} |
больше |
}= |
больше или равно |
{ |
меньше |
{= или ={ |
меньше или равно |
& |
тестирование битов |
| |
установка битов |
&& |
логическое И |
|| |
логическое ИЛИ |
CARRY? |
CF флаг займа/переноса |
OVERFLOW? |
OV флаг переполнения |
PARITY? |
PF флаг четности бит |
SIGN? |
SF флаг знака |
ZERO? |
ZF флаг равенства нулю |
CARRY?|ZERO? |
некоторые сочетания флагов |
~ZERO?&(SIGN?==OVERFLOW?) |
SIGN?==OVERFLOW? |
SIGN?{}OVERFLOW? |
ZERO?|(SIGN?{}OVERFLOW?) |
Конечно лучше-бы это были встроеные макросы, как это обстоит
с 32-битной версией ассемблерного транслятора. Знаки переменных
SWORD/SDWORD не учитываются. Обычные
макросы не принимают уголки <>. Но и в таких макросах есть свои
положительные стороны. С синтаксисом теперь можно эксперементировать.
Я сделал пару новых макросов. Первый это
.skip для перехода на начало тела цикла. .continue
осуществляет переход на проверку условия продолжения цикла т. е. если остатот
цикла нужно вместе с проверкой условия пропустить то можно
использовать .skip/.skip
.if.
Пример использования
.while TRUE
inc eax
.skip .if edx==eax
dec edx
.endw
Тоже самое но с использованием только старых макросов.
.while TRUE
.repeat
inc eax
.until edx~=eax
dec edx
.endw
Второй макрос это .goto label/.goto
label .if для условных переходов.
Пример использования
.goto Exit
.if eax==5 || esi}=edi
inc eax
Exit:
Тоже самое но с использованием только старых макросов.
.if eax~=5 && esi{edi
inc eax
.endif
Exit:
Для сравнения с учётом знака можно использовать одну из четырёх
текстовых макро-констант.
sxb equ <sbyte ptr>
sxw equ <sword ptr>
sxd equ <sdword ptr>
sxq equ <sqword ptr>
Пример использования
.if sxq [rdx]{=rax ||
sxd edx}=r12d || sxw
r12w}sp || sxb [rax]{0
add rax,4
.endif
Новый invoke
В temphls.inc определён макрос invoke,
который может обрабатывать до 14 параметров. Для вызова функций с большим
количеством аргументов можно использовать макрос invoke14.
invoke отличается от
invoke14, тем что он обрабатывает повторяющиеся параметры, для
загрузки параметров использует rbp или rsp регистр в зависимости от количества
памяти выделеной под локальные переменные, использует типизированные по размеру
константы, и вместо addr при загрузке указателей может использоваться знак
амперсанда (&, почти как в C/C++).
Исходник пустого окна
OPTION DOTNAME
option casemap:none
include temphls.inc
include win64.inc
include kernel32.inc
includelib kernel32.lib
include user32.inc
includelib user32.lib
OPTION PROLOGUE:rbpFramePrologue
OPTION EPILOGUE:rbpFrameEpilogue
.const
wcex label WNDCLASSEX
cbSize dd sizeof WNDCLASSEX
style dd 0
lpfnWndProc dq offset WndProc
cbClsExtra dd 0
cbWndExtra dd 0
hInstance dq 100400000h
hIcon dq 10003h
hCursor dq 10003h
hbrBackground dq COLOR_WINDOW
lpszMenuName dq 0
lpszClassName dq offset ClassName
hIconSm dq 10003h
ClassName db 'Asm64 window',0
AppName db 'The window',0
.code
WinMain proc <12> ;parmarea 12*8 bytes
LOCAL msg:MSG
invoke
RegisterClassEx,&wcex ;можно написать по
старому addr wcex
mov r10d,CW_USEDEFAULT
invoke
CreateWindowEx,0,addr ClassName,addr AppName,WS_OVERLAPPEDWINDOW or
WS_VISIBLE,\
dptr CW_USEDEFAULT,dptr
CW_USEDEFAULT,dptr CW_USEDEFAULT,dptr CW_USEDEFAULT,\
0,0,hInstance,0
lea rdi,msg
.while TRUE
invoke
GetMessage,rdi,0,0,0
.break .if
~eax
invoke
TranslateMessage,rdi
invoke
DispatchMessage,rdi
.endw
invoke ExitProcess,[rdi][MSG.wParam]
WinMain endp
WndProc proc <4>
hWnd:QWORD,uMsg:QWORD,wParam:WPARAM,lParam:LPARAM
.if edx==WM_DESTROY
invoke PostQuitMessage,NULL
.else
leavef
jmp DefWindowProc
.endif
xor eax,eax
ret
WndProc endp
end
Ассемблирование и линковка
@echo off
ML64 /I"masm64\include" /I"\masm64\include" /c /Cp "window.asm"
LINK /LIBPATH:"\masm64\lib" /BASE:0x100400000 /ENTRY:WinMain /SUBSYSTEM:WINDOWS
window.obj user32.lib kernel32.lib
pause
invoke с CreateWindowEx
разворачивается в следующую последовательность инструкций
0000000100401011:33C9
xor
ecx,ecx
;dwExStyle=0
0000000100401013:48894DB0
mov [rbp-50h],rcx
;hWndParent=0
0000000100401017:48894DB8
mov [rbp-48h],rcx
;hMenu=0
000000010040101B:48894DC8
mov [rbp-38h],rcx
;lpParam=0
000000010040101F:488D157A100000
lea rdx,[1004020A0h]
;lpClassName=1004020A0h
0000000100401026:4C8D0580100000
lea r8,[1004020ADh]
;lpWindowName=1004020ADh
000000010040102D:41B90000CF10
mov r9d,10CF0000h
;dwStyle=10CF0000h
0000000100401033:B800000080
mov eax,80000000h ;eax=80000000h
0000000100401038:894590
mov [rbp-70h],eax ;x=eax
000000010040103B:894598
mov [rbp-68h],eax
;y=eax
000000010040103E:8945A0
mov [rbp-60h],eax
;nWidth=eax
0000000100401041:8945A8
mov [rbp-58h],eax
;nHeight=eax
0000000100401044:488B051D100000 mov
rax,[100402068h] ;rax=[100402068h]
000000010040104B:488945C0
mov [rbp-40h],rax
;hInstance=rax
000000010040104F:FF15EB0F0000
call qword ptr [100402040h] ;call CreateWindowEx
Загрузка параметров происходит не последовательно, а с учётом повторяющихся
параметров. Если ecx равен нулю, то вместо and qword ptr [rbp-50h],0 будет
mov [rbp-50h],rcx что компактнее. Подобные вещи обычно делают
оптимизирующие компиляторы. Константа dptr CW_USEDEFAULT, является
типизированной. Так как invoke не
использует прототипы функций, то размер аргумента должен быть так или иначе
задан, по умолчанию размер аргумента 8 байт. dptr перед
CW_USEDEFAULT делает подсказку, что это DWORD а не QWORD и вместо mov
[rbp-70h],rax ставится более компактное mov [rbp-70h],eax.
Пролог и Эпилог
В temphls.inc есть пролог и эпилог. Для их
использования код должен начинаться со строк.
OPTION PROLOGUE:rbpFramePrologue
OPTION EPILOGUE:rbpFrameEpilogue
Для аддрессации к локальным переменным используется
регистр rbp, только потому-что так проще. На x64 системах регистры rax,
rbx, rcx, rdx, rsp, rsi, rdi для этой цели тоже подходят не меньше чем rbp.
Регистры r8..r15 требуют REX префикса, но тоже подходят. На x86 выбор
только между ebp и esp, с eax, ebx, ecx, edx, esi, edi в редких случаях
возможны проблемы из-за сегментации. Директиву uses можно использовать как
и раньше в 32-битном коде, только там нужно указывать имена 64-битных
регистров, а не 32-битных.
Если в процедуре под локальные переменные выделяется больше
4 кб, то по возможности используется функция __chkstk. Эта функция
находится в библиотеке kernel32.dll поэтому в исходнике должны быть
две строки.
include kernel32.inc
includelib kernel32.lib
Выделение стэковой памяти происходит таким образом, что
стэк выравнивается на границу 16 байт. Пролог работает по принципу "Если
для меня стэк выравняли, то и я это сделаю". При вызове функций можно
использовать макрос invoke, если вы хотите в ручную загрузить
аргументы, то для этого нужно использовать регистры rcx, rdx, r8, r9 для
первых 4 аргументов и для остальных макро-определения rbpArg5..rbpArg14 с
аддрессацией через rbp регистр или rspArg5..rspArg14 с аддрессацией через rsp.
Объявления этих макро-определений находятся в temphls.inc. На x86 для загрузки
аргументов обычно используются последоватьльности из push`ей, но для x64
это не целесообразно, потому-что стэк перед вызовом WINAPI функции должен
быть выровнен на границу 16-байт и выравненное значение rsp лучше лишний
раз не менять.
Пролог в уголках <> может принимать до 5
параметров.
Пример:
WndProc proc <12,8,4,8,8>
hWnd:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
ret
WndProc endp
Первый параметр 12 - означает что в процедуре можно вызывать
функции которым передаётся не более 12 аргументов. Для большей части WINAPI
функций этот параметр должен принимать значения от 4 до 14. Этот параметр
не должен быть меньше 4, даже если в процедуре вызываются только WINAPI
функции которым передаётся 3 и менее 3 аргументов, в противном случае
могут возникнуть трудно отслеживаемые баги. Однако этот параметр может быть
равен нулю если процедура не вызывает ни одной функции. Если уголки
вообще не использовать для определения параметров, то по умолчанию пролог
выделяет в стэке место для 7 аргументов.
Остальные четыре числа 8,4,8,8 это размеры в
байтах, первых четырёх аргументов которые в начале находятся в
регистрах rcx, rdx, r8, r9. Эти параметры могут быть равны 1, 2, 4 или 8. Если
эти параметры не указывать то аргументы из регистров rcx, rdx, r8, r9 не будут
сохраняться в отведённое для них место в стэке, но это можно будет сделать в
ручную.
Сохранение первых 4 аргументов в ручную:
WndProc proc <12>
hWnd:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
mov hWnd,rcx
mov uMsg,edx
mov wParam,r8
mov lParam,r9
ret
WndProc endp
Макрос leavef удаляет фрэйм выделенный в прологе,
ret отличается от leavef только тем, что он оканчивается инструкцией retn для
возврата к вызвавшей процедуре.
Макрос v
Макрос v (сокращенно от eval, находится в masm64\include\temphls.inc)
раскрывает выражения, помогая более понятно оформлять код. Макрос не
обрабатывает деление. Сколько я макрос не использовал необходимости в этом
действии просто не возникло.
Поддерживаемые операторы в порядке увеличения приоритета
Оператор |
Значение |
Инструкции выполняющие действие |
= |
равно |
mov, lea |
| |
логическое или |
or |
^ |
исключающее или |
xor |
and (обязательно маленькими буквами) |
логическое и |
and |
~ |
инверсия всех бит |
not |
+ |
операция сложения |
add, sub, inc, dec, lea, neg |
- |
операция вычитания |
add, sub, inc, dec, lea, neg |
* |
операция умножения |
imul, lea |
}} |
логический сдвиг вправо |
shr |
{{ |
логический сдвиг влево |
shl |
Из-за логических сдвигов может быть изменено значение регистра
cl, далеко не всегда, но это нужно учитывать. При умножении последний множитель
должен завершать строку выражения или он должен быть отделён скобками.
Можно написать так
v r9=rax*r8*rsi*r15
Вот так
v r9=(rax*r8*rsi*r15)|qword
ptr [rbp-10h]
Но только не так
v r9=rax*r8*rsi*r15|qword ptr
[rbp-10h]
Макрос v не на столько высокоуровневый, чтобы можно было забыть про то что у
процессора нет инструкций где оба операнда память. Нельзя копировать из памяти
в память, нельзя умножить память на память и. т. д.
Можно написать так
v qword ptr [rbp-10h]=rax-rdi
Вот так
v rax=qword ptr
[rbp-10h]+qword ptr [rbp-18h]
Но только не так
v qword ptr
[rbp-10h]=rax+qword ptr [rbp-18h]
Размеры операндов должны быть одинаковыми. Это могут быть регистры, память или
непосредственные значения. Исключение возможно при сдвигах.
Можно написать так
v edi=dword ptr
[rbp-30h]{{(al+bl)
Макрос развернется в последовательность
mov edi,dword ptr [rbp-30h]
mov cl,al
add cl,bl
shl edi,cl
С помощью амперсанда доступно масштабирование с инструкцией lea, но место
назначения должно быть регистром.
Можно написать так
v edi=&[4*esi]-dword ptr
[rbp-40h]
Вот так
v edi=dword ptr
[rbp-40h]+&[4*esi]
Но только не так
v dword ptr
[rbp-40h]=edi+&[4*esi]
При написании макроса возникло много проблем связаных с ограничениями
макросистемы. Уровень вложености при ветвлении IF/ENDIF ограничен всего 20
уровнями. Само по себе это не было-б проблемой, 20 уровней для .if/.endif мне
всегда хватало, но похоже что уровни при рекурсии суммируются, а макрос v как
раз рекурсивный. В некоторых местах выходило так что ключевое слово ELSE тихо
игнорировалось и приходилось использовать директиву GOTO, в паре с объявлением
макрометки.
Тестирование отдельных битов test vs bt
В макросах masm64.zip/Include/temphls.inc внесено немного изменений.
Ранее в макросах для проверки битов использовалась только инструкция test, ей соответствовал
логический
оператор амперсанд &, но опкод инструкции bt более компактный, а также с bt можно
еще проверять старшие 32 бита в 64-битных регистрах и переменных.
Например такой записи раньше не допускалось.
.if rax&(1 shl 55)
inc edi
.endif
Развернется как
bt rax,55
jnc labelxx
inc edi
labelxx:
Раньше происходила-б ошибка
test rax,(1 shl 55) ;в опкоде test есть только 4 байта для чисел, число (1 shl 55) не уместится
jz labelxx
inc edi
labelxx:
При тестировании самого старшего бита в WORD`е, DWORD`е или QWORD`е все-же используется инструкция test, потому-что
старший бит копируется в SF флаг основного регистра флагов RFLAGS.
.if rbx&(1 shl 63)
inc edi
.endif
Развернется как
test rbx,rbx
jns labelxx ;проверка SF флага
inc edi
labelxx:
Знаковый бит можно еще тестировать используя ключевое слово SIGN?
.if rbx&SIGN?
inc edi
.endif
Макрос отвечающий за генерацию этого кода изменен таким образом, что если вы ранее им пользовались и остались
исходники то для замены test`ов на bt везде где это нужно достаточно просто переассемблировать исходники.
|