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:

  1. rsp+8
  2. rsp+8*2
  3. ...

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