Most people just use lifetimes without understanding how they are implemented under the hood. So I decided to dig into the compiled binary to see how Rust actually manages lifetimes at the machine level.
The String Struct Layout
Before diving into the assembly, it helps to recall how Rust represents a String internally. A String is essentially a wrapper around a Vec<u8>, which in turn manages a heap-allocated buffer:
| Field | Description |
|---|---|
| ptr | Pointer to the heap-allocated buffer |
| cap | Capacity (number of bytes the buffer can hold without reallocating) |
| len | Current length (number of bytes used) |
Move value
In Rust, ownership ensures that when a value is passed to a function by move, the caller loses access to it. This guarantees that memory can be safely freed once the function finishes. Unlike in C, where the programmer decides when to free memory, Rust enforces this through the type system.
Here is a simple example that moves a String value into the f function.
| |
Let’s look at the main function first.
Function main
1 testRust::main:
2 sub rsp, 24
3 call qword, ptr, [rip, +, _RNvCs73fAdSrgOJL_7___rustc35___rust_no_alloc_shim_is_unstable_v2@GOTPCREL]
4 mov edi, 16
5 mov esi, 1
6 call qword, ptr, [rip, +, _RNvCs73fAdSrgOJL_7___rustc12___rust_alloc@GOTPCREL]
7 test rax, rax
8 je .LBB5_2
9 movups xmm0, xmmword, ptr, [rip, +, .Lanon.d3c4e510eecd334b8939fa13e4b98187.3]
10 movups xmmword, ptr, [rax], xmm0
11 mov qword, ptr, [rsp], 16
12 mov qword, ptr, [rsp, +, 8], rax
13 mov qword, ptr, [rsp, +, 16], 16
14 mov rdi, rsp
15 call testRust::f
16 add rsp, 24
17 ret
18 .LBB5_2:
19 lea rdx, [rip, +, .Lanon.d3c4e510eecd334b8939fa13e4b98187.2]
20 mov edi, 1
21 mov esi, 16
22 call qword, ptr, [rip, +, _ZN5alloc7raw_vec12handle_error17h5c9e72494d298ff8E@GOTPCREL]
Memory location
- 4-6 line: Allocate 16 bytes of memory like
malloc - 7-8 line: Check
rax- If
rax == 0-> allocation failed -> jump to error handler
- If
Load string
- 9-10:
- load 16 bytes of data from
.Lanon...5(String) - Store into allocated memory
- load 16 bytes of data from
Calling f Function
- 11-13 line: Prepare arguments for
f[rsp] = 16[rsp+8] = rax[rsp+16] = 16
- 14-15 line: Call
ffunction - 16-17 line: Finish
ffunction
Function F
1 testRust::f:
2 sub rsp, 24
3 mov rax, qword, ptr, [rdi, +, 16]
4 mov qword, ptr, [rsp, +, 16], rax
5 movups xmm0, xmmword, ptr, [rdi]
6 movaps xmmword, ptr, [rsp], xmm0
7 mov rax, rsp
8 #APP
9 #NO_APP
10 mov rsi, qword, ptr, [rsp]
11 test rsi, rsi
12 je .LBB4_2
13 mov rdi, qword, ptr, [rsp, +, 8]
14 mov edx, 1
15 call qword, ptr, [rip, +, _RNvCs73fAdSrgOJL_7___rustc14___rust_dealloc@GOTPCREL]
16 .LBB4_2:
17 add rsp, 24
18 ret
Function Prologue
- Line 3-6: Copy the parameter.
Prepare for Deallocation
- Line 10-12: Check Deallocate Size
rsisize for the deallocate- If it’s zero → skip deallocation, else proceed to free memory
Call Deallocator
- Line 13-15:
rdi = [rsp+8]pointer for the deallocateedx = 1= align for the deallocate- Call
deallocfunction to free the memory
Follow the Stream
Now we can see how this String value is moved:
- Allocate 16 bytes memory.
- Load the string literal from
.Lanon...5. - store the string into the allocated memory.
- Pass ownership into
f. - At the end of
f, the string is deallocated.
allocate → copy literal → pass to f → drop
In this case, the String value is moved into the function, and ownership is dropped inside the function.
Borrow value
Borrowing in Rust means the function only receives a reference, not the ownership of the value. The compiler ensures that the borrowed value will not be dropped while it is still borrowed. In our assembly view, you can see that the f function doesn’t deallocate memory — the deallocation happens when main finishes, because the owner is still main.
Here is a simple example where a String value is borrowed by the f function.
| |
Function main
1 testRust::main:
2 push r14
3 push rbx
4 sub rsp, 24
5 call qword, ptr, [rip, +, _RNvCs73fAdSrgOJL_7___rustc35___rust_no_alloc_shim_is_unstable_v2@GOTPCREL]
6 mov edi, 16
7 mov esi, 1
8 call qword, ptr, [rip, +, _RNvCs73fAdSrgOJL_7___rustc12___rust_alloc@GOTPCREL]
9 test rax, rax
10 je .LBB5_4
11 mov rbx, rax
12 movups xmm0, xmmword, ptr, [rip, +, .Lanon.d3c4e510eecd334b8939fa13e4b98187.3]
13 movups xmmword, ptr, [rax], xmm0
14 mov qword, ptr, [rsp], 16
15 mov qword, ptr, [rsp, +, 8], rax
16 mov qword, ptr, [rsp, +, 16], 16
17 mov rdi, rsp
18 call testRust::f
19 mov esi, 16
20 mov edx, 1
21 mov rdi, rbx
22 call qword, ptr, [rip, +, _RNvCs73fAdSrgOJL_7___rustc14___rust_dealloc@GOTPCREL]
23 add rsp, 24
24 pop rbx
25 pop r14
26 ret
27 .LBB5_4:
28 lea rdx, [rip, +, .Lanon.d3c4e510eecd334b8939fa13e4b98187.2]
29 mov edi, 1
30 mov esi, 16
31 call qword, ptr, [rip, +, _ZN5alloc7raw_vec12handle_error17h5c9e72494d298ff8E@GOTPCREL]
32 .LBB5_3:
33 mov r14, rax
34 mov esi, 16
35 mov edx, 1
36 mov rdi, rbx
37 call qword, ptr, [rip, +, _RNvCs73fAdSrgOJL_7___rustc14___rust_dealloc@GOTPCREL]
38 mov rdi, r14
39 call _Unwind_Resume
Function f
1 testRust::f:
2 mov qword, ptr, [rsp, -, 8], rdi
3 lea rax, [rsp, -, 8]
4 #APP
5 #NO_APP
6 ret
As you can see, the contents of main do not change much, and even the call to f looks the same. But inside f, the assembly shows almost nothing happening — it behaves like a dummy function. Borrowing essentially works by copying a pointer into the function. Once the function returns, the original owner (main) is still responsible for deallocating the value at the end of its scope.
allocate → pass pointer to f → f uses pointer → original caller drops
If you compare this with C, the generated binary looks familiar: memory is allocated (similar to malloc) and later freed (similar to free). This is because Rust uses LLVM under the hood, so the low-level instructions resemble C.
The crucial difference is when deallocation happens. In C, the programmer must remember to free manually, which can easily cause memory leaks or double frees. Rust, however, enforces ownership rules at compile time, guaranteeing that every allocated value is freed exactly once.
Looking Ahead
These topics will give us a broader perspective on how Rust enforces memory safety — not just in simple cases, but across the wide range of real-world scenarios developers face.