#tabula imaginarium: osdev experiments in zig
1 messages · Page 2 of 1
Which manual, there are slight differences between them and I wouldn't be surprised if this is an Intel vs and quirk
specifically the intel sdm. you might be right though since I am running on an amd processor
I'll have to dig up the amd one to see what it says
been using the intel manual since I find it more readable for reference stuff
Yeah I default to the Intel manual
I just find it more pleasing to look at while reading lol, please don't judge me
But there are some subtle differences in a few places
pulled up the amd manual and it's reserved must be 0 on amd for higher level page structures on amd apparently
Ah there you go
it's def annoying because I can't make the recursive mapping global but whatever
at least now I know
time to add a print thatll trigger an ungodly amount of times for debugging lmao
it just goes on and on and on and on lmao
brings me back to my rbtree debugging sessions
of figuring out where it went wrong in the 20k lines of logs
well im at 25k lines and counting
i can probably kill qemu and remove the print now
just wanted to make sure it was going through everything and ive got enough of a sample for that now
and the serial out to log this slows it down so much lmao
nvm it finished lol
26061 lines of that
replaced it with just a count in the kernel that it logs lol
the 0 unexpected state is nice
and then i had to spend a hot minute figuring out a dumb bug where the compiler was like oh the limine requests never get modified and this is a static executable ill just inline the access to the response pointer to be always null and always panic lmao
got that figured eventually at least
thank goodness for std.mem.doNotOptimizeAway ig lol
anyway i think im actually at the point i might be able to start working on the actual page allocator bit
volatile
std.mem.doNotOptimizeAway
volatile exists for pointers but exported global vars aren't pointers
glad i put in these logs, i found an off-by-one error today with it
at some point i gotta figure out how to swap both cr3 and the stack pointer at the same(ish) time safely
the new stack is in the new mappings but not mapped in the old mappings is the issue
if i can do that i can actually take my bsp kernel stack out of my actual kernel's data section too
ig my best option will be to load both addresses into gp registers and then load both properly from that
ok the semi-new plan for memory management:
- a simple allocator based on keeping an expandable free list of pages to be used for small object heap. this is to be implemented by having the first 8 bytes of each free page be a SinglyLinkedNode in an atomic linked list for the fast path, and the slow path allocates a new page directory of free pages from the block of virtual memory space reserved for the pool. this simple page allocator backs a GeneralPurposeAllocator which backs a number of MemoryPools
- pool is used among other things to allocate virtual address tree nodes for the main page allocator, which is capable of multi-page allocation and both bottom-up and top-down allocations. the bottom-up version backs a GeneralPurposeAllocator, and the top-down version is used for stacks.
- the virtual address tree is a struct itself so multiple address spaces can be managed - this to accomodate both paged and nonpaged allocations (nonpaged only for now as i dont have disk io yet)
the small object heap will probably be made available to general kernel things as well, not just for pools, but its not suitable for everything as any allocation larger than a page will always fail from that allocator
@last pebble btw, does uacpi_kernel_alloc need to handle large (> PAGE_SIZE) allocations?
Yes
then thatll have to run on the main address space thing
which is good tbh. most of the things im doing in my main kernel so far are pooled so uacpi will let me easily test the main alloc without having to swap things over that dont need it
my driver stack will probably use that too though for driver objects
though i can probably make driver objects globals tbh
this whole mm thing took me forever because it took me a while to mentally process how to allocate the tree nodes
i also have a syspte space, inspired by (aka like 75% a port of) the reactos one, which i use for temporary mappings of either io or other pages
e.g. if you request a zero page instead of a free page from the pfmdb and it only has undefined-content free pages, itll use a syspte to map it for the @memset and then unmap before returning the PFI
terminology:
- pfi = page frame index aka index into the pfmdb aka
PFN_NUMBERakaphys_addr >> 12. - pfm = page frame metadata aka
struct pageakaMMPFN. - pfmdb = a global array of pfms, where a pfm struct is guaranteed to exist in memory if the page is usable (theres a bitmap to check which pfms are real so the syspte stuff etc can properly refcount normal pages without having to create a full pfm for every possible physical page that might not actually exist)
OH GOD THE COMPILER IS REORDERING THINGS
debugging page allocators is pain evidently
i added a log and the behavior changed
though it just changed from doing the second iteration of the loop first to doing the first iteration first
oh i was looking at this all wrong
the page fault error code was nonzero so i checked the other bits and thought it was a write to a nonwritable page
but the code is 2 not 3 so the page isnt present
oh god is this a TLB thing
idk why itd be caching that page as non present
IT ISSS
fuck
i should probably actually write an invlpg thing so i dont just reload cr3 every time
OH FUCK THATS WHY
its not removing the pfm from the free list right
so it just grabs the same page twice
same physical page that is
for both the pdpt and the pd
now thats fixed at least
I CAN ALLOCATE THINGS \o/
--- a/src/krnl/hal/mm/pfmdb.zig (revision HEAD)
+++ b/src/krnl/hal/mm/pfmdb.zig (revision Staged)
@@ -145,8 +145,8 @@
pfm._1.alloc_end = false;
pfm._1.status = self.associated_status;
pfm._3.flink.next = terminator;
- if (self.last != terminator) {
- pfm._2.blink.prev = self.last;
+ pfm._2.blink.prev = self.last;
+ if (self.last != terminator) {
pfm_db[self.last]._3.flink.next = pfi;
} else {
std.debug.assert(self.first == terminator);
this was the single most annoying bug to find lol
this... this is progress
well technically its not because this used to work on old mm
but it gets pretty far on new mm\
just gotta figure out why this is index out of bounds ing on the map_io
and also figure a way to make it so i can not leak mmio space lol
the way ive got this set up now (which is the third iteration on mm design lmao):
- a region of virtual memory is dedicated to nonpaged pool (name comes from winnt things but doesnt matter)
- this is implemented as a buddy allocator of virtual memory.
- the pfmdb's
pfm._3.share_countis canniballized to be used as a free/not-free flag for buddies since that field is normally only used for swappable page refcounting - the smallest unit is one page and the biggest unit is 2M (one PDE, though its mapped using 4k pages since the pfmdb cant easily get contig. phys pages
- if the allocator is out of top-order blocks it maps the next 2M from the address space until that runs out (which is a kernel panic) and then splits as normal for a buddy allocator
the nonpaged pool is used to implement the vtable of a zig std.mem.Allocator which is then used as the backing page_allocator for a std.heap.GeneralPurposeAllocator which is used for most things though the page allocator can still be used directly for large allocations if need be
and then i use a syspte type setup for mmio mappings which is currently broken lmao
i think zig's GPA is a slab allocator but im not smart enough to understand it and be certain
really wish i wasnt always missing stack frames when tracing from an interrupt handler
adding a log before i start tearing this apart
but hey maybe my hunch will be right
it isnt
fuck
you can just use rsp+rbp from the interrupt handler to trace past that point
rsp is pushed by cpu and rbp (hopefully) by you
yeah ill just have to modify the zig stack iterator ig
no you won't
iirc you can create one from rsp+rbp
std.debug.StackIterator.init(@returnAddress(), @frameAddress())
close enough
first_address being null means just get the first address it finds
which is the first param
but why would rbp be null is my question
you can just StackIterator.init(frame.rip, frame.rbp)
no i mean the first param is null
ah
^
that should let you trace from the point where CPU left off
i mean i can quickly check lol
it's really as if you did @returnAddress() and @frameAddress() right before you got interrupted
yeah
i am running in releasesafe fwiw
passing rip for the first param didnt work btw
i think its because rip is the next frame up from where rbp is
so it never gets to rip when iterating
the first_address means skip until you get there and that never happens because its starting at the return address of whatever rip is in
secret hack: preinitialize the trace with rip as the first addr and then iterate starting at offset 1 :)
sadly still getting the same set of frames
except instead of my iret being on top the interrupt rip is
well i got it mostly working now at least
map_io/unmap_io works and the nonpaged pool allocator seems to mostly work
though it eventually panics in the allocator somewhere so ill need to debug that more later
but progress!

the big problem rn is the panic in the allocator is it tripping a debug assertion somewhere inside the std's generalpurposeallocator and thats annoying
and idk which assert because its the debug asserts which are implemented as
if(!cond) unreachable;
which just triggers a panic with "unreachable code reached" in safe modes and is a noop in unsafe modes
and freestanding debug info loading is iffy sadly
i might be able to make limine provide the debug file as a module though and then load it somehow tbh
worst case you can always output the return address (if you implement unreachable() as a function), or the instruction pointer if its not
it'll give you something
unreachable is a keyword
the rip helps a bit but the compiler decided wait all these panic calls look the same let's merge them so I have no idea which one tripped
why?
tbh I think it's best as a macro
oh wait nevermind, I thought this was one of the langdev projects xD
oh no lol this is using zig
turns out fixing my stack trace addresses is fairly easy thankfully
sadly the PRs im waiting for to be able to dump my own stack trace printing code are not yet merged
oh cool the compiler segfaults now, fuck
id just updated zig too and suddenly rip
i may start ripping up some of my code though and if that eventually fixes it thats great
thats what i get for using dev versions ig
yknow what i got some energy time to compile my compiler in debug mode
the way building zig from source works is kinda wild tbh
theres a minimal version of the zig compiler in wasm in the repo
first thing it does is convert that wasm to c and then compile it with the host c compiler
then it uses that to compile a slightly more complete compiler
and then that to compile the full thing
ok the compiler is panicking in my read_madt code now that ive got the compiler in debug mode
(the panic is an unreachable which gets optimized away in release modes which then led to UB and a segfault)
a bunch of other shit is unhappy because i dont parse the madt anymore
but at least it doesnt segfault the compiler now ig
found the error in my code that was triggering it (shouldve been a compiler error whoops)
IT WORKS
very slowly
but IT WORKS
in wasm? like in that lispy wasm source language?
compiled wasm binary
there's a build command to update it from the source
interesting
it gets converted to c and then run (its wasm because that way it doesnt have to go to c in theory) and that has just enugh to compile a stripped version of the compiler which can only output c
that then gets used to compile the full compiler
which in theory should then be idempotent if it compiles itself
ok time to test a basic ACPI processor object driver (it literally just attaches a pointer to the device object to the lcb for now)
just realised im allocating my LCBs twice whoops
also really ought to make a decision if i want to stick with LCB or swap to calling them PRCBs
or some other name
fuck
i put the bit size of a thing instead of the byte size and that was my pci issue
realised because comming out the ecam bit fixed that so i knew to look deeper there
there we go, nothing reporting no HID/CIDs anymore
(no HID/CID would mean that the ACPI driver reported it based on a device object that only has ADR and no other bus driver picked it up, which in this case is because my pci driver was a factor of 8 off on the numbering of things while using ECAM access and thus the addresses never matched)
next project: slab time
contender for least hinged line in my codebase btw
whatre you doing? oh just exporting a thing named kstart3 with the symbol name kstart2
ah yes, the CRIME bit
offset 0x14, bit 24
the crime bit
oh yeah i should probably fix my pci helpers to use the a register always
is that NVMe?
yes
yeah, some sources say you should do that but i don't think any real machine enforces that
"Controller Ready Independent of Media Enable" bit in the CC register
yeah mine doesnt. hence why ive not bothered
but i want to swap most of my mmio to use inline asm anyway at which point its easy to enforce that register
rn its all pointer access
which is like fine but might break if the compiler uses the wrong instructions
@fair dragon
ok i think i really need to implement BAR stuff in general before i can implement an nvme driver
so figuring that out will have to be the first step here
which means i ought to figure out device resource querying infrastructure
will say the biggest benefit to nvme so far is just that the spec is free and open online
looking at you, pci
wonder how good i can get this inline asm to generate
ideally i want this to generate as offset(%reg) if the offset is known as an immediate or as (%reg, %reg, 1) if not
ok does anyone here understand the "m" constraint for inline asm?
do i do asm volatile(... : [name] "m" (&thing)) or asm volatile(... : [name] "m" (thing))
i believe the latter
i think it wants a reference, so just thing should work as expected
ok
im trying to pass an array indexing expression to it and hoping that works lol
yeah that should work
ok
assuming zig's inline assembly tries to resembly C in any way
oh right i should turn my logging to debug here lol
(as it should)
its basically gcc inline asm and that gets given to llvm
yeah it should work exactly the same then
so any memory lvalue will do
or lvalue in general, im pretty sure passing a stack variable to an m constraint will allocate it on stack
const value = asm volatile (
\\ movl %[ptr], %[out]
: [out] "={eax}" (-> u32),
: [ptr] "m" (block[offset / 4]),
: "memory"
);
// const value = block[offset / 4];
new uncommented code isnt working but the commented out bit is
block is a pointer to an array of u32s
huh that's weird
(pointer to array because i know the length of ECAM so i can make it that instead of a slice or many pointer)
yknow what
gonna try [ptr] "r" (&block[offset / 4])
see if its just the m constraint being fucked in zig
that will work but you have to dereference it in the inline assembly template
by nooby of course
lmao
oh lmao of course
i think i remember nooby telling me to avoid the m constraint too
frickin zigbugggg
well i can do (%[base], %[offset], 4) ... : [base] "r" (block), [offset] "r" (offset / 4) now at least
the whole reason i wanted m constraint was to have that use offset(%reg) if reg was doable as an immediate tho
but fine ig
oh btw do i need to out to the addr register repeatedly for legacy pci access if the addr doesnt change?
alright
oh huh linux is cooking somethin weird with their legacy pci access code
say youre writing a u16 to offset 0x12
common advice is to outl port 0xCF8 to 0x10, do an inl at 0xCFC to get 0x10..0x13, do some bit math to replace the 16 bits you want, and then do an outl with the full 32 bits
linux says no, we're going to outl port 0xCF8 value 0x10, and then outw port 0xCFE the u16 they want to write
a 32 bit would be 0xCFC, 16 bits to either 0xCFC or 0xCFE, 8 bits to any of CFC..CFF
and same for reads
but with in instead of out
the hell are those nerds cookin
sometimes i feel like going ham with partial staging and push 10 well named commits
other times i name the commit AAAA and push it all at once
today is of the former kind it seems
This is probably my most common programming sin
same tbh
lmao
and scrolling down a bit we got idk shortly followed by idk2
anyway my brain is starting to get fried so its time to call it a night
i started work on nvme and changed a single bool to speed my uacpi init up by 500x
good enough for a days work
alright its nvme day 2
which is to say reading a spec day 2 and/or device resources infrastructure day 1
really not sure how i want to structure device resource querying in my io stack
actually nvm i got idea
my big thing was not being sure how to store/cache the resource list
and then i realised that i can just make the driver do it
device driver can filter down what it actually cares about anyway
and with that the enumerating resources (BAR config figuring out, acpi _CRS method, etc) being a theoretically kinda expensive operation isnt a big deal
mostly the acpi bit is the expensive part but like
yeah
i ended up adding a fourth io dispatch return status enum value
"partial"
might have to add partial_pending too
the possible non-error return codes from a driver's io dispatch routine before the addition were complete, pass, and pending
pending means its async from the driver's pov (which could just be signalling an event so blocking from the caller pov instead of callback based async io)
pass means i got nothin give it to the next lower driver
and complete is fairly self-explanatory
partial would be for enumerating lists of things, say i got this stuff but the next lower driver might have more
like a thing on the pci bus enumerating resources that also has an ACPI device object, the device driver says pass, the pci bus driver puts in the BARs and pci interrupt stuff and returns partial, and the acpi driver checks _CRS to fill out more of the list and also returns partial
io.execute_irp marks if it gets any partials when iterating but otherwise treats partial like pass
and if it gets to the end of the driver chain with all drivers passing thats an error, but if any of them said partial then its complete once its at the bottom of the driver stack
i may yet switch to recursion for passing the irp along though
so a driver can swap out the irp and then do post-processing to restore state after
whatever i do for resource enumeration will be only the second function code i add though
rn the only one is query property which takes a property uuid and an out pointer in the parameters
kinda funny that read and write will be so late on the list of io functions i add support for lmao
ok yeah def doing the recurse version i think
its a bit closer to how windows does it but in this case i dont think thats a bad thing per se
looks like i wont actually get anything done today, rip
tbh not getting stuff done until next week most likely
turns out took longer than that even lol
also i should really rip apart my docs folder because theres some very no-longer-correct stuff in there lol
update: migrated to split-out zuacpi and got everything working
also split up my acpi enumerating stuff a bit so that the _SB namespace gets its own device object
and lastly fixed a bug whereby uacpi's install_interrupt_handler was failing to back-update the cpu-side vector so it was trying to install the IDT handler always at vector 0 which is obviously wrong
nice
also (inspired somewhat by some stuff in #1326937732122935379) i may end up doing some absolute linker nonsense sometime soon to decouple percpu variables from a central prcb structure by just making the prcb structure itself a percpu variable on some particular scheme
the idea:
pub fn define_percpu(comptime T: type, comptime default: ?*T) type {
return struct {
var value: T linksection(".percpu") = if(default) |d| d.* else undefined;
pub inline fn offset() usize {
return @intFromPtr(value);
}
pub inline fn current() *addrspace(.gs) T {
return @ptrFromInt(offset());
}
pub inline fn on(cpu: u32) *T {
return @ptrCast(&prcbs[cpu][offset()..].ptr)
}
};
}
something like this on the zig side, and then use
percpu_start = .;
percpu 0 : AT(percpu_start) {
*(.percpu)
. = ALIGN(4096);
} : percpu =fill
. = percpu_start + SIZEOF(percpu);
and then at mm init i can use externs to get the percpu block initial value and load it in, or maybe get limine to provide the file for the executable and use that
i might also just be able to do a
var value: T linksection(".percpu") addrspace(.gs) for all i know tbh
maybe time to resurrect this
updates since the last time i posted:
- acpi device resource enumeration is a go
- updated to the latest version of @hoary aurora's excellent disk-image-step for building the disk image for testing. it still cant do GPT disks yet though so no uefi yet
- idk there might be other things im forgetting
also i blame infy that my next project on this os is liable to be a log ring lmao
crosslinking #1367927268294397982 for the disk image building tool as well
only warnings im logging now are "no driver found for device xxxx"
which is a nice place to be
of those most are for PNP0C0F
now try real hardware 
I dont have an easy way to rig up a uefi capable image and my only loggins is 0xE9
so maybe someday
thats actually part of why a log ring is on my todo list, just so i can have backlogs saved to then dump to serial once my serial driver is up and going
and i should figure out a framebuffer too tbh
also i should make a futex primitive if only because the zig std sync primitives can do all the work on top of a futex
yknow if i wasnt feeling like making a lockless log ring a condition variable would be great for synchronizing it
yknow if im going to start doing framebuffer stuff again i should probably actually map the framebuffer somewhere
i think i did something backwards
thats better, got a font rendering now
using https://leickh.itch.io/monofont-10x16-png as font
now to make a log ring and wire it in
why not flanterm
it's a relatively complete implementation and it's pretty well optimized
my kernel is written in zig and the fact that i have a single full external dependency in uacpi already bothers me a bit
i dont want to make bindings for flanterm and even if i was willing to make bindings i want nothing to do with any more dependencies
bindings for 2 C functions is a bit of an overstatement
but yeah the "purity" argument is fair
(also this kernel is meant largely as a learning exercise and i aint gonna learn anything if im just pulling in libraries for everything)
also writing your own tty is just fun, with stuff like ansi csi etc
yeah
the goal here is learning and fun, not trying to get a maximally featured thing as fast as possible
wondering if theres a reason linux's printk ring stores info in a separate array/ring as the descriptors
Yes
There's a commit not that long ago that changed that
Just less stack usage because most code doesn't care about this info
Also more cache friendly I guess
MultiArrayList moment for me then probs
Zig has that?
yeah it rules
takes a struct and automatically generates separate lists for each field
though its not actually a great fit here since its only meant as an arraylist so for a static sized array its less usable
Damn
you can get slices and access each field individually and theres a function that reassembles the struct from the fields at a given index too
its a great use of comptime
it also does stuff with tagged unions but that just splits out tag and the untagged union payload as separate arrays so ive not found it as useful
i made a bounded version for imaginarium that basically is the same but uses a statically allocated buffer instead of taking an allocator to use
Interesting
ill probably do things semi-manually instead of using multiarraylist for the log message info tbh
though i could reuse my MultiBoundedArray and ignore its resize-within-fixed-capacity ability
just init it at full size immediately
yknow what fuck it making a MultiArray type that doesnt do resizing for this purpose
tfw i manage to make the compiler print out a 32768-entry array in an error lol
not kidding btw
wtf is that XD
lol
the types are different because the statically known array length is different and it goes and prints out the whole dang thing in the error
idk what it says above that mess because it overran the length of my terminal's buffer
the actual issue is the "entries - 1" should be "entries - 2"
this is the zig equivalent of std::unordered_map<std::string, std::string>::const_iterator (std::unordered_map<std::basic_string<char>, std::basic_string<char>, std::hash<std::basic_string<char>>, std::allocator<std::pair<std::basic_string<char>, std::basic_string<char>>>>::const_iterator)
can already tell this is going to be one of those bits where i end up writing a ton of comments on how it all works before i even touch actual implementation
Yeah this thing feels like one of those things where I start hitting the limitations of my brain
Like when I was implementing rb trees
god trees destroyed my brain yeah
i ended up doing AVL trees instead of RB trees because i understand them marginally better
if i figure out this log ring stuff ill be happy to help you figure it out for your end lol
Yeah ill share my findings as well
Although ill probably start doing it a bit later, after doing mm bringup
ah yeah mm bringup is important
Currently I'm porting an early memory map allocator from my loader
So I can allocate struct pages etc
But excited to see how your log ring turns out
What does it do?
though i still kinda want to make a prekernel like 50% for mm bringup being cleaner there and better management of kernel pages
Atomics with a ton of comments are great because of all the subtle differences
starting from limine hhdm:
- in a static buffer in the data section, allocate copies of bootloader-provided information
- allocate new page table root and set up for recursive page tables
- using memmap allocation, map the kernel in the new page tables
4 my bootstrap thread stack is part of the kernel data section even though i use limine so its now fine to switch to the new CR3 - allocate SYSPTE space and PRCBs (which i call LCBs), since those have static size and virtual address
- allocate the PFMDB (aka list of struct page) and map it and its bitmap (which is used so i dont need a struct page for every mmio or reserved page)
- put free pages except for the block im allocating from into the PFMDB
- put the remaining free pages from the allocation memmap entry into the PFMDB. this prevents allocating phys pages until the next step is done
- fill out pte back-pointers in the pfmdb and fix any incorrectly-free pfmdb entries to make the pfmdb free list usable
- give the nonpaged pool its first PPE worth of physical pages to run with, after which normal kernel memory allocation is usable
- not yet implemented: reclaim the
.initsection and any other bootloader reclaimable stuff
https://github.com/Khitiara/imaginarium/blob/main/src/krnl/hal/mm/mminit.zig this is code, i think its fairly well commented if you want to see details
the reactos version starts with the PRCBs and the basic recursive page table (with kernel mappings) already provided by the loader
allocating the PRCBs is actually the only reason i need early table access from uacpi fun fact
if theres any questions on how this (or indeed anything in imaginarium) works LMK
What's a PRCB?
And why do your struct pages need a bitmap?
at any given time theres an address that is an array of them indexed by LAPIC id and GSBASE points to the current cpu's one
the bitmap is set to 1 for normal memory pages and 0 otherwise. lets me avoid allocating pages worth of structs for reserved/unusable physical pages without having to also then do lazy mapping of them or whatever to allocate entries for mmio pages
windows and reactos both do that too
I'm a bit confused, why cant that be sorted out at pfndb creation time?
Why would you have to allocate struct pages for them
this mostly guards against accessing struct pages for pages that dont have them
the same code that does mmio mapping also does temporary mapping copies of other pages for stuff like io in case the process with an io target buffer page isnt running, and for additional mappings of normal memory it needs to refcount the phys pages so they dont get swapped out
Is this all workarounds because no hhdm?
kinda yeah
Yeah
also again this is how reactos does it and im pretty sure also how NT does it
yeah it took me a minute to figure out why they did it and then i ran into where it helps when implementing my map function
Makes sense
@last pebble btw do you happen to know how kprint sinks get notified when new entries are available? is that even a thing that happens or do serial log sinks run separate from the log ring? started to wonder and the only thing i could think of is some sort of blocking event object which would kinda hurt the whole lockless premise (though ig locks still not needed just notify)
console_unlock
Goes through every console and prints to it from the last seqnum to current
This only happens if it managed to acquire the console lock
There's a separate work queue to do logging to consoles though
If u scroll prink.c you will see all of that stuff
and each console has its own seqnum
but in theory they should all be caught up
alright, thanks
ah i see, neat
next thing, even before log ring, is adding a way for the bootloader to request the mm init to map chunks of memory
which will be used at first to map the kernel binary file and (if the file exists in the disk) the separate debug info file
which will then allow stack traces that arent just the address
also i might be able to use it for other stuff too
framebuffers etc
oh neat found a bug in the zig std
in the stack trace unwinding code even
they handle signed eh pointers by casting the base addr to i64 which generates a panic when its a higher half address
im not 100% sure why this isnt getting source location info, might be some jank with how ive got the debug loading hacked in, but even getting symbol names is a nice improvement rn
oh thats why'
i had my page allocator doing one PDE at a time and thats only 2M
and it wanted more
ok so that last one is a bit off
but im not surprised it has no clue what to do about address 0
as for how this works, i use the kernel file and internal modules features of limine to get the kernel binary and optionally the external debug symbols file
if it has both it uses the external debug file to get full info
if not it uses the symtab and stuff that are in the main elf file
and in all cases uses eh_frame stuff instead of just frame pointer now
the only major updates to stuff outside of the debug/trace code to get that working:
- add a function for the boot protocol handling code to request mapping large blocks of pages (todo: memory typing/cache stuff for that)
- use that new function to map the kernel file and debug symbols file (latter from limine internal modules)
- add some extra orders to my buddy allocator to handle single allocations bigger than 2M/one pde
notably, the zig std functionality for loading external debug symbols is used here, which means that if objcopy/whatever puts the expected crc of the external symbols in the main kernel binary then that will be verified
so turns out its not actually using eh_frame tracing like id thought
it is still getting symbols now at least
theres more hacking i could try to integrate the zig std eh_frame support but meh
at some point it aint worth the work lol
as best i can tell theres only two bits missing for me to use zig's std eh_frame unwinding
one type and one function, which the std has defined as void and returning an error respectively on freestanding
that being ThreadContext and getContext
ThreadContext is defined to ucontext_t on posix-y systems and to CONTEXT on windows, and getContext captures register state using a variety of methods (on windows it calls RtlCaptureContext, on no-libc linux it runs a custom routine, idk what it does on macos and linux-with-libc)
def something i could plug in since its basically the interrupt frame/saved registers type that i already have for interrupts but itd mean even more modifying std
next zig version i may be able to rip out my intrusive collections stuff
@ornate oak idk if you noticed/knew this but on master they changed the std linked lists to be intrusive (mostly, they dont do the fieldParentPtr for you but thats realistically fine tbh)
uuuuh how is it intrusive if not with @fieldParentPtr?
its just the raw ungeneric version, so you have to do the fieldParentPtr yourself
so the node struct is just the node, no item field, and you give the list pointers to it
oh okay that's good
ive got an untypedqueue in my intrusive lists thing that the intrusive one is built on top of and that is basically the same as the new version of std SinglyLinkedList
the commit changing it over ends up adding a bunch of fieldparentptr usage in std wherever the linked lists get used
that's completely fine to me
that's how it should work imo
yeah
i may end up not fully adopting it because the version in std are missing some weird operations i use but its a good change
i have a clear operation that empties the list without changing the nodes so you can iterate what gets returned for batch removal/processing of a queue, and my singlylinkedlist tracks the tail in addition to head so i can do O(1) append which std cant without using the doubly linked list
also i have a fully atomic/lockless singly linked list that i need
you mean just head = null?
oooo how does that work
yeah that one isnt a dealbreaker to not have on std (and tail for the doubly linked version)
I don't think it has to be doubly linked to have a tail tbh 👀
yes but std singly linked doesnt have a tail. mine does tho
which is the primary reason i cant immediately switch because no append lol
exactly
https://github.com/Khitiara/imaginarium/blob/main/libs/collections/queue.zig#L444-L478 ill be honest i dont fully understand it still
lmaooo
the idea is you have a second int in the node that does some form of consistency tracking and then use a 128 bit cmpxchg
okay it requires cmpxchg128
yeah
okay I don't want to depend on that
but it's a neat solution if you're okay with that
yeah
its supported on everything after core 2 for sure
and any cpu that can run 64-bit windows 8.1 or newer supports it because windows uses the instruction for this exact type of list starting that version
idk the implementation is the same though
(and actually thats why my impl is called what it is, its the same name as the windows one and i couldnt come up with a better name at the time)
whats funny is i dont even use the dang thing yet, i ended up doing a thing differently and not needing it lol
had a talk with @hoary aurora and ended up making a wrapper for limine bios-install that has separate input and output file arguments because the way zig's build system interacts with run-a-program-to-make/change-file doesnt handle having both as the same argument at all without having to forgo the caching ability
and it also makes the optional gpt partition index arg a proper flag instead of positional since its transforming the args list anyway
actually considering having my prekernel act like a dynamic library that the main kernel links to, and then having the prekernel export uacpi dynamically
would take some fuckery to get fully working the way id want though
because id have to figure a way to swap out a function in the middle of init
because ideally the prekernel swaps out its map function for the main kernel's once the main kernel is up, so it can happen on the main mm
so the linking situation would be messy
prekernel statically links to and re-exports uacpi for dynamic loading, while uacpi is dynamically linking to kernel_api functions in the prekernel, and the main kernel dynamically links to the prekernel
Damn
I'm guessing this is so you can initialize xyz thing in prekernel, and then use it in the kernel (where e.g. xyz=uacpi). At this point why even have a prekernel? It's possible to do kernel bootstrapping in the kernel itself.
- i kinda want to shrug
- transitioning over from limine's memory layout to my own
- if i eventually make a custom file system, which i am considering, ill want to put the kernel in that fs but need something that limine can load
- get smp info from acpi tables without using limine, since i dont want limine to start the processors for me
if i could make one kernel binary and split out the bootstrap sections as its own elf to have limine load thatd also probably work
for point 3, wouldn't you need a disk driver in the prekernel stage to be able to load it in this case?
yeah probably - that one is lowest priority
most likely outcome tbh is i end up just writing my own bootloader instead of a prekernel but idk
(well most likely outcome is i keep rattling this around in my head forever and never actually do any prekernel or loader stuff lmao)
yeah, was going to say, this sounds like you'd be better off chainloading a 'dumb bootloader' instead
'i kinda want to' is a good reason though
i really should refactor my kernel's init/bootstrap stuff in general tbh
rn its all a bit intertwined which isnt ideal
now to figure out how to get ovmf into my zig build
if i could say get all artifacts of this release as a single zip thatd work
you could just call out to curl/wget with Build.addSystemCommand
yeah ig but that isnt crossplatform really, given i want building on windows to be supported
zig's dep management works great but it doesnt like having a single file, it wants it all in an archive
you could also just ask @pallid plaza to add a tarball with all files included to osdev0/edk2-ovmf-nightly :^)
true
i would also appreciate that given i just rewrote my build system to be entirely zig
including launching qemu
yeah zig build is super nice
so if i can just fetch ovmf as a zig dep it would be based
alternatively, if you (mint) aren't against merging that i can try doing it myself but i'm not very well versed in gha lol
aye
apparently the rust-osdev people have a repo that does stable builds as a single archive
all files?
you can download tarballs of releases
of all artifacts? i know you can download a tarball of the source but not of the artifacts iirc
much appreciated
thanks minttt :D
thanks!
np
thank youu
np
took a look at the actions, and i dont think the tarball is actually included in the releases here, just generated (also having the root dir inside the tarball be named something consistent would be nice but not a huge deal there)
the bigger thing is the release create bit still has the /* so it wont select the tarball
@pallid plaza unless im really misunderstanding globs this gh action still doesnt actually add the tarball as an artifact, since the release action is just selecting stuff inside the folder
can be solved by just adding a second line to files: with the tarball name
nice catch, i actually missed that
now just to wait on the cron job (which has me confused because midnight UTC was a half hour ago already and it hasnt started)
yeah gha is stupid, but the release should happen in about 45-50 minutes
i probably wont get around to setting up imaginarium to pull it until tomorrow tbh but idk yet
worked \o/
@haughty cipher: i will probably vendor the tar ball in a github repo at Ashet-Technologies, so i can download the files easily.
where would i download them right now? i'm interested in getting an EFI build running as well
releases on https://github.com/osdev0/edk2-ovmf-nightly
the new bit is adding the tarball artifact in addition to the direct downloads
i think this is the basic template for invocation, the whole pflash bit anyway
just add it as a pflash drive in unit 0 (the vars fd can go on unit 1 and be readwrite if desired)
for using zig dep management, either having a direct link to a tarball or having the files loose in a git repo work, so this setup is alright already now that the tarball is part of the build
as for actually efi booting, for that you also need the bootloader efi file at the right path (EFI/BOOT/BOOT<arch>.EFI)
i plan to have a build arg that makes it use gpt disk instead of mbr, and a build arg that makes it use ovmf instead of the default (and if both are passed then ovmf should detect the ESP and boot through efi)
yeah I'll let you know when I have it working
no idea on timing still tho since life has to have priority
fuckin incredible
gdisk -l zig-out/x86_64/img/drive.bin
GPT fdisk (gdisk) version 1.0.10
Caution! After loading partitions, the CRC doesn't check out!
Warning! Main and backup partition tables differ! Use the 'c' and 'e' options
on the recovery & transformation menu to examine the two tables.
Warning! One or more CRCs don't match. You should repair the disk!
Main header: OK
Backup header: OK
Main partition table: ERROR
Backup partition table: ERROR
ahh
heres the image btw
ok im confused this is adding up
i think i found it
ERROR (interrupts): unhandled interrupt: 0xE (page_fault) --- v=0e e=0000000000000009 cpl=0
rax=fffffbfdfee00000 rbx= fffffffff rcx= 1 rdx=ffffffff802950d0
rsi=ffffffff80217fb8 rdi=fffffbfe00000000 rbp=ffffffff80217b48 rsp=ffffffff80217a08
r08= 1 r09= fffffffff r10=fffffb8000000000 r11=fffffb8000000000
r12=fffffaffc0000000 r13= 1 r14=fffffa8000000000 r15= 0
rip=ffffffff80102be8 flg= 10046
cr0=80010011 cr2=fffffbfdfee00000 cr3=0000000001780010 cr4=000000a0
fsbase= 0 gsbase= 0
well
my os isnt happy
BUT it booted from efi
INFO (init): loading bootloader-provided system info
INFO (boot): LIMINE Boot Protocol, loaded by Limine v9.3.2 in uefi64 mode
yyup
alright @hoary aurora its working now
idk why my memory manager is dying but ive got it booting from efi with a gpt disk created by dimmer
bios/gpt isnt working though so idk what ive got wrong there
🤘 that rocks!
so bios/gpt isnt getting to limine somehow
bios/mbr still works the same
and efi/gpt results in my memory manager running off a cliff
(but after limine chainloads to my thing)
i thought limine dropped support for bios + gpt
if they did i didnt see anything about that
and the source suggests its still a thing
limine bios-install even has an optional arg to give it a gpt partition to stuff the stage2 into
which im using
and the stage2 location is showing up in the right offsets in the disk image too
but its def not getting to stage3
i literally tried compiling limine from scratch to get E9 output enabled and that doesnt seem to be working
oh btw, code to make it is pushed to imaginarium. same add_img function in build.zig.
GOT IT
dimmer isnt setting the bootable flag on the protective mbr
so seabios wouldnt boot it
that's part of the new code, right?
alright, with the latest push to the dimmer gpt branch imaginarium can now boot on bios/mbr, bios/gpt, and uefi/gpt
all from just flags passed to the build
zig build qemu -Dgpt for bios/gpt, zig build qemu -Defi for efi/gpt (efi implies gpt but passing both is ok; if you say gpt=false and pass efi it errors), neither flag for bios/mbr
both bios versions work perfectly but the efi version makes my memory manager shit itself for some reason so thatll be my next task lol
sounds great. all of these run the same disk image, right?
yup
well bios/mbr is different image from the other two
but bios/gpt and uefi/gpt are the same disk image
obviously 😄
technically not entirely the same in that the BOOTX64.EFI is only put in the disk for efi boots but thats whatever
i mean it should be possible to always have it, and MBR-only systems will ignore it
yeah i just never took off the if from when i was still trying to get it working yesterday
turns out i might be stupid
and i never bothered to zero out the new top level page table before filling it
which meant the bottom half of it was garbage random bits
that on bios were always 0 for some reason
now we workin on uefi lol
alright @hoary aurora has convinced me, im going to at least start working on re-architecting imaginarium's internals to support a second arch
going with aarch64 for the second one because of the qemu virt board and ovmf and limine both already having aarch64 support so ill just use uefi on aarch64
and riscv puts me off for some reason lol
aarch64 is so damn messy and difficult but good luck
At least its a real arch unlike riscv
im already noticing that reading the docs (which btw wtf arm why do you have no way to filter out excerpts from your documentation search) but thanks
yeah really. thats why riscv puts me off i think
and i aint touching anything 32-bit so ive got slightly limited other options lol
They really fucked up the EL model especially with regards to 2-1
Well CPU may not support disabling that bit that makes EL2 work like EL1
Like apple computers
But that mode also makes stores to el1 store to el2
So its sort of transparent but not quite
(cant actually do much work because its a heat wave here and i am boiling alive)
thats good at least that its mostly transparent?
Yeah otherwise they would have to rewrite every kernel to support it lol
lol fair
But most low end aarch64 stuff should support el1
cool
i dont have a single arm machine except maybe my nintendo switch? idk offhand what that thing actually runs
so id be vm only anyway
mostly doing a second arch to force me to get arch-specific code cleanly separated and organized
The most difficult part about aarch64 is the memory model, they have 99999 caching modes, cache maintenance instructions, barriers, tlb maintenance, configurations, bit granularity page tables etc etc
A big difference between real hw and virt is that qemu has stronger memory model then most hw, so its a little risky not having any real hw to test
ill cross that bridge when i get to it ig, but money is an issue unless the nintendo switch runs a processor with aarch64 and idk what it runs
Oh and also u don't get any firmware for power management so u must ship drivers for literally anything
i already have a modded switch that i could run linux on and then kvm on that i think? if the processor does aarch64
rip
Yeah
Also device dma addresses don't match the cpu address space so u must recursively walk the device tree to convert them to cpu address space
oh the switch is a cortex a57, runs armv8-a with 64-bit support which is what i was planning on targeting as a minimum anyway
So that's kinda annoying
Yeah, you're probably fine for a while but once you start running on hw your stuff would just break, that's to be expected
And no one is there to set up bars for u
yeah thats fair
well if nothing else looks like i might actually have one (1) hardware with arm that i can put linux on to run kvm under
since i got that switch
Do u have an android phone
Yeah
(rooted)
cool
so ive got the same amount of hardware for aarch64 as x86_64 ig (and also i have a homebrewed wii u that runs powerpc stuff but i aint touching powerpc anytime soon)
Anyway the aarch64 spec is really good, I find it easier to read than the x86 one
It's more structured in a technical way
yeah
So u should be fine
Ye
i really do like the way special registers are used in arm
compared to x86 msrs etc
Yeah
There's quite a few system registers, a lot are quire irrelevant unless you're working on something related to them
yeah but its nice to have everything thatd be some weird combination of cpuid and msrs and prayer on x86 be just check this one system register heres the name
Also the translation is a little more comlicated then x86 (enough to look at the qemu source to see the diff...)
im still super unsure how i feel about the way exception vectors are handled on aarch64 though
Wdym? I dont know much about x86 but they've split the exception vector nicely enough
Though sync exceptions is quite a wide term
on x86 (except the super old school real mode stuff) the vector table is an array of structs that hold the pointers to the handlers
whereas on aarch64 its a couple of 128-byte blocks that you just put code in
(on aarch32 i think its 4-byte blocks so you can only fit like one instruction each)
I mean in the first couple of instructions you'll branch somewhere, helps you set up the stack if you wish or disable some types of interrupt beforehand
Yeah aarch32 is quite terrible, luckily aarch64 learned a lot
yeah
the aarch64 way isnt bad really i just need to get my head wrapped around it a bit
i will miss having builtin interrupt priority stuff for external interrupts on arm though
using cr8 to just mask off whole chunks of interrupts easily is so nice
Also I'd keep esr.arm64.dev close by, arm encode a lot of info in that register and its a nice tool to parse
ill have to dig into the gic stuff though, maybe theres a priority register in there i can fiddle with
ah ok yeah the gic has interrupt priorities
My experience is that every "maybe theres a xxxx register" is "yes, there is."
fair lol
really cool! and even if it wont work or you'll drop it later, the code quality and flexbility of your OS will be increased a lot, as it's then ready for multi-platform
yeah that's the main idea more than anything
idk if going aarch64 will actually work out but it'll improve my code quality for sure to try
🙂 very cool
got a branch for that now
it can try to compile on aarch64 now
it compile errors ofc
but the build system can attempt it at least
took some inspo from @hoary aurora and made up my own custom target name format for it, though i might make a proper parser for that instead at some point instead of just an enum of hardcoded options lmao
nice!
format im using is arch-machine-boot-part
so e.g. x86_64-q35-bios-mbr or aarch64-virt-efi-gpt
the only part that directly matters to the os/build is the arch though
the machine goes to qemu, the boot part determines if ovmf is needed for qemu, and the partitioning type goes into disk image creation
i've split "run" and "kernel" target now
x86-pc-generic can use x86-pc-generic-bios or x86-pc-generic-uefi now
if i ever have more to the kernel target than just the arch i might do something like that
idk
🙂 i mean you basically do that already
anyone know if theres an aarch64 equivalent to qemu's debugcon?
Im not entirely sure what's debugcon but a quick google said it's to easily print to screen right? If so you can use semihosting
i have no idea what semihosting means but debugcon in qemu for x86_64 lets you do an io port out to port 0xE9 as a zero-setup logging option
any time you out a byte to that port it sends it to whatever the qemu command line has debugcon sending to
whether that be a file, stdio, or whatever
but that relies on x86 port io which aarch64 doesnt have
There's a uart
Yeah semihosting works, you essentially gives some register parameters and then invoke a seemily illegal instruction sequence (iirc its like 3 times brk, better google it) and then qemu would handle your request
Its not zero setup, but its simple
in qemu virt board its literally 0 setup
id be doing that already but both of my x86 hardware options use nonstandard order for the uart ports so in theory to do it right i need to discover those with acpi
u just write bytes to 0x9000000
so i dont have uart for x86 yet and idk if i want nonparity
Oh nice
oh that is nice
is this a -serial thing for the command line? if so ill need to tinker with the cli more to vary that depending on arch
yes
well its a place to start thats zero-setup for logging
my logging isnt persistent in the kernel so i need something zero-setup for early boot logging
my setup for testing that aarch64 boots at all was literally
volatile u8 *g_qemu_uart = (u8*)(0x9000000 + 0xFFFF000000000000);
static void qemu_uart_write(struct console *con, const char *str, size_t count)
{
for (size_t i = 0; i < count; ++i) {
*g_qemu_uart = (char)str[i];
}
}
cool
@last pebble oh also, are there any cpu features i need to toggle for kernel-mode aarch64? ive got a bunch of stuff i need to turn off for zig to not use sse on x86_64 since thats slow to save registers for in kernel-side but idk if i need anything like that for aarch64
i use -mgeneral-regs-only, seems to work
hm, ill have to see how to get zig to do that then
-mcpu=baseline-foo-bar
(or the target)
.aarch64 => {
const Target = std.Target.aarch64;
query.cpu_features_add = Target.featureSet(&.{});
query.cpu_features_sub = Target.featureSet(&.{ .fp_armv8, .crypto, .neon });
},
that's what i disable for aarch64 in my template
that seems to work but i'm not sure what more is required
thanks!
also thank you for saving me from spamming @intFromEnum with addFeature, didnt know about featureSet lol
well it still doesnt compile on aarch64 but thats mostly a bunch of "has no member" errors because the aarch64 arch files are stubs with not yet implemented comments
ok down to two compiler errors
one of which gets repeated three times for some reason
error 1: I havent defined the saved register state struct yet and the thread struct depends on it
error 2: the memory manager is trying to set cr3 on aarch64 because i havent refactored that yet
time to double check i didnt break x86_64 with all this
x86_64 still works
there are also a whole lot of not implemented things on aarch64
stubs and the like
but still, down to two errors
anyone know of an example of using efi runtime services when booted by limine?
trying to figure out what would be needed there
still kinda neat to me that the drive letter followed by a colon is a command on windows
why would you want to do that
i believe you can just take the address of the table and use it as is
aarch64 rtc
limine doesnt have it in page tables, i ended up asking in that server
why do you need efi runtime services for that
ahh
can't you just write a simple driver for it or something
or use the boot time request
qemu's virt board only does it by uefi when acpi is on and i only really plan to support acpi targets for as long as possible
that sounds really bad
can't you discover it by acpi then?
how the hell does uefi discover it
they explicitly dont put it on the acpi tables
no fuckin clue lmao
like theres a bit in the qemu source where the aml to do that is commented out with a note saying its removed in favor of efi runtime services
"When booting the VM with UEFI, UEFI takes ownership of the RTC hardware. While UEFI can use libfdt to disable the RTC device node in the DTB that it passes to the OS, it cannot modify AML. Therefore, we won't generate the RTC ACPI device at all when using UEFI."
and uefi is basically the standard way to boot aarch64, or at least the only way limine supports
that is pretty dumb
i think its probably a factor of efi firmware being decoupled from the actual device/machine generation in qemu
more than anything
gods i kinda hate the arm manual
its so unclear about the size of page tables
ive figured this out but its still hella unclear and i dislike it
when i first read the arm manual some months ago i got thrown off by the flag bit being different for large page vs table-or-small-page
and now i get it
felt weird that the final stage would use a different bit depending on level
but turns out recursive page tables would be impossible if table and small page were different flags
anyway i think i can keep the virtual memory map the same on both x86_64 and aarch64 which is nice
the main thing i still need to do before aarch64 will compile and i can start filling in not-implemented stuff is just decouple out the PTE stuff from the arch-agnostic memory manager logic
turn "valid/present pte" into a thing provided by the arch with functions to set address and set memory attributes thats opaque-with-known-size to the kernel proper
i also want to make the page table stuff use proper access flags for parts of the kernel
very cool!
sorry for the necro - did you end up finding any examples of using runtime services? I'm looking for a sanity check.
so afaict you need to map (in low virtual memory page tables) the stuff in the EFI memory map, and then the efi system table provided by limine will be valid
then you can use the runtime services pointer to get it, and use its setvirtualaddressmap thing to move efi to somewhere else in virtual memory
also nw about the necro, this project isnt so much dead as on hiatus atm and i do mean to get back to it soon
thanks, thats pretty much what I'm doing, I'm just having trouble getting it to work on real firmware and am wondering if there's some bs I missed in the spec.
yeah idk its confusing and im not 100% sure since ive not done it myself
mm yeah, it seems like it should be straight-forward enough 😛
ty anyway, if I get it working I'll let you know
the big thing is to make the mappings right
because efi can use internal pointers and the only thing that guarantees that is the efi memory map
anything thats listed with the runtime flag
but limine doesnt put those in paging
so you gotta do that yourself
if youre on an older limine that unconditionally identity maps a chunk of low memory then some firmware will be fine cause its usually identity mapped but thats not a requirement
limine can give you the efi memory map, which has all that info.
yeah you just gotta use it
Im not having any trouble (I think lol) mapping the runtime stuff
from what I can tell its dying when calling SetVirtualAddressMap()
after you do that you need to reconvert the system table pointer from physical to virtual
under the new map
and start from that root system table pointer
setvirtualaddressmap changes all the fields of the system table recursively but you need to start from the system table (which youll have the physical address for from limine, who gets it from the calling convention of efimain iirc)
yeah the issue is the call to SetVirtualAddressMap(), not after
it returning anything? or just dying?
it doesnt return to the kernel: the system freezes for a few seconds and then resets
eh i'll just setup an idt see what comes out
if you figure it out lmk, i want to know too
as for this project, imaginarium is largely on hold pending both a resolution for https://github.com/ziglang/zig/issues/24522 and a zig release post the merge of https://github.com/ziglang/zig/pull/25227
will do!
looking at this refactoring plan and debating just ripping it all out and starting over tbh
well mostly out
theres some parts that are fine
What would you refactor and why? How much untouched code would be left?
the arch stuff is too intertwined with almost everything else is the main issue
so id like to refactor out a HAL of some sort but its becoming too much of a tangle atm
Could you be more specific?
the memory manager is tied into the specific layout of x86 PTEs still, the io system is calling directly into APIC stuff in a lot of places
That's an ouch in terms of separation of concerns
and theres some other stuff that could really use redoing in general, the dispatcher/threading stuff is ugly rn and needs a hefty rework
hence the need to refactor it to fix that, im just not sure if its worth trying to salvage that much of my existing code for it, esp since the existing stuff is my literal first time doing osdev and i understand things better now
also im behind by a full major version on zig itself and would need to change at least some things to do that update
the entire startup sequence of the os is built assuming a different bootloader than im now using and had to get limine support bodged into it too
starting to work on a redo of my build infrastructure in advance of major refactoring
and also updating the zig version
running into a funky issue where the one dependency legit makes zig build crash without a message and idk why yet lmao
oh that issue was https://github.com/ziglang/zig/issues/25776 turns out
love when ive finally got the energy to work on a project and the dependencies just say no
as for the build infraastructure stuff, i took the setup that the zig std uses to store and manage cpu feature flags for build and duplicated the concept to use for my own kinds of feature flags
of which i have two sets, one for the build target which are passed through an options module to be checked by the kernel and one for the run target which are converted to qemu machine and cpu options for running the OS
the build target ones being passed through an options module makes them available at compile time, which means that I should be able to use them the same way as conditional compilation defines are used for feature flags in a C kernel
once i get it done ill be able to do stuff like:
# base x86_64 target features, enable huge pages on the vm
zig build ... -Dtarget=x86_64 \
-Dmachine=q35.qemu64+pdpe1gb
# aarch64 with acpi support compiled in and run on the qemu virt machine, cortex a35 processor emulation, and acpi support for the machine turned on
zig build ... -Dtarget-aarch64+acpi \
-Dmachine=virt.cortex_a35+acpi
or if im not running the qemu task and just building the kernel i can omit the machine option entirely
feature flags can also have dependencies too, though nothing ive got so far has them in any meaningful sense (i put rdrand as a dependency of the rdseed feature but idk if thats even true, idk if you can have rdseed without rdrand)
the target features can also imply cpu features for the compilation invocation
in case i want to forward a cpu feature through to the compiler from this system without putting it in the arch defaults for some reason
details on how it works to come once its done tested and pushed but so far so good and the code is mostly copied from how the std does similar things so hopes are high that itll work
note that a target feature does not generally imply a machine feature - an aarch64 kernel compiled with acpi support would ideally still be able to run on an aarch64 machine without acpi using devicetree stuff
one day of fixing the dang dependencies later, tomorrow i can start fixing the other dependencies lmao
all this dependency bs is driving me to the conclusion that WSL sucks tbh
but at least the first level of dependency bs is done officially now
thanks, WSL in this one specific case behaving like windows in a way that diverges from posix lmao
ive spent literally a week now trying to work around https://github.com/microsoft/WSL/issues/12897 lmao
Windows Version Microsoft Windows [Version10.0.22631.5189] WSL Version 2.3.26.0 Are you using WSL 1 or WSL 2? WSL 2 WSL 1 Kernel Version No response Distro Version No response Other Software No res...
some things i really wish i could customize/control about limine tbh
dont really need a stack provided, i want to control SMP startup myself but maybe still use the smp info from limine, stuff like that
regarding SMP you can just initialize the CPUs again yourself
but maybe that could just be a flag or something
you should just have to ipi
well yeah that's what i'm saying
you can just re-initialize the other CPUs yourself regardless of what limine did
i think?
i don't think, i know
oh ok
thats good to know at least
https://codeberg.org/Khitiara/imaginarium officially migrating to codeberg for imaginarium
i do wish codeberg let you choose what events to subscribe to when watching a repo but otherwise i do prefer it to github just for the no microsoft connections and not pushing ai bs down my throat like github has been
progress happening on the big rework
currently working on a custom bootloader for imaginarium, mostly to get access to uefi runtime services on aarch64 for the RTC but also the additional control is nice to have
current output from the new WIP bootloader
nice
next things for the bootloader:
- filesystem stuff
- load and map acpi tables and get the rsdp
ok, to work withal
also if any zig knowers want to see some of the most blursed shit ive ever come up with, have https://codeberg.org/Khitiara/imaginarium/src/branch/rewrite/src/target this directory
cc @ornate oak i feel like youd appreciate that nonsense
thank you 🫦
time to pop off
ITS WORKING
oh shit hang on its returning early and not hitting the memory manager insert-this-into-the-mdl part
and now it doesnt compile 😓
see this makes more sense lmaooo
there we go
before i keep going further on the ACPI part of the bootloader i need to get my act together wrt virtual memory allocation
though not actually that much of it tbh
shouldnt be that bad
just gotta actually do the work lmao
kinda hate that im doing this to myself with the bootloader but w/e its fiiine
i dont actually need full acpi tables here just enough to map em
am going to quickly change it so it overwrites reclaimable in the memmap for this
its now set up so when it maps acpi tables anything with the acpi NVS memory type gets left as is but anything with the reclaimable type gets turned into the acpi_tables type
that way the kernel knows not to nuke the acpi tables
though tbf im not actually sure thats something thatd be an issue
@last pebble is it allowed to overwrite the acpi tables after uacpi gets initialized if the tables are in acpi reclaimable memory?
If you dont need them anymore then yes
so which ones if any does uacpi itself need preserved?
i'm pretty sure uacpi copies all the tables into memory it allocates itself
thats what i thought too but figured id check
im nowhere near the point of actually needing to reclaim that memory, its more about knowing what i can mark it as in the bootloader
None
cool, thanks
(i do assume the FACP needs to be preserved but i also expect that to be put in an nvs or reserved memmap entry)
Aml bytecode is copied to heap, FACS ig but that's not gonna be marked reclaimable
or whatever that block of mmio stuff is called if my memory is bad on the name yeah
Yeah
If you mean fadt uacpi keeps a private copy
nah the FACS is what i meant
Yeah facs is needed later on
the thing with the mmio stuff in it
👍
Make sure u reclaim it after loading the namespace tho lol
obv
reason im asking is my new bootloader is going to be mapping all the acpi tables and the facs etc ahead of time so the kernel mm doesnt need the memory-mapping stuff up in advance for the initial load of acpi tables, and every memory change the bootloader does it puts in the memory map
and not all of them need to stop being reclaimable if the firmware put them as such in the memorymap the bootloader gets from it
the memmap changes for acpi is the same idea as what limine protocol v4 does where if the tables arent marked as nvs or reclaimable already then it marks them as a new memory type acpi_tables so the kernel can handle that
Fair
but yeah if the tables are reclaimable then i can leave it alone if theyre already marked as such
switch (switch (hdr.sig) {
SdtHdr.convert_sig("RSDT".*) => false,
SdtHdr.convert_sig("XSDT".*) => true,
else => return error.WrongAcpiSignature,
}) {
inline else => |is_64bit| {
const Entry = if (is_64bit) u64 else u32;
const entries: []align(1) Entry = @ptrCast(ptr[0..hdr.length][@sizeOf(SdtHdr)..]);
for (entries) |e| {
try map_table(e);
}
},
}
heres a fun dumb hack you can do in zig thats kinda fun
this ends up generating two separate switch cases for the two different tables, with the right bitwidth of entry automatically swapped in
not sure if its normal for all the acpi tables to be page aligned but if the ovmf firmware wants to do that i aint complaining
actually its not all
the XSDT and RSDP arent page aligned
but the rest for some reason are
on real hw 99% of the time all tables are page aligned
cool
in qemu they arent usually
must be an ovmf thing then
yeah
the only thing im not sure my current bootloader mm can handle is if a table spans two pages and one of the pages on either side also has a table that was previously marked
yeah thats possible
yeah i figured it would be
but i dont have it in me to make sure that works rn lol
are the various gpe/pm/sleep/etc registers listed in the FADT also left mapped at all times? or just the FACS?
id rather not deal with those in the bootloader if i can help it lol
in any case, more progress \o/
because the virtual mappings for these get put in the memmap, the kernel can put them into the PFMdb (aka struct page stuff) and then get an easy back-reference to get the virtual address from the physical address until they maybe get reclaimed where applicable
you can see from the "to go at virtual" lines how the FACS gets a different location too, thats on purpose as the reclaimable stuff is put in the region where the kernel nonpaged pool goes but top-down instead of bottom up
that way theyll get reclaimed but only if the nonpaged pool runs out of space
the table ordering not being sorted by address does lead to this nonsense though lol
love how the stuff is just randomly out of order in the middle lmao
if i did the dsdt before the fadt itd be in order though but i dont really care tbh
this shit wont be set up as large pages anyway
also, octal slaps for virtual page numbers
every 3 octal digits is a level of the page table
wdym? uacpi doesnt really map anything on its own unless u ask it
if im using the fixed hardware stuff do the fixed hardware registers need to be in virtual memory constantly if theyre a SystemMemory address space? or do they get map/unmapped as needed to access them?
they're mapped/unmapped separately ofc
yes but are they left mapped or unmapped after being accessed?
some of the most common ones are left mapped yeah
mhm
one of my goals here is to put stuff that gets left mapped in a different address area because the address range for temporary mappings has some limits (tbh probably not enough to warrant this but on principle i want stuff left mapped to be in its own spot)
so im trying to figure out what exactly i need to put there
why would those registers get mapped in a bootloader tho?
like when you're accessing them you're hopefully at a very late point of init
they wouldnt necessarily, just thinking ahead and trying to figure out how to tell theyre a long-term mapping
and the bootloader is already doing that sort of thing for the basic acpi table stuff since it has to get the rsdp anyway
ah
if uacpi could be configured to tell the kernel if its doing a long-term map or not thatd be nicer but ah well, not the biggest deal
what would be classified as a long term map?
something that wont be unmapped until the kernel makes a specific call to do so if ever
as opposed to something which is mapped, accessed, and then immediately unmapped
probably tbh
the real solution here is for me to just look through the uacpi code and figure out what i actually want lmao
lol yeah
tbh i think its basically the stuff in g_registers and the FACS that i care about keeping mapped here
the former im not really sure if its enough benefit to matter though
so not really worried about it atm
and the FACS is easy to wrap into my existing table stuff
on x86 its basically never SystemMemory
cool
the more i read the spec the more i get the impression that uefi fully expects you to just. use two different page tables and swap back and forth a ton when making a bootloader
cursed shit
and im not sure thats the worst option TBH
upsides of two parallel page tables:
- dont need to fixup pointers before transferring control to kernel
downsides of two parallel page tables: - have to swap back before every UEFI call
not sure which option ill actually go with
yall got thoughts there?
probably going to call it a night now and mull that over for a bit
ok i think im going to just keep putting that off. might ask around elsewhere if anyone has suggestions for what the right channel for that is though - #virtual-memory maybe?
also, and maybe this is because im used to pre-uefi x86 crap, but it somehow surprised me a bit how little this bootloader actually needs to do
remaining things i can think of offhand for tabula (the new imaginarium bootloader):
- figure out the ID for the BSP so the kernel bootstrap stack can get put in the right place for the per-core kernel stacks (used in interrupt handlers once the scheduler is up, each thread will get its own stack normally)
- do filesystem stuff so it can load the kernel and its config info, though if i can figure it out i might use PCD or something like that instead of a normal config file idk
- query video modes
- actually set up the page tables
- UEFI exitbootservices and setvirtualaddressmap
- verify uefi runtimeservices work on the virtual map
note that i expect that 99% of the time the BSPID will be 0 but i want to actually handle if it isnt and my per-core stacks are indexed by processor id
realised a benefit to doing paging later and fixing up pointers at the end actually
