LibMCPU

Программный процессор MCPU, объектный формат MELF и инструментарий системного программирования.

LibMCPU – программная реализация процессора MCPU и связанный с ним набор инструментов системного программирования. Проект объединяет исполнитель команд MCPU, assembler, linker, loader/runtime, средства исследования объектных файлов и архивов, собственный объектный формат MELF и целевой system root. Арифметический слой построен поверх LibMPU, поэтому архитектура MCPU изначально рассчитана не только на обычные машинные integer, но и на вещественные и комплексные операции повышенной разрядности.

Обзор

MCPU задуман как самостоятельная вычислительная архитектура, которую можно изучать на том же уровне, что и обычный процессор: у неё есть собственное адресное пространство программы, register file, PSW, инструкции, соглашение о вызовах, системные вызовы, объектные файлы, relocations, статическая и динамическая компоновка, startup protocol и граница между guest program и host supervisor. Отличие состоит в том, что процессор реализован программно и может использовать возможности LibMPU как естественную часть машинной модели.

Текущий публичный выпуск LibMCPU 0.4.2 является первой публичной версией проекта. Машинная часть этого выпуска сохраняет проверенную архитектурную семантику, сформированную до публичного выпуска, а новая публичная ветвь дополнительно оформляет нормальный production build layer: Autotools/Libtool, mcpu-config, pkg-config metadata и libmcpu.m4 для внешних Autoconf-проектов.

Проект не пытается имитировать конкретный существующий CPU и не является оболочкой над host instruction set. MCPU имеет собственные кодировки команд, собственный бинарный ABI и собственную модель адресов. Поэтому один и тот же MELF executable должен одинаково интерпретироваться на разных host systems, если сама LibMCPU и её зависимости доступны на этих системах.

Состав проекта

LibMCPU состоит из двух тесно связанных частей. Первая – собственно software CPU: register file, PSW, архитектурные memory spaces, decoder и executor команд, syscall boundary и механизмы lifecycle. Вторая – toolchain, которая превращает текст assembler в MELF objects, связывает их в executable или shared objects, загружает результат, выполняет relocations и предоставляет инструменты исследования бинарного представления.

Компонент Назначение
libmcpuМашинная модель, decoder, executor и public C interface
mcpu-asAssembler: UTF‑8 source → MELF ET_REL
mcpu-ldFinal linker, partial linker, static и dynamic linking
mcpu-runLoader/runtime, execution supervisor и test bench
mcpu-readelfСтруктурное исследование MELF
mcpu-arДетерминированные UNIX archives с MELF ET_REL members
mcpu-nmИсследование symbol tables
mcpu-objdumpSections, relocations и disassembly
mcpu-stripБезопасное удаление необязательных symbol metadata
mcpu-objcopyMELF-to-MELF structural editing

Все эти программы используют одну архитектурную модель. Например, disassembler в mcpu-objdump вызывает тот же decoder LibMCPU, который используется при исполнении. Это важный принцип: toolchain не должен содержать вторую независимую трактовку opcode или operands, которая со временем могла бы разойтись с реальным executor.

Register File и перекрывающиеся регистры

Register file MCPU представляет собой единый byte-addressable массив размером 512 KiB. Регистровое имя не обозначает отдельную физическую ячейку наподобие rax или r0. Оно задаёт view – ширину и начальный byte address внутри общего register pool. В assembler integer и real views записываются как r<bits>@addr, complex views – как c<bits>@addr.

r8@0x100      bytes [0x100 ... 0x100]
r16@0x100     bytes [0x100 ... 0x101]
r32@0x100     bytes [0x100 ... 0x103]
r64@0x100     bytes [0x100 ... 0x107]

Views могут перекрываться. Это не случайный побочный эффект, а часть архитектуры. Запись через широкий view изменяет те же физические bytes, которые затем видны через более узкий overlapping view. Такой подход удобен для compiler register allocation, представления составных значений и операций над числами различной разрядности. Допустимые physical widths лежат от 8 до 65536 bits, а address ограничен диапазоном текущего register file.

Архитектурно важная область находится в верхней части register file. Там расположены canonical return views и fixed syscall ABI registers. Например, 64‑битный scalar result возвращается через r64@0x7fff8. Поэтому обычный код не должен произвольно использовать верхнюю ABI область как временные регистры, если он участвует в CALL или SYSCALL conventions.

PTR64 и архитектурные пространства

Адрес MCPU – не host pointer. Архитектурный PTR64 делит 64‑битное значение на 4‑битный selector и 60‑битный byte offset. Благодаря этому guest program явно знает, к какому логическому пространству относится адрес, а runtime может независимо проверять права и границы каждого segment.

63             60 59                                      0
+----------------+------------------------------------------+
| segment selector|              byte offset                |
+----------------+------------------------------------------+
       4 bits                     60 bits
SelectorПространствоНазначение
0DATAОбычные изменяемые данные program image
1STACKСтек вызовов и локальные данные
2PROGRAMКод и program addresses
3CONTROLПараметры управления моделью
4INPUTВходные данные supervisor → guest
5OUTPUTВыходное состояние guest program
6TRACEТрассировочные данные model experiment
7HEAPДинамическая память

PROGRAM, CONTROL и INPUT являются guest read-only. При этом host supervisor может обновлять CONTROL и INPUT между lifecycle boundaries. DATA, STACK, OUTPUT, TRACE и HEAP доступны guest code на запись в рамках их архитектурных границ. Такая модель особенно удобна для вычислительных моделей: коэффициенты управления, внешние входы, результаты и trace являются разными типами данных уже на уровне machine address space.

