Собираю здесь все что связано с разработкой на C, предпочтительно с использованием кросскомпиляции GCC
-
В папке
gccлежит скриптbuild_gcc_uknc.shс помощью которого можно собрать тулчейн для сборки проектов для УКНЦ. -
В папке
debuggerлежит скриптbuild_debugger_uknc.sh— выкачивает веткуgdbserverизwdigger/ukncbtl-debuggerвsrc, собирает и кладётukncbtldebugвbin, рядом с ним прошивку изrom. Это эмулятор с сервером протокола gdb и без собственного отладчика: машину даёт он, отлаживает gdb. В нём здесь всё и проверяется. -
В папке
romлежитuknc_rom.bin— прошивка самой машины (32256 байт). Эмулятор ищет её в текущем каталоге при запуске, а доска DejaGnu изgcc/dejagnu— рядом с бинарником эмулятора, куда её и кладёт скрипт из пункта 2. Рядомuknc_rom_autoboot.bin— та же прошивка, но без меню загрузки: сразу диск, привод 0. Делается из первой скриптомmake_autoboot_rom.py, там же разобрано, что именно правится и почему приходится править ещё одно слово (прошивка считает свою контрольную сумму). До приглашения RT-11 такая машина доходит примерно за 700 кадров и без единого нажатия, против 2200 кадров и двух нажатий со штатной. -
В папке
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при линковке. Как собрать и загрузить код для самого ПП — см. раздел ниже.
Тулчейн собирается на трёх системах сам — GitHub Actions,
.github/workflows/build.yml. macOS и Windows отдают его готовым: .dmg и
установщик. Linux собирает всё то же самое, но в артефакт кладёт сам
репозиторий: собрать здесь — час по скрипту, который в нём и лежит, а бинарь,
слинкованный с тем, что оказалось на раннере, на другой машине скорее обуза.
Там сборка — это проверка, а не продукт.
Не по каждому пушу: час с лишним на платформу, так что запуск руками из вкладки
Actions (можно попросить одну платформу) или по тегу. Каждая сборка перед
упаковкой собирает examples/hello, гоняет его на машине и проверяет, что он
напечатал своё.
Версия проекта лежит в файле VERSION — одна строка, больше нигде её
дублировать не надо. Выпуск — это тег v<версия>: по нему собираются все три
платформы и создаётся релиз с пакетами и SHA256SUMS. Тег, не совпадающий с
файлом, сборку останавливает — два ответа на один вопрос на один больше, чем
нужно. Сборка по тегу так и называется, всякая другая
дописывает к ней коммит (0.1.0+695293e), чтобы пакет со случайного прогона
нельзя было спутать с выпущенным. Что внутри пакета — версии binutils, gcc,
newlib и коммит, из которого он собран, — пишется в VERSION.txt рядом с
распакованным деревом.
Благодаря 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.
Объектные файлы — ELF (elf32-pdp11). ELF-бэкенда для pdp11 нет ни в
upstream binutils, ни где-либо ещё: EM_PDP11 зарезервирован в спецификации,
и на этом всё, — поэтому bfd/elf32-pdp11.c со своими номерами релокаций
написан здесь. Прецедент 16-битной машины в ELF32 — msp430.
Практическая разница против a.out в том, что секцию можно назвать. Отсюда
работают -ffunction-sections/-fdata-sections, а с ними -Wl,--gc-sections,
которому есть что выбрасывать; отсюда же берётся место для отладочной
информации и для -flto. На машине с 64K адресного пространства это главное,
что даёт формат: линковщик выбрасывает не только неиспользуемые модули
библиотеки, но и отдельные функции внутри втянутых модулей.
newlib и libgcc собираются с -ffunction-sections -fdata-sections
(CFLAGS_FOR_TARGET в скрипте), примеры и тесты — через SECTION_FLAGS в
своих Makefile.
Четыре относящиеся к формату вещи, о которых стоит знать:
- Релокации свои, номера назначены здесь (
include/elf/pdp11.hв форке binutils); читать эти файлы, кроме этого же тулчейна, некому. - PC-относительный операнд машина читает от слова, следующего за
смещением, поэтому
R_PDP11_PCREL16— это обычноеS + A - P, а двойку несёт адденд, ровно какR_386_PC32несёт четвёрку. - Четырёхбайтовое значение в ELF-файле — обычное little-endian, а не
«старшее слово вперёд», как машина хранит
long. Это разные вещи: компилятор выпускаетlongдвумя.word, а.longв ELF значит то, что под ним понимает формат файла. - Секции, которые машина не грузит, не дополняются до чётной длины: у
отладочных секций и у потоков
-fltoдлина — часть содержания.
.sav получается из ELF постобработкой внутри ld (см. раздел binutils),
так что промежуточный ELF по умолчанию не сохраняется. Нужен — линкуйте с
-Wl,-m,pdp11rt11.
-g производит DWARF. Читается штатными средствами:
pdp11-uknc-rt11-readelf --debug-dump=info prog.o
pdp11-uknc-rt11-objdump -S prog.elf # дизассемблер вперемешку с исходником
pdp11-uknc-rt11-addr2line -f -e prog.elf 0x27aИ ничего не стоит по памяти: .sav с -g и без совпадает байт в байт —
отладочные секции не ALLOC и в образ не попадают.
Пошаговая отладка по исходнику работает — в pdp11-uknc-rt11-gdb. Линкуется
ELF рядом с .sav:
pdp11-uknc-rt11-gcc -g -Wl,-m,pdp11rt11 -o prog.elf prog.cАдреса в нём — те самые, по которым программа исполняется: RT-11 грузит .sav
по абсолютному 001000, куда линкер-скрипт и кладёт .text, так что никакой
релокации не нужно.
Цели pdp11 в gdb никогда не было — верхний configure прямо перечислял pdp11
среди целей, для которых gdb не собирается, — так что описание машины
(gdb/pdp11-tdep.c) в форке своё: восемь 16-разрядных регистров, слово вместо
int и указателя, двухсловный long, форматы плавающих DEC, BPT как точка
останова, возврат в R0 (или в R0 и R1, старшая половина в R0).
Машину подаёт ukncbtldebug из папки debugger — эмулятор с сервером
протокола gdb и без собственного отладчика:
$ ukncbtldebug --disk1 rt11.dsk --port 2345
Listening on localhost:2345 for CPU
Экрана у него по умолчанию нет — чаще всего его запускает uknc-run или
редактор, и окно там только мешает. --screen открывает окно с экраном
машины и заодно придерживает её на положенных 50 кадрах в секунду, пока
программа работает под gdb; без этого машина идёт настолько быстро, насколько
может хост, и смотреть на это бессмысленно. Окно можно тянуть за угол, а
закрытие окна останавливает машину — другого способа сказать «хватит» у того,
кто на неё смотрит, нет, и gdb видит обрыв соединения. Рисует SDL3
(brew install sdl3); собранный без неё эмулятор отличается только
отсутствием этой опции.
(gdb) target extended-remote :2345
(gdb) set remote exec-file T
(gdb) break g2.c:8
(gdb) run
Breakpoint 1, sum (n=10) at g2.c:8
(gdb) print total
$1 = 55
(gdb) bt
#0 sum (n=10) at g2.c:8
#1 0x000002aa in main () at g2.c:13
(gdb) continue
55 210
[Inferior 1 (Remote target) exited normally]
Регистры, память, точки останова, шаг, кадры, аргументы и локальные — всё
работает; последние три держатся на call frame information, то есть требуют
-g. Раскрутка обрывается на main: в стартовом коде CFI нет.
Программы здесь грузит RT-11, а не gdb, поэтому run означает то же, что
сделал бы человек: сброс, ожидание системы, набор R ИМЯ — и остановка на
точке входа. Нужна автозагрузочная прошивка: отвечать на меню штатной некому.
Вывод программы попадает в терминал gdb, и по .EXIT gdb узнаёт, что она
кончилась. Второй путь — load: gdb пишет программу прямо в память и
выставляет точку входа, и на образе диска её вообще может не быть.
Одно не работает: 32-разрядное целое печатается с переставленными половинами. Машина хранит старшее слово длинного по младшему адресу, а порядок байтов у gdb двузначный и такого не выражает. Плавающие не затронуты — их формат описан побитово и читается верно.
Машину включают-выключают, на ней печатают, на неё смотрят — пакета протокола
ни для чего из этого нет, и gdb на такой случай передаёт строку как есть
(monitor):
(gdb) monitor keys R DEMO
(gdb) monitor frames 300
(gdb) monitor screenshot /tmp/demo.bmp
Ещё есть monitor reset, monitor disk N ФАЙЛ (сменить дискету на ходу) и
monitor screen on|off — открыть окно с экраном посреди сеанса или закрыть.
monitor help перечисляет всё.
frames здесь не украшение: между остановками машина не работает, и
напечатанное ею ещё не сделано — это способ дать времени пройти, когда
продолжать нечего. Снимок экрана рисуется из памяти машины, а не с окна, так что
делается и без окна, и в сборке без SDL3 вовсе: программу, которая рисует,
иначе из скрипта не проверить.
Процессоры машины поданы gdb как два процесса: 1 — ЦПУ, 2 — ПП, в каждом по
одному потоку (p1.1 и p2.1 на проводе). Процессами, а не потоками, потому
что поток делит с остальными и память, и таблицу символов, а эти двое не делят
ничего: у процесса своё адресное пространство и свои символы — ровно то, что
здесь и есть. info inferiors показывает оба, inferior 2 переключает,
info threads показывает оба сразу, и дальше регистры, память и точки останова
относятся к выбранному: x/4xh 0x200 из разных процессов читает разное.
Просит об этом сам gdb (расширение multiprocess, о котором договариваются в
qSupported), и по умолчанию просит; клиент, который не попросил, получает те
же два под теми же номерами, но потоками одного процесса — так было сначала,
и так остаётся запасным ответом.
Одно из этого следует: continue у gdb по умолчанию продолжает только тот
процесс, что сейчас выбран, а машина так не умеет — работают всегда оба. Лечит
set schedule-multiple on, и ppu.gdb (ниже) его выставляет; без него точка
останова, до которой дошёл второй процессор, приходит к gdb как SIGTRAP
ниоткуда — тот процесс, по его сведениям, и не работал.
Символов у ПП при этом нет: gdb читает один исполняемый файл, а это программа
ЦПУ. Модуль .PPU ему не годится — это объектный файл RT-11, а не ELF, — да и
адрес, по которому он окажется в памяти ПП, выбирается во время работы. Поэтому
ПП-сторона линкуется вторым заходом ещё и как ELF:
pdp11-uknc-rt11-ld-ppu --elf -o gfppu.ppu.elf gfourppu.o
Тот же код и та же раскладка, но с символами и DWARF, которых объектному модулю
хранить негде. Накладывает их на работающий модуль libs/libppu/ppu.gdb
(ставится в тулчейн рядом с ppu.ld):
(gdb) source .../pdp11-uknc-rt11/lib/ppu.gdb
(gdb) inferior 2
(gdb) break ppu_main
Thread 2.1 hit Breakpoint 2, ppu_main () at gfourppu.c:325
Адрес он берёт из ppuc_code_base — переменной, куда ppuc_run() записывает,
что именно оно запустило (ppu_client.h), — и делает это сам на первом же
останове после старта ПП; ppu-symbols FILE то же самое по требованию,
ppu-symbols-auto off отключает. Примеры с ПП-стороной собирают такой .elf
рядом с .ppu, а расширение для VS Code подключает скрипт само.
Символы при этом кладутся в адресное пространство ПП и там и остаются: в
bt по ПП нет ни одного имени из программы ЦПУ, хотя модуль почти всегда
попадает в те же адреса, что занимает её образ (монитор ПП раздаёт память с
026450). Это и есть причина делить процессоры на процессы: пока они были
потоками с одной таблицей на двоих, для такого адреса оказывались два описания,
и выигрывало то, чью отладочную информацию gdb успел прочитать.
(gdb) info threads
Id Target Id Frame
* 1.1 Thread 1.1 (CPU (central processor)) clear_screen () at gfour.c:244
2.1 Thread 2.1 (PPU (peripheral processor)) ppu_main () at gfourppu.c:345
В VS Code это тоже видно: cpptools показывает оба потока, и кадр ПП подписан его собственными именами.
В .vscode лежит готовая обвязка: сборка, запуск и отладка на машине по F5.
Нужно одно расширение — ms-vscode.cpptools; сеанс ведёт оно, через
pdp11-uknc-rt11-gdb.
Устроено так. debugServerPath сам поднимает ukncbtldebug с ключом --boot,
дожидается строки Listening on localhost и подключается. Программа попадает
в машину командой load — gdb пишет её прямо в память, — поэтому на образе
диска её быть не должно: правка, F5, и уже выполняется новый код. Образ нужен
только чтобы поднялась RT-11.
Отлаживается ELF, а не .sav: символы и DWARF плоскому образу хранить негде.
У каждого примера для этого своя цель:
make hello.elfКод в .sav и в .elf один и тот же — -g добавлен во все примеры, и на
образ это не влияет: отладочные секции не ALLOC, .sav выходит байт в байт
прежним.
Задачи (Terminal → Run Task): собрать образ диска, собрать ELF, запустить на
машине через uknc-run, очистить, собрать тулчейн, собрать эмулятор. Ошибки
компилятора попадают в панель Problems.
IntelliSense настроен через compilerPath, так что заголовки и определения
берутся у самого компилятора, включая __SIZEOF_INT__ 2. Но режима pdp11
у расширения нет, поэтому подсказка sizeof может показать размер не той
машины — верный ответ всегда у компилятора. По той же причине в консоли
отладки будет строка Debuggee TargetArchitecture not detected, assuming x86_64 — расширение не знает такой архитектуры, на работу это не влияет.
Проверено на examples/hello прогоном через сам отладочный адаптер
расширения: машина поднимается, программа доставляется (.text, 9312 байт),
точка останова подтверждается и срабатывает, стек показывает main() hello.c:4,
вывод программы приходит в консоль, выход сообщается как код 0.
-flto работает: binutils собираются с --enable-plugins, и ld загружает
liblto_plugin.so из GCC.
Выигрыша на этом проекте он не даёт: каждый пример — одна единица трансляции,
а межмодульная оптимизация только между ними и бывает. Сборка newlib и libgcc
с -flto -ffat-lto-objects даёт 5–12%, но три программы из тринадцати после
неё не линкуются: float и math — на внутренностях fp-bit (undefined reference to __pack_d), lseek — на errno из newlib. Символы при этом в
архиве есть и в индексе есть; расходится разрешение символов между членами
архива в самом плагине LTO. Поэтому в скрипте сборки библиотека остаётся без
LTO.
К модулям ППУ -flto неприменим: релокируемая линковка промежуточного
представления валится с «failed to add object-only section».
Программа для ПП (периферийного процессора) — это не обычный .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.
График синуса, посчитанного библиотекой — examples/sin (бегущая
синусоида на весь экран, до нажатия любой клавиши). Кривую считает
стандартная библиотека, а не таблица, набитая руками, — тем он и интересен
на цели, где формат плавающих не IEEE (см. раздел про libm). Значения
считаются один раз, в таблицу на один период, и дальше
волна движется сдвигом по этой таблице: один sinf() занимает заметную
долю кадра, так что 320 вызовов на кадр не оставили бы времени ни на что
другое.
Две вещи в нём поучительны помимо самого синуса. Во-первых, одинарная
точность выбрана не из вкуса: с sin() программа весит около 50 КБ и не
загружается вовсе, с sinf() — 33 КБ. Во-вторых, из-за этих 33 КБ не
остаётся места под собственный буфер экрана на 23 КБ, как в gfour,
поэтому пиксели пишутся прямо в консольную экранную память RT-11 (смещение
плана 0100000) через оконные регистры 0176640/0176642 — тот самый способ,
с которого gfour начинал и от которого ушёл. ПП-сторона при этом взята у
gfour без изменений, ей всё равно, куда указывать.
В gcc/dejagnu лежит доска DejaGnu, которая гоняет тесты на настоящей
машине, эмулируемой ukncbtl:
cd gcc/dejagnu && make # собрать uknc_exit.o
cd ../build/gcc
DEJAGNU=<путь к проекту>/gcc/dejagnu/site.exp make check-gcc RUNTESTFLAGS="--target_board=uknc-sim execute.exp"Устройство простое. uknc-run кладёт программу на копию образа диска
RT-11 под именем T.SAV и просит gdb её запустить: эмулятор поднимается
только как сервер протокола gdb (gdbserver), а сброс, ожидание системы
и набор R T — это то, что делает run на той стороне. Прогон занимает
около секунды.
Через gdb, а не через консоль эмулятора, по трём причинам. Вывод приходит
потоком байт, а не считывается с 80-колоночного экрана, с которого длинный
вывод уехал бы. Машина сама сообщает, что программа кончилась — .EXIT
это вызов операционной системы, сервер за ним смотрит, — поэтому быстрая
программа завершает прогон сразу, а не досиживает отведённые кадры. И
зависшую программу ограничивает счётчик кадров, после которого сервер
говорит gdb, что она снята.
RT-11 не умеет сообщать код возврата, поэтому его печатает сама программа:
uknc_exit.o подменяет _exit строкой вида *** EXIT code N, ровно в том
виде, в каком её ждёт DejaGnu от любой машины без кода возврата, а доска
подставляет этот объект в каждую сборку. Заодно там же обёртка вокруг
main: стартовый код завершает программу через .EXIT сам, не вызывая
exit(), так что обычный возврат из main иначе прошёл бы мимо — и мимо
сброса буферов вывода.
Две мелочи, на которые стоит обратить внимание. Эмулятор печатает экран
через широкий поток вывода, который молча замолкает, если локаль не может
представить кириллицу, которую RT-11 выводит на экран; тестовые обвязки
работают в локали C, поэтому uknc-run сам выбирает подходящую UTF-8. И код
возврата надо отдавать именно строкой в выводе, а не статусом процесса:
DejaGnu предпочитает её.
Чего эта доска не делает: не запускает тесты параллельно быстрее, чем позволяет эмулятор, и не спасает от того, что многие тесты просто не влезают в 64 КБ или в 16-битный объектный файл. Такие честно падают.
-
sav-pdp11(bfd/sav-pdp11.c) — write-only BFD-таргет, который пишет исполняемые файлы RT-11 SAV. Заголовок SAV содержит стартовый адрес, начальный SP (адрес вершины пользовательской памяти, а не точки входа — RT-11 копирует это слово в адрес042перед передачей управления) и битовую карту занятых блоков. -
elf32-pdp11(bfd/elf32-pdp11.c,include/elf/pdp11.h) — формат объектных файлов, см. раздел выше. -
Вендор
uknc(config.sub) — при сборке GCC для этого вендора автоматически включается-mbm2(проц 1801ВМ2), если пользователь не указал-mbm1/-mbm2явно. -
Система
rt11(config.sub,gas/configure.tgt,bfd/config.bfd,ld/configure.tgt) — отдельная система в триплете (pdp11-uknc-rt11), под которой и живёт всё перечисленное;pdp11-*-aoutостаётся побайтово ванильным binutils. -
Встроенный линкер-скрипт (
ld/emulparams/pdp11rt11.sh,ld/scripttempl/pdp11rt11.sc) — эмуляцияpdp11rt11со скриптом, зашитым вld, так что-Tне требуется. -
pdp11rt11sav— эмуляция по умолчанию для rt11 —ldсразу производит.sav. Линковка идёт в обычныйelf32-pdp11, а конвертация в SAV происходит после того, как файл полностью записан на диск, через хук эмуляцииafter_close_output(ld/ldemul.h/.c,ld/emultempl/emulation.em, один вызов вld/ldmain.c). Прямая линковка вsav-pdp11не годится: у write-only формата нет своего кода разрешения релокаций, а generic BFD final-link даёт неверные значения. -
pdp11rt11rel— родной relocatable-объект RT-11 (ld/emultempl/pdp11rt11rel.em,ld/emulparams/pdp11rt11rel.sh) — формат RT-11 "Relocatable Object Language" (блоки GSD/TXT/RLD/ENDMOD, имена в RADIX-50), описанный в RT-11 Volume and File Formats Manual — то, что понимает штатный линкер RT-11 и что загружаетlibppu. Устройство то же, что уpdp11rt11sav: линковка-rвelf32-pdp11, затем файл перечитывается обычными вызовами 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-х, а не этой реализации. -
Ассемблер проверяет дальность перехода. Смещение перехода — это счёт слов в самом слове команды, поэтому не помещающееся значение переписало бы биты кода операции и дало бы другую, совершенно законную команду. Вместо этого выдаётся ошибка с указанием, насколько промахнулись. Принимается вперёд до +127 слов, назад до -128; у
sobполе шестибитное беззнаковое, 0…63; переход на нечётный адрес тоже отвергается.
- Поддержка процессоров 1801ВМ1/1801ВМ2 (
-mbm1/-mbm2) — советские клоны PDP-11, отличия в системе команд от штатных моделей DEC. Циклы сдвига используют командуSOB. -fstack-usage/-Wstack-usageработают, как и на других целях.- Система
rt11в триплете (gcc/config.gcc) —pdp11-uknc-rt11зарегистрирован как отдельная система: newlib'sconfigure.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}) — обычная линковка работает без-nostartfilesи ручной линковкиcrt0rt.o:crt0rt.o/parse_args.oсобираются как часть libgcc и подключаются автоматически.crt0rt.sберёт SP из заголовка SAV-файла и строитargc/argvиз области командной строки, которую RT-11 сама заполняет при запуске программы (argv[0]— пустая строка-плейсхолдер: RT-11 никогда не сообщает программе её собственное имя).-msoft-floatпо умолчанию для вендораuknc(gcc/common/config/pdp11/pdp11-common.cc) — настоящее железо УКНЦ не имеет сопроцессора плавающей точки.-mfpuработает как явное переопределение; для остальных pdp11-целей поведение штатное.- Плавающая точка в формате DEC.
floatиdoubleздесь не IEEE, а DEC (pdp11_f_format/pdp11_d_formatвpdp11.cc), и библиотека программной арифметики это учитывает:libgcc/fp-bit.cполучает параметры форматов DEC и признак отсутствия бесконечностей, NaN и денормализованных чисел (крайние значения порядка здесь обычные числа, а переполнение упирается в наибольшее конечное значение); более компактные преобразования и сравнения одинарной точности лежат вlibgcc/config/pdp11/fpconv-*.c. Проверяется запуском на машине сверкой с константами, которые свернул сам компилятор, — см.tests/float. - Команды FIS (
-mfis), по умолчанию включены для вендораuknc— FIS это самое дешёвое плавающее расширение линейки PDP-11 (KE11-F на 11/35 и 11/40, KEV11 на 11/03): четыре команды, только одинарная точность, операнды в памяти через регистр-указатель. Аппаратного FIS у 1801ВМ2 нет, но системное ПО УКНЦ эмулирует эти коды в обработчике прерывания в режиме HALT, и даже через прерывание это в 2.8 раза быстрее вызова библиотеки (замер профилировщиком тактов: 4602 такта на сложение против 13038).-mno-fisвыключает их для машины, монитор которой этих кодов не обслуживает, а-mfpuперебивает умолчание. Два следствия, о которых стоит знать: деление на ноль с FIS завершает программу через прерывание, а без FIS возвращает очень большое число; и три функции вlibgcc(__fixunssfsi,__mulsc3,__powisf2) содержат эти команды, так что-mno-fisв своей программе не делает двоичный файл полностью свободным от них. - Вложенные функции работают, включая те, чей адрес утекает и для которых строится трамплин.
- Weak-символы работают: неопределённая слабая ссылка разрешается в ноль,
а не роняет линковку, и сильное определение перекрывает слабое. В
a.outэтого не могло быть, иSUPPORTS_WEAKбыл нулём.
Математические функции newlib — это fdlibm, и они читают порядок и мантиссу
двойного числа прямо из битов, рассчитывая на раскладку IEEE. Здесь раскладка
другая (DEC), но все эти обращения идут через восемь макросов в одном файле
(newlib/libm/common/fdlibm.h), а с самим значением алгоритмы делают
арифметику, которую компилятор считает верно. Поэтому макросы выдают
представление числа DEC в виде IEEE и принимают его обратно, а сами алгоритмы
не тронуты ни на строку.
Представление точно на всём диапазоне этого формата, который уже, чем у IEEE, в обеих точностях, так что на выходе ничего не обрезается и привычные константы означают ровно то же самое. Три вещи поездку не переживают, все по краям: двойное число теряет младшие 3 бита мантиссы, 55 не помещаются в 52, поэтому результаты верны до 53 бит IEEE, а не до 56 бит этого формата; число, собранное через макросы с порядком вне диапазона, возвращается как наибольшее конечное или как ноль, потому что уходить в бесконечность здесь некуда; и два младших порядка одинарной точности, лежащие ниже наименьшего нормального у IEEE, читаются как ноль.
Проверяется запуском на машине сверкой с константами компилятора: sqrt,
exp, log, log10, pow, sin, cos, tan, atan, atan2, asin,
acos, fmod, hypot, sinh, cosh, tanh, expm1, floor, ceil,
fabs, ldexp, frexp, modf — см. tests/math.
Ограничение тут не корректность, а размер: libm огромна против 64 КБ
адресного пространства. Проверки в tests/math разбиты на пять программ,
синус и косинус в разных, потому что вместе не загружаются, а tan не вошёл
вовсе — с обвязкой он весит 47 КБ и не запускается.
Минимальный порт 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"для этой системы.- Перевод строки. RT-11's
.TTYOUT/.TTYINпередают байты как есть, без слоя драйвера, а соглашение RT-11 — CR LF. Порт делает перевод сам:_read()отображает CR в'\n',_write()выдаёт CR перед LF на fd 1/2. Файлы пишутся побайтово как есть, без различения текстового и двоичного режима. 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).
Ограничения файлового ввода-вывода (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). %f/%e/%gвprintfнужно затребовать явно и они приблизительны. По умолчанию форматирование плавающих в программу не линкуется: newlib объявляет_printf_floatслабым и сравнивает сNULL, неразрешённая слабая ссылка даёт ноль, иprintfпропускает преобразование — печатает всё, кроме самого числа. Затребовать реализацию можно штатным для newlib-nano способом,-Wl,-u,__printf_float; это стоит около 14.7 КБ (программа с одним%d— 10528 байт, она же с%f— 25184). Преобразование здесь неdtoa: тот корректно округляет, считает в длинной арифметике и занимает около 19 КБ из 64 КБ адресного пространства плюс кучу подBalloc, которой у программы такого размера уже нет. Вместо него порт масштабирует число таблицей степеней десяти и снимает цифры сверху — каждая цифра стоит одного умножения, и ошибка накапливается: из примерно 16 десятичных цифр формата последние две-три могут отличаться на единицу.%.6fпо умолчанию до этого не достаёт,%.15f— достаёт.