Know your assembly (part 5)

The other day I was looking at a crash dump for a friend. A discussion that followed made me realize it might be worth to write a short post explaining why sometimes 2 seemingly almost identical function calls behave very differently. Consider the following code snippet (simplified):

 1struct Foo
 2{
 3    __declspec(noinline) const int& GetX() const { return x; }
 4    virtual const int& GetY() const { return y; }
 5
 6    int x, y;
 7};
 8
 9int Cat(int);
10int Lol(Foo* f)
11{
12    const int& x = f->GetX();
13    const int& y = f->GetY(); // ***
14
15    return Cat(x+y);
16}

Ignore the __declspec(inline) for a moment, it’s mostly to simulate a situation where implementation of GetX (and GetY) is in a different module and not inlined. Our code was crashing in the line marked with stars (13), seemed like f was null, but what maybe is not obvious immediately is why isn’t it crashing in the previous line (12). After all, it’s doing almost exactly the same, calling a method (and presumably accessing) on a null object. Is it really, though? If you’re familiar with underlying model, you know that reference is basically an address, so all GetX is really doing is taking an address of our object (0 in this case) and adding an offset of x. It’s not really accessing the memory (yet). If we didn’t call GetY, but did Cat(x) instead, it would have crashed there, when trying to access x “for real”. Here’s a Compiler Explorer link showing generated assembly. As you can see, GetX is really just: “lea eax, DWORD PTR [ecx+4]” (ecx=this). If GetX returned x by value, it’d have been a different story:

1int const Foo::GetX(void)const  PROC                                ; Foo::GetX, COMDAT
2        mov     eax, DWORD PTR [ecx+4]
3        ret     0

So now, the question is, why does GetY crash immediately? As you have probably guessed, it all boils down to the virtual keyword. It doesn’t crash when trying to access y, it happens before, when trying to load a virtual table for Foo:

1        mov     edx, DWORD PTR [esi]  ; Trying to load a vtable address to EDX: ESI = f = NULL
2        mov     ecx, esi
3        mov     edi, eax
4        call    DWORD PTR [edx]