STACK и CALL ABI

STACK растёт в сторону меньших адресов. После выделения пустого стека SP == FP == stack_size. Инструкция CALL помещает в стек raw 64‑битный return PC. Явные arguments располагаются вызывающей программой перед CALL. Поэтому на входе в простую функцию первый argument находится по SP+8, второй – по SP+16 и так далее.

.text
_start_life:
        LDI     r64@0x00000, 11
        LDI     r64@0x00008, 22

        PUSH    r64@0x00008
        PUSH    r64@0x00000
        CALL    add2
        ADJSP   16

        MOV     r64@0x00010, r64@0x7fff8
        HALT

add2:
        LDSP    r64@0x00100, 8
        LDSP    r64@0x00108, 16
        IADD    r64@0x7fff8, r64@0x00100, r64@0x00108
        RET

В примере result помещается в canonical 64‑битный return view r64@0x7fff8. Cleanup explicit arguments выполняет вызывающая сторона. Для функций с frame pointer после типичного prologue сохранённый FP находится по FP+0, return PC по FP+8, а первый argument по FP+16. Эти правила делают ABI пригодным для будущего генератора кода языка высокого уровня и одновременно остаются полностью наблюдаемыми в assembler.

PSW и арифметические флаги

Program Status Word объединяет arithmetic/status flags и execution control state. Младшие 8 bits образуют CPU flag byte: AF CF OF SF PF ZF RF VF. Bits 8...15 содержат MPU mathematical flags: DOMF SNGF OVFF UDFF TLSF PLSF INDF INXF. Выше расположены HF, TF, IF и поле MODE.

Для явной подготовки arithmetic state существуют инструкции SETF, CLRF, CLRCPUF и CLRMPUF. SETF/CLRF изменяют ровно один именованный flag в диапазоне bits 0...15; CLRCPUF очищает bits 0...7, CLRMPUF – bits 8...15. HF, TF, IF и MODE через эти инструкции не изменяются. Branch instructions JSET/JCLR при этом могут читать более широкий набор selectors, поэтому read contract и write contract намеренно различаются.

        CLRCPUF
        CLRF    CF
        IADC    r64@0x00010, r64@0x00000, r64@0x00008
        IADC    r64@0x00018, r64@0x00020, r64@0x00028

        SETF    CF
        ISBB    r64@0x00030, r64@0x00038, r64@0x00040

Явное управление CF особенно важно для многоразрядной арифметики. Перед первой операцией цепочки IADC можно гарантированно очистить carry, а перед цепочкой ISBB – подготовить требуемое состояние borrow/carry в соответствии с машинной семантикой. При этом CALL ABI считает arithmetic/status часть PSW volatile: сама CALL не обязана разрушать flags, но вызываемая функция свободна изменить их, поэтому caller не должен рассчитывать на сохранение прежнего ZF, CF или математических flags после произвольного вызова.

Instruction Set

Instruction set организован вокруг typed register views, architectural addresses и операций LibMPU. В нём есть transfer instructions, integer arithmetic, real и complex operations, comparisons, branches, direct и indirect control transfer, stack operations, pointer arithmetic, block copy, byte swapping, explicit PSW control, lifecycle operations и SYSCALL. Полный authoritative перечень OPCODE16 находится в поставляемом архитектурном руководстве; исходный код assembler и executor использует те же публичные opcode definitions.

Важное свойство ISA – отсутствие жёсткой привязки арифметики к нескольким host widths. Register view может описывать очень широкий объект, а LibMPU выполняет соответствующую операцию в фиксированной целевой разрядности. Благодаря этому assembler program может явно моделировать вычисления, которые на обычном CPU потребовали бы software big-number library и дополнительного слоя вызовов.

Машинные immediates и поля PROGRAM имеют каноническое представление, а данные в register file и object spaces следуют целевому представлению LibMPU. Это различие важно для authors binary tools: byte sequence instruction нельзя интерпретировать как host C structure. Все поля должны кодироваться и декодироваться через архитектурные правила.

Модель assembler source и объектов данных

Язык assembler описывает не только последовательность instructions, но и объектную структуру будущего MELF file. Sections .text, .data, .bss, .init, .fini и model sections задают ownership данных ещё до link stage. Directives для integer, real, complex и string objects позволяют разместить target representation непосредственно в object. Для pointer constants существуют отдельные directives, чтобы program/data addresses не маскировались под обычные host integers.

Symbol name является case-sensitive assembler identifier. Обычная label создаёт local symbol, если source явно не повышает binding через .global/.globl или .weak. Имена, начинающиеся с .L_, удобно использовать для внутренних compiler labels, но assembler не приписывает им скрытую special binding semantics: это соглашение о стиле, а не отдельный механизм. Такой подход делает symbol table предсказуемой и не вводит неочевидных правил, зависящих от spelling имени.

String data также рассматриваются как target objects. Source file снаружи имеет UTF‑8 representation, а directives могут создавать UTF‑8 byte strings или strict UCS‑2 objects в зависимости от назначения. Это важно для будущих compilers: source encoding, target object encoding и runtime text ABI являются разными уровнями и не должны смешиваться в одном неявном преобразовании.

Исполнение, faults и детерминированное состояние

Execution engine работает с архитектурным state, а не с произвольными host C objects. PC определяет положение в PROGRAM, SP/FP – состояние STACK, register views обращаются к общему register file, а PSW хранит status и control state. Decoder сначала разбирает machine instruction и её operands, после чего executor проверяет register widths, addresses и segment permissions. Ошибка decoding, invalid register view или выход за границу guest memory превращаются в machine fault, а не в unchecked host memory access.

