#tabula imaginarium: osdev experiments in zig
1 messages Β· Page 5 of 1
but its literally svc (0xC400_0003, cpuid, entrypoint, arg0) and you get an int back
and this is the status codes for it
and the MADT tells you which of smc or hvc instructions to use
the defined state has the mmu off so the entry point has to be physical
invalid parameters is if you pass an id for a processor it cant turn on, invalid address if the hypervisor/securemonitor can tell in advance your entry point is bullshit, already_on, on_pending, internal_failure are self-explanatory, and denied is if the firmware policy says you cant use that core
they guarantee the new core has the same endianness as the core that calls cpu_on, interrupts are masked, stage 1 mmu and caches disabled, cntfreq for the timer is guaranteed correct, and the arg0 you passed is in x0 on the new core
it slaps actually
tbh thats why i dont have it implemented yet, it seems so simple im being lazy about it
anyway the debate for what to do now basically comes down to:
- scheduler
- rework buddy to do physically contiguous blocks
- irq abstraction rework again to be hierarchical
- storage drivers and/or vfs abstraction work
- something else that i cant think of but someone might suggest
could also work on some cleanups/refactoring for zuacpi, theres some badly organized stuff in there that ive been putting off working on
anyone got suggestions for which?
scheduler !!!!!
alright scheduler itll be!
hell yeah
do u have an idea of how your scheduler will work?
i wanna do something inspired by ule in my kernel
not a ton of idea yet tbh, though ive got context switching fully functional atm
and i think i want to use tsc/arm generic timer for tracking quantum rather than just ticking down by one whenever a timer interrupt happens
that is good, i plan to have a per-cpu "nearest deadline" timestamp that can either be a timer or the scheduler deadline
and whenever you schedule or arm a timer you update it to whatever is earliest
i believe some people refer to this as "tickless"
yeah thats how im leaning too, but not 100% sure either way yet
i might be biased 
my main timer interrupt is once per ms though so if i dont do true sleeps tighter than 1ms then i wont need tickless
oh right i forgot i suck at figuring out which way to multiply this shit lmao
i think the correct way to get "tsc counts per clock interrupt" is interrupt frequency divided by tsc frequency?
but i might have that backwards
yep backwards whee
the part my brain is now struggling at is how to do this division without overflowing
in the multiplication part
ok that number is now looking right
so i can start actually maybe using it
dw i will get to this - everything zuacpi is working great its just not as clean and idiomatic as id like atm
ok timer interrupt is now hooked up to preempt, we'll see if it works
thats a nope
i do know what the issue is though, its starting the timer before the dispatcher is online
lmao
did you realize it was a bit unhinged 
interrupt frame printer is older than when i swapped to octal, and also it cant know what is or isnt a pointer
i may make the tsp and rip prints use octal tho
mostly i like octal for debugging paging issues tho
doesnt matter a ton for other stuff
yeah its great
can you tell where i told it to fuck off and stop because the log spam was annoying π
what is the (krnl) at the end of symbol names?
the zig module
the other ones ive got are "target" which is literally just my target feature config flags, "zuacpi" (obvious), "config" (other configs) and "std" (the zig std)
yeah no not loadable
anyway we got timer preempting now
Zig gives you those for free right
i made the commit message "tick tock its my turn now"
mostly for free, it wasnt bad but wasnt zero effort either
Ooh why not
because c++ is a thing
the actual dwarf cfi stuff in in std but the debug info loading that wraps it relies on normal file system stuff so i had to override that part to load the elf from the mapped file the bootloader gives me
Ah ok
Btw what happened to the log ring?
i use it still
Did you hook it up?
dont have much with it atm because i dont have non-debugcon outputs for it
but all log messages are getting put in it
ye its working ok i think (hard to test before i get an output console for it)
now ive got a scheduler i might be able to hook it up to real outputs sometime
rn my log function pushes to the ring and then does the normal locked debugcon output anyway 
Why not hook it up to debugcon also

β οΈ
right as i was getting comfortable with c++, i even wrote my stupid shitty rbtree
havent gotten around to it yet, not sure how i want the output management to work yet
Make a clanker translate it into zig
ive got a unit-tested avl tree in zig in my repo you can steal
Where source and thread btw
when i get something going more than an rbtree test

