February 24th, 2024

Recap of Linux x64 ABI

When a function is called in C, the converted x64 assembly code will hold the function arguments in the following order:

  1. rdi
  2. rsi
  3. rdx
  4. rcx
  5. r8
  6. r9

If the function takes more than 6 arguments, the remaining ones will be on $rsp+8. This is because $rsp+0 stores the value of rip so that the return statement knows where to go:

7. rsp+8 8. rsp+8*2 9. ...

And finally, the result is stored in rax, if any.

How to inline code (naive approach)

Assembly code can be inlined with asm. You have to be careful with what you are doing, because compiled code with optimisations will look very different from compiled code without optimisation.

Take for example this simple function:

int foo(int a) {
  asm("add dword ptr [rbp-4], 25");
  return a;
}

int main() {
  return foo(42);
}

Note that you must compile this with -masm=intel.

!gcc -masm=intel -o /tmp/a.out % && /tmp/a.out

This is the output assembly:

  .text
  .globl  foo
foo:
  push  rbp
  mov  rbp, rsp
  mov  DWORD PTR -4[rbp], edi
#APP
# 2 "/tmp/c_to_assembly.c" 1
  add dword ptr [rbp-4], 25
# 0 "" 2
#NO_APP
  mov  eax, DWORD PTR -4[rbp]
  pop  rbp
  ret
main:
  push  rbp
  mov  rbp, rsp
  mov  edi, 42
  call  foo
  pop  rbp
  ret

Note how it all falls into place. The program exits returning 42+25 = 67.

However, if we compile with optimisation, this will all go to shambles:

  .text
  .globl  foo
  .globl  main
foo:
  mov  eax, edi
#APP
# 2 "/tmp/c_to_assembly.c" 1
  add dword ptr [rbp-4], 25
# 0 "" 2
#NO_APP
  ret
main:
#APP
# 2 "/tmp/c_to_assembly.c" 1
  add dword ptr [rbp-4], 25
# 0 "" 2
#NO_APP
  mov  eax, 42
  ret

Look how the compiler inlined the code from foo. The function isn't even called anymore.

Extended asm with gcc (better approach)

With extended asm you can read and write C variables from assembler and
perform jumps from assembler code to C labels. Extended asm syntax uses
colons (‘:’) to delimit the operand parameters after the assembler template:
asm asm-qualifiers ( AssemblerTemplate
                 : OutputOperands
                 [ : InputOperands
                 [ : Clobbers ] ])

asm asm-qualifiers ( AssemblerTemplate
                      : OutputOperands
                      : InputOperands
                      : Clobbers
                      : GotoLabels)

source

Note that:

The asm keyword is a GNU extension. When writing code that can be compiled
with -ansi and the various -std options, use __asm__ instead of asm (see
Alternate Keywords).

And very importantly:

Since GCC does not parse the assembler template, it has no visibility of any
symbols it references. This may result in GCC discarding those symbols as
unreferenced unless they are also listed as input, output, or goto operands.

Outputs, inputs, clobbers, and labels are all optional. Colons are only
required up to the parameter that you wish to use, and having nothing between
colons is valid when you wish to skip a parameter. For instance, each of
these (explained below) are valid.
    asm("movq %0, %0" : "+rm" (foo));
    asm("addl %0, %1" : "+r" (foo) : "g" (bar));
    asm("lfence" : /* no output */ : /* no input */ : "memory");

source

Prefer to name parameters:

When you specify the optional [Name] field, you become able to refer to that input or output using the %[Name] syntax in the assembler template. For instance:

int foo = 1;
asm("inc %[IncrementMe]" : [IncrementMe] "+r" (foo));
foo == 2

Even when names are specified, you can still refer to operands using the numbered syntax.

Constraints

In the example above we have, right next to the variable, the double quoted "+r" constraint.

The constraint string communicates to GCC what the variable represents. For example, take the asm code:

imul %0, %1, %2
The constraint string for each operand must communicate these requirements to
GCC. For instance, it will ensure that the destination value lives in a
register that can be used at the point of this statement.

GCC defines many types of constraints, but on 2019's desktop/mobile platforms, those are the constraints that are the most likely to be used:

For a list with more constraints go to:

Right next to r we have the symbol +, those symbols are:

Constraint modifying characters

Output constraints must begin with either = (a variable overwriting an existing value) or + (when reading and writing). When using =, do not assume the location contains the existing value on entry to the asm, except when the operand is tied to an input; see Input Operands.

Clobbers

While the compiler is aware of changes to entries listed in the output
operands, the inline asm code may modify more than just the outputs. For
example, calculations may require additional registers, or the processor may
overwrite a register as a side effect of a particular assembler instruction.
In order to inform the compiler of these changes, list them in the clobber
list. Clobber list items are either register names or the special clobbers
(listed below). Each clobber list item is a string constant enclosed in
double quotes and separated by commas.

Simple Example

int add(int a, int b) {
    asm("add %[a], %[b]" : [a] "+r" (a) : [b] "rm" (b));
    return a;
}

Now note that using this notation, when we compile this code with -O3, we still get the correct code!

add:
  mov  eax, edi
#APP
# 2 "/tmp/c_to_assembly.c" 1
  addl eax, esi
# 0 "" 2
#NO_APP
  ret

Fixing our initial example with extended assembly

int foo(int a) {
  asm(
    "add %[a], 25"
    : [a] "+r" (a)
  );
  return a;
}

int main() {
  return foo(42);
}

And the output assembly:

foo:
  mov  eax, edi
#APP
# 2 "/tmp/c_to_assembly.c" 1
  add eax, 25
# 0 "" 2
#NO_APP
  ret
  .size  foo, .-foo
  .section  .text.startup,"ax",@progbits
  .p2align 4
  .globl  main
  .type  main, @function
main:
  mov  eax, 42
#APP
# 2 "/tmp/c_to_assembly.c" 1
  add eax, 25
# 0 "" 2
#NO_APP
  ret