A method from 0
In today's post we will try to understand why the output of this
int main(void) {
int answer = ((Complex *)0)->answer();
cout << "The secret is: " << answer << endl;
}
program is
The answer is: 42
First of all we need to note first that, of course the answer is 42. What were you thinking guys. The second not so obvious question is, why in the flying f#c4 does this even works?
((Complex *)0)->answer()
Lets first decrypt this. Here we are
- Picking zero
- Casting it to a pointer to a complex type
- Invoking a method called answer().
Obvious question. Why does this work? Shouldn't this explode in a huge null pointer exception fireball?
Third question? How did we even get to do something as odd like this?
Lets start by trying to answer the last question. With an empty program
#include "../src/bmath/math.hpp"
#include "../src/bmath/complex.hpp"
#include <iostream>
using namespace BMath;
using namespace std;
int main(void) {
}
This programs does pretty much nothing. However it includes two header files. In particular one which as the names suggest is doing complex numbers arithmetic.
namespace BMath {
class Complex {
private:
Double real;
Double imaginary;
public:
Complex(Double real, Double imaginary);
Complex(const Complex &other);
Complex();
Complex &operator+=(const Complex &rightOp);
Complex operator+(const Complex &rightOp);
Complex operator-(const Complex &rigthOp);
Complex operator*(const Complex &rightOp);
Int answer();
// Overload to enable toString operations
friend std::ostream &operator<<(std::ostream &stream,
BMath::Complex const &c) {
return stream << "(" << c.real << "+" << c.imaginary << "i)";
}
};
By executing
readelf -a bin/Debug/maths | grep Complex
FUNC GLOBAL DEFAULT 16 _ZN5BMath7ComplexpLERKS0_
FUNC GLOBAL DEFAULT 16 _ZN5BMath7ComplexC1ERKS0_
FUNC GLOBAL DEFAULT 16 _ZN5BMath7ComplexC2Ev
FUNC GLOBAL DEFAULT 16 _ZN5BMath7ComplexC1Edd
FUNC GLOBAL DEFAULT 16 _ZN5BMath7ComplexC1Ev
FUNC GLOBAL DEFAULT 16 _ZN5BMath7ComplexplERKS0_
FUNC GLOBAL DEFAULT 16 _ZN5BMath7ComplexC2Edd
FUNC GLOBAL DEFAULT 16 _ZN5BMath7ComplexmiERKS0_
FUNC GLOBAL DEFAULT 16 _ZN5BMath7ComplexC2ERKS0_
FUNC GLOBAL DEFAULT 16 _ZN5BMath7ComplexmlERKS0_
FUNC GLOBAL DEFAULT 16 _ZN5BMath7Complex6answerE
we can verify that the Complex class was compiled and included in the binary. The journey started when I found something odd during machine code analysis. I was looking into the machine code program with radare2 and found something odd on the machine code generated for these three methods
//BMath::Complex::Complex(BMath::Complex const&)
94 0x000012b4 GLOBAL FUNC 53 BMath::Complex::Complex(BMath::Complex const&)
106 0x0000128c GLOBAL FUNC 40 BMath::Complex::Complex()
116 0x00001258 GLOBAL FUNC 52 BMath::Complex::Complex(double, double)
I found the following machine code being generated
// BMath::Complex::Complex(BMath::Complex const&)
49: fcn.000012b8 ();
; var int64_t var_10h @ rbp-0x10
; var int64_t var_8h @ rbp-0x8
0x000012b8 55 push rbp
0x000012b9 4889e5 mov rbp, rsp
0x000012bc 48897df8 mov qword [var_8h], rdi
0x000012c0 488975f0 mov qword [var_10h], rsi
0x000012c4 488b45f0 mov rax, qword [var_10h]
// BMath::Complex::Complex()
36: fcn.00001290 ();
; var int64_t var_8h @ rbp-0x8
0x00001290 55 push rbp
0x00001291 4889e5 mov rbp, rsp
0x00001294 48897df8 mov qword [var_8h], rdi
0x00001298 488b45f8 mov rax, qwrd [var_8h]
//BMath::Complex::Complex(double, double)
48: fcn.0000125c ();
; var int64_t var_18h @ rbp-0x18
; var int64_t var_10h @ rbp-0x10
; var int64_t var_8h @ rbp-0x8
0x0000125c 55 push rbp
0x0000125d 4889e5 mov rbp, rsp
0x00001260 48897df8 mov qword [var_8h], rdi
0x00001264 f20f1145f0 movsd qword [var_10h], xmm0
0x00001269 f20f114de8 movsd qword [var_18h], xmm1
If you pay attention to the function signatures something does not add.
//The pair of functions are the same
BMath::Complex::Complex(BMath::Complex const&)
fcn.000012b8 (var int64_t var_8h, var int64_t var_10h)
BMath::Complex::Complex()
fcn.00001290 (var int64_t var_8h @ rbp-0x8)
BMath::Complex::Complex(double, double)
fcn.0000125c (var int64_t var_8h @ rbp-0x8, var int64_t var_10h @ rbp-0x10,var int64_t var_18h @ rbp-0x18);
The machine code defines functions that take always more one argument than the C++ signatures we define in our program.
Now lets get back again. Lets see how does the machine code looks like when we use these functions. Lets start with the simple case of an empty main function
int main(void) {
}
This generated the following machine code
fcn.000014a0 ();
0x000014a0 55 push rbp //Saves the previous stack frame
0x000014a1 4889e5 mov rbp, rsp // Assigns rsp as the new stack frame by moving to rbp
0x000014a4 b800000000 mov eax, 0 //put 0 on eax register to be returned by the program
0x000014a9 5d pop rbp // loads previous stack frame
0x000014aa c3 ret //returns from the function
Now lets see what gets generated when we allocate a Complex instance on the stack and just return the memory address
int main(void) {
Complex c;
return &c;
}
The machine code looks like
fcn.0000118d ();
; var int64_t var_20h @ rbp-0x20
; var int64_t var_8h @ rbp-0x8
0x0000118d 55 push rbp
0x0000118e 4889e5 mov rbp, rsp
0x00001191 4883ec20 sub rsp, 0x20
0x00001195 64488b042528. mov rax, qword fs:[0x28]
0x0000119e 488945f8 mov qword [var_8h], rax
0x000011a2 31c0 xor eax, eax
0x000011a4 488d45e0 lea rax, qword [var_20h]
0x000011a8 4889c7 mov rdi, rax
0x000011ab e8b4000000 call method BMath::Complex::Complex() ; method.BMath::Complex.Complex
0x000011b0 488d45e0 lea rax, qword [var_20h]
0x000011b4 488b55f8 mov rdx, qword [var_8h]
0x000011b8 644833142528. xor rdx, qword fs:[0x28]
┌─< 0x000011c1 7405 je 0x11c8
│ 0x000011c3 e8b8feffff call sym.imp.__stack_chk_fail
│ ; CODE XREF from fcn.0000118d @ 0x11c1
└─> 0x000011c8 c9 leave
0x000011c9 c3 ret
Ok. It seems that the compiler went full on drugs. Why on the heck does he generates this amount of machine code for just two lines of code?
Lets pick this step by step. First just note that the first three and the last two just form the prologue and epilogue. So we can forget them. Lets jump to
0x00001195 64488b042528. mov rax, qword fs:[0x28]
0x0000119e 488945f8 mov qword [var_8h], rax
...
0x000011b4 488b55f8 mov rdx, qword [var_8h]
0x000011b8 644833142528. xor rdx, qword fs:[0x28]
┌─< 0x000011c1 7405 je 0x11c8
│ 0x000011c3 e8b8feffff call sym.imp.__stack_chk_fail
│ ; CODE XREF from fcn.0000118d @ 0x11c1
└─> 0x000011c8 c9 leave
Here we look into the next two instructions and then jump to a set of instructions in the end. Why? Because this is code generated by the compiler to prevent exploits based on address space layout randomization. This is a mechanism provided by linux kernel. You can check if you got it enabled by checking the value under /proc/sys/kernel/randomize_va_space. The previous code is doing the following
//reads from the fs segment controlled by the kernel a piece of data into the rax register
0x000011b4 488b55f8 mov rdx, qword [var_8h]
0x00001195 64488b042528. mov rax, qword fs:[0x28]
//stores the register data on the memory at [var_8h]
0x0000119e 488945f8 mov qword [var_8h], rax
Then in the end
// Loads previous kernel value stored at [var_8h] into rdx register
0x0000119e 488945f8 mov qword [var_8h], rdx
// Compares previous kernel value with the last one
0x000011b8 644833142528. xor rdx, qword fs:[0x28]
// If they do not match invoke sym.imp.__stack_chk_fail
// otherwise leave the program
┌─< 0x000011c1 7405 je 0x11c8
│ 0x000011c3 e8b8feffff call sym.imp.__stack_chk_fail
│ ; CODE XREF from fcn.0000118d @ 0x11c1
└─> 0x000011c8 c9 leave
Ok but what is this cryptic sym.imp._stackchk_fail. Well this is a function defined in the linux kernel that is responsible to signal anytime if finds corruption on the stack.
Ok we are almost done. Now we just need to understand the following
0x000011a4 488d45e0 lea rax, qword [var_20h]
0x000011a8 4889c7 mov rdi, rax
0x000011ab e8b4000000 call method BMath::Complex::Complex() ; method.BMath::Complex.Complex
0x000011b0 488d45e0 lea rax, qword [var_20h]
Here this is also pretty straightforward. So we are using var_20h as the memory location to store the Complex c value. This is done by assigning the stack memory location var_20h which is actually rbp-0x20 to rdi. rdi will then be used by BMath::Complex::Complex().
Ok but how do we know that var_20h is actually holding &c. Well we know that the return value of the program is stored in rax and if you notice the last time rax appears in the program is on
0x000011b0 488d45e0 lea rax, qword [var_20h]
So what in the hell did we conclude here? We conclude that the machine code generated for the Complex c++ class is of the form.
BMath::Complex::Complex(BMath::Complex const&)
fcn.000012b8 (this, BMath::Complex const&)
BMath::Complex::Complex()
fcn.00001290 (this)
BMath::Complex::Complex(double, double)
fcn.0000125c (this,double,double);
This is actually revealing. The point here is that there is no concept as such a method belonging to a class like in many other high level languages like java. Here the methods are simple functions and the class concept is build with trickery like name mangling and by passing the object reference around based on this approach.
What do we conclude then? Well we conclude first that the methods are functions and they do not depend on the instances they only depend somehow on the type of the class. Also we conclude that
c->hello(argument)
really means
class_type=get type of c
f=find_function(class_type,hello)
f(c,argument)
Is this important? Well not really but will explain why this can work
((Complex *)0)->answer()
//First we force a type by casting 0 into a pointer of type Complex
((Complex *)0)
//Then we invoke then answer method answer()
But why does this works? Well because actually behind the hoods that is equivalent to
answer(0)
and if you look into the definition
Int BMath::Complex::answer() { return 42; }
Nowhere we use the pointer (in this case 0). So this call will work fine.
But what if instead we got this definition
Int BMath::Complex::answer() { return this->real; }
In this case when we run the program we got
bin/Debug/maths
[1] 38696 segmentation fault (core dumped) bin/Debug/maths
Why? Well because now by invoking this->real we are actually trying to access 0x0 memory address which is, obviously, a memory access violation that ends unsurprisingly in a segmentation fault.