Hey there! In this post, I will be coding a clone of the lolcat program. Lolcat is basically cat with colors. It copies its standard input to its standard output, but modifies the content so that it’s colorful. Here’s a screenshot of lolcat.
Please note that this article is a bit long. You can go here for screenshots and the final code.
The original lolcat is written in Ruby, but our rewrite will be in x64 assembly. So let’s begin by creating a makefile in our project directory.
all:
nasm -f elf64 -o lolcat.o lolcat.asm
ld -o lolcat lolcat.o
clean:
rm -f lolcat.o lolcat
test:
make all
echo "Testing the lolcat ^_^" | ./lolcat
We will be using nasm as our assembler. The -f elf64
part means that we want a 64-bit ELF binary. After we write our code, we can run make
to compile it or make test
to test it. That said, let’s get to writing the code.
In this article I will write the assembly code and give an approximate translation to C under it
section .text
global _start
_start:
nop
void main() {
}
In assembly, all of our executable code lives in the .text section. We make _start global, because that’s where the assembler will look for the entry point of our program.
The exit function
This program basically does nothing. The nop instruction does not do anything, so this program should exit cleanly right? In reality, if we were to run this program we would be greeted with a Segmentation fault. That is because we need to use the exit syscall. Normally the C standard library does this for us but since we aren’t using it, we need to do it ourselves. So let’s create an exit function that we will call at the end of _start.
exit:
mov rax, 60
mov rdi, 0
syscall
void exit() {
exit_syscall(0); // Exits with return code 0
}
This code calls the 60th system call with the argument 0, which is basically exit(0);
in C. Now our start function should look like this.
_start:
call exit
void main() {
exit();
}
If we ran this now, we would get the expected result of nothing happening.
Putting characters on the screen
But doing nothing isn’t very exciting now, is it? Let’s do something more interesting and print something to the console. First of all, we are going to need a character buffer. This is the equivalent of a char in C. We will use this to read and write characters. This will go to a special section called .data. This is where our strings and static variables will live.
section .data
char_buffer db 0
This is similar to unsigned char char_buffer = 0;
in C. To print a character, we will use the 1st system call, which is write.
put_char:
mov [char_buffer], rdi
mov rax, 1
mov rdi, 1
mov rsi, char_buffer
mov rdx, 1
syscall
ret
void put_char(char character) {
// The write syscall takes a character buffer and a length
// Since our function prints only 1 character, that length is always 1
syscall_write(STDOUT, &character, 1);
}
This function gets the first argument, puts it into char_buffer and calls write(STDOUT, char_buffer, 1);
.
By modifying our _start function to this, we can print Test.
_start:
mov rdi, 'T'
call put_char
mov rdi, 'e'
call put_char
mov rdi, 's'
call put_char
mov rdi, 't'
call put_char
call exit
void main() {
put_char('T');
put_char('e');
put_char('s');
put_char('t');
exit();
}
Reading characters
In order to have a working cat, we need to read characters and write them back. Right now we can only write characters. Let’s create a function to read characters as well. We will use system call 0 for this, which is read.
read_char:
mov rax, 0
mov rdi, 0
mov rsi, char_buffer
mov rdx, 1
syscall
mov rax, [char_buffer]
ret
char read_char() {
char *char_buffer; //*
// Read one character from the standard input and put it into char_buffer.
syscall_read(STDIN, &char_buffer, 1);
return char_buffer;
}
I don’t know if you noticed this, but this function doesn’t do any error handling. We can make it return NULL when there are no more characters to read.
read_char:
mov rax, 0
mov rdi, 0
mov rsi, char_buffer
mov rdx, 1
syscall
cmp eax, 0
jg .success
mov rax, 0
ret
.success:
mov rax, [char_buffer]
ret
char read_char() {
char *char_buffer; //*
int result = syscall_read(STDIN, &char_buffer, 1);
if (result > 0) {
return char_buffer;
}else {
return 0
}
}
We have a cat
With those additions, we should be able to make a regular, non-colored cat now. Let’s change our start function to this.
_start:
call read_char
mov rdi, rax
cmp rdi, 0
je .end
call put_char
jmp _start
.end:
call exit
void main() {
while (true) {
char input_character = read_char();
if (input_character != 0) {
put_char(input_character);
}else {
break;
}
}
exit();
}
Bring in the rainbows
So how can we change the color of the text we’re printing? With ANSI Codes of course. This means we can print ESC[31m
to turn our text red or ESC[32;1m
to make it bright green. ESC is 27 in decimal.
We will make a function that takes an integer and changes colors to 1,2,3,4,5,6,7
repeating. The reason we don’t want to print black is because we don’t want to have invisible characters on a black terminal.
set_color:
push rdi
mov rdi, 27 ;escape
call put_char
mov rdi, '['
call put_char
mov rdi, '3'
call put_char
pop rdi
mov rax, rdi
mov rdi, 6
div rdi
mov rdi, '0'
inc rdx
add rdi, rdx
call put_char
mov rdi, ';'
call put_char
mov rdi, '1'
call put_char
mov rdi, 'm'
call put_char
ret
void set_color(int i) {
put_char(27); // ANSI Escape
put_char('[');
put_char('3');
char color = (i % 6) + 1; // Between 1-7
put_char('0' + color); // Turn integer to ASCII char
put_char(';');
put_char('1'); // Make the colors bright
put_char('m');
}
Now we can modify our _start function once again in order to use this new color function. We will keep an integer value and increment it every time we print a character. This will change the color of the printed text in a repeated fashion.
_start:
mov r12, 0
.loop:
call read_char
mov rdi, rax
cmp rdi, 0
je .end
call put_char
mov rdi, r12
call set_color
inc r12
jmp .loop
.end:
mov rdi, 0
call exit
void main() {
int color = 0;
while (true) {
char input_character = read_char();
if (input_character != 0) {
put_char(input_character);
set_color(i);
i++;
}else {
break;
}
}
exit();
}
Conclusion
And like that, our lolcat is complete. Time to test it. Here are some screenshots.
-
[cowsay lolcat](/img/articles/lolcat-clone-in-x64-assembly/screenshot.png) -
[hexdump lolcat](/img/articles/lolcat-clone-in-x64-assembly/screenshot_2.png)
You can find the whole asm file here.
Thanks for sticking till the end. That was quite a long article but I hope you liked it.