Такая схема особенно важна для instructions с pointers. LOADP/STOREP получают PTR64, декодируют selector и offset, проверяют тип пространства и только после этого выполняют доступ. Guest address никогда не кастуется напрямую в host pointer. Поэтому одна и та же guest pointer arithmetic сохраняет смысл независимо от ASLR, host page layout и размера native pointer на машине, где выполняется supervisor.

Детерминизм распространяется и на lifecycle. INIT выполняется не более одного раза в normal managed run, каждый LIFE step заканчивается на своей END boundary, а graceful completion проходит через FINI. Ограничение --steps может остановить experiment без подмены machine semantics: supervisor просто перестаёт запрашивать следующий LIFE step. Это отличие важно при сравнении numerical runs, потому что step count остаётся внешним параметром эксперимента, а не скрытым branch внутри guest algorithm.

MELF object format

MELF – native object and executable container MCPU. По роли он похож на ELF: хранит sections, symbols, relocations, program headers и dynamic metadata, но является собственным форматом с собственным ABI. Toolchain работает непосредственно с MELF и не требует промежуточного flat binary. Основные object classes – ET_REL, ET_EXEC и ET_DYN.

TypeРоль
ET_RELRelocatable object; обычный output assembler
ET_EXECИсполняемый image с lifecycle, STACK и optional HEAP policy
ET_DYNShared object для startup dynamic linking

Формат поддерживает несколько исторических ABI generations. Полностью static executables используют ABI v1. Historical ABI v2 сохраняется для explicit-preload compatibility. Нормальный current dynamic contract – frozen ABI v3 с .dynamic, DT_SONAME и DT_NEEDED. Несовместимое изменение execution format требует нового MELF ABI number, тогда как необязательные non-ALLOC development metadata могут развиваться отдельно.

Symbols и relocations

Relocatable model MELF сохраняет symbolic references до тех пор, пока linker не сможет превратить их в окончательные addresses. Это касается direct control transfers, references на DATA и PROGRAM, pointer constants и dynamic references. ET_REL поэтому является полноценным промежуточным объектом, а не почти готовым executable с несколькими неразрешёнными числами. Linker объединяет section contributions, пересчитывает symbol values и только на подходящем этапе применяет или сохраняет RELA records.

При partial link unresolved strong symbols допустимы, потому что следующий link stage может предоставить их definitions. При final link policy становится строже: linker должен доказать, что required references разрешены в соответствии с выбранным static/dynamic mode. WEAK references имеют отдельную семантику и, в частности, не заставляют static archive извлекать member только ради weak requirement. Это поведение важно для построения libraries с optional hooks.

Dynamic symbol resolution также сохраняет различие binding strength и scope order. Поэтому interposition не является случайным следствием порядка обхода внутренних structures. Main executable, DT_NEEDED dependencies и explicit preloads имеют определённые места в scope, а equal-strength conflict разрешается установленным priority rule. Такой контракт позволяет написать regression test на symbol resolution и получить тот же результат после изменения внутренних linker algorithms.

Assembler workflow

mcpu-as читает один UTF‑8 source и всегда создаёт MELF ET_REL. Отдельного compile-only switch нет, потому что relocatable object и есть нормальный результат assembler. Source управляет sections, labels, symbol visibility и relocatable references, а final lifecycle ownership и executable memory policy определяются позднее linker.

$ mcpu-as -o main.o main.s
$ mcpu-readelf -a main.o
$ mcpu-objdump -t -r -d main.o

Для source generated preprocessors assembler понимает directive #line с decimal line number и optional quoted filename. Директива изменяет locations последующих diagnostics и не попадает в MELF. Это позволяет связать сообщения assembler с исходным .s file после обработки через mcpu-cpp или другой frontend.

Static и partial linking

Простейший final link создаёт полностью static ABI-v1 executable. STACK является частью executable ABI, поэтому его размер задаётся linker, а не случайной runtime option. HEAP также описывается executable policy и может быть отключён или ограничен при компоновке.

$ mcpu-ld --stack-size=64K --no-heap \
    -o program main.o helper.o
$ mcpu-run program

Режим mcpu-ld -r выполняет partial relocatable link. Несколько ET_REL объединяются в новый ET_REL, contributions sections сливаются с сохранением alignment, symbols rebased к новым section offsets, а RELA records остаются для следующего link stage. Strong undefined symbols могут оставаться unresolved, что позволяет заранее собирать крупные объектные modules без создания executable.

$ mcpu-ld -r -o combined.o a.o b.o c.o
$ mcpu-ld --stack-size=64K \
    -o program main.o combined.o final-provider.o

Static archives

Static libraries используют обычный UNIX ar container, но members являются MELF ET_REL. mcpu-ar создаёт детерминированный archive и настоящий System-V/GNU-style symbol index. При link archive member извлекается только тогда, когда он определяет текущий unresolved strong GLOBAL symbol. Один unresolved WEAK reference сам по себе member не вытягивает.

$ mcpu-ar rcs libsimple.a add-one.o add-two.o value.o
$ mcpu-ar t libsimple.a
$ mcpu-nm libsimple.a

Опция --static у linker означает policy выбора libraries, а не смену output mode. В dynamic или shared link она заставляет каждый -lfoo рассматривать только libfoo.a, но выбранный ABI/output type остаётся прежним. Это позволяет точно отделить вопрос какой object создаётся от вопроса откуда берутся symbols.

ABI-v3 dynamic linking

