Bare metal panic generates undefined instructions

Hello everyone,

I am currently working on a baremetal project with rust and almost everything is working perfect. However panicking breaks the entire kernel.

Specifically, I am developing a kernel for the Raspberry Pi 4B, but I will spare you the unnecessary details. What is essential to know is that I have successfully implemented control over the GPIO pins and have also incorporated UART support. All of this is working very well. The only problem is that if I call panic! none of my code is ever executed. If I power an led and wait with 5000000 nops and panic afterwards the led code is never even executed. Without the panic everything works fine.

To investigate this issue further, I have examined the assembly code of a minimal example that activates an LED, introduces a delay using NOP instructions, and then triggers a panic. Within the panic handler, I attempt to enable another LED. I confirmed via test and assembly code that there's nothing wrong with my gpio module. Its the panic causing the issue.

Thats the rust source code for the example:

#![no_std]
#![no_main]

mod gpio;
use gpio::*;

use core::{arch::asm, panic::PanicInfo};

#[link_section = ".text._start"]
#[no_mangle]
pub extern "C" fn _start() -> ! {
    GPIO::PIN12.set_function(GPIOFunction::Output);
    GPIO::PIN12.set_output_state(OutputState::High);

    for _ in 0..5000000 {
        unsafe {
            asm!("nop");
        }
    }

    panic!();
}

#[panic_handler]
pub fn panic(_info: &PanicInfo) -> ! {
    GPIO::PIN21.set_function(GPIOFunction::Output);
    GPIO::PIN21.set_output_state(OutputState::High);
    loop {}
}

