DEV Community

Piotr Zarycki
Piotr Zarycki

Posted on • Originally published at pzarycki.com on

Rust Ownership - from source code to assembly

Image

Why?

Ownership is the concept in Rust that determines how memory is managed.

In languages with a garbage collector, allocating objects is straightforward — the GC scans for unreachable objects and frees their memory.

…but Rust has no garbage collector. Without any rules, we could keep allocating until memory unexpectedly ran out and the program crashed — and that’s exactly where Ownership comes in.

fn main() {
    let first_owner = String::from("Book");
    let second_owner = first_owner;
    println!("{}", first_owner);
    println!("{}", second_owner);
}
Enter fullscreen mode Exit fullscreen mode
 error[E0382]: borrow of moved value: `first_owner`
 --> src/main.rs:4:20
  |
2 |     let first_owner = String::from("Book");
  |         ----------- move occurs because `first_owner` has type `String`, which does not implement the `Copy` trait
3 |     let second_owner = first_owner;
  |                        ----------- value moved here
4 |     println!("{}", first_owner);
  |                    ^^^^^^^^^^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let second_owner = first_owner.clone();
  |                                   ++++++++

For more information about this error, try `rustc --explain E0382`.
error: could not compile `playground` (bin "playground") due to 1 previous error

Enter fullscreen mode Exit fullscreen mode

As you can see — we barely wrote anything and Rust is already complaining. But that’s intentional!

Remove the first println!() (you can try it right here :)) and the code compiles just fine.

Why?

Because every value has exactly one owner, and by assigning it to another variable we invalidated the first one.

Bug or feature?

To illustrate the problem more clearly, let’s write something similar in C:

  #include <stdio.h>
  #include <stdlib.h>
  #include <string.h>

  int main() {
      char *s = malloc(strlen("Rust") + 1);
      strcpy(s, "Rust");
      free(s);
      printf("%s\n", s);
      return 0;
  }
Enter fullscreen mode Exit fullscreen mode
  // random buffer output after free

Enter fullscreen mode Exit fullscreen mode

How does this relate to Rust?

In the C example above, we were able to free memory and then use it anyway — creating a potential exploit or undefined behavior.

In C there is no ownership, so who owns s?

Nobody — it’s just a pointer.

The programmer bears full responsibility for ensuring memory is freed at the right moment, without creating an opportunity for remote code execution.

This class of bugs has its own CVE category with high CVSS scores, and we’ve seen hundreds of them in the wild. Rust eliminates this statically — the program won’t compile instead of silently misbehaving.

Transferring ownership through functions

Let’s try passing ownership through a function:

fn main() {
    let first_owner = String::from("Book");
    takes_ownership(first_owner);
}

fn takes_ownership(str: String) {
    println!("takes_ownership: {}", str); 
}
Enter fullscreen mode Exit fullscreen mode
  takes_ownership: Book
Enter fullscreen mode Exit fullscreen mode

The program runs fine and prints the value. Now let’s add a println! to print first_owner after the call:

fn main() {
    let first_owner = String::from("Book");
    takes_ownership(first_owner);
    println!("first owner: {}", first_owner);
}

fn takes_ownership(str: String) {
    println!("takes_ownership: {}", str); 
}
Enter fullscreen mode Exit fullscreen mode
  error[E0382]: borrow of moved value: `first_owner`
 --> src/main.rs:4:33
  |
2 | let first_owner = String::from("Book");
  | ----------- move occurs because `first_owner` has type `String`, which does not implement the `Copy` trait
3 | takes_ownership(first_owner);
  | ----------- value moved here
4 | println!("first owner: {}", first_owner);
  | ^^^^^^^^^^^ value borrowed here after move
  |
note: consider changing this parameter type in function `takes_ownership` to borrow instead if owning the value isn't necessary
 --> src/main.rs:7:25
  |
7 | fn takes_ownership(str: String) {
  | --------------- ^^^^^^ this parameter takes ownership of the value
  | |
  | in this function
help: consider cloning the value if the performance cost is acceptable
  |
3 | takes_ownership(first_owner.clone());
  | ++++++++

For more information about this error, try `rustc --explain E0382`.
error: could not compile `playground` (bin "playground") due to 1 previous error
Enter fullscreen mode Exit fullscreen mode

Rust won’t compile again — ownership was transferred into takes_ownership.

The invisible Drop

We know Rust enforces a single owner. Let’s compile our program and inspect the resulting assembly using objdump:

objdump -d -M intel target/debug/ownership | grep -A 20 "takes_ownership"


  ===============================================================
  2318b: mov rsi, [rsp+0x8] ← second argument for _print (len)
  ========= println implementation ============
  2319e: jmp 231a0 ← jump to drop

  231a0: mov rdi, [rsp+0x18] ← pointer to String (argument for drop)
  231a5: call drop_in_place ← drop at }
  231aa: add rsp, 0x58 ← restore stack
  231ae: ret ← return from function


