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
Comments