#Ultra
1 messages · Page 3 of 1
not sure I got the push order and got indices correct but you get the idea
the got entry by default points to the pushq stuff
what is this doing exactly?
i think the indirection is done by prefilling the JUMP_SLOT got entries with a thunk that jumps to what the 0th got entry points to, not within the plt thunks themselves (or at least on a test binary the thunks are the same regardless of -z now and -z lazy)
i think the plt thunks are there purely to save space
since a relative call insn takes less space than the pc-rel call through the got
calls thunk code that jumps to the address in the got, which by default points to code that pushes values identifying the module and symbol before jumping to rtld code
that rtld code is supposed to fill the got entry and do a tail call to the resolved function
so on future calls the thunk jumps directly to the resolved functions
so the last line jumps to the pushq code or?
yeah
how does it know which whatever we came from?
during object initialization, JUMP_SLOT entries get the base address of the module added to what's already there (they contain the link-time addresses of the resolution thunks)
one of the things thats pushed
pushq $<got index I think? might be relocation index>
that presumably
so thats per-plt-entry code?
yeah
any reason why we need an extra jump?
when the dynamic linker resolves it it overwrites the initial instructions with a jump directly to the actual function
so you dont pay the cost of the pushes on subsequent calls
ohh
there's a different thunk for each symbol, and the thunk pushes a module-local id (forgot whether it was symbol idx or got idx or reloc idx) as well as a module id (which is filled by rtld during init)
requires flushing the icache on most architectures btw so kind of evil and another mark against late binding
well the instructions themselves don't get overwritten, just the got entry
that makes no sense
not really
windows doesnt do late binding
works fine
if the callers call into this code stub in the plt and you dont want to ever change that call instruction (bc itd COW text which is bad), your only choice is to change the code in the plt entry
before resolution:
- caller calls plt thunk
- plt thunk jumps to address in got, which is the resolution thunk
- resolution thunk pushes symbol id and jumps to rtld
when already resolved: - caller calls plt thunk
- plt thunk jumps to address in got, which is the resolved symbol address
there is no instruction overwriting involved in any of this
weirdly large number of steps
i guess i got confused from the SunOS paper where they would overwrite instructions in their PLT
must not be done in ELF
the only thing that changes between pre resolution and post resolution is whether the got (aka address array) contains the resolution thunk address or the resolved address
why cant that jump be inlined as a call into the caller?
is a direct call + PC rel indirect jump smaller than a pc rel indirect call?
it can, it just isn't done for some reason
strange
wdym inlined as a call into the caller
I believe the reason has to do with the fact it's impossible on x86
transformed into a call instruction directly into GOT
that would COW the text section
instead of direct call to something that does a pc rel indirect jump, do a pc rel indirect call
the GOT offset is fixed so no it wouldnt lol
thought you meant changed by the dynamic linker
ah wait no there is a reason for this
nah like just copy what plt does and replace jmp with a call
its because the compiler doesn't know whether an extern comes from another object or from a dso
and a pc rel indirect call has a different instruction size from a direct call so the linker can't transform it
so basically it does it so it doesnt have to use absolute calls for local symbols?
i mean it could just pad with nops
without the direct call to plt thunk stuff, the compiler would have to pessimistically assume all calls to external functions are to dsos
and so even module-local calls would use pc rel indirect through got
dont linkers move around code all the time
just patch .text at link time?
lto and stuff
transform into relative call and add nops
with LTO and linker relaxations yes but both of those are relatively new things and this scheme predates them by a lot
i mean it was never "that" hard to relocate part of a section to make more room for instructions or vice versa
could have done that in the 60s
i hate the discord mobile app
i mean msvc's linker still struggles with basic shit
i checked and no, the jmp in the plt thunk and the call through the got entry are the same size
im not even sure they have LTO
basically all tooling that generates object files assumes within a section things don't get moved around during linking
sure it's technically possible, that's what linker relaxations do
actually no it's 1 byte shorter per call ```
118b: e8 c0 fe ff ff call 1050 puts@plt
116b: ff 15 6f 2e 00 00 call *0x2e6f(%rip) # 3fe0 <puts@GLIBC_2.2.5>
yeah but with the former scheme you need an extra pc-rel indirect jump that you don't need with the latter, not counting it is a bit unfair imo
aka you need the puts@plt code
i mean the point is that the jump is only at one place in the final file
that isn't really important though? nothing breaks if it's at multiple places
if you call puts more than 10 times it saves memory
or well, 16 accounting for the alignment padding nop at the end of the thunk
i do wonder what is the difference for most programs though
since i assume there's a mix of rarely called stuff where the plt thunk wastes memory and stuff called very often that saves memory
anyone know how hot relinking works
property of some dev environments where you can change a source file, recompile the program while its running and have it relink dynamically and immediately observe your changes
the only thing i can think of is that it requires a quiescent point in a system library where the reloading can occur safely, like an event loop
but then theres multithreading
i'm guessing it suspends all threads and uses ptrace or whatever other native debugging interface to call into rtld
that's not based on any knowledge though, just how i'd implement it
the issue is that you cant really be executing any of the code that changed while this happens
well yeah that's why all threads are suspended
far too difficult to figure out what to do there
suspending them doesnt help
because you unsuspend them and now theyre in the middle of some changed code
with stuff in the wrong registers and in the wrong place and the function they were executing might not even exist anymore and so on
for stuff like this you need to handle changing stored function pointers anyway
you basically need the threads to all pass through a quiescent point
you can just treat it as having a function pointer stored in rip
could load the code at a new address and replace the got entry for it (assuming you built without -fvisibility=hidden or -Bsymbolic or -fno-semantic-interposition)
thats true
you could keep the old code and put the new code somewhere new and then you only call into it next time
yeah thats probably how they do it
ah yeah
and then you wouldn't have to deal with the function pointers stuff either
or just pointers to symbols in general
well but the thing is ive seen it demoed where the new code is called into without ever calling it. like, the thing that made me wonder about it was a wwdc demo in like 2003 where they showed off xcode being able to do this with C++ projects, and the thing i noticed was that they could change the main function and have the changes materialize
they did call a get next event type thing inside the main function though
so it could be a combination
i mean since it's their ecosystem they could be doing compiler magic
at the time they were using stock gcc
but they were using their own linker
fixing up the return address on the stack
sounds
nearly impossibleish
so i might have just misinterpreted what i saw
id say impossible but nothing is IMPOSSIBLE
you could probably have some 200kloc static analysis engine that could make it all work out
sounds impractical though
does it work well with intel syntax?
just use gas syntax tbh
do I have to invoke as separately to compile those, or can i just feed clang .S files and it figures it out?
how does that work?
idk, but it does :^)
does clang have like a GAS support module or something
these are the make rules I have, so literally the same for both S and c
and it compiles fine
interesting how that works
well that makes it easier than nasm becase i can just pipe all the files into the same target in cmake
apparently llvm has its own assembler
-no-integrated-as
llvms assembler is fucked in some parts
tbh my asm is super simple
iirc it couldn't properly encode far jumps
but I do use inline asm with clang that has cfi macros
it would not emit the correct opcodes
what happens if u do this
it uses binutils as
cross-binutils
yeah but clang has to be aware of it somehow
you only need it to work on your host afaik
oh wait
llvm-as?
idk i forgot how exactly it works
doesn't that just use the llvm assembler tho
i dont know
i just remember the clang as being broken for some reason
maybe it's fixed now
maybe it was fixed since tho
ill give it a try who knows
just use the integrated as until you encounter issues
ye
btw i had some cmake code for configurable modules/builtins
if you want to look at it
too late