Here`s the disassembled kernel.
As you can see it starts with some undefined instructions. I think the raspberry pi just stops loading the kernel after it encountered these lines. If I remove them and compile the disassembled assembly at least one of my led is activated. I need to remove all lines of code above my "set function for pin 12" not just the undefined ones for this to work though. From my understanding of the assembly there are many lines of instructions that are never executed. Including my custom panic handler. I annotated everything with comments.

./build/kernel8.img:     file format binary


Disassembly of section .data:

0000000000000000 <.data>:

// Stuff added by panic before the actual code
// This contains undefined instructions.
// I think thats the reason why my kernel is not loaded
   0:	6c707865 	ldnp	d5, d30, [x3, #-256]
   4:	74696369 	.inst	0x74696369 ; undefined
   8:	6e617020 	uabdl2	v0.4s, v1.8h, v1.8h
   c:	72736369 	.inst	0x72736369 ; undefined
  10:	616d2f63 	.inst	0x616d2f63 ; undefined
  14:	722e6e69 	ands	w9, w19, #0xfffc3fff
  18:	00000073 	udf	#115
  1c:	00000000 	udf	#0
  20:	0020000e 	.inst	0x0020000e ; NYI
  24:	00000000 	udf	#0
  28:	0000000b 	udf	#11
  2c:	00000000 	udf	#0
  30:	00000019 	udf	#25
  34:	00000005 	udf	#5
  38:	002000d4 	.inst	0x002000d4 ; NYI
	...
  48:	00000001 	udf	#1
  4c:	00000000 	udf	#0
  50:	002000d8 	.inst	0x002000d8 ; NYI
  54:	00000000 	udf	#0


  // Set function pin 12
  58:	52800089 	mov	w9, #0x4                   	// #4
  5c:	72bfc409 	movk	w9, #0xfe20, lsl #16
  60:	b9400128 	ldr	w8, [x9]
  64:	12177108 	and	w8, w8, #0xfffffe3f
  68:	321a0108 	orr	w8, w8, #0x40
  6c:	b9000128 	str	w8, [x9]

  // Set output pin 12 high
  70:	b9401928 	ldr	w8, [x9, #24]
  74:	3214010a 	orr	w10, w8, #0x1000
  80:	b900192a 	str	w10, [x9, #24]

  // Changed line order here a bit. But it has no effect other than better readability

  // Delay
  78:	52896808 	mov	w8, #0x4b40                	// #19264
  7c:	72a00988 	movk	w8, #0x4c, lsl #16
  84:	d503201f 	nop
  88:	71000508 	subs	w8, w8, #0x1
  8c:	54ffffc1 	b.ne	0x84  // b.any

  // Panic code
  90:	d503201f 	nop
  94:	10fffb60 	adr	x0, 0x0
  98:	d503201f 	nop
  9c:	10fffc22 	adr	x2, 0x20
  a0:	528001c1 	mov	w1, #0xe                   	// #14
  a4:	9400001e 	bl	0x11c
  a8:	d4200020 	brk	#0x1

  // My code below here is never executed because of the branch in line 0xa4 to 0x11c that never returns

  // Set function pin 21
  ac:	52800108 	mov	w8, #0x8                   	// #8
  b0:	72bfc408 	movk	w8, #0xfe20, lsl #16
  b4:	b9400109 	ldr	w9, [x8]
  b8:	121a7129 	and	w9, w9, #0xffffffc7
  bc:	321d0129 	orr	w9, w9, #0x8
  c0:	b9000109 	str	w9, [x8]

  // Set output pin 21 high
  c4:	b9401509 	ldr	w9, [x8, #20]
  c8:	320b0129 	orr	w9, w9, #0x200000
  cc:	b9001509 	str	w9, [x8, #20]

  // infinite loop
  d0:	14000000 	b	0xd0

  // this panic code is never executed
  d4:	d65f03c0 	ret
  d8:	d2997800 	mov	x0, #0xcbc0                	// #52160
  dc:	f2b02c00 	movk	x0, #0x8160, lsl #16
  e0:	f2da9920 	movk	x0, #0xd4c9, lsl #32
  e4:	f2eb58e0 	movk	x0, #0x5ac7, lsl #48
  e8:	d65f03c0 	ret
  ec:	d100c3ff 	sub	sp, sp, #0x30
  f0:	d503201f 	nop
  f4:	10fffa28 	adr	x8, 0x38
  f8:	d503201f 	nop
  fc:	10fff9e9 	adr	x9, 0x38
 100:	a90187e0 	stp	x0, x1, [sp, #24]
 104:	910023e0 	add	x0, sp, #0x8
 108:	a900a7e8 	stp	x8, x9, [sp, #8]
 10c:	52800028 	mov	w8, #0x1                   	// #1
 110:	3900a3e8 	strb	w8, [sp, #40]
 114:	97ffffe6 	bl	0xac
 118:	d4200020 	brk	#0x1

 // The panic code branches to this location
 11c:	d10103ff 	sub	sp, sp, #0x40
 120:	9100c3e8 	add	x8, sp, #0x30
 124:	52800029 	mov	w9, #0x1                   	// #1
 128:	a90307e0 	stp	x0, x1, [sp, #48]
 12c:	910003e0 	mov	x0, sp
 130:	aa0203e1 	mov	x1, x2
 134:	f90003ff 	str	xzr, [sp]
 138:	a90127e8 	stp	x8, x9, [sp, #16]
 13c:	d503201f 	nop
 140:	10fff7c8 	adr	x8, 0x38
 144:	a9027fe8 	stp	x8, xzr, [sp, #32]
 // I have no idea where this branches to?
 148:	97ffffe9 	bl	0xec
 14c:	d4200020 	brk	#0x1

I need to somehow tell the rust compiler to generate the correct assembly code. But I have no idea where to even start. Here are the relevant steps I take to build my kernel:

I tried to disable unwinding by adding these lines to my cargo.toml. But I doubt its working because adding these lines wont change the filesize of the kernel. This could be the root of the problem. Rust trying to unwind but it cannot do so in a bare metal environment. But I have no idea how to get this to work

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

My config file specifies a custom linker script and the target tripple

[build]
target = "aarch64-unknown-none"
rustflags = [
  "-C", "link-arg=-Tlinker.ld",
]

My linker script is very simple:

ENTRY(_start)

SECTIONS
{
    . = 0x200000;
    .text :
    {
        KEEP(*(.text._start))
    }
}

I then use these commands to create the final binary. These could also be the reason as I am using "aarch64-linux-gnu-" because there is no "aarch64-unknown-none-" But from what I have read this should be fine.

cargo build --release
	aarch64-linux-gnu-objcopy target/aarch64-unknown-none/release/pi4os -O binary ./build/kernel8.img

I would be very grateful for any help as I am at the end of my rust knowledge.

This is some working assembly code I hacked together from the disassembled rust program. It will turn the led connected with pin12 on:

.global _start
.section .text

_start:
// panic code containing undefined instructions that most likely cause the binary to break
//    0:	6c707865 	ldnp	d5, d30, [x3, #-256]
//    4:	74696369 	.inst	0x74696369 ; undefined
//    8:	6e617020 	uabdl2	v0.4s, v1.8h, v1.8h
//    c:	72736369 	.inst	0x72736369 ; undefined
//   10:	616d2f63 	.inst	0x616d2f63 ; undefined
//   14:	722e6e69 	ands	w9, w19, #0xfffc3fff
//   18:	00000073 	udf	#115
//   1c:	00000000 	udf	#0
//   20:	0020000e 	.inst	0x0020000e ; NYI
//   24:	00000000 	udf	#0
//   28:	0000000b 	udf	#11
//   2c:	00000000 	udf	#0
//   30:	00000019 	udf	#25
//   34:	00000005 	udf	#5
//   38:	002000d4 	.inst	0x002000d4 ; NYI
// 	...
//   48:	00000001 	udf	#1
//   4c:	00000000 	udf	#0
//   50:	002000d8 	.inst	0x002000d8 ; NYI
//   54:	00000000 	udf	#0

  // Set function pin 12
  mov w9, #0x4                   	// #4
  movk w9, #0xfe20, lsl #16
  ldr w8, [x9]
  and w8, w8, #0xfffffe3f
  orr w8, w8, #0x40
  str w8, [x9]

  // Set output pin 12 high
  ldr w8, [x9, #24]
  orr w10, w8, #0x1000
  str w10, [x9, #24]

  // Changed line order here

  // Delay
  mov w8, #0x4b40                	// #19264
  movk w8, #0x4c, lsl #16
loop1:
  nop
  subs w8, w8, #0x1
  b.ne loop1  // b.any

    // Panic code
    nop
    adr	x0, 0x0
    nop
    adr	x2, 0x20
    mov	w1, #0xe                   	// #14
    bl	fncall
    brk	#0x1

  // Never returns from fncall so this will never be executed

  // Set function pin 21
  mov w8, #0x8                   	// #8
  movk w8, #0xfe20, lsl #16
  ldr w9, [x8]
  and w9, w9, #0xffffffc7
  orr w9, w9, #0x8
  str w9, [x8]

  // Set output pin 21 high
  ldr w9, [x8, #20]
  orr w9, w9, #0x200000
  str w9, [x8, #20]

  // infinite loop
loop2:
  b loop2

  // panic code that will never be executed
    ret
    mov	x0, #0xcbc0                	// #52160
    movk	x0, #0x8160, lsl #16
    movk	x0, #0xd4c9, lsl #32
    movk	x0, #0x5ac7, lsl #48
    ret
    sub	sp, sp, #0x30
    nop
    adr	x8, 0x38
    nop
    adr	x9, 0x38
   stp	x0, x1, [sp, #24]
   add	x0, sp, #0x8
   stp	x8, x9, [sp, #8]
   mov	w8, #0x1                   	// #1
   strb	w8, [sp, #40]
   // I have no idea where this branches to
   bl	0xac
   brk	#0x1

 fncall:
    sub	sp, sp, #0x40
    add	x8, sp, #0x30
    mov	w9, #0x1                   	// #1
    stp	x0, x1, [sp, #48]
    mov	x0, sp
    mov	x1, x2
    str	xzr, [sp]
    stp	x8, x9, [sp, #16]
    nop
    adr	x8, 0x38
    stp	x8, xzr, [sp, #32]
    bl	0xec
    brk	#0x1

Why is your code in the .data section? It seems somewhere along the way your code has merged with data, as those "undefined instructions" you mention are really ascii:

This data 6c707865 74696369 6e617020 72736369 616d2f63 722e6e69 00000073 spells "explicit panic" and "src/main.rs" in little-endian 4-byte words.

8 Likes

Thank you. That must happen due to the objcopy command.
aarch64-linux-gnu-objcopy target/aarch64-unknown-none/release/pi4os -O binary ./build/kernel8.img

Or the disassembly is faulty.
aarch64-linux-gnu-objdump -D -b binary -m aarch64 ./build/kernel8.img
I was already wondering why it said starting at 0000000000000000 instead of 0x200000. But I thought it must be the fault of the disassembler as the code executed fine.

Any idea how I can fix this?

That's an excerpt of:
aarch64-linux-gnu-objdump -D ./target/aarch64-unknown-none/release/pi4os
The file contains lots of unwanted unwind stuff. The file has 225303 lines


./target/aarch64-unknown-none/release/pi4os:     file format elf64-littleaarch64


Disassembly of section .rodata..Lanon.199b25154eef2430d3d8813648c6db47.1:

0000000000200000 <.rodata..Lanon.199b25154eef2430d3d8813648c6db47.1>:
  200000:	6c707865 	ldnp	d5, d30, [x3, #-256]
  200004:	74696369 	.inst	0x74696369 ; undefined
  200008:	6e617020 	uabdl2	v0.4s, v1.8h, v1.8h
  20000c:	Address 0x000000000020000c is out of bounds.


Disassembly of section .rodata..Lanon.199b25154eef2430d3d8813648c6db47.2:

000000000020000e <.rodata..Lanon.199b25154eef2430d3d8813648c6db47.2>:
  20000e:	2f637273 	fcmla	v19.4h, v19.4h, v3.h[1], #270
  200012:	6e69616d 	rsubhn2	v13.8h, v11.4s, v9.4s
  200016:	Address 0x0000000000200016 is out of bounds.


Disassembly of section .rodata..Lanon.199b25154eef2430d3d8813648c6db47.3:

0000000000200020 <.rodata..Lanon.199b25154eef2430d3d8813648c6db47.3>:
  200020:	0020000e 	.inst	0x0020000e ; NYI
  200024:	00000000 	udf	#0
  200028:	0000000b 	udf	#11
  20002c:	00000000 	udf	#0
  200030:	00000019 	udf	#25
  200034:	00000005 	udf	#5

Disassembly of section .rodata..Lanon.d2abf3bee5f45b130f5cdc7faf743548.267:

0000000000200038 <.rodata..Lanon.d2abf3bee5f45b130f5cdc7faf743548.267>:
  200038:	002000d4 	.inst	0x002000d4 ; NYI
	...
  200048:	00000001 	udf	#1
  20004c:	00000000 	udf	#0
  200050:	002000d8 	.inst	0x002000d8 ; NYI
  200054:	00000000 	udf	#0


Disassembly of section .text:

0000000000200058 <_start>:
  200058:	52800089 	mov	w9, #0x4                   	// #4
  20005c:	72bfc409 	movk	w9, #0xfe20, lsl #16
  200060:	b9400128 	ldr	w8, [x9]
  200064:	12177108 	and	w8, w8, #0xfffffe3f
  200068:	321a0108 	orr	w8, w8, #0x40
  20006c:	b9000128 	str	w8, [x9]
  200070:	b9401928 	ldr	w8, [x9, #24]
  200074:	3214010a 	orr	w10, w8, #0x1000
  200078:	52896808 	mov	w8, #0x4b40       

Disassembly of section .text.rust_begin_unwind:

00000000002000ac <rust_begin_unwind>:
  2000ac:	52800108 	mov	w8, #0x8                   	// #8
  2000b0:	72bfc408 	movk	w8, #0xfe20, lsl #16
  2000b4:	b9400109 	ldr	w9, [x8]
  2000b8:	121a7129 	and	w9, w9, #0xffffffc7
  2000bc:	321d0129 	orr	w9, w9, #0x8
  2000c0:	b9000109 	str	w9, [x8]
  2000c4:	b9401509 	ldr	w9, [x8, #20]
  2000c8:	320b0129 	orr	w9, w9, #0x200000
  2000cc:	b9001509 	str	w9, [x8, #20]
  2000d0:	14000000 	b	2000d0 <rust_begin_unwind+0x24>

Disassembly of section .text._ZN4core3ptr102drop_in_place$LT$$RF$core..iter..adapters..copied..Copied$LT$core..slice..iter..Iter$LT$u8$GT$$GT$$GT$17h6d71928ba6642118E:

00000000002000d4 <_ZN4core3ptr102drop_in_place$LT$$RF$core..iter..adapters..copied..Copied$LT$core..slice..iter..Iter$LT$u8$GT$$GT$$GT$17h6d71928ba6642118E>:
  2000d4:	d65f03c0 	ret

Disassembly of section .text._ZN36_$LT$T$u20$as$u20$core..any..Any$GT$7type_id17hd489b30ce2060f05E:

00000000002000d8 <_ZN36_$LT$T$u20$as$u20$core..any..Any$GT$7type_id17hd489b30ce2060f05E>:
  2000d8:	d2997800 	mov	x0, #0xcbc0                	// #52160
  2000dc:	f2b02c00 	movk	x0, #0x8160, lsl #16
  2000e0:	f2da9920 	movk	x0, #0xd4c9, lsl #32
  2000e4:	f2eb58e0 	movk	x0, #0x5ac7, lsl #48
  2000e8:	d65f03c0 	ret

Disassembly of section .text.unlikely._ZN4core9panicking9panic_fmt17he0582e49bf683becE:

Check out the min-sized-rust guide if you're looking to reduce your binary size. In particular, compiling on nightly with -Z build-std=std,panic_abort -Z build-std features=panic_immediate_abort should help remove more unwind code/data which is baked into the libcore/libstd binaries you end up linking.

You need to tell a linker to put your _start function in a section before any other section like .data. I'm not great with linkers, but maybe something like this?

SECTIONS {
  . = 0x200000;
  .text._start:
  {
    KEEP(*(.text._start))
  }
  .text : {
    *(.text)
  }
  .data : {
    *(.data)
  }
  /DISCARD/ : {
    *(*)
  }
}

And you're right, objdump just shows everything as .data because objcopy -O binary produces a memory dump with no section information, which is fine, but objdump won't understand that. The underlying issue, again, is that your _start function needs to be located at the beginning of the memory dump.

1 Like

Thank you. I updated my linker script to move the rodata sections behind the _start

ENTRY(_start)

SECTIONS {
    . = 0x200000;

    .text :
    {
        *(.text._start)
        *(.text*)
    }

    .rodata* : {
        *(.rodata*)
    }
}

Now the first led lights up. I guess the second led not lighting up is because of unwinding. Thank you alot. I will reply once I resolve the last issue

@jessa0 I am very confused now.
My deassembled code looks fine. If I follow all of the bl branches I end up at the code that enables my led. I created the program below from the deassembled code. The program breaks if I include the commented lines. Seems like there is a problem with writing to the stack. Do you have an idea what could cause this. Do I need to initialize it somehow?

.global _start
.section .text

_start:
	nop
	adr	x0, data1
	nop
	adr	x2, data2
	mov	w1, #0xe
	bl	fncall1
	brk	#0x1

fncall1:
	sub	sp, sp, #0x40
	add	x8, sp, #0x30
	mov	w9, #0x1
	// stp	x0, x1, [sp, #48]
	mov	x0, sp
	mov	x1, x2
	// str	xzr, [sp]
	// stp	x8, x9, [sp, #16]
	nop
	adr	x8, data3
	// stp	x8, xzr, [sp, #32]
	bl	fncall2
	brk	#0x1

fncall2:
	sub	sp, sp, #0x30
	nop
	adr	x8, data3
	nop
	adr	x9, data3
	// stp	x0, x1, [sp, #24]
	add	x0, sp, #0x8
	// stp	x8, x9, [sp, #8]
	mov	w8, #0x1
	// strb	w8, [sp, #40]
	bl	fncall3
	brk	#0x1

  // function pin21 output
fncall3:
	mov	w8, #0x8
	movk	w8, #0xfe20, lsl #16
	ldr	w9, [x8]
	and	w9, w9, #0xffffffc7
	orr	w9, w9, #0x8
	str	w9, [x8]
  // output pin21 high
	ldr	w9, [x8, #20]
	orr	w9, w9, #0x200000
	str	w9, [x8, #20]
  // infinite loop
loop:
	b	loop
	ret

// This is never executed
	mov	x0, #0x1d16
	movk	x0, #0x2ee4, lsl #16
	movk	x0, #0xb27, lsl #32
	movk	x0, #0x53e1, lsl #48
	ret

data1:
  .byte 0x6c, 0x70, 0x78, 0x65

data2:
  .byte 0x00, 0x20, 0x00, 0xce

data3:
  .byte 0x00, 0x20, 0x00, 0x44

In a bit outside my comfort zone here, but what's your expected behavior?

panic=abort generates a deliberately undefined instruction (at least on x86_64-pc targets) to raise a kernel exception and take down the process (not my preferred approach if you have an OS API for that, but whatever).

On #![no_std] projects, the "get it working" approach I've normally seen is to provide a custom panic handler which is just an infinite loop.

Without special consideration, I would expect both of these to halt the process.

1 Like

You do indeed need to set the stack pointer to somewhere you choose and reserve so it won't be later clobbered by memory allocations or whatever you may do with memory, and which won't overflow and overwrite your kernel code. It could be 4k or so after your loaded kernel image in memory. I don't think you need to zero it.

As a side note, you might also wanna make sure you include the .bss section in your kernel image as well, or zero it programmatically early during kernel init.

1 Like

Linker script is also way out of my comfort zone, but maybe you find looking at (one of?) the linker file(s) of the cortex-m project useful:

1 Like

I do expect the process to end. The problem was that it did not even execute at all. The rust compiler added some code on panic to the very top of main that modified the stack pointer. Because I did not set the stack pointer properly up it broke the entire program because the stack pointer accessed negative memory. I fixed this now. Thank you for your time. I will add an answer how it was fixed shortly

@raidwas , @jessa0 much thanks for your help. I was able to resolve the problem and get the panic handler to work properly.

The first step was updating the linker script. (Do not forget to delete the target folder after modifying it)

  • I changed the entrypoint to a new function _spsetup. More on that later
  • I added a .stack section thats 0x2000 bytes long. I also created a variable _stack_start to reference the stack from rust
  • I added the bss section even though my code currently does not generate one
  • I generalized the .rodata section so it also handles data. This was not necessary as there is no data section currently.
  • I aligned all the sections at 4kib as suggested.
  • I ensured that the _spsetup function is called before the _start function
ENTRY(_spsetup)

STACK_SIZE = 0x2000;

SECTIONS {

    . = 0x200000;
    .text :
    {
        *(.text._spsetup)
        *(.text._start)
        *(.text*)
    }

     . = ALIGN(4096);
    .stack (NOLOAD) :
    {
    _stack_start = .;
    . = . + STACK_SIZE;
    }

    . = ALIGN(4096);
    .bss (NOLOAD) :
    {
        *(.bss*)
    }

    . = ALIGN(4096);
    .data :
    {
        *(.data*)
        *(.rodata*)
    }

   
}

After updating the linker script I included some assembly at the top of the main function that should initialize the stack pointer.

unsafe {
        asm!("adr x0, _stack_start");
        asm!("mov sp, x0");
    }

Unfortunately the rust compiler was not always putting my assembly instructions at the very top of _start. Sometimes they got entangled or overwritten by values rust moved into the same registers. Therefore I came up with a special function _spsetup thats always called before _start. This is now my entry point and ensures that the stack pointer is always setup properly. I guess just removing the explicit panic call from main would have worked as well. But its more secure this way

#[link_section = ".text._spsetup"]
#[no_mangle]
pub extern "C" fn _spsetup() {
    unsafe {
        asm!("adr x0, _stack_start");
        asm!("mov sp, x0");
    }
    _start();
}

Thats the final main.rs

#![no_std]
#![no_main]

mod gpio;
use gpio::*;

use core::{arch::asm, panic::PanicInfo};

#[link_section = ".text._spsetup"]
#[no_mangle]
pub extern "C" fn _spsetup() {
    unsafe {
        asm!("adr x0, _stack_start");
        asm!("mov sp, x0");
    }
    _start();
}

#[link_section = ".text._start"]
#[no_mangle]
pub extern "C" fn _start() -> ! {
    GPIO::PIN12.set_function(GPIOFunction::Output);
    GPIO::PIN12.set_output_state(OutputState::High);

    panic!();

    loop {}
}

#[panic_handler]
pub fn panic(_info: &PanicInfo) -> ! {
    GPIO::PIN21.set_function(GPIOFunction::Output);
    GPIO::PIN21.set_output_state(OutputState::High);

    loop {}
}

Many thanks to everyone who took the time to read and help

1 Like

That sounds like it's trying to set up a stack frame (or at least spilling registers), so perhaps any (not inlined) function would have caused the same issue?

In a less hardware environment dependant situation, I would think there's some undefined behavior going on (reaching unreachable branches, for example), and would recommend testing under MIRI, but I'm pretty sure that won't handle asm!

1 Like

I was trying to add support for print macros. The code works until the call to print. My panic handler is not called. I had no time to check the assembly yet but could this stack frame be the reason? I will need to read up on that.

#[link_section = ".text._start"]
#[no_mangle]
pub extern "C" fn _start() -> ! {
    GPIO::PIN12.set_function(GPIOFunction::Output);
    GPIO::PIN12.set_output_state(OutputState::High);
    uart::init(115200);

    uart::println("Started Kernel!");

    print!("test");

    uart::println("Stopped Kernel!");

    loop {}
}
#[macro_export]
macro_rules! println {
    () => (print!("\n"));
    ($($arg:tt)*) => (print!("{}\n", format_args!($($arg)*)));
}

#[macro_export]
macro_rules! print {
    ($($arg:tt)*) => ($crate::util::_print(format_args!($($arg)*)));
}

#[doc(hidden)]
#[inline(never)]
pub fn _print(args: fmt::Arguments) {
    uart::Writer.write_fmt(args).unwrap();
}

Damn these print macros added lots of assembly. I thought they are evaluated at compile time. I think I will not fix that for now. Unless someone has a concrete idea. But I do not have the time to read through all this assembly currently

It sounds like an ABI issue still?

This blog post is using global_asm!() to set up their stack pointer in their entry point, but otherwise it looks pretty similar - my best guess is the C ABI on your entry point is messing something up? I wouldn't expect to see the issues you're describing there is until you try and return, but I'm not familiar with ARMs ABI, and maybe it's confusing the optimizer.

If you're on nightly you could try adding #[naked] to tell Rust not to add the prologue and epilogue, or you simply use global_asm!() like in the blog.

1 Like

That sounds like you need the #[naked] attribute on the function.
Alternatively you could try to find out what the default stack register contents are after a reset, and just put your stack there.

Regarding your assembly: I would recommend putting the two assembly operations into the same asm! call, since the rust compiler
You might generate code around them (freeing registers and stuff).
In this regard I think your asm! calls are technically incorrect because you should tell the compiler which registers you are using (x0 in your case for example).

1 Like

Thank you @simonbuchan , @raidwas

I modified my code to use global assembly. Now I do not need another rust functions and I also separated the rust and assembly code. This works. But calls to format_args!() not only increase kernel filesize by 19000 bytes but stop execution at call. Anything before format_args! runs. This code works: uart::Writer.write_str("test").unwrap(); but this one causes the execution to stop without panicing: uart::Writer.write_fmt(format_args!("test")).unwrap();

global_asm!(include_str!("start.s"));

#[no_mangle]
pub extern "C" fn main() -> ! {
    GPIO::PIN12.set_function(GPIOFunction::Output);
    GPIO::PIN12.set_output_state(OutputState::High);
    uart::init(115200);
    uart::println("Started Kernel!");

    uart::Writer.write_fmt(format_args!("test")).unwrap();

    uart::println("Stopped Kernel!");

    loop {}
}
.global _start
.section ".text._start"

_start:
    adr x0, _stack_start
    mov sp, x0
    bl main
ENTRY(_start)

STACK_SIZE = 0x2000;

SECTIONS {

    . = 0x200000;
    .text :
    {
        *(.text._start)
        *(.text*)
    }

    . = ALIGN(4096);
    .bss (NOLOAD) :
    {
        *(.bss*)
    }

     . = ALIGN(4096);
    .stack (NOLOAD) :
    {
    _stack_start = .;
    . = . + STACK_SIZE;
    }

    . = ALIGN(4096);
    .data :
    {
        *(.data*)
        *(.rodata*)
    }

   
}

This is exciting to follow!

At this point, I would maybe look into setting up interrupt handlers so you can catch CPU faults and print something simple when that happens so you at least know why execution is stopping.

I would also set up memory protection and mark your text and rodata segments as read-only so that you know when UB happens and can safely assume your code doesn't accidentally change from underneath you. You could also set up a larger stack space and put some read-only "guard pages" so that if the stack overflows, it likely will trigger a page fault instead of overwriting something important. (Does anyone know if rustc/LLVM emits stack probes for arm64? If so, you only need one guard page.)

1 Like

Only x86/x86_64, PowerPC, s390x and the UEFI targets currently uses stack probes.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.