Совмещение 32 и 64-битного кода

Главная
Обновления дизассемблера за просмотр рекламы (текущая версия 1.6)
Рекламная пауза
Среда разработки
Совмещение 32 и 64-битного кода
Эпиморфный ассемблер
База данных
Calculation Engine (Длинные числа)
Документация
Копирование сайтов
Ссылки
Гостевая книга
В этой статье описываются способы совмещения 32 и 64-битного кода в приложениях WOW64.
Исходники к статье находятся в архиве ld64.zip.
   Переход из режима совместимости в 64-битный режим и обратно сводится к загрузке нового селектора в регистр cs при дальнем вызове (call) или переходе (jmp). В Windows для режима совместимости используется селектор 23h, и для 64-битного режима селектор 33h.

1. 32 и 64-битный код можно располагать в одном исходнике/модуле.
Не все ассемблеры хорошо приспособлены для этого. В частности в MASM можно только сбоку к инструкциям приписывать REX префикс. Здесь я приведу в качестве примера не большой примитив, выполняющий деление 12388889931329909/2323423667677 в 64-битном режиме.


;div32_64.asm
.686
.model flat,stdcall
OPTION casemap:none
include div32_64.inc
.const
Arg1 dq 12388889931329909 ;ДЕЛИМОЕ
Arg2 dq 2323423667677 ;ДЕЛИТЕЛЬ
_Caption db "Division 12388889931329909/2323423667677",0
pattern db "Quotient %I64d",13,10,"Remainder %I64d",13,10,0
.code
WinMain proc
LOCAL Arg3:QWORD ;ЧАСТНОЕ
LOCAL Arg4:QWORD ;ОСТАТОК
LOCAL buffer[128]:BYTE
    invoke Divide,addr Arg1,addr Arg2,addr Arg3,addr Arg4
    invoke wsprintf,addr buffer,addr pattern,Arg3,Arg4
    invoke MessageBox,0,addr buffer,addr _Caption,MB_ICONINFORMATION
    ret
WinMain endp
Divide proc arg_1:PVOID,arg_2:PVOID,arg_3:PVOID,arg_4:PVOID
    nop
    org $-1
    db 0EAh ;ДАЛЬНИЙ ПЕРЕХОД В 64-БИТНЫЙ РЕЖИМ
    dd offset Divide_64
    dw 33h
dexit LABEL FWORD
    dd offset DivideExit
    dw 23h
Divide_64::
    mov eax,arg_1
    REXW() mov rax,[rax]
    mov ecx,arg_2
    REXW() mov rcx,[rcx]
    xor edx,edx
    REXW() div rcx
    mov ecx,arg_3
    REXW() mov [rcx],rax
    mov ecx,arg_4
    REXW() mov [rcx],rdx
    mov rax,offset dexit
    jmp fword ptr [rax] ;ВОЗВРАТ В РЕЖИМ СОВМЕСТИМОСТИ
DivideExit::
    ret
Divide endp
end WinMain

;div32_64.Inc
include windows.inc
include kernel32.inc
includelib kernel32.lib
include user32.inc
includelib user32.lib
Divide PROTO arg_1:PVOID,arg_2:PVOID,arg_3:PVOID,arg_4:PVOID
rax equ eax
rdx equ edx
rcx equ ecx
rbx equ ebx
rsp equ esp
r8 equ eax
r9 equ ecx
REXW MACRO ____rxb:=<0>
    db 48h or ____rxb
    EXITM<>
ENDM

   В 64-битном режиме для деления 64-битных чисел можно использовать инструкцию div. Макрос REXW позволяет сбоку приписывать REX префикс. Надо также понимать, что таким способом нельзя определить REX префикс для инструкций у которых есть хотя-бы еще один другой префикс т. е. запись REXW() mov rax,gs:[rdx] будет эквивалентна mov eax,gs:[rdx] из-за сегментного префикса для gs. Вообщем это хороший способ проверить свои знания формата инструкций.
   Возможно у кого-то возникнет вопрос, а для чего в начале функции Divide стоит nop? Тем более потом он всё равно затирается из-за org $-1.
   Это обусловлено сложной особенностью MASM. Пролог функции можно считать развернутым только после первой простой инструкции без макросов. Пролог не разворачивается сразу, потому-что перед этим нужно обработать все директивы LOCAL, а их допускается произвольное количество т.е. неизвестно будет это 0, 1, 2, 3 или сколько-нибудь еще строк с объявлением локальных переменных, отсюда и привязка к первой простой инструкции, а без макросов из-за того что подразумевается возможность макросов изменять количество директив LOCAL. Если это не учитывать, то могут возникнуть проблемы.

