
- Главная
- Каталог
- Интернет технологии
- С/С++ Portal | Программирование
С/С++ Portal | Программирование
Присоединяйтесь к нашему каналу и погрузитесь в мир для C/C++-разработчика
Статистика канала
goto cleanup и хотели получить что-то вроде RAII.
GCC добавил похожий механизм для C ещё в 2003 году.
__attribute__((cleanup)) вызывает указанную функцию в момент, когда переменная выходит из области видимости — в любом scope и при любом пути выхода из функции, включая ранние return.
Этим уже пользуются такие проекты, как libvirt и QEMU, а Linux kernel принял этот подход в 2023 году.
И всё это уже много лет поддерживается как в GCC, так и в Clang.
static void freep(void **p) { if (*p) { free(*p); *p = NULL; } }
static void fclosep(FILE **f) { if (*f) { fclose(*f); *f = NULL; } }
int process_file(const char *path) {
__attribute__((cleanup(fclosep))) FILE *f = fopen(path, "r");
__attribute__((cleanup(freep))) char *buf = malloc(4096);
if (!f || !buf) return -1; // both freed automatically
fgets(buf, 4096, f);
printf("%s", buf);
return 0; // fclose and free run automatically here
}{}
В libvirt на базе этого был построен полноценный RAII-подобный механизм на C, где с помощью макроса заменили большинство шаблонов очистки через goto cleanup.
Приоритет конструкторов имеет значение в экосистеме Linux, поскольку glibc резервирует приоритеты от 0 до 100, поэтому пользовательские конструкторы должны использовать приоритет 101 и выше.
/* cleanup.h из Linux kernel: показывает, как ядро использует это начиная с v6.2 */
#define DEFINE_FREE(_name, _type, _free) \
static inline void __free_##_name(void *p) \
{ _type _T = *(_type *)p; _free; } /* достаём значение и вызываем функцию освобождения */
#define __free(_name) __attribute__((__cleanup__(__free_##_name)))
DEFINE_FREE(kfree, void *, kfree(_T)) /* автоосвобождение памяти через kfree */
DEFINE_FREE(fput, struct file *, fput(_T)) /* автоосвобождение ссылки на file */
/* пример использования в реальном коде ядра */
struct file *f __free(fput) = fget(fd);
/* когда переменная f выходит из области видимости,
fput(f) вызывается автоматически */{}
printf сам по себе он ничего не выводит
printf — это по сути всего три строки, и на этом вся функция заканчивается
/* glibc stdio-common/printf.c — вся функция принтф */
int __printf(const char* format, ...) {
va_list arg;
int done;
va_start(arg, format);
done = vfprintf(stdout, format, arg); // вся реальная работа здесь
va_end(arg);
return done;
}
/*
printf — это обёртка, а всю работу делает vfprintf
внутренняя реализация vfprintf в glibc — больше 2400 строк
для функции, которую ты вызываешь в своей первой программе
*/{}
vfprintf — это место, где реально разбирается форматная строка, это конечный автомат
он проходит по форматной строке побайтно, ищет символ процента, когда находит — читает флаги, ширину, точность, модификатор длины и спецификатор преобразования, после чего прыгает в соответствующий обработчик
printf("%05.2f", 3.14) — это просто цикл с большим оператором switch, и он не пишет напрямую в терминал, а записывает в буфер, который принадлежит stdout
этот буфер сбрасывается в ОС через один системный вызов write, но только когда он заполняется, либо когда печатается перевод строки, либо при завершении программы
/* упрощено из внутреннего парсера vfprintf в C, основной цикл разбора */
while (*f != '\0') {
if (*f != '%') {
/* копирование литеральных символов в буфер */
f = strchrnul(f, '%'); // использует векторные инструкции (SIMD) для поиска следующего '%' или конца
} else {
/* разбор флагов, ширины, точности, модификаторов длины и преобразования */
switch (conv) {
case 'd': /* формат: знаковое целое */ break;
case 's': /* формат: строка */ break;
case 'f': /* формат: число с плавающей точкой */ break;
/* больше вариантов */
}
}
}
/* strchrnul использует векторные инструкции для сканирования нескольких байтов за раз */
/* даже сканирование форматной строки оптимизировано */{}
// поэтому printf без переноса строки иногда ничего не выводит
stdout использует построчную буферизацию при подключении к терминалу, поэтому отсутствие '\n' не вызывает сброс буфера и вывода
но если перенаправить stdout в файл, он становится полностью буферизованным, и ничего не выводится до завершения программы или вызова fflush
это десятилетиями сбивало с толку разработчиков, и причина всегда в режиме буферизации
printf("hello"); // может не появиться сразу
printf("hello\n"); // вызывает сброс и выводится сразу
fflush(stdout); // или явный сброс{}
функция, которую вызывают на первом занятии по C, запускает тысячи строк кода glibc и использует векторные инструкции до того, как хотя бы один байт попадёт в терминал
// минимальный TCP-сервер по гайду Beej
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(8080),
.sin_addr.s_addr = INADDR_ANY
};
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sockfd, 10);
int client = accept(sockfd, NULL, NULL);
send(client, "hello\n", 6, 0);
// nginx, redis и большинство серверов начинают с этого паттерна{}
Зацени. Оно бесплатное и таким останется всегда.
http://beej.us/guide/bgnet
attribute((constructor)) помечает функцию, которая выполняется на этапе загрузки
attribute((destructor)) выполняется после возврата из main()
так shared-библиотеки инициализируют себя при вызове dlopen()
__attribute__((constructor)) void before_main() {
printf("this runs before main\n");
}
int main() {
printf("this runs second\n");
return 0;
}{}
числовой приоритет — это то, что делает механизм реально управляемым.
меньшее число выполняется раньше, поэтому если две библиотеки регистрируют конструкторы, функция с приоритетом 101 отработает до 102.
glibc резервирует всё ниже 100 под себя
туда лучше не лезть
деструкторы работают в обратном порядке: большее число выполняется раньше при завершении, симметрия сделана специально
__attribute__((constructor(101))) void first() {
printf("runs first\n");
}
__attribute__((constructor(102))) void second() {
printf("runs second\n");
}
int main() {
printf("runs last\n");
return 0;
}{}
суть работы LD_PRELOAD как раз в этом механизме загрузки разделяемых библиотек.
ты подгружаешь .so в процесс, её конструкторы выполняются до входа в main(), и к моменту старта пользовательского кода таблица символов уже переопределена.
из-за этого можно перехватывать malloc, open, read и любые функции из libc, не модифицируя сам бинарник.
gcc -O2, который выводил:
“Fermat's Last Theorem has been disproved.”
Проблема была не в математике, а в undefined behavior.
Вот что на самом деле увидел gcc.
В цикле нет I/O и нет volatile. Стандарт C11 разрешает компилятору предполагать, что такой цикл может завершиться.
Поэтому gcc может считать, что while(1) не обязательно бесконечный. Но проблема была глубже — в undefined behavior.
Переполнение signed int в aaa — это UB. Компилятор исходит из того, что такого переполнения никогда не произойдёт.
С этим допущением он может трансформировать программу так, что логика поиска полностью ломается.
В результате gcc способен удалить или изменить части вычислений так, будто программа всегда находит решение.
Компилятор не ошибся в математике. Он просто следовал стандарту.
Ошибка была в коде, который полагался на undefined behavior.
// что gcc увидел при -O2:
// в цикле нет volatile, нет I/O, нет атомарных операций
// стандарт C11 разрешает считать, что цикл может завершиться
// следовательно: если fermat() завершится, то вернёт только 1
// следовательно: можно сразу вернуть 1
// что примерно сгенерировал компилятор:
int fermat() { return 1; } // не гарантируется, но допустимо при UB{}
Потом Рейгер решил проверить компилятор. Он добавил printf в конец программы, чтобы вывести найденный контрпример: a b c.
После этого gcc снова начал выполнять реальные вычисления, потому что теперь у программы появился наблюдаемый вывод.
Часть агрессивных оптимизаций исчезла.
Блеф перестал работать.
Позже Рейгер написал:
> “I got the feeling these tools like Fermat himself had not enough room in the margin to explain their reasoning.”
И проблема решается не простым добавлением volatile.
Настоящее решение — не допускать undefined behavior, например переполнения signed int.
volatile лишь запрещает часть оптимизаций, но не исправляет саму ошибку.
// Приём 1: добавление printf заставляет компилятор выполнить вычисления
printf("%d %d %d\n", a, b, c); // теперь компилятор должен их вычислить
// Приём 2: volatile ограничивает оптимизацию
volatile int a = 1, b = 1, c = 1; // чтение теперь — наблюдаемый побочный эффект
// Этот же баг часто встречается во встраиваемой прошивке
// Цикл ожидания без volatile удаляется при -O2
// Процессор пропускает ожидание, что приводит к нерабочему поведению{}
Отзывы канала
всего 2 отзыва
- Добавлен: Сначала новые
- Добавлен: Сначала старые
- Оценка: По убыванию
- Оценка: По возрастанию
Каталог Телеграм-каналов для нативных размещений
С/С++ Portal | Программирование — это Telegam канал в категории «Интернет технологии», который предлагает эффективные форматы для размещения рекламных постов в Телеграмме. Количество подписчиков канала в 15.5K и качественный контент помогают брендам привлекать внимание аудитории и увеличивать охват. Рейтинг канала составляет 13.3, количество отзывов – 2, со средней оценкой 5.0.
Вы можете запустить рекламную кампанию через сервис Telega.in, выбрав удобный формат размещения. Платформа обеспечивает прозрачные условия сотрудничества и предоставляет детальную аналитику. Стоимость размещения составляет 6293.7 ₽, а за 29 выполненных заявок канал зарекомендовал себя как надежный партнер для рекламы в TG. Размещайте интеграции уже сегодня и привлекайте новых клиентов вместе с Telega.in!
Вы снова сможете добавить каналы в корзину из каталога
Комментарий