Instructions after ret, side effect of hot/cold splitting and exceptions?

97 Views Asked by At

I wanted to check if GCC makes sure extra code generated for running destructors when exceptions are thrown is put in a cold section of the binary, to keep those instructions away from the "happy path" and avoid contributing to instruction cache pressure. So I made the example below (Godbolt link) where GCC can't tell if an exception is going to be thrown. Sure enough I see a separate buzz(int) [clone .cold.0]: label in the assembly. But there's this curious artifact above it:

.L9:
        mov     edi, OFFSET FLAT:.LC0
        xor     eax, eax
        call    printf
        mov     eax, ebx
        add     rsp, 16
        imul    eax, ebx
        add     eax, ebx
        pop     rbx
        ret
        mov     rbx, rax
        jmp     .L3
buzz(int) [clone .cold.0]:
.L3:
        mov     eax, DWORD PTR [rsp+12]
        ...

Notice the ret instruction is follow by a jmp into the cold area. But instructions after ret that aren't a jump target (no label) should never run. I thought maybe it could be for instruction alignment purposes, but following with a jump into the cold section seems like too weird a coincidence. What's going on? Why are these instructions here? Maybe something to do with the protocol for how unwinding works requires it to be there, and the exception unwind table makes it a jump target, but it's just unlabeled? But if that's the case why not jump straight to the cold section?

C++ code:

#include <stdio.h>

extern int global;

struct Foo
{
    Foo(int x)
    : x(x)
    {}

    ~Foo() { 
        if(x % global == 0) {
            printf("Wow."); 
        }
    }
    int x;
};

void bar(Foo& f); // compiler can't see impl, has to assume could throw

// Type your code here, or load an example.
int buzz(int num) {
    {
       Foo foo(3);
       bar(foo);
    }
    return num * num + num;
}
0

There are 0 best solutions below