Перейти к содержанию

Встроенный ассемблер в GCC (Inline Assembly)

GCC позволяет встраивать ассемблерные инструкции непосредственно в код на C/C++ с помощью оператора asm. Это позволяет использовать специфические инструкции процессора, недоступные через стандартный C, или написать точно оптимизированные участки кода без ущерба читаемости остальной программы.

Синтаксис asm()

asm [volatile] (
    "инструкции"
    : выходные_операнды
    : входные_операнды
    : список_затрагиваемых_регистров
);
  • инструкции — строка с ассемблерными командами в AT&T-синтаксисе; строки разделяются \n\t;
  • выходные операнды — C-переменные, куда записывается результат;
  • входные операнды — C-переменные или константы, используемые как входные данные;
  • затрагиваемые регистры (clobbers) — список регистров, которые изменяет ассемблерная вставка.

Ссылки на операнды в строке инструкций: %0 — первый операнд (первый выходной), %1 — второй и т.д.

Constraint-коды

Код Значение
"r" любой регистр общего назначения
"=r" выходной регистр (запись)
"+r" регистр, который и читается, и записывается
"m" ячейка памяти
"i" непосредственная константа (immediate)
"a" конкретно rax/eax
"d" конкретно rdx/edx
"g" регистр, память или константа (любой)

В строке инструкций регистры пишутся с двойным процентом (%%rax).

Базовые примеры

Сложение двух переменных через ассемблер

#include <stdio.h>

int main(void) {
    int a = 5, b = 3, result;

    asm("movl %1, %%eax\n\t"
        "addl %2, %%eax\n\t"
        "movl %%eax, %0"
        : "=r" (result)
        : "r"  (a), "r" (b)
        : "eax"
    );

    printf("result = %d\n", result);   /* result = 8 */
    return 0;
}

Зачем нужен volatile

Без volatile компилятор вправе переместить, объединить или удалить ассемблерную вставку. С volatile вставка всегда выполняется ровно там, где написана:

asm volatile("nop");         /* не будет удалена оптимизатором */
asm volatile("mfence");      /* барьер памяти — должен быть на месте */

Обмен значений через xchg

#include <stdio.h>

int main(void) {
    int a = 5, b = 10;
    printf("Before: a=%d, b=%d\n", a, b);

    asm("xchgl %0, %1"
        : "+r" (a), "+r" (b)
    );

    printf("After:  a=%d, b=%d\n", a, b);
    /* After: a=10, b=5 */
}

Измерение тактов процессора через rdtsc

Инструкция rdtsc (Read Time-Stamp Counter) возвращает 64-битный счётчик тактов процессора. Это полезно для точного измерения производительности:

#include <stdint.h>
#include <stdio.h>

uint64_t rdtsc(void) {
    uint32_t lo, hi;
    asm volatile("rdtsc" : "=a"(lo), "=d"(hi));
    return ((uint64_t)hi << 32) | lo;
}

int main(void) {
    uint64_t start = rdtsc();
    for (int i = 0; i < 1000; i++)
        asm volatile("nop");
    uint64_t end = rdtsc();
    printf("Elapsed cycles: %lu\n", end - start);
}

Из-за out-of-order execution rdtsc может быть переставлена. Для точного измерения используют cpuid как барьер сериализации:

uint64_t rdtsc_ordered(void) {
    uint32_t lo, hi;
    asm volatile(
        "xorl %%eax, %%eax\n\t"
        "cpuid\n\t"
        "rdtsc"
        : "=a"(lo), "=d"(hi)
        :
        : "rbx", "rcx"
    );
    return ((uint64_t)hi << 32) | lo;
}

cpuid — непривилегированная инструкция; её главное свойство — полная сериализация: все предыдущие инструкции завершаются до cpuid, и никакие последующие не начинаются раньше неё. При eax=0 она возвращает информацию о производителе процессора.

Альтернатива — инструкция rdtscp, которая сериализует только предшествующие нагрузки (load) и дополнительно возвращает идентификатор процессора в ecx:

uint64_t rdtscp(uint32_t *cpu_id) {
    uint32_t lo, hi, aux;
    asm volatile("rdtscp" : "=a"(lo), "=d"(hi), "=c"(aux));
    if (cpu_id) *cpu_id = aux;
    return ((uint64_t)hi << 32) | lo;
}

rdtscp является полным барьером для нагрузок: все предшествующие load-операции завершатся до выборки счётчика.

Защита от timing-атак в криптографии

Timing-атака — атака по сторонним каналам, при которой противник измеряет время выполнения криптографической функции, чтобы восстановить секретный ключ или пароль.

Уязвимая реализация:

int insecure_compare(const char *input, const char *secret, int len) {
    for (int i = 0; i < len; i++) {
        if (input[i] != secret[i])
            return 0;   /* ранний выход — раскрывает позицию несовпадения! */
    }
    return 1;
}

Constant-time реализация:

#include <stdint.h>

int constant_time_compare(const uint8_t *a, const uint8_t *b, int len) {
    volatile uint8_t diff = 0;
    for (int i = 0; i < len; i++)
        diff |= a[i] ^ b[i];   /* XOR = 0 только при совпадении */
    return diff == 0;
}

volatile запрещает компилятору добавить ранний выход. Все len байт всегда проверяются, независимо от того, где обнаружено несовпадение.

В реальном производственном коде используйте готовые функции: CRYPTO_memcmp (OpenSSL), sodium_memcmp (libsodium), timingsafe_bcmp (glibc 2.33+).

Связанные темы

Источники

  • GCC documentation: Extended Asm — https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html
  • GCC documentation: Constraints — https://gcc.gnu.org/onlinedocs/gcc/Constraints.html
  • Intel SDM Vol. 2: инструкции rdtsc, rdtscp, cpuid, xchg
  • libsodium: constant-time comparison — https://libsodium.gitbook.io/doc/helpers