Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Hello, world!

Source

Initialize a new workspace with cargo init.

fn main() {
    println!("Hello, world!");
}

println! is a macro that wraps the _print function.

$ cargo rustc --release --quiet -- -Z unpretty=expanded
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2024::*;
#[macro_use]
extern crate std;
fn main() { { ::std::io::_print(format_args!("Hello, world!\n")); }; }

Alternatively, you can see all macro expansions (including built-in ones) in the HIR:

$ cargo rustc --release --quiet -- -Z unpretty=hir
#[prelude_import]
use std::prelude::rust_2024::*;
#[macro_use]
extern crate std;
fn main() {
    { ::std::io::_print(format_arguments::new_const(&["Hello, world!\n"])); };
}

Build

$ cargo rustc --release

Ghidra

Load the binary into Ghidra and auto-analyze it.

rust_lab::main

As we saw above, println! is expanded to a _print call, which accepts an Arguments struct.

While reconstructing the Arguments type, the type size information is very useful. Note that the compiler might reorder the struct fields.

$ cargo rustc --release --quiet -- -Z print-type-sizes 
print-type-size type: `core::fmt::rt::Placeholder`: 48 bytes, alignment: 8 bytes
print-type-size     field `.precision`: 16 bytes
print-type-size     field `.width`: 16 bytes
print-type-size     field `.position`: 8 bytes
print-type-size     field `.flags`: 4 bytes
print-type-size     end padding: 4 bytes
print-type-size type: `std::fmt::Arguments<'_>`: 48 bytes, alignment: 8 bytes
print-type-size     field `.pieces`: 16 bytes
print-type-size     field `.args`: 16 bytes
print-type-size     field `.fmt`: 16 bytes
...
print-type-size type: `core::fmt::rt::Argument<'_>`: 16 bytes, alignment: 8 bytes
print-type-size     field `.ty`: 16 bytes
print-type-size type: `core::fmt::rt::ArgumentType<'_>`: 16 bytes, alignment: 8 bytes
print-type-size     variant `Placeholder`: 16 bytes
print-type-size         field `.value`: 8 bytes
print-type-size         field `.formatter`: 8 bytes
print-type-size         field `._lifetime`: 0 bytes
print-type-size     variant `Count`: 10 bytes
print-type-size         padding: 8 bytes
print-type-size         field `.0`: 2 bytes, alignment: 2 bytes
print-type-size type: `core::fmt::rt::Count`: 16 bytes, alignment: 8 bytes
print-type-size     discriminant: 2 bytes
print-type-size     variant `Param`: 14 bytes
print-type-size         padding: 6 bytes
print-type-size         field `.0`: 8 bytes, alignment: 8 bytes
print-type-size     variant `Is`: 2 bytes
print-type-size         field `.0`: 2 bytes
print-type-size     variant `Implied`: 0 bytes
print-type-size type: `std::option::Option<&[core::fmt::rt::Placeholder]>`: 16 bytes, alignment: 8 bytes
print-type-size     variant `Some`: 16 bytes
print-type-size         field `.0`: 16 bytes
print-type-size     variant `None`: 0 bytes
...

The simplified Arguments type can be represented like this (explained in detail later). This is not valid C syntax of course, as &, [] or <> cannot be used in C struct names.

struct &[&str] {
    ptr64 ptr;
    usize len;
};

struct &[Argument] {
    ptr64 ptr;
    usize len;
};

struct Option<&[Placeholder]> {
    ptr64 ptr;
    usize len;
};

struct Arguments {
    struct &[&str] pieces;
    struct &[Argument] args;
    struct Option<&[Placeholder]> fmt;
};

