A lot of this is OS-Dev specific, so bear with me here. I'm on the freestanding target for 32 bit x86 using the multiboot1 specification to boot up my kernel. The important part there if you're unfamiliar with the spec (tbh so am I) is that once my entry point is called by the boot loader, EBX is guaranteed to have a pointer to a multiboot structure. When I use grub-legacy, this works surprisingly well (more explanation incoming)
#Memory Address seems to bypass nullcheck
1 messages · Page 1 of 1 (latest)
However, when I use limine (just a different bootloader that follows the same spec) I get a pointer val of 0 passed to setup. The super weird thing is that manually looking at EBX in the trampoline fn, I can clearly see ebx has the proper address of a struct (0x10000 here, grub just works differently AFAIK). There are 2 weird things here (the title will start to make sense now)
The pointer being set to 0x0 makes no sense, as that bypasses safe mode's null cast check. I know this to be true because 1) I have kernel panic properly set up, and this doesn't trigger the panic, and 2) I can see the null check in the assembly
Even weirder, is I hardcode that 0x1000 address when using limine (again, which I got from manually breakpoint-ing at trampoline and looking at ebx), I get proper behavior in terms of getting the proper pointer (and ofc the structure being pointed to makes sense now)
Am I somehow pulling the EBX value in trampoline incorrectly?
the compiler seems to just not safety check the result of inline assembly, which tbh is fair since its unsafe to begin with.
But it probably should check that the output(s) are valid values for their type since it is akin to a cast/reinterpreting of the data.
you can get safety checks by changing the result type to be ?* or usize then converting that to a non optional pointer.
I forgot to mention, I got the exact same behavior doing this
As in, I would store ebx as a usize first, then cast to ptr, and same thing
I would even see the nullcheck pass at that call site (trampoline) but when passed to setup, it was zero
I have to ask what mode you are compiling with, also what target you are using
ReleaseSafe, x86-freestanding-none
FYI i have a custom panic handler that works and I’ve seen properly pickup and report IB (in my case a kernel panic)
Does it work if you relax the ={ecx} constraint to =r
Lemme try rq
Same issue
But for the "correct solution" you shouldn’t expect any register to be in any state in a non-naked function
The best choice is usually to make trampoline a callconv something well defined (like sysv) and then in your inline asm to jump out of naked, place the arguments in the right registers and use trampoline like a normal function
and sanity checked even using usize and casting into a ptr
what's wrong with jumping to trampoline like this?
I get ebx can't be guaranteed to be preserved
nothing wrong, but zig makes no guarantee that ebx will not be clobbered before the first line of trampoline
but I can see it is preserved in the generated asm
so yeah I should probably address that, but I'm still perplexed with why my bound variable isn't keeping the value in ebx
without the disassembly of setup theres really no way to say for certain
and also that the null check isn't triggering a panic
setup is quite beefy now, but I check the address at this point in setup
I'd recommend reducing that down to the minimal code that reproduces the issue because 6500 lines of asm is not exactly quickly debuggable
I’ll give that a shot in the AM
also, as for how I would clean up the trampoline:
export fn boot() linksection(".boot") callconv(.naked) noreturn {
asm volatile(
\\ movl %[stack_top], %esp
\\ pushl %ebx
\\ pushl %eax
\\ jmp *%[trampoline]
:
: [stack_top] "i" (stack_top),
[trampoline] "r" (&trampoline),
);
}
fn trampoline(mbmagic: u32, mbinfo: *const MultibootInfo) linksection(".trampoline") callconv(.{ .x86_sysv = .{} }) noreturn {
assert(mbmagic == 0x2BADB002);
@import("setup.zig").setup(mbinfo);
}
Why push eax?
Oh the magic num
I have the whole mb structure baked in at compile time anyway
/// Header to mark our kernel as bootable. Will be placed at the beginning of
/// our kernel's binary, and will be interpretted by the bootloader as the header
/// of bytes defining how the kernel will be booted.
pub export const multiboot_header linksection(".multiboot") = switch (bootoptions.boot_specification) {
.MultibootOne => bootutils.MultiBoot.V1.init(
.{
.flags = .{
.enforce_all_4kb_alignment = false,
.include_memory_information = false,
.include_video_mode_info = true,
.activate_address_configurations = false,
},
.header_addr = undefined,
.load_addr = undefined,
.load_end_addr = undefined,
.bss_end_addr = undefined,
.entry_addr = undefined,
},
.{ .height = 25, .width = 80, .mode_type = .ega_text, .depth = 0 },
),
else => |e| @compileError(
"(Currently) Unsupported boot specification for x86: " ++ @tagName(e),
),
};
the magic number comes from the bootloader, you should generally check it to ensure that you actually got multiboot loaded
So getting rid of all that code between the start of setup and when I first use the address masks the problem
That code being setting up the IDT and GDT
Though I don't see anything fishy with either of these, at least immediately
... what the, it was my SSE causing me issues, limine works just fine like this. I can't see what's wrong with my enabling of SSE, I pulled this from the wiki