Bei ɒiʜꟼOS hatte ich dieser Tage das Phänomen, dass Code, der in der Debugging-Version tadellos lief, in der Release-Version abstürzte. Der Unterschied zwischen beiden Versionen ist – da ich die Debugsymbole nicht ins Binärfile übernommen hatte1– lediglich die Optimierung.

Nach längerer Fehlersuche stellte sich folgende Funktion als Verursacherin heraus:

#[inline(never)]
fn init_stacks() {
    let adr = determine_irq_stack();
    Cpu::set_mode(ProcessorMode::Irq);
    Cpu::set_stack(adr);
    Cpu::set_mode(ProcessorMode::Fiq);
    Cpu::set_stack(adr);
    Cpu::set_mode(ProcessorMode::Abort);
    Cpu::set_stack(adr);
    Cpu::set_mode(ProcessorMode::Undef);
    Cpu::set_stack(adr);
    // ...und zurück in den Svc-Mode
    Cpu::set_mode(ProcessorMode::Svc);
}

Das war etwas verwunderlich, da sowohl Cpu::set_mode() als auch Cpu::set_stack() jeweils nur einen Assembler-Befehl erzeugen sollten:

    /// Setzt Prozessormodus
    #[inline(always)]
    pub fn set_mode(mode: ProcessorMode) {
        unsafe{
            match mode {
                ProcessorMode::User =>   asm!("cps 0x10"),
                ProcessorMode::Fiq =>    asm!("cps 0x11"),
                ProcessorMode::Irq =>    asm!("cps 0x12"),
                ProcessorMode::Svc =>    asm!("cps 0x13"),
                ProcessorMode::Abort =>  asm!("cps 0x17"),
                ProcessorMode::Undef =>  asm!("cps 0x1B"),
                ProcessorMode::System => asm!("cps 0x1F"),
            };
        }
    }

    /// Setzt das Stack-Register 
    #[inline(always)]
    pub fn set_stack(adr: Address) {
        unsafe{
            asm!("mov sp, $0"::"r"(adr)::"volatile");
        }
    }

Der Blick auf den generierten Assembler-Code (via objdump) zeigt das Problem. Während die Debug-Version so aussah …

 000006b4 <_ZN13aihpos_kernel11init_stacks17hbe91fb6176eaf21dE>:
     6b4:   e92d4800    push    {fp, lr}
     6b8:   e1a0b00d    mov fp, sp
     6bc:   ebffffba    bl  5ac <_ZN13aihpos_kernel19determine_irq_stack17h107193fbe9f3e16eE>
     6c0:   f1020012    cps #18
     6c4:   e1a0d000    mov sp, r0
     6c8:   f1020011    cps #17
     6cc:   e1a0d000    mov sp, r0
     6d0:   f1020017    cps #23
     6d4:   e1a0d000    mov sp, r0
     6d8:   f102001b    cps #27
     6dc:   e1a0d000    mov sp, r0
     6e0:   f1020013    cps #19
     6e4:   e8bd8800    pop {fp, pc}

… lautete die optimierte Version so;

000010bc <_ZN13aihpos_kernel11init_stacks17h0c7ec04f5f8147a7E>:
    10bc:   e92d4800    push    {fp, lr}
    10c0:   ebfffe4d    bl  9fc <_ZN13aihpos_kernel19determine_irq_stack17hc16efbf461b228f8E>
    10c4:   e1a0d000    mov sp, r0
    10c8:   f1020012    cps #18
    10cc:   f1020011    cps #17
    10d0:   f1020017    cps #23
    10d4:   f102001b    cps #27
    10d8:   f1020013    cps #19
    10dc:   e1a0d000    mov sp, r0
    10e0:   e1a0d000    mov sp, r0
    10e4:   e1a0d000    mov sp, r0
    10e8:   e8bd8800    pop {fp, pc}

Der Optimierer hatte also die Befehle umgeordnet. Warum er das für besser hielt, ist mir nicht ganz klar. Dass er es allerdings glaubt zu dürfen, liegt daran, dass cps für ihn kein Lese- oder Schreibebefehl ist, so dass er keine Datenabhängigkeit sah.

Zur Behebung dieses Problems genügt der alte Trick, eine Abhängigkeit zu behaupten. Jetzt sieht der set_mode()-Code so aus:

    #[inline(always)]
    pub fn set_mode(mode: ProcessorMode) {
        unsafe{
            match mode {
                ProcessorMode::User =>   asm!("cps 0x10":::"memory":),
                ProcessorMode::Fiq =>    asm!("cps 0x11":::"memory":),
                ProcessorMode::Irq =>    asm!("cps 0x12":::"memory":),
                ProcessorMode::Svc =>    asm!("cps 0x13":::"memory":),
                ProcessorMode::Abort =>  asm!("cps 0x17":::"memory":),
                ProcessorMode::Undef =>  asm!("cps 0x1B":::"memory":),
                ProcessorMode::System => asm!("cps 0x1F":::"memory":),
            };
        }
    }

Der vierte Parameter im asm!()-Macro sagt normalerweise, welche Register „verschmutzt“ werden, so dass der Compiler sich nicht darauf verlassen darf, dass deren Inhalte gleich bleiben. Man kann dort aber auch „memory“ hinschreiben, was behauptet, dass der Assember-Code Nebeneffekte hat. Damit darf der Compiler die Reihenfolge nicht mehr ändern, und ɒiʜꟼOS funktioniert auch mit --opt-level=3.


  1. Die Symbole nützten mir auf dem Raspberry Pi nichts, da das Debugging über den Entwicklungsrechner läuft. 


Kommentare