Recently I was helping someone really junior to write and run simple C and NASM programm.

I had some difficulties, as the last time I used Assembly to write something was around 16 years ago. Time to refresh the memory.

Tools required: nasm, gcc, gdb (lldb for Mac)

Numeric calculations

Let’s create a simple C wrapper program that will invoke external function, written with NASM:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <inttypes.h>

extern uint64_t asm_compute(uint64_t n1, uint64_t n2, uint64_t n3);

int main(int argc, char* argv[]) {
  if (argc < 4) {
    printf("Not enough arguments.\nUsage: %s [n1] [n2] [n3]\n", argv[0]);
    return EXIT_FAILURE;

  int64_t n1 = strtoull(argv[1], NULL, 10);
  int64_t n2 = strtoull(argv[2], NULL, 10);
  int64_t n3 = strtoull(argv[3], NULL, 10);

  int64_t result = asm_compute(n1, n2, n3);
  printf("asm_compute(%lld, %lld, %lld) = %lld\n", n1, n2, n3, result);

  return EXIT_SUCCESS;

Here we have defined an external function asm_compute that will accept 3 numeric arguments.

Now we have to create a simple Assembly file in NASM to provide this asm_compute function.

global _asm_compute

section .text

    ; do nothing yet

Here we defined a global function that we will export, so our C-wrapper will link properly. Beware of the underscore before the function name _asm_compute. As described in this comment on Stackoverflow, this behavior can be changed, but it is a good thing to follow conventions ;)

Nasm documentation states following:

Most 32-bit C compilers share the convention used by 16-bit compilers, that the names of all global symbols (functions or data) they define are formed by prefixing an underscore to the name as it appears in the C program. However, not all of them do: the ELF specification states that C symbols do not have a leading underscore on their assembly-language names.

Calling Conventions

Before we will dive into writing ASM code, let’s have a look at the Calling Conventions

This is important to understand, in order to make external calls possible. Caller and Callee should respect the same calling convention, in order to send arguments and receive return values.

We will use for our example x86-64 calling convention. Please note, that different platforms will implement this differently. For example, *nix will send integer arguments via RDI, RSI, RDX, RCX and so on, while Windows will use RCX, RDX, R8, R9, and so on.

So in our case, the parameters would be passed through those three: RDI: n1, RSI: n2, RDX: n3

    ; RDI is the 1st param
    ; RSI is the 2nd param
    ; RDX is the 3rd param

One other trick to understand how parameters were passed, is to disassemble C code with gcc -S, which will produce file.s assembly, which format is not NASM, btw (so the operands are in reverse order!)

gcc -S num_calc.asm produces num_calc.s:

... ; important part:
    movq    -24(%rbp), %rdi       ; 1st param is copied to rdi
    movq    -32(%rbp), %rsi       ; 2nd param is copied to rsi
    movq    -40(%rbp), %rdx       ; 3rd param is copied to rdx
    callq   _asm_compute          ; call is being made
    movq    %rax, -48(%rbp)       ; return value is grabbed from rax  

Ok, so now we are pretty sure that the values go where expected, and the result is taken from RAX register. Our codes should look like this:

    mov RAX, 42 ; precisely calculated value, returned to the caller

Back to calculations

Let’s do some useful calculations here, like (n1 * 8 + n2 / 4) * (n3 - 5)

We have here all basic operations: *, /, +, -. There’s plenty documentation online, you can also check those cheat sheets: 1, 2, 3.

One should always remember two things: size of the operand (8/16/32/64/.. bits) and if it is signed or unsigned. Proper Asm command should be used, in order to avoid unpredicted numbers.

Let’s break down our formula into easy calculations:

    ; n1 * 8        ; 8 = 2^3
    shl RDI, 3      ; really easy when you have degree of '2', 
                    ; just shift left to multiply by degree of 2, or shift right to divide
                    ; for signed ints, SAR/SAL should be used instead, to preserve sign bit
    ; normal way:
    mov rax, rdi
    mov rcx, 8 
    mul rcx         ; Careful! size matters, using cl/cx/eax/rcx will produce unexpected values, 
                    ; as multiplications would be done to a subset of RAX bits (al/ax/eax/rax);
                    ; Again, for signed ints there is IMUL
                    ; result is in rax (rdx:rax)

    shr RSI, 2      ; n2 / 4 ; same binary shift trick  4 = 2^2
    ; normal way
    mov rax, rsi
    xor rdx, rdx    ; cleanup rdx, as div will give EAX:= EDX:EAX / ECX
    mov rcx, 8
    div rcx         ; result is in rax

    ; n3 - 5
    ; by the time we get to our 3rd parameter, which was in RDX, we have already lost its value by doing mul/div
    ; so it would have made sense to save it in the beginning, by moving to the unused registers, or stack
    ; push RDX
    pop RDX         ; load back our rdx value
    sub RDX, 3

Ok, let’s just collect everything into one piece of code:

; RDI - n1
; RSI - n2
; RDX - n3

  sub rdx, 5    ; calculate 2nd part

  shl rdi, 3    ; multiply by 2^3 : rdi = n1 * 8
  shr rsi, 2    ; divide by 2     : rsi = n2 / 4

  add rdi, rsi  ; rdi = n1*8 + n2/4

  mov rax, rdx  ; rax = n3 - 5
  mul rdi       ; rax = (n3 - 5) * (n1*8 + n2/4)

  ;result is already in rax


Time to compile everything back into one lovely executable:

nasm -f macho64 num_calc.asm            # will produce num_calc.o
gcc -Wall -o num_calc num_calc.c num_calc.o   # will compile num_calc.c and link compiled asm code

If we were lucky enough, everything should be compiled into num_calc executable, let’s run it:

$ ./num_calc 20 3 10
asm_compute(20, 3, 10) = 800

Awesome, it runs. Just let’s remember that we used Integers, so we are only talking so far about whole numbers. No floating point calculations.

Continue reading here: Running NASM inside C inside GDB. Part 2. More arguments

Source code on github: nasm-c-gdb