i wanna do a shitty linux clone
oh speaking of clankers i finally got around to writing up in my repo my strict no-ai no-llm policy for imaginarium
put it in the readme, in a contributing.md, and on the docs site
U can also add the made by human badge or whatever
more shitty and less overengineered log ring and everything else, but yeah "same as you" 
That supercip made
lemme go find that to add it then, unless you already got a link ready so i dont need to search
Hey at least you will get kde running in like a year, whereas me probably never
whereas ill never get kde working on imaginarium because i hate fork(), devfs, and sysfs so much that ive completely thrown out any possibility of ever having posix compat
there's more to osdev than running kde
and i think whatever you're doing is infinitely more creative and fun lol
If you think its more fun why are you making a Linux clone 
i kinda want to try to write a driver stack as well
or, actually, i think it's more of an io stack rather than a driver stack
i might use c++ coroutines for continuations
Like streams?
i have no idea how streams work, assuming we're talking about the thing that fadanoid implemented in keyronex recently
the more i develop around it the more i actually love the NT driver stack architecture
Yeah
no idea then, but i'm thinking about implementing something similar to the nt driver/io stack but use coroutines for driver calls/completions
anyway time to stop clobbering this thread π
Ah I see
the thing im still struggling with is how to actually get reader(s) going on the log ring tbh
struggling to figure out that is
I can show you my code tomorrow
would appreciate that yeah
But tldr is you flush the consoles after every write, each console has a seq number, you iterate all records until no further seq numbers exist for each console
(the other part i struggle with is the bootstrap problem - ie i want logs output to debugcon before threading and smp are available, and also i kinda want to skip the bootloader logs but still put them in the ring)
yeah its how to handle and synchronize the "flush the consoles" bit that im struggling with
If you're doing smp flushing can be done synchronosuly or deferred via a thread depending on context where the message was logged from, you do need a spinlock for it ofc, also Linux makes the last writer takeover flushing if they can so that first writer doesn't sit there flushing records forever if everyone is spamming the ring
yeah its the bootstrapping before i can get a thread up to do the flushing thats throwing me off. once i get a thread up i can just have a thread sit on an auto reset event that gets signalled by writers
or use a dpc for it
also an option
A normal preemptible or just early context can do flushing synchronosuly I really dont see a problem there
ig yeah
ill need a bool somewhere to tell it to swap over
i wish there was a branch hint option for "this will start with this value and change exactly once"
If (!preempt_enabled() || ! sched_online()) flush()
yeah
i might cheat and make the starting seqnum for readers not be the actual starting seqnum for some of them
so the console ones can skip the stuff that the bootloader already output there with its own console/serial stuff
Sure, there's an api to fetch the last seq num
yeah
U can just assign that to the console
Linux has a flag for that when registering a console
neat
Whether it wants to see the entire backlog
id still like to see your code for flush at some point tomorrow or whenever just to get a more concrete idea but this is starting to make sense
Sure, but keep in mind its throwaway basically just to make it work until I have more proper infra, so its more of an api usage example
yeah
api usage example would def help rn also tbf
(ill also probs look at the linux code its just that linux code is a hot mess sometimes lmao)
yeah
gonna wrap up for tonight by giving drivers pub named references in a namespace so there can be autodocs for them even if that wont be that helpful really
oh btw @last pebble since there was talk about ap startup earlier, did you know that theres a uefi protocol for multiprocessor stuff with a startupaps method, and you can even use it for os ap startup purposes? it doesnt return until all the aps return control back to the firmware
iirc that protocol got removed at some point
I remember looking it while wanting to write a game that ran under uefi π
looks like the MP protocol is uefi 1 only ok nvm they did a nvme and put it in a separate spec.
Yeah, it requires you to shut them down before exiting boot services
static void print_flush(void)
{
static char buf[512];
struct console *con;
struct log_record rec;
error_t ret;
for (con = consoles; con; con = con->next) {
for (;;) {
ret = log_ring_read(
&g_log_ring, con->log_seq_num, buf, sizeof(buf), &rec
);
if (ret != EOK)
break;
con->write(con, buf, rec.length);
con->log_seq_num++;
}
}
}
technically its wrong too since it should do con->log_seq_num = rec.seq_num + 1
thanks!
and you do this under a trylock of the console list?
alrighty
throw away slop code in ultra? impossible
lmao
i would never commit such a thing 
the slop is just temporary downstream stuff so i can at least see some output lmfao
yeah makes sense
might even add a temporary flanterm thing
BASED
im kind of interested in making a parser for that stuff myself
uVTE
lol
i doubt its gonna be a library, just ultra specific stuff
i wouldnt try to take flanterms space on the market
so the thing i realised here is i can just do this as a single if tpr >= dispatch which is nice
yeah i guess
depends how early you connect the consoles
the debugcon is connecting instantly because im still paranoid about my early init sometimes
at super super early boot it just puts the messages into the ring without any consoles to flush to
at least for me
then a bit later it parses the early command line params
where x86 defines an earlycon option
but yeah fair
i just didnt find a clean way to do it
my plan atm is on initial startup if its x86 then install the debugcon right after getting info from the bootlaoder (just so debugcon can skip the bootloader logs that it already does its own thing for)
and then during acpi early tables we parse the DBG2 table and replace debugcon with that if it exists (or just use it wholesale on aarch64)
and then during driver init the serial driver takes over whatever DBG2 was giving
(and add a log to file option at some point when i get there)
fair
ok, while im still ruminating on this
plan is to have log outputs provide one required and one optional function
required function "write" - writes to the output immediately, in any non-nmi context, no questions asked or answered
optional function "defer" - copies the message into a device queue, used preferentially over write in no-preempt situations after the scheduler is alive, if available
so write will be used if the scheduler is preinit or the log happens in a thread-ish context
still not 100% sure yet on this design tho
mostly because taking the spinlock on the outputs list requires disabling interrupts ideally
alright here heckin goes
turned the debugcon into a log ring output
it doesnt compile lmao
ah i see the issue now
lol
well it seems to work
* the device tree print is still locking debugcon manually cause that shit is far too long for a logmessage
normal logging is working tho
well the log ring has pr_cont if u want to utilize that
or rather, has api to add support for that
i didnt but also device tree being printed here specifically is 100% a debugging thing so i dont care that much
adding a second processor is making it hang now though
ikr
(i dont think its the logging actually, i havent tested smp in a hot minute atm)
theres a bunch of stuff thats been added between the last log statement im getting and when it calls ap startup too
i think this is actually due to the ap startup getting preempted lmao
huh
this is very odd
it crashes way earlier on tcg π
im so confused now
its triple faulting on a page fault to the pfmdb entry for page FEE00
which is a uh familiar looking number 
found that bug now i think
i got so used to testing with x2apic that i forgot tcg doesnt do it and hadnt tested that in a while lmao
bro does qemu tcg ignore the mask bit on the timer lvt??
hello???
ok no its fine it just is such a train wreck timing wise since ive got no calibration for it that the timers swamp everything
How many times do you have set to interrupt things?
What I do is I setup all non xAPIC timers to poll
And then pick the best one to calibrate the xAPIC
(Or x2
i dont have timer calibration
at all
i trust the hypervisor or i do nothin
ok the hang on whpx is somewhere in here
the first log prints, the second doesnt
oh the .create returned
next spot
printf debugging so fun lmao
ok hang on
no that wasnt it hmm
so the first log here goes off
and the second does not
wait no im stupid
my flush is going off before the publish whoops
so i might be off by 1
its hanging in the sending of the ipi
oh lmao
ok
this was stupid
turns out theres no pending bit on the icr in x2apic
so my icr code for sending the init ipi was hanging
wait no that didnt fix it either
going to attempt something stupid on a whim
because i keep forgetting this weird detail about apic shit
yall wanna see the diff that fixed it?
im mad
cc @last pebble
log ring debugcon output is hooked up now and working on smp
mostly theres some bugs in my ap startup still
(namely im not copying apic base register over so the ap doesnt have x2apic on lmao)
fascinating that the self-dpc isnt working for some reason
oh i think i know why the self-dpc is being lost
i forgot that cli/sti drops interrupts instead of delaying them
in that log the test dpc fired on bit is running on the ap and the only sync is the log ring
so a dpc is being dropped definitely
which is weird
its self-dpcs specifically too
hmm
try spamming it from multiple CPUs for a bit, could be a fun test
how is it possible for them to be out of sync?
the panic doesnt use the log ring
ah
but also doesnt synchronize with the other cpus
gonna quickly add that for at least x86
ok if i try to use a broadcast nmi here then whpx bails out
and tcg freezes
i think thats my not waiting on all aps before trying shit tho\
oh lmao
ok thats one error down
next up
ok for some reason it just never fires on the bsp
welp the log output is fucked on aarch64 apparently
hang on if this is the issue... 
ok no that wasnt it
??????

WHAT THE FUCK????
im so confused π
ok so
@last pebble you might have some idea i dont here
enabling the apic timer on the bsp causes it to not get DPCs from anything else
all other processors get dpcs fine even with the timer enabled
the thing i dont get is why this is only impacting the bsp
ill do that in a bit, dunno if it works with an accel tho
Does it only break with accel
ill check that in a bit too
the timer is fucked without accel anyway though
because i dont have calibration other than trusting the hypervisor
oh wait hang on i think i just lucked into it
yup
idk why this did it
but it did
and i aint gonna complain about having it fixed ig
now i gotta figure out why the log ring straight up isnt working on aarch64
which smells of atomic ordering
what was the issue?
i mean unless you're running it on a real aarch64 device it doesnt really emulate ordering
i was restoring the irql flag twice in the dispatch yield dpc if the yield got short-circuited because of no lower prio thread to swap to
oh true
the worst part about debugging printf is you cant use printf debugging 
i hate that i cant tell if the issue is reading or writing
literally going to start using panic statements to printf debug
wait wtf
ok the issue is reading
uhhh
why is register never getting called
OH LMAOOOOOOO
passing the actual head of the bootloader protocol linked list to kmain is probably a good idea huh
aarch64 was just never getting the fuckin bootloader logs, which means it never called register
turns out the log doesnt output if you never tell it where to output 
alright we good
@last pebble got your log ring ported to zig, working fully on both x86_64 and aarch64, with smp-safe debugcon output on both
officially
alright next up thing to do options:
- rework buddy to do physically contiguous blocks
- irq abstraction rework again to be hierarchical
- storage drivers and/or vfs abstraction work
- bunch of boring work to make zuacpi a bit more idiomatic
- something else that i cant think of but someone might suggest
- aarch64 smp startup
id post a poll but i dont feel like posting it in the big polls channel and cant post one here lmao
oh yeah i dont have framebuffers yet, i could work on that
or work on docs more
i can make one for you
nice
if you can thatd be nice, thanks
12 if thats an option, im gettin tired so probably done for today
i always forget the options lol
theres 4 and 8 and then 24
then 8
knew there was some memory making me unsure if 12 was an option (and also why the fuck isnt it an option discord)
thanks!
np π
yeah its kinda stupid
just let me enter the exact amount of time i want
i think every single time ive gone to make a poll ive been like "12 hours is about right wait why cant i pick 12"
(i also think there should be an option to do permissions for the op of a post in a forums setup like this in addition to normal roles)
yeah
let post ops pinning things be an option discord dangit lmao
ah well
also dont forget to vote on that if you got opinions @last pebble
sure
tbh that shouldnt be too bad, ive got tsc calibration from the bootloader using efi stall, just need to translate that into apic timer calibration
i just so cant be bothered rn 
If you have a calibrated constant TSC use that to calibrate APIC
Or use PIT/HPET/ACPI
i dont have constant tsc anywhere that i dont already have apic from a hypervisor because tcg doesnt do invtsc
but i dont think theres really any constant speed timers on tcg
HPET/ACPI/PIT
it says it cant do invtsc for TCG
Damn
Well you got these
it might actually be doing constant
but it says not supported and doesnt set the cpuid bit
yeah ig
mhm
you got source btw? i was going to look at how obos does calibration but he does it in full asm and that scared me off for now lmao
And IIRC somebody made me rework my PIT driver to be polling which is surprisingly more annoying than you think
i think acpi pm timer is guaranteed on x86? if so ill use that, i aint touching anything PIT related if i can help it
https://git.evalyngoemer.com/evalynOS/evalynOS/src/commit/d0bc87a5f381c506f4ef1039368140aac9e74ee6/src/kernel/drivers/x86_64/apic
https://git.evalyngoemer.com/evalynOS/evalynOS/src/commit/d0bc87a5f381c506f4ef1039368140aac9e74ee6/src/kernel/drivers/x86_64/timers
https://git.evalyngoemer.com/evalynOS/evalynOS/src/commit/d0bc87a5f381c506f4ef1039368140aac9e74ee6/src/kernel/acpi/acpi_timer.c
could always use hpet too ig
All of my code is in C
I keep as much as possible out of ASM
And I keep it clean
I find OBOS codes style horrid π
lol
This is where I calibrate it where needed
If it canβt do TSC deadline mode
You just need a blocking sleepMS function
mhm
Which all my timers implement
I just set the right function pointer for the current timer source etc
But itβs pretty basic
If itβs there and you have a constant TSC use it
No reason not to
And I donβt like normal deadline mode :p
I hope my code is readable for you >ww<
Some of it Iβm not too proud of
it is so far
This code works on every machine I have tried
nice
It somtimes fucks up but very rarely and I CBA to fond out
Worst case itβs been off 20%??
I assume itβs a hardware issue
And I can just do more rounds of calibration across cores with SMP :p
what to do next
3
11
well one of the things tied for the lead will be relevant for the other so ig ill start with the irq abstraction rework when i work on imaginarium next
actual thing i work on today will either be non-programming or maybe reviving my old PR to fix zig on wsl that needs a rebase and clenaup
https://github.com/ziglang/zig/pull/25838 the old pr in question that needs rebasing
https://codeberg.org/ziglang/zig/pulls/31547 alright thats done now i can consider working on the os again lmao
@haughty cipher how do you do context switching in zig if the ABI is not stable?
do you force C callconv
the abi isnt stable but its still limited by the arch so im not really seeing why unstable abi would impact context switching
because you need to save/restore callee-saved registers?
you can always use an interrupt frame to context switch, but in my case i actually use a trick that the zig std uses for evented io/fibers which is to make the inline asm constraints engine save everything
that sucks
why would I save everything when I can avoid doing so?
because context switching doesnt necessarily have to be cleanly inside a function?
it often happens from interrupt contexts with preempts yknow
?
this
the callee/caller saved regs are handled by the function prolog/epilog, and if you can context switch from anywhere you can context switch during those
you could context switch in the middle of a caller saving its regs
well no because in this case a context switch acts like a function call
so? it'd just put them on the stack
or something
i mean if you want context switch to act like a function call but only save callee registers you can make just that one function be callconv(.sysv) or whatever
amd64_context_switch:
; Save callee-saved registers
push r15
push r14
push r13
push r12
push rbx
push rbp
mov [rdi], rsp ; Save stack
mov rsp, [rsi] ; Load next thread's stack
mov rax, rdi ; Save prev thread in rax
xor rbx, rbx
xchg [rdi+72], rbx ; Unlock previous thread
; Restore next thread's registers
pop rbp
pop rbx
pop r12
pop r13
pop r14
pop r15
ret
this is what i do currently
different functions can have different callconvs on zig after all
I can't do that in zig since abi is unstable
you could use that exact function and just declare the extern of it in zig as callconv(.whatever_explicit_callconv)
no?? because it can call into any code
unless I mark every function as callconv sysv it wouldnt work
can I do that?
no it can call into the second half of itself in a different thread, or any thread start
wha
if a context is swapped away using that function, then the saved IP is in that function
no?
wdym
i basically do what you have, except instead of saving a passed IP i save the address of a local label
ah you mean it'll be back in context_switch
I'm not sure that changes anything though
I think I'd have to save every register like you say
which I kinda dont wanna do
why wouldnt it? if you know that the thread is back in that context_switch function then you can rely on the actual callconv for caller-saved regs if you declare just context_switch as sysv
the only other cases are thread start, where you also know exactly where it ends up, and thread terminate, where it just never goes back into running ever and you also dont care
wait
export fn context_switch(old_sp: **anyopaque, new_sp: *anyopaque) callconv(.Naked) void {
asm volatile (
\\ push %%rbp
\\ push %%rbx
\\ push %%r12
\\ push %%r13
\\ push %%r14
\\ push %%r15
\\
\\ mov %%rsp, (%%rdi)
\\
\\ mov %%rsi, %%rsp
\\
\\ pop %%r15
\\ pop %%r14
\\ pop %%r13
\\ pop %%r12
\\ pop %%rbx
\\ pop %%rbp
\\
\\ ret
);
}
i think this then you expose it as sysv
I think you're right
youre actually missing one thing from that in that you cant pick a new IP with yours
so thread start is gonna be hard
yes its just annoying to set up that stack like that
ah well I already do that
yeah if you already are then its fine
void ke_thread_init_context(struct ke_thread *thread, vaddr_t stack_base,
size_t stack_size, void (*entry)(void *), void *arg)
{
vaddr_t stack = (stack_base + stack_size);
if (stack % 16) {
stack -= (stack % 16);
}
struct arch_thread_regs *sp =
(struct arch_thread_regs *)(stack - sizeof(struct arch_thread_regs));
memset(sp, 0, sizeof(*sp));
thread->ctx.rsp = (uintptr_t)sp;
sp->rip = (uintptr_t)amd64_asm_thread_entry;
sp->r12 = (uintptr_t)entry;
sp->r13 = (uintptr_t)arg;
}
youll need to push the callee saved manually onto this too, no?
no it happens on the first switch
oh right i see what youre doing now
https://codeberg.org/Khitiara/imaginarium/src/branch/rewrite/src/krnl/hal/arch/x86_64/context.zig heres mine for context btw
im considering rewriting in zig but idk
actually no hang on, because your context_switch loads the sp from the new thread itll stack overflow when you try to context switch into a thread_start
unless thats just what arch_thread_regs is doing in which case fine
i missed that youre putting a struct on the stack and not just pushing with the last four lines here because i just woke up
struct PACKED arch_thread_regs {
uint64_t rbp, rbx, r12, r13, r14, r15, rip;
uint64_t padding;
};
this is the struct
so i didnt realise that the stack space that context_switch pops would get taken up by the struct yeah
lmao
zig for osdev is a lot of fun imo but i cant say whether or not itll work right for whatever youre doing
ye
ok so it's not that strongly typed
you could make it strongly typed
the standard in zig for "an int but distinct typed" is to make a non-exhaustive enum backed by that int and use @enumFromInt/@intFromEnum but i personally prefer making it a packed union with a single field of that int instead
mhm I don't really care about that, though it is nice
yeah i dont really either lmao
i just use aliases for those
the big things in zig for osdev is the first-class bitfields imo, which you can even put bitint-backed enums into which slaps
you mean like u<N>?
when i say bitints yeah
when i say bitfields i mean what zig calls "packed structs" (and c calls bitfields) which are structs where bit-ints get packed to the bit and the whole thing is considered interchangeable with a bigger int of the total bit size
ahh yeah
like
const Lol = packed struct { flag: bool; something_else: bool; }
would be over a byte
thatd be two bits
well yeah but it'd need to be a byte
if its used on its own itd be yeah
yeah that's what I meant
that is really cool
tho I'm kinda scared because yesterday I was playing around with it and already got a compiler bug lol
but that's on the self-hosted backend
yeah
self-hosted dies if you ask it to softfloat or use a linkerscript atm so its not ready for osdev, i just use llvm backend for the actual os
it was a userspace program
#1312232715193683988 message anyway if youre wondering about some of zigs niceties for osdev start here and scroll up and around a bit
it wasnt even that complex lol
oh? dang
either way self-hosted is still fairly new and you can always make it use llvm instead*
* except for the build scripts themselves, which is part of why you straight cant use zig on mounted windows partitions under wsl rn, im workin on fixing that when i got time
(albeit workin on fixing that by fixing the actual bug, not by letting you pick llvm for the build scripts)
yeah that is pretty cool
what is tempting me the most is the nicer type system
I'd use C++ but zig is simpler
which I like
yeah
my project is mostly snake_case because fuck camelCase
I use PascalCase for types tho
there was a debate a hot minute ago about whether to swap zig to snake_case but i dont think anything ever came of it lmao
yeah I think that is a good convention
i say mostly snake_case because im terrible at actually being consistent and sometimes i need to name things in a certain way to make zig detect them as overriding std features (like my debug info stuff)
ill be honest one of the nicest things with zig is how lazy it is lmao
(as in lazy evaluation)
which is why the std can have ntdll imports, posix libc imports, linux syscalls, and whatever the fuck macos does, and all at the same time, and only compile the ones it needs for a particular program
i think a nice thing is modules as well
so it can do cross-compile unit optimizations
yeah
yep
minor downside of it being hard to do dynamic linking/loading stuff but upside of everything is optimized for your exact target
also ngl even if i was using c id be sorely tempted to still use zig for the build system lmao
I am planning a build system rework soonβ¦
Could I use zig for that?
Instead of meson or whatever
yup
if you use WSL at all id wait for my fix to get polished up and land but you can absolutely use zig as a build system for C
the one asterisk is i hope youre ok with clang/llvm since i dont think itll do gcc
nice lmao
I cannot be assed to figure it out
Esp when I use clang overloadavle a good bit now
iirc zig defaults to the strictest undefined behavior flags it can when building C also
thats one of the original motivators for zig iirc was how much undefined behavior C has
Fair fair
UBSAN crashes my kernel way before it should
π₯
It hates my PFNDB shit
Megan though it works fine
I do some fuckery with it
maybe you should fix that 
Yeah Iβm gonna redo my PMM at some point

And il do a virtually sparse PFNdb too
And just init it after the PMM
virtually sparse pfndb slaps
thats actually what my PMM is as a whole
is a virtually sparse pfndb
I can just not track pages that are part of it after they get added because I wonβt ever free them?
I think
Iβm also gonna improve my freelist
bootstrapped off of the biggest block of memory in the memmap
I will use scatter gather DMA because I target modern hardware and I donβt care about speed of older hardware
Only matters it runs
mhm
yeah the thing i still need to fix with my buddy/pmm is making it physically contiguous
rn its a free list of single physical pages and i then make virtually contiguous ranges for the buddy
You donβt need physical contiguous FWIW
i think i do for the GIC ITS on aarch64
i dont think the gic its can be behind an iommu
You could also just pre reserve things
yeah thats the alternative im considering 
Like oh I need a GIC chunk our 2mb for contiguous things
I mean on x86 you need memory in the lower 1mb anyways reserved so
Not the worst idea to have support for reserving chunks with various constraints per arch
most of the gic stuff is just boring mmio, but the MSI stuff using the GIC ITS you need to allocate your own tables for and the "page size" of the tables is 64k and required physically contiguous
What would a build script look like in zig for a kernel tho?
π
Whatβs the point π
Why arm
You could also hear me out
why do you need that nowadays anyway? if using like uefi or whatever
Use 64kb page sizes on arm
SMP
oh i just reserve one page for that lmao
well technically two buts thats because i preemptively throw out the physical 0 page for some reason i forget
true but i can just iterate the first 1mb of the pfmdb and grab the first one thats open
So you can use null as an error?
yep
I just panic on OOM and page zero gets used depending on how I do things
Because my pfndb is cursed
though i dont do enough real mode shit for the physical 0 page to really matter
i mean im doing that too
Yeah but itβs not virtually continuous stuff
For virtually contiguous I would demand allocate too. If I allocate a page and it isnβt found in the PFNdb though a safe fault function it will allocate it with a function that skips the PFNdb
neither is my pfmdb (sorta, its virtually contiguous but sparse so depends on definitions)
me too sometimes
Itβs my native language and I mix shit up so much
yeah nah id just use meson
oh btw if you use a virtually sparse pfmdb i do recommend making a bitmap for the presence of entries so you dont page fault if something tries to access the entry for some mmio thing that isnt in it
I have safe functions for this
That fault and return an error code
So I can add probe functions for pfndb things
Like βprobe deferenceβ
for a kernel zig as the build system isnt really giving as much benefit as it might on a normal xplat project yeah, the biggest thing for zig build for c is that it can take a simple target triple and the rest just worksβ’
And if it faults I get a pointer to -1 I can check for etc
one of these days ill make stuff like that using abortable instructions from ultra or some other design
The way I do it is very simple
on aarch64 i think theres literally an instruction for "try translating this address but dont fault" too
I make primatives in assembly
Like memcpy or strlen
And then I make a bunch of wrappers in C
That add extra guards but donβt touch bad things
Then on fault it checks if the RIP was inside of the assembly primatives
And if it is it changes the RIP
also technically I can run arbitrary code right?
so I can use kconfig
thats literally what "abortable instructions" from ultra are
yes you can run executables with it, ive got a few tools i run in my build, but also wtf why would anyone in their right mind want kconfig in particular that shit looks like is such a mess
zig build has a step for config headers btw, supports cmakedefine, autoconf #undef, autoconf @FOO@, and blank starts that can output either nasm-style asm or c #defines for the config options you give it btw
but yeah you can still run arbitrary programs at build for whatever purpose
if you want to do it at configure time theres the run function on the build object that takes an argv and returns the stdout from the child process
if you want to do it at make time theres the Run step
(theres also a fallible version of run for configure time that returns an error and the out code of the process instead of aborting the build if it doesnt exit with 0)
kconfig format is just KEY=VALUE, so I can probably parse that
or I can just generate a C header and import it directly
yeah
LazyPath slaps in zig build (thanks @hoary aurora iirc you came up with that)
all path things in zig build are represented using LazyPath, which can be generated by a step
and if you use addOutputFileArg on a Run step then you give it a basename and it gives you a lazypath
and using that as input for a different step automatically adds dependencies etc
and caching for those also works generally
what would be cool if I do it in zig too is I think I would be one of the first ones to do an actually proper kernel
not throwing shade at you, you're doing it too
one of the first yeah
but most other projects I've seen are shit
you me and a scant few others
no u right lmao
is that one even still being worked on?
I don't think so
a lot of zig osdev projects out there are abandoned
anyway also for context https://codeberg.org/Khitiara/imaginarium/src/branch/rewrite/build.zig this is my build script atm
ive got a one-file executable written in zig that hashes the kernel in case i want to pass that along to the bootloader, a different one-file executable that generates the bootloader config file, i use dimmer to generate the disk image at build time which is another tool that gets built by zig, obv the kernel and bootloader get built by zig lmao, and then a run step to run qemu
and zig supports every arch llvm does right
if you use llvm backend yeah
the temptation is strong...
(theres very alpha self-hosted for aarch64 and x86_64 and i think maybe riscv but im not sure on that one. oh and a self-hosted backend that outputs c code but thats more for bootstrapping zig itself than anything)
the only things you need installed to build my kernel is zig master (because im tracking master instead of tags) and qemu
its great
huh well excluding platform code and other goodies my kernel is like 4-5k lines
a rewrite is doable
mhm
zig also has a build step that takes a c header and outputs a file with externs to link to that c thing
so you can interop well with c
id say i do that for uacpi but i actually wrote all the extern bindings for that by hand lmao
so is this legit just abortable functions lol
@haughty cipher last question
if I dont know zig
is it doable
like is there a lot of idiomatic stuff I need to learn
yesterday I did write a bit of zig code and liked it
ultra's abortable instructions is a thing that uses a list of rip ranges that can be aborted cleanly by GP fault and page fault and its just a macro to push the thing onto that list using some asm pushsection stuff
not really? there is idiomatic stuff but if youre doing osdev a bunch of that goes out the window lmao
ahh huh neat
but like I dont need to be a zig master
if you know osdev and have done some zig youll be fine
this kernel is literally the first thing i wrote in zig
it took me two years but that was mostly me struggling with osdev concepts lmao
(and its not even close to done yet after those two years lmao)
yeah I can just rewrite my current thing pretty much 1:1 I think
the big thing with zig in terms of idiomatic stuff is to optimize for readability wherever possible
rather than optimizing for how fast you can write it or how compact it is
I'm kinda scared of having too many abstractions
theres a big emphasis on make the code easier to read than to write
i wonder how this may look fore pure C? or how you may add all the TUs and linking steps
btw dont u already have a kernel project that u were working on with some other person?
kinda
not really since he said he's busy atm
and I wrote all the code anyway
so 
the module type has a function called addCSourceFiles
and you give it all the source files
and theres also addIncludePath
and i presume similar for linker scripts?
so i could say. hey zig. glob all these fliles except these ones based on arch and the compile to an elf with a linker script
and then it just workβ’
and i can add a bunch of other steps and hooks for deps i may need
can I do something like export the same functions from two modules
like I say I have some generic platform abstraction
in a non crappy langage?
idk how globs are with it but yeah
how do I avoid having to do amd64.function and just do platform.function or something
lowk i wana try and make this build with zig
would mean i can drop python and other similar things?
can the zig.build include other zig files too lmfao?
to make things cleaner
the compiler makes a module called "builtin" that has the target on it. you can then switch on the target and do https://codeberg.org/Khitiara/imaginarium/src/branch/rewrite/src/krnl/hal/arch.zig#L10-L15 this and then (in this case) arch.system gets swapped out depending on target
yup the build.zig is just a zig file
yeah ok that I dont get
pub const system = switch (tag) {
inline else => |a| @field(arch, @tagName(a)),
};
wtf is that
thats me being a bit silly
inline else means compiler please make separate branches for each option
which means the captured a becomes known at comptime
so thats equivalent to
pub const system = switch(tag) {
.x86_64 => x86_64,
.aarch64 => aarch64,
}
and because zig is lazy and tag is a compile time constant whichever branch isnt used is literally just dropped and never compiled in
i just did it with inline else and @field so if i add more arches theyll get their own switch cases added automatically
@field(a, "b") is equivalent to a.b except b is a compile-time string instead of having to be a literal identifier
okok I get it
strings are kinda weird tho
fieldParentPtr or whatever takes a string
fieldParentPtr is just container_of
yeah I know
it takes a string because theres no macros, only compile-time interpreted stuff
so @offsetOf also takes a string etc
its a bit weird at first but you get used to it
yk what I'm gonna try it and see
worst case I dont like it and go back to my C code
yeah
ill be happy to read through your code too if you ever got any questions or want feedback on the zig aspect
is zag a good name 
good to me but theres probably already something called that lmao
https://zagjs.com/ there's this
oh js shit fuck them then
I also found a zig kernel named zag on github but it has 0 stars and hasnt been worked on in a while
so I think I'm fine 
ye
another consideration was something like bowie or stardust, but stardust sounds ass
yeah
(because of the ziggy stardust album)
you're welcome!
or you'll stick with the build system because all others are paiiiin
Nah my current one is really nice
meson + make + kconfig + scripts
It's nice to work with
it's what you're saying right now π
Imagine your whole project setup is literally "install zig, run zig build"
what scripts? do they run on windows? macos? freebsd? π
yes
it's python scripts
ah okay, so i need python as a dependency π
why are you randomly roasting my build system
na, just curious π
you need python anyway as it uses meson
right
and kconfig is done through python
for me it works fine and the build system hasnt really been a pain point
python + meson + ninja + compiler
you know, i've seen projects where a build starts with "sudo make" π
I have a makefile as well but it's more used as a command runner
and i could avoid it
i have just, but not make π
i dont have either technically
i do have msbuild and ninja though which ig ninja counts as a make?
on wsl i have make tho
also i knew there had to be some reason for me to have this itch to not even be looking at meson, if its python thatd be why lmao
im a python hater
there are very very few things in programming i hate more than python
ok, so, first thoughts on irq abstraction things
an interrupt domain (using that name even though not taking the linux model) has some fixed number of input ports that can each be routed to ports on other controllers, and the type of the id for the port varies by controller
some vectors can get given a piece of data if the controller supports it
something like an ioapic has one port per redirection entry that takes no extra data, a cpu has one port that takes whatever the vector number/arm intid is as its data
a GIC ITS has 2^32 input ports and a 32-bit data channel, and can send to one output port with a 24-bit data field
the GICD has one input port that takes a 24-bit data field and is restricted to not change the data field in routing
not fully decided yet if this is how i want to do it tho
Definitely more of a formalism I'll use to conceptualize this though, the impl will have to basically be backwards from that
side quest because i still cant figure out how the fuck im going to do irq abstraction:
rig up some writer stuff to normalize line endings
one bonus thing i can do if i make a custom writer is make one that buffers up and then discards extra for counting
idk
because log ring as long as i add an extra count for each instance of newline i can normalize endings in place anyway
ok so making the base lowest-level interrupt domain percpu will actually make so much of this shit cleaner
just how the hell do i actually do the data structures for forwarding
def gonna need to give the irq object some notion of port vs vector though
just to be able to keep the internals even slightly separated here
probably still keep a context pointer and isr for each irq object, and irqdomains will just be required to have a forward function you can use for that
(since if some irqs need an irs pointer then they all have that pointer of space anyway so may as well use it)
so an irqdomain will have a function reserve that returns irq objects, which can then have their ISRs set and vectors queried for setting up higher level things etc
and an irqdomain will have a forward function that operates as an ISR and routes an IRQ to the next level upward
and enable/disable optional functions probably
and reserve can take a fixed vector in the context of that irqdomain in case the firmware or whatever specifies it, or a list of options actually makes more sense, and probably also a cpu affinity thing as well? im still waffling on whether i want to go full percpu irq tables
as an example here, a device enumerated in acpi tables says it can have any of 8 different GSI numbers (call them 24-31 for example why not)
so it calls reserve on the gsi domain using those 8 options
the gsi domain then goes through the ioapics until it finds a free redirection table entry with one of those GSIs
it then calls reserve on the x86 vector irqdomain to reserve a vector for that io redirect table entry
the x86 vector irqdomain looks at the provided IRQL and affinity and goes through cpus looking for a cpu with a free vector of that priority
if that fails it then loops through looking for a vector with exactly one irq, and again for 2 or less, then 4 or less etc (thanks @floral prawn for that idea)
once the x86 vector irqdomain finds a vector/cpu pair that can handle the new irq, it returns it to the gsi irqdomain
the gsi irqdomain then plugs that info into the redirection table for the gsi it reserved and returns that gsi as an irq object
and then the device that got enumerated by acpi (and had an interrupt in its _PRS) puts its ISR into the new irq object and puts the "vector" of that IRQ (which for the gsi domain is a GSI) into the acpi resource and calls _SRS
a device that has an MSI would call the x86 msi domain, which would use the same x86 vector domain to get a cpu and vector, and then return an irq object with the port changed to the msi address and the vector changed to the msi data
and as an aarch64 example
the actual processor side is way simpler, theres no vectors, only attempts to load-balance
so a device requests an irq from the GSI domain using a fixed vector from the acpi tables and all reserve has to do is pick a processor for it
or for MSI, a device requests from the gic msi domain.
the gic msi domain requests from the LPI domain, which probably uses some tree system for allocating them
the gic msi domain then figures out what cpu to route the msi to and invokes the MAPI command on the ITS for the source deviceid, allocated lpi, and target processor
and then it returns the GIC_TRANSLATER its address as the port and the eventid it allocated for the vector
why is my brain only able to figure out ideas for these design challenges why im half-asleep and delirious from exhaustion in my bed at midnight
why am i out here reading the fuckin "intel virtualization technology for directed io" spec π
(found a bug in my ioapic shit thats why)
it was going to crash if i ever tried to set the destination affinity of an interrupt to not be processor id 0
the thing is the only thing im installing with ioapics so far is the SCI
which goes to the BSP
which is generally pid 0
(clarification, it would crash if the processor id overflowed a u8, which the legacy-style xapic id is left shifted 24 so on my small-enough-to-not-x2apic-by-default tests that would be true for everything excecpt the bsp)
are u designing your irq interface?
slowly but surely yeah
got distracted by baking cookies atm but ill be back on the grind later
@last pebble #1312232715193683988 message this message forward is a ramble about the design im going to go with, inspired by https://people.inf.ethz.ch/troscoe/pubs/humbel_plos_2017.pdf this paper
its similar to linux irqdomains but i dont have global interrupt numbers
so if you want a unique number you use the pointer to the irq struct
Interesting
that and the internal per-domain interrupt number is two numbers not one
(the paper calls the two numbers port and vector and gives examples of what they mean for a bunch of different irq domains)
ok here goes nothing
and found the first stupid bug
i was trying to install the timer interrupt before setting gs_base
@last pebble https://codeberg.org/Khitiara/imaginarium/commit/c71e040c80d6c1ab5b8d53cf5786950a2541b057 if youre curious (and im happy to explain things)
new irq abstraction is working on x86_64
(apart from MSIs which i still dont have yet but will be so so so so much easier with this abstraction anyway)
whats the tldr of the abstraction?
pub const Domain = struct {
pub const VTable = struct {
reserve: *const fn (*Domain, ReserveOptions) ReserveError!*Irq,
move: ?*const fn (*Domain, *Irq, from: Id, to: Id) MoveError!void = null,
mask: ?*const fn (*Domain, *Irq) MaskError!void = null,
unmask: ?*const fn (*Domain, *Irq) MaskError!void = null,
destroy: *const fn (*Domain, *Irq) void,
};
this
is basically the tldr
and each domain can reserve from other domains
is domain also an abstraction for the irq controller?
so theres a domain for x86 vectors that lets you get a vector on a specific or any cpu, theres a domain for GSIs that handles ioapic redirection, theres a domain for the local apic stuff that does lint and timer etc
the irq controller will provide one or more domains
for example the apic provides two: one for GSIs that routes through ioapics and one for the LVT stuff
so irq controller owns a list of domains?
(both of which call into the x86 architectural one that handles x86 vector allocation)
effectively yes
How would generic code allocate interrupts?
How would it know what domain to call into
the requirement i have rn is an (architecture, irq controller) pair is requried to provide a domain for GSI routing and one for IPI routing
so rn my uacpi shim calls into the gsi_irqdomain which is swapped out based on the combo of arch and irq controller (rn theres only one implemented pair because ive not done arm for this yet)
so there are like fixed domains all arches must implement?
so legacy/msi/ipi are three domains?
yep
or the three domains there will be in total
there are more but those are mostly internal, and devices can stack others on top
a gpio controller will probably provide its own domain which reserves a GSI and then has a vector for each pin
yeah that makes snese
as an example
and then internally theres the x86 vector domain, which just reserves vectors on processors and is called into by the main ones
is the client code expected to always know the top most domain it wants to allocate from?
yes and no, the actual allocation will be handled by the IO manager's resource allocation
which has MSIs and GSIs as types of resources
i see
but in general the client code will know what domain it wants to allocate from because thats either fixed by the context (an IPI will always use the IPI domain, etc) or discovered by enumeration (a gpioint resource tells you to get the domain from a gpio controller, a basic acpi interrupt resource tells you to get from the GSI domain, the pci(e) msi capabilities tell you to get from the MSI domain, etc)
yknow hang on i should test this with smp
ive only tested uniprocessor so far
i suppose
ok yeah good i tested with smp
- pci interrupts will be requested by generic pci code so it knows the domain, u could even have a pci domain maybe?
- msi interrupts will be requested by msi aware driver specific code
- legacy/generic interrupts will be requested by isa devices on x86, this also partially overlaps with pci but thats x86 specific probably
when requesting an interrupt within the domain u do tell it a number or no?
E.g. for pci i want a specific GSI, whereas for MSI i dont
how is that handled
you can optionally provide a list of options
and itll give you the first option that isnt taken
what are ReserveOptions
so allowed is like a list of desired vectors?
yep
i assume for msi u can leave that as null?
correct
interesting
even for GSI i might eventually let you make that null and have it just pick any GSI but im not sure about that
i mean for pci interrupts u must use whatever gsi acpi told u
the idea with it being a list instead of a single value is that acpi already gives a list of alternatives for its irq descriptors so may as well bake that concept into this
yep, thats why i dont have take any gsi implemented yet, its not needed in practice so far
what even is the use case for take any gsi?
both isa and pci have hardcoded irqs no?
i dont think there actually is one, the type allowing it is more for other domains
yeah
if i find one i can add it, rn asking for take any gsi errors
the other thing btw that probably seems weird at first is that the interrupt number within a domain is actually a pair of numbers
for the GSI domain one of the two numbers is always 0, but having two numbers simplifies a lot with the internal domains
(for the base x86 vector domain one number is processor id and the other is vector, for ipi and lvt domains one number is processor id and the other is an enum, for MSI domains one number will be address and one number will be data)
effectively yeah
the pair is whats unique within a domain, rather than either single number being unique
theyre both u32 so i could just pack them into a u64
one thing i dont like about these papers is they never have damn code examples
but whatever
ikr
so this two number thing is duplicated per domain per irq allocation?
yeah its a field of the irq struct
yeah but u may have nested domains right
oh so the object is nested also
which is the information for it in the context of the nested domain yeah
I asked a clanker to read the paper and sketch top level api 
https://pastebin.com/MHbUrWEe
Pastebin.com is the number one paste tool since 2002. Pastebin is a website where you can store text online for a set period of time.
lol
is that even remotely correct?
maybe? idk
wait so
/*
* Globally unique port identifier (paper: assign a globally unique id to all
* ports in the decoding net).
*/
Is this true?
port is one of the two numbers
idk where the thing came up with making it global, i dont remember seeing that in the paper
i did diverge from the paper somewhat
in making the pair global
in the paper there are ports which can receive an irq with a vector as attached data
What does your irq struct look like?
i found it easier to just treat it as a pair being a key
domain: *Domain,
id: Id,
/// the interrupt's polarity
polarity: pic.Polarity = undefined,
/// the interrupts trigger mode. the architectural interrupt controller SHALL ensure EOI signals are properly sent
/// for level-signalled interrupts.
trigger: pic.Trigger = undefined,
/// the priority of this interrupt, which is also the lowest IRQL which will mask this interrupt. set during
/// register based on the fixed or found vector and should not be modified directly outside of the
/// interrupt dispatcher/manager
priority: Irql = undefined,
/// whether the interrupt has a fixed vector number. set based on the target passed to register, and should not
/// be modified directly outside of the interrupt dispatcher/manager
fixed: bool = undefined,
/// check callback. called for all IRQs on a shared interrupt vector, and handler is only called if check returns true.
/// NOTE: if an irq is installed as sharable, check SHALL NOT be null
/// NOTE: if an irq is installed as non-sharable, check MAY be null and the handler SHALL be run if check is null
/// NOTE: edge-triggered irqs are never installed as sharable, even if specified as such in the register options
check: ?CheckFn = null,
/// the actual handler function, required
handler: IrqHandler,
/// the affinity of the interrupt, ie the cpus on which it is allowed to be run. ignored for the SGI domain
dest_affinity: ?u32 = null, // TODO cpu mask
/// the driver that installed this IRQ gets up to three userdata pointers for whatever it wants to get in callbacks
userdata: [3]UserValue = @splat(.empty),
parent: ?*Irq = null,
fields
makes sense
I think what it means by this global port id is like 0 -> pci, 1 -> msi etc
u can use struct pointers instead
the clanker or the paper?
both
i dont remember seeing global port ids in the paper at all, and they are clear about examples using port the way i use it
ah ok
theres multiple ports per domain
or can be
unless theyre overloading the term
for some reason
honestly the main thing i took from the paper is the two numbers per irq within a domain bit
yeah i get the overall idea
oh damn i see my bug
oh nope
didnt fix
ok yeah i see now
going to take more pain to solve properly
fuckin x86 startup bullshit
@last pebble did you know that whether you need to shift the destination register of the ICR over 24 bits doesnt depend on if the current processor is in x2apic mode, it depends on if the target processor is in x2apic mode?
which means that for startup you have to do the shift sometimes anyway even in x2apic mode?
how do u even know the state of destination
you dont π
but when in x2apic mode i dont shift the apic id over 24 and then it never sends the fuckin init and sipi to the right AP unless i force do that shift in my ap startup
π€
but once the AP starts up and switches to x2apic mode the ipi doesnt send if i do the shift
it might actually correlate with whether its a lapic or x2apic entry in the MADT
maybe worth looking at linux
yeah


