In this post we’ll be exploring one of the most important and powerful features of the Rust programming language: Structs. Structs are a fundamental part of Rust’s type system and are used to define custom data types that can represent complex data structures. They play a crucial role in organizing code and making it more maintainable and readable.

We’ll take a closer look at what a struct is, how to define and use it, and the various ways in which it can be customized to suit your specific needs. We’ll also explore some of the advanced features of Rust’s structs, such as adding methods and implementing traits.

Like in C, in Rust, a struct is a data structure that allows you to group together related data and define your own custom data types. Here’s a comparison of C struct and equivalent definition in rust:


struct person {
    char *name;
    uint32_t age;
    bool is_student;
};
void main() {
    struct person john = {
        .name = "John Doe",
        .age = 30,
        .is_student = true
    };
    printf("Name: %s\n", john.name);
    printf("Age: %d\n", john.age);
    printf("Is student: %d\n", john.is_student);
}

struct Person {
    name: String,
    age: u32,
    is_student: bool,
}
fn main() {
    let john = Person {
        name: String::from("John Doe"),
        age: 30,
        is_student: true,
    };
    println!("Name: {}", john.name);
    println!("Age: {}", john.age);
    println!("Is student: {}", john.is_student);
}

Rust and C have similar syntax for defining structs, but there are some key differences between the two.
First difference is how they handle method calls. In C, structs do not have methods, so you must use functions that take a pointer to the struct as an argument. In Rust, structs can have methods defined directly on them, which allows for a more object-oriented style of programming.

Secondly, Rust’s struct system is more flexible than C’s. Rust’s struct system allows for the definition of “tuple structs” and “unit structs”, which don’t have named fields. This allows for more flexibility in defining custom data types. Additionally, Rust’s struct system has the concept of “derived traits”, which allows for the automatic implementation of certain traits like Eq, Ord, and Debug based on the struct’s fields.

We will learn rust sturcts by building a simple tool to dump ELF headers. Similar to what the command readelf -h does.


$ readelf -h ./a.out
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Position-Independent Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1060
  Start of program headers:          64 (bytes into file)
  Start of section headers:          13976 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         13
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 30

Quick intro to ELF – The ELF (Executable and Linkable Format) file format is used in most Unix-like operating systems, including Linux. It serves as a standard file format for executables, shared libraries, and object code. The ELF file format consists of several sections, including the header, the program headers, the section headers, and the sections themselves. The header provides information about the type of file and its architecture. We will print the header in human readable format. You can read more above the ELF file format from the ELF specification

Here is a C program to print the first two lines of readelf -h.


#include <stdint.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <elf.h>
#include <errno.h>

/*  The following are defined in elf.h; added in this comment for quick reference.
#define EI_NIDENT 16

typedef uint64_t	Elf64_Addr;
typedef uint16_t	Elf64_Half;
typedef uint64_t	Elf64_Off;
typedef int32_t		Elf64_Sword;
typedef int64_t		Elf64_Sxword;
typedef uint32_t	Elf64_Word;
typedef uint64_t	Elf64_Lword;
typedef uint64_t	Elf64_Xword;

typedef struct {
        unsigned char   e_ident[EI_NIDENT];
        Elf64_Half      e_type;
        Elf64_Half      e_machine;
        Elf64_Word      e_version;
        Elf64_Addr      e_entry;
        Elf64_Off       e_phoff;
        Elf64_Off       e_shoff;
        Elf64_Word      e_flags;
        Elf64_Half      e_ehsize;
        Elf64_Half      e_phentsize;
        Elf64_Half      e_phnum;
        Elf64_Half      e_shentsize;
        Elf64_Half      e_shnum;
        Elf64_Half      e_shstrndx;
} Elf64_Ehdr;
*/
bool is_valid_elf_header(Elf64_Ehdr *header) {
    return header->e_ident[0] == 0x7f &&
            header->e_ident[1] == 'E' &&
            header->e_ident[2] == 'L' &&
            header->e_ident[3] == 'F'
    ;
}

void print_elf_magic(Elf64_Ehdr *header) {
    int i;

    printf("  Magic:   ");
    for (i=0; i < sizeof(header->e_ident); i++)
        printf("%02x ", header->e_ident[i]);
    printf("\n");
}

