RTP DesportoLiga Nações. Sudakov dispensado da seleção ucraniana devido a lesãoESPNInactives watch: DeVonta, Price out; McConkey, Coker questionableESPN DeportesDuelo de desesperados entre Cowboys y TexansThe Jerusalem PostOver 25 NYC Jewish groups oppose Mamdani antisemitism plan, demand new onePunchOsun moves to vacate court order freezing govt accountsZDF heuteEntdecken Sie das ZDF-NachrichtenstudioVanguardNavy disrupts 2 illegal refineries, recovers 58,000 litresVariety‘Spotlight’ Director Says The Catholic Church Hasn’t Done ‘Enough’ To Protect Children From Sexual Assault 10 Years After Film’s Best Picture Win: ‘Not Until Every Child Is Safe’n-tvAuthentisch vs. professionell: Bühne oder unverstellt: Wie viel Ich verträgt der Job?Seeking AlphaUlta Beauty tops consumer discretionary gainers in Q3; MGM Resorts declinesynet ספורטחי, מחצית: מכבי ת"א - הפועל ירושלים 32:31The South AfricanLive score: Egypt vs Bafana Bafana
The Daily Newsstand · Free, Always
Sunday, October 4, 2026

ABI‑разрыв

Translate

Привет, Хабр! Ни для кого не секрет, что в языке C существует множество красивых способов положить вашу программу со всеми вытекающими последствиями, но большинство из них обходятся банальными средствами гигиены, санитайзерами, статическими анализаторами, линтерами, да и компиляторы в целом не плохо помогают решать большинство проблем до их обнаружения в продакшене. Но существуют проблемы, которые возникают только во время исполнения и понять, в чем причина, не так просто, хотя сама проблема может оказаться очень даже понятной после установления причинно-следственной связи.

Останавливаться на том, что такое ABI, я не буду, дабы не разводить лишнюю полемику, на Хабре достаточно отличных статей на эту тему. Поговорим предметно о том что такое ABI‑разрыв или ABI‑несовметимость. ABI‑разрыв - это ситуация, когда два и более бинарных файла представляют работу с одними данными по-разному. Представим ситуацию в кофейне, вы заказали большой кофе, бариста готовит нужный объем, но стажер подает ему стакан меньшего объема, как результат вы либо получите меньше кофе, либо произойдет потеря ценных миллилитров. Это и есть ABI‑разрыв, один компонент системы представляет себе работу не так как другой и на стыке их взаимодействия получатся поведение, которое не закладывалось изначально.

Из кофейни к коду

Команда из нескольких разработчиков трудится над одним большим продуктом, компоненты системы разнесены на множество репозиториев. Во время выпуска новой версии продукта билд-система собирает все репозитории, проверяет зависимости и сливает всё в одну рабочую систему. Для ускорения разработки, любой модуль можно собрать отдельно без пересборки всего остального.

Оперировать будем только с 4-мя репозиториями:

  1. Абстрактная кофейня, описанная на языке на С.

  2. Профессиональный бариста, подгружаемый в виде плагина к движку.

  3. Плагин стажера.

  4. Библиотека для работы со стаканчиками.

> tree
├── build_run.sh
├── coffee_shop
│   └── coffee_shop.c
├── cuplib
│   └── cup.h
├── plugin_barista
│   ├── api.h
│   └── plugin_barista.c
└── plugin_intern
    └── plugin_intern.c

Плагины к основному движку подгружаются с помощью библиотеки libdl.

plugin_barista - предоставляет API, в виде заполненной структуры с указателями на функции для управления кофейным цехом. Другие плагины экспортируют эту структуру и используют для своих нужд.

Моделирование ABI‑разрыва

Библиотека для работы со стаканчиками описывает только одну структуру - стаканчик.

// cuplib/cup.h
#pragma once

typedef struct cup_s {
  //...
} cup_t;

Объявление API баристы.

// plugin_barista/api.h
#pragma once

#include <stdbool.h>
#include <cuplib/cup.h>

// Структура API
typedef struct barista_plugin_api_s {
    void        (*make_coffee)(void); // Приготовить кофе
    void        (*pour_coffee)(cup_t* cup); // Налить кофе
    bool        (*is_coffee_ready)(void); // Возвращает true, если кофе готово

} barista_plugin_api_t;

extern const barista_plugin_api_t *get_barista_plugin_api(void);

Хорошей практикой является не выносить внутренние структуры плагина в API, чтобы вызывающий API код не зависел от структур и функций извне, для этого все типы должны быть базовыми, а вся реализация будет внутренне связанна в библиотеке плагина.

// plugin_barista/plugin_barista.c
#include <stdio.h>
#include <plugin_barista/api.h>

// Флаг готовности кофе
static bool g_is_coffee_ready = false;

