Files
Sprinter-SDCC/third_party/solid-c/DOC/CC.RUS
T
snark13 c71e249a4e Add full compiler toolchain, libc, examples and reference docs
First substantive commit: the entire Sprinter C compiler tree on top of
the bare README+gitignore initial commit.

What's in here:
  bin/sprinter-cc        — driver script invoking SDCC + linker + mkexe
  libc/                  — Sprinter-specific libc layer over ESTEX/BIOS
                           (conio, gfx, io, mem, stdio + headers)
  runtime/               — crt0 variants (default/small/banked/minimal)
                           + heap + bank trampolines
  toolchain/             — mkexe (SprintEXE packer, C + tests)
  examples/              — 30 demo programs (gfx, file I/O, env, time, …)
  lib/Makefile           — builds the libc archive (sprinter.lib)
  docs/                  — converted Sprinter manuals + asm reference samples
  third_party/           — solid-c reference compiler dump + sdcc setup script
  release_docs/          — packaging / release notes

gitignore overhaul:
  • Drop dangerous blanket patterns: *.asm (would hide docs/samples/*.asm)
    and *.exe (case-insensitive match was hiding third_party/solid-c/*.EXE
    on macOS APFS).  Replaced with examples/*/*.{asm,exe,…} and lib/*.lib.
  • Restore tracking of toolchain/mkexe/tests/{one,big}.bin — those are
    INPUT fixtures, not build outputs.
  • Collapse the duplicated SDCC/C/Sdcc sections into one section per
    concern (build outputs / vendored / OS-junk).
  • Add .sprinter-cc-*/, build/ (catches lib/build/ too), .claude/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 16:13:21 +03:00

969 lines
66 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Компилятор языка Си в системе ESTEX
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ВВЕДЕНИЕ
Одной из целей разработки оптимизирующего компилятора SOLID C было создание
инструментального средства, которое позволяет писать и саму операционную сис-
тему, и программное обеспечение для нее. Для достижения этой цели в кодогене-
ратор введены различные дополнительные средства. Так, разработаны средства
автоматического распределения регистров, благодаря которым удалось существенно
повысить эффективность объектного кода. В результате стало возможным писать
на высоко-уровневом языке (которым является Си) даже системы, работающие в
условиях жестких ограничений по памяти и реальному масштабу времени. При этом
почти полностью устранена необходимость программирования на Ассемблере.
Основная цель данного руководства - дать пользователям представление о
применении компилятора в рамках операционной системы ESTEX и некоторых
особенностях этого компилятора.
SOLID C является двухпроходным компилятором языка, преобразующим исходный
си-текст в программу на языке Ассемблера. Компилятор состоит из двух файлов
"cc1.exe" и "cc2.exe", выполняющих соответственно первый и второй проходы.
На первом проходе входной си-текст преобразуется в объектный код (т-код),
сохраняемый в файле с расширением ".tmc" (по-умолчанию). На первом проходе
производятся также диагностика ошибок и выдача предупреждающих сообщений.
На втором проходе полученный т-код преобразуется в ассемблерный листинг,
сохраняемый в файле с расширением ".asm" (по-умолчанию).
Следует отметить тот факт, что поскольку данный компилятор является оптимизи-
рующим, то время, затраченное на компиляцию си-программы возрастает от 2-х и
более раз, по сравнению с обычными (не оптимизирующими) си-компиляторами.
ГЕНЕРАТОР ОБЪЕКТНОГО КОДА
Этот компонент (cc1.exe) компилятора обеспечивает первый проход. Он произ-
водит разбор исходного си-текста, его контроль на допустимость синтаксиса
и семантики. В результате первого прохода появляется промежуточный файл с
объектным кодом, т.н. Т-код. Стандартным расширением имен файлов, содержащих
т-код, является ".tmc".
Команда запуска компилятора первого прохода имеет следующий формат:
CC1 [опция] имя_файла
Имя файла может содержать букву диска и путь, при этом максимальная длина
строки не может превышать 80 символов. Обычно для исходных си-файлов исполь-
зуется расширение ".c". Если расширение входного файла ".c", то его можно
не указывать.
Опции генератора объектного кода
-c В отличие от стандартного языка Си, компилятор SOLID C
обрабатывает гнездование (вложение) комментариев.
Данная опция отключает режим гнездования комментариев.
-dNAME Определить макро-определение с именем "NAME". Между буквой опции
и именем "NAME" пробелы не допускаются.
-e Режим переопределения выдачи сообщений в файл ошибок, имеющего
расширение ".err". Файл создается всегда по текущему пути исходного
".c" файла.
-jN Установка максимального числа ошибок компиляции. Происходит
прерывание компиляции, если число ошибок равно или превышает
"N". Между буквой опции и числом "N" пробелы не допускаются.
Например: -j30 (прерывание процесса компиляции при 30 ошибках).
-k Режим совместимости со стандартным языком Си (стандарт K&R).
Разрешает делать неявные объявления функций и параметров.
Данную опцию необходимо использовать при компиляции "чужых"
исходников.
-m Режим вывода на экран статистики по таблицам компилятора.
-oNAME Задать имя для выходного файла с т-кодом. Максимальная длина
строки полного пути (диск\каталог\имя) файла составляет
80 символов. Между буквой опции и именем "NAME" пробелы не
допускаются.
-rP:S:H Установка отношения размеров частей рабочей области компилятора:
P - пула, S - таблицы символов, H - хеш-таблицы.
По-умолчанию используется соотношение 13:6:4. В случае переполнения
какой-либо части, используйте эту опцию для назначения другого
соотношения частей рабочей области.
Между буквой опции и строкой "P:S:H" пробелы не допускаются.
-x Режим строгой совместимости с MSX-C компилятором. При этом
расширение файла т-кода меняется с ".tmc" на ".tco".
КОДОГЕНЕРАТОР
Кодогенератор (cc2.exe) выполняет второй проход компилятора. Он читает
файл с т-кодом и создает выходной файл на языке Ассемблера.
Команда запуска компилятора второго прохода имеет следующий формат:
CC2 [опция] имя_файла
Если у входного файла с т-кодом расширение не указано, то используется
стандартное расширение ".tmc". Если не указано расширение для выходного
файла - используется расширение ".asm". Максимальная длина строки
"диск\каталог\имя_файла" должна быть не более 64 символов.
Если путь расположения для выходного файла не задан, то по-умолчанию
он создается по текущему пути расположения входного файла т-кода.
Опции кодогенератора
-k Не удалять входной файл т-кода (.tmc) по завершению работы
кодогенератора.
-oNAME Задать имя для выходного asm-файла. Максимальная длина
строки полного пути (диск\каталог\имя) файла составляет
64 символа. Между буквой опции и именем "NAME" пробелы
не допускаются.
-rN Резервирование области под таблицу символов компилятора размером
N байтов (N - десятичный формат). Между буквой опции и числом "N"
пробелы не допускаются. Например: -r2000 (резервируется таблица
символов размером в 2000 байтов).
-uX Определить пост-символ "X" для имен в asm-листинге. По-умолчанию
используется символ "_". Например: -u@ (использовать пост-символ
"@" для имен в выходном asm-листинге).
-q "Тихий" режим. Индикаторы процесса компиляции "---" и "+++"
на экран не выводятся.
ОСОБЕННОСТИ КОМПИЛЯТОРА SOLID C ДЛЯ ОС ESTEX
Как было сказано выше, целью разработки данного Си-компилятора было созда-
ние продукта, с помощью которого можно было бы (пользуясь всеми преимуществами
языка высокого уровня) разрабатывать программы, удовлетворяющие жестким требо-
ваниям реального масштаба времени и ограничений на память. В данной главе опи-
саны особенности компилятора с этой точки зрения. Кроме того, здесь будут даны
рекомендации по программированию на SOLID C.
Компилятор SOLID C может работать в двух различных режимах. Один из них
является "собственным" для данного компилятора, а второй - служит для обеспе-
чения совместимости со стандартом языка Си. В этом режиме все арифметические
преобразования выполняются точно так же, как в стандартном языке Си. Самое
большое различие между этим режимом и "собственным" состоит в том, что в пос-
леднем не выполняются автоматические преобразования форматов при переходах от
типа "char" к типу "int". По-умолчанию, компилятор работает в "собственном"
режиме. Для смены режима работы компилятора служит опция "-k" на первом прохо-
де компилятора.
Собственный режим компилятора
Для данного режима характерны два новых формата: целой константы (intconst)
и символьной константы (charconst). Обычно указанные форматы используются иден-
тично с соответствующими форматами "int" и "char", однако в случаях анализа
типов операндов и выражений, и приведения их к соответствию, указанные два
формата рассматриваются как различные.
Как упоминалось выше, в этом режиме не производится автоматическое преобразо-
вание форматов при переходе от типа "char" к "int". Действуют следующие согла-
шения о преобразованиях типов:
unsigned + ? -> unsigned
int + ? -> int
char + ? -> char
intconst + ? -> intconst
charconst + ? -> charconst
Режим совместимости со стандартом языка Си
Выполняются все арифметические преобразования, действительные в рамках реа-
лизации стандартного языка Си. Единственное различие состоит в том, что компи-
лятор SOLID C интерпретирует тип "char", как значение без знака.
Для этого режима действуют следующие правила преобразования типов:
char -> int
unsigned + ? -> unsigned
int + int -> int
Рекурсивные функции
Как известно, все функции стандартного языка Си рекурсивны. В среде компи-
лятора SOLID C можно писать также и нерекурсивные функции. Для различия
функций этих двух типов введены и зарезервированы следующие ключевые слова:
"recursive" - рекурсивная и "nonrec" - нерекурсивная. Эти ключевые слова
записываются в заголовке функции перед ее именем и служат описателями режимов
ее выполнения. Это показано в следующем примере:
nonrec main()
{
printf("Hello, world!\n");
}
recursive
int factorial(n)
int n;
{
return(n > 1) ? n*factorial(n-1) : 1;
}
int max(a,b)
int a,b
{
return(a > b) ? a : b;
}
В приведенном примере даны определения: нерекурсивной функции main() и
рекурсивной функции factorial(). Для функции max() описатель режима выпол-
нения функции в явном виде не задан, поэтому она будет выполняться так, как
задано по-умолчанию. Обычно по-умолчанию задается режим выполнения функций
как рекурсивных. Однако это умолчание можно изменять с помощью следующих
предложений препроцессора:
#pragma nonrec
#pragma recursive
Первым предложением по умолчанию задается режим выполнения функций, как
нерекурсивных. Второе предложение предписывает рассматривать все функции,
как рекурсивные (если явно не оговорено противоположное).
Для случая, когда действует предложение #pragma, функция max() будет выпол-
няться согласно последнему описателю режимов "рекурсивная/нерекурсивная".
Рекомендуется давать явное указание "nonrec" кампилятору, если нужно, чтобы он
воспринимал функции как нерекурсивные. По такому указанию, компилятор гаран-
тированно сгенерирует объектный код, более короткий и более быстрый, чем в
других случаях. В обычных прикладных программах бОльшая часть функций нерекур-
сивна, поэтому в начале исходного текста следует писать предложения препроцес-
сора: #pragma nonrec
Функции с переменными параметрами
Компилятор SOLID C обрабатывает функции двух типов: с переменным числом
параметров и с фиксированным. В функциях первого типа число и представление
параметров могут изменяться, тогда как в функциях второго типа они фиксированы.
Например, функции printf, scanf и ряд других являются функциями с переменным
числом параметров. В компиляторе SOLID C функции указанных двух типов разли-
чаются с помощью специальных описателей.
Структура предложений языка изменяется с введением простых и абстрактных
описателей следующим образом:
С простым описателем:
идентификатор
(описатель)
*описатель
описатель()
описатель(.)
описатель [константное выражение]
С абстрактным описателем:
пусто
абстрактный описатель
*абстрактный описатель
абстрактный описатель()
абстрактный описатель(.)
абстрактный описатель [константное выражение]
Конструкции "описатель(.)" и "абстрактный описатель(.)" впервые введены
для обозначения функций с переменными параметрами. Конструкции "описатель()"
и "абстрактный описатель()" обозначают функции с фиксированными параметрами.
Данный метод описания функций с переменными параметрами используется в пред-
ложениях типа cast, sizeof. Например, в следующих описаниях функции func и fp
определены, как функции с переменными параметрами:
int func(.), (*fp)(.);
Перед применением функции с переменными параметрами, ее необходимо всякий раз
описывать подобным образом. В библиотеке стандартных функций имеются функции
с переменными параметрами, как например:
printf, fprintf, sprintf, scanf, fscanf, sscanf
Описания перечисленных функций содержатся в стандартном файле "stdio.h".
Вызов функции с переменными параметрами совершенно идентичен вызову функции
с фиксированными параметрами.
Эти функции не обладают мобильностью с точки зрения рабочей машины (главным
образом по методу доступа к параметрам). Отсутствие переносимости имеет место
только для самой функции с переменными параметрами. Внешняя функция, ее вызы-
вающая, является переносимой.
Определение функции с переменными параметрами должно быть записано в самом
начале, перед ее телом. Ниже приведено типичное определение функции func,
являющейся функцией с переменными параметрами:
int func(.); /* это определение обязательно должно идти в начале */
int func(nargs, args)
int nargs, args;
{
...
}
В момент вызова функции с переменными параметрами, вместо фактического
параметра подставляется его копия - осуществляется вызов по значению.
Значением параметра nargs является количество фактических параметров - фикси-
рованная величина. Благодаря ей можно установить, сколько фактических парамет-
ров реально передается в функцию с переменными параметрами со стороны вызыва-
ющей функции. Однако возможности ограничены тем, что можно знать только число
передаваемых параметров. При этом, установить тип каждого из этих параметров
невозможно.
Для того, чтобы организовать доступ к указанным параметрам, нужно описать
каждый из передаваемых при вызове функции параметров. Первый из параметров
(если он имеется) расположен по адресу массива args. Другими словами, значение
первого из параметров args присваивается идентификатору args. Второй из пара-
метров помещается по адресу, сдвинутому в сторону роста адресов памяти. Вообще
можно составить последовательность адресов:
адрес 2-го параметра = адрес args + 2
адрес 3-го параметра = адрес args + 4
В следующей таблице приведено соответствие параметров и их адресов:
________________________________________________
Номер параметра Адрес
________________________________________________
1 Адрес начала массива args
2 Адрес начала + 2
3 Адрес начала + 4
...
n Адрес начала + (n-1)*2
________________________________________________
Согласно соответствию адресов и последовательности параметров, приведенному
в таблице, можно организовать доступ к этим параметрам. Например, для того,
чтобы сослаться на первый и второй параметры типа "char", необходимо написать
в программе:
(char)args - ссылка на первый параметр типа "char"
(char)+(&args+1) - ссылка на второй параметр типа "char"
Примечание: Если в приведенном выше примере args является "int", то благодаря
описанию, значение &args преобразуется в тип "int". Адрес следующего по порядку
аргумента формируется согласно формуле &args+1, в которой константа "1" преоб-
разуется в соответствии с масштабом разрядной сетки конкретной машины: как пра-
вило, это два байта (вообще, можно определить эту величину с помощью выражения
sizeof(int*)). В конкретном случае это дает "адрес начала массива параметров
плюс два". Аналогичным образом можно организовать доступ к параметрам других
типов.
В заключение данного параграфа приведены два примера простых функций с
переменными параметрами: функция sum возвращает значение суммы переменного
числа "int" параметров. Функция cmax возвращает максимальное значение из ряда
"int" параметров в TINY (char), число которых - переменное.
#pragma nonrec
typedef char TINY;
int sum(.);
TINY cmax(.);
int sum(nargs, args)
int nargs, args
{
int i, *p;
for(i=0, p=&args; nargs --;)
i += *p++;
return i;
}
TINY cmax(nargs, args)
int nargs;
TINY args;
{
int *p, max;
for(max=0, p=(int*)&args; nargs--; p++)
if(max < (TINY)*p)
max = (TINY)*p;
return max;
}
Преобразования байтовой арифметики
Для повышения эффективности объектного кода, компилятор SOLID C выполняет
преобразования байтовой арифметики. Благодаря этой особенности, все преобразо-
вания переменных, имеющих тип "char", выполняются не со словами, а с байтами.
Поскольку процессор Z80 с восьмибитовым форматом команд выполняет эти операции
значительно быстрее, нежели другие. Байтовая арифметика особенно эффективна
при написании специальных программ, включающих байтовые преобразования.
Результаты логических операций и операций отношения
&& || ! = != > < >= <=
имеют тип "char".
Традиционно, те байтовые переменные, которые обозначают внешние кодовые
символы, специфицируются с помощью определения типа typedef в виде TINY или
BOOL. Первый определяет арифметические объекты, а второй - логические.
Определения указанных типов включены в файл стандартных заголовков "types.h".
Ниже даны несколько примеров употребления байтовых операций, на которые
следует обратить особое внимание!.
1). Переполнение и исчезновение
При выполнении байтовых арифметических операций, возможны случаи перепол-
нения или исчезновения. Например, в процессе выполнения операторов
int i;
TINY x,y;
x = 10; y = 30;
i = x * y;
вместо правильного значения 300, переменная "i" получает неправильное зна-
чение 44. Это происходит вследствие того, что в процессе выполнения умножения
по правилам байтовой арифметики теряется старший байт результата. Для того,
чтобы получить правильный результат (число 300), нужно оперировать с целыми
операндами, для чего следует специфицировать их с помощью функции cast, как
показано в следующем примере:
i = (int)x * (int)y;
Такое же явление возникает и в следующей ситуации:
i = x << 8 | y;
При выполнении операции сдвига над операндом байтовой длины, может быть
потеряна его часть вследствие переполнения. На это также нужно обратить внима-
ние. Величину "y" необходимо подставить в выражение для "i" в том же самом
виде, что и показано ниже:
i = (unsigned)x << 8 | y;
2). У типа TINY (char) отсутсвует знак числа. Переменные типа "char" исполь-
зуются без знака. Поэтому иногда возникают ситуации, в которых результат опе-
рации вычитания становится отрицательным. Поэтому, если выполнить ниже-следую-
щую программу, в переменную "i" будет записано 255:
int i;
TINY x,y;
x = 2;
y = 3;
i = x - y;
Для того, чтобы получить правильный результат, нужно выполнить эту операцию
в соответствии с типом "int":
i = (int)x - (int)y;
3). Согласование параметров
Компилятор SOLID C проводит четкое различие между типами "int" и "char".
Поэтому фактические и формальные параметры должны быть обязательно согласованы
между собой по типу. Следующий пример иллюстрирует неправильный вызов функции:
func(x)
TINY x;
{
...
}
main()
{
func(1); /* неправильный вызов функции */
}
Нужно изменить вызов следующим образом:
func((TINY)1);
Согласование параметров по типу, компилятором не проверяется. Поэтому,
следует обращать особое внимание на подобные ситуации.
Распределение регистров
Поскольку компилятор SOLID C поддерживает распределение регистровых пере-
менных, он способен генерировать более эффективный объектный код. В стандарт-
ном языке Си предусмотрено размещение непосредственно в регистрах (класс памя-
ти "register") только нескольких первых переменных из всего их множества.
(Какое именно количество регистровых переменных будет помещено в регистры,
зависит от того, какая рабочая машина будет использоваться). В отличие от
стандартного, компилятор SOLID C автоматически выполняет оптимальное распре-
деление регистров, в соответствии с потоком данных. Этим самым снята необхо-
димость обязательного указания переменных программы, как регистровых. Вопрос,
какие из переменных будут помещены в регистры, решается на уровне кодогенера-
ции. Поэтому описание "register" идентично пo смыслу описанию "auto" и никакого
другого смысла не имеет.
В каждой функции программы, первые 16 переменных простого типа становятся
кандидатами на размещение в регистрах, как переменные типов "аuto" или
"register". K числу таких простых типов относятся "int", "char", "unsigned"
и "указатель".
Средствами языка Си можно получать адреса переменных. Компилятор SOLID C
также, безусловно, располагает этими средствами. Однако, в случаях тех перемен-
ных, которые являются объектами распределения регистровой памяти, возникает
проблема. Она обусловлена тем, что как таковой "адрес" регистра не существует.
Например, если написать
int n;
n = 10;
scanf("%d", &n);
printf("%d", n);
и ввести 100, то какое значение получит переменная "n" ?. Конечно, 100.
Компилятор поместит значение переменной по заданному адресу в память, если
указать ему значение этого адреса. Следовательно, никаких проблем не возникнет,
если при вызове функции передать ей в качестве параметра адрес переменной.
Однако в примере
int n;
int *p;
p = &n;
n = 10;
*p = 100;
printf("%d", n);
возможна ситуация, когда в качестве значения переменной "n" будет индициро-
вано не 100, а 10. Между двумя предложениями, одно из которых "n=10", a другое
"printf("%d",n)", не фигурирует "&n". Поэтому компилятор не помещает "n" в па-
мять.
НЕКОТОРЫЕ ДРУГИЕ ОСОБЕННОСТИ КОМПИЛЯТОРА
Таблица представления данных различного типа
-----------------------------------------------------
Тип Разрядность Диапазон значений
-----------------------------------------------------
char 8 0..255
short 16 -32768..32767
int 16 -32768..32767
unsigned 16 0..65535
_____________________________________________________
1. В рамках компилятора SOLID C проводится идентификация переменных и
функций по первым 30-ти символам имени. Благодаря такому расширению имен,
можно использовать легко поддающиеся анализу имена переменных и функций, и
получать удобочитаемые тексты программ.
2. Позиция ошибки диагностируется по номеру ошибочной строки и начальному
символу (колонки в строке). Начальная позиция считается с 0. Таким образом,
позиция первого символа программного текста есть "нулевая строка, нулевая ко-
лонка".
3. Для указания имени включаемого файла используется предложение "#include"
препроцессора, как описано ниже:
<имя файла> - ссылка на файл, расположенный в подкаталоге "INCLUDE"
каталога компилятора.
"имя файла" - ссылка на файл, расположенный по текущему пути исходного
".c" файла.
Задание пути в имени файла не допускается. Например, если имеются предложения
#include <stdio.h>
#include "common.h"
то в первом случае сделана ссылка на стандартный файл компилятора Си, распо-
ложенного в подкаталоге с зарезервированным именем "INCLUDE" каталога SOLID C
(по-умолчанию "SOLID"), а во втором случае - на файл, расположенный по теку-
щему пути входного ".c" файла.
Следует обратить внимание на то, что имя подкаталога "INCLUDE" зарезервировано
и его не следует менять.
4. Типы long, float, double не поддерживаются, но зарезервированы. Поэтому
нельзя использовать слова "long", "float" и "double" в качестве идентифика-
торов переменных и меток.
5. Не поддерживаются битовые поля структур.
6. Некоторые ограничения в константных выражениях. Нельзя включать в
константные выражения указанных ниже типов унарную операцию "sizeof":
a) сразу после оператора "case" (в качестве метки);
b) в описателе массива в качестве его размерности.
Подобные ограничения не действуют на те константные выражения, которые
появляются в инициаторе (т.е. при инициализации).
7. Имеется отличие от стандартного языка Си по эффективным пределам имен
членов структур и объединений (struct & union). В стандартном Си одно и то же
имя члена может появляться в двух различных структурах только в случае, когда
они оба одного типа и имеют одинаковое смещение в обеих структурах. Компилятор
SOLID C воспринимает имя члена так, как принято в Паскале или в Аде. Таким об-
разом, одно и то же имя члена может появляться в разных структурах и будет
правильно обработано компилятором SOLID C.
В качестве примера рассмотрим следующее описание:
struct node {
char *word;
int count;
struct node *next;
} pool[1000], *p;
struct noad {
int atr;
struct noad *next;
} table[10], *q;
В стандартном языке Си данная запись является неправильной, потому, что имя
члена "next" появляется в двух структурах - "node" и "noad", где их типы и сме-
щения не согласованы. Однако в среде компилятора SOLID C эти записи являются
правильными.
Следует обратить внимание на следующую запись
p->next
которая описывает член типа "struct node" (потому что "p" есть указатель типа
"struct node"). Благодаря такому приему можно подбирать правильные члены путем
строгого указания их типов. Так, в операциях "." и "->" левые члены должны быть
указателями структур, включающих члены, записанные в качестве правой части этих
операций. Например:
i->count
включает "i" не как указатель типа "struct node", а как "int" (целое). Поэтому
компилятор SOLID C такую ситуацию не диагностирует. Для того, чтобы получить
такой же результат, необходимо переписать приведенное выражение следующим
образом:
((struct node *)i)->count
В приведенном выше примере для того, чтобы операция "->" была более высокого
ранга, нежели операция "cast", следует терм
(struct node *) i
заключить в скобки.
До того, пока не будет преобразован тип указателя в "int", что выполняется
с помощью функции cast, операция не может быть выполнена.
Даже при выполнении арифметических операций в случае указателей разных ти-
пов, также необходимо использовать функцию cast. Например, в случае описаний
int i;
char *p;
int *q;
операции
p = i;
if(p == i)
q = p;
if(p == q)
будут отмечены как ошибочные.
Две первые ошибочны потому, что производятся подстановки между типами указа-
теля и "int" (целого), и операции сравнения между ними. Две последние также
ошибочны, потому, что несмотря на то, что и "p", и "q" обе являются указате-
лями, но они специфицированы разными типами и поэтому компилятор отметит их,
как ошибочные.
Эти ошибки можно устранить, либо использованием функции cast, либо указанием
опции "-k" на первом проходе компилятора.
С другой стороны, разрешены преобразования типов из целого в указатель и
обратно. Это обусловлено тем, что в языке Си целая константа 0 используется
как указатель особого адреса. Например, запись
char *p;
if(p != 0)
p = 0;
не содержит ошибки.
Типы "int" и указателей нельзя смешивать в среде компилятора SOLID C. Дан-
ное соглашение вообще может считаться показателем хорошего стиля программиро-
вания, при котором отладка программы становится проще.
8. Нет автоматического преобразования между типами "указатель" и "int",
и обратно. В стандартном языке Си разрешены операции сравнения значений указа-
телей и целых (int), но в SOLID C смешивание указанных типов вызовет сообщение
об ошибке. Однако этот режим можно разрешить, указав опцию компилятора "-k"
(на первом проходе).
9. Тип "char" интерпретируется как значение без знака, т.е. может иметь
значение 0..255.
10. В рамках стандартного языка Си, в случаях появления необъявленных ранее
функций, они интерпретируются как возвращающие "int" значение. Параметры
необъявленного типа, которые вошли в список параметров, считаются "int".
Рассматриваемый здесь компилятор не позволяет использовать эти умолчания. То
есть, хороший стиль программирования предполагает описание всех идентификато-
ров до того, как на них будут выполняться ссылки - автоматические ссылки вы-
полняться не будут.
Все объявления, которые относятся к функциям стандартной библиотеки, запи-
саны в файле стандартных заголовков, поэтому любую из стандартных функций можно
использовать без объявления. Поэтому все пользовательские программы должны
содержать ссылку на файл "stdio.h" в самом начале текста, в предложении
#include <stdio.h>.
Однако, если на первом проходе компилятора указать опцию "-k", указанные
описания функций будут объявлены по-умолчанию. От этого теряется качество полу-
чаемого кода, поэтому данной опцией целесообразно пользоваться, если важно
обеспечить совместимость со стандартным языком Си.
11. В данной версии рассматриваемого компилятора не поддерживается предложе-
ние препроцессора "#if". Вполне возможно заменить указанное предложение дру-
гими: "#ifdef" или же "#ifndef". Возможны также другие варианты замены предло-
жения "#if". Рассматриваемый здесь компилятор не генерирует код тех участков
программы, выполнение которых не требуется при ее прогоне. Поэтому можно полу-
чить точно такой же результат, если применить оператор "if" вместо предложе-
ния "#if". Например, можно заменить текст одной из ниже-следующих программ на
другую:
Программа 1:
#if sizeof(FCB) != sizeof(char[36])
Open_MSX();
#else
Open_OTHER();
#endif
Программа 2:
if (sizeof(FCB) != sizeof(char[36]))
Open_MSX();
else
Open_OTHER();
12. Введена директива препроцессора "#pragma". Применяется для различных
указаний компилятору. Все предложения данного вида могут появляться в любом
месте исходной программы. Ниже, в качестве примеров приведены четыре предложе-
ния с заданием соответствующих директив компилятору:
1. #pragma optimize time
Данное указание компилятор интерпретирует так, что он старается сгенерировать
объектный код, отличающийся более высокой скоростью выполнения, нежели в слу-
чае, когда он должен быть более компактным.
2. #pragma optimize size
При таком указании, компилятор считает более важным сгенерировать компактный
объектный код, хотя бы и не очень быстрый в исполнении.
3. #pragma nonrec
Этой директивой изменяется задание по-умолчанию режима выполнения функций,
как нерекурсивных.
4. #pragma recursive
С помощью данного предложения устанавливается по-умолчанию режим выполнения
функций как рекурсивных.
13. Поскольку компилятор SOLID C поддерживает для переменных автоматическое
распределение регистров, класс памяти всегда устанавливается "auto" (автомати-
ческий).
14. Компилятор SOLID C позволяет гнездовать тексты комментариев. Эта функция
обеспечивает возможность написания больших программ, содержащих комментарии, в
теле которых, в свою очередь содержатся комментарии. Если на первом проходе
указать компилятору опцию "-c", гнездование комментариев производиться не бу-
дет, что отвечает возможностям стандартного языка Си.
ПЕРЕДАЧА ПАРАМЕТРОВ
Функции с фиксированными параметрами
В случае обычных функций (функций с фиксированными параметрами), первые три
параметра помещаются в регистры, а все последующие - в стек. Если какой-либо
из 3-х параметров имеет тип "char", он помещается в соответствующий 8-ми битный
регистр. Параметры (первые три) других типов помещаются в регистровые пары.
4-й параметр (если есть) и все последующие - помещаются в стек, в обратном по-
рядке (паскалевский способ ?). На вершину стека помещается адрес возврата.
Каждый из размещаемых в стеке параметров занимает два байта. Значение "char"
параметра помещается в младший байт, а значение старшего байта остается неопре-
деленным.
В режиме совместимости со стандартным языком Си, использование параметра типа
"char" имеет отличия: преобразование из "char" формата в "int" производится
автоматически (при этом старший байт обнуляется), после чего данные рассматри-
ваются как "другой тип".
_______________________________________________________________________
Тип параметра Первый Второй Третий Четвертый Пятый ...
_______________________________________________________________________
char A E C (SP+2) (SP+4) ...
другой тип HL DE BC (SP+2) (SP+4) ...
(SP+3) (SP+5) ...
_______________________________________________________________________
Функции с переменными параметрами
Когда вызывается функция с переменными параметрами, все параметры заталки-
ваются в стек (в обратном порядке). Число параметров, помещенных в стек, запи-
сывается в регистровую пару HL. На вершину стека помещается адрес возврата.
В "собственном" режиме, "char" параметр помещается в младший байт, значение
старшего байта остается неопределенным.
В режиме совместимости со стандартом языка Си, "char" тип автоматически
преобразуется в "int" тип (при этом старший байт обнуляется), который далее
трактуется как "другой тип".
Параметры других типов размещаются в стеке согласно правилам стандарта для
процессора Z80, а именно: первым запоминается младший байт, вторым - старший.
_______________________________________________________________________
Тип параметра Количество Первый Второй Третий ...
_______________________________________________________________________
char HL (SP+2) (SP+4) (SP+6) ...
другой тип HL (SP+2) (SP+4) (SP+6) ...
(SP+3) (SP+5) (SP+7)
_______________________________________________________________________
ВОЗВРАТ ЗНАЧЕНИЯ ФУНКЦИИ
При передаче управления вызывающей программе, функция может передавать свое
значение через регистры. При этом функция типа "char" передает значение через
регистр "A". Функции других типов передают свои значения через регистровую
пару HL.
В режиме совместимости со стандартным языком Си, возвращаемое значение всегда
передается через регистровую пару HL, даже в том случае, когда функция имеет
"char" тип.
Для возврата в вызывающую функцию двух и более значений, необходимо исполь-
зовать указатели переменных, как параметры любых других си-программ.
ПРАВИЛА ВЫЗОВА ФУНКЦИЙ
1. Во-первых, вызывающая программа оформляет параметры и помещает их в ре-
гистры или заталкивает в стек согласно правилам, описанным выше.
2. Программа вызывает функцию. Для вызова обычно используется слово "call".
3. После возврата управления функцией, параметры выталкиваются из стека
(с использованием команды "pop"). Если все параметры размещены в регистрах,
использовать команду "pop" нет необходимости. Следует отметить, что в случае
применения команды "pop", возвращаемые через регистры "A" или "HL" значения
функций не будут потеряны.
4. Значение функции возвращается через регистр "A", если функция имеет
"char" тип. Для функций другого типа, значение всегда возвращается через
регистровую пару HL.
ПЕРЕДАЧИ УПРАВЛЕНИЯ ВЫЗЫВАЕМОЙ ФУНКЦИИ
Вызываемая функция не должна разрушить содержимое указателя стека. Иными
словами, содержимое указателя стека при выходе из функции должно остаться тем
же, что и при входе в нее. Параметры, загруженные в стек, должны выталкиваться
из него вызывающей программой. Вызываемая функция может изменить значения па-
раметров (как тех, которые передаются через регистры, так и загруженных в стек).
Однако следует подчеркнуть, что в языке Си действует соглашение, в рамках кото-
рого вызов функций осуществляется по значению, поэтому даже если будут возвра-
щены измененные параметры, это никакого влияния на процесс вызова функции не
окажет.
Возвращаемое значение функции помещается в регистр "A" или в регистровую
пару HL - в зависимости от типа вызванной функции. Эти правила описаны в пре-
дыдущем разделе.
Пример 1:
В следующем примере объявлена ассемблерная функция ISDIGIT, которая возвра-
щает значение функции BOOL в регистре "A". Вызывающая функция передает параметр
"char" типа через регистр "A".
... вызывающая программа (на языке Си)...
typedef char BOOL; /* определение "char" типа как BOOL */
...
BOOL isdigit(c);
char c;
...
if (isdigit(c))
...
...
... вызываемая функция (на языке Ассемблера)...
public isdigit_
isdigit_:cp '0'
jr c,false
cp '9'+1
ret c ; цифра, вернуть true
false: xor a ; не цифра, вернуть false
ret
Пример 2:
Этот пример соответствует случаю вызова функции, написанной на Си, из ас-
семблерной процедуры.
Стандартные библиотечные функции printf и puts вызываются из ассемблерной
процедуры. Их исходные тексты находятся в исходниках библиотеки, поэтому здесь
функции printf и puts не будут описаны. Следует заметить, что поскольку функ-
ция printf - с переменными параметрами, ее параметры заталкиваются в стек.
В данном примере параметр всего один, однако в случаях двух и большего числа
параметров, они помещаются в стек в обратной последовательности. Число парамет-
ров передается в вызываемую функцию через регистровую пару HL (в приведенном
примере передается число 1). После возврата из вызываемой функции, будут выпол-
няться операции выталкивания из стека (в примере команда "pop" будет выполнена
1 раз). Следует помнить, что параметры восстанавливаются из стека вызывающей
программой, а не вызываемой.
С другой стороны, функция puts принадлежит к числу функций с фиксированными
параметрами, поэтому адрес символьной строки передается через регистровую пару
HL.
extern printf_, puts_
example: ld hl,msg
push hl ; поместить в стек адрес символьной строки
ld hl,1 ; загрузить число параметров
call printf_ ; вызов процедуры, формирующей вывод
pop hl ; выталкивание параметров из стека
;
ld hl,msg ; загрузить адрес строки
jp puts_ ; вызов функции вывода строки
msg: db 'Hello, world!',0Ah,0
РЕКОМЕНДАЦИИ ПО ПРОГРАММИРОВАНИЮ
1. Следует пользоваться нерекурсивными функциями, если нет особой необхо-
димости использовать рекурсию. Благодаря такому подходу, можно достичь более
быстрого доступа к тем локальным переменным, для которых не распределяется
регистровая память и следовательно, получить эффективный объектный код.
2. Если указан режим совместимости со стандартным языком Си (опция "-k"),
то качество объектного кода будет ниже, поэтому указание на этот режим нужно
применять только в случаях, когда требуемая совместимость на уровне стандарт-
ного языка Си нужна больше, чем эффективность кода.
3. Используйте тип "char" (с 8-битовой длиной данных) везде, где только
можно. Байтовые операции выполняются с помощью 8-битового процессора более
эффективно, чем над данными, длиной в слово (типы "int" или "unsigned").
Поэтому определяйте переменные, числовые значения которых не выходят за пре-
делы диапазона 0..255, как TINY (char).
4. При использовании 16-битовых переменных следует определять их преимущест-
венно типом "unsigned", который в этих случаях предпочтительнее, нежели "int".
Это обусловлено тем, что в процессоре Z80 выполнение операций со знаком (в част-
ности, операций сравнения величин со знаком) реализовано не лучшим образом.
5. Нежелательно установление статического режима для локальных переменных.
Переменные, определенные как статические или внешние, не подлежат загрузке в
регистровую память. Поэтому, если для повышения скорости выполнения программы
определить локальные переменные как "static", а функции как "extern", то
результат будет противоположным - эффективность объектного кода существенно
понизится. Компилятор SOLID C сам производит распределение регистров, поэтому
следует обязательно определять локальные переменные, как "auto".
ЗАРЕЗЕРВИРОВАННЫЕ СЛОВА ЯЗЫКА
Следующие слова используются как ключевые в компиляторе SOLID C и их нельзя
использовать в качестве меток или имен переменных.
asm float signed
auto for static
break goto struct
case if switch
char int typedef
const long union
continue nonrec unsigned
default recursive void
do register volatile
double return while
else short
extern sizeof
ДИРЕКТИВЫ КОМПИЛЯТОРА
define nonrec
endif optimize
error pragma
ifdef recursive
ifndef size
include time
line undef