Skip to content

Latest commit

 

History

104 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Электроника МС 0511 (УКНЦ)

разработка на С

GCC кросскомпиляция

Собираю здесь все что связано с разработкой на C, предпочтительно с использованием кросскомпиляции GCC

  1. В папке gcc лежит скрипт build_gcc_uknc.sh с помощью которого можно собрать тулчейн для сборки проектов для УКНЦ.

  2. В папке debugger лежит скрипт build_debugger_uknc.sh — выкачивает ветку gdbserver из wdigger/ukncbtl-debugger в src, собирает и кладёт ukncbtldebug в bin, рядом с ним прошивку из rom. Это эмулятор с сервером протокола gdb и без собственного отладчика: машину даёт он, отлаживает gdb. В нём здесь всё и проверяется.

  3. В папке rom лежит uknc_rom.bin — прошивка самой машины (32256 байт). Эмулятор ищет её в текущем каталоге при запуске, а доска DejaGnu из gcc/dejagnu — рядом с бинарником эмулятора, куда её и кладёт скрипт из пункта 2. Рядом uknc_rom_autoboot.bin — та же прошивка, но без меню загрузки: сразу диск, привод 0. Делается из первой скриптом make_autoboot_rom.py, там же разобрано, что именно правится и почему приходится править ещё одно слово (прошивка считает свою контрольную сумму). До приглашения RT-11 такая машина доходит примерно за 700 кадров и без единого нажатия, против 2200 кадров и двух нажатий со штатной.

  4. В папке examples лежат минимальные примеры приложений, собираемых с помощью тулчейна, получаемого скриптом из пункта 1.

  5. В папке 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-cygwinbuild_gcc_uknc.sh скачивает их автоматически. Ниже — что этот тулчейн умеет сверх ванильных binutils/GCC/newlib.

Формат объектных файлов — ELF

Объектные файлы — 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, так что никакой релокации не нужно.

gdb

Цели 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 показывает оба потока, и кадр ПП подписан его собственными именами.

Visual Studio Code

В .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.

LTO

-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».

Сборка и запуск кода на ПП (libppu)

Программа для ПП (периферийного процессора) — это не обычный .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)

В 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-битный объектный файл. Такие честно падают.

binutils

  • 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 — эмуляция по умолчанию для rt11ld сразу производит .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; переход на нечётный адрес тоже отвергается.

GCC

  • Поддержка процессоров 1801ВМ1/1801ВМ2 (-mbm1/-mbm2) — советские клоны PDP-11, отличия в системе команд от штатных моделей DEC. Циклы сдвига используют команду SOB.
  • -fstack-usage/-Wstack-usage работают, как и на других целях.
  • Система rt11 в триплете (gcc/config.gcc) — pdp11-uknc-rt11 зарегистрирован как отдельная система: newlib's 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}) — обычная линковка работает без -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 был нулём.

libm

Математические функции 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

Минимальный порт 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's STARTFILE_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):

Ограничения, продиктованные системой:

  1. Размер файла известен только с точностью до блока (512 байт) — для файла, который эта сессия не писала (чтение, либо переоткрытие после close).
  2. RADIX-50 имя: 3/6/3 символа (устройство/имя/расширение) — жёсткий лимит самой RT-11; если имя длиннее, выдаётся ошибка ENAMETOOLONG.
  3. Монитор RT-11 SJ синхронный — .LOOKUP/.ENTER/.READW/.WRITW блокируют вызывающую программу, никакого queued/асинхронного I/O.

Осознанно принятые ограничения:

  1. Работают только уже резидентные устройства (SY:, TT:, MQ:, UB:, PI:) — .FETCH не вызывается.
  2. O_APPEND побайтово точен только в рамках одной непрерывно открытой сессии fd; после close+reopen — точность до блока, выделение не растёт за пределы исходного .LOOKUP.

Открытая проблема:

  1. Смешивание 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 — достаёт.

About

No description, website, or topics provided.

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages