Библиотека Интернет Индустрии I2R.ru |
|||
|
Win32ASM: Консольный ввод, томограф IDA и скальпель SoftICEСодержание Консольный ввод и томограф IDA #1. В прошлый раз мы разобрались с консольным выводом. Сегодня - разберемся с
консольным вводом. Для этого напишем простую программу, запрашивающую строку символов, а затем её же
(строку) и выводящую. Это будет очередной маленький шажок, который для вас вполне может стать решающим,
так как именно эта программа впоследтвии послужит нам жертвой для негуманных экспериментов, в которых мы
впервые используем два хирургических инструмента - отладчик SoftIce (на правах скальпеля) и дизассемблер
IDA Pro (на правах томографа). И если при виде окровавленных внутренностей программы вам не поплохеет,
более того - если вам ЭТО понравится, значит вы имеете неплохие шансы стать либо серийным маньяком
(крэкером), либо - исследователем программ (реверсером). Все же остальные профессии отныне перестанут
для вас существовать ;) .386 .model flat, stdcall option casemap :none ; case sensitive ; ###################################################### include \tools\masm32\include\windows.inc include \tools\masm32\include\kernel32.inc includelib \tools\masm32\lib\kernel32.lib ; ###################################################### .data Msg1 db "Type something > " Msg2 db "You typed > " ConsoleTitle db 'Input & Output',0 ; ###################################################### .code ; ###################################################### Main proc LOCAL InputBuffer[128] :BYTE ;буффер для ввода LOCAL hOutPut :DWORD ;хэндл для вывода LOCAL hInput :DWORD ;хэндл для ввода LOCAL lpszBuffer :DWORD ;адрес буфера LOCAL nRead :DWORD ;прочитано байт LOCAL nWriten :DWORD ;напечатано байт ;устанавливаем титл окна invoke SetConsoleTitle, addr ConsoleTitle ;получаем хэндл для вывода invoke GetStdHandle, STD_OUTPUT_HANDLE mov hOutPut, eax ;печатаем "Type something > " invoke WriteConsole, hOutPut, addr Msg1, 17, addr nWriten,NULL ;получаем хэндл для ввода invoke GetStdHandle,STD_INPUT_HANDLE mov hInput, eax ;вводим invoke ReadConsole, hInput, addr InputBuffer, 10, ADDR nRead, NULL ;печатаем "You typed > " invoke WriteConsole, hOutPut, addr Msg2, 12, addr nWriten, NULL ;печатаем то, что ввели invoke WriteConsole, hOutPut, addr InputBuffer, nRead, addr nWriten, NULL ;задержка, чтобы полюбоваться invoke Sleep, 2000d ;выход invoke ExitProcess,0 Main endp ; ###################################################### end Main ПРИМЕЧАНИЕ: Ручной подсчет числа выводимых символов хорошим стилем программирования, конечно же, не назовешь :(. Немного попозже я расскажу, как делать это дело правильно ;). И да простят меня продвинутые программеры... Строка BOOL ReadConsole( HANDLE hConsoleInput, // handle to console input buffer LPVOID lpBuffer, // data buffer DWORD nNumberOfCharsToRead, // number of characters to read LPDWORD lpNumberOfCharsRead, // number of characters read LPVOID lpReserved // reserved ); Попутно даю урок английского языка, который сам знаю хреново (академиев не кончал, но высшее образование вам даду): "number of characters to read" переводится как "число символов, подлежащих чтению", а "number of characters read" - как "число прочитанных символов". Наверное. Согласитесь, это существенная разница ;). #2. Далее обратите внимание, что адрес переменной мы получаем не при помощи lea eax, LocalVar push eax Как обычно, я надеюсь на то, что вы не поверили мне на слово. Мы обязательно разберемся с тем, как происходит резервирование места в стеке и обращение к локальным переменным, но сделаем немного позже. Для начала нам нужно хотя бы чуть-чуть ознакомиться с инструментарием. Начнем мы, пожалуй, с IDA - The Interactive Disassembler. #3. IDA относится и интенсивно развивающимся продуктам, то есть вследствие постоянного совершенствования даже близкие версии могут вести себя по-разному. А по сему оговорюсь: описываемая мной последовательность действий рассчитана на версию 4.1.5.520, самым честным образом купленную. Если у вас другая версия, и мои советы "не проходят", то: во-первых, я не виноват, а во-вторых - для разнообразия попробуйте не только тупо следовать руководству, но еще и головой думать (сорри за грубость). #4. Итак, давайте проведем первое знакомство с этим плодом человеческого гения...
Запускаем ИДУ и озадачиваем ее нашим исполнимым файлом (File > Open).
Я не менял настройки, оставил все дефолтом. Но посмотрите, например, на список Processor type, разве он не впечатляет? Жмем на ОК и получаем дизассемблированный листинг нашей программы. Лезем в пункт меню Views, и знакомимся с некоторыми его подпунктами. Сразу же совет: запоминайте горячие клавиши того или иного пункта меню. Это сэкономит вам кучу времени ;) .text:00401000 55 8B EC 81 C4 6C FF FF-FF 68 1D 30 40 00 E8 A7 "UEuA-l h_0@.oc" .text:00401010 00 00 00 6A F5 E8 94 00-00 00 89 85 7C FF FF FF "...j?oO...EA| " .text:00401020 6A 00 8D 85 6C FF FF FF-50 6A 11 68 00 30 40 00 "j.IAl Pj_h.0@." .text:00401030 FF B5 7C FF FF FF E8 8B-00 00 00 6A F6 E8 6C 00 " ¦| oE...j?ol." ... Не правда ли, до боли знакомо? Переключаемся назад в режим листинга нажатием клавиши F4. View > Open subview > Functions выдаст нам окошко со всеми имеющимися в программе функциями:
Как видим, здесь в одну простыню собраны как наши собственные (в нашем примере это одна-единственная c именем start), так и обращения к внешним, апишным функциям. К каждой из функций в прилагается еще и дополнительная информация, подробнее об этом мы еще поговорим. Кликаем View > Open subviews > Segments и видим следующую картинку:
Как вы уже, должно быть, догадались, это окошко показывает нам, из каких секций состоит дизассемблированная программа. #5. Двойной щелчок по той или иной секции переместит указатель на то место дизассемблированного листинга, где эта секция начинается. .data:00403000 ; Segment type: Pure data .data:00403000 _data segment para public 'DATA' use32 .data:00403000 assume cs:_data .data:00403000 ;org 403000h .data:00403000 unk_403000 db 54h ; T ; DATA XREF: start+2B^o .data:00403001 db 79h ; y .data:00403002 db 70h ; p .data:00403003 db 65h ; e .data:00403004 db 20h ; .data:00403005 db 73h ; s .data:00403006 db 6Fh ; o .data:00403007 db 6Dh ; m .data:00403008 db 65h ; e .data:00403009 db 74h ; t .data:0040300A db 68h ; h .data:0040300B db 69h ; i .data:0040300C db 6Eh ; n .data:0040300D db 67h ; g .data:0040300E db 20h ; .data:0040300F db 3Eh ; > .data:00403010 db 20h ; .data:00403011 unk_403011 db 59h ; Y ; DATA XREF: start+6D^o .data:00403012 db 6Fh ; o .data:00403013 db 75h ; u .data:00403014 db 20h ; .data:00403015 db 74h ; t .data:00403016 db 79h ; y .data:00403017 db 70h ; p .data:00403018 db 65h ; e .data:00403019 db 64h ; d .data:0040301A db 20h ; .data:0040301B db 3Eh ; > .data:0040301C db 20h ; .data:0040301D unk_40301D db 49h ; I ; DATA XREF: start+9^o .data:0040301E db 6Eh ; n .data:0040301F db 70h ; p .data:00403020 db 75h ; u .data:00403021 db 74h ; t .data:00403022 db 20h ; .data:00403023 db 26h ; & .data:00403024 db 20h ; .data:00403025 db 4Fh ; O .data:00403026 db 75h ; u .data:00403027 db 74h ; t .data:00403028 db 70h ; p .data:00403029 db 75h ; u .data:0040302A db 74h ; t .data:0040302B db 0 ; .data:0040302C align 200h .data:0040302C _data ends .data:0040302C .data:0040302C .data:0040302C end start... Что мы видим? Длиннющую простыню из байтов! Ида нам подсказывает, какой символ соответствует тому или иному шестнадцатеричному значению. Не нужно быть семи пядей во лбу, чтобы догадаться, что эта простыня соответствует следующему куску нашего исходника: .data Msg1 db "Type something > " Msg2 db "You typed > " ConsoleTitle db 'Input & Output',0 То есть .text:0040102B push offset unk_403000 ; lpBuffer И в самом деле, мы видим, что обращение к переменной (пихание оной в стек) происходит по смещениию 2B при помощи префикса offset. И тут же видим ну вообще потрясающую вещь - IDA смекнула, что череда пушей перед #6. Честно говоря, мне не нравится то, какой простыней Ида дизассемблировала блок данных. Хотелось бы лицезреть его в таком виде, каков он был в исходном тексте ;). Для этого ставим указатель на последний db в секции данных и жмем на правую кнопку мыши, а вывалившейся контекстной менюшке
выбираем "s", то бишь "переопределить в строку". В итоге получим крАсивую, и, что самое главное, сходу понятную, строчку: .data:0040301D aInputOutput db 'Input & Output',0 ; DATA XREF: start+9_o Проделаем такую же процедуру и с двумя "вышележащими" метками, получив в итоге следующее отображение секции данных: .data:00403000 _data segment para public 'DATA' use32 .data:00403000 assume cs:_data .data:00403000 ;org 403000h .data:00403000 aTypeSomething db 'Type something > ' ; DATA XREF: start+2B_o .data:00403011 aYouTyped db 'You typed > ' ; DATA XREF: start+6D_o .data:0040301D aInputOutput db 'Input & Output',0 ; DATA XREF: start+9_o .data:0040302C align 200h .data:0040302C _data ends Желающие могут обозвать метки по-своему. Для этого кликните правой кнопокой по адресу и выберите пункт Rename.
И можно вводить любые ругательные слова, которые только придут на ум ;) #7. Обратите внимание вот на что: ... .text:00401009 push offset unk_40301D ; lpConsoleTitle ... .text:0040102B push offset unk_403000 ; lpBuffer ... .text:0040106D push offset unk_403011 ; lpBuffer ... Когда мы переопределили данные из байтовых в строковые, то Ида переобозвала переменные в .data:00403000 aTypeSomethingY db 'Type something > You typed > Input & Output',0 А обращение к ней осуществлялось бы вот как: ... .text:00401009 push (offset aTypeSomethingY+1Dh) ; lpConsoleTitle ... .text:0040102B push offset aTypeSomethingY ; lpBuffer ... .text:0040106D push (offset aTypeSomethingY+11h) ; lpBuffer ... Как я уже говорил, на сегодняшний день не существует ни одного полностью автоматического дизассемблера, способного генерировать безупречно работоспособный листинг (вырожденые примеры наподобие нашего - не в счет), поэтому в той или иной мере, но доводить его готовности приходится человеку. Что мы помаленьку и будем учиться делать. За сим будем считать что первое знакомство с Идой состоялось, а в качестве домашнего задания - попробуйте диассемблировать нашу программу Sourcer'ом и WinDASM'ом, чтобы, как говориться, почувствовать разницу ;) Консольный ввод и скальпель SoftICE #1. Сначала был 8086-й процессор, операционная система DOS и отладчик debug фирмы Microsoft. Отладчик неудобный, со скромными возможностями - он был (однако, есть и будет!) пригоден разве что для разного рода низкоуровневых забав и изучения ассемблера ;). В то время отладчики росли подобно грибам после мягкого кислотного дождика. И с такой же скоростью уходили в забвение - ибо от своего мелкомягкого прототипа отличались лишь интерфейсом. Это было золотое время для разработчиков всемозможных защит, так как достаточно было "запереть" клавиатуру, запретить прерывания, сбросить флаг трассировки, и у незадачливого хакера надолго отпадала охота копаться в чужом исполняемом коде... #2. Как вы, должно быть, уже поняли, способ запуска сайса (именно так мы будем назвать SoftIce) несколько сложнее, чем всенародно любимого пасьянса "косынка". В Windows 9X для этого нужно было редактировать autoexec.bat и делать мультиконфигурацию, но в NТ с этим дела обстоят немного проще. Всю последовательность действий я привожу из предположения, что у читателя установлена операционная система Windows 2000 (которая, как известно, "build on NT technology"), английская, а сайс берется из пакета DriverStudio версии 2.5. При этом желающие "сходу" произвести полную установку этого пакета должны иметь в виду, что программа инсталляции попросит, помимо всего прочего, указать пути к Driver Development Kit ;).
Честно говоря, меня вполне устраивает ручной режим загрузки, то есть Manual - наверное, это потому что я еще не сталкивался с отладкой "core device driver" ;). Поэтому насчет остальных режимов ничего сказать не могу. Итак, ставим Manual ;). Чтобы запустить сайс в этом режиме, необходимо ввести команду: NET START NTICE либо запустить "Start SoftICE" из менюшки (pif на обыкновенный батник, где эта команда написана). -------------------------------------------------------------------------------- EAX=00005305 EBX=C4920074 ECX=C14698E4 EDX=00000000 ESI=C1476EC0 EDI=C49202B0 EBP=67890000 ESP=C4687E2C EIP=000080D2 o d I s z a P c CS=0128 DS=0030 SS=0030 ES=0030 FS=0078 GS=0030 --------------------------------------------------byte-------------------PROT16- 0030:00000000 9E 0F C9 D8 65 04 70 00-16 00 C9 09 65 04 70 00 ....e.p.....e.p. 0030:00000010 65 04 70 00 54 00 FF F0-58 7F 00 F0 FF E7 00 F0 e.p.T........... 0030:00000020 00 00 00 C9 D2 08 A3 0A-6F EF 00 F0 6F 00 F0 00 .........o...o.. 0030:00000030 6F EF 00 F0 6F EF 00 F0-9A 00 C9 09 65 04 70 00 o...o.......e.p. -----Cancel_Call_When_Idle+002C------------------------------------------PROT16- 0128:80D1 POPF 0128:80D2 CLS 0128:80D3 RETF 0128:80D4 POPF 0128:80D5 STC 0128:80D6 RETF 0128:80D7 CMP AL,13 0128:80D9 NOP 0128:80DA NOP 0128:80DB JBE 80E1 -------------------------------------------------------------------------------- :rs :g WINICE: Free32 Obj=01 Mod=NOTEPAD WINICE: Free32 Obj=02 Mod=NOTEPAD WINICE: Free32 Obj=03 Mod=NOTEPAD WINICE: Free32 Obj=04 Mod=NOTEPAD WINICE: Free32 Obj=05 Mod=NOTEPAD WINICE: Free16 Sel=351F :X -------------------------------------------------------------------------------- X, XFRAME, XG, XP, XRSET, XT KERNEL32 -------------------------------------------------------------------------------- Сверху, как вы уже, должно быть, догадались, регистры. Чуть ниже - дамп памяти. Еще ниже - дизассемблированные команды процессора. Далее следуют окно диспетчера, в котором мы можем вводить команды и читать различные матюгальники, и контекстная подсказка. #3. Не знаю, как на вашем дисплее, но на моем 17-дюймовом с разpешением 1024х768 окошко отладчика получилось больно уж маленьким. Чтобы не напрягать глаза, его можно немножко "под себя" настроить.
Ок! Давайте попробуем ввести несколько команд, которые позволяют выполнить подобную настройку. Для этого пишем следующие команды:
Теперь, когда мы настроили "под себя" внешний вид отладчика, давайте позаботимся о том, чтобы нам не нужно было при его запуске каждый раз вводить эти семь команд. Т.е. пропишем все эти команды в строку инициализации - простыню команд, которые автоматически будут выполняться при загрузке отладчика.
Для этого ищем конфигурационный файл winice.dat (в 2000 я его нашел в Windows\system32\drivers\; в 9X, насколько я помню, он находился в том же каталоге, что и проинсталлированный SoftICE) и дополняем строчку INIT="SET FONT 2; LINES 60; WD 22; WC 25; CODE ON; COLOR A A 20 20 2; FAULTS OFF; X;" Далее раскомментируйте в winice.dat все строки наподобие EXP=\SystemRoot\System32\kernel32.dll Это необходимо для того чтобы сайс загрузил имена экспортируемых функций, находящихся в этих биб-лиотеках. Иначе вместо понятных команд, наподобие call USER32!MessageBoxA мы увидим безобразие типа call 0044F2A1 Теперь нам нужно перезапустить отладчик, чтобы проверить, подхватывает ли сайс наши настройки. Для этого нужно: В общем, как я уже говорил, сайс - прога, весьма специфическая, и выгрузить ее "из компьютера" можно только одним способом - перезагрузкой ;). Start > Shutdow > Restart: #4. Существуют две области применения сайса - отладка собственных программ и исследование чужих, так называемый reverse ingeneering. Для начала давайте научимся использовать сайс в качестве инструмента для отладки и изучения собственных приложений. 1. Сассемблировать исходник с ключем Zi: ml /c /coff /Zi src.asm В результате этого мы получим объектный файл, содержащий отладочную информацию для отладчика CodeView. Легко заметить, что размер этого файла намного больше, чем у его "нормального" аналога. 2. Слинковать объектный файл с ключами /DEBUG и /DEBUGTYPE:CV link.exe /SUBSYSTEM:CONSOLE /DEBUG /DEBUGTYPE:CV src.obj После этого, помимо экзешника, мы получим отладочные файлы src.ilk и simple.pdb, Microsoft Linker Database и Microsoft C/C++ program database соответственно. Теперь загружаем нашу отладочную версию программы в отладчик. Для этого запускаем Symbol Loader:
В левом окне мы видим, из каких файлов подгружена символическая информация. Зеленая лампочка в строке статуса свидетельствует о том, что отладчик подгружен (конечно, если вы это сделали после перезагрузки). Правое окно - это окно отчета, там появляется информация о выполненных действиях, ошибках и т.д. Ах да:, еще есть заголовок окна, там помимо названия программы есть еще и надпись в скобках [No Module Opened], то есть "не открыт модуль". Не правда ли, не очень тонкий намек?
Main 001B:00401010 55 PUSH EBP #5. Для начала рассмотрим команду display memory (отобразить память). Ее синтаксис: D[size] [address [l length]] Если ее использовать без параметров, то просто будет отображаться следующая страница дампа. Необязательный параметр size - это размерность, в которой выводится дамп. Вот расшифровка его значений: b Byte W Word D Double Word S Short Real L Long Real T 10-Byte Real Размерности byte, word и double word вам, конечно же, хорошо знакомы. А вот загадочные short long и 10-byte real мы рассмотрим немного позже. DD EIP то увидим дамп той области памяти, в которой в настоящее время располагается секция кода нашей программы (конечно, если после всплытия отладчика на точке входа вы ничего не успели напортачить). DW EIP L 1000 выведет в командную строку 1000 байтов памяти, начиная с текущего значения регистра EIP, группированных пословно, и вы будете вынуждены несколько раз нажать на any key, прежде чем все это просмотрите. По большому секрету скажу, что нажатие на ESC сразу же прервет просмотр. #6. Пожалуй, одна из главных возможностей любого отладчика - это трассировка, которая позволяет выполнять программу пошагово. Итак, загрузим нашу программу в SymbolLoader, нажмем на Load, а в появившемся окне отладчика введем команду "p" (она же - клавиша F10). В результате этого выполнится один логический шаг нашей программы (program step). Под "логическим шагом" подразумевается, своего рода "поверхностная" трассировка, без входа в процедуры, циклы и т.д. T [=start-address] [count] Первый - это адрес, с которого вы желаете начать трассировку, а count - это число инструкций, которое сайс протрассирует, прежде чем остановиться (и не забывайте о том, что он "заходит" в апишные функции!). #7. Еще одна полезная команда - это rs (она же F4), restore the program screen (восстановление экрана программы). А как же без этого? Ведь внешне работа программы заключается вовсе не в выполнении команд процессора и пересылках данных между регистрами ;). и затем нажать на F4, для того чтобы подсмотреть, действительно ли в консольном окне появилась строчка "Type something >". А затем нажать на any key и снова очутиться в отладчике.001B:00401046 E8BD000000 CALL _WriteConsoleA Или же, например, выполнив апишную функцию 001B: 00401070 E881000000 CALL _ReadConsoleA которая "просит" ввести строку символов, нажать на F4 и ввести необходимую строку символов, а по нажатию на Enter (типа "ввод закончен") снова оказаться в отладчике. #8. Теперь, когда мы поверхностно ознакомились с "хакерскими" инструментами, пришло время вспомнить о цели, которую мы преследовали, начав знакомство с идой и сайсом. Напоминаю - мы хотели исследовать, каким образом происходит декларирование локальных переменных в стеке, и как происходит к ним обращение. #1. Представьте себе картину: накачанный колесами и протеиновыми коктейлями шварценеггер бьет со всей дури по боксерской груше. Та отлетает в сторону, а потом по каким-то абсолютно нефизическим законам кинемотографа возвращается назад и бьет этому боксеру по морде лица, в результате чего шварценеггер отлетает на несколько метров, и, обязательно задев что-нибудь из мебели, размазывается соплями по стене - к неописуемому удовольствию зрителя. Посмотрев на такую картину, Станиславский бы сказал: "не верю"! А вот дZенствующей программер, увидя это безобразие, подумал бы: "ба! Да совсем как стек. Помницца, в одной из своих кулхацкерных прог я на похожие грабли как раз и напоролся". Ранее мы уже разобрались с очередностью записи в стек и чтения из а него. Напомню, что доступ к стеку осуществляется в соответствии с принципом LIFO (Last In First Out – Последним Пришел, Первым Ушел). Однако это отнюдь не единственное, что нам нужно знать о стеке - конечно же, если мы не собираемся время от времени получать "отдачу" от "боксерской груши" ;). Нам уже хорошо известно, что стек можно использовать для временного хранения данных – с его помощью мы выкручивались из такой проблемы, как недостаточное для полета нашей фантазии количество регистров. То есть мы временно сохраняли значения регистров в стеке, активно юзали их для наших нужд, а потом снова восстанавливали "статус кво", за исключением, в большинстве случаев, одного-единственного регистра, в котором хранился результат проделанной работы. Также известно, что в инструкциях процессоров от Интел переменные (которые в памяти) не могут выступать в качестве приемника и источника одновременно, то есть инструкция: mov [dwVar1],[dwVar2] не проходит. А выкрутиться из такой ситуации можно двумя способами - либо использовать в качестве посредника регистр (которых, как всегда, мало): mov eax,[dwVar2] mov [dwVar1],eax либо задействовав стек: push [dwVar2] pop [dwVar1] Кстати, недавно в почтовой рассылке RTFM_helpers прошло обсуждение того, как можно копировать из памяти в память - там было упомянуто, например, использование movs. А если подумать, можно найти и другие нетривиальные способы. Для тех, кто ещё не понял. Вот этот кусок кода: push 1 push 2 push 3 pop eax pop ebx pop ecx делает то же самое, что и следующий код (если только не считать разницу в скорости, размере кода и побочных эффектах): mov eax,3 mov ebx,2 mov ecx,1 Сомневающиеся могут проверить под отладчиком:). Также попробуйте сравнить: push 1 pop eax и психоделическое: sub esp,4 mov dword ptr [esp],1 mov eax,[esp] add esp,4 Вы можете мне не поверить, но эти два куска кода тоже "делают" одно и то же, хотя последний и кажется плодом больного воображения. - Что за esp такой? – спросите вы. Пошире откройте глаза и слушайте – сейчас я поведаю вам страшные тайны. ;) #2. Помните мою аналогию с блинами от штанги и вделанным в пол штырём, на который они надевались для хранения. Там еще был учитель физкультуры, который приставал к нашим неполовозрелым одноклассницам, в результате чего ему перебили нос. Так вот, адрес самого верхнего блина – это вершина стека и хранится он (адрес) в регистре esp/sp. Другими словами, вершина стека – это адрес последнего занесенного в стек элемента. Давайте посмотрим на поведение esp при трассировке следующего кода: Main proc push 1 push 2 push 3 pop eax pop ebx pop ecx invoke ExitProcess,0 Main endp При загрузке мы видим, что регистр esp инициализирован значением 12FFC4 (в других условиях стартовое значение может быть другим, но суть от этого не меняется). Давайте выполним один шаг (команда "t" - trace). В результате этого в стек ляжет 1, а значение регистра esp поменяется на 12FFC0. Трассируем дальше и наблюдаем, как изменяется esp: push 1 ;esp=12FFC0 push 2 ;esp=12FFBC push 3 ;esp=12FFB8 pop eax ;esp=12FFBC pop ebx ;esp=12FFC0 pop ecx ;esp=12FFC4 Протрассировав первые три строчки, мы можем сделать вывод, что стек "растет" в сторону младших адресов (12FFC0 > 12FFBC > 12FFB8, логично?), а шагом изменения регистра esp является 4. И это правильно, так как при 32-битном режиме адресации в стеке сохраняются двойные слова, они же - 4 байта (хотя допускается класть в стек также и 2-байтные слова - при этом шаг равен 2). К слову сказать: если бы мы писали под DOS (точнее, в реальном или виртуальном режиме), то стек у нас адресовался бы регистром sp и изменялся бы он на плюс-минус 2. Обобщаем. Алгоритм работы команды push <источник> следующий:
Об алгоритме работы команды pop догадайтесь сами… Для последователей дZена предлагаем тему для исследования: какое значение будет лежать в стеке после инструкции push esp. :) ПРЕДУПРЕЖДЕНИЕ: ни в коем случае не принимайте результаты отдельных экспериментов в конкретных условиях за абсолютную истину, верную всюду и всегда! Теперь, после вышесказанного, вы легко сообразите, какое значение примет регистр eax в следующем извращенном случае: push 1 ;esp=12FFC0 push 2 ;esp=12FFBC push 3 ;esp=12FFB8 add esp,4 pop eax ;esp=12FFC0 pop ebx ;esp=12FFC4 [Правильный ответ – 2. :)] #3. Есть ещё один регистр, ассоциируемый со стеком - ebp/bp, и описывается его функция так, что выговорить страшно – указатель базы кадра стека. Такое название этого регистра я нашел в книжке Юрова & Хорошенко "ASSEMBLER, учебный курс". Нет, конечно же, можно назвать калоши "мокроступами", а bitmap "двоично-точечной картинкой", но... "У меня нет слов, у меня есть только выражения в адрес того, кто заворачивает такие коленца" (C) Аркадий Белоусов. А по сему давайте заменим словосочетание "кадр стека" простым народным :) словом "фрейм", а "указатель базы" заменим на просто "база" (или "указатель" - по вкусу). В результате подобных терминологических "подстановок" получается следующая картина: есть у нас в компьютере некие "фреймы", располагаются они в стеке и адреса этих пока что непонятных нам штук завязаны с регистром ebp/bp. "Дело в следующем". Любая процедура/функция в терминах любого процедурного языка имеет (может иметь) нуль и больше параметров и локальных переменных. Область памяти, создаваемая (выделяемая) при вызове процедур для аргументов и локальных переменных, и называется фреймом. А чтобы процедура могла быть рекусивной (т.е. могла вызывать саму себя или вызываться из других процедур, которые она вызывает) или реентерабельной (т.е. чтобы код процедуры мог использоваться в параллельных процессах), фреймы для неё должны размещаться в стекоподобной структуре данных - "стеке". А поскольку процедурные языки ближе к "естественному" мышлению, то в наборы инструкций процессоров и ассемблеры вводят поддержку подобных высокоуровневых конструкций. Существуют нюансы и различия в реализациях, например:
Подобные "нюансы" сущности фрейма, конечно же, не меняют. Приведены они по одной единственной причине – чтобы вы поняли некоторую условность такого термина как "фрейм". Теперь, собсна, про ebp/ep. В случае стекового фрейма его адрес не фиксирован, поэтому адресация параметров и переменных в нём должна быть "базисно-индексной", относительно начала фрейма. На писюке под это идеально подошёл (или изначально проектировался) BP/EBP - в отличие от SP, он может служить базой, и также адресуется относительно SS. Я знаю, что вы мало что поняли из всего этого бреда, а по сему будем познавать истину путем долгой и продолжительной медитации. #4. Мы привыкли к тому, что извлекать данные из стека можно только повинуясь очередности LIFO. А что, если нам понадобилось обратиться к произвольному элементу стека? Один из возможных способов мы уже рассмотрели: это изменение значения регистра esp/sp плюс команда pop. Это далеко не совсем хорошая идея, и вам не стоит издеваться над стеком таким изощренным способом. Если, конечно же, вы не хотите уподобляться Штирлицу и Мюллеру, которые стреляли по очереди... Напомню: регистр ebp/bp ведет себя приблизительно таким же образом, как и хорошо нам знакомый ebx/bx. То есть он может выполнять роль базы. Напомню, что, например, следующий код: mov ebx,12FFC0h mov al,[ebx] присвоит регистру AL значение байта по адресу 12FFC0 из сегмента, задаваемого регистром DS. Точно таким же образом можно использовать и регистр ebp/bp. Говоря другими словами, это один из немногих регистров, которые можно "брать в квадратные скобки" не увеличивая при этом размер инструкции :). То есть (и это будут уже третьи слова) он позволяет работать с ячейкой памяти, адрес которой находится в регистре. Проиллюстрирую это на простом примере. Допустим, занесли вы в стек разных параметров кучу: адрес значение 0012FFC4 77E7EB69 0012FFC8 0047E4AC 0012FFCC 0012DAB4 0012FFD0 7FFDF080 И возжелалось вам в силу какой-нибудь нездоровой производственной необходимости прочитать "здесь и сейчаc", например, предпоследний элемент. В этом случае делаем вот что: mov ebp,esp mov eax,[ebp+4] Расшифровываю. Мы принимаем адрес самого последнего из записанных в стек элемента за точку отсчета, и путем прибавления к этой "точке отсчета" четвёрок можно легко получить доступ к тому элементу стека, который нашей программерской душеньке возжелался. В принципе, в данном примере можно было бы использовать ESP вместо EBP, но, во-первых, должны же мы были показать использование EBP, :) а, во-вторых, в больших фрагментах кода использование ESP непосредственно имеет свои недостатки (больший размер инструкций и необходимость отслеживать изменение вершины стека). Вот именно такое безобразие и называется "организация произвольного доступа к данным внутри стека". #5. Перед тем, как мы пойдем дальше и разберемся-таки с локальными переменными – пара слов для особо продвинутых: В защищённом режиме (или в реальном режиме с префиксами смены разрядности) базой может служить любой регистр, поэтому, если мы точно знаем состояние регистра ESP (т.е. мы точно знаем, сколько раз мы делали push, а сколько pop), то для доступа к фрейму можно использовать ESP (при этом индексы одной и той же перменной в разных местах процедуры могут отличаться из-за промежуточных push/pop). Собсно, подобного рода оптимизацией занимаются, насколько я знаю, последние версии BC и VC. А в их "асмах" появилась директива "фраме-поин-оммисинс" как раз для таких извратов и предназначенная. Однако, здесь есть недостатки по сравнению с использованием более-менее статичного EBP, как об этом было упомянуто выше: во-первых, с [ESP] инструкции длиннее, во-вторых, нужно быть очень аккуратным в подсчёте промежуточных push/pop, чтобы верно подставлять смещение до аргумента или переменной в [ESP+xx] (а ведь есть относительно непредсказуемые инструкции вида push [esp+xxx]). Наконец, поскольку индекс xx может постоянно меняться, поэтому использовать встроенные директивы типа ARGS или даже вручную раставленные EQU становится малореальным. Поэтому возможность использования ESP в качестве базы (при ручной кодогенерации – глюкалово полное) отнюдь не умаляет полезности EBP. #6. Ну вот, мы и подошли к самой интересной части марлезонского балета. Сейчас мы готовы проанализировать нашу программу на предмет того, чего она там вытворяет с локальными переменными. :00401000 NumberOfCharsWritten= dword ptr -90h :00401000 nNumberOfCharsToWrite= dword ptr -8Ch :00401000 hConsoleInput = dword ptr -88h :00401000 hConsoleOutput = dword ptr -84h :00401000 Buffer = byte ptr -80h :00401000 :00401000 push ebp :00401001 mov ebp, esp :00401003 add esp, 0FFFFFF70h :00401009 push offset aInputOutput ; lpConsoleTitle :0040100E call SetConsoleTitleA :00401013 push 0FFFFFFF5h ; nStdHandle :00401015 call GetStdHandle :0040101A mov [ebp+hConsoleOutput], eax :00401020 push 0 ; lpReserved :00401022 lea eax, [ebp+NumberOfCharsWritten] :00401028 push eax ; lpNumberOfCharsWritten :00401029 push 11h ; nNumberOfCharsToWrite :0040102B push offset aTypeSomething ; lpBuffer :00401030 push [ebp+hConsoleOutput] ; hConsoleOutput :00401036 call WriteConsoleA :0040103B push 0FFFFFFF6h ; nStdHandle :0040103D call GetStdHandle :00401042 mov [ebp+hConsoleInput], eax :00401048 push 0 ; lpReserved :0040104A lea eax, [ebp+nNumberOfCharsToWrite] :00401050 push eax ; lpNumberOfCharsRead :00401051 push 80h ; nNumberOfCharsToRead :00401056 lea eax, [ebp+Buffer] :00401059 push eax ; lpBuffer :0040105A push [ebp+hConsoleInput] ; hConsoleInput :00401060 call ReadConsoleA :00401065 push 0 ; lpReserved :00401067 lea eax, [ebp+NumberOfCharsWritten] :0040106D push eax ; lpNumberOfCharsWritten :0040106E push 0Ch ; nNumberOfCharsToWrite :00401070 push offset aYouTyped ; lpBuffer :00401075 push [ebp+hConsoleOutput] ; hConsoleOutput :0040107B call WriteConsoleA :00401080 push 0 ; lpReserved :00401082 lea eax, [ebp+NumberOfCharsWritten] :00401088 push eax ; lpNumberOfCharsWritten :00401089 push [ebp+nNumberOfCharsToWrite] ; nNumberOfCharsToWrite :0040108F lea eax, [ebp+Buffer] :00401092 push eax ; lpBuffer :00401093 push [ebp+hConsoleOutput] ; hConsoleOutput :00401099 call WriteConsoleA :0040109E push 7D0h ; dwMilliseconds :004010A3 call Sleep :004010A8 push 0 ; uExitCode :004010AA call ExitProcess Начнем с того, что полегче ;). Нетрудно заметить, что команда addr в применении к локальным переменным (в контексте invoke) , во всех случаях генерит следующий код: lea eax,[ebp-X] push eax Предвидя грабли, на которые может натолкнуться начинающий, сразу оговорюсь, эти строки принципиально отличаются от push [ebp-X] Вы не поверите, но почему-то многие новички здесь путаются... Как, вы тоже? ;)) Первое – заносит в стек указатель на. А второе – значение. Всем медитировать! Обратите внимание, что Ида услужливо вынесла вверх листинга блок констант, каждая из которых имеет отрицательной значение. Но мы-то с вами ещё со школы умеем решать простенькие задачки на сложение отрицательных чисел и без труда высчитаем, что система уравнений: a = -84 b = x+a имеет более чем тривиальное решение: b = x-84 Хе-хе... слышала бы меня сейчас моя школьная учительница математики ;)). #7. Очевидно, что ebp – это некая "точка отсчета", относительно которой адресуются локальные переменные. А что ж у нас в ebp? Смотрим на начало процедуры, и ищем там строчки :00401001 mov ebp, esp :00401003 add esp, 0FFFFFF70h И медленно ("брюки превращаются... брюки превращаются...") приоткрываем завесу над тайной. "Дело в следующем": ассемблер смотрит, сколько мы задекларировали локальных переменных и какова размерность каждой. На основании этой информации определяется размер фрейма. В нашем случае задекларировано (смотрим исходник): LOCAL InputBuffer[128] :BYTE ;буффер для ввода LOCAL hOutPut :DWORD ;хэндл для вывода LOCAL hInput :DWORD ;хэндл для ввода LOCAL nRead :DWORD ;прочитано байт LOCAL nWriten :DWORD ;напечатано байт То есть 128 штук байт плюс 4 двойных слова по 4 байта в каждом. Итого для этого всего богатства нужно выделить 144 байт памяти. Ну и замечательно! Сохраняем адрес вершины стека (память под переменные еще не отведена!) в регистре ebp. Теперь это у нас вовсе не вершина, а "точка отсчёта" для локальных переменных. А саму вершину стека передвигаем на 144 байта "вверх", в сторону младших адресов (там у нас будет область для хранения локальных переменных). Как видите, все очень просто. :) Задание для медитации: чем будут отличаться генеримые инструкции для директивы addr в случае, если addr будет применяться к аргументам процедуры, а не к локальным переменным. Если вас смущает то, что для "поднятия планки" используется инструкция add и столь большое число, то вернитесь к выпуску о кодировании отрицательных чисел и особенностях дополнительного кода. Либо поставьте в Иде указатель на смущающее вас число 0FFFFFF70h и нажмите на Ctrl+-, после чего долго и упорно медитируйте. :) Serrgio/HI-TECH |
|
2000-2008 г. Все авторские права соблюдены. |
|