Enter fullscreen mode Exit fullscreen mode

In garbage-collected languages, the GC checks whether objects are out of scope and frees their memory. In Rust, the compiler injects the resource cleanup at the end of the value’s lifetime — in this case at }.

fn main() {
    let first_owner = String::from("Book"); // move ownership into takes_ownership
    takes_ownership(first_owner);
    println!("first owner: {}", first_owner);
} 

fn takes_ownership(str: String) {
    println!("takes_ownership: {}", str); 
} // drop injected here

Enter fullscreen mode Exit fullscreen mode

This is analogous to a destructor (~MyClass()) in C++ — with the difference that Rust guarantees it runs exactly once through the ownership system, whereas in C++ it depends on the programmer’s discipline.

Rust also allows implementing the Drop trait, which is called whenever a value goes out of scope.

Let’s use the file-opening mechanism from std:

  use std::fs::File;

  fn main() {
      {
          let f = File::open("/etc/hostname").unwrap();
          println!("file opened");
      } // close(fd) here

      println!("file already closed");
  }


  impl Drop for OwnedFd {
      fn drop(&mut self) {
          unsafe {
              let _ = libc::close(self.fd.as_inner()); // ← syscall close(fd)
          }
      }
  }

Enter fullscreen mode Exit fullscreen mode

libc::close is called automatically — it all happens under the hood!

To give ownership back, simply return the value:

fn takes_ownership(str: String) {
    println!("takes_ownership: {}", str);
    str // return ownership back
}

Enter fullscreen mode Exit fullscreen mode

Copy: when ownership transfer becomes a copy

fn main() {
    let first_owner:i32 = 10;
    takes_ownership(first_owner);
    println!("first_owner: {}", first_owner);
}

fn takes_ownership(num: i32) {
    println!("takes_ownership: {}", num); 
}
Enter fullscreen mode Exit fullscreen mode

Compared to our first example, Rust should be complaining here — but instead it compiles just fine.

  use std::fs::File;

  fn main() {
      println!("i32: {}", std::mem::needs_drop::<i32>());
      println!("f64: {}", std::mem::needs_drop::<f64>());
      println!("bool: {}", std::mem::needs_drop::<bool>());
      println!("char: {}", std::mem::needs_drop::<char>());

      println!("String: {}", std::mem::needs_drop::<String>());
      println!("Vec: {}", std::mem::needs_drop::<Vec<i32>>());
      println!("File: {}", std::mem::needs_drop::<File>());
      println!("Box: {}", std::mem::needs_drop::<Box<i32>>());

      println!("(i32, i32): {}", std::mem::needs_drop::<(i32, i32)>());
      println!("(i32, String): {}", std::mem::needs_drop::<(i32, String)>());
      println!("[i32; 3]: {}", std::mem::needs_drop::<[i32; 3]>());
  }
Enter fullscreen mode Exit fullscreen mode
i32: false
f64: false
bool: false
char: false
String: true
Vec: true
File: true
Box: true
(i32, i32): false
(i32, String): true
[i32; 3]: false
Enter fullscreen mode Exit fullscreen mode

For primitive types, Rust creates a copy instead of moving ownership. These types live entirely on the stack — a copy is literally a few bytes, with no side effects, no allocations, no pointers.

Two copies are completely independent.

Rust implements a trait that tells the compiler a type can be copied bitwise:

#[derive(Copy, Clone)]
struct Point {
    x: f64,
    y: f64,
}

Enter fullscreen mode Exit fullscreen mode

This only makes sense when a type lives entirely on the stack and doesn’t manage any resources (e.g. heap memory, file descriptors).

That’s why String cannot implement Copy — it owns an internal buffer on the heap.

But code sometimes needs not just to transfer ownership, but to access the same value from multiple places.

That’s what Borrowing is for — which will be the topic of the next post. Stay tuned!

Links

Top comments (0)