2. Можно обращаться к 64-битной подсистеме для загрузки 64-битных *.dll модулей.
Для автоматизации этого процесса я написал несколько функций и собрал их в 32-битную библиотеку ld64.dll. Суть метода состоит в том, что во всех 32-битных процессах присутствует не только системная библиотека ntdll.dll (из SysWOW64), но и 64-битный аналог (из system32), а там находится минимум необходимых функций для загрузки 64-битных библиотек. Кроме этого есть 64-битный вариант структуры TEB, и доступ к ней можно получить через регистр gs. С помощью этой структуры можно получить базу 64-битного модуля ntdll.dll и определять аддресса к нужным функциям.
   В ld64.dll при инициализации в DllEntryPoint сначала определяется база 64-битной ntdll.dll функцией GetNtdllBase. Далее функцией GetProcAddressX определяются аддресса 64-битных функций LdrLoadDll, LdrUnloadDll, LdtGetDllHandle и RtlInitUnicodeString. Здесь размещу часть кода.

;ld64.asm
.data?
hNtdll64 HMODULE64 ?
mbstowcs PANYARGS ?
LdrLoadDll PVOID64 ?
LdrUnloadDll PVOID64 ?
LdrGetDllHandle PVOID64 ?
RtlInitUnicodeString PVOID64 ?
.code
NtdllName db 'ntdll.dll',0
mbstowcs_name db 'mbstowcs',0
LdrLoadDllName db 'LdrLoadDll',0
LdrUnloadDllName db 'LdrUnloadDll',0
LdrGetDllHandleName db 'LdrGetDllHandle',0
RtlInitUnicodeStringName db 'RtlInitUnicodeString',0
DllEntryPoint proc hinstDLL:HINSTANCE,fdwReason:DWORD,lpvReserved:LPVOID
    .if fdwReason==DLL_PROCESS_ATTACH ;ИНИЦИАЛИЗАЦИЯ ПРИ ЗАПУСКЕ
        invoke GetModuleHandle,addr NtdllName
        invoke GetProcAddress,eax,addr mbstowcs_name
        mov mbstowcs,eax
        invoke GetNtdllBase ;ПОЛУЧЕНИЕ БАЗЫ ntdll.dll
        mov dptr hNtdll64,eax
        mov dptr hNtdll64[4],edx
        invoke GetProcAddressX,hNtdll64,addr LdrLoadDllName ;ПОЛУЧЕНИЕ АДДРЕССОВ НЕКОТОРЫХ ФУНКЦИЙ
        mov dptr LdrLoadDll,eax
        mov dptr LdrLoadDll[4],edx
        invoke GetProcAddressX,hNtdll64,addr LdrUnloadDllName
        mov dptr LdrUnloadDll,eax
        mov dptr LdrUnloadDll[4],edx
        invoke GetProcAddressX,hNtdll64,addr LdrGetDllHandleName
        mov dptr LdrGetDllHandle,eax
        mov dptr LdrGetDllHandle[4],edx
        invoke GetProcAddressX,hNtdll64,addr RtlInitUnicodeStringName
        mov dptr RtlInitUnicodeString,eax
        mov dptr RtlInitUnicodeString[4],edx
        invoke DisableThreadLibraryCalls,hinstDLL
    .endif
    mov eax,TRUE
    ret
DllEntryPoint endp

   Все получаемые здесь аддресса и база модуля 64-битные и возвращаются в регистровой паре edx:eax. Всего из ld64.dll экпортируется 5 функций LoadLibraryX, GetModuleHandleX, GetProcAddressX, FreeLibraryX и CallFN64. Первые 4 во многом повторяют часто используемые WINAPI функции, только к именам на конце приписан X, у них нет юникод версий, и они позволяют работать с 64-битными модулями оставаясь в режиме совместимости. GetProcAddressX не работает с ординалами. CallFN64 является переходником для вызова 64-битных функций и у него C конвенция для передачи аргументов.

CallFN64 PROTO C FuncPPtr:PVOID,Arg64:VARARG

FuncPPtr - это 32-битный указатель на 64-битный указатель на 64-битную функцию.
Arg64 - аргументы, которые нужно передать 64-битной функции.

Вместо CallFN64 удобнее использовать макрос callx. При вызове 64-битных функций он просто пишется вместо invoke.

