Cogs and Levers A blog full of technical stuff

Getting More Productive With NASM and glibc

Writing “pure syscall” assembly can be fun and educational — right up until you find yourself rewriting strlen, strcmp, line input, formatting, and file handling for the tenth time.

If you’re building tooling (monitors, debuggers, CLIs, experiments), the fastest path is often to write your core logic in assembly and call out to glibc for the boring parts.

In today’s article, we’ll walk through a basic example to get you up and running. You should quickly see just how thin the C language really is as a layer over assembly and the machine itself.

Hello, world

We’ll start with a simple “Hello, world” style application.

BITS 64
DEFAULT REL

extern puts
global main

section .rodata
msg db "Hello from NASM + glibc (puts)!", 0

section .text
main:
  ; puts(const char *s)
  lea   rdi, [rel msg]
  call  puts wrt ..plt      ; <-- PIE-friendly call via PLT

  xor   eax, eax            ; return 0
  ret

Let’s break this down.

BITS 64
DEFAULT REL

First, we tell the assembler that we’re generating code for x86-64 using the BITS directive.

DEFAULT REL changes the default addressing mode in 64-bit assembly from absolute addressing to RIP-relative addressing. This is an important step when writing modern position-independent code (PIC), and allows the resulting executable to work correctly with security features like Address Space Layout Randomisation (ASLR).

extern puts

Functions that are implemented outside our module are resolved at link time. Since the implementation of puts lives inside glibc, we declare it as an external symbol.

global main

The true entry point of a Linux program is _start. When you write a fully standalone binary, you need to define this yourself.

Because we’re linking against glibc, the C runtime provides the startup code for us. Internally, this eventually calls our main function. To make this work, we simply mark main as global so the linker can find it.

section .rodata
msg db "Hello from NASM + glibc (puts)!", 0

Here we define our string in the read-only data section (.rodata). From a C perspective, this is equivalent to storing a const char *.

section .text
main:

This marks the beginning of our executable code and defines the main entry point.

  lea   rdi, [rel msg]
  call  puts wrt ..plt

This is where we actually print the message.

According to the x86-64 System V ABI (used by Linux and glibc), function arguments are passed in registers using the following order:

  • rdi
  • rsi
  • rdx
  • rcx
  • r8
  • r9

Floating-point arguments are passed in XMM registers.

We load the address of our string into rdi, then call puts.

The wrt ..plt modifier tells NASM to generate a call through the Procedure Linkage Table (PLT). This is required for producing position-independent executables (PIE), which are the default on many modern Linux systems. Without this, the linker may fail or produce non-relocatable binaries.

xor   eax, eax
ret

Finally, we return zero from main by clearing eax. Control then returns to glibc, which performs cleanup and exits back to the operating system.

Building

We first assemble the file into an object file:

nasm -felf64 hello.asm -o hello.o

Next, we link it using gcc. This automatically pulls in glibc and the required runtime startup code:

gcc hello.o -o hello

On many modern Linux distributions, position-independent executables are enabled by default. If you encounter relocation errors during linking, you can explicitly enable PIE support:

gcc -fPIE -pie hello.o -o hello

Or temporarily disable it while experimenting:

gcc -no-pie hello.o -o hello

The PLT-based call form shown earlier works correctly in both cases.

Conclusion

Calling glibc from NASM is one of those “unlock” moments.

You retain full control over registers, memory layout, and calling conventions — while gaining access to decades of well-tested functionality for free.

Instead of rewriting basic infrastructure, you can focus your energy on the interesting low-level parts of your project.

For tools like debuggers, monitors, loaders, and CLIs, this hybrid approach often provides the best balance between productivity and control.

In the next article, we’ll build a small interactive REPL in NASM using getline, strcmp, and printf, and start layering real debugger-style functionality on top.

Assembly doesn’t have to be painful — it just needs the right leverage.