lol
clang usually knows about the backing gcc toolchain, so it can use it
I think it depends on the target
just like the target can choose which linker to use (by default at least)
@sterile kayak is this an rbtree https://github.com/proxima-os/hydrogen/blob/c18f439c1a8c8f72f14e8eea62ee0d77dc3be7bc/kernel/mem/vmm.c ?
it's avl
ah
me when i inline avl.h inside vmm.c
left tons of comments about how the kernel symbol table works so i dont forget lol
actually its slightly wrong because if a token is longer than 128 chars then the length is encoded as a ULEB128 byte (aka as a sequence of bytes)
finally someone documenting their stuff
reading impls with 0 comments at all makes learning from code hard
cuz you don't know why it does it, just that it does it
yep
If a token is longer than 128 chars it is probably a code readability issue
(unless C++ with nested templates or smth)
linux supports it because of rust probably
but yeah the actual encoding is
if name_len <= leb128_max(1):
repr.append(name_len)
else:
repr.append(LEB128_HAS_NEXT_BYTE | to_uleb128(name_len, 0))
repr.append(to_uleb128(name_len, 1))
Glad to not be the only person with ptr_t 🙏
Yes!
what is it? uintptr_t minus the uint?
void* 
yeah, just a uintptr_t defined by me that i can use in printf in a stable way
%tx
i dont see t there
you don't see t where? in your manpage? get a better manpage, then.
ah wait i do but thats ptrdiff
eh, uintptr ≈ ptrdiff
attribute(format) will still complain
huh, for me it doesn't
strange because it definitely complains about signedness mismatch usually
but if it works it works
huh? %tx is unsigned because %x is unsigned.
or do you try printing uintptr_t's as %ti?
Yeah nvm that should work
It says "unsigned version of ptrdiff_t"
But doesnt name such type lol
alright uhh
CFI time
but first ill make helpers for accessing the symbol table
or more like, just one called lookup_symbol for now
Waiiit
i just realized
.long in GAS is fucking U32
my thing has been wrong all along
who tf thought of that
it's not so long now

oh lmfao
i thought it didnt because of this
but im dumb
i see
now that i think about it, when unwinding the current task stack, i have to jump to an asm stub to save all registers of the previous call frame don't I
because CFI depends on the state of the registers matching the point in time its unwinding right
yeah
yes it does
@sterile kayak do u think this would be good enough?
Should be, yeah
after this the frame should point to the rip after this call which is where unwinding should start hopefully
thanks! btw i saw u use .hidden in your definitions, why is that? I read the manual but i didnt understand what it meant
Which ones?
also u seem to manually align the functions, is this also needed?
Ah yeah that's string.S, it's part of the common sources that are compiled into the vDSO so .hidden is necessary to ensure mem* doesn't get exported
ah
There's no abi-mandated function alignment but the compiler seems to align generated functions to 16 byte boundaries so I do the same
ah cool
thanks a lot
for some reason objdump seems to not care about my .size and disassembles garbage after the function
Yeah objdump ignores .size
ah
Is it really garbage though? Both the assembler and linker should fill the gap with nops
Huh, weird
yeah lol
i have a setting that fills .text with 0xCC in my linker script
maybe its that
but why it has 00 before that idk
Maybe the fill value is 32 bit?
hmm
Actually no because it'd have to be big endian for that to make sense
right
yeah
it is the fill byte
i just changed it to 0xEE
and it changed as well
you're a genius
its literally big endian too
like you have to literally go out of your way to make it big endian too
props to the gnu linker team
Me when I just force frame pointers instead of doing all of this :^)
me when i say goodbye to 10-15% of perf 
Where did you see that?
but yeah its probably more practical
i read this in linux the kernel text size will grow by ~3% and the kernel's overall performance will degrade by roughly 5-10%
Just last year both Ubuntu and fedora enabled globally to compile with frame pointers
also like, with a frame pointer based unwinder u have to manually detect stack changes or iret frames etc
with cfi it just does it for u
idk, real hw and stuff? its more for fun than anything
Yeah but at this rate you will take forever before having any actual code in the kernel 
thats fine lol
For real hardware I have a gdb stub
In general it's nice once you have threads since you can debug at a thread level instead of a core level
I don't have anything with serial
And it will take me tons of time to get to usb
I wonder how hard is xdi
or whatever that xhci debug interface is called
I assume its simpler than full usb
No clue, probably not that easy
linux has support for it for early console iirc :^)
Does it require like special devices and stuff
I have no idea tbh
I never actually used it
it might require a crossed usb 3 cable
or that might be for the intel jtag stuff
EARLY_PRINTK_USB_XDBC
ah
Documentation/driver-api/usb/usb3-debug-port.rst
on one hand it says ```
When DbC is initialized and enabled, it will present a debug
device through the debug port (normally the first USB3
super-speed port). The debug device is fully compliant with
the USB framework and provides the equivalent of a very high
performance full-duplex serial link between the debug target
(the system under debugging) and a debug host.
on the other hand it says
DbC has been designed to log early printk messages
ok it does have a mini usb driver for it
its a single file with less than 1k lines, so it can't be that bad
yeah its basically the most minimal xhci driver, it allocates a single ring and does bulk transfers to the device
it is more complex than I had hoped tho
Ah damn
technically, its a dma logging api :^)
Virtio console too
lol yeah - there's .2byte/.4byte/.8byte, which are clearer imo.
Ooh good to know
are you doing 32-bit?
Since when 😭
I really hate GAS's .long crap
GAS has a lot of directive shenanigans
do you know about the .align memes
what about it?
it has a different meaning between architectures
on x86 .align 8 means align to 8 byte boundary
on risc-v or arm, .align 3 means align to 1 << 3 byte boundary
that's why i use .balign instead
ah
lmao
im using balign as well because i stole it from monkuous' code but good to know align sucks
lol
there's also p2align if you prefer the other meaning
true
imo balign is more explicit, but p2align also makes sense because you rarely want an alignment that isn't a power of 2
Why would you ever want p2align in the assembler tho
As in, just put it the number
And you can always do it yourself with 1<<x
Can you even align to non power of 2?
No reason why you couldn't
ofc
why would u do that?
idk
imagine a char buffer
something like utsname
idk
there has to be a use for that
hmm
i don't think that works
no, it doesn't
test.S: Assembler messages:
test.S:1: Error: alignment not a power of 2
the point of balign is not allowing non-2-powers but allowing the user to specify the alignment in a different way
Makes sense
It's stupid to not allow any alignment tbh
I have had hardware where I need 3 byte alignment
maybe that's just on x86
No it isn't; accessing aligned is usually more efficient than accessing misaligned and always uses less hardware to do so.
that's unrelated imo
just because it's not efficient doesn't mean it should be impossible
It also doesn't mix very well
If you have something that needs to be aligned to a page, something that needs to be aligned to 3 and something that needs to be aligned to 5 you suddenly need 0xF000 alignment
^ This is a good reason why we only do power-of-2 alignment
Also I'd love to know what hardware needs this because that'd be the first time I hear of hardware needing something to aligned to a non-power-of-2 number bytes.
63 bit pointers :D
what kind of hardware requires that
some proprietary asic, it had a 24bit data bus and while not all the command descriptors were a 3 byte product in size, they all had to start on a 3 byte product
now tbf, that is not a good argument for an assembler
because I didn't really need to use an assembler to load the data into it nicely
interesting
im doing CFI
so far DWARF is very convenient
and eh_frame_hdr is super helpeful too
i think this entire unwinder will be like 500 loc tops
and its arch agnostic for the most part
the spec is impossible to understand without actually looking at the data
but thats fine
progress
the eh_frame location is encoded as a signed pc-relative 4 byte value
so the decoder seems to be working
also why the hell does it not generate fallthrough warnings at -Wall -Wextra -Werror
Are you writing:
switch (foo) {
case BAR:
do_something();
case BAZ:
do_something();
break;
or:
switch (foo) {
case BAR:
case BAZ:
do_something();
break;
}
?
AFAIK only the former will generate fallthrough warnings.
can you paste the code snippet here?
i already fixed both instances but let me craft something
also wtf this thread has existed for like five days but already has more messages than my thread.
it had random discussions
I don't think you'll have warnings in normal execution though
Imo its just cluttering the source rather then logs, but not too bad
I say, let the caller complain when your function fails, except in extraordinary circumstances.
yeah but then i have to go and put printfs to figure out which line failed
sure
but IME more often than not, with some inner knowledge of the code, that can be guessed anyways.
but yeah ill prbably remove msot of these
I mean it allows for very simple alteration to turn all warnings into asserts
e.g.
switch (scaling) {
case DW_EH_PE_absptr:
*out_value = 0;
break;
case DW_EH_PE_pcrel:
*out_value = (ptr_t)*cursor;
break;
case DW_EH_PE_textrel:
*out_value = (ptr_t)g_linker_symbol_text_begin; // no warning here
case DW_EH_PE_datarel:
*out_value = (ptr_t)g_linker_symbol_eh_frame_hdr_begin;
break;
case DW_EH_PE_funcrel:
case DW_EH_PE_aligned:
default:
pr_warn("unhandled DWARF scaling %d\n", scaling);
return ENOSYS;
}
Can remove all the warnings that indicate code errors after you're more stable
if you OOM, it's not very interesting to ask yourself 'where did I OOM? where did I OOM?' rather, 'who is leaking memory?' WARN_ON doesn't help you here.
hm, strange.
my clang is broken
Gcc wins again
i checked with make VERBOSE=1 it does pass all those flags so idk
--target=x86_64-none-none -std=gnu17 -ffreestanding -ggdb3 -Wall -Wextra -Werror -mcmodel=kernel -mno-red-zone -mno-80387 -mno-sse -mno-mmx -mno-sse2 -mno-3dnow -mno-avx -fasynchronous-unwind-tables
True, though a lot of other errors can easily be traced like that instead of being propogated onwards. For example, a suspicious result from a parser could indicate a parser error which is much better to know early before diving in the code that consume the returned data
lol literally nothing in the DWARF/LSB core explains the format of this field
absolute value? ok? How many fucking bytes
Seems to decode fine with DW_EH_PE_absptr
ill just leave it at that
actually no it's a 4 byte value for some reason
okay i guess lmfao
also success
off to bed now
tomorrow we execute dwarf CFA opcodes
to actually unwind the frame
objdump looks at the version and apparently since version 4 they specify this lmao
mine is version 1 tho
okay i take my words back about how good this format is
they somehow failed to specify the width of a very important field and u have to use weird heuristics to figure it out
So this shit works as follows: you check how many bytes it took to decode the PC value, then you memcpy this same amount of bytes, but without applying any augmentation
and that gives u the length
what a stupid format jesus
there are literally augmentation types that arent constant size so this heuristic works only because luck
if (fc->fde_encoding)
encoded_ptr_size = size_of_encoded_value (fc->fde_encoding);
fc->pc_begin = get_encoded_value (start, fc->fde_encoding, section);
start += encoded_ptr_size;
/* FIXME: It appears that sometimes the final pc_range value is
encoded in less than encoded_ptr_size bytes. See the x86_64
run of the "objcopy on compressed debug sections" test for an
example of this. */
SAFE_BYTE_GET_AND_INC (fc->pc_range, start, encoded_ptr_size, end);
objdump code
i should work on cfi stuff at some point tbh. the zig std even already has it i just need to hook into/swap out out the register context
yeah lol
if you want it for reference i can dig up the source for how zig does it btw
the only issue with the std version is it only accepts it if you have posix ucontext or windows RtlCaptureContext
(the build system lets me swap out the std though so its fine just annoying)
whats a ucontext?
const pc_range = try readEhPointer(&fbr, cie.fde_pointer_enc, addr_size_bytes, .{
.pc_rel_base = 0,
.follow_indirect = false,
}) orelse return bad();```
bruh i see new syscalls every day
this is part of FDE parsing
Frame Description Entry
idk if that's the right thing
i have no idea what this is supposed to do
i have no idea how this format works 😁
am i even looking at the right thing?
you are yeah
interesting, so yeah, it seems to get the encoding from the CIE (Common Information Entry)
if im reading this right this will blow up
https://ziglang.org/documentation/master/std/#std.debug.Dwarf.readEhPointer heres readEhPointer
i linked that above
it reuses the same encoding for pc_range as it does for pc_begin
but pc_range is a raw value
ah yeah cool
if theres no base it just intCasts it to u64
and actually theres a bug here those intcasts should probably be bitcasts, i think i already opened an issue on zig for that tho
it does pc_rel_base = 0 and follow_indirect = false
so it should give you a raw value
ah yeah in the args
the ctx is a local struct to the call (probably so args can be optional without it being pain)
hmm why does that matter
for example for my kernel it would read it as EH.PE.sdata4
because thats the encoding for pc_start
in which case itd read it as an i32
and then depending on the fde pointer encoding either add nothing or add the passed pc_rel_base which is 0 so also adding nothing
and then the last big if in the function goes to the false path because follow_indirect was set false in the args
in this case rel_mask will be datarel i think
so it will end up with a huge value
or sorry pcrel
hmm
oh yeah
then pc_base = 0
so you end up with a raw value
that's good, right?
yeah you have a type_mask and rel_mask
yeah pc_rel_base was passed in as 0 to the function for this call
so it adds 0 which is fine
there's also
if ((enc & EH.PE.indirect) > 0 and ctx.follow_indirect) {
if (@sizeOf(usize) != addr_size_bytes) {
// See the documentation for `follow_indirect`
return error.NonNativeIndirection;
}
const native_ptr = cast(usize, ptr) orelse return error.PointerOverflow;
return switch (addr_size_bytes) {
2, 4, 8 => return @as(*const usize, @ptrFromInt(native_ptr)).*,
else => return error.UnsupportedAddrSize,
};
} else {
return ptr;
}```
and follow_indirect is false
so it just returns the actual raw value
so that falls through to return ptr yeah
the trick is to pass context info that isnt the real context but makes the same logic work
intCasts not bitCasts
oh that wasnt meant to reply
i thought this was a raw union, not a tagged one
intCast panics if you try to cast a negative to unsigned
its only a theoretical problem if u have a frame thats at least 1 << 31 long
(well on safe modes it does, on unsafe modes intcast is undefined behavior)
so does it sign extend or not?
ah
if its sdata4 it does sign extend to 64 bits
i already have an open issue for zig's debug stuff breaking for kernel mode things with higher half sized numbers
thats a zig issue here
it goes from i32->i64->u64 where the last one is a checked conversion to ensure it's within range of u64
yeah thats a bug that comes from this having only been tested officially for normal usermode stuff
is it still sdata4 for frames that big though
because if the frame is that big it cant fit in signed 32-bits anyway
so thats an issue with the format
i don't think you have to worry about that
using the dwarf format
but pc_range DOES NOT
the piece of code generating the dwarf info
well technically not
they tried to work around the crappy encoding using base 0 pcrel
it will work for smaller frames
where i32 == u32
yep
what u should be doing there is checking how many bytes it took to decode pc_start, and then just memcpy that many bytes into an integer
and the format of pc_start is known
so the core issue is that pc_range should never use a signed encoding
id say the core issue is it tries to decode a value thats not even encoded
ok fair lol
dwarf moment ig
yep
i guess various implementations found different solutions for parsing it
literally all other fields are: hardcoded encoding (e.g. ULEB128), encoding specified in a field, encoding specified in an augmentation
this field: uhhh just memcpy it based on heuristics
rip
llvm is interesting
they discard the rel value as well
what does the 0x0F work out to?
yeah thats probably where they got it from tbh lol
these are basically the possible values
so the & 0x0F just makes it relative to nothing
basically yeah, literally same as zig, except they mask it in a different way
although in llvm this will not sign extend probably
so they will get the right value anyway
ah fuck no nvm
case DW_EH_PE_sdata4:
// Sign extend from signed 32-bit value.
result = (pint_t)(int32_t)get32(addr);
p += 4;
addr = (pint_t) p;
break;
well maybe if LLVM can afford to just interpreter this field like that then i can too
i mean in a kernel thats higher half u cant possibly have that anyway
its limited to 2gb
unless u place it further but then u cant mcmodel=kernel
yeah
didnt actually know that was part of mcmodel=kernel lol
still dont entirely understand what that does
also i just realized
this
*out_value += is_signed ? value.as_i32 : value.as_u32;
is different from this
if (is_signed)
*out_value += value.as_i32;
else
*out_value += value.as_u32;
im still not sure how
but one breaks and other doesnt
which one breaks?
the whole ternary has to have the same type
whereas the if lets the two use different coercions
but i don't see why it would be different
the ternary should have the same value
as the individual ones
maybe it first casts to u32 because thats the rhs side, and then upcasts to u64 so no sign extension?
beacuse it's part of += anyway
this exactly i think
whereas the if does a direct cast from signed to u64
if you put manual casts in the ternary does that fix it?
thats the only explanation i can think of
didnt try that one
let me try
actually an i32 to u64 cast might trigger a warning but lets see
*out_value += is_signed ? (u64)value.as_i32 : value.as_u32;
this seems to produce the right result
ok yeah it was a result type priority thing
when i thought i knew C 
where the compiler finding a common type for the ternary branches didnt take the += into account
basically yeah
okay some other libunwind
maybe its me who's retarded?
maybe
maybe what they were trying to say here is in fact that code
aka ignore the upper 4 bits
lmao
so far llvm, zig and this libunwind seem to do this
and objdump is the only one that disagrees
but you have to have a giant brain to realize that this means fde_encoding & 0x0F
based on that spec text id have ended up with the zig impl personally
what does the official spec say?
lmao thanks spec
Implicit casting in C be like
ok im actually like
insanely bad at binary search
im always off by one no matter what i do
If you want a battle-tested reference I've got this here:
https://github.com/badgeteam/badgelib/blob/e3d02a931e87b70d2cde5734ba1d89514087d980/src/arrays.c#L30
https://github.com/badgeteam/badgelib/blob/e3d02a931e87b70d2cde5734ba1d89514087d980/include/arrays.h#L14
I've been using it forever and it never failed me once.
You could even just copy-paste it and add a link to the gh repo since it's licensed MIT 
the problem is not like a basic binary search
this one i could do just fine
its the one where i need the lower bound value
aka greater than or eq
basically where im searching in an array of pointers and i need to find the closest pointer right below
so that would be lower bound - 1
i cant for the life of me figure out which value corresponds to lb
Just use the result and read the index before that returned by the binary search function.
but the pointer im looking for doesnt exist in the array, which one would it return?
My function returns the index where the value should be, in combination with a boolean that says if that value actually is there.
Java does it with negative integers but yeah same idea.
im gonna write some code and see how it behaves under a debugger i guess, otherwise im just randomly adding ones
That doesn't work if there are duplicates
I didn't know that this was what infy wanted to do
i cant really have duplicates
that said i found the perfect algo for floor() binary search
while (end - begin > 1) {
i = begin + ((end - begin) / 2);
if (data[i] <= target)
begin = i;
else
end = i;
}
begin is always <= target in this one
or well, 0 if target is below the first element
Reminds me of sleep sort
lol
If you don't have duplicates in your array then my binary search would've also worked.
lol unwind opcodes with -fomit-frame-pointers are so trivial
it literally just keeps offsetting CFA as more stack allocations keep happening
if u enable frame pointers it adds a ton more stuff
like switching the CFA reg to RBP etc
for >99% of compiler-generated stack frames, you don't need to care at all for any registers other than rip and cfa
the effect of -fomit-frame-pointers is that it opts in to a behavior where cfa is no longer preferred to be rbp
I'd imagine that with VLAs or any use of __builtin_alloca it's very possible for the compiler to make rbp cfa anyway.
Not to mention nontrivial destructors in C++, which, unless you compile with -fno-exceptions, probably require way more CFI to be able to unwind past scopes with RAII types in them.
Yeah
Luckily for me its just C so I don't have to support that
I believe destructors don't have any impact on CFI and are instead handled by pausing unwind, calling a handler, and resuming it
But I could be wrong about that
i think that's accurate
but you do need to restore all registers for that to work etc
afaict
holy fucking shit
only took 700 loc
this is completely fp-less
what got me is the fact that the CFA rule/offset propagates down to the previous frame
took a while to figure out
also i wasnt offsetting the RSP in my fake reg frame to mask the ret address
basicaly how it works, its completely arch agnostic too, just needs a few defines and a way to suspend ctx/convert arch regs to dwarf layout
ggwp, you won 'documentation hell: the game' level 42: dwarf specification.
(after having done the massive sidequest that is ACPI)
ikr
i wouldnt recommend implementing it, and probably wouldnt if i knew how bad it was from the start
do you parse it in-kernel, or do you run a prepass on it at build time (much like how I assume ksymtab works)?
parse entirely in kernel
binary search eh_frame_hdr to find the fde, then parse that to find cie, than execute both etc etc
yeah... I wouldn't want that sort of stuff in my kernel
Previously I ran a prepass where I generated a binary myself containing much easier to parse debug information, and then linked that binary into the kernel somehow (idr exactly how, though :P).
lol yeah kinda like orc
i mean honestly 700 loc is not that bad considering i made it check literally everything before proceeding
it bails out basically on tiniest things
but for symbols i do preprocess myself
using my kallsyms ™ generator
thats a 850 loc python file

(and is also slow af)
did u lose that code or smth?
no, it's in davix-old
this thing
generating your own simpler dwarf from real dwarf sounds kinda hard so im curious
and then it is parsed in https://github.com/dbstream/davix/blob/davix-old/arch/x86/kernel/unwind.c
not really
well it's not DWARF
this was still a massive PITA to get right.
it generates two data structures:
- a lookup table, from address to symbol name
(this is just an array of (address, symbol) tuples sorted by address iirc)
why are u not using it anymore btw
- a lookup table from address to unwind information (where the unwind information tells offset to "CFA-like" and flags (unwindable/not unwindable, IRQ frame etc.)
it was a PITA to integrate with the build system and not something I want to do again
so basically u simplified dwarf by parsing it at compile time
i mean honstly the dwarf produced for a C kernel is so simple i dont see the need for that
this snippet captures relatively well the build system hell that I created to accomodate stuff like this
the pain of a make user
well you're writing a Linux "clone" aren't you? shouldn't that also include the build system?
nah lol

hopefully its only abi wise
otherwise i would do ORC 
but i've looked at orc and it's not that different from dwarf tbh lol, i'd have to look really hard to understand what exactly they were solving there
Probably this.
but which part specifically
IDK, you'd have to refer to the mailing lists for that one. 
no like, which aspect of dwarf do u consider dangerous
nothing is dangerous per se
i'd honestly argue a frame pointer unwinder is more dangerous because it won't handle NMIs correctly unless u try super hard
whereas this thing will because its designed to work with arbitrarily suspended execution state
I never really understood why anything more modern than a 6502 has NMIs literally at all
well hardware errors, watchdogs, etc
but a IMO valid solution is 'what is NMI?'
stuff that is kinda urgent
i guess profiling as well
some virtualization stuff uses NMIs iirc?
like the only two reasons that I can think of that you want NMI is:
- performance related, profiling.
- NMI reboot.
and for 2, just hold down the goddamn power button
Is it a thing that's really mandatory for a kernel to support?
IIRC someone said PCI devices can trigger NMI via some error pin.
well if by support u mean hlt then yeah
or just panic()
most hobby kernels do that
Yeah ok
yeah iirc pci errors trigger nmis
NMI seems like a PITA to try to ever resume from anyway
just needs some carefully written code
and some core things to be reentrant
like logging and stuff related to the type of work u do in an nmi handler
Anyway I just do -fno-omit-frame-pointer unwinding and dump the addresses to serial. As soon as either it traps while trying to read or it sees NULL it stops.
thats fair i guess
Then my funny python script runs addr2line over it
Works well enough and as soon as you need an actual debugger you'd just use its unwinding instead.
Do you have some kind of roadmap of what you plan to do? It seems like you're starting with various utility things before you get to the actual meat of the kernel?
i have it in my brain i guess
Like any good programmer 
once i clean up unwind stuff ill do
- gdt
- vmemmap
- buddy allocator
- slab allocator
- havent thought that far
this list will probably take me like 6 months
since i will keep coming up with related stuff around those subsystems that needs to be done before i implement them
i already technically have everything needed to implement vmemmap, all the primitives to allocate contiguous chunks and map them arbitrarily
but hidden in there is like an arch-agnostic tlb invalidation mechanism/api etc
tons of shit basically
i wanna have a very very stable foundation before moving forward
I still don't do any cross-CPU TLB invalidation 
yeah thats like a very complex thing to get right
That should probably go after I'm done with my current goal of real disk with real FS
ill have to probably spend like 10 more hours just studying how it works high level
For real. I can think of tons of simple implementations that either don't scale very well or have poor performance.
oh i also missed the idt in the list lol the reason i even got into backtracing and symbols was because i started looking into why people did CFI in their idt handlers
and that got me onto like a 3 week long tangent
but now i have basically military grade symbols and backtracing will be very good as well
yeah
Also fuck I just remembered per cpu variables and areas
Not looking forward
just have every cpu map a page of memory at 0xffffbeef00000000
I statically allocate the one for the BSP (which always gets ID 0 assigned, regardless of what hardware core it is) and put it into a CPU-local place immediately (in my case, the current isr_ctx_t stores a pointer to the cpulocal_t struct). When other processors are brought up, more of these structs are allocated to them and passed as an argument when starting them up.
Yeah but for example Linux has per cpu globals or even struct fields
I have no idea how those are implemented
I imagine with similar mechanisms to how userland does TLS
Probably yeah
However I personally think it's cleaner not to allow CPU-local globals and am generally not a super big fan of TLS
you dont have to do that
After what I've learned from my kernel, I'd still advocate for the approach of having one struct per CPU hidden in some register like sscratch or at gs
Ez per cpu stuff
That's a must yeah
What I'm saying more specifically is not to do more things on top of this.
I'm curious how they do per cpu struct fields
Probably by dynamically resizing per cpu areas
Still cool
Like u can survive without that obviously
its too inflexible not to
you do want other components to be able to do per-cpu things without necessarily needing to add more stuff to the big per-cpu struct
especially if its like a kernel module
important for scalability
Well at that point we're discussing how big the scope should be, no?
But u just said u don't have to do that lol
I can think of lots of things you need in a kernel before it seems like this would before a limiting factor.
per-cpu globals isnt the only way to do it
It being?
And if you need CPU-local in modules that bad, just make a fast function to get the CPU ID and have a table somewhere.
^ This is a way to do CPU-local without TLS-like globals
Just do kconfig
100% agree. I am still suffering for my poor config system choices.
It's cmake options now but I still don't really jive with it
Yeah id honestly just use that because it has so many frontends
I was gonna parse kconfig output into cmake options
they use it to parse .plist files
Thats userspace right
no
Uhhh
i said the kernel
I hope its at least a subset of XML
Yeah but cmake options are my config input and it's kinda not über flexible compared to kconfig
This rust macro here:
macro_rules! cmake_config {
($name: ident, true) => {
pub const $name: bool = true;
};
($name: ident, false) => {
pub const $name: bool = false;
};
($name: ident, $value: literal) => {
pub const $name: i32 = $value;
};
($name: ident, $value: ident) => {
pub const $name: &'static str = "$value";
};
}
From a suitably horrific one-liner file:
cmake_config!(BOOT_PROTOCOL, limine);cmake_config!(SOC, generic);cmake_config!(ENABLE_ACPI, true);cmake_config!(STACK_SIZE, 16384);cmake_config!(EMBED_UBOOT, true);cmake_config!(NOMMU, false);cmake_config!(PAGE_SIZE, 4096);
So this is like runtime kernel config or?
It's supposed to be for compile-time configuration including what features to enable for example.
But this of course doesn't work with Rust's #[cfg(...)]
How do u propagate these options to work at compile time
this gets included here https://github.com/apple-oss-distributions/xnu/blob/main/libkern/c%2B%2B/OSUnserializeXML.cpp
I generate a file, which Rust includes:
# Export configs in a way visible to Rust.
set(RUST_CONFIG_FILE "")
foreach(config_name ${ALL_CONFIGS})
set(RUST_CONFIG_FILE "${RUST_CONFIG_FILE}cmake_config!(${config_name}, ${CONFIG_${config_name}});")
endforeach()
file(CONFIGURE OUTPUT "cmake_config.rs" CONTENT "${RUST_CONFIG_FILE}")
Ah
So how do u make use of that?
Probably so many rces hidden in there lol
i doubt it
its not particularly large and its 26 years old
Thats a header file tho
so
it has survived like 18 years of people scouring xnu for shit thatll help them jailbreak ios
Well currently I don't actually have any Rust that depends on config options but I'd do config::SOMETHINGIMAJIG used in expressions. This doesn't really let me do any code that depends on what needs to or needs not to compile with certain options. So I can't currently use DTB nor ACPI from Rust.
Isn't your entire os in rust?
No, it's still majority C. I'm doing a sort of soft re-write in Rust so the policy is new code is Rust if possible but old code is not re-written just to make it Rust.
Rust solves all the things I find most annoying about C:
- Memory bugs are much better protected against
- You can just have generic collections
- You have the legendary ? operator for error handling
So you could say I like it very much. This is basically my first time doing a major project in Rust.
Interesting
There is a small stipulation, though: Rust's strictness can cause some weird issues with e.g. global variables. I made an unsafe const function today because to do safe mutex init, it either has to be in the .bss section or it must have an atomic thread fence, and I wanted a global mutex guarding some array.
Yeah I'm personally not a fan of how it does globals
AFAIK there isn't really a way to enforce that you only use something from a compile-time context but never run-time so I marked it as unsafe to deter from accidentally not initializing a mutex properly.
I mean you can do everything you could with C too, it's just less polished compared to the rest of the language IMO.
Oh and I haven't done this yet but: Have fun doing RCU in Rust 
Sometimes you can't get around them.
it reminds me of how v has that hack to have globals
or at least what it used to have, idk how it is now
But the way I've been programming doesn't use globals that much so it's not too much of a problem.
ok yeah if you want globals in v you have to both add -enable-globals and put them in __global
Yep
A lot of these core kernel primitives go against rust
Like intrusive stuff, rcus, containerof, globals
Oh yes they do
im pretty ignorant on rust because i tend to stay in my little topics and not stray too far but i should probably try doing something in it just to see right
Yeah
Basically all primitives that integrate tightly with the scheduler in some way will be full of unsafe.
Fortunately, though, I've got a lot more Rust to write than just scheduler primitives so it's probably fine 
I can't really make a blanket endorsement of Rust because I feel like it
Does it still have problems with oom propagation or did they fix that
It's very much a thing where it's per person whether they like it
Alloc stuff has try_ versions for the most part, so you can just ? operator out of it.
or you can be infy and implement an unwinder and catch the panic :^)
soemtimes people ask me my opinion on rust for kernel dev and i keep having to say "i dont know ive never used it"
but why
I'm not going to its a meme
whyd you implement an unwinder though
I even went and made this little thing here:
impl From<AllocError> for Errno {
fn from(_: AllocError) -> Self {
Errno::ENOMEM
}
}
So alloc errors integrate nicely with my standard EResult type
tbh I don't think you can do that in C without some help from the compiler
Just stack traces
Without frame pointers
does clang have support for the msvc try/catch?
ngl the main reason I force frame pointers is because my friend really doesn't want to emit cfi stuff in the jit
Yeah I don't think u can unless u compile c code in c++ mode
so we just do the easy route of everything having frame pointers and unwinding with that whenever an exception is thrown
or well, we will once he will implement landing pads
That's fair
Is that some custom lang were talking about
You see, this is why I like Rust's ? operator so much. No wacky nonsense with try/catch that causes unexpected things to happen when something throws.
Its C#
you dont wanna know how i did this
Don't tell me u have 10 level recursive unwind table for it
Ahh
how does this work lol, IPI?
Unless you have hardware TLB shootdown, yes.
look into async IO schedulers, they're fun 😉
high quality
tbf
The only problem is its impossible to catch signal frames
This sounds like a months-long tangent lol
Yep
if entry == interrupt_stub:
print("IRQ frame")
XD
it took me 4 days 💥 I'm still implementing the necessary API I designed for some block devices but it has full support on NVMe and SATA SSDs rn
Real
Probably safer in nmis than a fp unwinder
i only subtract the stack pointer at the start of the function. and its a risc with fixed size instructions. so basically i can just walk backwards 4 bytes at a time until i find an instruction that looks like subi sp, sp, X and the immediate value is the stack frame size. then i can add that to get to the next stack frame
What are the advantages, anyway? I don't support non-blocking disk I/O requests anyway.
it's more optimal on some hardware
like request coalescing
HDDs benefit from sending less requests so if you schedule requests you might have two with adjacent LBAs that you can merge
That's fair enough, so what would u do if I have a large switch with each branch allocating locals?
I don't even coalesce two consecutive block reads even if I know I could 💀
wait wdym
my compiler is stupid so it will never change the stack allocation during the runtime of a function
if one day it did, i would simply have to add more debug information like youre using now
So it basically preallocates all variables of all branches?
void (*do_coalesce)(struct generic_disk *dev, struct bio_request *into,
struct bio_request *from);
void (*reorder)(struct generic_disk *dev);``` these are the two big optimizations
only the stuff that ends up on the stack but yeah
Yeah
coalesce and reorder
Makes sense
I batch writes to disk in up to 64 blocks at once, and if you have a long contiguous access, you could easily (with the scatter-gather of AHCI) do multiple blocks in one ATA command.
Meant to reply to this
But I actually just do individual block reads. Or writes.
on fox32 i just use a frame pointer
with async IO you can get prefetching and readahead tho
which is fun
Does fox have nmis?
basically if you start reading a file you can spam a bunch of read requests for that file's blocks to get them in cache
then further reads do less work
it doesnt
You know, this sounds like something I could deal with if it weren't the case that my generic block device API is in C but the driver is in Rust.
Btw do u do some sort of safe stack deref when traversing it, or does it just die if a frame is bogus?
Like maybe propagate an error from the pf handler
Interop between manual implementations of vtables in C and Rust traits is a thing™
one other cool thing about async IO is you can have an auto-boosting system where requests naturally get checked every so often (20ms for me) and boosted if enough time has passed and the queue they are boosting to has enough availability
+-> Average completion time of level 0 is 261 ms
+-> Average completion time of level 1 is 81 ms
+-> Average completion time of level 2 is 90 ms
+-> Average completion time of level 3 is 13 ms
+-> Average completion time of level 4 is 0 ms``` like I submitted a few thousand requests here and I let all of them just naturally boost until dispatch and the timing is pretty ok
ignore that weird bit of prioritiy inversion with level 2, the priorities are PRNG generated so I think they're unbalanced sometimes
i MmIsVirtualValid every virtual address before i use it, and currently i only do stack tracing while the debugger is running and every other cpu is suspended while thats ongoing, so it shouldnt be a toctou
Remind me about this in #1153085124959809616 once I've re-written the device abstraction layer in Rust.
Fair enough
one issue is that this doesnt work with pageable code
Sounds like a cool optimization, just not one I'm in a happy spot to do.
if a stack trace passes thru a function in a page of code that happened to be outpaged then itll suddenly end the stack trace and youll be sad
Probably wouldn't work anyway because u cant wake up the pager thread?
yeah
U could force all paged kernel code to get read when attaching a debugger ig
only works if you voluntarily break into the debugger
Why would you want to page out kernel code though? It's small compared to the userspace stuff that you'll likely run on top in the end.




