Skip to content

Calling Assembly from C and C++

Let C or C++ handle input and reporting while a small assembly function implements one explicit contract. Link the same assembly object to both callers.

Before you start

Complete functions and the ABI. Basic C declarations are needed here; the optional C++ caller uses only a wrapper and C linkage.

Outcome: build separate objects and distinguish a link-name problem from an ABI or bounds problem. Work in a fresh directory on the Pi.

Define the contract first

add_u64(a, b) returns their sum modulo 2^64. It has no pointers, allocation, exceptions or hardware access. Fixed-width types make the intended widths explicit; they do not automatically fix an incorrect assembly implementation.

1
2
3
4
5
6
7
8
9
// file: interop.S
.text
.global add_u64
.type add_u64, %function
add_u64:
    add x0, x0, x1
    ret
.size add_u64, .-add_u64
.section .note.GNU-stack,"",%progbits
/* file: interop.c */
#include <stdint.h>
#include <stdio.h>
extern uint64_t add_u64(uint64_t, uint64_t);
int main(void)
{
    if (add_u64(2, 3) != 5 || add_u64(UINT64_MAX, 1) != 0) return 1;
    puts("PASS C linkage: unsigned wrap is specified");
    return 0;
}
1
2
3
4
5
gcc -g -c interop.S -o interop.o
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -g -c interop.c -o interop-c.o
gcc interop-c.o interop.o -o interop-c
./interop-c
nm interop.o
PASS C linkage: unsigned wrap is specified

The symbol table should expose add_u64. The compiler checks declarations at C call sites; it does not prove that the assembly agrees.

Call the same object from C++

Save this alternative driver; it has its own main, so do not link both drivers together.

// file: interop.cpp
#include <cstdint>
#include <iostream>
#include <limits>
extern "C" std::uint64_t add_u64(std::uint64_t, std::uint64_t);
int main()
{
    if (add_u64(2, 3) != 5 ||
        add_u64(std::numeric_limits<std::uint64_t>::max(), 1) != 0) return 1;
    std::cout << "PASS C++ linkage: same assembly object\n";
}
g++ -std=c++17 -Wall -Wextra -Wpedantic -Werror -g interop.cpp interop.o -o interop-cpp
./interop-cpp
PASS C++ linkage: same assembly object

extern "C" controls language linkage; it does not transform an arbitrary class into a portable layout or make exceptions safe across hand-written frames. Keep the boundary simple. A member function's implicit object argument, struct return and varargs need their own ABI analysis, not a copied integer example.

Diagnose the right layer

Failure Inspect
Undefined reference Exact symbol spelling, C++ mangling, missing object
Multiple definition of main Only one alternative driver per executable
Correct link, wrong result Signature, widths, argument order and preservation
Crash with pointers Accessible storage, lifetime and length contract
Exec format error Object and executable target architecture

Do not add sudo to fix a link error. Do not add device addresses or memory mapping to establish basic interoperability.

Where hardware fits

GPIO register maps vary by SoC and controller. A Linux user-space assembly course is not a reason to bypass kernel interfaces. After this course, use the separate C++ Hardware Programming course for wiring, line ownership and safe kernel-supported access. The old mixed GPIO/ABI lesson is replaced by this explicit, hardware-independent boundary.

Check your understanding

Why can a C++ declaration with the wrong type still link to this assembly symbol?

Compare your reasoning

The linker matches symbol names, not the full source-level contract of a hand-written assembly body. Matching names are necessary but do not prove widths or semantics.

Continue the course

Previous: Functions, Stack and the ABI · Course roadmap · Next: Debugging with GDB

Donate