"Совершите вы массу открытий, иногда не желая того..." Не помню автора, но пел Андрей Миронов. Предисловие. Идея написать компилятор для Win32 родилась при моем первом знакомстве с компилятором Sphinx C-- Питера Селлика (Peter Cellik) в 1996 году, но реализовать ее удалось лишь недавно, когда появилась возможность через Интернет найти исходные тексты для C--, поскольку начинать с нуля желания не было. Итак, коротко о главном. Основной принцип построения приложений для 32-разрядной системы, предлагаемый в различных компиляторах, заключается в разделении кода на несколько секций: .text, .data, .idata, .reloc, .rsrc и т.д., которые строятся из самих исходных текстов и различных файлов описаний и ресурсов на стадии сборки программой-линковщиком (link). По сути дела осталась старая схема построения: compiler + linker -> exe-file. Кроме того, большой набор различных библиотек... которые к тому же существенно меняются от версии к версии, что совсем не радует. И если вы переходите на новую, более продвинутую версию компилятора, то нет никакой гарантии, что вы сможете на нем перекомпилировать свои старые программы - примером служит DDK от MicroSoft. Sphinx C-- изначально был задуман и реализован для создания компактных по размеру COM-файлов и поэтому вполне подходит для генерации 32-разрядных приложения в модели плоской (flat) памяти. Помимо этого большим плюсом является идея открытого кода для всех вызываемых в программе библиотечных функций, реализованная посредством *.h-- файлов. Еще одна идея из компилятора Sphinx C--, заслуживающая внимания, - это динамические процедуры, т.е. процедуры, которые могут быть помещены в тело компилируемой программы только тогда, когда к ним есть обращение. Таким образом, в процессе написания программы с использованием динамических процедур гарантируется включение в тело программы только того кода, который действительно необходим. В процессе работы по переводу компилятора Sphinx C-- на платформу Win32 произведены весьма существенные изменения как в самом компиляторе, так и в идеях, на которых он реализован. Я не буду здесь выделять все отличия настоящего компилятора от его прародителя по той простой причине, что те, кто с ним знаком, их смогут увидеть сами, а тем, кто не имел дела с С--, нет нужды вникать в эти детали. Отмечу, однако, что принцип генерации кода, использовавшийся в С--, заменен на принцип, используемый в макроассемблере TMA Свена Клозе (TMA macro assembler Sven Klose), с проведением его адаптации к нуждам С--. В результате появился данный компилятор, целью которого является заполнение ниши между языком ассемблера и языком С. Описание языка. Идентификаторы. Идентификаторы должны начинаться с буквы или знака подчеркивания, если являются глобальными, или же должны начинаться с символа @, если они локальные. Далее может идти любая комбинация цифр, букв или знаков подчеркивания длиной не более 63 символов. Буквы могут быть как из латинского, так и из национального алфавита. Несколько примеров: GoHome _1234 ПримерИдентификатора @LocalLabel2 Все идентификаторы, кроме зарезервированных, являются чувствительными к регистру, т.е. идентификаторы: ToDo и Todo компилятор воспримет как разные. Зарезервированные идентификаторы Ниже приводится список зарезервированных идентификаторов языка, которые не могут быть использованы при создании программы иначе, как это определено в языке: byte char word short dword int if else cycle do while return docase case continue break extract from enum struct carryflag overflow zeroflag notcarryflag notoverflow notzeroflag а также названия регистров и мнемоники команд ассемблера. Еще раз подчеркну, что зарезервироанные слова не зависят от регистра, т.е. Enum и eNuM компилятор воспримет в любом из вариантов, как начало списка нумерованных констант. Константы Числовые константы могут быть заданы в одной из четырех систем счисления: десятичной, шестнадцатиричной, восмеричной или двоичной. Десятичные числовые константы задаются обычным образом: 444 или 007. Шестнадцатиричное представление констант начинается с комбинации 0x: 0xFFF или 0x088. Восмеричное представление констант начинается с комбинации 0o: 0o777 или 0o10. Двоичное представление констант начинается с комбинации 0b: 0b111101 или 0b11101010. Символы в константах заключаются в одиночные кавычки ('). Так же как и в С символы могут заданы с помощью указания их кода после символа '\'. Список спецсимволов: '\a', '\b', '\f', '\l', '\n', '\r', '\t' - для форматирования вывода '\x??' или '\???' - ASCII-символ. Код символа задан либо в 16, 10 значением. Любые символы после '\' просто принимаются компилятором, таким образом одиночную кавычку можно указать как '\''. Строковые константы задаются путем заключения их в двойные кавычки ("). Внутри строковой константы допускается указание спецсимволов в одиночных кавычках. Примеры строковых констант: "Пример строковой константы\n" "Need '\"'word'\"' in data declaration" -> Need "word" in data declaration При определении числовых и символьных констант допускается использование выражений, которые вычисляются в процессе компиляции. Значением константы будет результат вычисления выражения: 1*2*3/2+4 даст значение константы 7. Вычисления проводятся слева-направо без учета приоритета операций. Стандартные типы данных Имеется шесть стандартных типов данных: byte, char, word, short, dword и int. В таблице приведены размер и диапазон значений для каждого типа: ------------------------------------------------------------------- Тип | Размер | Диапазон значений переменной | в байтах | (десятичн.) | (hex) ------------------------------------------------------------------- byte | 1 | 0...255 | 0...0xFF char | 1 | -127...127 | 0x80...0x7F word | 2 | 0...65535 | 0...0xFFFF short | 2 | -32768...32767 | 0x8000...0x7FFF dword | 4 | 0...4294967295 | 0...0xFFFFFFFF int | 4 | -2147483648... | 0x80000000 ... | | 2147483647 | 0x7FFFFFFF ------------------------------------------------------------------- Глобальные переменные Объявление переменных имеет обычный для С синтаксис: <тип> <список идентификаторов>; Список идентификаторов состоит из одного или более идентификаторов, разделенных запятыми. В списке также могут присутсвовать одномерные массивы, объявляемые в виде: <идентификатор>[<размерность>]. Примеры объявлений переменных: dword i,j; // i и j типа dword byte Tab='\t'; // переменная Tab типа byte с начальным значением char string[]="This is a string\n"; int z,b[6]; // z типа int и массив целых - b Выражения Выражения состоят из левой и правой частей, разделенных либо операцией присваивания, либо операцией сравнения. Левая часть выражения может быть либо переменной, либо регистром. В правой части выражения может находиться произвольное количество переменных, функций, констант, скобок и знаков операций. Ниже приводится таблица всех допустимых операций: ------------------------------------------------------------------------- Операция | Значение | Пример ------------------------------------------------------------------------- = | присвоить или | edi = 33; | проверить равенство | while(ch = 'a') + | сложить | eax = Count + 5; - | вычесть | Count = eax - edi; * | умножить | x = y * 3; / | разделить | y = ecx / x; % | остаток деления | y = edi / 7; & | логическое AND | a = B & c; | | логическое OR | a = B | c; ^ | логическое XOR | a = B ^ c; << | сдвиг бит влево | x = y << 5; >> | сдвиг бит вправо | x = y >> 6; += | увеличить на | a += 6; // a = a + 6; -= | уменьшить на | a -= 5; // a = a - 5; &= | побитный AND | a &= 0xF; // a = a & 0xF; |= | побитный OR | a |= 0o77; // a = a | 0o77; ^= | побитный XOR | a ^= 0b1101; // a = a ^ 0b1101; <<= | сдвиг бит влево | a <<= 7; // a = a << 7; >>= | сдвиг бит вправо | a >>= 3; // a = a >> 3; >< | обмен(swap) | x >< y; // temp=y; y=x; x=temp; == | проверить равенство | if( x=='7' ) // для тех, кому так привычнее > | больше чем | case( x > y ) < | меньше чем | if( a < 0 ) >= | больше или равно | while(( b >= a ) & ( x >= ( y - 7 ))) <= | меньше или равно | if( y <= ( a + b - 30 )) != или <>| не равно | case( a != b) или же case( a <> b) # | адрес переменной | esi = #Count; // esi=адрес переменной Count Функции Объявление функций имеет вид: <тип> <идентификатор>(<список параметров>) Список параметров задает типы и имена формальных параметров, используемых при вызове функции. Компилятор не осуществляет проверку соответствия списка формальных параметров функции фактическим, поэтому следует внимательно следить за корректностью вызова функций. Тип возвращаемого из функции значения можно не указывать. В этом случае по умолчанию считается, что функция возвращает значение типа dword. Значение помещается при возврате из функции в регистр eax для типов dword и int, в регистр ax для типов word и short и в регистр al для типов byte и char. В списке параметров для каждого параметра указывается его тип. Параметры одного типа, идущие подряд, разделяются запятыми. Формальные параметры разного типа в объявлении функции разделяются символом ;. Если тип параметра не задан, то считается, что параметр имеет тип dword. Не зависимо от типа параметра при вызове функции для каждого параметра выделяется 4 байта (dword). Это связано с тем, что в Win32 стек всегда должен иметь выравнивание (alignment) на границу двойного слова. Примеры объявления функций и их вызовов. Объявление Пример вызова char ToUpper(char ch) upChar = ToUpper('i'); MergeStrings(dword dest,str1,str2) MergeStrings(#str,"One","+Two"); получим str="One+Two" Convert(dword str; int number,base) Convert(#num, -567, 16); При вызове функции первый параметр помещается в стек последним. Пример: WriteFile(handle,"Hello World!\n",14,#dWriteFileCount,0); будет реализован: push 0 push #dWriteFileCount push 14 push #"Hello World!" push handle call WriteFile При возврате из функции стек очищается от параметров командой: ret number. Все объявляемые в программе функции являются динамическими. Это значит, что код функции вставляется в программу лишь только в случае обращения к ней. То же самое относится и к любым глобальным переменным в программе. Структурные операторы Применение структурных операторов в программе делает ее более удобной для чтения и анализа. Кроме того, написать несколько структурных операторов проще, чем путаться в похожих именах большого числа меток и мучиться, придумывая уникальное имя для каждой новой метки. В то же время не запрещается использовать и метки в любом месте программы. Хорошо поставленная метка способна здорово облегчить написание и анализ программы. Оператор if. В общем виде условный оператор можно записать так: if(<условие>) <группа операторов1> else <группа операторов2> Алгоритм выполнения условного оператора состоит в следующем: проверяется <условие>, и если оно истинно, то выполняется <группа операторов1>, следующая за if, после чего управление передается за конец условного оператора. Если условие ложно, то управление передается на <группу операторов2>, следующую за else. Порядок выполнения условного оператора в случае отсутствия else очевиден. В качестве группы операторов может быть либо один оператор, либо блок из нескольких операторов в {} скобках. Вот несколько примеров: if(edx<=2){ WriteStr("Equal or less two\n"); return(); } else{ WriteStr("Greater than two\n"); return(0); } if((x<>0)&(y<>0)) return(x/y); Оператор cycle Оператор цикла cycle имеет вид: cycle(<счетчик>) <группа операторов> Цикл выполняется до тех пор пока значение счетчика не будет равно нулю. Проверка счетчика на равенство нулю и его уменьшение на единицу производится в конце группы операторов цикла. Допускается внутри цикла использовать и менять значение счетчика. Если счетчик не указан, то цикл будет бесконечным. Пример: #import "kernel32.dll" #import "user32.dll" dword Count; dword dWriteFileCount; dword handle; byte s[20]=0; main(){ handle=GetStdHandle(-11); Count=4; cycle(Count){ if(Count=2) Count--; wsprintfA(#s,"Count=%d\n",Count); ESP+=12; WriteFile(handle,#s,lstrlenA(#s),#dWriteFileCount,0); } } При выполнении будет выведено: Count=4 Count=3 Count=1 Оператор while Оператор цикла while имеет вид: while(<условие>) <группа операторов> Группа операторов в цикле while выполняется пока <условие> остается истинным. Пример из описания оператора cycle может быть реализован с помощью while следующим образом: Count=4; while(Count){ if(Count=2) Count--; wsprintfA(#s,"Count=%d\n",Count); ESP+=12; WriteFile(handle,#s,lstrlenA(#s),#dWriteFileCount,0); Count--; } } Оператор do ... while Оператор цикла do ... while имеет вид: do <группа операторов> while(<условие>) Группа операторов в цикле do ... while выполняется пока <условие> остается истинным. Пример из описания оператора cycle может быть реализован с помощью do ... while следующим образом: Count=4; do{ if(Count=2) Count--; wsprintfA(#s,"Count=%d\n",Count); ESP+=12; WriteFile(handle,#s,lstrlenA(#s),#dWriteFileCount,0); Count--; } while(Count) } Особенностью оператора do ... while является то, что <группа операторов> в цикле всегда выполняется не менее одного раза. Оператор docase Оператор ветвления docase имеет вид: docase <группа операторов 0> case(<условие1>) <группа операторов 1> ... case(<условиеN>) <группа операторов N> default <группа операторов N1> Оператор docase позволяет заменить вложенные группы из if ... else if ... else ... . Кроме того далее будет показана на примере универсальность этого оператора. Пример из описания оператора cycle может быть реализован с помощью docase следующим образом: Count=4; docase{ if(Count=2) Count--; wsprintfA(#s,"Count=%d\n",Count); ESP+=12; WriteFile(handle,#s,lstrlenA(#s),#dWriteFileCount,0); Count--; case(Count=0) break; default continue; } } Операторы continue и break Эти операторы используются внутри выше описанных операторов цикла cycle, while, do...while и операторе docase для перехода на начало цикла или docase по оператору continue и на выход за конец оператора по break. Пример: while(cond1){<--╘ ... | if(cond2) | continue; --+ ... | if(cond3) | break; ---+ | ... | | } ----------|-+ <----------+ Оператор enum Назначение оператора заключается в создании групп нумерованных констант. Пример: enum { ab, ac=2, ad, ae=6}; при этом будет: ab=0, ac=2, ad=3, ae=6. Оператор struc Служит для описания структурированных данных, аналогично С. Пока не реализован. Можно, используя enum, без всяких проблем работать с данными любой структуры. Пример: Для использования структуры: struct localrec{ struct localrec *next; char localid[IDLENGTH]; int localtok; int localnumber; }; создадим: // ---- Структура localrec - описание локальной переменной enum{ localnext=0, // Указатель на следующую localrec localid=4, // Имя локальной переменной localtok=localid+IDLENGTH, // Значение token localtype=localtok+4, // тип переменной localnumber=localtype+4, // Позиция в стеке local_size=localnumber+4}; // Размер структуры И теперь в программе можно использовать нумерованные константы для обращения к элементам структуры localrec: // ---- Добавить локальную переменную в список AddLocalvar(dword str,tk,ltype,num) dword ptr,newptr; { newptr=LocalAlloc(0x40, local_size); if(newptr==NULL){ preerror("Compiler out of memory for local symbol linked list"); ExitProcess(e_outofmemory); } if(locallist==NULL) locallist = newptr; else{ ptr = locallist; EBX=ptr; docase{ EAX=[EBX+localnext]; case(EAX!=0){ EBX=EAX; continue; } } [EBX+localnext]=newptr; } EBX=newptr; lstrcpyA(EBX+localid, str); EBX=newptr; [EBX+localtok] = tk; [EBX+localtype] = ltype; [EBX+localnumber] = num; [EBX+localnext] = NULL; localptr = EBX; } Метки Требования к именам меток те же, что и к идентификаторам. Исключением являются локальные метки - они должны начинаться с символа '@'. Локальные метки доступны только в пределах той функции, в которой они определены, а глобальные - по всей программе. При создании имен локальных меток следует учитывать, что компилятор при реализации структурных операторов генерирует метки вида: @l<число>. Чтобы избежать коллизий, следует для своих меток не применять такого вида. Любая метка завершается символом двоеточия (:). Примеры меток: NotUpperCase: // это глобальная метка @NotUpperCase: // а это локальная метка Индексация массивов Элементы массива любого типа индексируются в байтовых единицах, независимо от типа данных. Это ВСЕГДА следует помнить при работе. Индексы имеют вид, принятый в ассемблере для 386 CPU: <переменная>[<базовый регистр>+<масштаб>*<индексный регистр>+<индекс>] Вот несколько примеров: Str[7]; // седьмой байт из массива Str IntArray[4*ebx]; // ebx элемент из массива целых - IntArray ByteArray[esi+8*ecx+300]; Специальные условные выражения Имеется шесть специальных условных выражений: CarryFlag, NotCarryFlag, Overflow, NotOverflow, ZeroFlag, NotZeroFlag. Они служат для генерации кода проверяющего состояние флагов CPU. Комментарии Комментарии задаются аналогично С. Директивы компилятора Все директивы компилятора начинаются с символа '#'. Список директив и их назначение приводятся ниже: #debug // указывает компилятору на необходимость генерации // отладочной информации для компилируемой программы #define // определить константу или идентификатор. Пример: // #define MAXLINES 400 // #define less < // #define SetTrue "eax=1" // if(lines less MAXLINES) --> if(lines<400) // SetTrue; --> eax=1; #dll // указывает компилятору на генерацию DLL-файла. Обычно - exe. #include // подключение файла с исходным текстом. Аналогично С. #import // импорт функций из DLL-файла по имени. #importN // импорт функций из DLL-файла по номеру. Пример смотрите в // описании структурных операторов cycle, while и т.д. #list // указывает компилятору на генерацию файла с листингом (.lst) #map // указывает компилятору на генерацию map-файла (.map) Встроенный ассемблер Ассемблер поддерживает большую часть инструкций из набора 386, 486 и 586 процессоров. Мнемоники ассемблера могут быть помещены внутри тела любой функции без каких-либо ограничений. Пример: // Выделение слова в Dest из строки символов Source dword GetWord(dword Source,Dest){ push esi; push edi; esi=Source; edi=Dest; // Ищем первый непустой символ @up: lodsb; cmp al,' '; jz @up; // Пробел cmp al,0; jz @down // Конец строки Source // Копируем слово в Dest @up1: stosb; cmp al,0; jz @down; // Конец строки Source lodsb; cmp al,' '; jnz @up1; // Не пробел al=0; jmp @up1 // Отметим конец слова @down: // Слово выделено и скопировано в Dest eax=esi-Source; eax--; // Вычислим длину слова pop edi; pop esi // Восстановим esi и edi } Заключение Не все из вышеописанного реализовано. Это обусловлено в первую очередь тем, что данная версия является предварительной и основной ее целью является выявление интереса компьютерной общественности к такому компилятору. Если Вы заинтересовались этим продуктом или у Вас возникли какие-либо вопросы, идеи или предложения, то прошу о них сообщить мне по адресу: halimovskiy@usa.net. С уважением А.Халимовский E-mail: halimovskiy@usa.net