-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Description
Zig Version
0.13.0
Steps to Reproduce and Observed Behavior
Taking a volatile pointer to a nested array of packed structs (such as an array of machine registers) causes loads to not be volatile which can lead to bad machine code like infinite loops being generated
The minimal repro I could achieve is this:
https://godbolt.org/z/dcrae4639
This while loop doesn't execute the ldr(line 5 in the 0.12 asm) again each loop on arm or riscv32 since at least 0.13, which causes an infinite loop if polling a machine register for a change. In 0.12 it could be worked around by populating the loop.
In the latest versions it can still be worked around by putting a memory clobber in the loop. Notably this doesn't repro on x86 but does repro on at least aarch64, thumb, and riscv32.
This affects the mmio.Mmio struct used for accessing machine registers in Microzig in some cases, so does have some impact for common embedded use cases.
Copy of code in case godbolt gets lost:
// Build args: -target thumb-freestanding -OReleaseSmall
const struct_view = packed struct(u32) {
raw: u32,
};
const Nested = extern struct {
bad: [1]struct_view,
good: struct_view,
};
export fn main_bad() void {
const mmio_ptr: *volatile Nested = @ptrFromInt(0x40000000);
while (mmio_ptr.bad[0].raw == 0) {} // Skips the load on repeated loops
// ldr r0, [r0] // <- Load outside the loop
// .LBB0_1:
// cbnz r0, .LBB0_3
// b .LBB0_1
// .LBB0_3:
}
export fn main_good() void {
const mmio_ptr: *volatile Nested = @ptrFromInt(0x40000000);
while (mmio_ptr.good.raw == 0) {} // Does volatile load of register
// .LBB0_1:
// ldr r2, [r0] // <- Load inside the loop
// cbnz r2, .LBB0_3
// b .LBB0_1
// .LBB0_3:
}Expected Behavior
Expect loads through *volatile to always be treated as volatile.