bare-metal: Panics can bloat binary. No handling at all possible?

Hi,

when building a bare-metal binary, in certain use-cases, it can happen that the compiler pulls in code and data that bloats the binary but is in the end unwanted and ineffective. I provided an example here: https://github.com/andre-richter/panic-test

This is a bare-metal binary with panic = "abort" strategy.

#![no_main]
#![no_std]
#![feature(core_intrinsics)]

use core::intrinsics;
use core::panic::PanicInfo;

#[panic_handler]
#[inline(never)]
fn panic(_info: &PanicInfo) -> ! {
    unsafe { intrinsics::abort() }
}

#[no_mangle]
pub unsafe extern "C" fn _start() -> ! {
    const ARBITRARY_ADDR: *mut u64 = 0x1337 as *mut u64;

    let mut x = core::ptr::read_volatile(ARBITRARY_ADDR);

    x = 0x100 / x; // The division here pulls in core::panic* functions that
                   // have unneeded overhead given the panic handler above.
                   //
                   // Also adds debug strings to .rodata that can/will not be
                   // used (struct PanicInfo).

    // get rid of compiler warnings
    core::ptr::write_volatile(ARBITRARY_ADDR, x);

    loop {}
}

Looking at the generated assembly, it seems the compiler is inserting a software-test to catch a division by zero, and if detecting one, pulls in our custom panic handler (which is inlined after making some preparatory calls, I guess). Here is a snippet, in the repository linked above you'll find the whole objdump containing more core::panic* code.

  201010:	48 8b 0c 25 37 13 00 00 	movq	0x1337, %rcx
  201018:	48 85 c9 	testq	%rcx, %rcx
  20101b:	74 15 	je	0x15 <_start+0x22>
  20101d:	b8 00 01 00 00 	movl	$0x100, %eax
  201022:	31 d2 	xorl	%edx, %edx
  201024:	48 f7 f1 	divq	%rcx
  201027:	48 89 04 25 37 13 00 00 	movq	%rax, 0x1337
  20102f:	90 	nop
  201030:	eb fe 	jmp	-0x2 <_start+0x20>
  201032:	50 	pushq	%rax
  201033:	48 8d 3d c6 0f 00 00 	leaq	0xfc6(%rip), %rdi
  20103a:	e8 21 00 00 00 	callq	0x21 <core::panicking::panic::h505722727939be58>

Also, the .rodata section of the binary is filled with error handling strings (for struct PanicInfo?) that won't be used at all:

Hex dump of section '.rodata':
  0x00200190 7372632f 6d61696e 2e727300 00000000 src/main.rs.....
  0x002001a0 61747465 6d707420 746f2064 69766964 attempt to divid
  0x002001b0 65206279 207a6572 6f000000 00000000 e by zero.......

My point is that when writing a bare-metal binary, e.g. an OS Kernel, in that specific case, I don't want the software check for divison by zero, but rather let the CPU invoke it's hardware exception and handle it from there on.
Such a test might happen at an early time where I don't even have a vehicle for printing out information from PanicInfo ready yet.

Is there a way to turn these kinds of software checks of?

BR,
Andre

CC @therealprof @japaric @phil-opp

I don't think there's a way to disable the panicking behavior of the / operator, but integer types do have a checked_div function that returns an Option instead of panicking. And there's also intrinsics::unchecked_div if you want the nuclear option.

Thanks for the pointer, I will try to leverage it.

Do you think this problem could be more generic, aka can it happen in more places in early boot binaries where it's not yet feasible to process the PanicInfo, or is the division operator a rare occurrence and manually coding around it is acceptable?

I'm trying to get an idea of the bloat that could happen because of this and if it's acceptable, or if it would be nice to have a panic mechanism that aborts more early and doesn't produce rodata strings.
I understand that this would probably need sophisticated changes to the compiler.

If you want to optimize for smallest size, don't forget to enable the following options in Cargo.toml, which may help a little:

[profile.release]
opt-level = 'z'  # Optimize for size.
lto = true       # Enable Link Time Optimization
2 Likes

Alas, in a language with an optimizing compiler, "unchecked" is not the same as "bare metal." The documentation of this function merely says that behavior is undefined in edge cases, which is indistinguishable from a function like the following:

#[inline(always)]
unsafe fn unsafe_div(a: i64, b: i64) -> i64 {
    if b == 0 || (a, b) == (i64::min_value(), -1) {
        // potentially enable other optimizations in the caller's body
        unsafe { intrinsics::unreachable() }
    } else {
        a / b
    }
}
1 Like

Yeah, ideally I want to emit the "one-line" assembly "div" instruction of the respective target, and take the HW exception route if something goes wrong.

Any comments on my other question: Anyone sees other places where this software-path of panicking would be emitted by the compiler where I would not want it?

From some simple testing on the playground, --release seems to disable range checking on as casts and overflow checks on integer addition/subtraction. I have forgotten/can't seem to locate information currently on whether these are specified to wrap or if they are "implementation defined." (whatever the Rust equivalent of that term may be.) You can use Wrapping<u32> to be sure. I don't think there's any way to get the equivalent of "do a bare metal add-with-carry, and let me check the carry flag on my own."

Unfortunately, lots of methods on primitive types like [T] and str panic in a variety of cases. [T] obviously panics on out-of-bounds access, and str additionally cares about UTF8 boundaries. Many methods like <[T]>::swap have no non-panicking equivalent.

I think it will be tricky to completely avoid everything that uses the panic framework...

Maybe something like inline assembly?
https://doc.rust-lang.org/unstable-book/language-features/asm.html