Win32ASM: Форматы данных от лукавого — Архив WASM.RU
#1. Как известно, объем жидкости измеряется в см3 ;). Но когда мы покупаем, например, пиво, то измеряем его вовсе не в кубических сантиметрах, а в совершенно других единицах измерения: бутылках, банках, ящиках, канистрах, бокалах или, на худой конец, в литрах. Точно так и информация: с одной стороны, она, конечно же, измеряется битами, байтами, килобайтами и и т.д., но с другой стороны - существуют и её элементарные "потребительские куски". Как пиво мы пьем глотками, так и процессор потребляет ее своими небольшими "глоточками" ;).
Ранее мы уже разбирались что такое регистры и какими кусками они могут держать в себе информацию. Однако со времен процессора 8086 прошло очень много времени, и регистры немного подросли. Поэтому мы для начала познакомимся с современным вариантом "нездоровой" схемки из главы 1.2 #2 (Ведение в машинный код), и только потом поговорим о данных.
Типичный регистр общего назначения выглядит следующим образом:
EAX AX AH AL
Что в нем изменилось? Да просто его (регистра) стало в два раза больше и максимальное число, которое мы можем в этот регистр записать - это FFFFFFFFh (4294967295d, 11111111111111111111111111111111b). Однако обратите внимание (см. рисунок), что EAX - это все лишь расширенная версия регистра AX. То есть мы не можем обратиться к "старшей половине" EAX, как мы делали это, например, при обращении к частям AX через "половинки" AH|AL. Другими словами, если mov AX,1234h и парочка mov AH,12h ; mov AL,34h - это один чёрт, то про mov EAX,12345678h ничего похожего мы сказать не можем.
"Неделимые" регистры SI, DI, SP и BP также имеют свои расширенные версии (ESI, EDI, ESP и EBP), а вот сегментные регистры CS, SS, DS, ES, GS и FS сия участь минула, так как парочек наподобие ES:EBX (сегмент:смещение) вполне достаточно и сейчас. Заметим: сегментные регистры по-прежднему задействованы во всех инструкциях доступа к памяти (например mov ax,[var] всегда идёт через DS, а call [func-var] использует оба DS и CS), однако при программировании под Win32 об этом обычно можно не беспокоиться.Повторюсь опять и опять - если простеньким прожкам под Win32 сегментные регистры явно использовать нет надобности, то это вовсе не значит, что они перестали использоваться процессором при адресации данных в "сегменте данных", "на стеке" и в строковых инструкциях. Не говоря уже про явное использование сегментных регистров в сУрьёзных программах ;)
На первых порах мы будем "замалчивать" существование сегментных регистров, но знайте: утверждение об их "устарелости" есмъ брехня.Однако, вернемся к нашим баранам ;)
#2. Для описания простейших типов данных на языке ассемблера используются так называемые директивы резервирования и инициализации данных, которые по сути являются указаниями транслятору на выделение определённого объёма памяти.
Например, мы неоднократно использовали директиву DB (define byte), которая инициализировала столько байт, сколько нашей душеньке было угодно. Например, DB 'Hello, World' "резервировала и инициализировала" столько байт, сколько символов нам вздумалось написать между кавычками или апострофами.
Однако помимо DB существует еще куча подобных директив. Я приведу вам их все. Некоторые из них вас ужаснут, однако расслабьтесь - вовсе не обязательно что вы будете все их использовать ;). Но ознакомиться с ними - крайне желательно желательно ;) Подозреваю, что впоследствии мы вернемся к этой главе еще не раз.
Для начала мы "построим" эти директивы согласно размера памяти, который они резервируют. А заодно с трех сторон посмотрим на диапазон значений, которые эти "элементы данных" могут принимать.
Расшифровывается эта табличка следующим образом: первая колонка - это сама инструкция. Первая буква - D - от буржуйского define, то есть "определить", вторая - B, W, D и т. д. - от размера определяемого элемента данных, то есть byte (байт), word (слово), doubleword (двойное слово), и т. д. Третья колонка - размер в байтах. Четвертая - диапазон принимаемых значений в шестнадцатеричной системе счисления (хе... я понимаю, что последние две строчки несколько длинноваты ;), однако вроде никто еще не придумал писать шестнадцатеричные числа в экспоненциальном виде). Пятая колонка - это соответствующий диапазон десятичных беззнаковых чисел, шестой - знаковых десятичных.
DB байт,
byte1 0...FFh 0...255 -128...+127 DW слово,
word2 0...FFFFh 0...65 535 -32768...+32767 DD двойное слово,
double word4 0...FFFFFFFFh 0...4294967295 -2147483 648...+2147483647 DF указатель дальнего слова,
far word6 0...FFFFFFFFFFFFh 0...248-1 -247...+247-1 DQ учетверенное слово,
quadword8 0...FFFFFFFFFFFFFFFFh 0...264-1 -263...263-1 DT 10 байт,
ten bytes10 0...FFFFFFFFFFFFFFFFFFFFh 0...280-1 -279...279-1
Во-первых, обратите внимание на DF - это как раз случай длинного сегментированного указателя (32 бит смещения + 16 бит сегмента). И для него, кстати, есть синоним: DP. Во-вторых, помимо целых чисел как аргументы можно указывать нецелые. DD может использоваться для описания и хранения коротких (single-precision) нецелых чисел +/-10^-38...10^38, DQ для длинных (long, double) нецелых +/-10^-308...10^308, а DT для "временных" (temporary) нецелых (диапазон не помню). Например:
Код (Text):
dd 3.141 dq 1.2345678987654321 dt 0.0000000001Вообще, если быть совсем точным, то помимо интерпретации значений в соответствующих ячейках как целых, они могут интерпретироваться следующим образом: DB - байт или знак; DW - int/unsigned или 16-битное смещение; DD - long, float, 16-битный far (сегмент:смещение) или 32-битный близкий (смещение) указатель; DF - 32-битный far (сегмент:смещение) указатель; DQ - double; DT - упакованное десятичное (packed BCD) число длинной 20 цифр или long double (оба - формат FPU).
Вот еще одна интересная табличка. Все таки у нас программирование для дZенствующих ;), а это подразумевает в первую очередь рассмотрение любого "объекта" с нескольких точек зрения. Первая колонка - это директива, а вторая - так называемое адресное выражение:
DW 16-битный адрес сегмента; смещение в 16-битном сегменте DD 16-битный адрес + 16-битное смещение; 32-битный адрес DF 16-битный адрес сегмента + 32-битное смещение
В принципе, все выше перечисленные директивы можно использовать для "задавания" строкового значения. Однако имейте в виду, что в памяти они могут выглядеть совсем не так, как вы задали их в директиве, поэтому для инициализации строк мы всегда будем использовать DB. Следующая же простынка приводится исключительно в "образовательных" целях и ни в коем случае не следует воспринимать ее как руководство к действию ;).
Код (Text):
String_1 DB 'A' String_2 DW 'As' String_3 DD 'Asse' String_4 DF 'Assemb' String_5 DQ 'Assemble' String_6 DT 'Assembler ' ;после r - пробел String_7 DB 'Assembler is ruleZ forever!'Четко вы должны усвоить одно: существует только 6 типов переменных - байт, слово, двойное слово, "дальнее слово" (давайте будем именно так называть "дальний указатель" в формате сегмент : 32-битное смещение), учетверенное слово и "тенбайт" ("10 байт"). Все остальное - от лукавого ;).
И, наконец, третья табличка. В тех случаях когда мы захотим инициализировать локальную переменную, существующую только в пределах какой-либо функции и умирающую, как только функция свое "отработает", мы будем использовать несколько другие директивы. Вот табличка их "соответствия":
DB BYTE/SBYTE DW WORD/SWORD DD DWORD/SDWORD DF FWORD DQ QWORD DT TBYTE
Два варианта "соответствия" через слэш - это, беззнаковое (unsigned) и знаковое (signed) соответственно.
#3. Как я уже говорил в прошлом выпуске, вся документация, которую добренький микрософт задарма предоставляет своим девелоперам, заточена под си. А си - это, да будет вам известно, ;) все же язык программирования, претендующий на высокоуровневость. И те же 32 бита двойного слова могут быть обозваны и как указатель на строку символов (LPCSTR), и как результат, возвращаемый функцией (LRESULT), и даже, страшно подумать, цветом (COLORREF).
Вам это нравится? Мне тоже - нет. Однако, если мы хотим программировать приложения под win32, нам нужно научиться использовать функции API. Информация же об этих функциях находится в си-заточенном MSDN'е. Что ж делать? Будем учиться перезатачивать!
Загрузите свой MSDN и поищите там, например, функцию WriteConsole (в закладке Index). Смотрим на страничку в описании:
Функция WriteConsole записывает в строку символов буфер экрана консоли (console screen buffer), начиная с текущей позиции курсора.Код (Text):
BOOL WriteConsole( HANDLE hConsoleOutput // хэндл буфера экрана CONST *lpBuffer VOID // буфер для записи DWORD nNumberOfCharsToWrite // число симоволов для записи LPDWORD lpNumberOfCharsWritten // число записаных символов (указатель) LPVOID lpReserved // резерв );Слово BOOL перед нашей функцией означает, что результат, который она нам вернет через регистр EAX (все апишные функции возвращают результат через этот регистр) - булевский.
Читаем описание. Возвращаемое значение не-ноль, если функция выполнилась успешно и 0, если сглючила. А дальше, в скобках, какие-то непонятные парочки слов наподобие HANDLE hConsoleOutput... Как их прикажете понимать?
Первое слово из "сладкой парочки" - это тип переменной, а второе - это её "имя собственное". Извлечь необходимую нам информацию мы можем как из первого, так и из второго слова ;). Первое слово - это тип переменной, которых в сях целый вагон с маленькой тележкой, расгрузить его попробуйте самостоятельно, обратившись к разделу Platform SDK MSDN'а, к топику Data Types. Второе же слово - это "имя собственное" переменной, однако имя хитрое - с префиксом, о котором мы с той или иной степенью вероятности может судить о типе этой переменной.Это не потому, что правила языка таковы, это потому, что в мелкософте решили все кишки переменной описывать в её имени, причём как префикс, а не как суффикс - это чтобы жизнь мёдом не казалась при работе с сорсами от мелкософта; называется сия гадость "венгерская нотация".Расшифровываю:
b байт BYTE w слово WORD dw двойное слово DWORD h хэндл DWORD lp указатель DWORD n "количество" DWORD
Это - небольшой кусочек из так называемой венгерской нотации, которой мелкософт старается следовать. Суть её состоит в том, что имя переменной должно быть не просто идентификатором, но еще и нести в себе некоторые целевые характеристики. Притом не обязательно "размерные", но и "функциональные", с которыми вы можете ознакомиться в MSDN'овском топике "Hungarian Notation"
#4. Теперь, когда мы краешком глаза взглянули на многообразие типов данных великого и претендующего на сильную могучесть языка Си, давайте попробуем перевести топик про WriteConsole на язык Эллочки-людоедки, то бишь ассемблерный.
hConsoleOutput. Префикс h, значит (смотрим на табличку) - DWORD.
CONST *lpBuffer VOID. CONST и VOID, конечно же, смущают, но в имени переменной есть префикс lp, значит - DWORD. DWORD nNumberOfCharsToWrite. Префикс n, значит DWORD (хотя, собсно, при чём тут префикс? Про DWORD же прямым текстом написано!)
И так далее...
Остается открытым вопрос, чтож это за "указатель" за такой. По большому секрету скажу, что это самый обыкновенный адрес. А вот хэндл... брррр... в общем, это индекс в массиве данных, который позволяет таскать вместо всей структуры данных только её относительно короткий "идентификатор". Cама же структура хранится, разумеется, где-то в глубинах системы, что к тому же позволяет обеспечить её целостность несмотря на неосторожные или агрессивные действия со стороны программы.
Короче, хэндл - тот же адрес, только адрес является индексом памяти (массива байт), а хендл - индексом в некотором массиве, который лежит в этой памяти. Попробуйте это представить. Получилось?
На этой оптимистической ноте ;) переходим к следующей части - написанию "Неllo, World" под Win32. © Serrgio / HI-TECH
Win32ASM: Форматы данных от лукавого
Дата публикации 19 авг 2002