Lesson 06-Assembly and High-Level Languages

The basic syntax structure of inline assembly in C is as follows:

The general format for basic inline assembly is:

asm [volatile] ("assembly_code" : [output_operands] : [input_operands] : [clobbered_registers]);
  • volatile: Optional keyword instructing the compiler not to optimize this assembly block (to avoid deletion or reordering).
  • assembly_code: A string containing specific assembly instructions (e.g., "movl $5, %eax").
  • Output Operands: C variables written to by the assembly instructions (format: "constraint"(variable_name)).
  • Input Operands: C variables read by the assembly instructions (same format as above).
  • Clobbered Registers: Registers modified by the assembly code (e.g., "eax", "memory"), informing the compiler to avoid storing critical data in these registers.

GCC-Style Inline Assembly

Syntax:

asm [volatile] (
    "instruction1\t"
    "instruction2\t"
    ...
    : output_operand_list
    : input_operand_list
    : clobber_list
);

MSVC-Style Inline Assembly

Syntax:

__asm {
    instruction1
    instruction2
    ; comment
}

Complete Example: Swapping Two Variables

GCC Implementation

void swap(int *a, int *b) {
    asm volatile (
        "movl %0, %%eax\t "   // eax = *a
        "movl %1, %%ebx\t "   // ebx = *b
        "movl %%ebx, %0\t "   // *a = ebx
        "movl %%eax, %1 "     // *b = eax
        : "+r" (*a), "+r" (*b)  // Output operands (read-write)
        :                       // No input operands
        : "%eax", "%ebx"        // Clobbered registers
    );
}

MSVC Implementation

void swap(int *a, int *b) {
    __asm {
        mov eax, [a]
        mov ebx, [b]
        mov ecx, [eax]
        mov edx, [ebx]
        mov [eax], edx
        mov [ebx], ecx
    }
}

Modern GCC Extended Syntax: Constraint Details

ConstraintMeaningExample
rAny general-purpose registerasm("movl %0, %%eax" : : "r"(value))
mMemory operandasm("movl %0, %%eax" : : "m"(var))
+Read-write operandasm("addl %1, %0" : "+r"(sum) : "r"(val))
&Early clobberasm("..." : "=&r"(result) : "r"(input))

Extended inline assembly supports more complex operations (e.g., multi-instruction blocks, conditional execution). Its syntax is:

asm [volatile] (
    "label: instruction1\n\t"
    "instruction2\n\t"
    ...
    : [outputs]
    : [inputs]
    : [clobbers]
);

It supports labels, conditional jumps, loops, and other advanced control flow constructs, making it suitable for implementing complex assembly logic.

Complex Example: Computing Array Sum

int array_sum(int *arr, int len) {
    int sum = 0;
    asm volatile (
        "xorl %%eax, %%eax\t "    // Clear eax (used as sum)
        "movl %1, %%ecx\t "       // ecx = len
        "movl %0, %%edi\t "       // edi = arr
        "rep stosl\t "            // Placeholder—should be actual loop
        "movl %%eax, %0 "
        : "+r" (sum)
        : "r" (len), "r" (arr)
        : "%eax", "%ecx", "%edi"
    );
    return sum;
}

Example: Loop Accumulation Using Extended Inline Assembly

#include <stdio.h>

int sum(int n) {
    int result = 0;
    asm volatile (
        "movl $0, %%eax\n\t"      // eax = 0 (initialize result)
        "movl %1, %%ecx\n\t"      // ecx = n (loop counter)
        "L1:\n\t"                 // Loop label
        "addl $1, %%eax\n\t"      // eax += 1 (accumulate)
        "decl %%ecx\n\t"          // ecx -= 1 (decrement counter)
        "jnz L1\n\t"              // Jump to L1 if ecx ≠ 0
        : "=a" (result)           // Output: eax → result
        : "r" (n)                 // Input: n into any register (here ecx)
        : "%eax", "%ecx", "%cc"   // Clobbered: eax, ecx, condition codes
    );
    return result;
}

int main() {
    printf("sum(5)=%d\n", sum(5)); // Output: sum(5)=5
    return 0;
}

Calling Conventions Explained

A calling convention defines rules for function calls regarding parameter passing, register preservation, and stack cleanup. It is the core specification governing interaction between assembly and high-level languages. Different platforms and compilers support different conventions; common ones include cdecl, stdcall, and fastcall.

Comparison of Major Calling Conventions