void make_coffee_fn(void) { 
    g_is_coffee_ready = false; 
    printf("Called make_coffee_fn\n"); 
};

static void pour_coffee_fn(cup_t* cup) { 
    printf("Called pour_coffee_fn, g_is_coffee_ready = %s\n", g_is_coffee_ready ? "true" : "false"); 
};

static bool is_coffee_ready_fn(void) { 
    // Для простоты воспроизведения на третий раз вызова функции, кофе готово
    static unsigned char call_counter = 0;

    if (++call_counter == 3) {
        call_counter = 0;
        g_is_coffee_ready = true;
    }
    printf("Called is_coffee_ready_fn, g_is_coffee_ready = %s\n", g_is_coffee_ready ? "true" : "false");
    return g_is_coffee_ready; 
};

// Заполнение структуры API
static barista_plugin_api_t barista_plugin_api = {
    .make_coffee = make_coffee_fn,
    .pour_coffee = pour_coffee_fn,
    .is_coffee_ready = is_coffee_ready_fn,
};

// Реализация функции экспорта API
const barista_plugin_api_t *get_barista_plugin_api(void) {
    return &barista_plugin_api;
}

С plugin_intern всё просто. Экспоритруем API, далее проверяем, если кофе готово, наливаем.

// plugin_intern/plugin_intern.c
#include <stdio.h>
#include <plugin_barista/api.h>

bool intern_work(cup_t* cup) {
    const barista_plugin_api_t *api = get_barista_plugin_api();

    if (api->is_coffee_ready()) {
        api->pour_coffee(cup);
        return true;
    }

    printf("Coffee not ready\n");
    return false;
}

Реализация движка.

// coffee_shop/coffee_shop.c
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>

#include <plugin_barista/api.h>
#include <cuplib/cup.h>

int main(int agrc, char **argv) {
    // Подгружаем plugin_barista, RTLD_GLOBAL нужен для использования 
    // get_barista_plugin_api в других подгружаемых плагинах
    void *plugin_barista = dlopen("./plugin_barista.so", RTLD_NOW | RTLD_GLOBAL);
    if (!plugin_barista) { fprintf(stderr, "dlopen plugin_barista: %s\n", dlerror()); return 1; }

    // Подгружаем plugin_intern
    void *plugin_intern = dlopen("./plugin_intern.so", RTLD_NOW);
    if (!plugin_intern) { fprintf(stderr, "dlopen plugin_intern: %s\n", dlerror()); return 1; }

    // Получаем все неободимые для работы функции
    bool (*intern_work)(cup_t* cup) = dlsym(plugin_intern, "intern_work");
    void (*make_coffee)(void) = dlsym(plugin_barista, "make_coffee_fn");
    if (!intern_work) { fprintf(stderr, "dlopen: %s\n",  dlerror()); return 1; }

    // Моделируем работу кофейни
    // Заказ кофе
    // Бариста начинает готовку
    make_coffee();
    // Проходит время, стажер 5 раз опрашивает о готовности и 
    // пытается налить кофе, если кофе налито цикл прекращается
    cup_t cup;
    for (size_t i = 0; i < 5; ++i) { 
        if (intern_work(&cup)) {
            break;
        }
    }
    // Отдаем кофе
    return 0;
}

В примере используется тулчейн GCC, но вы можете использовать любой удобный вам. Вся сборка и запуск объеденины в один bash скрипт. Флаги -g и -O0 добавлены для удобства отладки в дебагере.

# !/bin/bash

if [ -e "build" ]; then
    rm -rf build
fi

# Сборочная директория
mkdir build

# Собираем плагин баристы
gcc -c -fPIC plugin_barista/plugin_barista.c -g -O0 -I . --warn-all -o build/plugin_barista.o && gcc -shared -o build/plugin_barista.so build/plugin_barista.o
rm build/plugin_barista.o

# Собираем плагин стажера
gcc -c -fPIC plugin_intern/plugin_intern.c -g -O0 -I . --warn-all -o build/plugin_intern.o && gcc -shared -o build/plugin_intern.so build/plugin_intern.o
rm build/plugin_intern.o

# Собираем движек
gcc coffee_shop/coffee_shop.c -g -O0 --warn-all -ldl -I . -o build/coffee_shop

# Запускаем
cd build/ && ./coffee_shop

Для запуска скрипта не забудьте сделать:

chmod +x build_run.sh

Ожидаемый результат в STDOUT во время выполнения и фактический совпадают:

>Called make_coffee_fn
>Called is_coffee_ready_fn, g_is_coffee_ready = false
>Coffee not ready
>Called is_coffee_ready_fn, g_is_coffee_ready = false
>Coffee not ready
>Called is_coffee_ready_fn, g_is_coffee_ready = true
>Called pour_coffee_fn, g_is_coffee_ready = true

