addr=0那个,必定报错,用其他值都没问题
thread 'main' panicked at src/main.rs:32:9:
assertion left == right
failed
left: 0
right: 0
addr=0那个,必定报错,用其他值都没问题
thread 'main' panicked at src/main.rs:32:9:
assertion left == right
failed
left: 0
right: 0
Null references are UB:
You can run miri on the playground, it will throw an error like this:
error: Undefined Behavior: constructing invalid value: encountered a null reference
--> src/main.rs:16:26
|
16 | let f = unsafe { &mut *(addr as *mut Foo) };
| ^^^^^^^^^^^^^^^^^^^^^^^^ constructing invalid value: encountered a null reference
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
= note: BACKTRACE:
= note: inside `main` at src/main.rs:16:26: 16:50
References aren't just pointers. References must always be initialized and non-NULL.
The compiler takes advantage of references never being 0. If your unsafe code will try to turn NULL into a reference, it will cause bugs, and you'll get buggy results. This is Undefined Behavior, and UB doesn't give any guarantees or bounds or reasons why it does what it does. You're just not allowed to cause it.
Specifically in this case the optimizer saw a value converted from a reference compared to a null, and just replaced it with false, because that can never be true.
Heres' the best I can do to show what the optimizer did, seems it's tricky to do cleanly (playground):
struct Foo;
#[unsafe(no_mangle)]
fn test() -> bool {
let addr = 0;
let f = unsafe { &mut *(addr as *mut Foo) };
eq_addr(f, addr)
}
fn eq_addr(f: &mut Foo, addr: usize) -> bool {
std::hint::black_box(&f);
f as *mut Foo as usize == addr
}
LLVM IR:
; Function Attrs: nonlazybind uwtable
define noundef zeroext i1 @test() unnamed_addr #0 {
start:
%0 = alloca [8 x i8], align 8
%f.i = alloca [8 x i8], align 8
call void @llvm.lifetime.start.p0(i64 8, ptr nonnull %f.i)
store ptr null, ptr %f.i, align 8
call void @llvm.lifetime.start.p0(i64 8, ptr nonnull %0)
store ptr %f.i, ptr %0, align 8
call void asm sideeffect "", "r,~{memory}"(ptr nonnull %0) #2, !srcloc !3
call void @llvm.lifetime.end.p0(i64 8, ptr nonnull %0)
call void @llvm.lifetime.end.p0(i64 8, ptr nonnull %f.i)
ret i1 false
}
Here you can see it's just unconditionally returning false after the cruft put in by black_box
.
It's weirdly tricky to get it to reproduce this result with a minimized output: it seems you need to hide just enough from the optimizer that it can't see that f
/ self
came from addr
without hiding that it's a ref in general.
The latter is much cleaner to see as just:
struct Foo;
#[unsafe(no_mangle)]
fn is_null(f: &mut Foo) -> bool {
f as *mut Foo as usize == 0
}
LLVM IR:
; Function Attrs: mustprogress nofree norecurse nosync nounwind nonlazybind willreturn memory(none) uwtable
define noundef zeroext i1 @is_null(ptr noalias nocapture noundef nonnull readnone align 1 %f) unnamed_addr #0 {
start:
ret i1 false
}