Перейти к основному содержимому

Взаимодействие с C и C++

Разработчику

Контекст: Linux x86-64, NASM + GCC. Соглашение System V AMD64 ABI. Windows использует другой порядок регистров — см. подпрограммы.

Общая схема

  1. Ассемблерная функция с именем, видимым линкеру (global).
  2. C-файл с extern объявлением с тем же именем (без подчёркивания в Linux для C; C++ требует extern "C").
  3. Сборка: nasm -f elf64.o, затем gcc линкует с main.c.

Имя в объектнике должно совпадать: в NASM для C-линковки часто пишут global my_add и в C — long my_add(long, long);.


Ассемблер вызывается из C

add.asm:

section .text
global my_add

my_add:
mov rax, rdi ; 1-й аргумент (long)
add rax, rsi ; 2-й аргумент
ret

main.c:

#include <stdio.h>

long my_add(long a, long b);

int main(void) {
printf("%ld\n", my_add(10, 32));
return 0;
}

Сборка:

nasm -f elf64 add.asm -o add.o
gcc main.c add.o -o demo

my_add возвращает значение в RAX (целые до 64 бит). Вещественные — в XMM0 и далее по ABI.


C вызывается из ассемблера

main.asm:

section .text
global main
extern printf

main:
push rbp
mov rbp, rsp
sub rsp, 16 ; выравнивание стека на 16 байт перед call в ABI

lea rdi, [rel fmt] ; 1-й аргумент C
mov rsi, 42 ; 2-й
xor rax, rax ; для variadic: число XMM-регистров = 0
call printf

xor eax, eax
leave
ret

section .rodata
fmt db 'value=%ld', 10, 0

Сборка: nasm -f elf64 main.asm -o main.o && gcc main.o -o demo -no-pie (или точка входа main через стандартный crt).

Важно для вызовов libc:

  • Перед call к функциям вроде printf стек RSP должен быть кратен 16 (после call внутри libc снова выровняют; типичный пролог push rbp; mov rbp,rsp; sub rsp,N с нечётным числом push ломает выравнивание).
  • Для variadic (printf, scanf) в AL передают число использованных XMM-аргументов.

Кто сохраняет регистры

По System V AMD64 (кратко):

Сохраняет вызывающий (caller-saved)Сохраняет вызываемый (callee-saved)
RAX, RCX, RDX, RSI, RDI, R8–R11, XMM0–XMM15RBX, RBP, R12–R15

Если ассемблерная функция портит RBX, она обязана восстановить его до ret. Если C вызывает ваш код и вы меняете R12R15 — то же самое.

Подробнее — Команды и подпрограммы.


C++ и extern "C"

Имена в C++ декорируются (_Z3fooil). Ассемблерный модуль для линковки с C++:

extern "C" int asm_helper(int x);

и в NASM: global asm_helper.


Точка входа — _start и main

ТочкаКто инициализирует
_startникто — сами syscall, без libc
mainкод запуска из crt (libc), можно printf, malloc

Смешивать в одном проекте два global main или _start нельзя.


Практический минимум для горячего пути

Типичный паттерн оптимизации: ядро на ассемблере, обвязка на C.

// crypto_wrapper.c
extern void block_transform(const unsigned char *in, unsigned char *out);

void process_buffer(const unsigned char *buf, size_t len) {
unsigned char tmp[16];
for (size_t i = 0; i < len; i += 16)
block_transform(buf + i, tmp);
}

Ассемблер реализует block_transform с фиксированным ABI; C не знает про регистры внутри.


Ошибки при стыковке

СимптомВероятная причина
Segfault в printfстек не выровнен на 16
Неверный результатаргументы не в rdi/rsi/...
undefined referenceнет global в .asm или другое имя
Работает в C, падает из asmиспорчен callee-saved регистр

Связанные материалы


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).