ConventionParameter PassingStack Cleanup ResponsibilityTypical Use CaseRegister Preservation Rules
cdeclRight-to-left push onto stack (caller pushes args)Caller (ret n)C standard library (e.g., printf)Caller saves eax, ecx, edx
stdcallRight-to-left push onto stack (caller pushes args)Callee (ret n)Win32 API (e.g., MessageBox)Callee saves eax, ecx, edx
fastcallFirst two args in ECX/EDX; rest on stackCalleeHigh-frequency functions (e.g., math libraries)Callee saves ebx, esi, edi, ebp
thiscallthis pointer in ECX; rest on stackCalleeC++ member functionsSame as fastcall

System V AMD64 ABI (x86-64)

  • Integer arguments: RDI, RSI, RDX, RCX, R8, R9
  • Floating-point arguments: XMM0–XMM7
  • Return value: RAX (integer), XMM0 (floating-point)
  • Stack alignment: 16-byte boundary

Calling Convention Implementation Examples

cdecl Function Call

C function declaration: int __cdecl add(int a, int b);

Assembly implementation (GCC compiled):

section .text
global add
add:
    push ebp
    mov ebp, esp
    mov eax, [ebp+8]    ; First parameter a
    add eax, [ebp+12]   ; Add second parameter b
    pop ebp
    ret                 ; Caller cleans up stack

stdcall Function Call

C function declaration: int __stdcall sub(int a, int b);

Assembly implementation:

section .text
global sub
sub:
    push ebp
    mov ebp, esp
    mov eax, [ebp+8]    ; Parameter a
    sub eax, [ebp+12]   ; Subtract parameter b
    pop ebp
    ret 8               ; Clean up 8 bytes (2 × 4)

Parameter Passing Mechanisms

Parameter Passing Comparison

MethodApplicable ScenarioPerformanceFlexibility
RegistersFew parametersBestLimited
StackMany parameters / complex typesSlowerFlexible

Example (x86-64, System V ABI)

For function int add(int a, int b, int c, int d, int e, int f):

  • a → rdi
  • b → rsi
  • c → rdx
  • d → rcx
  • e → r8
  • f → r9

Parameters beyond the first six are pushed onto the stack.

x86-64 System V Parameter Passing Example

// C function: long func(int a, double b, int c);
asm volatile (
    "movl %1, %%edi\t"      // First arg a → EDI
    "movsd %2, %%xmm1\t"    // Second arg b → XMM1
    "movl %3, %%esi\t"      // Third arg c → ESI
    "call func"
    :
    : "r"(a), "m"(b), "r"(c)
    : "%edi", "%xmm1", "%esi"
);

Floating-Point Parameter Handling

Floating-point parameters are typically passed via SSE registers (x86-64) or ST registers (x86):

  • x86-64 (System V ABI): First 8 floating-point parameters use xmm0–xmm7.
  • x86 (cdecl): Floating-point parameters are pushed onto the stack (8-byte aligned).

Floating-Point Parameter Rules

  • First 8 FP args passed via XMM0–XMM7
  • Integer and floating-point registers are counted independently when mixed
  • Return values:
    • Single/double precision: XMM0
    • Quad precision: XMM0:XMM1

Example

// C function: double calc(double a, int b, float c);
asm volatile (
    "movsd %1, %%xmm0\t"    // a → XMM0
    "movl %2, %%edi\t"      // b → EDI
    "movss %3, %%xmm1\t"    // c → XMM1
    "call calc"
    :
    : "m"(a), "r"(b), "m"(c)
    : "%xmm0", "%edi", "%xmm1"
);

Return Value Handling

Return values are typically passed as follows:

  • Integer/Pointer: eax (x86) or rax (x86-64)
  • Floating-point: xmm0 (x86-64) or st(0) (x86)
  • Large structs: Passed via hidden pointer (caller allocates memory; callee fills it)

Return Value Passing Rules

Data Typex86 (EAX)x86-64 (RAX)Float (XMM0)
char/shortALAL—
int/pointerEAXRAX—
long longEDX:EAXRDX:RAX—
float——XMM0
double——XMM0

Struct Return Example

// C function: struct Point {int x,y;} get_point();
struct Point p;
asm volatile (
    "call get_point\t"
    "movdqu %%xmm0, %0"     // Assume return via XMM0
    : "=m"(p)
    :
    : "%xmm0"
);

Complex Return Value Handling

Large Struct Return (via Hidden Pointer)

// C function: struct BigStruct get_big_struct();
struct BigStruct { int data[100]; };

