Hello All!
Делиться со-всякими алго и наработками уже входит у меня в привычку, и в продолжении цикла этих заметок предлагаю разбор всякого хлама из старого сундука. На повестке дня сегодня следующее:
1. Фиктивный стек
Герою восточного фольклора Ходже Насреддину приписывают выражение: -"Если гора не идёт к Магомеду, то Магомед пойдёт к горе". В этой части статьи попробуем спроецировать данное утверждение на системный стек, но для начала рассмотрим микро-архитектуру центрального процессора, и какое место занимает в ней этот стек.
На рисунке ниже представлена структурная схема входящих в состав CPU основных блоков.
При запуске нашей программы, функции системного загрузчика с префиксом LDR загружают образ двоичного кода с диска в оперативную память DDR-SDRAM (Synchronous Dynamic Random Access Memory), после чего код становится доступным центральному процессору CPU. Как только регистр-указатель EIP упрётся в точку-входа в программу EntryPoint, диспетчер памяти тут-же в пакетном режиме считывает из ОЗУ как-минимум по одной 4 Кбайтной странице из секции-кода и секции-данных (итого 8Кб), и сбрасывает их в кэш процессора L3.
Такого алго придерживаются процессоры только на старте, а дальше – данные читаются из ОЗУ исключительно по-востребованию, блоками по 64-байт, чтобы их можно было поместить в одну линейку кэш "Cache-Line". Если софт гигантских размеров типа Photoshop или Word из пакета Office, то диспетчер может заполнить кодом\данными весь кэш L3, что влечёт за собой тормоза на старте. Здесь всё в штатном режиме, а вот дальше уже интересней..
Кэши L2 и L3 не разделяют информацию на код и данные, более того в архитектуре НТ (гипертрейдинг) они являются общими для всех ядер одного процессора. Зато кэшей L1 уже два – отдельно для кода и отдельно для данных. Структура кэш-линеек такова, что помимо самой информации, в них имеются и специальные поля под названием "Tag", где хранится старшая часть виртуального адреса ОЗУ, от куда была скопирована инфа. Проверяя эти теги процессор ищет в L2 байты, которые принадлежат секции-кода и отправляет их в L1-инструкций, и далее в исполнительный конвейер. Соответственно если в теге прописан адрес секции-данных, то линейка отправляется в L1-данных, который ведёт диалог исключительно с блоками Load\Store ядра процессора Execute, минуя его Front-End.
Теперь рассмотрим ситуацию, когда декодер обнаруживает в L1 инструкцию PUSH – это может быть, например, передача параметров функции через стек. Поскольку стек представляет собой своеобразную секцию-данных, процессору приходится перегонять операнд инструкции PUSH по большой ветке кровообращения, из L1 инструкций, в L1 данных и обратно. Здесь становится очевидно, что в алгоритме вызова процедур и функций имеются недочёты, поскольку бесполезный транспорт данных явно снижает общую производительность. Для инженеров это было легче запрограммировать, чем искать компромиссы, ..тем-более что ситуации бывают разные и лучше выбрать золотую середину.
Однако фанатиков нетрадиционного кода такой расклад не устраивал, и ещё на третьих пеньках они придумали вполне разумное решение этой проблемы (салам Магомед). В основе оригинальной мысли лежал тот факт, что если в секции-данных заранее подготовить стековый фрейм с готовыми аргументами, можно будет не копировать их в стек, а наоборот натравить на этот фрейм регистр-указатель стека ESP (Stack-Pointer). Поскольку процессор слепо верит этому регистру, то примет подложный стек за чистую монету и без лишних слов отработает запрос. Тут главное правильно расположить все аргументы функции в секции-данных, не забыв при этом зарезервировать место под адрес-возврата, куда его неявно помещает инструкция CALL. Посмотрим на такой пример:
Немного комментов к этому исходнику..
#1. Значит директива
#2. Вызываемая функция может иметь локальные переменные, которые процессор кладёт выше указателя командой
#3. Теперь в самом коде запоминаем в регистре EBP указатель на системный стек
В результате, на момент вызова функции стек будет выглядеть примерно так (обратите внимание на адрес, который ведёт к секции-данных):
Если адрес-возврата заранее известен (а в большинстве случаях это именно так), то можно сразу поместить его во-фрейм с аргументами и вызывать функцию не через CALL, а обычным переходом JMP. В этом случае указатель ESP не требует уже смещения, и его нужно сфокусировать на макушке стека, как в примере ниже:
К сожалению такие шутки не понимают функции системных библиотек типа Kernel32.dll и прочие. Однако для своих внутренних самопальных процедур метод вполне себя оправдывает, причём чем больше мы передаём аргументов, тем в более выигрышном положении оказываемся. Попробуйте натравить профайлер на вызовы с фиктивным и обычным стеком, и легко убедитесь в этом.
2. Список программ в системном реестре
Однажды мне потребовалось создать список установленных в системе программ, и как оказалось – задача эта совсем не тривиальная. ПродумАны из мелкософт создали спец.либу для установки, получения списка и удаления программ под названием msi.dll (Microsoft Installer, см.дир System32), но этот герой с подмоченной репутацией на костылях, и воркает исключительно с продуктами самой Microsoft, пропуская ниже радаров программы сторонних разработчиков. Такие функции как MsiEnumProducts() и MsiGetProductInfo() обнаруживают только те прожки, которые система зарегистрировала у себя назначив им глобальный идентификатор GUID. Здесь уместно вспомнить выражение: "грешат не заметно – постятся на показ".
Поэтому было решено искать альтернативные пути, и гугл предложил мне ознакомиться с веткой реестра:
Так по волею судьбы пришлось парсить системный реестр, хоть я не отношу себя к сторонникам подобных методов: во-первых лучше доверить эту работу готовым системным функциям, а во-вторых – иногда доступ к реестру требует админских прав, что ущемляет наши собственные моральные права. Но поскольку ситуация зашла в тупик, пришлось смириться.
Функции API для работы с реестром изначально были сосредоточены в библиотеке advapi32.dll, но начиная с Vista их копии перекочевали и в Kernel32.dll. Это было сделано для удобства, чтобы наши приложения не тянули за собой ворох лишних библиотек. Конкретно в данном случае, мою проблему решили аж 7 функций, а в одной из них я обнаружил и нюанс, который изрядно потрепал мне нервы. Вот общий алгоритм программы:
Если мы не наделаем ошибок и угомоним системного агента правильными параметрами функций, то данный алгоритм вернёт нам полный список удаляемых программ, и остаётся лишь выводить их на консоль или в форточку мастдая. При необходимости можно применить и более жёсткие меры, например снести к чертям неугодную прожку, опираясь на свойство "UninstallString". Вот как выглядят реалии:
Обратите внимание, что функция RegQueryInfoKeyA() нашла всего 103 (под)каталога в ветке "Uninstall", и судя по логу 13 из них пустые – итого имеем 90 установленных в системе программ, где файлы приплюснутого си отнимают львиную долю. Такие дела..
3. Базовые операции с текстом
В заключении рассмотрим элементарные действия с текстом, которыми часто напрягают студентов учебных заведений. Ясно, что захватить весь текстовый периметр не получится, а потому остановимся лишь на простом алгоритме удалении лишних пробелов из строки, и смене регистра букв с прописных на заглавные и обратно.
• Если нас интересует регистр символов, то ставка здесь делается на их расположении в таблице ASCII. Заглянув в неё можно обнаружить, что разница между кодами прописных и заглавных составляет ровно 20h. Например код латиницы(А) в верхнем регистре (заглавной) равен 41h, а её-же прописной равен 61h. Аналогичный сдвиг наблюдаем и со-всеми остальными буквами, хоть в латинице, хоть в кириллице. Такой расклад наводит на мысль, что таблицу составляли отнюдь не глупые люди.
Теперь, если принять во-внимание, что 20h равно в двоичном 0010.0000, то оперируя всего одним битом(5) можно с лёгкостью менять регистр с верхнего на нижний и обратно. Для бинарных операций в ассемблере имеются свои инструкции –
• Избавиться от лишних пробелов в строке – ещё одна часто встречающаяся задача.
В виду того-что готовой функции API для этих целей в природе не существует, всё приходится делать в ручную. Суть в том, чтобы запоминать предыдущий символ, и сравнивать его с текущим. Если оба пробелы, то пропускаем перезапись текущего в буфер, иначе всё в штатном режиме, без изменений. Вот простая как 2-копейки реализация, зато пользу от неё можно наблюдать в консоли:
4. Заключение.
Мелочи подобного рода сильно отравляют жизнь начинающим асматикам, а так.. (на случай, если грянет гром) "зонт" у нас уже имеется. В скрепке можно найти исполняемые файлы для тестов. Надеюсь ещё встретимся в сообществе Codeby, всем удачи и пока.
Делиться со-всякими алго и наработками уже входит у меня в привычку, и в продолжении цикла этих заметок предлагаю разбор всякого хлама из старого сундука. На повестке дня сегодня следующее:
1. Фиктивный стек;
2. Список установленных программ в реестре;
3. Базовые операции с текстом;
4. Заключение.
----------------------------------------------1. Фиктивный стек
Герою восточного фольклора Ходже Насреддину приписывают выражение: -"Если гора не идёт к Магомеду, то Магомед пойдёт к горе". В этой части статьи попробуем спроецировать данное утверждение на системный стек, но для начала рассмотрим микро-архитектуру центрального процессора, и какое место занимает в ней этот стек.
На рисунке ниже представлена структурная схема входящих в состав CPU основных блоков.
При запуске нашей программы, функции системного загрузчика с префиксом LDR загружают образ двоичного кода с диска в оперативную память DDR-SDRAM (Synchronous Dynamic Random Access Memory), после чего код становится доступным центральному процессору CPU. Как только регистр-указатель EIP упрётся в точку-входа в программу EntryPoint, диспетчер памяти тут-же в пакетном режиме считывает из ОЗУ как-минимум по одной 4 Кбайтной странице из секции-кода и секции-данных (итого 8Кб), и сбрасывает их в кэш процессора L3.
Такого алго придерживаются процессоры только на старте, а дальше – данные читаются из ОЗУ исключительно по-востребованию, блоками по 64-байт, чтобы их можно было поместить в одну линейку кэш "Cache-Line". Если софт гигантских размеров типа Photoshop или Word из пакета Office, то диспетчер может заполнить кодом\данными весь кэш L3, что влечёт за собой тормоза на старте. Здесь всё в штатном режиме, а вот дальше уже интересней..
Кэши L2 и L3 не разделяют информацию на код и данные, более того в архитектуре НТ (гипертрейдинг) они являются общими для всех ядер одного процессора. Зато кэшей L1 уже два – отдельно для кода и отдельно для данных. Структура кэш-линеек такова, что помимо самой информации, в них имеются и специальные поля под названием "Tag", где хранится старшая часть виртуального адреса ОЗУ, от куда была скопирована инфа. Проверяя эти теги процессор ищет в L2 байты, которые принадлежат секции-кода и отправляет их в L1-инструкций, и далее в исполнительный конвейер. Соответственно если в теге прописан адрес секции-данных, то линейка отправляется в L1-данных, который ведёт диалог исключительно с блоками Load\Store ядра процессора Execute, минуя его Front-End.
Теперь рассмотрим ситуацию, когда декодер обнаруживает в L1 инструкцию PUSH – это может быть, например, передача параметров функции через стек. Поскольку стек представляет собой своеобразную секцию-данных, процессору приходится перегонять операнд инструкции PUSH по большой ветке кровообращения, из L1 инструкций, в L1 данных и обратно. Здесь становится очевидно, что в алгоритме вызова процедур и функций имеются недочёты, поскольку бесполезный транспорт данных явно снижает общую производительность. Для инженеров это было легче запрограммировать, чем искать компромиссы, ..тем-более что ситуации бывают разные и лучше выбрать золотую середину.
Однако фанатиков нетрадиционного кода такой расклад не устраивал, и ещё на третьих пеньках они придумали вполне разумное решение этой проблемы (салам Магомед). В основе оригинальной мысли лежал тот факт, что если в секции-данных заранее подготовить стековый фрейм с готовыми аргументами, можно будет не копировать их в стек, а наоборот натравить на этот фрейм регистр-указатель стека ESP (Stack-Pointer). Поскольку процессор слепо верит этому регистру, то примет подложный стек за чистую монету и без лишних слов отработает запрос. Тут главное правильно расположить все аргументы функции в секции-данных, не забыв при этом зарезервировать место под адрес-возврата, куда его неявно помещает инструкция CALL. Посмотрим на такой пример:
C-подобный:
.data
text db 'Hello!',0
mes1 db 'Codeby.net',0
mes2 db 'Marylin',0
align 16
localStack rd 64
funcArg dd 0,text,2222h,3333h,4444h,5555h ;//<--- аргументы функции (0 = резерв под RetAddr)
realStack rd 64
buff db 0
;//-------
.code
start: mov ebp,esp ;// EBP = линк на оригинальный стек системы
mov esp,funcArg +4 ;// ESP = линк на наш стек с аргументами!
call foo ;// funcArg +0 = адрес возврата
mov esp,ebp ;// вернуть указатель стека
cinvoke printf,<10,' Func: call OK!',0>
@exit: cinvoke _getch
cinvoke exit,0
;//-------
proc foo a1,a2,a3,a4,a5 ;//<---- функция с 5 аргументами
local i dd mes1 ;//<-------- Локальные переменные функции
local j dd mes2 ;//<-------- ^^^^
mov eax,[a1] ;// EAX = значение первого аргумента
mov ebx,[a2] ;// EBX = второго
push eax ebx ;// здесь можно пользоваться стеком
pop eax ebx ;// ^^^^
mov esi,[i] ;// ESI и EDI = локальные переменные
mov edi,[j] ;// ^^^^
ret ;// уйти по адресу возврата!
endp
Немного комментов к этому исходнику..
#1. Значит директива
align 16
в секции-данных тут обязательна, т.к. стек должен быть выровнен как-минимум на 4-байтную границу (в х64 на 8-байт), иначе поведение CPU непредсказуемо, и он может уйти в штопор в самый неподходящий момент.#2. Вызываемая функция может иметь локальные переменные, которые процессор кладёт выше указателя командой
SUB ESP,размер лок.переменных
. Поэтому выделяем под них 64 двойных слова в блоке памяти с меткой localStack
. Чтобы функция имела возможность пользоваться стеком в привычном нам понимании, необходимо выделить такой-же блок ниже стекого фрейма с аргументами – в примере на него указывает метка realStack
.#3. Теперь в самом коде запоминаем в регистре EBP указатель на системный стек
MOV EBP,ESP
, после чего меняем его расположение инструкцией MOV ESP,funcArg+4
. Смещением[+4] мы резервируем место под адрес-возврата из функции, для чего предусмотрен dword со-значением нуль в нашем фрейме.В результате, на момент вызова функции стек будет выглядеть примерно так (обратите внимание на адрес, который ведёт к секции-данных):
Если адрес-возврата заранее известен (а в большинстве случаях это именно так), то можно сразу поместить его во-фрейм с аргументами и вызывать функцию не через CALL, а обычным переходом JMP. В этом случае указатель ESP не требует уже смещения, и его нужно сфокусировать на макушке стека, как в примере ниже:
C-подобный:
.data
text db 'Hello!',0
align 16
localStack rd 64
funcArg dd @ret,text,2,3,4,5 ;//<--- стековый фрейм (@ret = RetAddr)
realStack rd 64
buff db 0
;//-------
.code
start: mov ebp,esp ;// EBP = линк на оригинальный стек
mov esp,funcArg ;// ESP = линк на наш стек, без смещения +4
jmp foo ;// прыгаем на функцию!
@ret: mov esp,ebp ;// восстановить указатель стека
cinvoke printf,<10,' Func: jmp OK!',0>
@exit: cinvoke _getch
cinvoke exit,0
;//-------
proc foo a1,a2,a3,a4,a5 ;//<---- функция с 5 аргументами
mov eax,[a2] ;// EAX = значение второго аргумента
mov ebx,[a5] ;// EBX = пятого
push eax ;// здесь можно пользоваться стеком
pop ebx ;// ^^^^
ret ;// уйти по адресу-возврата на метку @ret!
endp
К сожалению такие шутки не понимают функции системных библиотек типа Kernel32.dll и прочие. Однако для своих внутренних самопальных процедур метод вполне себя оправдывает, причём чем больше мы передаём аргументов, тем в более выигрышном положении оказываемся. Попробуйте натравить профайлер на вызовы с фиктивным и обычным стеком, и легко убедитесь в этом.
2. Список программ в системном реестре
Однажды мне потребовалось создать список установленных в системе программ, и как оказалось – задача эта совсем не тривиальная. ПродумАны из мелкософт создали спец.либу для установки, получения списка и удаления программ под названием msi.dll (Microsoft Installer, см.дир System32), но этот герой с подмоченной репутацией на костылях, и воркает исключительно с продуктами самой Microsoft, пропуская ниже радаров программы сторонних разработчиков. Такие функции как MsiEnumProducts() и MsiGetProductInfo() обнаруживают только те прожки, которые система зарегистрировала у себя назначив им глобальный идентификатор GUID. Здесь уместно вспомнить выражение: "грешат не заметно – постятся на показ".
Поэтому было решено искать альтернативные пути, и гугл предложил мне ознакомиться с веткой реестра:
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
. Как видим, её название говорит само за себя – она имеет столько (под)каталогов, сколько в системе зарегано удаляемых программ. Те, у которых вместо имени GUID, это доступные функциям MSI продукты Microsoft, а дальше уже идут программы с вполне дружелюбными именами. В свойствах каждого дира будем наблюдать два строковых значения: "DisplayName" и "UninstallString", что позволит нам не только создавать списки установленного софта, но и в избирательном порядке удалять любой из них:Так по волею судьбы пришлось парсить системный реестр, хоть я не отношу себя к сторонникам подобных методов: во-первых лучше доверить эту работу готовым системным функциям, а во-вторых – иногда доступ к реестру требует админских прав, что ущемляет наши собственные моральные права. Но поскольку ситуация зашла в тупик, пришлось смириться.
Функции API для работы с реестром изначально были сосредоточены в библиотеке advapi32.dll, но начиная с Vista их копии перекочевали и в Kernel32.dll. Это было сделано для удобства, чтобы наши приложения не тянули за собой ворох лишних библиотек. Конкретно в данном случае, мою проблему решили аж 7 функций, а в одной из них я обнаружил и нюанс, который изрядно потрепал мне нервы. Вот общий алгоритм программы:
1. GetSystemRegistryQuota() – в двух своих параметрах возвращает текущий, и макс.возможный размер реестра в системе (т.н. квота).
2. RegOpenKeyEx() – открывает ветку реестра "..\Uninstall", и возвращает её дескриптор. Чтобы была возможность читать значение ключей и перечислять (под)каталоги, нужно указать в параметре(4) этой функции флаги "KEY_QUERY_VALUE=1" + "KEY_ENUMERATE_SUB_KEYS=8".
3. RegQueryInfoKeyA() – передаём ей дескриптор открытой на предыдущем этапе ветки, и получаем взамен кол-во (под)каталогов в ней. Позже, это число будем использовать в качестве счётчика, для организации цикла перечисления всех программ. В доках на эту функцию, счётчик назвали "индексом" начиная с нуля, однако ничто не мешает вести отсчёт и в обратную сторону, т.е. от общего числа (под)каталогов и до нуля включительно.
4. •••• RegEnumKeyEx() – с этой функции начинается цикл! Она возвращает имя очередного (под)каталога в ветке "Uninstall", и на каждой итерации требует во-втором параметре изменения индекса на 1. Здесь нужно быть внимательней – именно с ней у меня случился конфуз! Дело в том, что
Ссылка скрыта от гостей
мы указываем линк на переменную с макс.размером буфера для строки с именем =256, а на выходе функция сбрасывает в эту-же переменную кол-во символов в текущей найденной строке. Таким образом, если внутри цикла не восстанавливать размер буфера 256, то в ней остаётся возвращённое от предыдущего вызова значение и функция может возвратить ошибку, типа "выделенный буф слишком мал". Не зря говорят, что пара часов дебага в отладчике, избавят нас от пяти минут чтения документации. Вот это как-раз мой случай..
5. lstrcat() – на данном этапе, к строке с основной веткой реестра "Uninstall" нужно добавить имя найденного (под)каталога, чтобы сформировать полный путь до папки с программой. Для этого применяем конкатенацию и "женим" две строки.
6. RegOpenKeyEx() – эту функцию мы вызываем уже повторно (см.пункт 2). Если предыдущая возвратила нам дескриптор ветки, то теперь нужно получить дескриптор (под)каталога из этой ветки, чтобы при помощи следующей функции считать в свойствах требуемое значение.
7. RegQueryValueExA() – ищет свойство по имени (в нашем случае "DisplayName"), и возвращает его значение в буфер. Не редки случаи, когда мы удалили программу, а её пустой каталог остался в реестре. Соответственно и свойства, которое мы ищем в нём не будет. Функция даёт об этом знать ошибкой "ERROR_FILE_NOT_FOUND=2", и мы должны пропускать такие каталоги.
8. •••• В хвосте цикла уменьшаем индекс на 1 для функции RegEnumKeyEx() (т.к.отсчёт у нас обратный) и если он не перевалил за нуль = FFFFFFFFh, то уходим на п.4 для обхода всего древа.
Если мы не наделаем ошибок и угомоним системного агента правильными параметрами функций, то данный алгоритм вернёт нам полный список удаляемых программ, и остаётся лишь выводить их на консоль или в форточку мастдая. При необходимости можно применить и более жёсткие меры, например снести к чертям неугодную прожку, опираясь на свойство "UninstallString". Вот как выглядят реалии:
C-подобный:
format pe console
entry start
include 'win32ax.inc'
;//-------
.data
key db 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
subKey rb 256
hKey dd 0
hsubKey dd 0
index dd 0
counter dd 1
maxReg dd 0
curReg dd 0
align 16
lpName rb 256
nameLen dd 256
buffLen dd 256
buff db 0
;---------
.code
start: invoke SetConsoleTitle,<'*** Install programm list v1.0 ***',0>
;// Квота реестра!
invoke GetSystemRegistryQuota,maxReg,curReg
shr [maxReg],20 ;//<--- в Мегабайтах
shr [curReg],20
cinvoke printf,<10,' Max registry size: %4d Mb',\
10,' Current size.....: %4d Mb',10,0>,[maxReg],[curReg]
;// Открыть ветку реестра "..\Uninstall"
invoke RegOpenKeyEx,HKEY_LOCAL_MACHINE, key,0,\
KEY_QUERY_VALUE + KEY_ENUMERATE_SUB_KEYS,\
hKey
;// Вычислить кол-во подразделов в ней (нули - типы информации)
invoke RegQueryInfoKeyA,[hKey],0,0,0,index,0,0,0,0,0,0,0
cinvoke printf,<10,' Registry Key: Uninstall. Total subKey = %d',\
10,' -------------------------------------------',10,0>,[index]
dec [index]
;//<---8<---- ЦИКЛ -----8<---------8<--------------------
@@: mov ecx,256/4 ;//<---- Очистить буфера
push ecx ecx
xor eax,eax
mov edi,lpName
rep stosd
pop ecx
mov edi,buff
rep stosd
pop ecx
mov edi,subKey
rep stosd
mov [nameLen],256 ;//<--- Внимание! Обновить размеры буферов
mov [buffLen],256
;// Получить имя очередного (под)раздела в разделе "Uninstall"
invoke RegEnumKeyEx,[hKey],[index],lpName,nameLen,0,0,0,0
mov byte[subKey],'\'
invoke lstrcat,key,lpName ;//<--- поженить две строки
;// Открыть его на чтение\энумерацию
invoke RegOpenKeyEx,HKEY_LOCAL_MACHINE,key,0,\
KEY_QUERY_VALUE + KEY_ENUMERATE_SUB_KEYS,\
hsubKey
;// Запросить в буфер нужное свойство
invoke RegQueryValueExA,[hsubKey],<'DisplayName',0>,0,0,buff,buffLen
cmp eax,2 ;//<--- ERROR_FILE_NOT_FOUND = пустой каталог
je @notFound
invoke CharToOem,buff,buff ;//<---- на случай, если встретится имя в кириллице
cinvoke printf,<10,' %03d. %s'>,[counter],buff
inc [counter] ;//<---- счётчик найденных +1
@notFound:
dec [index] ;//<---- следующий индекс..
cmp [index],-1 ;//<---- проверить его не переполнение
jnz @b ;//<---- нет: повторить.
@exit: cinvoke _getch
cinvoke exit,0
;//-------
section '.idata' import data readable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll',user32,'user32.dll'
include 'api\msvcrt.inc'
include 'api\kernel32.inc'
include 'api\user32.inc'
Обратите внимание, что функция RegQueryInfoKeyA() нашла всего 103 (под)каталога в ветке "Uninstall", и судя по логу 13 из них пустые – итого имеем 90 установленных в системе программ, где файлы приплюснутого си отнимают львиную долю. Такие дела..
3. Базовые операции с текстом
В заключении рассмотрим элементарные действия с текстом, которыми часто напрягают студентов учебных заведений. Ясно, что захватить весь текстовый периметр не получится, а потому остановимся лишь на простом алгоритме удалении лишних пробелов из строки, и смене регистра букв с прописных на заглавные и обратно.
• Если нас интересует регистр символов, то ставка здесь делается на их расположении в таблице ASCII. Заглянув в неё можно обнаружить, что разница между кодами прописных и заглавных составляет ровно 20h. Например код латиницы(А) в верхнем регистре (заглавной) равен 41h, а её-же прописной равен 61h. Аналогичный сдвиг наблюдаем и со-всеми остальными буквами, хоть в латинице, хоть в кириллице. Такой расклад наводит на мысль, что таблицу составляли отнюдь не глупые люди.
Теперь, если принять во-внимание, что 20h равно в двоичном 0010.0000, то оперируя всего одним битом(5) можно с лёгкостью менять регистр с верхнего на нижний и обратно. Для бинарных операций в ассемблере имеются свои инструкции –
OR
взводит указанный в маске бит (прописные +20h), а AND
его сбрасывает (заглавные -20h).
C-подобный:
format pe console
entry start
include 'win32ax.inc'
;//----------
.data
inpBuff rb 128
bigBuff rb 128
smallBuff rb 128
inpHndl dd 0
strLen dd 0
buff db 0
;//----------
.code
start: invoke SetConsoleTitle,<'*** Big\Small text example ***',0>
;// Запросить дескриптор ввода для ReadConsoleA()
invoke GetStdHandle,STD_INPUT_HANDLE
mov [inpHndl],eax
;// Запрос на ввод строки в буфер
cinvoke printf,<10,' String: ',0>
invoke ReadConsoleA,[inpHndl],inpBuff,128,strLen,0
;// Перевод в верхний регистр
mov ecx,[strLen] ;// длина строки\цикла
mov esi,inpBuff ;// источник
push ecx esi ;// (про запас..)
mov edi,bigBuff ;// приёмник
@@: lodsb ;// AL = очередной символ из ESI
cmp al,'A' ;// фильтр букв, отсеивая цифры и знаки
jb @fuck1 ;// ^^^^ (меньше Below)
cmp al,'z' ;// ^^^^
ja @fuck1 ;// ^^^^ (больше Above)
and al,11011111b ;// сбросить бит(5) маской
@fuck1: stosb ;// записать в приёмник EDI
loop @b ;// промотать цикл ECX-раз..
;// Перевод в нижний регистр
pop esi ecx
mov edi,smallBuff
@@: lodsb
cmp al,'A'
jb @fuck2
cmp al,'z'
ja @fuck2
or al,00100000b ;//<--- взвести бит(5)
@fuck2: stosb
loop @b
;// Результат операций
cinvoke printf,<10,' Big...: %s',\
' Small.: %s',0>,bigBuff,smallBuff
@exit: cinvoke _getch
cinvoke exit,0
;//---------------
section '.idata' import data readable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll'
include 'api\msvcrt.inc'
include 'api\kernel32.inc'
• Избавиться от лишних пробелов в строке – ещё одна часто встречающаяся задача.
В виду того-что готовой функции API для этих целей в природе не существует, всё приходится делать в ручную. Суть в том, чтобы запоминать предыдущий символ, и сравнивать его с текущим. Если оба пробелы, то пропускаем перезапись текущего в буфер, иначе всё в штатном режиме, без изменений. Вот простая как 2-копейки реализация, зато пользу от неё можно наблюдать в консоли:
C-подобный:
format pe console
entry start
include 'win32ax.inc'
;//----------
.data
inpBuff rb 128
inpHndl dd 0
strLen dd 0
buff db 0
;//----------
.code
start: invoke SetConsoleTitle,<'*** Space killer ***',0>
;// Запросить дескриптор ввода для ReadConsoleA()
invoke GetStdHandle,STD_INPUT_HANDLE
mov [inpHndl],eax
;// Запрос на ввод строки в буфер
cinvoke printf,<10,' String: ',0>
invoke ReadConsoleA,[inpHndl],inpBuff,128,strLen,0
;// Парсим строку на лишние пробелы --------------------------
mov ecx,[strLen] ;// длина строки\цикла
mov esi,inpBuff ;// источник
mov edi,esi ;// приёмник
@@: lodsb ;// AL = очередной символ
cmp al,' ' ;// это пробел?
jne @miss ;// нет: пропускаем
cmp ax,' ' ;// да: тест с предыдущим
je @next ;// 2 пробела - пропускаем
@miss: stosb ;// перезапись символа
@next: xchg ah,al ;// запомним текущий символ
loop @b ;// мотаем цикл по длине ЕСХ..
mov byte[edi],0 ;// вставить маркер конца стоки
;//-----------------------------------------------------------
;// Результат
cinvoke printf,<10,' Result: %s',0>,inpBuff
@exit: cinvoke _getch
cinvoke exit,0
;//---------------
section '.idata' import data readable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll'
include 'api\msvcrt.inc'
include 'api\kernel32.inc'
4. Заключение.
Мелочи подобного рода сильно отравляют жизнь начинающим асматикам, а так.. (на случай, если грянет гром) "зонт" у нас уже имеется. В скрепке можно найти исполняемые файлы для тестов. Надеюсь ещё встретимся в сообществе Codeby, всем удачи и пока.