How the get the address of variables?

Hi all,
How can I print out the memory address of a variable e.g. print out the memory address of x in let x = 100; rather than the value 100?

Same question for array, struct variables.

Thank you,

fn main() {
    let my_var = 100;
    
    println!("{:#x}", (&my_var) as *const i32 as usize);
}
0x7fffe76ac864
1 Like

Thank you very much, it really helps.
Btw, what if I want to print out the memory address of xref in let xref = &x rather than the value it points to?

Thanks,

You need to cast the reference to a raw pointer to do that. References print the value behind them.

1 Like
use std::ptr::addr_of;

fn main() {
    let num = 32;
    println!("Address: {:?}", addr_of!(num));
    let borrow = #
    println!("Address: {:?}", addr_of!(borrow));
}
4 Likes

You can also use the {:p} (pointer) format:

fn main() {
    let x = 100;
    let xref = &x;
    println!("{:p}", &x);
    println!("{:p}", &xref);
}
2 Likes

WARNING!

This is a very unreliable and misleading way of trying to figure out what Rust does.

  1. Taking an address of a variable changes what the program is doing. It happens to be a very important side effect for the optimizer. It may prevent data from being in registers (variables can exist in registers, but registers don't have addresses). It can also make the optimizer pessimistically assume the address "escapes", and therefore the variable can't be optimized away. Just taking an address of a variable can make code behave differently than the same code that never saw the address.

  2. Rust allows moves, so addresses of most things are temporary and meaningless. It can move variable to one location, let you take its address, and then move the variable to entirely different address. The value you'll print could be out of date and irrelevant. It may be reused for other variables or other data.

17 Likes

Is this true? I would think a variable's address stays constant throughout its life. Can you provide example code?

If you don't take its address, it could be optimized to be stored in a register. In that case, it has no address.

It also definitely can be moved. The program only has to behave as-if it doesn't move.

1 Like

But that's true about everything -- everything can be changed in compiled code if it doesn't modify the behavior. That's like saying: u32 might have 64 bits (as long as the difference is not observable).

Sure, it might be compiled to something that moves in memory, but that's not in the semantics of Rust (as I understand), it's just a code transformation that might be performed. You seem to agree that as far as the semantics of the language is concerned, addresses are fixed.

"What The Hardware Does" is not What Your Program Does.

It is absolutely possible for variables to change address, if they don't have their address taken. This is dictated by register pressure, and LLVM may decide to spill registers to stack and load them back many times throughout the function (e.g. dump variables to stack before a loop that doesn't use them, reload them from the stack after, and then dump (some of) them elsewhere next time registers are needed).

This is also why observing an address can have such a big impact. If the address is known to the program, it forces the place behind it to be more like the variable you expect, with a known address that must stay valid for the lifetime of the variable. But if the address never leaks, the variable can be treated as a simple anonymous value, with no obligation to keep it at all, and can be merged into other expressions or discarded before its scope ends.

Even struct fields are not going to stay in the same place as the struct. LLVM has a pass that breaks up struct fields into individual variables, and then decides individually whether to keep them in registers or memory. So something that looks like a single struct variable in the code, may end up being multiple variables, which then may be folded into other expressions, and their scope moved around as the code is reordered.

And there's even a funny case of WONTFIX UB in LLVM. It has both:

  • an assumption that the address of every variable is unique, so if you compare addresses of different variables the result can be hardcoded as false without checking,
  • many de-facto necessary code transformations that contradict this, such as reuse of stack after scopes are left and entered again, or optimization passes that avoid needless copies from one variable to another, leaving both at the same address instead.
4 Likes

Obviously, but that just follows from the general rule that the compiler may translate your Rust code into any machine code it wants with the same observed behavior. That has nothing to do with some special rule that says:

I believe Rust doesn't have any such rule that allows extra moves at arbitrary times, moves only happen explicitly (but I may be wrong, which is why I asked).

I think the point here is the indirect one that you can't really do anything useful by looking at the variables this way, since by looking you've perturbed what happens.

So while sure, it'll give some addresses, you can't make any useful decisions with those addresses, since any kind of "why does it _______?" questions will probably end up answered with "well it wouldn't if you weren't printing it".

1 Like

I disagree. It depends on what kind of questions you want answered. You will not learn about how the code is optimized exactly in many cases, but that's not the only thing you might want to investigate.

For example, by looking at addresses you will learn that:

  • x and xref are not aliases, they are different variables
  • array elements are laid out consecutively with a stride
  • struct elements are laid out next to each other
  • Vec contents don't move when you move a Vec to a different variable

Yes, there is the important caveat that optimization might change what happens under the hood under the as-if rule when you're not looking. But if you're not specifically investigating optimization that's OK because the resulting observed behavior is the same anyway. It's still useful information to see what happens "in theory".

If you're interested in figuring out "what Rust does" with your code, you're a lot better off sticking it into https://rust.godbolt.org/ and using Matt Godbolt's Compiler Explorer to look at LLVM IR, machine code etc. You can even set up a view with llvm-mca and the optimization pipeline exposed to get a sense of what the compiler finally outputs in terms of CPU resources, and how the LLVM IR that rustc translated your program into is turned into machine code.

1 Like

I like cargo asm even more than godbolt, because it can show code of any project with dependencies, without need to reproduce a minimal example online.

And I think it's important to note that Rust is designed to work with an optimizer. The unoptimized code is going to have many function calls, wrapper types, temporary values, and copies that are not meant to be there. The unfortunate flip side of that is that it requires looking at the actual assembly (or final LLVM IR) to really understand what Rust is supposed to be doing.

The question whether &i32 and i32 are different depends on what abstraction level you ask. In Rust's semantics they're different just because Rust says so, and their addresses don't play a role here (that's an implementation detail). OTOH in the implementation, the difference between &i32 and i32 may not exist. The compiler may choose to never store i32 in memory and ignore the reference entirely, or may choose to dereference a &i32 early and treat it as i32 (it easily can, since it's immutable).

1 Like

All this talk about assembly, optimizations, etc, is completely irrelevant to the original question. It was asked by a beginner that wanted to see that there is a difference between an i32 variable and another &i32 variable that points to it.

They didn't ask to see assembly or how printing something affects optimizations. These are different, advanced topics.

It's not really true that taking an address of something "changes what the program is doing". It just prints something in addition to what the program is doing.

It (like many things) affects what kind of optimizations will be performed, but generally optimizations do not, by design, affect any functionality, they only affect performance. Yes, if you never look at the address, a variable might be optimized to be stored in register, or to not be stored anywhere, but that doesn't actually affect anything (except performance) since you're not printing the address in that scenario (which is why the optimizer can do this).

1 Like

Hi All,
Thank you all very much for both solutions and extended conversation through it I learn a lot.

  • Thanks to @alice for the casting approach which reveals the details
  • Thanks to @Schard for the macro approach which is much more facilitated by the library however I prefer the core::ptr::addr_of! because some of my application don't use stdlib.
  • Thanks to @kornel & @tczajka for extended conversion which shows some very interesting points for my further digging.
5 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.