void wrapper(struct BigStruct *result) {
    asm volatile (
        "movq %1, %%rdi\t"      // Hidden pointer → RDI
        "call get_big_struct"
        :
        : "r"(result)
        : "%rdi"
    );
}

Assembly Implementation (x86-64)

; Function prototype: Pair make_pair(int a, int b)
_make_pair:
    push rbp
    mov rbp, rsp
    sub rsp, 16             ; Allocate stack space (16-byte aligned)

    mov DWORD PTR [rsp], edi    ; a → edi (first parameter)
    mov DWORD PTR [rsp+4], esi  ; b → esi (second parameter)

    lea rax, [rsp]          ; Load address of struct into rax (return value)
    mov rsp, rbp
    pop rbp
    ret

Mixed-Language Debugging Techniques

GDB Mixed Debugging Setup

Compilation Command:

gcc -g -o mixed_program main.c asm_code.s

GDB Debugging Commands:

# Set mixed breakpoints
break main.c:15         # C source breakpoint
break *0x4005a0         # Assembly address breakpoint

# View mixed call stack
bt full

# Monitor both C variables and assembly registers
info locals             # C local variables
info registers          # Assembly registers
x/10i $pc               # View current assembly instructions

Key Commands

  • layout asm: Display assembly code window.
  • break *0xaddress: Set breakpoint at assembly instruction address.
  • info registers: View all register values.
  • x/10x $esp: Inspect stack memory (10 double words).
  • ni / si: Step next/step into instruction.

Debugging Tips

Breakpoint Strategy

  • Set breakpoints at C function entries
  • Set address breakpoints at assembly function entries
  • Set watchpoints at key parameter-passing locations

Typical Debugging Workflow

(gdb) break main                # C program entry
(gdb) run
(gdb) stepi                     # Enter assembly code
(gdb) info registers            # Check register state
(gdb) x/5i $pc                  # View next 5 instructions
(gdb) finish                    # Finish current assembly function
(gdb) next                      # Continue with C code

Common Issue Diagnosis

Parameter Passing Errors

# Verify parameter passing
(gdb) x/4wx $esp        # Inspect stack arguments
(gdb) info registers    # Check register arguments

Return Value Anomalies

# Validate return value handling
(gdb) p $eax            # Check integer return value
(gdb) p/x $xmm0         # Check floating-point return value

Stack Imbalance Issues

# Monitor stack pointer changes
(gdb) info registers esp
(gdb) x/10wx $esp

Comprehensive Practical Example: Optimized Matrix Multiplication

C Main Program

#include <stdio.h>
void matmul_asm(float *A, float *B, float *C, int n);

int main() {
    float A[4][4] = {...};
    float B[4][4] = {...};
    float C[4][4] = {0};
    matmul_asm(&A[0][0], &B[0][0], &C[0][0], 4);
    // Print results...
    return 0;
}

Assembly Implementation (matmul.asm)

section .text
global matmul_asm
matmul_asm:
    ; Parameters: rdi=A, rsi=B, rdx=C, rcx=n
    push rbp
    mov rbp, rsp
    ; ... matrix multiplication implementation ...
    pop rbp
    ret

Compilation and Linking

nasm -f elf64 matmul.asm -o matmul.o
gcc -O3 -march=native -o program main.c matmul.o

System Call Wrapping

System calls are interfaces for user-space programs to interact with the kernel, triggered via software interrupts (e.g., x86 int 0x80) or dedicated instructions (e.g., x86-64 syscall). High-level languages (like C) wrap system calls via standard libraries (e.g., glibc), while assembly can invoke them directly.

C Interface Example

int my_write(int fd, const void *buf, size_t count);

Implementation (asm_write.s):

section .text
global my_write
my_write:
    mov rax, 1      ; sys_write syscall number
    ret

Debugging Tips

# Trace system calls
(gdb) catch syscall write
(gdb) break my_write
(gdb) commands
>silent
>printf "fd=%d, buf=%p, count=%zu", $rdi, $rsi, $rdx
>continue
>end

System Call Implementation Principles

  • x86 (32-bit): Uses int 0x80; eax holds syscall number; ebx, ecx, etc., hold parameters.
  • x86-64 (64-bit): Uses syscall; rax holds syscall number; rdi, rsi, etc., hold parameters.

Example: Direct write System Call (x86-64)

The write syscall number on x86-64 is 1, with parameters:

  • rdi: File descriptor (STDOUT_FILENO = 1)
  • rsi: Buffer address
  • rdx: Write length