Shared object создаётся как MELF ET_DYN и обязан иметь SONAME. Development symlink может называться libfoo.so, но executable записывает в DT_NEEDED provider SONAME, например libfoo.so.1. Физическое имя файла libfoo.so.1.0.0 не становится частью dependency ABI.

$ mcpu-ld --shared --soname=libfoo.so.1 \
    -o libfoo.so.1.0.0 foo.o
$ ln -s libfoo.so.1.0.0 libfoo.so.1
$ ln -s libfoo.so.1 libfoo.so

$ mcpu-ld --dynamic --stack-size=64K --no-heap \
    -L. -lfoo -o program main.o

mcpu-run разрешает DT_NEEDED graph до запуска _start и выполняет retained relocations до начала guest execution. Модель намеренно startup-dynamic: в ABI v3 нет PLT/GOT lazy binding, dlopen() или runtime dlsym() API. Это уменьшает число состояний loader и делает dependency scope детерминированным.

Normal dependency scope начинается с main executable, затем обходит DT_NEEDED graph breadth-first: direct dependencies идут перед transitive dependencies, siblings – в recorded order. Explicit preload имеет более высокий equal-strength priority, а среди нескольких preload objects правый элемент имеет более высокий priority. Strong GLOBAL definition превосходит WEAK definition.

Explicit preload и interposition

Для controlled startup interposition runtime поддерживает --ldpreload= и environment variable MCPU_LD_PRELOAD. Environment preloads помещаются раньше command-line preloads, а среди equal-strength explicit preload definitions более правый элемент имеет более высокий priority. Это позволяет заранее подготовить общий environment override, а затем точечно заменить его конкретным object в одном запуске без изменения executable.

$ MCPU_LD_PRELOAD=liba.so mcpu-run program
$ MCPU_LD_PRELOAD=liba.so mcpu-run --ldpreload=libb.so program

Preload mechanism действует только на startup dependency world и не превращает MCPU в process с mutable dynamic namespace. После старта отсутствует dlopen-style изменение graph. Это делает interposition достаточно мощным для profiling, testing и controlled replacement библиотечных symbols, но сохраняет детерминированный набор loaded objects на протяжении guest execution.

Low-level lifecycle

Каждый low-level ET_EXEC имеет обязательные symbols _start и _life; e_entry всегда указывает на _start. Section .init optional. Если её нет, assembler syntax _start_life: удобно создаёт совмещённую точку старта и life entry в .text. Optional .fini/_fini задаёт finalization stage.

.init
_start:
        ; one-time initialization
        END

.text
_life:
        ; one model step
        END
        JMP     _life

.fini
_fini:
        ; finalization
        END

Инструкция END является границей между guest и supervisor. Она не эквивалентна произвольному process exit: runtime использует END для завершения INIT, отдельного LIFE step и FINI. Поэтому один executable может быть не только обычной программой, но и управляемой вычислительной моделью, которую host запускает шаг за шагом, меняя INPUT/CONTROL и считывая OUTPUT/TRACE между шагами.

HLL Runtime ABI и CRT

Помимо low-level assembler lifecycle, linker умеет распознавать compiler-normalized complete-program objects. Здесь source language не обязана определять _start, _life или _fini. Вместо этого object содержит обычные strong GLOBAL/FUNC definitions, а mcpu-ld классифицирует программу и автоматически вставляет подходящий CRT.

PROGRAM:  main + fini
MODEL:    init + life + fini
ФормаBinary signaturesCRT
PROGRAMint32 main(int64 argc, PTR64 argv)
void fini(void)
crt0.o + crti.o + crtn.o
MODELint32 init(int64 argc, PTR64 argv)
int32 life(void)
void fini(void)
crt1.o + crti.o + crtn.o

PROGRAM получает crt0.o, MODEL – crt1.o; обе формы получают crti.o перед user objects и crtn.o после них. CRT objects берутся только из active MCPU root в lib/mcpu/. Отсутствие или несовместимость нужного CRT – fatal link error: linker не молча откатывается к другому root и не превращает HLL program в low-level executable.

Loader создаёт persistent 32‑byte McpuStartup v1 object перед _start. Он содержит version, size, 64‑битный argc, PTR64 argv и reserved zero field. CRT проверяет структуру, использует loader-owned argv и вызывает main/init через обычный CALL ABI. HLL functions завершаются RET; END/EXIT остаются операциями CRT/MELF lifecycle, а не обязанностью compiler-generated main или life.

Контракт MODEL life()

Для MODEL функция life() возвращает 32‑битный status. Значение __INT32_MIN__ означает продолжить model execution через ровно одну следующую LIFE/END boundary; zero означает нормальное завершение модели; положительные значения являются error exit statuses. Остальные отрицательные значения зарезервированы HLL Runtime ABI v1.

Такой контракт разделяет алгоритм модели и supervisor loop. Source function принимает решение только о том, существует ли следующий шаг и успешно ли он выполнен. Повторный вход в life, подсчёт steps, останов по limit и вызов finalization остаются обязанностью CRT/runtime. Благодаря этому compiler не должен генерировать собственный host-facing control loop для каждой модели.

Loader validation и ownership

Loader не рассматривает MELF как доверенный memory image, который достаточно mmap в host address space. Program headers, section metadata, dynamic records, segment sizes и relocations проходят архитектурную проверку до начала guest execution. Host backing может быть округлён до page size, однако guest bounds всегда определяются архитектурным размером segment, а не величиной host mapping. Поэтому page rounding не создаёт скрытой доступной области за концом DATA, STACK или HEAP.