callx MACRO FuncAddr:REQ,call64parms:VARARG
    IFDIF <call64parms>,<>
        invoke CallFN64,addr FuncAddr,call64parms
    ELSE
        invoke CallFN64,addr FuncAddr
    ENDIF
ENDM

Для возможности переассемблировать ld64.asm, у вас ещё должен быть заголовочный файл win64.inc из моего комплекта, хотя ассемблироваться должно 32-битным ml.exe.

    Теперь если в примитиве с делением использовать ld64.dll. Будет всё выглядеть следующим образом.

;div32_64x.asm
.686
.model flat,stdcall
OPTION casemap:none
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
include ld64.Inc
includelib ld64.lib
.const
Arg1 dq 12388889931329909 ;ДЕЛИМОЕ
Arg2 dq 2323423667677 ;ДЕЛИТЕЛЬ
_Caption db "Division 12388889931329909/2323423667677",0
pattern db "Quotient %I64d",13,10,"Remainder %I64d",13,10,0
divtest_name db "divtest.dll",0
divide_name db "Divide",0
.data? ;ЗДЕСЬ СОСТАВЛЯЕТСЯ ПОЛНЫЙ ПУТЬ К 64-БИТНОМУ МОДУЛЮ
FullPath db MAX_PATH dup(?)
.code
WinMain proc
LOCAL Arg3:QWORD ;ЧАСТНОЕ
LOCAL Arg4:QWORD ;ОСТАТОК
LOCAL hDivTest:HMODULE64 ;БАЗА МОДУЛЯ divtest.dll
LOCAL Divide:PVOID64 ;УКАЗАТЕЛЬ НА ФУНКЦИЮ
LOCAL buffer[128]:BYTE
    invoke GetModuleHandle,0 ;divtest.dll ИЩЕТСЯ С ПАПКЕ С div32_64x.exe
    invoke GetModuleFileName,eax,offset FullPath,sizeof FullPath
    invoke lstrlen,offset FullPath
    .while eax && FullPath[eax]!='/' && FullPath[eax]!='\'
        dec eax
    .endw
    .if FullPath[eax]=='/' || FullPath[eax]=='\'
        inc eax
    .endif
    invoke lstrcpy,addr FullPath[eax],addr divtest_name
    invoke LoadLibraryX,offset FullPath ;ЗАГРУЗКА divtest.dll
    mov dword ptr hDivTest,eax
    mov dword ptr hDivTest[4],edx
    invoke GetProcAddressX,hDivTest,addr divide_name ;ПОЛУЧЕНИЕ АДДРЕССА ФУНКЦИИ Divide
    mov dword ptr Divide,eax
    mov dword ptr Divide[4],edx
    ;32-БИТНЫЕ УКАЗАТЕЛИ ЧЕРЕДУЮТСЯ С НУЛЯМИ, ЧТОБЫ ПРИОБРЕСТИ 64-БИТНЫЙ РАЗМЕР
    callx Divide,addr Arg1,0,addr Arg2,0,addr Arg3,0,addr Arg4,0
    invoke FreeLibraryX,hDivTest
    invoke wsprintf,addr buffer,addr pattern,Arg3,Arg4
    invoke MessageBox,0,addr buffer,addr _Caption,MB_ICONINFORMATION
    invoke ExitProcess,0
WinMain endp
end WinMain

;divtest.asm
OPTION DOTNAME
option casemap:none
include win64.inc
include temphls.inc
OPTION PROLOGUE:rbpFramePrologue
OPTION EPILOGUE:rbpFrameEpilogue
.code
Divide proc ;arg_1:PVOID,arg_2:PVOID,arg_3:PVOID,arg_4:PVOID
    mov rax,[rcx]
    mov rcx,[rdx]
    xor edx,edx
    div rcx
    mov [r8],rax
    mov [r9],rdx
    ret
Divide endp
end

Теперь как видно макрос callx используется вместо invoke при вызове 64-битных функций, но так как каждый аргумент для 64-битной функции в стэке должен занимать 64-бита, то 32-битные указатели приходится чередовать с нулями.

P.S. При загрузке библиотек заметил, что их базы всегда умещаются в 32 битах, исключение составляет только ntdll.dll. Соответственно аддресса функций всех библиотек кроме ntdll.dll тоже умещаются в 32-битах, и использование 64-битных указателей необходимо только для функций из ntdll.dll. Возможно на все остальные случаи следует сделать набор упрощённых функций работающих с 32-битными указателями и базами. В 64-битных процессах тоже возможен переход в режим совместимости и обратно. Быть может позже я дополню статью каким-нибудь набором функций ld32.