libpagh — единственный слой между программой и ядром
pagh-riscv. Не libc, не Linux-совместимость и не обёртка
«как получилось»: это #![no_std]-крейт без внешних зависимостей, который
реализует ровно контракт из docs/abi.md — таблицу syscall'ов,
структуры, раскладку начального стека, ошибки и рантайм.
docs/abi.md замороженный контракт (источник истины)
pagh-user.ld раскладка ELF: база 0x10000, _start в .text.entry, R+X / R+W
libpagh/src/sys.rs 29 ecall-обёрток + номера/флаги
libpagh/src/errno.rs Errno (31 значение из ABI) и Result<T>
libpagh/src/abi.rs Stat/Timespec/Uname/EntryKind, проверка размеров
libpagh/src/rt.rs _start, разбор argc/argv/envp, паника
libpagh/src/alloc_impl.rs bump-аллокатор на brk
libpagh/src/io.rs Read/Write/Seek, Stdin/Stdout/Stderr
libpagh/src/fs.rs File, Dir/DirEntry, stat, read_link
libpagh/src/net.rs Ipv4, TcpStream (один активный TCP-клиент, без listener'а)
libpagh/src/path.rs normalize/resolve относительно PWD
libpagh/src/env.rs args(), var(), current_dir()
libpagh/src/print.rs print!/println!/eprintln!
examples/hello.rs пример программы
examples/http_get.rs HTTP/1.0 GET на 10.0.2.2:8000 (нативный сокет)
examples/mutex.rs четыре потока под futex Mutex (counter=1000)
tools/check_abi.py сверка чисел в docs/abi.md с sys.rs/errno.rs/abi.rs
Тулчейн закреплён в rust-toolchain.toml — тот же
nightly-2026-07-12, что и у ядра; rustup сам подхватит его в этом каталоге.
Цель и линкер заданы в .cargo/config.toml, так что
достаточно:
cargo build --release --target riscv64gc-unknown-none-elf # rlib libpagh
./build.sh # ELF examples/hello
rustup run nightly-2026-07-12 cargo build --release \
--example http_get --target riscv64gc-unknown-none-elf # ELF examples/http_get
python3 tools/check_abi.py # дрейф ABIbuild.sh печатает точную команду сборки и путь к статическому ELF
(target/riscv64gc-unknown-none-elf/release/examples/hello), который ядро
запускает через run <path>.
examples/http_get.rs — живой сокетный пример: TcpStream::connect к
10.0.2.2:8000 (QEMU user network), HTTP/1.0 GET /, печать ответа до EOF.
Его прогоняет tools/test_sockets.py из репозитория ядра; адрес — литерал,
потому что DNS в ABI нет.
pagh-user.ld повторяет pagh-riscv/userland/user.ld: образ начинается с
0x1_0000, _start лежит первым в .text.entry, .text/.rodata дают
R+X/R-only PT_LOAD, .data/.bss — R+W. _start находится по адресу
0x10000, что проверяется readelf -h (см. отчёт в коммите).
Ядро загружает статический RV64 ELF, кладёт на стек argc/argv/envp и входит
в U-mode:
a0 = argc
a1 = argv
sp -> [argc][argv0 … argvN-1][NULL][env0 … envK-1][NULL][строки]
_start (global_asm в rt.rs) сохраняет sp, вызывает libpagh::start(sp),
тот разбирает стек, вызывает
extern "C" fn main(argc: i32, argv: *const *const u8, envp: *const *const u8) -> i32и делает exit(code). Возврата из _start нет. argv[0] — путь, набранный в
шелле; окружение содержит как минимум PWD, относительные пути программа
резолвит сама (path::resolve), потому что путь в syscall'ах ядра абсолютный.
#![no_std]
#![no_main]
use libpagh::println;
#[no_mangle]
pub extern "C" fn main(argc: i32, _argv: *const *const u8, _envp: *const *const u8) -> i32 {
println!("hello, argc = {argc}");
0
}Важно: программа обязана использовать libpagh (здесь — println!).
Иначе rustc не подключает rlib, а вместе с ним не приходят _start и
panic-handler. Зависимость — обычная path-зависимость:
[dependencies]
libpagh = { path = "../pagh-api/libpagh" }| Модуль | Что даёт |
|---|---|
sys |
read write openat close lseek fstat getdents64 brk clock_gettime nanosleep uname readlinkat getpid sched_yield exit + mkdirat unlinkat symlinkat linkat renameat2 fchmodat ftruncate + socket connect send recv shutdown; все, кроме exit, возвращают Result<T, Errno>; незнакомый ядру номер даёт ENOSYS |
abi |
Stat (24 B), Timespec (16 B), Uname (80 B), SockAddrIn (16 B), EntryKind, Dirent-константы; размеры проверяются const assert |
errno |
31 errno из ABI + Errno::Unknown для дрейфа; Display с текстами для CLI |
io |
Read/Write/Seek, Stdin/Stdout/Stderr, copy, SeekFrom |
net |
Ipv4 (литерал и Ipv4::parse), TcpStream::connect(host, port), Read/Write (неблокирующий read: EAGAIN → повтор), read_to_end, shutdown; только IPv4-литералы, одно соединение, без TcpListener/UDP/DNS/TLS |
fs |
File::open/create/open_with, File::set_len, read_to_end, read_to_string, write/write_all, create_dir/create_dir_all, remove_file/remove_dir, rename, hard_link, symlink, truncate (по пути), set_permissions, stat, read_link, Dir/DirEntry поверх getdents64 |
path |
normalize, resolve (относительно PWD), join, basename, dirname, Path |
env |
args() (включая argv[0]), vars(), var("PWD"), current_dir(), program_name() |
alloc |
bump-аллокатор на brk: работают Vec, String, format! |
print |
print! println! eprint! eprintln! |
Ошибки не паникуют: main сам решает, что печатать и с каким кодом выходить.
EAGAIN не ретраится внутри Read: это сигнал «на fd 0 пока нет байта»
(и «на сокете пока нет данных»), и прикладной код делает sched_yield и
повторяет (см. ниже). Исключение — TcpStream::read_to_end: он сам крутит
этот цикл до EOF.
brk-аллокатор страничный: первый brk(0) узнаёт текущую границу, дальше
кусок не меньше 64 KiB, выравнивание по 4 KiB, brk только растёт. dealloc
— no-op: память живёт до exit. Это осознанный компромисс для одноразового
CLI-процесса, а не универсальный malloc.
Если ядро ещё не реализовало brk, первый же Vec/String/format!
приводит к program: out of memory (brk failed); … на fd 2 и exit(102).
Это не паника и не зависание.
read(0) по ABI возвращает -EAGAIN, пока в очереди нет байта; poll(7)
(0.12.0) умеет ждать POLLIN на консоли, поэтому новые читатели могут спать
вместо read + sched_yield. Старые applets оставлены на yield-цикле —
поведение то же. 0 байт считается EOF, но консольный fd 0 в текущем ядре
EOF не выдаёт — cat без файлов будет ждать ввод вечно, как интерактивный
cat. Это ограничение ядра, записано в отчёт.
- паника:
program: panic at <file>:<line>:<col>в fd 2 →exit(101); без размотки, без backtrace (у ядра пока нет потребителя DWARF); - нехватка памяти:
program: out of memory (brk failed); requested N bytes at alignment A→exit(102); Errno::Unknown— признак расхождения ядра иdocs/abi.md; это не нормальная ошибка, а сигнал вызвать Lead'а.
Ни динамического линкера, ни auxv/TLS, ни libc, ни std. Процессы есть:
fork/execve/wait4/getppid (0.16.0). Потоки и futex есть (0.13.0: thread::Builder), sync::Mutex/RwLock — 0.5.0,
сигналы — kill/tgkill/rt_sigaction/rt_sigprocmask/rt_sigreturn
(0.14.0), память — mmap/munmap/mprotect, дескрипторы — pipe2/dup/
dup2/poll/ppoll. Файловые мутации есть:
mkdirat/unlinkat/symlinkat/linkat/renameat2/fchmodat/ftruncate
(номера 34–37, 53, 46, 276) и обёртки fs::create_dir, remove_file,
remove_dir, rename, hard_link, symlink, truncate, set_permissions,
File::set_len. Чего по-прежнему нет: chown, utimes, O_EXCL/O_DIRECTORY,
AT_SYMLINK_NOFOLLOW, жёстких ссылок на каталог и долговременного chmod —
режим живёт только до конца прогона (см. ниже).
Сокеты есть ровно в объёме одного активного TCP-клиента: AF_INET +
SOCK_STREAM, connect по IPv4-литералу, send/recv, shutdown. Нет
bind/listen/accept, UDP, IPv6, DNS/getaddrinfo, sendmsg/recvmsg и
TLS; второе соединение одновременно невозможно. poll на socket-fd работает
(готовность к чтению/записи).
recv неблокирующий (EAGAIN — повторить), shutdown закрывает оба
направления, setsockopt для типовых SO_*/TCP_NODELAY принимается и
игнорируется.
Всё, что неоднозначно в docs/abi.md, решено консервативно и перечислено
здесь; это же — список для Lead'а:
-
dirfdигнорируется. ПередаёмAT_FDCWD = -100(Linux-значение), хотя ABI его не описывает; важно только то, что ядро его игнорирует. -
stat=openat(O_RDONLY)+fstat+close. Отдельногоstatatв ABI нет, поэтому файл с mode000нельзяstat'ить, если нельзя открыть. Это ограничение ABI, а не реализации. -
openatна каталог. ABI не говорит, разрешён лиO_RDONLY-open каталога, но без этогоgetdents64бесполезен.Dir::openна это рассчитывает; если ядро откажет — нуженO_DIRECTORYили отдельный syscall. -
nanosleep. Второй аргумент (rem) ABI игнорирует; передаём указатель на занулённыйTimespec, а не NULL, чтобы ядро, валидирующее оба указателя, не вернулоEFAULT. -
readlinkat. Длина не определена; начинаем с 256 B и удваиваем до 4096 B, дальшеENAMETOOLONG. Цель декодируется lossy (имена не обязаны быть UTF-8). -
PWD. Если переменной нет или она не абсолютная — считаем cwd/. Контракт обещаетPWD, но программа не должна падать от кривого окружения. -
Начальный
brk. ABI не фиксирует, где он находится после загрузки; аллокатор вызываетbrk(0)и доверяет ответу. Ядру нужно вернуть границу после последнего PT_LOAD, иначе первыйVecзатрёт.bss/кучу. -
Dirent. Заголовок записи — 12 байт (u64 ino,u8 kind,u8 name_len,u16 reserved), запись выровнена на 8. Это не Linux-ныйstruct linux_dirent64; парсер вfs.rs— тому доказательство. -
UTF-8. argv/envp/имена lossy-декодируются, а
read_to_stringпри невалидном UTF-8 возвращаетEINVAL(в ABI нетEILSEQ). -
clock_gettime. 0 — realtime/uptime, 1 — monotonic, как в таблице;Timespec— 16 байт,nanosв диапазоне 0..1e9 (ядро обязано нормализовать). -
Длина пути. ABI не задаёт
PATH_MAX, аnative-abiпринимает не длиннее 255 байт + NUL (PATH_CAPACITY = 256).path::resolveдлинных путей не обрезает: syscall вернётENAMETOOLONG, программа напечатает ошибку. Это ограничение ядра, а не скрытый truncate. -
modeприO_CREATиmkdirat. ABI говорит, что mode применяется; текущее ядро его игнорирует (ext2 создаёт 0o644/0o755): в VFS нетset_mode.File::createпередаёт0o666и не полагается на права. -
chmodне сохраняется.fchmodatзаписываетmode & 0o7777в таблицу прогона, иfstatпоказывает новые биты до конца программы; в inode они не пишутся, права ни на что не влияют, следующий процесс видит диск снова. Это задокументированный компромисс (в VFS ядра нетset_mode), а не обещание persistence. На синтетическом узле (корень/) —ENOTSUP. -
unlinkatиlinkatне разыменовывают последнюю ссылку.rm linkудаляет саму ссылку, аlinkatсоздаёт жёсткую ссылку на неё;fchmodat, наоборот, идёт по ссылке, какchmod.AT_SYMLINK_NOFOLLOWв ABI нет: ненулевые неизвестные флаги —EINVAL, а не тихий игнор. -
Сокеты — один TCP-клиент.
connectпринимает толькоsockaddr_in(16 байт) с IPv4-литералом; DNS-а нет.recvнеблокирующий по контрактуread(0):EAGAIN— это «пока нет данных», 0 — EOF.shutdownне имеет half-close: оба направления закрываются, дескриптор остаётся открытым (чтение → 0, запись →EPIPE) доclose. Второйsocket()при открытом первом —ENOSPC, потому что у ядра одно соединение; это записано вdocs/abi.md. -
Номера
setsockopt/getsockopt. В generic RISC-V таблице это 208/209; 200/201 — этоbind/listen, которых в ABI нет.tools/check_abi.pyсверяет таблицу сsys.rs, так что «200/201» не могли бы пройти проверку.
Ветка native-mut (pagh-riscv-mut, база 0.10.1) отвечает на 22 базовых и
файловых syscall'а этого контракта; ветка native-sock добавляет семь
сокетных (всего 29) и живой HTTP-тест. Встроенная ABI-фикстура проходит целиком (abi: mutations ok,
abi: ok), selftest с диском даёт SELFTEST SUMMARY (user): 10/10 passed,
без /mnt — 8/10 passed, 2 skipped (no-fs половина и mutation-половина
работают независимо). Ниже — историческая сводка по более старому срезу
native-abi; перечисленные в ней баги на native-mut не воспроизводятся.
Старая сводка: ветка native-abi рабочего дерева Lead'а уже отвечала на 15
syscall'ов этого контракта. Но проверка в QEMU на срезе
1ce872f + незакоммиченные правки упёрлась в баги ядра, не в libpagh:
- encoder
getdents64считаетrecord_len = 8 + align8(name_len), хотя имя пишется со смещения 12 (12-байтовый заголовок из ABI); встроенныйselftestпадает наabi: FAIL getdents64 /mnt names (code 70); - чтение ext2 за 12-м прямым блоком (файловое смещение ≥ 48 KiB) зависает
в S-mode без исключения. Поэтому ELF с
.rodata, пересекающим 48 KiB (в частностиpaghboxс зеркальной раскладкой), не загружается; файл на 221 KiB с маленькими PT_LOAD загружается нормально; - после завершения user-программы оболочка получает store page fault
(
stval=0x201000000,Arc::clone) и машина останавливается — воспроизводится даже на пробе, вызывающей толькоgetpid; - часть загрузок падает в
src/arch/riscv64/mmu.rs:206(misalignedread_volatile) или в загрузчике ELF (0x1a000 is not mapped); повторный запуск проходит.
Поэтому libpagh и hello собираются в валидный ELF; paghbox и все 12
applet'ов успешно отработали на временной 2-segment раскладке (тот же исходник,
.rodata сразу после .text), но коммитнутая зеркальная user.ld ждёт
исправления ext2. Подробности, адреса и логи — в отчёте Lead'у. Это
зафиксировано, а не замаскировано.
python3 tools/check_abi.py
# ABI check OK: 29 syscalls, 31 errnos, 7 open flags, 4 struct sizesСкрипт читает docs/abi.md, парсит таблицу syscall'ов, секцию ошибок, флаги
O_* и комментарии размеров структур, затем грепает sys.rs, errno.rs,
abi.rs. Проверяет и обратное направление: лишний SYS_*/O_*/errno без
строки в контракте — тоже ошибка.