void print_elf_class(Elf64_Ehdr *header) {
    printf("  Class:   ");
    switch (header->e_ident[EI_CLASS]) {
        case ELFCLASS32:
            printf("ELF32\n");
            break;
        case ELFCLASS64:
            printf("ELF64\n");
            break;
        default:
            printf("Invalid\n");
    }
}

int print_elf_header(char *file_path) {
    int fd, len;
    Elf64_Ehdr header;

    fd = open(file_path, O_RDONLY);
    if (fd == -1) {
        fprintf(stderr, "unable to open file %s: ", file_path);
        perror("");
        return errno;
    }
    
    len = read(fd, &header, sizeof(header));
    if (len != sizeof(header)) {
        fprintf(stderr, "unable to read file %s: ", file_path);
        return -1;
    }

    if (!is_valid_elf_header(&header)) {
        fprintf(stderr, "Not a ELF file");
        return -2;
    }
    
    printf("ELF Header:\n");
    print_elf_magic(&header);
    print_elf_class(&header);
    close(fd);
    return 0;
}

int main(int argc, char *argv[]) {
    if (argc <= 1) {
        printf("Usage : %s \n", argv[0]);
        return -1;
    }
    return print_elf_header(argv[1]);
}

If we compile and run this program, it print the Magic and Class of the ELF.

$ gcc elf_info.c -o elf_info
$ elf_info ./elf_info
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:   ELF64

Lets convert this program into Rust and see how struct advanced in Rust.


use std::env;
use std::io::Read;

const EI_NIDENT:usize = 16;
const EI_CLASS:usize = 4;

type Elf64Addr = u64;
type Elf64Half = u16;
type Elf64Off = u64;
type Elf64Word = u32;

const ELFCLASSNONE:u8 =	0;
const ELFCLASS32:u8 = 1;
const ELFCLASS64:u8 = 2;

#[derive(Debug)]
struct Elf64Ehdr {
        e_ident: [u8; EI_NIDENT],
        e_type: Elf64Half,
        e_machine: Elf64Half,
        e_version: Elf64Word,
        e_entry: Elf64Addr,
        e_phoff: Elf64Off,
        e_shoff: Elf64Off,
        e_flags: Elf64Word,
        e_ehsize: Elf64Half,
        e_phentsize: Elf64Half,
        e_phnum: Elf64Half,
        e_shentsize: Elf64Half,
        e_shnum: Elf64Half,
        e_shstrndx: Elf64Half,
}

impl Elf64Ehdr {
    fn is_valid_elf_header(&self) -> bool {
        self.e_ident[0] == 0x7f &&
        self.e_ident[1] == b'E' &&
        self.e_ident[2] == b'L' &&
        self.e_ident[3] == b'F'
    }
}

impl std::fmt::Display for Elf64Ehdr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "ELF Header:");
        write!(f, "ELF Header:")
    }
}

fn print_elf_magic(header: &Elf64Ehdr) {
    print!("  Magic:   ");
    for b in header.e_ident {
        print!("{b:02x} ");
    }
    println!();
}

fn print_elf_class(header: &Elf64Ehdr) {
    print!("  Class:   ");
    if header.e_ident[EI_CLASS] == ELFCLASS32 {
        println!("ELF32");
    } else if header.e_ident[EI_CLASS] == ELFCLASS64 {
        println!("ELF64");
    } else {
        println!("Invalid class");
    }
}

fn read_elf_header_from_file(file_path: &String) -> Elf64Ehdr {
    let mut file = std::fs::File::open(file_path).unwrap();
    let header: Elf64Ehdr = {
        let mut h = [0u8; std::mem::size_of::()];

        file.read_exact(&mut h[..]).unwrap();

        unsafe { std::mem::transmute(h) }
    };
    header
}

fn print_elf_header(file_path: &String) {
    let header = read_elf_header_from_file(file_path);
    if !header.is_valid_elf_header() {
        eprintln!("Not a ELF file");
        return;
    }
    println!("{}", header);
    
    println!("ELF Header:");
    print_elf_magic(&header);
    print_elf_class(&header);
}

fn main() {
    let args: Vec<_> = env::args().collect();
    print_elf_header(&args[2]);
}

This article can be viewed as video here

Categorized in:

Tagged in: