Собираю здесь все что связано с разработкой на C, предпочтительно с использованием кросскомпиляции GCC
-
В папке
gccлежит скриптbuild_gcc_uknc.shс помощью которого можно собрать тулчейн для сборки проектов для УКНЦ. -
В папке
examplesлежат минимальные примеры приложений, собираемых с помощью тулчейна, получаемого скриптом из пункта 1. -
В папке
libsлежитlibppu— библиотека для загрузки и запуска кода в ПП (периферийном процессоре) со стороны ЦПУ (ppuc_alloc/ppuc_load_code/ppuc_runи т.п. — префиксppuc_у клиентских функций отличает их от серверных, которые работают в самом ПП; см.libs/libppu/ppu_client.h). Собирается и устанавливается в тулчейн тем же скриптом из пункта 1 —libppu.a/ppu_client.hоказываются рядом сlibc.a, так что достаточно-lppuпри линковке. Как собрать и загрузить код для самого ПП — см. раздел ниже.
Благодаря STARTFILE_SPEC и полноценной стандартной библиотеке (newlib, см.
ниже) отдельный Makefile не обязателен — gcc сам скомпилирует и слинкует
программу за один вызов, с printf/malloc/atoi и остальным stdio.h/
stdlib.h из коробки:
pdp11-uknc-rt11-gcc -std=gnu23 -fomit-frame-pointer -O2 \
-Wl,--gc-sections -o example.sav example.cВ результате получается готовый .sav файл — самостоятельный образ памяти
в формате RT-11 SAV, который можно сразу записывать на диск (rt11dsk) и
запускать на УКНЦ под RT-11 без какой-либо дополнительной обработки.
Целевой триплет — pdp11-uknc-rt11. Тулчейн (binutils + GCC + newlib)
собирается из ванильных исходников (binutils-2.45, gcc-15.2.0, newlib
снапшот, см. раздел newlib ниже) с наложением патчей из форков
wdigger/binutils-gdb,
wdigger/gcc и
wdigger/sourceware-mirror-newlib-cygwin
— build_gcc_uknc.sh скачивает их автоматически. Ниже — что именно эти
патчи меняют относительно ванильных binutils/GCC/newlib.
Программа для ПП (периферийного процессора) — это не обычный .sav, а
отдельный relocatable-модуль в родном формате RT-11 (.ppu), который
ЦПУ-программа загружает в память ПП и запускает во время своей работы,
а не то, что грузит и запускает сама RT-11.
Со стороны ПП (это как раз код, который будет реально выполняться на
периферийном процессоре) достаточно определить точку входа ppu_main
— аналог main для обычных программ:
#include "ppu_server.h"
void ppu_main(void) {
ppus_debug_print(" PPU IS ALIVE");
}Настоящую точку входа (START — обязательное имя по конвенции
RT-11/PRUN) и обвязку вокруг неё (вызов ppu_main, затем автоматический
выход через ppus_exit()) даёт стартовый код из самой libppu
(libs/libppu/ppus_start.c) — писать это самому не нужно.
ppus_debug_print() печатает строку через EMT 056 резидентного
отладочного монитора ПП; ppus_exit() можно вызвать и самому, если
нужен ранний выход, не дожидаясь возврата из ppu_main.
Собирается .ppu-файл специальным линкером pdp11-uknc-rt11-ld-ppu
(тонкая обёртка над pdp11-uknc-rt11-ld, устанавливается тем же
скриптом из пункта 1 вместе с libppu) — она уже прячет внутри и
нужный линкер-скрипт (ppu.ld, гарантирующий, что START всегда
попадает на смещение 0 — то есть не зависит от того, в каком порядке
файлы перечислены в командной строке), и линковку с libppu.a:
pdp11-uknc-rt11-gcc -c -O2 pptest.c -o pptest.o
pdp11-uknc-rt11-ld-ppu -o pptest.ppu pptest.oСо стороны ЦПУ загрузка и запуск — через ppuc_load_code()/ppuc_run()
из ppu_client.h:
#include "ppu_client.h"
long ppu_addr = ppuc_load_code("PPTEST.PPU");
if (ppu_addr >= 0) {
ppuc_run((unsigned short) ppu_addr);
}ppuc_load_code() сам читает файл с диска RT-11, разбирает блоки
GSD/TXT/RLD/ENDMOD, раскладывает .text/.data/.bss по памяти ПП,
применяет релокации и возвращает адрес загрузки — он же адрес точки
входа (гарантия ppu.ld выше избавляет от отдельного поиска символа
START в самом загрузчике). Рабочий пример целиком — examples/ppurun
(ЦПУ-часть ppurun.c загружает и запускает pptest.ppu, собранный из
pptest.c).
Обмен данными между уже запущенными ЦПУ- и ПП-программами — поверх
ppuc_load_code()/ppuc_run() выше, для программы, которая уже
работает на ПП (а не только загружается/стартует), есть два
независимых канала передачи произвольных буферов, оба поверх
CPU↔PPU-каналов, независимых от канала 2, который использует
резидентный монитор:
- ЦПУ → ПП:
ppuc_send()(ppu_client.h) на стороне ЦПУ,ppus_recv_init(callback)на стороне ПП (ppu_server.h) — программа для ПП вызываетppus_recv_init()один раз изppu_main(), передавая ей свой обработчик (void (*)(const void *buf, unsigned int size), вызывается из прерывания на каждый принятый буфер);ppuc_send()можно вызывать многократно, её первый вызов после каждогоppuc_run()дожидается подтверждения отppus_recv_init(), что тот реально перехватил канал у резидентного монитора (иначе первые несколько байт мог бы получить не он). - ПП → ЦПУ:
ppus_send()(ppu_server.h) на стороне ПП, на стороне ЦПУ — на выбор: простой блокирующийppuc_recv(), либо (тем же вектором 0460, что и у ПП свой 0340)ppuc_recv_init(callback)— interrupt-driven приём, не блокирующий выполнение между сообщениями. У обоих одно и то же условие: если программа на ПП также используетppus_recv_init()/ppuc_send(), на ЦПУ первым обязательно должен быть вызванppuc_send()(см. выше), иначе служебный байт подтверждения будет спутан с началом настоящего сообщения —ppuc_recv_init()нужно вызывать уже после этого первогоppuc_send(), не раньше. И симметричноppus_recv_shutdown()ниже: если использовалиppuc_recv_init(), перед выходом программы обязателен парныйppuc_recv_shutdown().
Оба канала используют один и тот же формат — little-endian 16-битная
длина, затем сами байты; подробности и точные гарантии — в
комментариях ppu_client.h/ppu_server.h. Сама библиотека внутри
буфера смысл байтов не разбирает — если сообщения бывают разных видов
(не только данные), это на совести программы: рабочий пример обоих
каналов сразу — examples/ppupong (циклический обмен "ping N"/"pong
N"), где каждое сообщение начинается с байта типа
(MSG_TYPE_DATA/MSG_TYPE_EXIT), а не только с самих данных — так
ПП-программа отличает обычный пинг от явной команды на выход из своего
цикла и возврат управления резидентному монитору. Важно: если
ПП-программа использует ppus_recv_init(), она обязана перед выходом
из ppu_main() вызвать ppus_recv_shutdown() — иначе вектор
прерывания остаётся указывать на память, которую ppus_exit() тут же
освобождает, и монитор ПП зависает при первом же последующем
прерывании канала 2. Тот же риск и в обратную сторону: ЦПУ-программа,
использовавшая ppuc_recv_init(), обязана вызвать
ppuc_recv_shutdown() перед своим .EXIT — RT-11 память между
программами не очищает, так что не восстановленный вектор 0460 после
завершения программы будет указывать в память, которую перезапишет
следующая загруженная программа.
Реальный ввод с клавиатуры и захват экрана — examples/gfour
(три прыгающих цветных квадрата на экране, до первого нажатия любой
клавиши): ПП-часть (gfourppu.c) строит собственный тег-лист в plane 0
поверх штатного текстового экрана RT-11 и ставит свой обработчик
прерывания на вектор 0300 — единственный реальный путь к клавиатуре
(0177700/0177702), который есть только у ПП; канал 0 (тот, что
использует .TTYIN) для этого не годится — он просто перестаёт
обслуживаться, как только тег-лист захвачен. Каждое нажатие ПП
пересылает ЦПУ через ppus_send()/ppuc_recv_init() (канал 1, см.
выше), а по первому же настоящему нажатию (не отпусканию) сама
восстанавливает то, что подменила — вектор 0300, исходный тег 0 и
очищенный экран — и только после этого возвращает управление резидентному
монитору ПП, чтобы консоль RT-11 действительно ожила снова, а не
осталась зависшей картинкой. Подробности и почему это не сработало бы
через канал 0 — в комментариях gfourppu.c/gfour.c.
-
sav-pdp11(bfd/sav-pdp11.c) — новый write-only BFD-таргет, который пишет исполняемые файлы RT-11 SAV напрямую, без внешнего конвейера bin2load/lda2sav. Заголовок SAV содержит стартовый адрес, начальный SP (адрес вершины пользовательской памяти, а не точки входа — RT-11 копирует это слово в адрес042перед передачей управления) и битовую карту занятых блоков. -
Вендор
uknc(config.sub) — алиас дляpdp11-uknc-aout; при сборке GCC для этого вендора автоматически включается-mbm2(проц 1801ВМ2), если пользователь не указал-mbm1/-mbm2явно. -
Система
rt11(config.sub,gas/configure.tgt,bfd/config.bfd,ld/configure.tgt) — отдельная (отaout) система в триплете (pdp11-uknc-rt11). Форматsav-pdp11компилируется и виден только для неё (через-DBFD_SUPPORTS_SAV_PDP11вtarg_cflags);aoutостаётся побайтово ванильным binutils. -
Встроенный линкер-скрипт (
ld/emulparams/pdp11rt11.sh,ld/scripttempl/pdp11rt11.sc) — эмуляцияpdp11rt11со скриптом, зашитым вld, так что-Tбольше не требуется. -
pdp11rt11sav— эмуляция по умолчанию для rt11 —ldдля этой системы сразу производит.sav. Технически линковка всё равно идёт черезa.out-pdp11(у него есть собственный, проверенный код разрешения релокаций —pdp11_aout_link_input_section), а конвертация в SAV происходит уже после того, как файл полностью записан на диск, через новый необязательный хук эмуляцииld—after_close_output(ld/ldemul.h/.c,ld/emultempl/emulation.em, один вызов вld/ldmain.c). Так получилось после того, как выяснилось, что прямая линковка вsav-pdp11через generic BFD final-link даёт неверные релокации (например, для строковых констант). -
pdp11rt11rel— альтернативный формат вывода: родной relocatable-объект RT-11 (ld/emultempl/pdp11rt11rel.em,ld/emulparams/pdp11rt11rel.sh) — вместоa.out-pdp11пишет собственный формат RT-11 "Relocatable Object Language" (блоки GSD/TXT/RLD/ENDMOD, имена в RADIX-50), описанный в RT-11 Volume and File Formats Manual — то, что понимает штатный линкер RT-11. Устройство то же, что уpdp11rt11sav: линковка идёт через-rвa.out-pdp11(тот же проверенныйpdp11_aout_link_input_section), а готовый файл затем перечитывается обычными вызовами BFD (bfd_canonicalize_symtab/reloc) и переупаковывается в формат RT-11 — без отдельного BFD-таргета в обе стороны. Нужна именно-r:pdp11-uknc-rt11-gcc -c -O2 foo.c -o foo.o pdp11-uknc-rt11-ld -r -m pdp11rt11rel foo.o -o foo.rel
Символы длиннее 6 символов и непредставимые в RADIX-50 (например,
_в начале имени, которое добавляет сам компилятор) усекаются/заменяются на.— ограничение самого формата 1970-х, а не этой реализации.
- Поддержка процессоров 1801ВМ1/1801ВМ2 (
-mbm1/-mbm2) — Советские клоны PDP-11, отличия в системе команд от штатных моделей DEC. - Исправление кодогенерации и использование инструкции
SOBдля организации циклов сдвига — по мелочи улучшают качество генерируемого кода под эти процессоры. - Исправление реального бага компилятора: 32×32-битное знаковое
умножение (
mulhisi3) компилировалось неверно (ошибка в reload/LRA) — найдено и исправлено на реальном тестовом коде. -fstack-usage/-Wstack-usage— бэкенд pdp11 раньше молча игнорировал эти опции; теперь размер стека кадра репортится, как и на других таргетах.- Система
rt11в триплете (gcc/config.gcc) —pdp11-uknc-rt11зарегистрирован как отдельная (отpdp11-uknc-aout) система; задел под будущий порт newlib (у негоconfigure.hostвыбираетlibc/sys/<os>по системе триплета, аaoutдля этого не подходит — это формат объектных файлов, а не среда выполнения). STARTFILE_SPECдля rt11 (gcc/config/pdp11/pdp11.h,libgcc/config.host,libgcc/config/pdp11/{t-pdp11rt11,crt0rt.s, parse_args.c}) — обычная линковка дляpdp11-uknc-rt11работает без-nostartfilesи ручной линковкиcrt0rt.o:crt0rt.o/parse_args.oсобираются как часть сборки libgcc (по образцуlibgcc/config/nds32/t-nds32) и подключаются автоматически.crt0rt.sберёт SP из заголовка SAV-файла и строитargc/argvиз области командной строки, которую RT-11 сама заполняет при запуске программы (argv[0]— пустая строка-плейсхолдер: RT-11 никогда не сообщает программе её собственное имя).pdp11-uknc-aoutэту особенность не получает и продолжает использовать штатныйSTARTFILE_SPECGCC.-msoft-floatпо умолчанию для вендораuknc(gcc/common/config/pdp11/pdp11-common.cc) — настоящее железо УКНЦ не имеет сопроцессора плавающей точки, а штатный дефолт pdp11-таргета (TARGET_DEFAULT_TARGET_FLAGS) рассчитан на 11/45 с аппаратным FPU, из-за чего-msoft-floatраньше приходилось указывать вручную в каждой сборке. Теперь это дефолт для вендораuknc(иrt11, иaout);-mfpuпо-прежнему работает как явное переопределение, а для остальных pdp11-таргетов поведение не меняется.- Исправление второго реального бага компилятора: ICE в проходе
cprop_hardreg(internal compiler error: in extract_constrain_insn, at recog.cc:2783) на обычном коде видаfor (;; --a) { if (*a) break; *a = 1; }. Причина —cbranch<mode>4вpdp11.mdделит один паттерн на все режимы QI/HI/SI/DI через слишком широкий предикатgeneral_operand, хотя широкие (SI/DI) сравнения эмулируютсяcmpsi/cmpdiчерез раздельное чтение адреса дважды (по слову) — с автоинкрементной/автодекрементной адресацией это в принципе небезопасно, иcmpsi/cmpdiсами это запрещают через свои ограничения, ноcbranch<mode>4пропускал такую адресацию до проходаauto_inc_dec, который сворачивал декремент прямо в адрес сравнения. Исправлено новым предикатомcmp_operand(gcc/config/pdp11/predicates.md), который отклоняет side-effect-адреса именно для SImode/DImode — однословные (HI/QI) сравнения, где такая адресация легитимна, не затронуты.
Минимальный порт newlib для pdp11-uknc-rt11 (форк
wdigger/sourceware-mirror-newlib-cygwin,
ветка rt11-port) — даёт обычный printf/malloc/atoi/файловый I/O вместо
того, чтобы каждый пример писал свою крошечную libc с нуля.
libc/sys/rt11/syscalls.c—_read/_write/_open/_close/_lseek/_fstat/_isatty/_sbrk/_exit, плюс ENOSYS-заглушки для_kill/_getpid/_link/_unlink/_stat. Консольный ввод-вывод (fd 0/1/2) идёт через RT-11's.TTYIN/.TTYOUT(EMT), значение явно закреплено за регистром R0 черезregister ... asm("r0")— обычного ограничения"r"недостаточно, EMT читает/пишет именно R0._sbrkрастит кучу от_end(символ линкера) вверх, с ограничением по текущему SP и запасом. Стартовый код (crt0rt.o/parse_args.o) сюда не входит — он приходит из GCC'sSTARTFILE_SPEC(см. раздел GCC выше), поэтомуconfigure.hostставитhave_crt0="no"для этой системы.configure.host:sys_dir=rt11дляpdp11-uknc-rt11; отдельная веткаpdp11*в CPU-диспетчере не даёт провалиться в дефолтный кейс, который ставит-DMISSING_SYSCALL_NAMES(это подменило бы_read/_write/и т.д. на голые имена без подчёркивания, которых этот порт не определяет — используется обычная конвенция с подчёркиванием, как у d10v).- Флаги сборки (
build_gcc_uknc.sh):--enable-newlib-nano-malloc,--enable-newlib-nano-formatted-io,--disable-newlib-wide-orient— компактныеmalloc/printfи отключение поддержкиwchar_tв stdio (она здесь не нужна). Без этогоprintfв простой программе легко перевешивает за 60KB — критично на 64KB адресном пространстве PDP-11. configure/Makefile.inпересобираютсяautoreconfс закреплёнными версиями autoconf 2.69 и automake 1.15.1 (build_gcc_uknc.shсам их скачивает и собирает) — именно теми, которыми newlib сама была сгенерирована. Более новый automake меняет схему префиксов имён объектных файлов, из-за чего собственный жёстко прописанный списокMATHOBJS_IN_LIBCвnewlib/Makefile.amперестаёт совпадать с реальными именами, иlibc.aне собирается («no entry ... in archive» приar).- Два реальных бага компилятора нашлись и исправлены при разработке
порта (см. раздел GCC выше, не workaround — реальные патчи в
wdigger/gcc): reload-ICE на объединенииdoubleс меньшим целым в union (обходится-msoft-float, теперь дефолт дляuknc) иcprop_hardreg-ICE на 32-битном сравнении с автодекрементом (libc/stdlib/gdtoa-hexnan.c— единственное место в newlib, что на это напоролось; исправлено в бэкенде новым предикатомcmp_operand).
Ограничения файлового ввода-вывода (libc/sys/rt11/syscalls.c):
Ограничения, продиктованные системой:
- Размер файла известен только с точностью до блока (512 байт) — для файла, который эта сессия не писала (чтение, либо переоткрытие после close).
- RADIX-50 имя: 3/6/3 символа (устройство/имя/расширение) — жёсткий
лимит самой RT-11; если имя длиннее, выдаётся ошибка
ENAMETOOLONG. - Монитор RT-11 SJ синхронный —
.LOOKUP/.ENTER/.READW/.WRITWблокируют вызывающую программу, никакого queued/асинхронного I/O.
Осознанно принятые ограничения:
- Работают только уже резидентные устройства (
SY:,TT:,MQ:,UB:,PI:) —.FETCHне вызывается. O_APPENDпобайтово точен только в рамках одной непрерывно открытой сессии fd; после close+reopen — точность до блока, выделение не растёт за пределы исходного.LOOKUP.
Открытая проблема:
- Смешивание
puts()/printfс реальным файловым I/O иногда приводит к тихому обрыву программы; локализовано до места (послеwrite(), не породившего ни одного.WRITW), причина предположительно в самом мониторе RT-11 или в эмуляции, не в GCC.
- Бэкенд pdp11 не поддерживает global constructors (
__attribute__ ((constructor)), а также конструкторы статических C++ объектов — но C++ и так не включён, тулчейн собирается с--enable-languages=c). При попытке их использовать компилятор выдаётsorry, unimplemented: global constructors not supported on this target. Практическое следствие —-fstack-protectorне работает: кодогенерация проверки канарейки вставляется как обычно, но линковка падает сundefined reference to __stack_chk_guard/__stack_chk_fail, а не понятной ошибкой на этапе компиляции (newlib не может собрать код, который инициализирует__stack_chk_guard, — именно из-за отсутствия constructors). Это старое ограничение бэкенда, не что-то, привнесённое этим проектом; чинить не планируется, поскольку ничего в проекте пока не требует ни-fstack-protector, ни constructors как таковых. - Бэкенд pdp11 не поддерживает weak-символы — компилятор явно
предупреждает:
warning: weak declaration of 'foo' not supported(проверено на изолированном минимальном примере). Практическое следствие: newlib's_printf_floatспециально объявленweak, чтобыdtoa/mprec(арифметика произвольной точности для форматирования float, ~10KB кода) линковались только при реальном использовании%f/%e/%g— но на этом таргете weak-ссылка становится обычной сильной, иdtoa/mprecподтягиваются при любом вызовеprintf, даже если в форматной строке только%d/%ld. Для программ, которым важен размер (см.spigotвexamples), единственный обходной путь — не использоватьprintfвовсе (свойltoa/itoa+fputs/putchar). Тоже старое ограничение формата/бэкенда, не баг newlib-порта. - Бэкенд pdp11 не поддерживает
-ffunction-sections/-fdata-sections— компилятор явно предупреждает:warning: '-ffunction-sections' not supported for this target(и то же для-fdata-sections), флаги молча игнорируются. Судя по всему, из-за формата a.out — фиксированный набор секций.text/.data/.bss, а не произвольные именованные секции, нужные--gc-sections, чтобы выбрасывать отдельные неиспользуемые функции/переменные. Из-за этого-Wl,--gc-sectionsвexamples/*/Makefileреально помогает только на уровне целых объектных файлов библиотек (что линковщик и так делает при линковке из.a), а не внутри пользовательского.c-файла.