Ownership rules проверяются не только на runtime. Linker не разрешает shared object присвоить себе sections, которые определяют жизненный цикл main executable. ET_DYN предназначен для ordinary .text/.data/.bss contributions; model sections и lifecycle остаются у ET_EXEC. Благодаря этому startup linker не обязан разрешать конфликт между двумя разными owners STACK, CONTROL или FINI.

Тот же принцип действует для HLL classification. Linker анализирует explicit input ET_REL objects до library extraction и требует complete-program definitions в одном defining object. PROGRAM main/fini нельзя смешать с MODEL init/life, definitions нельзя незаметно собрать из нескольких архивных members, а raw _start/_life ownership нельзя совместить с HLL root. Ошибка структуры обнаруживается при link, а не превращается в неопределённое startup behavior.

Relocatable MCPU root

Установленный toolchain имеет собственный relocatable target root. При обычной конфигурации public command names находятся в /usr/bin, а реальные private binaries – в /usr/lib64/mcpu/bin. Root выводится из фактического location программы, поэтому MCPU target environment не обязан быть жёстко привязан к одному compile-time prefix.

MCPU root
|
+-- bin/                 private mcpu-* programs
+-- etc/
|   `-- mcpu-ld.conf
+-- include/
|   `-- sys/asm/         target assembler ABI headers
`-- lib/
    `-- mcpu/
        +-- crt0.o
        +-- crt1.o
        +-- crti.o
        `-- crtn.o

Canonical public assembler ABI headers хранятся в source tree под sysroot/include/sys/asm/. Здесь определены syscall numbers, MCPU errno, file flags, target structures для stat/dirent/select/time и другие target-side constants. Это именно MCPU target ABI; host headers не должны случайно становиться источником target numeric values.

Library search configuration

mcpu-ld и mcpu-run используют общую policy поиска MCPU libraries. Configuration читается в возрастающем порядке priority из runtime root, system override и user override. Таким образом package может предоставить default path, administrator – system policy, а пользователь – персональную настройку без модификации package files.

<runtime-root>/etc/mcpu-ld.conf
/etc/mcpu/mcpu-ld.conf
$HOME/.mcpu/etc/mcpu-ld.conf

Для диагностики доступны --dump-config, --print-search-dirs, --no-config и --sys-root=PATH. Linker option -L имеет priority над configured system path; runtime --library-path аналогично предшествует effective system path.

mcpu-run как supervisor и test bench

mcpu-run выполняет больше работы, чем обычный process launcher. Он загружает MELF, строит dynamic dependency graph, применяет relocations, создаёт architectural memory spaces, формирует startup area, управляет INIT/LIFE/FINI boundaries и после остановки может читать machine state. Поэтому одна и та же программа подходит и для normal execution, и для regression experiments.

$ mcpu-run --steps=10 program arg1 arg2
$ mcpu-run --init-only --int-data=64@16 model
$ mcpu-run --raw-trace=256@0 model

Post-stop inspection options могут читать integer/real register views и объекты DATA, CONTROL, HEAP, INPUT, OUTPUT и TRACE. Raw forms выводят заданный диапазон bytes. Это даёт удобную форму observability без добавления в guest program отладочного I/O: test script может проверить внутреннее состояние непосредственно после execution.

Stage actions

Runtime также поддерживает actions, привязанные к lifecycle stages: --before-run, --after-init, --on-step и --after-run. Scalar actions могут читать или устанавливать типизированные register/data values, а raw actions предназначены только для read-only dumps. Несколько одинаковых stage options выполняются в порядке command line.

$ mcpu-run \
    --before-run=set-int-regs:r64@0x100=123 \
    --on-step=print-int-data:64@16,64@32 \
    --after-run=print-int-regs:r64@0x7fff8 \
    model

Stage actions особенно полезны для численных моделей. CONTROL или INPUT можно подготовить до первого instruction, после INIT проверить вычисленные параметры, на каждом LIFE boundary сохранить OUTPUT/TRACE, а после FINI прочитать final state. При этом сам assembler source остаётся свободным от host-specific test code.

System Call ABI

SYSCALL использует фиксированную register ABI. Количество arguments находится в r32@0x7fff8, syscall number – в r32@0x7fffc, arguments начинаются с r64@0x7fff0 и идут вниз по register file. Result возвращается в r64@0x7fff8. Такая схема не зависит от host calling convention.

NARGS : r32@0x7fff8
SYSNO : r32@0x7fffc
ARG0  : r64@0x7fff0
ARG1  : r64@0x7ffe8
ARG2  : r64@0x7ffe0
...
RET   : r64@0x7fff8

Публичный syscall set уже включает heap management, time/sleep, файловый I/O, pathname operations, stat family, fcntl, pipes, directory iteration, select, fsync, truncate/ftruncate, descriptor-local chmod/chown/chdir и dup/dup2. Guest file descriptors принадлежат MCPU context и не являются прямыми host descriptor numbers. Sysdep layer переводит host errors в стабильный host-independent MCPU errno namespace.

Исключением является SYS_brk: его семантика совпадает с raw brk model и возвращает фактический current HEAP address, а не negative errno. Heap имеет immutable lower boundary _end и configured upper limit. При shrink host backing не обязан уменьшаться, но освобождённая guest область зануляется, поэтому последующий regrow открывает детерминированную zero memory.

HEAP и brk model

HEAP в MCPU имеет две разные величины: current break и immutable limit. Linker задаёт initial heap size и верхнюю границу executable policy, а runtime не может самовольно расширить heap дальше этого limit. Нижняя граница задаётся linker-defined _end. Guest может уменьшить break до _end или увеличить его в пределах limit, но запрос ниже _end отклоняется без изменения состояния.

Важная деталь детерминизма: shrink не обязан уменьшать host allocation capacity, однако guest range выше нового break немедленно зануляется. Если программа потом снова увеличит break в уже существующий backing, она увидит zeros, а не старое содержимое. Поэтому поведение guest memory не зависит от того, решил ли host allocator реально вернуть physical pages операционной системе.

Такой raw brk contract предназначен как низкоуровневая основа будущей target libc. Более привычный sbrk(), malloc allocator, arenas и другие policies должны строиться поверх SYS_brk, а не становиться частью instruction set или supervisor API. Это сохраняет правильную границу между machine ABI и library policy.

Guest descriptors и errno

File descriptor в guest ABI является номером внутри конкретного cpu_context. Host descriptor хранится приватно в runtime и не просачивается напрямую в MCPU program. Поэтому операции open, close, dup, dup2, pipe2 и fcntl работают в guest descriptor namespace. Даже если host случайно использует тот же integer, совпадение не является ABI.

Ошибки system calls также не используют host numeric errno напрямую. Публичный target header определяет стабильные MCPU errno values; sysdeps boundary преобразует symbolic host errors в этот namespace. Unknown или неподдерживаемая host error попадает в target EUNKNOWN. Тем самым MELF program не меняет смысл только потому, что на другой UNIX system номер EIO или ENOTSUP отличается.

Отдельно существует набор LibMPU mathematical error values, опубликованный в sys/asm/mpu-errno.h. Эти значения относятся к conceptual Math Processor Unit state и не конвертируются автоматически в system errno. Arithmetic domain/singularity/overflow flags и filesystem errors представляют разные error domains; будущая libc при необходимости сможет связать их на своём уровне.

Time ABI и sleeping

Time-related syscalls используют target structures с фиксированными signed 64‑bit fields, а не host time_t. Поддерживаются gettimeofday, clock_gettime, clock_getres, nanosleep и clock_nanosleep. Таким образом размер target timespec остаётся частью MCPU ABI и не зависит от host libc data model.

Исследование MELF objects

Для системного программирования важно видеть не только source, но и то, что действительно записано в object. mcpu-readelf показывает MELF headers, sections, symbols, relocations, program headers, dynamic metadata и aggregate information. Section headers выводятся в широком one-section-per-line format, удобном для grep, awk и обычного diff.

$ mcpu-readelf -h file.o
$ mcpu-readelf -S file.o
$ mcpu-readelf -s file.o
$ mcpu-readelf -r file.o
$ mcpu-readelf -l program
$ mcpu-readelf -d program
$ mcpu-readelf -a program

mcpu-nm работает с ET_REL, ET_EXEC, ET_DYN и static archives. Он умеет сортировать symbols по имени или value, сохранять original table order, показывать только GLOBAL/WEAK или undefined symbols, а также выбирать .dynsym вместо .symtab. Для archives имя member может включаться прямо в output prefix.

mcpu-objdump связывает section inspection с disassembly. Опция -d декодирует executable PROGBITS, -r показывает RELA и аннотирует instruction, содержащую relocation field, -s делает raw section dump, а повторяемая -j NAME ограничивает выбор sections. Undecodable bytes выводятся как .byte, после чего inspection продолжается.

objcopy и strip

mcpu-objcopy намеренно является MELF-to-MELF structural editor, а не converter в ROM images. Он умеет dump/add/update/rename/remove безопасные non-allocated sections и strict filtering через --only-section. Операция отклоняется, если filtering уничтожил бы runtime-visible, dynamic-linking или link-critical sections. Нет режимов Intel HEX, S-record, arbitrary VMA/LMA или изменения section flags ради получения firmware blob.

mcpu-strip следует тому же консервативному принципу: удаляет только symbol metadata, которая не требуется последующей компоновке, загрузке или execution. ET_REL symbol tables и dynamic-linking metadata сохраняются намеренно. Это важнее агрессивного уменьшения файла, потому что MELF object должен оставаться корректным участником дальнейшего toolchain workflow.

Поведение command-line tools

Успешные mcpu-as и mcpu-ld работают тихо по умолчанию. Опция -v/--verbose добавляет summary создаваемого MELF object, но normal build не засоряет stdout служебной диагностикой. Это полезно для Makefiles и automated builds: отсутствие output не нужно фильтровать, а verbose mode включается только когда действительно требуется исследовать результат.

Все installed tools используют общий version-reporting module. Форма --version печатает имя utility, identity MCPU и package version; --dump-version выводит только number. Обе команды завершаются успешно без input file. Это небольшая деталь, но она позволяет configure scripts, packaging и regression tests получать version одинаковым способом для assembler, linker, runtime и inspection utilities.

Сборка и установка

LibMCPU 0.4.2 разработана и проверяется с LibMPU 1.0.25 или новее и LibMPUIO 1.0.4 или новее. Release tarball уже содержит generated Autotools files, поэтому для обычной установки не требуется запускать bootstrap. Типичная Slackware-style конфигурация использует --prefix=/usr и --libdir=/usr/lib64 на x86_64.

$ mkdir build
$ cd build
$ ../libmcpu-0.4.0/configure \
    --prefix=/usr \
    --libdir=/usr/lib64
$ make
$ make check
$ make install

Для development tree после изменения Autotools inputs используется ./bootstrap. Target make tests является alias regression suite, а make distcheck выполняет чистую distribution rebuild/install/uninstall verification. Такая проверка особенно важна для проекта, который устанавливает не только host library, но и target sysroot, CRT objects, headers, manual pages и private tool binaries.

Интеграция внешних проектов

Публичный build layer предоставляет несколько привычных UNIX interfaces. Команда mcpu-config сообщает prefix, version и compile/link flags. Pkg-config metadata mcpu.pc позволяет получать flags стандартным способом. Для Autoconf-проектов устанавливается libmcpu.m4 с проверкой наличия и версии LibMCPU.

$ mcpu-config --version
0.4.0

$ mcpu-config --cppflags
$ mcpu-config --cflags
$ mcpu-config --ldflags
$ mcpu-config --libs

Package version и shared-library interface version являются разными понятиями. Release number описывает выпуск проекта, а Libtool version-info описывает binary interface host library. Разделение этих двух шкал позволяет выпускать documentation/build-system updates без ложного изменения library ABI и, наоборот, явно отражать несовместимое изменение interface независимо от marketing release number.

Что устанавливается в систему

Host-side development interface включает public headers libmcpu.h и mcpu-elf.h, shared library LibMCPU, pkg-config metadata, Autoconf macro и mcpu-config. Отдельно устанавливаются command-line tools и их manual pages. Target environment размещается внутри MCPU root и содержит свои assembler ABI headers, CRT и linker configuration. Такое расположение не смешивает host include namespace с target sys/asm namespace.

Public command в bindir является стабильной точкой входа пользователя, но реальный binary хранится под MCPU-specific library directory. Это позволяет command найти свой runtime root через real location даже если public name достигнут через symlink. Для relocatable toolchain это надёжнее, чем вычислять target root только из compile-time prefix.

Практический workflow

Для небольшого low-level проекта минимальная цепочка очень короткая: source собирается в ET_REL, linker создаёт ET_EXEC с заданным STACK/HEAP contract, после чего runtime выполняет программу. На каждом этапе binary можно исследовать соответствующим tool, не полагаясь на скрытое состояние build system.

$ mcpu-as -o main.o main.s
$ mcpu-readelf -S -s -r main.o

$ mcpu-ld --stack-size=64K --heap-size=64K --heap-limit=1M \
    -o main main.o
$ mcpu-readelf -h -l main

$ mcpu-run main arg1 arg2

Для library-oriented проекта добавляются mcpu-ar и library search. Для dynamic project появляются ET_DYN, SONAME/NEEDED и runtime library path. Для HLL compiler final objects могут состоять только из main/fini или init/life/fini, а linker сам вставляет CRT. Таким образом один toolchain обслуживает три уровня: raw assembler, libraries и compiler-generated complete programs.

Вычислительная модель

Архитектурные sections CONTROL, INPUT, OUTPUT и TRACE позволяют строить model executable, в котором интерфейс с supervisor виден непосредственно из object layout. Например, INIT вычисляет параметры модели, LIFE выполняет один численный шаг, OUTPUT содержит текущее состояние, TRACE – историю наблюдений, а FINI завершает experiment. Host при этом не обязан знать внутренние addresses DATA и register allocation алгоритма.

.control
model_step:
        .real 64, 1.0

.input
external_delta:
        .integer 64, 2

.output
current_state:
        .integer 64, 0

.trace
telemetry_state:
        .integer 64, 0

Это делает MCPU удобной исследовательской средой для numerical software: arithmetic precision, machine ABI, model lifecycle и experiment control можно рассматривать как отдельные уровни. Программа не обязана смешивать физическую модель, host UI и filesystem protocol в одном process. Вместо этого executable предоставляет чистую machine-facing boundary, а supervisor управляет экспериментом снаружи.

Для каких задач предназначен LibMCPU

Первый естественный класс задач – изучение системного программирования. В одном достаточно компактном проекте можно пройти всю цепочку от source syntax и instruction encoding до object format, relocations, symbol resolution, static archives, dynamic loading, CALL ABI, CRT и system calls. При этом это не учебный mock-up: те же механизмы используются реальными tools проекта и покрываются regression suite.

Второй класс – разработка compiler infrastructure. MCPU предоставляет будущему compiler достаточно ясную target contract: typed register views, PTR64, CALL ABI, relocatable object model, PROGRAM/MODEL normalization и automatic CRT insertion. Compiler может сосредоточиться на lowering языка в target operations, не создавая одновременно собственный assembler, linker и runtime loader.

Третий класс – numerical experiments. Благодаря LibMPU machine operations могут работать с широкими integer, real и complex objects, а lifecycle MODEL отделяет one-time initialization от повторяемого шага. CONTROL/INPUT и OUTPUT/TRACE создают естественную границу между parameterization, external forcing, current state и experiment history. Это удобно для воспроизводимых вычислительных моделей, где precision и execution protocol должны быть частью формального experiment setup.

Наконец, LibMCPU удобен как лаборатория ABI design. Здесь можно явно увидеть последствия выбора caller cleanup, volatility flags, symbol binding, SONAME policy, stable errno numbering или ownership model sections. Изменение такого правила сразу проявляется в assembler, linker, loader, tools и tests, поэтому архитектурное решение нельзя безнаказанно спрятать внутри одной функции реализации.

Архитектурные границы проекта

Некоторые ограничения LibMCPU являются сознательными. Dynamic loader выполняет startup relocation, но не реализует runtime dlopen. objcopy редактирует MELF, но не превращает его в набор firmware formats. ET_DYN не владеет lifecycle sections и model I/O spaces: lifecycle, STACK, HEAP, CONTROL, INPUT, OUTPUT и TRACE принадлежат main executable. Эти границы уменьшают число неоднозначных состояний и делают object ownership проверяемым.

Аналогично разделены host и target layers. Каталог cpu/ реализует host-side machine, sysroot/ содержит MCPU target headers и CRT, tests/ является regression suite. User-facing examples распространяются отдельно и не являются dependency test suite. Такое разделение позволяет в будущем развивать MCPU Libc и compiler toolchain, не превращая host library в смесь target headers, demonstration sources и test fixtures.

Regression testing и воспроизводимость

Regression suite проверяет machine behavior на нескольких уровнях. Есть прямые C tests register file, byte order, PSW, pointer arithmetic, memory bounds, instruction decoding и syscall mapping; есть shell runners, которые собирают assembler fixtures настоящими mcpu-as/mcpu-ld, запускают результат и исследуют его binary tools. Поэтому test проходит через те же границы, которыми пользуется обычный developer.

Отдельные regressions проверяют relocatable linking, archive extraction, dynamic dependencies, preload priority, HLL CRT insertion, lifecycle, stage actions, object editing и version/config interfaces. Это важно потому, что системный toolchain может быть individually correct, но ломаться на стыке двух компонентов. Например, правильный assembler object бесполезен, если linker теряет local relocation, а правильный dynamic table бесполезен, если loader разрешает scope в другом порядке.

User-facing examples при этом намеренно не являются частью regression dependency graph. Tests владеют своими fixtures, а examples/book material распространяются отдельно и могут развиваться как учебный слой. Это позволяет удалять, перестраивать или расширять examples без риска, что make check случайно зависит от демонстрационного файла, предназначенного для человека.

Место LibMCPU в MCPU toolchain

LibMCPU сознательно не пытается быть одновременно preprocessor, parser generator, compiler и target libc. Preprocessing вынесен в mcpu-cpp, parser-generator work выполняет ZUBR, арифметический engine находится в LibMPU, host formatted I/O – в LibMPUIO. LibMCPU определяет тот слой, к которому в итоге должны прийти остальные компоненты: machine instructions, MELF objects, target ABI, linker и execution runtime.

Для будущего compiler это означает полезное разделение ответственности. Frontend может анализировать язык и строить semantic representation, generator code – выбирать register views, stack layout и machine instructions, assembler – создавать ET_REL, linker – разрешать symbols и вставлять CRT, runtime – загружать и исполнять final MELF. Ни один этап не обязан знать внутренние host pointers или повторять работу соседнего stage.

Target libc в этой модели также является отдельным проектом. Machine предоставляет system calls и HLL CRT hooks, но такие вещи, как stdio, malloc, locale, string functions или higher-level startup policy, должны реализовываться библиотекой пользователя пространства. Это позволяет развивать libc независимо от frozen MELF ABI и не помещать library semantics внутрь CPU executor.

Документация

Release содержит два поддерживаемых русскоязычных PDF reference manuals: libmcpu-bytecode-architecture-ru.pdf и libmcpu-melf-abi-ru.pdf. Первое руководство описывает architecture, ISA, ABI и HLL runtime contract; второе – MELF container, relocations, lifecycle и linker/loader rules. Документы намеренно не привязаны к package release number: они фиксируют текущий architecture contract, а история выпусков находится в NEWS.md и ChangeLog.

Кроме PDF устанавливаются English и Russian man pages для каждого command и overview libmcpu(7). Manual pages описывают current CLI contracts и дополняют архитектурные документы. Версию программы следует получать через --version или --dump-version; man sources специально не требуют механической замены release number при каждом выпуске.

Типичные ошибки понимания

Неверная модельПравильная модель
PTR64 – host pointerPTR64 содержит MCPU segment selector и target byte offset
Каждый r64 – отдельный registerr<bits>@addr является view в общем 512 KiB register file
STACK задаёт mcpu-runSTACK policy является частью linked executable ABI
main является MELF entry pointe_entry указывает на _start; HLL main вызывается CRT
CALL сохраняет arithmetic PSW contractPSW arithmetic/status state volatile across arbitrary function calls
--static всегда означает static ET_EXEC--static меняет library selection, не выбранный output mode
ET_DYN может владеть CONTROL/TRACEModel spaces и lifecycle принадлежат main ET_EXEC
Host errno возвращается guest как естьSysdeps переводит errors в stable MCPU errno ABI

Краткий справочник команд

ЗадачаКоманда
Собрать sourcemcpu-as -o file.o file.s
Создать executablemcpu-ld --stack-size=64K -o program file.o
Partial linkmcpu-ld -r -o combined.o a.o b.o
Создать archivemcpu-ar rcs libfoo.a a.o b.o
Создать shared objectmcpu-ld --shared --soname=libfoo.so.1 -o libfoo.so.1.0.0 foo.o
Запустить programmcpu-run program [args...]
Посмотреть MELFmcpu-readelf -a file
Посмотреть symbolsmcpu-nm [-D] file
Disassembly и relocationsmcpu-objdump -t -r -d file
Версия toolchainmcpu-<tool> --version

Исходные тексты и выпуски

Текущий публичный выпуск: LibMCPU 0.4.2. Git repository содержит историю разработки, а FTP directory – опубликованные source archives и сопутствующие release files.

LibMCPU развивается вместе с LibMPU, LibMPUIO, mcpu-cpp и будущими компонентами MCPU toolchain. При этом границы между проектами остаются явными: LibMPU отвечает за arithmetic engine, LibMPUIO – за host-side formatted I/O, mcpu-cpp – за preprocessing, а LibMCPU – за machine architecture, MELF и binary toolchain.