Note that you don't have to just learn ASM, there's a variety of tools to let you know what's changed, for example you could check if using the suggestions of casting a pointer or using a union makes any difference.
You might want to try two different ways to write the same function, and see if the output is substantially different (e.g. other than names), or you could look at the Rust MIR and llvm IR to see how Rust transformed your code into LLVM's view of instructions, then the LLVM optimization pipeline to see how it decided to transform your code into assembly.
Just because lea
is a meaningless term or xor eax, eax
is bizzare to see, doesn't mean Compiler Explorer can't help you understand what's happening at every level.
But yes, if you intend to care about the specifics at this level, you should probably learn at least enough of assembly to be able to read it. It's mostly pretty straightforward:
mov foo, bar
is foo = bar;
add foo, bar
is foo += bar;
, etc. (so xor eax, eax
is eax ^= eax;
, e.g. eax = 0
, but smaller due to x86 being weird)
call foo
is foo(...)
, where the arguments are what's in registers and on the stack (this is what a "calling convention means"
ret
is return
Thats most of the instructions you need to care about, so other than picking up what registers do what and knowing about the stack and so on, there's really not that much to it.
In particular, your example without printing the pointers and using an extern function to avoid it just completely eliminating the entire body:
extern { fn bar(foo: Foo1); }
pub fn foo() {
let foo_0 = Foo0 { bar: 1 };
let foo_0 = unsafe { transmute::<_, Foo1>(foo_0) };
unsafe { bar(foo_0) };
}
optimizes (don't forget to pass -O
to the compiler) to this LLVM IR:
define void @example::foo::hb73cef95ff019d63() unnamed_addr {
start:
tail call void @bar(i64 1)
ret void
}
declare void @bar(i64) unnamed_addr #0
Where you can see it has completely optimized away literally everything. The assembly is just:
example::foo::hb73cef95ff019d63:
mov edi, 1
jmp qword ptr [rip + bar@GOTPCREL]
(apologies to those who hate intel syntax)
That is just:
- set a register to 1 (this being the register used for the first argument to a call)
- "jump" to
bar
- this is an optimized call since it's the last thing we are doing in this function so returning from bar
is also returning from foo
.
You can play around and look at references for assembly and pick the important stuff up really quick, if already are caring about stack layout.