Совмещение 32 и 64-битного кода |
|
В этой статье описываются способы совмещения 32 и 64-битного кода в приложениях
WOW64. Исходники к статье находятся в архиве ld64.zip. Переход из режима совместимости в 64-битный режим и обратно сводится к загрузке нового селектора в регистр cs при дальнем вызове (call) или переходе (jmp). В Windows для режима совместимости используется селектор 23h, и для 64-битного режима селектор 33h. 1. 32 и 64-битный код можно располагать в одном исходнике/модуле. Не все ассемблеры хорошо приспособлены для этого. В частности в MASM можно только сбоку к инструкциям приписывать REX префикс. Здесь я приведу в качестве примера не большой примитив, выполняющий деление ;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. |