Но, в кофейню нагрянули изменения, повилось несколько видов стаканов разного объема, API баристы меняется, но стажер об этом не уведомлен...

is_coffee_ready_fn теперь должно возвращать не bool, а размер стаканчика при готовности и 0, если кофе не готово.

Ниже измененное объявление API.

// plugin_barista/api.h
#pragma once

#include <stdbool.h>
#include <cuplib/cup.h>

// Структура API
typedef struct barista_plugin_api_s {
    void        (*make_coffee)(void); // Приготовить кофе
    void        (*pour_coffee)(cup_t* cup); // Налить кофе
    unsigned short        (*is_coffee_ready)(void); // Возвращает объем если кофе готово, иначе 0

} barista_plugin_api_t;

extern const barista_plugin_api_t *get_barista_plugin_api(void);

Реализация нового API.

// plugin_barista/plugin_barista.c
#include <stdio.h>
#include <plugin_barista/api.h>

// Флаг готовности кофе
static bool g_is_coffee_ready = false;

void make_coffee_fn(void) { 
    g_is_coffee_ready = false; 
    printf("Called make_coffee_fn\n"); 
};

static void pour_coffee_fn(cup_t* cup) { 
    printf("Called pour_coffee_fn, g_is_coffee_ready = %s\n", g_is_coffee_ready ? "true" : "false"); 
};

static unsigned short is_coffee_ready_fn(void) { 
    // Для простоты воспроизведения на третий раз вызова функции, кофе готово
    static unsigned char call_counter = 0;

    if (++call_counter == 3) {
        call_counter = 0;
        g_is_coffee_ready = true;
    }
    printf("Called is_coffee_ready_fn, g_is_coffee_ready = %s\n", g_is_coffee_ready ? "true" : "false");
    return g_is_coffee_ready ? 330 /* Или любой другой объем */ : 0; 
};

// Заполнение структуры API
static barista_plugin_api_t barista_plugin_api = {
    .make_coffee = make_coffee_fn,
    .pour_coffee = pour_coffee_fn,
    .is_coffee_ready = is_coffee_ready_fn,
};

// Реализация функции экспорта API
const barista_plugin_api_t *get_barista_plugin_api(void) {
    return &barista_plugin_api;
}

Собираем плагин баристы и запускаем.

gcc -c -fPIC plugin_barista/plugin_barista.c -g -O0 -I . --warn-all -o build/plugin_barista.o && gcc -shared -o build/plugin_barista.so build/plugin_barista.o && rm build/plugin_barista.o && cd build/ && ./coffee_shop

Вывод в STDOUT абсолютно корректный, только теперь стажер продолжает трактовать возвращаемое значение is_coffee_ready_fn как флаг готово/не готово, как следствие мы не дополучаем или теряем нужный объем напитка.
Описанный выше пример является достаточно мягкой формой ABI‑разрыва. В реальных же системах ошибки могут возникать гораздо серьезнее. Например, сильно подпортить ситуацию может порядок расположения полей в структуре.

// Было
struct API { A_fn; B_fn; C_fn; };

Добавили новую функцию в начало.

// Стало
struct API { D_fn; A_fn; B_fn; C_fn; };

Без пересборки всей системы, вызывающий API код, будет обращаться к функциям по старым смещениям в структуре, лучший исход - это падение программы. В худшем случае программа будет работать, выдавая непредсказуемое поведение в разных местах.

Ещё одним следствием ABI‑разрыва может стать повреждение кучи, когда из-за расхождения представления о размере стркутур, перезаписываются или затираются участки памяти со служебной информацией необходимой для корректной работы аллокатора:

/* engine_old.c — собран со старым api.h */
#include <stdlib.h>
#include <stdio.h>

#include <api.h>
extern void fill_api(struct plugin_api *out);     /* реализовано в новом API */

int main(void) {
    struct plugin_api *p = calloc(1, sizeof(*p)); /* старый размер допустим 24*/
    fill_api(p);                                  /* новый размер 32 байта */

    /* В этом месте куча уже повреждена */
    char *s = malloc(32);
    // Далее поведение не определено, всё зависит от реализации аллокатора или от погоды за окном
    free(s); /* может упасть тут */
    printf("Hello!\n"); /* или тут */
    return 0;
}

Примеры выше связанны только с разделяемым API, но аналогичная проблема может возникать и для структур, в которых хранятся данные.

Как избегать подобных ситуаций

Решением может быть добавление поле version в начало подобных разделяемых структур. Все функции, которые обращаются к данным структурам, проверяют поле version на соответствие со своей локальной копией полученой из заголовочного файла. В случае расхождения версий, программа аварийно завершается, так как поведение при обращении к данным структурам становится неопределенным.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.