Rust-y Memory. How Safe?
Tags: rust, rustc, llvm, memory
Published: 2022-02-04 20:03:34


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5whsu4epptijauzz3vry.jpg)By [Austin Aigbe](https://www.linkedin.com/in/austin-aigbe-1ba59b50/)


## Introduction
The [Rust](https://www.rust-lang.org/) programming language is said to be memory-safe but how does it achieve this? Why should we even care about memory safety? It turns out that the major cause of security vulnerabilities in modern day software systems (including Desktop and mobile applications) is due to memory safety issues and to address this, there is a general consensus, which I personally agree to, that a memory-safe systems programming language like `Rust` is required. This is one of the reasons big tech companies have decided to [invest in Rust](https://foundation.rust-lang.org/members/) and re-engineer some of their key products and services with it.


## How does `Rust` Achieve Memory Safety?
Memory safety is achieved through three key concepts: `ownership` (a language feature the compiler uses to free and allocate memory based on the scope of a variable binding), `borrow checking` and `lifetimes`. All these analyses are done during compile-time.

For simplicity, I will focus more on the `ownership` concept and show us how the compiler guarantees memory safety at compile-time using the scope of a variable binding to determine when to allocate and deallocate memory on the stack. 

Before we proceed, let's review how our code is analyzed by the Rust compiler (`rustc`) for memory safety.


## Code Compilation With Memory Management
In simple terms, a `memory` is a storage space (e.g `RAM`) on your computer where instructions to be executed by the computer's CPU are stored. These instructions are the lines of Rust code you have written and compiled with `rustc` (the Rust compiler) or `cargo` into a machine executable file (e.g [`.exe`](https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#ms-dos-stub-image-only) file format on Windows and [`elf`](https://refspecs.linuxfoundation.org/) on Linux). The executable file tells the Operating System how to load your `Rust` program into `memory` for execution by the CPU.

Now, let's compile a very simple `Rust` program with `rustc` and examine how memory management (`ownership`) works in Rust.

```rust
// hello.rs
fn main() {
    let rust_edition = 2021;
    let message = "Hello, Rust";
    println!("{message} {rust_edition}!");
}
```
Compiling the above code with `rustc .\hello.rs` on Windows will produce an executable file,`hello.exe` (about 148KB in size), in the same directory as the .rs file.

Next, let's see how the `hello.exe` was produced by `rustc` and how the memory-safety checks were done.

The Rust compiler, `rustc`, performed several analyses on the `hello.rs` file before it produced the `hello.exe` executable file. I will give a high-level overview of this process so we understand how memory safety is achieved by the compiler and at what phase of the compilation process it is done.

![](https://i.imgur.com/72SEKSw.png)
Figure 1: An simplified view of `rustc` compilation process 

**Phase 1**: `hello.rs` was translated to basic tokens using the `rustc_lexer` crate and then to Rust `tokens` using the `rustc_parser` crate. Tokens are easier for the compiler to work with than the text format of your .rs file.

**Phase 2**:`Tokens` were translated to `AST` (Abstract Syntax Tree) format. Syntax analysis is done here. Use `cargo inspect --unpretty=ast-tree .\hello.rs` to examine the AST output.
![](https://i.imgur.com/wHwJRPz.png)
Figure 2: AST tree of `hello.rs` with `cargo-inspect`

**Phase 3** The `println!` macro was refined (or desugared) to a `std::io::_print` statement and `core::fmt::Arguments` function calls.The data type of our expressions were inferred and checked. We can say that type safety is guarateed at this phase of the code compilation. The HIR (High-Level Intermediate Representation) is the output of this stage. You can use `crago inspect --unpretty=hir .\hello.rs` to view the HIR representation of your Rust code.


![](https://i.imgur.com/kSPdFvv.png)
Figure 3:`println!` macro desugared in the HIR. 

**Phase 4** HIR was converted to MIR (Mid-level Intermediate Representation). Ownership, borrow-checking and optimizations are done here. In fact, the MIR shows the scope of each variable and helps the compiler know at what point a variable binding (or ownership) is out of scope and when to dellocate memory from the stack. How the compiler tracks the scope of each variable is indicated in the screenshot below. In fact, the assembly code generated by [LLVM](https://llvm.org/) (in phase 6) is the machine representation of the MIR (after the ownership, borrow checking and optimizations have been done). In the next phase, the `asm` .S file is examined to see how the variables are allocated on the `Stack` when they are in scope and how they are deallocated when they are out of scope. In this phase, memory safety is guaranteed by the compiler.
![](https://i.imgur.com/9BSWZ32.png)
Figure 4: MIR representation of `hello.rs`

**Phase 5 and 6** This is the code generation phase - LLVM was used to generate the final executable file `hello.exe` from the optimized and memory-safe MIR representation. Further optimizations can also be done by LLVM. 

Let's briefly examine the assembly code (.S file) generated by LLVM. You can use `rustc --emit asm .\hello.rs` to generate the file. To keep things simple, I will only examine how the `ownership` memory-safety feature was achieved by the allocation and deallocation of memory on the stack.
 

![](https://i.imgur.com/E1VoudA.png)

Figure 5: A simplified memory layout of the stack from the perspective of the compiler generated `main` function.

The assembly code for the `main` function is shown above (in Figure 5). The entry point is not our `hello::main` function. As we will see later, Rust has a runtime (`std::rt::lang_start_internal`). This runtime handles a lot of complexities for us that we don't need to bother about when writing our Rust code.

- Line 516: The compiler generated main function is the entry point for our program.
- Line 518: 40 bytes of memory is allocated on the stack.`rsp` is the 64-bit stack pointer register for x86_64. It always points to the top the stack.

- Line 523: `_ZN5hello4main17h0767239aa2b5c6caE` is the mangled symbol for our `hello.rs` `main()` function. The address of our `hello::main` function is stored in %rcx and then passed to the runtime function as a reference in line 103.
 
- Line 524: This line calls the Rust runtime function `std::rt::lang_start` (defined in line 94) and subsequently, the internal runtime function `std::rt::lang_start_internal` (defined in line 104). Rust has a runtime that executes our `hello.rs main()` function.
![](https://i.imgur.com/gx2fnqx.png)

Figure 6: Rust runtime function executes our hello::main function

![](https://i.imgur.com/hSGodss.png)
Figure 7: Execution of `hello::main` (part 1)

![](https://i.imgur.com/oWxVc3T.png)
Figure 8: Execution of `hello::main` (part 2)

Figures 7 and 8 show how sufficient memory (200 bytes) was first allocated on the stack by the compiler before allocating memory to the two local variables `rust_edition` (an `i32`), and `message` (a `&str`). Before returning from the `hello::main` function, the compiler deallocates memory on the stack and frees up resources.


## Conclusion
We have seen how Rust guarantees memory safety during compile-time by examining the compilation phases and how memory is allocated and deallocated on the stack in assembly code. A very simple Rust program (`hello.rs`) was used to examine the output of each compilation phase and the memory management of the stack in assembly code.

We also learnt that the Rust compiler generates a `main` function for us as the entry point of our program. This main function uses the Rust runtime to execute the main function of our Rust program.

In summary, Rust tries to guaranty memory safety during compile time and it does a pretty good job in ensuring this.


## References
1. [Overview of the Compiler](https://rustc-dev-guide.rust-lang.org/overview.html)
2. [Ownership](https://doc.rust-lang.org/1.8.0/book/ownership.html)
3. [References and Borrowing](https://doc.rust-lang.org/1.8.0/book/references-and-borrowing.html)
4. [x86 Assembly Language Reference Manual](https://docs.oracle.com/cd/E19253-01/817-5477/817-5477.pdf)
5. [Assembly Language: Function Calls]( https://www.cs.princeton.edu/courses/archive/spr16/cos217/lectures/15_AssemblyFunctions.pdf)
6. [x64 Cheat Sheet](https://cs.brown.edu/courses/cs033/docs/guides/x64_cheatsheet.pdf))
7. [The Stack & Procedures](https://courses.cs.washington.edu/courses/cse351/17au/lectures/11/CSE351-L11-procedures-I_17au-ink.pdf)