Writing our first Libc-free software

Mahdi Sharifi
2 min readApr 30, 2024

--

In the previous article, we wrote a simple software using Linux syscalls. Now, let’s go completely Libc-free today.

But how was the previous example using Libc when we didn’t include anything at the top?

Although we didn’t include any headers in our program, Libc was still used to bootstrap the application. Let’s get rid of that as well today.

// The write syscall number on x86_64 is 1
#define SYS_write 1
// The write syscall number on x86_64 is 60
#define SYS_exit 60

// We explained how these syscallN functions work so
// checkout the previous article for a breif explaination.
long syscall1(long type, long a1) {
register long _rax __asm__("rax") = type;
register long _rdi __asm__("rdi") = a1;
__asm__ __volatile__("syscall"
: "=r"(_rax)
: "r"(_rax), "r"(_rdi)
: "rcx", "r11", "memory");
return _rax;
}
long syscall3(long type, long a1, long a2, long a3) {
register long _rax __asm__("rax") = type;
register long _rdi __asm__("rdi") = a1;
register long _rsi __asm__("rsi") = a2;
register long _rdx __asm__("rdx") = a3;
__asm__ __volatile__("syscall"
: "=r"(_rax)
: "r"(_rax), "r"(_rdi), "r"(_rsi), "r"(_rdx)
: "rcx", "r11", "memory");
return _rax;
}

// Defining a simple main function like every other program
// makes the compiler use libc to bootstrap the program.
// Each program has a starting point, here we define our
// starting point with the "_start" symbol. The function should never
// return, meaning the OS should terminate the program before
// it reaches its end. So here we define the "noreturn" attribute
// which tells the compiler that this function never reaches its
// end and we call the "__builtin_unreachable()" at its end to
// inform the compiler that our program is unreachable at that point.
// We also need to extern the symbol so it can be accessed by the
// OS outside of binary and start the program.
__attribute__((noreturn)) extern void _start(void) {
// As before, we make a write syscall to stdout.
syscall3(SYS_write, 0, (long)"Successful write syscall!\n", 27);

// We ask the OS to terminate the program here. The exit
// syscall takes one argument and that is the return status
// code. A 0 return status code means the program has finished
// successfully and no errors occured.
syscall1(SYS_exit, 0);

// Let the compiler know that this point is not reachable.
__builtin_unreachable();
}

We should pass the “-nostdlib” flag to the compiler so it doesn’t link the standard library to our application and lets it be libc-free. It works for both gcc and clang if you’re wondering.

Congratulations! You just had your first, completely libc-free program. Follow me for more articles as I publish more articles about this topic. You know the essentials of libc-free programming at this point, you just need to practice more complex programs which I’m going to cover in the next articles.

--

--