gcc file.c
, prints Never gonna give you up
#include <stdio.h>
#include <stdint.h>
int main(){
uint64_t a = 4610877184;
uint64_t b = 752229920040515872ull;
uint64_t c = 7311146936728383086ull;
uint64_t d = 8027420536511817038ull;
printf("%s", (char*)&d);
}
But why is this?
The program leverages some behaviors about how C gets compiled on macOS to accomplish this. Firstly, the four 64-bit int variables are holding the actual string. Bytes are bytes, and so by changing our interpretation of those bytes with the cast to a C character pointer, what appear to be meaningless numbers transform into the message.
We can use this program to take a string and output it as 64-bit integers:
#include <iostream>
using namespace std;
int main(){
const char str[] = "Never gonna give you up\n";
for(const uint64_t* i = reinterpret_cast<decltype(i)>(str); i < reinterpret_cast<decltype(i)>(str + sizeof(str)); i++){
cout << *i << "\n";
}
}
However, if you run this program, you'll notice that the output appears like this:
8027420536511817038 7311146936728383086 752229920040515872 4610877184Which is the reverse order that they are listed in the rickroll program. This is because x86_64 and arm64 computers are little-endian, so the order of the bytes in memory starts at the least-significant byte and ends at the most significant byte. This, combined with the stack growing down, means we must list our variables in reverse order, and begin printing from the last one. In addition, on macOS, the variables are placed one-after-another in memory without any padding inserted, which enables us to print all four of them.
However, if you do not have a macOS device, and instead run the program on a Windows or Linux machine, you may instead
get something like Never go??$@
as the output. Why is that?
From what I've been able to gather, the alignment and padding of stack memory on Linux and Windows is different than it is on macOS, such that one can't simply print all four variables at once. To make our program stable across compilers, operating systems, and endians, the program needs a few modifications:
#include <stdio.h>
#include <stdint.h>
int main(){
struct {
uint64_t a, b, c, d;
} s = {
.a = 8027420536511817038ull,
.b = 7311146936728383086ull,
.c = 752229920040515872ull,
.d = 4610877184
};
printf("%s", (char*)&s);
}
In this version, we use a struct
to guarantee alignment and padding placement. Since the four variables are aligned,
the compiler does not insert padding between the items. The struct also solves the endianness issue, so we instead start printing from
the beginning of the struct instead of the end, and sort our variables accordingly.
In my opinion this version is less fun because it is more obvious what is happening, since what we've done is essentially make a C array out of a struct. One can name the struct members with underscores and shuffle the order of the initializers to obfuscate it a little bit in an attempt to restore some of the mystery of the macOS version.