Статистика |
Онлайн всего: 1 Гостей: 1 Пользователей: 0 |
|
Вызов функций по указателю в Visual Basic (часть 4)
Комментарии Статья замечательная, однако, есть ряд неучтённых нюансов, которые делают предложенный метод не очень удобным.
Начнём с мелочей. В качестве инструкции ветвления автор предлагает использовать инструкцию ветвления - безусловный переход по относительному адресу: Однако, очевидно, что данный метод весьма неудобен по ряду причин. Если вдруг захочется изменить адрес, придётся заново обращаться к SetFunction, а это не очень удобно. Да и зачем это делать зазря , если можно и вовсе обойтись без повторного обращения к SetFunction? Достаточно лишь использовать безусловный переход по адресу, содержащемуся в таком-то месте адресного пространства. Под <таким-то местом адресного пространства> я подразумеваю наибанальнейшую (глобальную) переменную. Ладно, от разговора на языке жестов перейдём к инструкции:
Код Visual Basic | 1
2
3
4
| Public vCallAddress as Long, Ia() as Byte
ReDim Ia(1 to 6)
'jmp dword ptr ds:[imm32=VarPtr(vCallAddress)]
Ia(1)=&HFF: Ia(2)=&H25: PutMem4 VarPtr(Ia(3)), VarPtr(vCallAddress) |
| Массив объявлен условно, лишь для того, чтобы показать, как это выглядит в машинных кодах.
На самом деле, чтобы адекватно заменить инструкцию в примере Сергея необходимо учитывать, что его инструкция занимает пять байт. Она устанавливается из SetFunction в PlaceholderFor1ParamFunction вместо находящихся там после линковки инструкций которые тоже занимают пять байт. А наша инструкция занимает шесть байт. Но это очень просто поправить, достаточно переписать PlaceholderFor1ParamFunction в виде
Код Visual Basic | 1
2
3
4
5
6
7
8
9
10
11
| Public Function PlaceholderFor1ParamFunction(ByVal p As Long) As Long
#If ReleaseBuild Then
PlaceholderFor1ParamFunction = 1&
#Else
Dim a As Long
On Error Resume Next
a = mCol(CStr(ReturnMe(AddressOf modFuncCall.PlaceholderFor1ParamFunction)))
On Error GoTo 0
If a Then PlaceholderFor1ParamFunction = CallFunction(a, 1, p)
#End If
End Function |
| тогда при линковке туда будут записаны инструкции занимающие восемь байт, теперь перезаписываемый фрагмент будет выглядеть
Код Visual Basic | 1
2
3
4
5
6
| ReDim Ia(1 to 8)
'jmp dword ptr ds:[imm32=VarPtr(vCallAddress)]
Ia(1)=&HFF: Ia(2)=&H25: PutMem4 VarPtr(Ia(3)), VarPtr(vCallAddress)
'nop
'nop
Ia(7)=&H90: Ia(8)=&H90 |
| Теперь, чтобы сделать переадресацию, не нужно больше обращаться к SetFunction, а достаточно всего лишь: и можно опять обращаться к PlaceholderFor1ParamFunction.
Это были мелкие замечания, так сказать, <по процедурному вопросу>. Теперь хотелось бы поговорить об упущенных нюансах, имеющих, тем не менее, глобальный характер. Не очень верным является мажорное высказывание автора в адрес CallWindowProc. Да, безусловно, чем-то эта функция хороша, однако, мне не кажется, что это удобный метод переадресации, тем более под IDE. Какие будут мои аргУменты? Да очень простые! Давайте сначала вспомним, как вообще процессор <понимает> из какого места адресного пространства необходимо исполнять инструкцию. Для указания адреса исполняемой инструкции используется регистр eip. Причём, нет инструкций явных чтения/записи из этого регистра. А в качестве неявно изменяющих содержимое регистра eip используются jmp, jxx и пары call-ret, enter-leave. VB'эшные компилятор и интерпретатор обычно используют jmp, jxx и пару call-ret. Последняя, как раз, нас и интересует. Выполнение инструкции сводится. или ещё подробнее к
Код ASM | 1
2
3
| mov dword ptr ds:[esp-4],eip
sub esp,4
mov eip,operand |
| После того, как старое значение eip сохраняется в стеке (в регистре esp находится указатель на начало стека), а значение operand'а загружается в eip, процессор <сам по себе> переходит к выполнению инструкции, находящейся по этому адресу. Итак <на вершине стека> у нас находится указатель на то место, куда будет необходимо вернуться после выполнения вызываемой функции. Возврат осуществляется инструкцией optional imm16 используется для очистки стека, если это необходимо. Развёрнутая интерпретация этой инструкции выглядит
Код ASM | 1
2
| add esp,optional imm16+4
mov eip,dword ptr ds:[esp-optional imm16-4] |
| Причём, если прочее содержимое стека не меняется, то место по адресу [esp-optional imm16-4] после этого уже не содержит адрес возврата. Теперь после маленького ликбеза <вернёмся к нашим баранам>. В среде VB из любой процедуры, независимо от того IDE это или откомпилированный файл, возврат ВСЕГДА осуществляется с помощью инструкции Таким образом, если внутри нашей процедуры изменить значение <на вершине стека>, то вместо возврата произойдёт передача управления на тот адрес, который мы запишем по адресу, содержащемуся в esp. Но как узнать значение регистра esp, как узнать адрес начала стека? Заметьте, сам автор пишет: <перед передачей управления в функцию : стек приобретает вид:> Но как будто он ничего при этом не замечает. Заметьте, что первым параметром в стеке является адрес возврата! Тот самый, по которому происходит возврат из функции при выполнении инструкции А где у нас находится начало стека при передаче управления, скажем, в такую функцию:
Код Visual Basic | 1
2
3
| Private Function Test(byval vP1 as Long)as Long
:
End Function |
| Ну, разумеется, в откомпилированном виде начало стека будет находиться по адресу: А в IDE, спросите вы, где будет начало стека при вызове функции в IDE? Ну, здесь всё очень просто. По ряду причин, которые я не хотел бы здесь обсуждать, вызов процедуры <из под> IDE осуществляется почти так же, как осуществляется в откомпилированном виде вызов функции из чужого ActiveX (кстати, думаю, что самые сметливые уже догадались, почему так происходит в IDE). Что это значит? Да вот что: начало стека и первый параметр разделяет ещё один параметр (указатель на адреса ObjTable). То есть начало стека находится следующим образом: Теперь достаточно сохранить этот адрес в <какой-нибудь> (специально для этого предназначенной) переменной
Код Visual Basic | 1
2
| Public pRet as Long
CopyMem VarPtr(pRet), vESP, 4 |
| после этого по адресу vESP можно записать новый (необходимый нам) адрес. И далее, процессор <сам по себе> переходит на адрес, скажем, на следующий переключатель в маш. кодах:
Код Visual Basic | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| Public RDa() as Byte, pRD as Long
Private Sub InitRD()
ReDim RDa(1 to 16)
'восстанавливаем значение регистра esp
'sub esp,imm8=4
RDa(1)=&H83: RDa(2)=&HEC: RDa(3)=4
'заново прописываем адрес возврата, предварительно сохранённый нами в pRet
'push dword ptr ds:[imm32=VarPtr(pRet)]
RDa(4)=&HFF: RDa(5)=&H24: RDa(6)=&H25: PutMem4 VarPtr(RDa(7)), VarPtr(pRet)
'ну, теперь, наконец, можно перейти по интересующему нас адресу
'предварительно записанному нами в vCallAddress
'jmp dword ptr ds:[imm32=VarPtr(vCallAddress)]
RDa(11)=&HFF: RDa(12)=&H25: PutMem4 VarPtr(RDa(13)), VarPtr(vCallAddress)
pRD=VarPtr(RDa(1))
End Sub |
| А тело функции Test необходимо оформить так, чтобы у нас внутри неё вычислялось значение pRet и вместо адреса возврата записывался указатель на наш переключатель. Тело функции должно выглядеть следующим образом:
Код Visual Basic | 1
2
3
4
5
6
7
8
9
10
11
12
13
| Private Function Test(byval vP1 as Long)as Long
#If ReleaseBuild Then
'вычисляем адрес начала стека
vESP = VarPtr(vP1) - 4
#Else
'вычисляем адрес начала стека
vESP = VarPtr(vP1) - 8
#End If
'сохраняем адрес возврата в переменной pRet
CopyMem VarPtr(pRet), vESP, 4
'прописываем в начало стека новый адрес pRD
PutMem4 vESP, pRD
End Function |
| И всё! И не нужны никакие CallWindowProc! Всё работает и под IDE, и в откомпилированном виде.
|
Категория: Basic | Добавил: Admin (14.06.2012)
|
Просмотров: 2632
| Рейтинг: 5.0/1 |
Добавлять комментарии могут только зарегистрированные пользователи. [ Регистрация | Вход ]
|
|
|