Кажется я первый раз попал в тупик.
Не то, чтобы я сильно умный, но и задачка — не "Балтика 9".
Первая часть задачи выглядела вот так:
Что не так с этим кодом?
Приступим.
Конечно, первое что бросается в глаза — вызов калбэка, в котором происходит обращение к переменной Wnd.
Жалко, что Delphi позволяет вообще такому компилироваться.
Правда это обусловлено не совсем верной декларацией самой функции EnumWindows.
Если бы она была объявлена вот так:
То ошибка была бы выявлена еще на этапе компиляции и Delphi выдала бы сообщение:
Если же мы будем получать адрес функции EnumWindowsProc посредством оператора "@", то даже такая декларация не поможет, т.к. в данном случае мы будем работать с нетипизированным указателем и все проверки на этапе компиляции будут отключены.
Конечно можно включить в настройках компилятора флаг "Typed @ operator" и получить сообщение:
Впрочем, так как объявление у EnumWindows другое, и данный код все же компилируется, попробуем разобраться с ним по пунктам:
Вот так выглядит вызов EnumWindows:
Тут нас интересует значение, помещенное на стек по адресу EBP-4, грубо говоря хэдл, полученный вызовом GetHandle.
На стеке он будет размещен примерно вот так:
Число 12F558 - это адрес локальной переменной Wnd, а 16051A - это хэндл главной формы, т.е. значение самой переменной. Запомним адрес 12F558 и посмотрим что произойдет при вызове калбэка EnumWindowsProc.
А вот это уже настоящая беда.
После генерации стекового фрейма (push+mov), EBP+8 указывает на параметр AWnd калбэка, Но посмотрите как происходит обращение к внешней переменной Wnd, с которой потом будет происходить сравнение.
Delphi компилятор, ошибшись, сгенерировал асм код получения указателя на верхушку фреймового стека, полагаясь на то, что мы все еще находимся в исполнении процедуры Button1Click, после чего ошибочно начал читать значение локальной переменной Wnd по адресу EAX-4.
Это грозит - глобальным AV, ведь текущий стек не принадлежит Button1Click и мы реально можем влететь на ошибку как при получении верхушки фрейма, а если повезло и не упали, то упадем на чтении якобы Wnd с битого адреса (если повезет).
Показываю с использованием карты памяти процесса:
Как вы видите, EAX после выполнения первой инструкции, вместо правильного адреса на стеке указывает куда-то вглубь памяти процесса, на которую отмаплен текущий исполняемый файл.
Можно сказать повезло, могли попасть и в секцию проверки битых указателей (первые 64 кб памяти процесса).
Сами понимаете что вторая строка (получение значения Wnd) уже как минимум не будет корректной, при работе с такими данными.
А нам нужен был адрес 12F558 - но не судьба.
Впрочем, перейдем к пункту за номером 4.
А именно к строчке:
Конечно-же в калбэк не передается перемеменна Self, но Delphi компилер не смог предположить такого цимуса, и даже в этом случае пытается выйти на нее.
Понятно, что такого у него не получится, но... если вдруг он все-же получит какой-то не совсем убитый адрес, то произойдет глобальный бадабум на вызове сеттера Caption, ибо произойдет вызов WindowProc на не понятно каком окне (я даже про VMT пока не говорю).
Если с первой задачей проблем никаких не было — все на виду, то вот вторая поставила меня в глобальный тупик.
Сразу скажу — на мой взгляд код правильный, но раз есть заковыка (а иначе и не было бы такой задачи поставлено), значит надо анализировать.
Единственный момент с условием выхода из EnumWindowsProc, ибо практически гарантированно, что первое найденное окно не будет хэндлом формы, что означает завершение работы EnumWindowsProc после первого-же сравнения (по False результату).
Повтыкав минут двадцать в данный код и не обнаружив ошибки, я выкатил его всему нашему IT отделу, с целью - может коллективный разум осилит?
Через час мы сдались и я начал анализировать уже с точки зрения реверсера.
Итак, что мы имеем:
Первичный анализ асм кода показывает, что использованные локальные перменные Wnd выкинуты оптимизатором, т.е. вот такой вот код будет выдавать абсолютно идентичный асм листинг:
Значит заковыка не в использовании локальных переменных.
Вариант с ошибками каста HWND/LPARAM отпадает, тут все правильно.
Вариант с выносом калбэка за тело процедуры асм код не меняет (да и смысл?).
Вариант с неверной декларацией EnumWindowsProc тоже отпадает (спецом исходники винды поднял - может там какой нюанс крылся?)
Значит остается последний вариант: может что-то с условием, как-то странно оно выглядит.
Переписываю вот так:
Выхлоп:
И опять нет ничего странного.
Тут я сдулся, ибо если последний вариант кода считать верным (а он верный, иначе даже в MSDN ошибка) то я категорически не понимаю - где здесь тонкий нюанс?
Но раз GunSmoker выкатил такую задачу - значит что-то тут есть еще такое, а я лично сдаюсь :)
UPDATE:
Ан нет - рано еще здаваться, как оказалось дело было в том что я забыл про 64 бита.
Смотрим ASM код функции EnumWindowsProc:
Вот оно что, Михалыч.
Причина простая, по умолчанию параметры передаются через регистры RCX, RDX, R8 и т.п.
Так вот для Nested функций, Delphi в регистр RCX помещает адрес вершины стека.
Таким образом оставшиеся два параметра идут в RDX и R8.
А код внутри EnumWindows этого не делает и по честному передает оба параметра через RCX + RDX.
Получается что мы работаем совершенно убитыми данными.
Как избавится от такого поведения?
Ответ прост - вынести функцию EnumWindowsProc за пределы процедуры TForm1.Button1Click.
После чего регистр RCX не будет задействован и сгенерируется правильный ASM код:
Собственно, как мне подсказали, этот момент описан в документации (о чем я не знал).
Такие вот пироги :)
Не то, чтобы я сильно умный, но и задачка — не "Балтика 9".
Первая часть задачи выглядела вот так:
Что не так с этим кодом?
procedure TForm1.Button1Click(Sender: TObject); var Wnd: HWND; function EnumWindowsProc(const AWnd: HWND; const AParam: LPARAM): BOOL; stdcall; begin if AWnd = Wnd then Caption := 'OK'; Result := True; end; begin Wnd := Handle; EnumWindows(@EnumWindowsProc, 0); end;
Приступим.
Конечно, первое что бросается в глаза — вызов калбэка, в котором происходит обращение к переменной Wnd.
Жалко, что Delphi позволяет вообще такому компилироваться.
Правда это обусловлено не совсем верной декларацией самой функции EnumWindows.
Если бы она была объявлена вот так:
type TFNWndEnumProc = function(AWnd: HWND; AParam: LPARAM): BOOL; stdcall; function EnumWindows(lpEnumFunc: TFNWndEnumProc; lParam: LPARAM): BOOL; stdcall; external user32 name 'EnumWindows';
То ошибка была бы выявлена еще на этапе компиляции и Delphi выдала бы сообщение:
[dcc64 Error] Unit1.pas(49): E2094 Local procedure/function 'EnumWindowsProc' assigned to procedure variableПри этом вызывать нужно без взятия указателя, т.е. вот так:
EnumWindows(EnumWindowsProc, 0);
Если же мы будем получать адрес функции EnumWindowsProc посредством оператора "@", то даже такая декларация не поможет, т.к. в данном случае мы будем работать с нетипизированным указателем и все проверки на этапе компиляции будут отключены.
Конечно можно включить в настройках компилятора флаг "Typed @ operator" и получить сообщение:
[dcc64 Error] Unit1.pas(45): E2010 Incompatible types: 'TFNWndEnumProc' and 'Pointer'Но это уже как-то избыточно :)
Впрочем, так как объявление у EnumWindows другое, и данный код все же компилируется, попробуем разобраться с ним по пунктам:
- Переменная Wnd объявлена как локальная и память под нее выделена на стеке, фрейм которого принадлежит процедуре TForm1.Button1Click.
- EnumWindowsProc - это калбэк процедура, с своим собственным стеком, вызываемая не напрямую через EnumWindows, а через еще кучу пертрубаций в User32.dll
- EnumWindowsProc пытается получить доступ к Wnd и компилятор пытается этому помочь, ориентируясь на код.
Вот так выглядит вызов EnumWindows:
Тут нас интересует значение, помещенное на стек по адресу EBP-4, грубо говоря хэдл, полученный вызовом GetHandle.
На стеке он будет размещен примерно вот так:
Число 12F558 - это адрес локальной переменной Wnd, а 16051A - это хэндл главной формы, т.е. значение самой переменной. Запомним адрес 12F558 и посмотрим что произойдет при вызове калбэка EnumWindowsProc.
А вот это уже настоящая беда.
После генерации стекового фрейма (push+mov), EBP+8 указывает на параметр AWnd калбэка, Но посмотрите как происходит обращение к внешней переменной Wnd, с которой потом будет происходить сравнение.
005C734B 8B4510 mov eax,[ebp+$10] 005C734E 8B40FC mov eax,[eax-$04]
Delphi компилятор, ошибшись, сгенерировал асм код получения указателя на верхушку фреймового стека, полагаясь на то, что мы все еще находимся в исполнении процедуры Button1Click, после чего ошибочно начал читать значение локальной переменной Wnd по адресу EAX-4.
Это грозит - глобальным AV, ведь текущий стек не принадлежит Button1Click и мы реально можем влететь на ошибку как при получении верхушки фрейма, а если повезло и не упали, то упадем на чтении якобы Wnd с битого адреса (если повезет).
Показываю с использованием карты памяти процесса:
Можно сказать повезло, могли попасть и в секцию проверки битых указателей (первые 64 кб памяти процесса).
Сами понимаете что вторая строка (получение значения Wnd) уже как минимум не будет корректной, при работе с такими данными.
А нам нужен был адрес 12F558 - но не судьба.
Впрочем, перейдем к пункту за номером 4.
А именно к строчке:
Caption := 'OK';
Конечно-же в калбэк не передается перемеменна Self, но Delphi компилер не смог предположить такого цимуса, и даже в этом случае пытается выйти на нее.
005C7356 8B4510 mov eax,[ebp+$10] 005C7359 8B40F8 mov eax,[eax-$08]
Понятно, что такого у него не получится, но... если вдруг он все-же получит какой-то не совсем убитый адрес, то произойдет глобальный бадабум на вызове сеттера Caption, ибо произойдет вызов WindowProc на не понятно каком окне (я даже про VMT пока не говорю).
Часть вторая
Если с первой задачей проблем никаких не было — все на виду, то вот вторая поставила меня в глобальный тупик.
procedure TForm1.Button1Click(Sender: TObject); function EnumWindowsProc(const AWnd: HWND; const AParam: LPARAM): BOOL; stdcall; var Wnd: HWND; begin Wnd := HWND(AParam); if AWnd = Wnd then Result := True else Result := False; end; var Wnd: HWND; begin Wnd := Handle; EnumWindows(@EnumWindowsProc, LPARAM(Wnd)); end;
Сразу скажу — на мой взгляд код правильный, но раз есть заковыка (а иначе и не было бы такой задачи поставлено), значит надо анализировать.
Единственный момент с условием выхода из EnumWindowsProc, ибо практически гарантированно, что первое найденное окно не будет хэндлом формы, что означает завершение работы EnumWindowsProc после первого-же сравнения (по False результату).
Повтыкав минут двадцать в данный код и не обнаружив ошибки, я выкатил его всему нашему IT отделу, с целью - может коллективный разум осилит?
Через час мы сдались и я начал анализировать уже с точки зрения реверсера.
Итак, что мы имеем:
sdf.pas.31: begin 005C7348 55 push ebp 005C7349 8BEC mov ebp,esp sdf.pas.32: Wnd := HWND(AParam); 005C734B 8B450C mov eax,[ebp+$0c] sdf.pas.33: if AWnd = Wnd then 005C734E 3B4508 cmp eax,[ebp+$08] 005C7351 7505 jnz $005c7358 sdf.pas.34: Result := True 005C7353 83C8FF or eax,-$01 005C7356 EB02 jmp $005c735a sdf.pas.36: Result := False; 005C7358 33C0 xor eax,eax sdf.pas.37: end; 005C735A 5D pop ebp 005C735B C20800 ret $0008 005C735E 8BC0 mov eax,eax sdf.pas.41: begin 005C7360 55 push ebp 005C7361 8BEC mov ebp,esp sdf.pas.42: Wnd := Handle; 005C7363 E8248EF5FF call TWinControl.GetHandle sdf.pas.43: EnumWindows(@EnumWindowsProc, LPARAM(Wnd)); 005C7368 50 push eax 005C7369 6848735C00 push $005c7348 005C736E E879BBE4FF call EnumWindows sdf.pas.44: end; 005C7373 5D pop ebp 005C7374 C3 ret
Первичный анализ асм кода показывает, что использованные локальные перменные Wnd выкинуты оптимизатором, т.е. вот такой вот код будет выдавать абсолютно идентичный асм листинг:
procedure TForm1.Button1Click(Sender: TObject); function EnumWindowsProc(const AWnd: HWND; const AParam: LPARAM): BOOL; stdcall; begin if AWnd = HWND(AParam) then Result := True else Result := False; end; begin EnumWindows(@EnumWindowsProc, LPARAM(Handle)); end;
Значит заковыка не в использовании локальных переменных.
Вариант с ошибками каста HWND/LPARAM отпадает, тут все правильно.
Вариант с выносом калбэка за тело процедуры асм код не меняет (да и смысл?).
Вариант с неверной декларацией EnumWindowsProc тоже отпадает (спецом исходники винды поднял - может там какой нюанс крылся?)
Значит остается последний вариант: может что-то с условием, как-то странно оно выглядит.
Переписываю вот так:
procedure TForm1.Button1Click(Sender: TObject); function EnumWindowsProc(const AWnd: HWND; const AParam: LPARAM): BOOL; stdcall; begin Result := AWnd = HWND(AParam); end; begin EnumWindows(@EnumWindowsProc, LPARAM(Handle)); end;
Выхлоп:
sdf.pas.29: begin 005C7348 55 push ebp 005C7349 8BEC mov ebp,esp sdf.pas.30: Result := AWnd = HWND(AParam); 005C734B 8B4508 mov eax,[ebp+$08] 005C734E 3B450C cmp eax,[ebp+$0c] 005C7351 0F94C0 setz al 005C7354 F6D8 neg al 005C7356 1BC0 sbb eax,eax sdf.pas.31: end; 005C7358 5D pop ebp 005C7359 C20800 ret $0008 sdf.pas.33: begin 005C735C 55 push ebp 005C735D 8BEC mov ebp,esp 005C735F 53 push ebx 005C7360 8BD8 mov ebx,eax sdf.pas.34: EnumWindows(@EnumWindowsProc, LPARAM(Handle)); 005C7362 8BC3 mov eax,ebx 005C7364 E8238EF5FF call TWinControl.GetHandle 005C7369 50 push eax 005C736A 6848735C00 push $005c7348 005C736F E878BBE4FF call EnumWindows sdf.pas.35: end; 005C7374 5B pop ebx 005C7375 5D pop ebp 005C7376 C3 ret
И опять нет ничего странного.
Тут я сдулся, ибо если последний вариант кода считать верным (а он верный, иначе даже в MSDN ошибка) то я категорически не понимаю - где здесь тонкий нюанс?
Но раз GunSmoker выкатил такую задачу - значит что-то тут есть еще такое, а я лично сдаюсь :)
UPDATE:
Ан нет - рано еще здаваться, как оказалось дело было в том что я забыл про 64 бита.
Смотрим ASM код функции EnumWindowsProc:
00000000006960A0 55 push rbp 00000000006960A1 4883EC10 sub rsp,$10 00000000006960A5 488BEC mov rbp,rsp 00000000006960A8 48894D20 mov [rbp+$20],rcx // сохраняем значение AWnd 00000000006960AC 48895528 mov [rbp+$28],rdx // сохраняем значение AParam 00000000006960B0 4C894530 mov [rbp+$30],r8 // и вот пошел ошибочный код, сохраняем R8 hgfjgfjh.pas.55: Wnd := HWND(AParam); 00000000006960B4 488B4530 mov rax,[rbp+$30] // перемещаем значение R8 на верхушку стека 00000000006960B8 48894500 mov [rbp+$00],rax hgfjgfjh.pas.56: if AWnd = Wnd then 00000000006960BC 488B4528 mov rax,[rbp+$28] // читаем значение AParam 00000000006960C0 483B4500 cmp rax,[rbp+$00] // и сравниваем его с R8, а нужно с AWnd 00000000006960C4 7509 jnz EnumWindowsProc + $2F hgfjgfjh.pas.57: Result := True 00000000006960C6 C7450CFFFFFFFF mov [rbp+$0c],$ffffffff 00000000006960CD EB07 jmp EnumWindowsProc + $36 hgfjgfjh.pas.59: Result := False; 00000000006960CF C7450C00000000 mov [rbp+$0c],$00000000 hgfjgfjh.pas.60: end; 00000000006960D6 8B450C mov eax,[rbp+$0c] 00000000006960D9 488D6510 lea rsp,[rbp+$10] 00000000006960DD 5D pop rbp 00000000006960DE C3 ret
Вот оно что, Михалыч.
Причина простая, по умолчанию параметры передаются через регистры RCX, RDX, R8 и т.п.
Так вот для Nested функций, Delphi в регистр RCX помещает адрес вершины стека.
Таким образом оставшиеся два параметра идут в RDX и R8.
А код внутри EnumWindows этого не делает и по честному передает оба параметра через RCX + RDX.
Получается что мы работаем совершенно убитыми данными.
Как избавится от такого поведения?
Ответ прост - вынести функцию EnumWindowsProc за пределы процедуры TForm1.Button1Click.
function EnumWindowsProc(const AWnd: HWND; const AParam: LPARAM): BOOL; stdcall; var Wnd: HWND; begin Wnd := HWND(AParam); if AWnd = Wnd then Result := True else Result := False; end; procedure TForm1.Button1Click(Sender: TObject); var Wnd: HWND; begin Wnd := Handle; EnumWindows(@EnumWindowsProc, LPARAM(Wnd)); end;
После чего регистр RCX не будет задействован и сгенерируется правильный ASM код:
0000000000696030 55 push rbp 0000000000696031 4883EC10 sub rsp,$10 0000000000696035 488BEC mov rbp,rsp 0000000000696038 48894D20 mov [rbp+$20],rcx // сохраняем значение AWnd 000000000069603C 48895528 mov [rbp+$28],rdx // сохраняем значение AParam hgfjgfjh.pas.31: Wnd := HWND(AParam); 0000000000696040 488B4528 mov rax,[rbp+$28] // перемещаем значение AParam на верхушку стека 0000000000696044 48894500 mov [rbp+$00],rax hgfjgfjh.pas.32: if AWnd = Wnd then 0000000000696048 488B4520 mov rax,[rbp+$20] // читаем значение AWnd 000000000069604C 483B4500 cmp rax,[rbp+$00] // и сравниваем его с AParam 0000000000696050 7509 jnz EnumWindowsProc + $2B hgfjgfjh.pas.33: Result := True 0000000000696052 C7450CFFFFFFFF mov [rbp+$0c],$ffffffff 0000000000696059 EB07 jmp EnumWindowsProc + $32 hgfjgfjh.pas.35: Result := False; 000000000069605B C7450C00000000 mov [rbp+$0c],$00000000 hgfjgfjh.pas.36: end; 0000000000696062 8B450C mov eax,[rbp+$0c] 0000000000696065 488D6510 lea rsp,[rbp+$10] 0000000000696069 5D pop rbp 000000000069606A C3 ret
Собственно, как мне подсказали, этот момент описан в документации (о чем я не знал).
Nested procedures and functions (routines declared within other routines) cannot be used as procedural values, nor can predefined procedures and functions.http://docwiki.embarcadero.com/RADStudio/XE8/en/Procedural_Types#Method_Pointers
Такие вот пироги :)
Слишком сильно подошёл, там всё проще :) будешь ответом разочарован.
ОтветитьУдалитьМне что-то такое и видится - но не подсказывай, еще время на решение у меня есть :)
УдалитьНекоторые функции WinAPI слишком прихотливы к возвращаемым callback-ами значениям: они хотят видеть False=0 и True=1, а Delphi выдаёт False=0 и True=-1 (минус один). Но насколько я помню, EnumWindows такой проблемы не имеет.
ОтветитьУдалитьВ данном случае для True генерируется именно -1
Удалитьпервый вариант:
or eax,-$01
второй:
setz al
neg al
sbb eax,eax
Соответственно -1 равный FFFFFFFF на операции сравнения всегда снимает ZF флг
УдалитьРечь о том, что некоторые API делают проверку с помощью "if (res=TRUE) ...", а не "if (res) ...".
ОтветитьУдалитьПримеры: EnumSystemLocales, CreateMutex, IAudioEndpointVolume.SetMute
Delphi знает, что BOOL - это не совсем Boolean
УдалитьРазумеется "не совсем": у них даже длина разная. Проблема в том, что, по неведомым мне причинам, BOOL(True) в Delphi равен минус единице (как в VB), а в C (и WinAPI на нём написанном) единице. Про разные bool-ы:
Удалитьhttp://blogs.msdn.com/b/oldnewthing/archive/2004/12/22/329884.aspx
http://blogs.msdn.com/b/oldnewthing/archive/2011/03/28/10146459.aspx
http://blogs.msdn.com/b/ericlippert/archive/2004/07/15/184431.aspx
Размер результат возврата функции (BOOL/Boolean) не имеет значения (EAX всегда 4 байта/или RAX - 8), а вот на разницу проверки Вы верно указали.
УдалитьВ одном случае будет проверятся первый бит результата, в другом первый байт (если не трогаем OLE) - такой момент известен, но не относится к рассматриваемой ситуации.
_Rouse, только не бей :D
ОтветитьУдалитьЗадачка №18В
Вызов принят - Boolean :)
УдалитьНа этом остановимся или раскатать идею? :)
"Размер результат возврата функции (BOOL/Boolean) не имеет значения (EAX всегда 4 байта/или RAX - 8)" ;)
УдалитьНу... слегка маханул, конечно. Случаи разные бывают :)
УдалитьСань, так мне как, на третью задачу ответ давать или попридержим?
УдалитьНу, на твоё усмотрение. Я ж её специально тебе написал :))) Тут если кто все комменты читает - и так смекнёт.
УдалитьПринято, на третью задачу ответ не делаю.
УдалитьНо, Саш - вторая задача это было круто :)
Давно я так не разминал мозги :)