Marylin
20.09.2021, 00:22
Hello All!
Делиться со-всякими алго и наработками уже входит у меня в привычку, и в продолжении цикла этих заметок предлагаю разбор всякого хлама из старого сундука. На повестке дня сегодня следующее:
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.
https://forum.antichat.xyz/attachments/4911833/img_eaed329d05.png
Теперь рассмотрим ситуацию, когда декодер обнаруживает в 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
,
2222
h
,
3333
h
,
4444
h
,
5555
h
;
//
@exit
:
cinvoke _getch
cinvoke exit
,
0
;
//-------
proc foo a1
,
a2
,
a3
,
a4
,
a5
;
//
@exit
:
cinvoke _getch
cinvoke exit
,
0
;
//-------
proc foo a1
,
a2
,
a3
,
a4
,
a5
;
//
;
// Квота реестра!
invoke GetSystemRegistryQuota
,
maxReg
,
curReg
shr
[
maxReg
]
,
20
;
//
,
[
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
,
,
[
index
]
dec
[
index
]
;
//
,
0
,
0
,
buff
,
buffLen
cmp eax
,
2
;
//
,
[
counter
]
,
buff
inc
[
counter
]
;
//
;
// Запросить дескриптор ввода для ReadConsoleA()
invoke GetStdHandle
,
STD_INPUT_HANDLE
mov
[
inpHndl
]
,
eax
;
// Запрос на ввод строки в буфер
cinvoke printf
,
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
,
11011111
b
;
// сбросить бит(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
,
00100000
b
;
//
,
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'
https://forum.antichat.xyz/attachments/4911833/img_2fe9bda943.png
• Избавиться от лишних пробелов в строке – ещё одна часто встречающаяся задача.
В виду того-что готовой функции 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
,
;
// Запросить дескриптор ввода для ReadConsoleA()
invoke GetStdHandle
,
STD_INPUT_HANDLE
mov
[
inpHndl
]
,
eax
;
// Запрос на ввод строки в буфер
cinvoke printf
,
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
,
,
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'
https://forum.antichat.xyz/attachments/4911833/img_c8a3883055.png
4. Заключение.
Мелочи подобного рода сильно отравляют жизнь начинающим асматикам, а так.. (на случай, если грянет гром) "зонт" у нас уже имеется. В скрепке можно найти исполняемые файлы для тестов. Надеюсь ещё встретимся в сообществе античат , всем удачи и пока.
Делиться со-всякими алго и наработками уже входит у меня в привычку, и в продолжении цикла этих заметок предлагаю разбор всякого хлама из старого сундука. На повестке дня сегодня следующее:
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.
https://forum.antichat.xyz/attachments/4911833/img_eaed329d05.png
Теперь рассмотрим ситуацию, когда декодер обнаруживает в 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
,
2222
h
,
3333
h
,
4444
h
,
5555
h
;
//
@exit
:
cinvoke _getch
cinvoke exit
,
0
;
//-------
proc foo a1
,
a2
,
a3
,
a4
,
a5
;
//
@exit
:
cinvoke _getch
cinvoke exit
,
0
;
//-------
proc foo a1
,
a2
,
a3
,
a4
,
a5
;
//
;
// Квота реестра!
invoke GetSystemRegistryQuota
,
maxReg
,
curReg
shr
[
maxReg
]
,
20
;
//
,
[
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
,
,
[
index
]
dec
[
index
]
;
//
,
0
,
0
,
buff
,
buffLen
cmp eax
,
2
;
//
,
[
counter
]
,
buff
inc
[
counter
]
;
//
;
// Запросить дескриптор ввода для ReadConsoleA()
invoke GetStdHandle
,
STD_INPUT_HANDLE
mov
[
inpHndl
]
,
eax
;
// Запрос на ввод строки в буфер
cinvoke printf
,
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
,
11011111
b
;
// сбросить бит(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
,
00100000
b
;
//
,
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'
https://forum.antichat.xyz/attachments/4911833/img_2fe9bda943.png
• Избавиться от лишних пробелов в строке – ещё одна часто встречающаяся задача.
В виду того-что готовой функции 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
,
;
// Запросить дескриптор ввода для ReadConsoleA()
invoke GetStdHandle
,
STD_INPUT_HANDLE
mov
[
inpHndl
]
,
eax
;
// Запрос на ввод строки в буфер
cinvoke printf
,
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
,
,
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'
https://forum.antichat.xyz/attachments/4911833/img_c8a3883055.png
4. Заключение.
Мелочи подобного рода сильно отравляют жизнь начинающим асматикам, а так.. (на случай, если грянет гром) "зонт" у нас уже имеется. В скрепке можно найти исполняемые файлы для тестов. Надеюсь ещё встретимся в сообществе античат , всем удачи и пока.