My &str is all null characters

Hello All,

I'm just playing around with rust on bare metal Raspberry Pi, to learn both Rust and more about the Pi. I've got something booting and I can interact via the MiniUART, so I'm happy to a point.

HOWEVER! I am really struggling to understand why a statically allocated string ("Hello World", in fact) is turning into a &str with \0000 characters but the correct length when passed from my entry function to the function which iterates through the characters and sends the bytes individually.

When I look with GDB, I just see that it's a &str with the right numbers of '\000' characters instead of my actual message. If I put an extra send character in the for loop, to send the character 'A' at the same time as the character from the &str should go, I get exactly the right number of 'A's!

If I look in the ELF file, the string is there, and GDB even picks it up from the debug symbols before it's passed.

What am I missing?


1 Like

Tough to say without code.


It's hard to debug the code you show above. :thinking:


It'd be easier to help with some code, but some things to keep in mind; static slices in Rust usually have near-enough to compile-time lengths, so the pointer could still be getting messed up without the length getting corrupted. One thing that would be interesting to see there would be, what happens when you include a multibyte character - do you get 0s equal to the number of characters or the number of bytes?

This is just a guess, but is your ELF binary being loaded into memory correctly? Normally the OS (technically the loader - man will make sure the correct segments from the binary are mapped to the right spot. However, if this is a bare-metal application with no OS, you might need to do this yourself.


Appreciate the honesty - I was wondering whether I was making some complete rookie error.

It's probably easiest to look at the github I'm using (GitHub - chunky125/rust-mach: GNU Mach Compatible Kernel in Rust), with the two main files of interest being the main rust entry point (src/ and the MiniUART code (src/raspi/peripherals/

What I will say is that this is the call that doesn't work:

// Say Hello!
mu.send_string("Rust Mach OS, initialising\r\n");

Even if I try to iterate in the main function, so I don't think it's the passing of the variable that's the issue.

In the assembly (messy - I've copied it from a previous project and not updated it yet, or thought about), I try to clear the BSS, but we have no BSS to clear here.



Does your MiniUART::send() calls below work, or do they also not work? I assume they work, since you get '\0' output from your MiniUART::send_string() function. I don't see anything obvious, but I don't know the hardware at all, so I have no idea if you are setting up the registers correctly.

If you look in, this call:

mu.send_string("Rust Mach OS, initialising\r\n");

Doesn't work, but the individual calls:

if mbinfo.valid() == true {
mu.send('M' as u8);
mu.send('B' as u8);
mu.send('2' as u8);
mu.send('O' as u8);
mu.send('K' as u8);
mu.send('\r' as u8);
mu.send('\n' as u8);

All work.

If I move the iterator code below

    for c in s.chars() {
        self.send(c as u8);

from the send_string into the boot_entry function, it still fails as nulls.

Maybe try for b in s.bytes(). Chars in Rust is not byte-sized. They are UTF-32 dwords. That may be what is causing your issue, but I doubt.

I get the same issue when I try with bytes too, I did wonder what might cause it.

All I can think is that my linking or init code is butchering the value somehow, but I'm not sure.

Hi there,

even though the code in general looks good so far there are some caveats with your miniUART setup which might influence the way the characters are passed depending on the timing of the actual send. Sending each character individually might cause some microsecond delay which keeps it working, but as soon as you send a bunch of bytes it is failing on the UART.

The Pull-Up/Down setting for your miniUART GPIO pins require a very special "procedure" to get it right on the Raspberry PI 3.
First if all you can only set one pin at a time.
Second the procedure:

  1. write the desired pud control value to the PUD control register GPPUD (0x0 in your case)
  2. wait 150 CPU cycles
for _ in 0..150 {
  unsafe { asm!("NOP") }
  1. write the pin to update into the PUDCLCK register
  2. wait 150 cycles to settle the new settings
for _ in 0..150 {
  unsafe { asm!("NOP") }
  1. clear the pud control value in the PUD control register GPPUD
  2. write the pin to the PUDCLCK register again to finish the update cycle

With this in place you should definitely use send_str.as_bytes() to get an &[u8] slice of your string to be send via miniUART.

I'm also wondering if setting the FIFO settings in register AUX_MU_IIR_REG would be required as I'm not sure on the default values there after powering of the PI...

In case you want to get inspiration you might want to check out the ruspiro-* crate family. Especially ruspiro-gpio and ruspiro-uart might help.

There is also a small tutorial for the ruspiro-* crate family available here
Those crates (which I'm the creator and maintainer of) provide already a great abstraction of most low-level stuff of the Raspberry Pi written in Rust to get you kick-started with Rust BareMetal coding on the Pi. Raspberry Pi 4 support is currently in the making :wink:

1 Like

Thanks for your detailed reply and your taking pity on a rust beginner,

I'll have a look at the ruspiro-gpio/uart bits to see if they can help me understand what's going on. I'm actually running most of this in qemu at the moment with the occasional check on the hardware to save time iterating through things until I get to a stage where I can actually get something so basic running.

What's confusing me is that this looks like some kind of issue with my code on it's own rather than the hardware, this is a debugging session against QEMU. It looks like the &str never even makes it to the send_string function. When I break in the boot_entry function it appears to be there, but if I try to iterate in that function, it still appears as 28 x '\0000'.

(gdb) break send_string
Breakpoint 1 at 0x3200754: file src/raspi/peripherals/, line 125.
(gdb) cont
Thread 1 hit Breakpoint 1, rust_mach::raspi::peripherals::miniuart::MiniUART::send_string (self=0x3fffa8, s=...) at src/raspi/peripherals/
warning: Source file is more recent than executable.
(gdb) p s
$1 = '\000' <repeats 28 times>`

I'm not sure how this has come about! :frowning:

It seems your string is cleared somehow then. Maybe put a watchpoint on the address of your string you're sending in GDB and see where it is modified. This might help you catch the offending part of the code.


well I checked again your code base and especially looked at your linker script and I guess, this could be the root cause of the issue.

The raspberry Pi in bare metal mode will load the kernel to a specific address - either to 0x8000 in aarch32 mode or 0x80000 in aarch64 mode. As addressing to static objects and the like within your binary are based sometimes on relative and sometimes on fixed addresses it is crucial to tell the linker script where the actual code will be loaded to. This way all the addresses work fine while running the kernel either in QEMU or on the real hardware.

To check the compiled result you could do:
aarch64-none-elf-objdump ./target/<your-target>/release/kernel -D > kernel.asm
you might want to replace aarch64-none-elf-* with the toolchain of your choice aarch64-linux-unknown-elf-* if I'm not mistaken.

An example from one of my test kernels:

./target/aarch64-ruspiro/release/kernel:     file format elf64-littleaarch64

Disassembly of section .text:

0000000000080000 <__boot>:
   80000:	d53800a0 	mrs	x0, mpidr_el1
   80004:	92400400 	and	x0, x0, #0x3
   80008:	58000701 	ldr	x1, 800e8 <.SwitchReturn+0x28>
   8000c:	58000722 	ldr	x2, 800f0 <.SwitchReturn+0x30>

Doing so the very first entry in the assembly should start at address 0x80000 if you build for a64. If this is not the case (which I'd guess) you need to adjust your linker script to:


	. = 0x80000;

Hope this helps.

All of this is helping me loads, though I'm not sure I'm at a solution yet! The kernel is not loaded directly by the Pi, I'm going via grub (for multiboot2).

When I inspect the binary, with objdump, the contents of .rodata are:

 3201280 7372632f 6d756c74 69626f6f 74322e72  src/multiboot2.r
 3201290 73000000 00000000 80122003 00000000  s......... .....
 32012a0 11000000 00000000 2a000000 27000000  ........*...'...
 32012b0 61747465 6d707420 746f2061 64642077  attempt to add w
 32012c0 69746820 6f766572 666c6f77 00000000  ith overflow....
 32012d0 80122003 00000000 11000000 00000000  .. .............
 32012e0 30000000 23000000 80122003 00000000  0...#..... .....
 32012f0 11000000 00000000 33000000 21000000  ........3...!...
 3201300 52757374 204d6163 68204f53 2c20696e  Rust Mach OS, in
 3201310 69746961 6c697369 6e670d0a 7372632f  itialising..src/
 3201320 6d756c74 69626f6f 74322f74 61672e72  multiboot2/tag.r
 3201330 73000000 00000000 1c132003 00000000  s......... .....
 3201340 15000000 00000000 12000000 25000000  ............%...
 3201350 61747465 6d707420 746f2061 64642077  attempt to add w
 3201360 69746820 6f766572 666c6f77 00000000  ith overflow....

Which seems pretty good, but when after grub loads the ELF file but before booting, if I inspect any of that region in gdb I get null, so I think something is clobbering .rodata (HOW!?).

Furthermore, when I look at the symbol table, there is no symbol defined for the string (at 0x3201300), which seems strange to me.

target/aarch64-unknown-none/debug/rust-mach:     file format elf64-littleaarch64

00000000032012b0 l     O .rodata        000000000000001c str.0
0000000003201350 l     O .rodata        000000000000001c str.0

Surely there should be a symbol defined for it?

I think my next steps are:

  1. Reset the offset to the default of 0x80000
  2. Is to compare the ELF file for the C version of the kernel with the rust ELF file in a little more depth, to try and see if some linker problem is causing this
  3. See if I can use gdb to dig into grub's loading process a little more
  4. Maybe try linking with GNU ld instead of rust-lld.


well I'd try to get rid of much as possible additional steps that might cause or at least interfere with your issue at hand to a minimum. Try to build a simple "standard" a64 kernel as kernel8.img and load this into QEMU to see if this is running. If this fails you could dig into the single problem at hand. If this runs fine but fails with any other tool in place like grub/multiboot it is quite likely with those tools.

You could run your single kernel in QEMU with this command:

$> qemu-system-aarch64 -M raspi3 -kernel ./target/kernel8.img -nographic -serial null -serial mon:stdio -D qemu.log

This would run QEMU "headless" and creates a qemu.log file containing possible issues if something goes wrong...

I would not expect the specific static string available within .rodata to appear in the symbol table. Access to this data is pretty much calculated based on a known "base" address and the offset. If your whole kernel is relocated in memory before execution this address calculation might fail and point to nothing or garbage or whatever ... :upside_down_face:

Agreed, I think that’s a good idea, I’m relying too much on things that worked for me previously.

I just need to find time to do this now!