100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Interfacing Assembly with C

How to call C functions from hand-written assembly and expose assembly routines to C, following the System V AMD64 calling convention.

System InteractionIntermediate10 min readJul 10, 2026
Analogies

Why Mix Assembly and C at All

Assembly and C are mixed for two practical reasons: performance-critical kernels (checksum loops, cryptographic primitives, SIMD-heavy math) that the compiler cannot express as efficiently as hand-tuned code, and low-level glue that C cannot express at all, such as a syscall wrapper, a context-switch routine, or a bootloader entry point. Rather than writing an entire program in assembly, real-world code isolates the assembly to a small number of functions and lets C handle everything else — parsing, memory management, application logic — calling into and out of the assembly only at well-defined function boundaries.

🏏

Cricket analogy: It is like a Test team that plays with specialist bowlers who come on only for short, high-impact spells — say, a strike bowler brought in purely to attack the tail — rather than bowling every single over of the innings themselves.

The System V AMD64 Calling Convention

On Linux, macOS, and other System V systems, integer and pointer arguments are passed in RDI, RSI, RDX, RCX, R8, R9 in order, floating-point arguments go in XMM0 through XMM7, and the return value comes back in RAX (or RAX:RDX for 128-bit values, or XMM0 for floats). RBX, RBP, and R12–R15 are callee-saved, meaning an assembly function that uses them must push and restore their original values before returning; RAX, RCX, RDX, RSI, RDI, R8–R11 are caller-saved and may be freely clobbered. Crucially, the stack must be 16-byte aligned at the point of a CALL instruction, so RSP+8 is 16-byte aligned immediately inside the callee — getting this wrong causes crashes in functions that use aligned SSE loads like MOVAPS.

🏏

Cricket analogy: It is like a fixed batting lineup contract: the top six slots (RDI through R9) are pre-assigned to specific batters, but any fielder in the deep (caller-saved registers) can be freely rotated between overs while the wicketkeeper and slip cordon (callee-saved registers) must return to their exact original positions.

Calling C From Assembly, and Assembly From C

To call a C function like printf from NASM, you declare it with extern printf, load arguments into the ABI registers described above, ensure the stack is 16-byte aligned, and issue CALL printf — variadic functions additionally require AL to hold the count of vector registers used for floating-point arguments. To expose an assembly function to C, you declare it global my_asm_func, give it a matching prototype in a C header (extern "C" if compiled as C++, to suppress name mangling), and make sure it respects the callee-saved register contract and returns its result in RAX/XMM0 exactly as C expects. Getting either direction wrong — misaligned stack, clobbered callee-saved registers, or forgetting AL for variadics — produces crashes that are notoriously hard to trace because the corruption surfaces far from its actual cause.

🏏

Cricket analogy: It is like an overseas player joining an IPL franchise: they must register under the exact squad rules (function prototype), follow the team's fielding restrictions (calling convention), and the scorer must log their contribution under their registered name, not a nickname (name mangling).

nasm
; NASM: call printf("sum = %d\n", a + b) then return the sum to a C caller
extern printf
global add_and_print

section .rodata
    fmt db "sum = %d", 10, 0

section .text
add_and_print:                 ; int add_and_print(int a, int b)  -- a in edi, b in esi
    push rbx                   ; callee-saved register we intend to use
    mov ebx, edi
    add ebx, esi                ; ebx = a + b

    ; align stack to 16 bytes before CALL (RSP was 16-aligned on entry to this
    ; function per the ABI, then push rbx made it 8 mod 16, so no extra push needed here)
    mov edi, fmt
    mov esi, ebx
    xor eax, eax                ; AL = 0 vector registers used (printf is variadic)
    call printf

    mov eax, ebx                ; return sum in eax
    pop rbx
    ret

The 128-byte 'red zone' below RSP on System V AMD64 is a scratch area a leaf function (one that makes no further calls) may use without adjusting RSP at all. It is a free lunch for small leaf assembly routines but must not be used by any function that itself calls out, since a CALL instruction can overwrite it via signal handlers or nested calls.

Forgetting to keep RSP 16-byte aligned before a CALL to a C function is one of the most common assembly-C interop bugs. It rarely crashes immediately; instead it corrupts SSE-aligned local variables inside the callee, producing intermittent segmentation faults that seem unrelated to the actual misalignment.

  • Assembly is typically mixed with C for hot-path performance or for operations C cannot express, like raw syscalls or context switches.
  • System V AMD64 passes integer arguments in RDI, RSI, RDX, RCX, R8, R9 and floats in XMM0–XMM7.
  • RBX, RBP, and R12–R15 are callee-saved; RAX, RCX, RDX, RSI, RDI, R8–R11 are caller-saved.
  • The stack must be 16-byte aligned at every CALL instruction, or SSE-aligned loads in the callee can fault.
  • Variadic C functions like printf require AL to hold the number of vector registers used for float arguments.
  • extern declares an external C symbol to call; global exposes an assembly symbol for C to call.
  • The 128-byte red zone lets leaf functions use stack space below RSP without adjusting the pointer.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#AssemblyLanguageStudyNotes#InterfacingAssemblyWithC#Interfacing#Assembly#Mix#System#StudyNotes#SkillVeris#ExamPrep