Listing:

                             **************************************************************
                             * rust_lab::main                                             *
                             **************************************************************
                             undefined __rustcall main()
             undefined         <UNASSIGNED>   <RETURN>
             undefined8        Stack[-0x10]:8 local_10                                XREF[2]:     00401af4(W), 
                                                                                                   00401b1c(R)  
             Arguments         Stack[-0x40]   arguments                               XREF[1,2]:   00401b04(W), 
                                                                                                   00401b14(W), 
                                                                                                   00401b10(W)  
                             _ZN8rust_lab4main17hf9a0ba7e2c977e69E           XREF[3]:     main:00401b38(*), 00453c6c, 
                             rust_lab::main                                               004648a8(*)  
        00401af0 ff 03 01 d1     sub        sp,sp,#0x40
        00401af4 fe 1b 00 f9     str        x30,[sp, #local_10]
        00401af8 68 03 00 90     adrp       x8,0x46d000
        00401afc 08 61 1c 91     add        x8,x8,#0x718
        00401b00 29 00 80 52     mov        w9,#0x1
                             store pieces.ptr and pieces.len
        00401b04 e8 27 00 a9     stp        x8=>PTR_s_Hello,_world!_0046d718,x9,[sp]=>argu   = 0044c1a0
        00401b08 08 01 80 52     mov        w8,#0x8
                             move the struct address to the first argument
        00401b0c e0 03 00 91     mov        x0,sp
                             zero out args.len and fmt.ptr
        00401b10 ff ff 01 a9     stp        xzr,xzr,[sp, #arguments+0x18]
                             store args.ptr
        00401b14 e8 0b 00 f9     str        x8,[sp, #arguments.args.ptr]
        00401b18 da 63 00 94     bl         std::io::stdio::_print                           undefined _print()
        00401b1c fe 1b 40 f9     ldr        x30,[sp, #local_10]
        00401b20 ff 03 01 91     add        sp,sp,#0x40
        00401b24 c0 03 5f d6     ret

The logic is simple: it constructs an Arguments struct on the stack and passes the address of it via sp to the _print function.

Decompiled code (after creating the Arguments type in the Structure Editor and applying it in the code):

/* WARNING: Unknown calling convention: __rustcall */
/* rust_lab::main */

void __rustcall rust_lab::main(void)

{
  Arguments arguments;
  
                    /* store pieces.ptr and pieces.len */
  arguments.pieces.ptr = (ptr64)&PTR_s_Hello,_world!_0046d718;
  arguments.pieces.len = 1;
                    /* move the struct address to the first argument */
                    /* zero out args.len and fmt.ptr */
  arguments.args.len = 0;
  arguments.fmt.ptr = (ptr64)0x0;
                    /* store args.ptr */
  arguments.args.ptr = (ptr64)0x8;
  std::io::stdio::_print(&arguments);
  return;
}

From the Rust reference:

Though you should not rely on this, all pointers to DSTs are currently twice the size of the size of usize and have the same alignment.

In practice, this means that the fields of the struct Arguments are 16 bytes in memory: an 8 byte pointer and an 8 byte length. This is confirmed by the output of -Z print-type-sizes above.

pieces is a reference to a slice of str references (&str). In this case, pieces references only 1 &str which is also an 8 byte pointer and an 8 byte length.

                             PTR_s_Hello,_world!_0046d718                    XREF[1]:     main:00401b04(*)  
        0046d718 a0 c1 44        addr       s_Hello,_world!_0044c1a0                         = "Hello, world!\n"
                 00 00 00 
                 00 00
        0046d720 0e              ??         0Eh
        0046d721 00              ??         00h
        0046d722 00              ??         00h
        0046d723 00              ??         00h
        0046d724 00              ??         00h
        0046d725 00              ??         00h
        0046d726 00              ??         00h
        0046d727 00              ??         00h

args is a reference to a slice of Argument items and it references an empty slice now. Empty slices do not point to null but their size is 0. They point to valid addresses instead, depending on the alignment (8 bytes here).

print-type-size type: `core::fmt::rt::Argument<'_>`: 16 bytes, alignment: 8 bytes
print-type-size     field `.ty`: 16 bytes
fn main() {
    let empty_u8: &[u8] = &[];      // 1-byte aligned
    let empty_u32: &[u32] = &[];    // 4-byte aligned  
    let empty_u64: &[u64] = &[];    // 8-byte aligned
   
    println!("u8 address: {}", empty_u8.as_ptr() as usize);
    println!("u32 address: {}", empty_u32.as_ptr() as usize);
    println!("u64 address: {}", empty_u64.as_ptr() as usize);
}
$ cargo run --release --quiet
u8 address: 1
u32 address: 4
u64 address: 8

fmt is an optional reference to a slice of Placeholder items. For Option<&[T]>, Rust -often- uses null pointer optimization where None is represented by a null pointer. Therefore, the length field is irrelevant and is not populated in the current example.

print-type-size type: `std::option::Option<&[core::fmt::rt::Placeholder]>`: 16 bytes, alignment: 8 bytes
print-type-size     variant `Some`: 16 bytes
print-type-size         field `.0`: 16 bytes
print-type-size     variant `None`: 0 bytes

rust-gdb

We can verify the results of our static analysis using rust-gdb (or rust-lldb) which supports Rust types.

First we need to create a debug build where the function new_const constructing the Arguments struct is not optimized and inlined.

$ cargo rustc

Then we start a GDB server and connect to it with the rust-gdb client. We will examine the Arguments struct returned by new_const.

$ qemu-aarch64 -g 1234 target/aarch64-unknown-linux-musl/debug/rust-lab
$ rust-gdb -q -ex "target remote localhost:1234" target/aarch64-unknown-linux-musl/debug/rust-lab
Reading symbols from target/aarch64-unknown-linux-musl/debug/rust-lab...
Remote debugging using localhost:1234

This GDB supports auto-downloading debuginfo from the following URLs:
  <https://debuginfod.fedoraproject.org/>
Enable debuginfod for this session? (y or [n]) y
Debuginfod has been enabled.
To make this setting permanent, add 'set debuginfod enabled on' to .gdbinit.
0x00000000004019bc in _start ()
(gdb) b rust_lab::main
Breakpoint 1 at 0x401bd4: file src/main.rs, line 2.
(gdb) c
Continuing.

Breakpoint 1, rust_lab::main () at src/main.rs:2
2	    println!("Hello, world!");
(gdb) disas
Dump of assembler code for function _ZN8rust_lab4main17hb3ccde9ab543d852E:
   0x0000000000401bc0 <+0>:	sub	sp, sp, #0x50
   0x0000000000401bc4 <+4>:	stp	x29, x30, [sp, #64]
   0x0000000000401bc8 <+8>:	add	x29, sp, #0x40
   0x0000000000401bcc <+12>:	add	x8, sp, #0x10
   0x0000000000401bd0 <+16>:	str	x8, [sp, #8]
=> 0x0000000000401bd4 <+20>:	adrp	x0, 0x46d000
   0x0000000000401bd8 <+24>:	add	x0, x0, #0x710
   0x0000000000401bdc <+28>:	bl	0x401b54 <_ZN4core3fmt2rt38_$LT$impl$u20$core..fmt..Arguments$GT$9new_const17h2005e5bc47942c4fE>
   0x0000000000401be0 <+32>:	ldr	x0, [sp, #8]
   0x0000000000401be4 <+36>:	bl	0x41abf0 <_ZN3std2io5stdio6_print17h5a3b0843896b0124E>
   0x0000000000401be8 <+40>:	ldp	x29, x30, [sp, #64]
   0x0000000000401bec <+44>:	add	sp, sp, #0x50
   0x0000000000401bf0 <+48>:	ret
End of assembler dump.
(gdb) si 3
core::fmt::Arguments::new_const<1> (pieces=0x7f867d3fe830)
    at /home/gemesa/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/fmt/rt.rs:226
226	    pub const fn new_const<const N: usize>(pieces: &'a [&'static str; N]) -> Self {
(gdb) fin
Run till exit from #0  core::fmt::Arguments::new_const<1> (pieces=0x7f867d3fe830)
    at /home/gemesa/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/fmt/rt.rs:226
0x0000000000401be0 in rust_lab::main () at src/main.rs:2
2	    println!("Hello, world!");
Value returned is $1 = core::fmt::Arguments {pieces: &[&str](size=1) = {"Hello, world!\n"}, fmt: core::option::Option<&[core::fmt::rt::Placeholder]>::None, args: &[core::fmt::rt::Argument](size=0)}
(gdb)

The value returned matches the expected Arguments struct.