Взаимодействие с C и C++
Контекст: Linux x86-64, NASM + GCC. Соглашение System V AMD64 ABI. Windows использует другой порядок регистров — см. подпрограммы.
Общая схема
- Ассемблерная функция с именем, видимым линкеру (
global). - C-файл с
externобъявлением с тем же именем (без подчёркивания в Linux для C; C++ требуетextern "C"). - Сборка:
nasm -f elf64→.o, затемgccлинкует сmain.c.
Имя в объектнике должно совпадать: в NASM для C-линковки часто пишут global my_add и в C — long my_add(long, long);.
Это лучший практический формат применения asm в реальных проектах: "тонкая" горячая функция на ассемблере + остальная логика на C/C++. Такой подход сохраняет производительность там, где она нужна, и не превращает весь код в трудносопровождаемый низкоуровневый монолит.
Ассемблер вызывается из C
add.asm:
section .text
global my_add
my_add:
mov rax, rdi ; 1-й аргумент (long)
add rax, rsi ; 2-й аргумент
ret
Разбор:
section .textуказывает, что дальше идёт исполняемый код.global my_addэкспортирует функцию для линковки с C-модулем.- В System V AMD64 первый
longприходит вRDI, второй — вRSI. mov rax, rdiпереносит первый аргумент в регистр возврата.add rax, rsiдобавляет второй аргумент и формирует результат.retвозвращает управление вызывающему C-коду, результат остаётся вRAX.
main.c:
#include <stdio.h>
long my_add(long a, long b);
int main(void) {
printf("%ld\n", my_add(10, 32));
return 0;
}
Разбор:
- Объявление
long my_add(long a, long b);сообщает компилятору сигнатуру внешней asm-функции. - В
mainвызываетсяmy_add(10, 32), а возвращённыйlongпередаётся вprintf. - Формат
%ldсоответствует типуlongна целевой платформе Linux x86-64. return 0;завершает программу с кодом успеха.
Сборка:
nasm -f elf64 add.asm -o add.o
gcc main.c add.o -o demo
Разбор:
nasm -f elf64компилирует ассемблер в 64-битный ELF-объект.gcc main.c add.o -o demoкомпилирует C и линкует его сadd.oв один исполняемый файл.- На этапе линковки
my_addиз C сопоставляется сglobal my_addиз NASM.
my_add возвращает значение в RAX (целые до 64 бит). Вещественные — в XMM0 и далее по ABI.
Функция с сохранением callee-saved регистра:
Код ITЗагрузка примера кода…
Разбор:
RBX— callee-saved: перед изменением его сохраняютpush rbx, передretвосстанавливаютpop rbx.RDI,RSI,RDX— аргументы по System V AMD64 (указатель, длина, множитель).test rsi, rsi/jz .doneобрабатывает пустой массив без лишней работы.imul qword [rbx], rdxумножает текущий элемент на месте.- Цикл сдвигает указатель на 8 байт (
add rbx, 8) и уменьшает счётчикRSI. - Без
push/pop rbxвызов из C мог бы испортить значениеRBXу вызывающего кода.
Возврат float из asm в C:
Код ITЗагрузка примера кода…
float dot_product(const float *a, const float *b, long n);
int main(void) {
float a[] = {1.0f, 2.0f, 3.0f};
float b[] = {4.0f, 5.0f, 6.0f};
printf("%f\n", dot_product(a, b, 3)); /* 32.0 */
}
Разбор:
- Скалярные
floatпередаются указателями вRDIиRSI, длина — вRDX. xorps xmm0, xmm0обнуляет аккумулятор (чистый старт суммы).movss/mulss/addssобрабатывают по одному элементу за итерацию.- Итоговая сумма остаётся в
XMM0— так C получает возвращаемыйfloat. - Указатели сдвигаются на 4 байта (
add rdi, 4), потому чтоfloatзанимает 32 бита. - Сборка та же:
nasm -f elf64 dot.asm -o dot.o && gcc main.c dot.o -o demo.
C вызывается из ассемблера
main.asm:
Код ITЗагрузка примера кода…
Разбор:
global mainделает asm-функцию точкой входа уровня libc (main), а не_start.extern printfимпортирует функцию из стандартной библиотеки C.- Пролог
push rbp / mov rbp, rsp / sub rsp, 16создаёт стековый кадр и сохраняет выравнивание. lea rdi, [rel fmt]кладёт адрес форматной строки в первый аргументprintf.mov rsi, 42задаёт второй аргумент%ld.xor rax, raxустанавливаетAL=0для variadic-вызова без XMM-аргументов.call printfпечатает строку, затемxor eax, eaxготовит код возврата0.leaveиretкорректно закрывают кадр и возвращают управление runtime.
Сборка: 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–XMM15 | RBX, RBP, R12–R15 |
Если ассемблерная функция портит RBX, она обязана восстановить его до ret. Если C вызывает ваш код и вы меняете R12–R15 — то же самое.
Подробнее — Команды и подпрограммы.
C++ и extern "C"
Имена в C++ декорируются (_Z3fooil). Ассемблерный модуль для линковки с C++:
extern "C" int asm_helper(int x);
Разбор:
extern "C"отключает C++ name mangling для указанной функции.- Благодаря этому линкер ищет символ
asm_helperв "C-форме" имени. - Сигнатура в C++ должна совпадать с реальной asm-реализацией по ABI и типам.
и в 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);
}
Разбор:
extern void block_transform(...)объявляет внешнюю asm-функцию блочной обработки.tmp[16]задаёт временный буфер размером в один блок.- Цикл шагает по входному буферу с шагом
16байт. - На каждой итерации передаются адрес текущего блока
buf + iи адрес выходного буфераtmp. - Основная логика управления остаётся на C, а вычислительно дорогая часть уходит в asm.
Ассемблер реализует block_transform с фиксированным ABI; C не знает про регистры внутри.
Реализация block_transform на asm:
Код ITЗагрузка примера кода…
Разбор:
- Сигнатура совпадает с C: два указателя в
RDIиRSI. mov rcx, 16задаёт фиксированный размер блока (16 байт).loop .copyповторяет тело, покаRCXне станет нулём (удобно для фиксированного размера).xor al, 0x5A— учебная "обработка" байта; в реальном коде здесь будет криптопримитив.retвозвращает управление в C-циклprocess_bufferбез изменения callee-saved регистров.- C-обёртка и asm-ядро линкуются одной командой
gcc wrapper.c block.o -o demo.
Ошибки при стыковке
| Симптом | Вероятная причина |
|---|---|
Segfault в printf | стек не выровнен на 16 |
| Неверный результат | аргументы не в rdi/rsi/... |
undefined reference | нет global в .asm или другое имя |
| Работает в C, падает из asm | испорчен callee-saved регистр |
Короткий pre-flight перед линковкой
Перед запуском полезно проверить 6 пунктов:
- Совпадают ли имена символов в C и NASM.
- Совпадают ли размеры типов (
int,long, указатели) для целевой платформы. - Соблюдён ли ABI целевой ОС (System V vs Microsoft x64).
- Выровнен ли стек перед вызовом функций libc.
- Восстановлены ли callee-saved регистры.
- Нет ли смешения 32/64-битных объектных файлов.
После такой проверки интеграция asm/C обычно проходит с первого-второго запуска.
Связанные материалы
- Разбиение на файлы — линковка модулей.
- Системные вызовы без libc — архитектура программ и справочник.