section .data
    msg db 'Hello, System Call!', 0xA
    len equ $ - msg

section .text
global _start
_start:
    ; System call: write(1, msg, len)
    mov rax, 1          ; syscall #1 (write)
    mov rdi, 1          ; stdout
    mov rsi, msg        ; buffer address
    mov rdx, len        ; length
    syscall             ; trigger syscall

    ; Exit program (syscall #60)
    mov rax, 60         ; exit
    xor rdi, rdi        ; status 0
    syscall

Compile and Run:

nasm -f elf64 sys_write.asm -o sys_write.o
ld sys_write.o -o sys_write
./sys_write  # Output: Hello, System Call!

High-Level Language System Call Wrapping (C)

C standard libraries (e.g., glibc) provide user-friendly wrappers:

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count) {
    return syscall(SYS_write, fd, buf, count); // Calls glibc's syscall wrapper
}

Comprehensive Practical Example: Optimizing Matrix Multiplication

Inline assembly can optimize the inner loop of matrix multiplication to improve performance. Assuming N×N matrices, the goal is to reduce memory accesses and utilize registers for intermediate results.

Original C Implementation

#define N 1024
int A[N][N], B[N][N], C[N][N];

void matrix_multiply() {
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            int sum = 0;
            for (int k = 0; k < N; k++) {
                sum += A[i][k] * B[k][j];
            }
            C[i][j] = sum;
        }
    }
}

Optimized Version (Loop Unrolling + Register Caching)

Unroll the inner loop to reduce branch misprediction and cache A[i][k] and B[k][j] in registers:

void matrix_multiply_optimized() {
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            int sum = 0;
            // Unroll inner loop (k=0,1,2,3)
            for (int k = 0; k < N; k += 4) {
                int a0 = A[i][k];
                int a1 = A[i][k+1];
                int a2 = A[i][k+2];
                int a3 = A[i][k+3];

                int b0 = B[k][j];
                int b1 = B[k+1][j];
                int b2 = B[k+2][j];
                int b3 = B[k+3][j];

                sum += a0*b0 + a1*b1 + a2*b2 + a3*b3;
            }
            C[i][j] = sum;
        }
    }
}

Inline Assembly Implementation (x86-64)

Use GCC extended inline assembly to optimize the inner loop directly:

void matrix_multiply_asm() {
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            int sum = 0;
            asm volatile (
                "xor %%rcx, %%rcx\n\t"          // k = 0 (loop counter)
                "L_loop:\n\t"
                "cmp %[N], %%rcx\n\t"           // Compare k with N
                "jge L_end\n\t"                 // Exit if k >= N

                "mov %[A_i_k], %%eax\n\t"       // Load A[i][k] into eax
                "mov %[B_k_j], %%ebx\n\t"       // Load B[k][j] into ebx

                "imul %%ebx, %%eax\n\t"         // eax = A[i][k] * B[k][j]
                "add %%eax, %[sum]\n\t"         // sum += product

                "inc %%rcx\n\t"                 // k++

                // Compute next addresses (row-major layout, int = 4 bytes)
                "lea (%[A],%%rcx,4), %[A_i_k]\n\t"  // A[i][k] = A + i*N + k
                "lea (%[B],%%rcx,4), %[B_k_j]\n\t"  // B[k][j] = B + k*N + j

                "jmp L_loop\n\t"
                "L_end:\n\t"
                : [sum] "+r" (sum)
                : [A] "r" (A), [B] "r" (B), [i] "r" (i), [j] "r" (j), [N] "r" (N)
                : "%rax", "%rbx", "%rcx"
            );
            C[i][j] = sum;
        }
    }
}

Note: The above example assumes simplified address calculation; real implementations must account for full 2D indexing (A[i*N + k], B[k*N + j]).

Summary and Outlook

The integration of assembly and high-level languages is a core skill in low-level development. Inline assembly enables optimization of critical paths, understanding calling conventions clarifies function interactions, and mixed debugging resolves complex issues.

In the future, as compiler optimization advances (e.g., auto-vectorization, loop unrolling), the need for hand-written assembly may diminish. However, it will remain indispensable in domains such as:

  • High-performance computing
  • Embedded systems development
  • Reverse engineering

Developers should master:

  • Inline assembly syntax and register management
  • Calling convention rules and parameter passing mechanisms
  • Mixed-language debugging tools and techniques
  • Low-level implementation and wrapping of system calls

By combining theory with practice, developers can effectively leverage the strengths of both assembly and high-level languages to build high-performance, highly reliable software systems.

Share your love